payload-mcp-toolkit 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -237
- package/dist/api-keys.js +16 -0
- package/dist/api-keys.js.map +1 -1
- package/dist/auth-strategy.js +49 -49
- package/dist/auth-strategy.js.map +1 -1
- package/dist/components/AgentConnectPill.d.ts +3 -0
- package/dist/components/AgentConnectPill.js +121 -0
- package/dist/components/AgentConnectPill.js.map +1 -0
- package/dist/components/CollectionScopesMatrix.js.map +1 -1
- package/dist/components/ConsentPermissions.d.ts +10 -0
- package/dist/components/ConsentPermissions.js +165 -0
- package/dist/components/ConsentPermissions.js.map +1 -0
- package/dist/components/GlobalScopesMatrix.js.map +1 -1
- package/dist/components/OAuthView.d.ts +4 -0
- package/dist/components/OAuthView.js +269 -0
- package/dist/components/OAuthView.js.map +1 -0
- package/dist/components/ScopesTable.d.ts +28 -0
- package/dist/components/ScopesTable.js +165 -130
- package/dist/components/ScopesTable.js.map +1 -1
- package/dist/components/agentInstructions.d.ts +2 -0
- package/dist/components/agentInstructions.js +34 -0
- package/dist/components/agentInstructions.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/oauth.css +111 -0
- package/dist/conflict-detection.js +13 -13
- package/dist/conflict-detection.js.map +1 -1
- package/dist/draft-workflow.js +26 -26
- package/dist/draft-workflow.js.map +1 -1
- package/dist/endpoint.d.ts +5 -0
- package/dist/endpoint.js +21 -13
- package/dist/endpoint.js.map +1 -1
- package/dist/hash.js +15 -15
- package/dist/hash.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +67 -3
- package/dist/index.js.map +1 -1
- package/dist/introspection.js +47 -47
- package/dist/introspection.js.map +1 -1
- package/dist/oauth-permissions.d.ts +52 -0
- package/dist/oauth-permissions.js +100 -0
- package/dist/oauth-permissions.js.map +1 -0
- package/dist/oauth-store.d.ts +19 -0
- package/dist/oauth-store.js +122 -0
- package/dist/oauth-store.js.map +1 -0
- package/dist/oauth.d.ts +19 -0
- package/dist/oauth.js +523 -0
- package/dist/oauth.js.map +1 -0
- package/dist/prompts.js +5 -5
- package/dist/prompts.js.map +1 -1
- package/dist/registry.js +7 -1
- package/dist/registry.js.map +1 -1
- package/dist/resources.js +10 -10
- package/dist/resources.js.map +1 -1
- package/dist/scope/audit-log.js +9 -9
- package/dist/scope/audit-log.js.map +1 -1
- package/dist/scope/policy.js +7 -7
- package/dist/scope/policy.js.map +1 -1
- package/dist/tools/_helpers.d.ts +10 -0
- package/dist/tools/_helpers.js +53 -41
- package/dist/tools/_helpers.js.map +1 -1
- package/dist/tools/_layout-helpers.js +33 -33
- package/dist/tools/_layout-helpers.js.map +1 -1
- package/dist/tools/create-document.js +20 -19
- package/dist/tools/create-document.js.map +1 -1
- package/dist/tools/delete-document.js +10 -9
- package/dist/tools/delete-document.js.map +1 -1
- package/dist/tools/find-document.js +18 -15
- package/dist/tools/find-document.js.map +1 -1
- package/dist/tools/find-global.js +13 -13
- package/dist/tools/find-global.js.map +1 -1
- package/dist/tools/global-versions.js +8 -7
- package/dist/tools/global-versions.js.map +1 -1
- package/dist/tools/patch-global-layout.js +9 -9
- package/dist/tools/patch-global-layout.js.map +1 -1
- package/dist/tools/patch-layout.js +15 -14
- package/dist/tools/patch-layout.js.map +1 -1
- package/dist/tools/publish-draft.js +2 -1
- package/dist/tools/publish-draft.js.map +1 -1
- package/dist/tools/publish-global-draft.js +4 -4
- package/dist/tools/publish-global-draft.js.map +1 -1
- package/dist/tools/resolve-reference.js.map +1 -1
- package/dist/tools/safe-delete.js +14 -14
- package/dist/tools/safe-delete.js.map +1 -1
- package/dist/tools/schedule-publish.js +21 -20
- package/dist/tools/schedule-publish.js.map +1 -1
- package/dist/tools/search-content.js +12 -12
- package/dist/tools/search-content.js.map +1 -1
- package/dist/tools/update-document.js +6 -5
- package/dist/tools/update-document.js.map +1 -1
- package/dist/tools/update-global.js +9 -9
- package/dist/tools/update-global.js.map +1 -1
- package/dist/tools/upload-media.js +2 -1
- package/dist/tools/upload-media.js.map +1 -1
- package/dist/tools/versions.js +10 -8
- package/dist/tools/versions.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.js.map +1 -1
- package/docs/oauth.md +96 -0
- package/package.json +7 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ResourceKind } from './scope/policy';
|
|
2
|
+
import type { CollectionAction, GlobalAction, KeyScopes } from './types';
|
|
3
|
+
/** OAuth grants top out at editor: delete and global writes need an API key. */
|
|
4
|
+
export type Level = 'read-only' | 'editor';
|
|
5
|
+
export interface CatalogTool {
|
|
6
|
+
name: string;
|
|
7
|
+
kind: ResourceKind;
|
|
8
|
+
action: CollectionAction;
|
|
9
|
+
}
|
|
10
|
+
/** What the consent screen can offer: exposed collections, globals and registered tools. */
|
|
11
|
+
export interface Catalog {
|
|
12
|
+
collections: string[];
|
|
13
|
+
globals: string[];
|
|
14
|
+
tools: CatalogTool[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* What the consent form submits. An omitted list means "all of them" (the form sends only the lists
|
|
18
|
+
* the user changed); anything outside the catalog or the level is dropped.
|
|
19
|
+
*/
|
|
20
|
+
export interface Choice {
|
|
21
|
+
level?: Level;
|
|
22
|
+
collections?: Record<string, string[]>;
|
|
23
|
+
globals?: Record<string, string[]>;
|
|
24
|
+
tools?: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare const collectionActions: (level: Level) => CollectionAction[];
|
|
27
|
+
export declare const globalActions: (level: Level) => GlobalAction[];
|
|
28
|
+
/** Tools a grant at this level could ever run. The rest are hidden, not offered. */
|
|
29
|
+
export declare function toolsFor(catalog: Catalog, level: Level): CatalogTool[];
|
|
30
|
+
/** Full access at a level, in the shape the consent form submits. The consent screen's default. */
|
|
31
|
+
export declare function fullChoice(catalog: Catalog, level: Level): Required<Choice>;
|
|
32
|
+
/** Turns a submitted choice into the scopes stored on the grant, capped at `max`. */
|
|
33
|
+
export declare function permissionsFrom(choice: Choice, catalog: Catalog, max: Level): {
|
|
34
|
+
level: Level;
|
|
35
|
+
permissions: KeyScopes;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Re-caps a grant's stored scopes on every request. The site's access level or the token's scope may have
|
|
39
|
+
* shrunk since consent, and the scope checker honours a collection/global list over the preset, so the
|
|
40
|
+
* lists themselves must lose the actions the current level no longer allows.
|
|
41
|
+
*/
|
|
42
|
+
export declare function clampScopes(stored: KeyScopes, max: Level): KeyScopes;
|
|
43
|
+
/** One line for the connections page, e.g. "Read, create and update · 3 of 12 collections · all tools". */
|
|
44
|
+
type Lists = {
|
|
45
|
+
collections?: Record<string, unknown[]>;
|
|
46
|
+
globals?: Record<string, unknown[]>;
|
|
47
|
+
tools?: {
|
|
48
|
+
allow?: string[];
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export declare function summarize(permissions: Lists | undefined, level: Level, catalog: Catalog): string;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Pure helpers shared by the OAuth server (src/oauth.ts) and the consent screen
|
|
2
|
+
// (src/components/OAuthView.tsx). No Node or Payload runtime imports: the admin bundle loads this file.
|
|
3
|
+
import { PRESET_ACTIONS, PRESET_GLOBAL_ACTIONS, PRESET_TOOL_DENY } from './scope/policy';
|
|
4
|
+
export const collectionActions = (level)=>PRESET_ACTIONS[level];
|
|
5
|
+
export const globalActions = (level)=>PRESET_GLOBAL_ACTIONS[level];
|
|
6
|
+
/** Tools a grant at this level could ever run. The rest are hidden, not offered. */ export function toolsFor(catalog, level) {
|
|
7
|
+
return catalog.tools.filter((t)=>!PRESET_TOOL_DENY[level].includes(t.name) && (t.kind === 'global' ? globalActions(level) : collectionActions(level)).includes(t.action));
|
|
8
|
+
}
|
|
9
|
+
/** Full access at a level, in the shape the consent form submits. The consent screen's default. */ export function fullChoice(catalog, level) {
|
|
10
|
+
return {
|
|
11
|
+
level,
|
|
12
|
+
collections: Object.fromEntries(catalog.collections.map((slug)=>[
|
|
13
|
+
slug,
|
|
14
|
+
collectionActions(level)
|
|
15
|
+
])),
|
|
16
|
+
globals: Object.fromEntries(catalog.globals.map((slug)=>[
|
|
17
|
+
slug,
|
|
18
|
+
globalActions(level)
|
|
19
|
+
])),
|
|
20
|
+
tools: toolsFor(catalog, level).map((t)=>t.name)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// A whitelist equal to "everything" is left out, so account-wide tools (search, upload) keep working
|
|
24
|
+
// and collections added later are included. The scope checker denies account tools once a whitelist is set.
|
|
25
|
+
function pick(slugs, chosen, allowed) {
|
|
26
|
+
if (chosen === undefined) return undefined;
|
|
27
|
+
const map = {};
|
|
28
|
+
let full = true;
|
|
29
|
+
for (const slug of slugs){
|
|
30
|
+
const picked = chosen && typeof chosen === 'object' && Object.hasOwn(chosen, slug) && Array.isArray(chosen[slug]) ? chosen[slug] : [];
|
|
31
|
+
const actions = allowed.filter((a)=>picked.includes(a));
|
|
32
|
+
if (actions.length) map[slug] = actions;
|
|
33
|
+
if (actions.length !== allowed.length) full = false;
|
|
34
|
+
}
|
|
35
|
+
return full ? undefined : map;
|
|
36
|
+
}
|
|
37
|
+
/** Turns a submitted choice into the scopes stored on the grant, capped at `max`. */ export function permissionsFrom(choice, catalog, max) {
|
|
38
|
+
const level = choice.level === 'editor' && max === 'editor' ? 'editor' : 'read-only';
|
|
39
|
+
const offered = toolsFor(catalog, level).map((t)=>t.name);
|
|
40
|
+
const tools = choice.tools === undefined ? offered : Array.isArray(choice.tools) ? offered.filter((name)=>choice.tools.includes(name)) : [];
|
|
41
|
+
const collections = pick(catalog.collections, choice.collections, collectionActions(level));
|
|
42
|
+
const globals = pick(catalog.globals, choice.globals, globalActions(level));
|
|
43
|
+
return {
|
|
44
|
+
level,
|
|
45
|
+
permissions: {
|
|
46
|
+
preset: level,
|
|
47
|
+
...collections && {
|
|
48
|
+
collections
|
|
49
|
+
},
|
|
50
|
+
...globals && {
|
|
51
|
+
globals
|
|
52
|
+
},
|
|
53
|
+
...tools.length !== offered.length && {
|
|
54
|
+
tools: {
|
|
55
|
+
allow: tools
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Re-caps a grant's stored scopes on every request. The site's access level or the token's scope may have
|
|
63
|
+
* shrunk since consent, and the scope checker honours a collection/global list over the preset, so the
|
|
64
|
+
* lists themselves must lose the actions the current level no longer allows.
|
|
65
|
+
*/ export function clampScopes(stored, max) {
|
|
66
|
+
const level = stored.preset === 'editor' && max === 'editor' ? 'editor' : 'read-only';
|
|
67
|
+
const within = (map, allowed)=>Object.fromEntries(Object.entries(map).map(([slug, actions])=>[
|
|
68
|
+
slug,
|
|
69
|
+
actions.filter((a)=>allowed.includes(a))
|
|
70
|
+
]));
|
|
71
|
+
return {
|
|
72
|
+
preset: level,
|
|
73
|
+
...stored.collections && {
|
|
74
|
+
collections: within(stored.collections, collectionActions(level))
|
|
75
|
+
},
|
|
76
|
+
...stored.globals && {
|
|
77
|
+
globals: within(stored.globals, globalActions(level))
|
|
78
|
+
},
|
|
79
|
+
...stored.tools && {
|
|
80
|
+
tools: stored.tools
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function summarize(permissions, level, catalog) {
|
|
85
|
+
const count = (map, total, noun)=>!map ? `all ${noun}` : `${Object.keys(map).length} of ${total} ${noun}`;
|
|
86
|
+
// Counts tools that will actually run: limited collections or globals switch account-wide tools off.
|
|
87
|
+
const offered = toolsFor(catalog, level);
|
|
88
|
+
const limited = Boolean(permissions?.collections || permissions?.globals);
|
|
89
|
+
const usable = offered.filter((t)=>(permissions?.tools?.allow?.includes(t.name) ?? true) && !(limited && t.kind === 'account'));
|
|
90
|
+
return [
|
|
91
|
+
level === 'editor' ? 'Read, create and update' : 'Read only',
|
|
92
|
+
count(permissions?.collections, catalog.collections.length, 'collections'),
|
|
93
|
+
...catalog.globals.length ? [
|
|
94
|
+
count(permissions?.globals, catalog.globals.length, 'globals')
|
|
95
|
+
] : [],
|
|
96
|
+
usable.length === offered.length ? 'all tools' : `${usable.length} of ${offered.length} tools`
|
|
97
|
+
].join(' · ');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//# sourceMappingURL=oauth-permissions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/oauth-permissions.ts"],"sourcesContent":["// Pure helpers shared by the OAuth server (src/oauth.ts) and the consent screen\n// (src/components/OAuthView.tsx). No Node or Payload runtime imports: the admin bundle loads this file.\nimport { PRESET_ACTIONS, PRESET_GLOBAL_ACTIONS, PRESET_TOOL_DENY, type ResourceKind } from './scope/policy'\nimport type { CollectionAction, GlobalAction, KeyScopes } from './types'\n\n/** OAuth grants top out at editor: delete and global writes need an API key. */\nexport type Level = 'read-only' | 'editor'\n\nexport interface CatalogTool { name: string; kind: ResourceKind; action: CollectionAction }\n/** What the consent screen can offer: exposed collections, globals and registered tools. */\nexport interface Catalog { collections: string[]; globals: string[]; tools: CatalogTool[] }\n\n/**\n * What the consent form submits. An omitted list means \"all of them\" (the form sends only the lists\n * the user changed); anything outside the catalog or the level is dropped.\n */\nexport interface Choice {\n level?: Level\n collections?: Record<string, string[]>\n globals?: Record<string, string[]>\n tools?: string[]\n}\n\nexport const collectionActions = (level: Level): CollectionAction[] => PRESET_ACTIONS[level]\nexport const globalActions = (level: Level): GlobalAction[] => PRESET_GLOBAL_ACTIONS[level]\n\n/** Tools a grant at this level could ever run. The rest are hidden, not offered. */\nexport function toolsFor(catalog: Catalog, level: Level): CatalogTool[] {\n return catalog.tools.filter(t => !PRESET_TOOL_DENY[level].includes(t.name) &&\n (t.kind === 'global' ? globalActions(level) : collectionActions(level)).includes(t.action as never))\n}\n\n/** Full access at a level, in the shape the consent form submits. The consent screen's default. */\nexport function fullChoice(catalog: Catalog, level: Level): Required<Choice> {\n return {\n level,\n collections: Object.fromEntries(catalog.collections.map(slug => [slug, collectionActions(level)])),\n globals: Object.fromEntries(catalog.globals.map(slug => [slug, globalActions(level)])),\n tools: toolsFor(catalog, level).map(t => t.name),\n }\n}\n\n// A whitelist equal to \"everything\" is left out, so account-wide tools (search, upload) keep working\n// and collections added later are included. The scope checker denies account tools once a whitelist is set.\nfunction pick<A extends string>(slugs: string[], chosen: Record<string, string[]> | undefined, allowed: A[]): Record<string, A[]> | undefined {\n if (chosen === undefined) return undefined\n const map: Record<string, A[]> = {}\n let full = true\n for (const slug of slugs) {\n const picked = chosen && typeof chosen === 'object' && Object.hasOwn(chosen, slug) && Array.isArray(chosen[slug]) ? chosen[slug] : []\n const actions = allowed.filter(a => picked.includes(a))\n if (actions.length) map[slug] = actions\n if (actions.length !== allowed.length) full = false\n }\n return full ? undefined : map\n}\n\n/** Turns a submitted choice into the scopes stored on the grant, capped at `max`. */\nexport function permissionsFrom(choice: Choice, catalog: Catalog, max: Level): { level: Level; permissions: KeyScopes } {\n const level: Level = choice.level === 'editor' && max === 'editor' ? 'editor' : 'read-only'\n const offered = toolsFor(catalog, level).map(t => t.name)\n const tools = choice.tools === undefined ? offered : Array.isArray(choice.tools) ? offered.filter(name => choice.tools!.includes(name)) : []\n const collections = pick(catalog.collections, choice.collections, collectionActions(level))\n const globals = pick(catalog.globals, choice.globals, globalActions(level))\n return { level, permissions: {\n preset: level,\n ...(collections && { collections }),\n ...(globals && { globals }),\n ...(tools.length !== offered.length && { tools: { allow: tools } }),\n } }\n}\n\n/**\n * Re-caps a grant's stored scopes on every request. The site's access level or the token's scope may have\n * shrunk since consent, and the scope checker honours a collection/global list over the preset, so the\n * lists themselves must lose the actions the current level no longer allows.\n */\nexport function clampScopes(stored: KeyScopes, max: Level): KeyScopes {\n const level: Level = stored.preset === 'editor' && max === 'editor' ? 'editor' : 'read-only'\n const within = <A extends string>(map: Record<string, A[]>, allowed: A[]) =>\n Object.fromEntries(Object.entries(map).map(([slug, actions]) => [slug, actions.filter(a => allowed.includes(a))]))\n return {\n preset: level,\n ...(stored.collections && { collections: within(stored.collections, collectionActions(level)) }),\n ...(stored.globals && { globals: within(stored.globals, globalActions(level)) }),\n ...(stored.tools && { tools: stored.tools }),\n }\n}\n\n/** One line for the connections page, e.g. \"Read, create and update · 3 of 12 collections · all tools\". */\ntype Lists = { collections?: Record<string, unknown[]>; globals?: Record<string, unknown[]>; tools?: { allow?: string[] } }\nexport function summarize(permissions: Lists | undefined, level: Level, catalog: Catalog): string {\n const count = (map: Record<string, unknown[]> | undefined, total: number, noun: string) =>\n !map ? `all ${noun}` : `${Object.keys(map).length} of ${total} ${noun}`\n // Counts tools that will actually run: limited collections or globals switch account-wide tools off.\n const offered = toolsFor(catalog, level)\n const limited = Boolean(permissions?.collections || permissions?.globals)\n const usable = offered.filter(t => (permissions?.tools?.allow?.includes(t.name) ?? true) && !(limited && t.kind === 'account'))\n return [\n level === 'editor' ? 'Read, create and update' : 'Read only',\n count(permissions?.collections, catalog.collections.length, 'collections'),\n ...(catalog.globals.length ? [count(permissions?.globals, catalog.globals.length, 'globals')] : []),\n usable.length === offered.length ? 'all tools' : `${usable.length} of ${offered.length} tools`,\n ].join(' · ')\n}\n"],"names":["PRESET_ACTIONS","PRESET_GLOBAL_ACTIONS","PRESET_TOOL_DENY","collectionActions","level","globalActions","toolsFor","catalog","tools","filter","t","includes","name","kind","action","fullChoice","collections","Object","fromEntries","map","slug","globals","pick","slugs","chosen","allowed","undefined","full","picked","hasOwn","Array","isArray","actions","a","length","permissionsFrom","choice","max","offered","permissions","preset","allow","clampScopes","stored","within","entries","summarize","count","total","noun","keys","limited","Boolean","usable","join"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CollectionConfig, Payload } from 'payload';
|
|
2
|
+
export declare const OAUTH_COLLECTION = "payload-mcp-oauth";
|
|
3
|
+
export interface OAuthRecord {
|
|
4
|
+
id: string | number;
|
|
5
|
+
key: string;
|
|
6
|
+
kind: 'grant' | 'code' | 'access' | 'refresh' | 'used';
|
|
7
|
+
expiresAt: string;
|
|
8
|
+
owner?: string;
|
|
9
|
+
data: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export declare function createOAuthCollection(): CollectionConfig;
|
|
12
|
+
export declare function tokenKey(payload: Payload, token: string): string;
|
|
13
|
+
export declare function readRecord(payload: Payload, key: string): Promise<OAuthRecord | undefined>;
|
|
14
|
+
export declare function writeRecord(payload: Payload, record: Omit<OAuthRecord, 'id'>): Promise<void>;
|
|
15
|
+
export declare function isLive(record: OAuthRecord | undefined): record is OAuthRecord;
|
|
16
|
+
export declare function consumeRecord(payload: Payload, record: OAuthRecord): Promise<boolean>;
|
|
17
|
+
export declare function revokeGrant(payload: Payload, grant: OAuthRecord): Promise<void>;
|
|
18
|
+
/** Run periodically in the host's existing scheduler. Never remove unexpired consumption claims. */
|
|
19
|
+
export declare function pruneOAuthRecords(payload: Payload): Promise<void>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { hashKey } from './hash';
|
|
2
|
+
export const OAUTH_COLLECTION = 'payload-mcp-oauth';
|
|
3
|
+
export function createOAuthCollection() {
|
|
4
|
+
return {
|
|
5
|
+
slug: OAUTH_COLLECTION,
|
|
6
|
+
admin: {
|
|
7
|
+
hidden: true
|
|
8
|
+
},
|
|
9
|
+
access: {
|
|
10
|
+
create: ()=>false,
|
|
11
|
+
read: ()=>false,
|
|
12
|
+
update: ()=>false,
|
|
13
|
+
delete: ()=>false
|
|
14
|
+
},
|
|
15
|
+
fields: [
|
|
16
|
+
{
|
|
17
|
+
name: 'key',
|
|
18
|
+
type: 'text',
|
|
19
|
+
required: true,
|
|
20
|
+
unique: true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'kind',
|
|
24
|
+
type: 'select',
|
|
25
|
+
required: true,
|
|
26
|
+
options: [
|
|
27
|
+
'grant',
|
|
28
|
+
'code',
|
|
29
|
+
'access',
|
|
30
|
+
'refresh',
|
|
31
|
+
'used'
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'expiresAt',
|
|
36
|
+
type: 'date',
|
|
37
|
+
required: true,
|
|
38
|
+
index: true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'owner',
|
|
42
|
+
type: 'text',
|
|
43
|
+
index: true
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'data',
|
|
47
|
+
type: 'json',
|
|
48
|
+
required: true
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function tokenKey(payload, token) {
|
|
54
|
+
return hashKey(`oauth:${token}`, payload.secret);
|
|
55
|
+
}
|
|
56
|
+
export async function readRecord(payload, key) {
|
|
57
|
+
const result = await payload.find({
|
|
58
|
+
collection: OAUTH_COLLECTION,
|
|
59
|
+
where: {
|
|
60
|
+
key: {
|
|
61
|
+
equals: key
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
limit: 1,
|
|
65
|
+
depth: 0,
|
|
66
|
+
pagination: false,
|
|
67
|
+
overrideAccess: true
|
|
68
|
+
});
|
|
69
|
+
return result.docs[0];
|
|
70
|
+
}
|
|
71
|
+
export async function writeRecord(payload, record) {
|
|
72
|
+
await payload.create({
|
|
73
|
+
collection: OAUTH_COLLECTION,
|
|
74
|
+
data: record,
|
|
75
|
+
overrideAccess: true
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export function isLive(record) {
|
|
79
|
+
return !!record && Date.parse(record.expiresAt) > Date.now();
|
|
80
|
+
}
|
|
81
|
+
export async function consumeRecord(payload, record) {
|
|
82
|
+
const key = `used:${record.key}`;
|
|
83
|
+
// A unique INSERT elects one winner across processes. Payload's update(where) is not a CAS.
|
|
84
|
+
try {
|
|
85
|
+
await writeRecord(payload, {
|
|
86
|
+
key,
|
|
87
|
+
kind: 'used',
|
|
88
|
+
expiresAt: record.expiresAt,
|
|
89
|
+
data: {}
|
|
90
|
+
});
|
|
91
|
+
return true;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const validation = error;
|
|
94
|
+
// Payload normalizes adapter uniqueness errors to a field ValidationError.
|
|
95
|
+
if (validation?.name !== 'ValidationError' || validation.data?.collection !== OAUTH_COLLECTION || validation.data.errors?.length !== 1 || validation.data.errors[0].path !== 'key') throw error;
|
|
96
|
+
if (await readRecord(payload, key)) return false;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export async function revokeGrant(payload, grant) {
|
|
101
|
+
await payload.update({
|
|
102
|
+
collection: OAUTH_COLLECTION,
|
|
103
|
+
id: grant.id,
|
|
104
|
+
data: {
|
|
105
|
+
expiresAt: new Date(0).toISOString()
|
|
106
|
+
},
|
|
107
|
+
overrideAccess: true
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** Run periodically in the host's existing scheduler. Never remove unexpired consumption claims. */ export async function pruneOAuthRecords(payload) {
|
|
111
|
+
await payload.delete({
|
|
112
|
+
collection: OAUTH_COLLECTION,
|
|
113
|
+
where: {
|
|
114
|
+
expiresAt: {
|
|
115
|
+
less_than: new Date().toISOString()
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
overrideAccess: true
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
//# sourceMappingURL=oauth-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/oauth-store.ts"],"sourcesContent":["import type { CollectionConfig, Payload } from 'payload'\nimport { hashKey } from './hash'\n\nexport const OAUTH_COLLECTION = 'payload-mcp-oauth'\n\nexport interface OAuthRecord {\n id: string | number\n key: string\n kind: 'grant' | 'code' | 'access' | 'refresh' | 'used'\n expiresAt: string\n owner?: string\n data: Record<string, unknown>\n}\n\nexport function createOAuthCollection(): CollectionConfig {\n return {\n slug: OAUTH_COLLECTION,\n admin: { hidden: true },\n access: { create: () => false, read: () => false, update: () => false, delete: () => false },\n fields: [\n { name: 'key', type: 'text', required: true, unique: true },\n { name: 'kind', type: 'select', required: true, options: ['grant', 'code', 'access', 'refresh', 'used'] },\n { name: 'expiresAt', type: 'date', required: true, index: true },\n { name: 'owner', type: 'text', index: true },\n { name: 'data', type: 'json', required: true },\n ],\n }\n}\n\nexport function tokenKey(payload: Payload, token: string): string {\n return hashKey(`oauth:${token}`, payload.secret)\n}\n\nexport async function readRecord(payload: Payload, key: string): Promise<OAuthRecord | undefined> {\n const result = await payload.find({\n collection: OAUTH_COLLECTION, where: { key: { equals: key } },\n limit: 1, depth: 0, pagination: false, overrideAccess: true,\n })\n return result.docs[0] as unknown as OAuthRecord | undefined\n}\n\nexport async function writeRecord(payload: Payload, record: Omit<OAuthRecord, 'id'>): Promise<void> {\n await payload.create({ collection: OAUTH_COLLECTION, data: record, overrideAccess: true })\n}\n\nexport function isLive(record: OAuthRecord | undefined): record is OAuthRecord {\n return !!record && Date.parse(record.expiresAt) > Date.now()\n}\n\nexport async function consumeRecord(payload: Payload, record: OAuthRecord): Promise<boolean> {\n const key = `used:${record.key}`\n // A unique INSERT elects one winner across processes. Payload's update(where) is not a CAS.\n try {\n await writeRecord(payload, { key, kind: 'used', expiresAt: record.expiresAt, data: {} })\n return true\n } catch (error) {\n const validation = error as { name?: string; data?: { collection?: string; errors?: { path?: string }[] } }\n // Payload normalizes adapter uniqueness errors to a field ValidationError.\n if (validation?.name !== 'ValidationError' || validation.data?.collection !== OAUTH_COLLECTION ||\n validation.data.errors?.length !== 1 || validation.data.errors[0].path !== 'key') throw error\n if (await readRecord(payload, key)) return false\n throw error\n }\n}\n\nexport async function revokeGrant(payload: Payload, grant: OAuthRecord): Promise<void> {\n await payload.update({ collection: OAUTH_COLLECTION, id: grant.id,\n data: { expiresAt: new Date(0).toISOString() }, overrideAccess: true })\n}\n\n/** Run periodically in the host's existing scheduler. Never remove unexpired consumption claims. */\nexport async function pruneOAuthRecords(payload: Payload): Promise<void> {\n await payload.delete({ collection: OAUTH_COLLECTION,\n where: { expiresAt: { less_than: new Date().toISOString() } }, overrideAccess: true })\n}\n"],"names":["hashKey","OAUTH_COLLECTION","createOAuthCollection","slug","admin","hidden","access","create","read","update","delete","fields","name","type","required","unique","options","index","tokenKey","payload","token","secret","readRecord","key","result","find","collection","where","equals","limit","depth","pagination","overrideAccess","docs","writeRecord","record","data","isLive","Date","parse","expiresAt","now","consumeRecord","kind","error","validation","errors","length","path","revokeGrant","grant","id","toISOString","pruneOAuthRecords","less_than"],"mappings":"AACA,SAASA,OAAO,QAAQ,SAAQ;AAEhC,OAAO,MAAMC,mBAAmB,oBAAmB;AAWnD,OAAO,SAASC;IACd,OAAO;QACLC,MAAMF;QACNG,OAAO;YAAEC,QAAQ;QAAK;QACtBC,QAAQ;YAAEC,QAAQ,IAAM;YAAOC,MAAM,IAAM;YAAOC,QAAQ,IAAM;YAAOC,QAAQ,IAAM;QAAM;QAC3FC,QAAQ;YACN;gBAAEC,MAAM;gBAAOC,MAAM;gBAAQC,UAAU;gBAAMC,QAAQ;YAAK;YAC1D;gBAAEH,MAAM;gBAAQC,MAAM;gBAAUC,UAAU;gBAAME,SAAS;oBAAC;oBAAS;oBAAQ;oBAAU;oBAAW;iBAAO;YAAC;YACxG;gBAAEJ,MAAM;gBAAaC,MAAM;gBAAQC,UAAU;gBAAMG,OAAO;YAAK;YAC/D;gBAAEL,MAAM;gBAASC,MAAM;gBAAQI,OAAO;YAAK;YAC3C;gBAAEL,MAAM;gBAAQC,MAAM;gBAAQC,UAAU;YAAK;SAC9C;IACH;AACF;AAEA,OAAO,SAASI,SAASC,OAAgB,EAAEC,KAAa;IACtD,OAAOpB,QAAQ,CAAC,MAAM,EAAEoB,OAAO,EAAED,QAAQE,MAAM;AACjD;AAEA,OAAO,eAAeC,WAAWH,OAAgB,EAAEI,GAAW;IAC5D,MAAMC,SAAS,MAAML,QAAQM,IAAI,CAAC;QAChCC,YAAYzB;QAAkB0B,OAAO;YAAEJ,KAAK;gBAAEK,QAAQL;YAAI;QAAE;QAC5DM,OAAO;QAAGC,OAAO;QAAGC,YAAY;QAAOC,gBAAgB;IACzD;IACA,OAAOR,OAAOS,IAAI,CAAC,EAAE;AACvB;AAEA,OAAO,eAAeC,YAAYf,OAAgB,EAAEgB,MAA+B;IACjF,MAAMhB,QAAQZ,MAAM,CAAC;QAAEmB,YAAYzB;QAAkBmC,MAAMD;QAAQH,gBAAgB;IAAK;AAC1F;AAEA,OAAO,SAASK,OAAOF,MAA+B;IACpD,OAAO,CAAC,CAACA,UAAUG,KAAKC,KAAK,CAACJ,OAAOK,SAAS,IAAIF,KAAKG,GAAG;AAC5D;AAEA,OAAO,eAAeC,cAAcvB,OAAgB,EAAEgB,MAAmB;IACvE,MAAMZ,MAAM,CAAC,KAAK,EAAEY,OAAOZ,GAAG,EAAE;IAChC,4FAA4F;IAC5F,IAAI;QACF,MAAMW,YAAYf,SAAS;YAAEI;YAAKoB,MAAM;YAAQH,WAAWL,OAAOK,SAAS;YAAEJ,MAAM,CAAC;QAAE;QACtF,OAAO;IACT,EAAE,OAAOQ,OAAO;QACd,MAAMC,aAAaD;QACnB,2EAA2E;QAC3E,IAAIC,YAAYjC,SAAS,qBAAqBiC,WAAWT,IAAI,EAAEV,eAAezB,oBAC5E4C,WAAWT,IAAI,CAACU,MAAM,EAAEC,WAAW,KAAKF,WAAWT,IAAI,CAACU,MAAM,CAAC,EAAE,CAACE,IAAI,KAAK,OAAO,MAAMJ;QAC1F,IAAI,MAAMtB,WAAWH,SAASI,MAAM,OAAO;QAC3C,MAAMqB;IACR;AACF;AAEA,OAAO,eAAeK,YAAY9B,OAAgB,EAAE+B,KAAkB;IACpE,MAAM/B,QAAQV,MAAM,CAAC;QAAEiB,YAAYzB;QAAkBkD,IAAID,MAAMC,EAAE;QAC/Df,MAAM;YAAEI,WAAW,IAAIF,KAAK,GAAGc,WAAW;QAAG;QAAGpB,gBAAgB;IAAK;AACzE;AAEA,kGAAkG,GAClG,OAAO,eAAeqB,kBAAkBlC,OAAgB;IACtD,MAAMA,QAAQT,MAAM,CAAC;QAAEgB,YAAYzB;QACjC0B,OAAO;YAAEa,WAAW;gBAAEc,WAAW,IAAIhB,OAAOc,WAAW;YAAG;QAAE;QAAGpB,gBAAgB;IAAK;AACxF"}
|
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Config, Endpoint, PayloadRequest } from 'payload';
|
|
2
|
+
import { type Catalog } from './oauth-permissions';
|
|
3
|
+
export interface OAuthOptions {
|
|
4
|
+
/** Required site policy. Rechecked at consent, refresh and every MCP request. */
|
|
5
|
+
canAuthorize: (req: PayloadRequest) => boolean | Promise<boolean>;
|
|
6
|
+
/** Defaults to Payload admin login. Return a same-origin login URL that returns to returnTo. */
|
|
7
|
+
loginURL?: (returnTo: string) => string;
|
|
8
|
+
/** Exact trusted callbacks; defaults to the Claude (web/Desktop) and ChatGPT remote connector callbacks. */
|
|
9
|
+
redirectURIs?: string[];
|
|
10
|
+
/** Read-only by default. Editor enables create/update, still subject to Payload access controls. */
|
|
11
|
+
access?: 'read-only' | 'editor';
|
|
12
|
+
}
|
|
13
|
+
/** `catalog` is what the consent screen offers: exposed collections, globals and registered tools. */
|
|
14
|
+
export declare function createOAuth(options: OAuthOptions, config: Config, userCollection: string, catalog: Catalog): {
|
|
15
|
+
endpoints: Endpoint[];
|
|
16
|
+
authenticate: (req: PayloadRequest) => Promise<boolean>;
|
|
17
|
+
metadataURL: string;
|
|
18
|
+
scope: string;
|
|
19
|
+
};
|