Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/routes/source.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ export async function deleteSource({ req, env, daCtx }) {

export async function postSource({ req, env, daCtx }) {
if (!hasPermission(daCtx, daCtx.key, 'write')) return { status: 403 };

// Flag MCP-initiated writes so the version author is tagged as an agent.
// Done after the permission check, which also reads daCtx.users[].email.
const initiator = req.headers.get('x-da-initiator');
if (initiator === 'mcp' && Array.isArray(daCtx.users)) {
daCtx.users = daCtx.users.map((user) => ({ ...user, isAgentic: true }));
}

const obj = await putHelper(req, env, daCtx);
const resp = await putObject(env, daCtx, obj);

if (resp.status === 201 || resp.status === 200) {
const initiator = req.headers.get('x-da-initiator');
if (initiator !== 'collab') {
await notifyCollab('syncadmin', req.url, env);
}
Expand Down
5 changes: 4 additions & 1 deletion src/storage/utils/version.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,8 @@ export function getUsersForMetadata(users) {
return undefined;
}

return users.map((user) => ({ email: user.email }));
return users.map((user) => ({
email: user.email,
...(user.isAgentic && { isAgentic: true }),
}));
}
47 changes: 47 additions & 0 deletions test/routes/source.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,53 @@ describe('Source Route', () => {
assert.deepStrictEqual(['https://localhost/api/v1/syncadmin?doc=http://localhost:9876/source/somedoc.html'], sb_callbacks);
});

it('Test postSource from mcp flags users as agent and still notifies collab', async () => {
const putCalled = [];
const putCall = (e, c, o) => {
putCalled.push({ e, c, o });
return { status: 200 };
};

const { postSource } = await esmock('../../src/routes/source.js', {
'../../src/storage/object/put.js': {
default: putCall,
},
'../../src/utils/auth.js': {
hasPermission: () => true,
},
});

const callbacks = [];
const env = {
dacollab: {
fetch: async (url) => {
callbacks.push(url);
return { body: { cancel: () => {} } };
},
},
DA_COLLAB: 'http://localhost:1234',
};

const headers = new Map();
headers.set('x-da-initiator', 'mcp');

const req = { headers, url: 'http://localhost:8787/source/a/b/mydoc.html' };
const daCtx = {
key: '/a/b/mydoc.html',
aclCtx: { pathLookup: new Map() },
users: [{ email: 'jane@example.com', ident: '123' }],
};

const resp = await postSource({ req, env, daCtx });
assert.equal(200, resp.status);
// users passed to putObject are flagged so the version author is an agent
assert.deepStrictEqual(
putCalled[0].c.users,
[{ email: 'jane@example.com', ident: '123', isAgentic: true }],
);
assert.equal(1, callbacks.length);
});

it('Test postSource from collab does not trigger invalidate callback', async () => {
const { postSource } = await esmock('../../src/routes/source.js', {
'../../src/storage/object/put.js': {
Expand Down
44 changes: 44 additions & 0 deletions test/storage/utils/version.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import assert from 'node:assert';

import { getUsersForMetadata } from '../../../src/storage/utils/version.js';

describe('getUsersForMetadata', () => {
it('returns undefined when users is missing', () => {
assert.equal(getUsersForMetadata(undefined), undefined);
});

it('projects to email only, dropping ident/orgs', () => {
const users = [{ email: 'jane@example.com', ident: '123', orgs: [{}] }];
assert.deepStrictEqual(getUsersForMetadata(users), [{ email: 'jane@example.com' }]);
});

it('keeps the email clean and adds isAgentic when the user is an agent', () => {
const users = [{ email: 'jane@example.com', ident: '123', isAgentic: true }];
assert.deepStrictEqual(
getUsersForMetadata(users),
[{ email: 'jane@example.com', isAgentic: true }],
);
});

it('flags only the agent users in a mixed list', () => {
const users = [
{ email: 'jane@example.com', isAgentic: true },
{ email: 'bob@example.com' },
];
assert.deepStrictEqual(getUsersForMetadata(users), [
{ email: 'jane@example.com', isAgentic: true },
{ email: 'bob@example.com' },
]);
});
});
Loading