@forumone/throughline-approvals 0.0.1 → 0.2.1

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -43
  4. package/dist/collection.d.ts +18 -0
  5. package/dist/collection.d.ts.map +1 -0
  6. package/dist/collection.js +95 -0
  7. package/dist/collection.js.map +1 -0
  8. package/dist/endpoints/action.d.ts +31 -0
  9. package/dist/endpoints/action.d.ts.map +1 -0
  10. package/dist/endpoints/action.js +205 -0
  11. package/dist/endpoints/action.js.map +1 -0
  12. package/dist/index.d.ts +11 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +6 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/options.d.ts +50 -0
  17. package/dist/options.d.ts.map +1 -0
  18. package/dist/options.js +28 -0
  19. package/dist/options.js.map +1 -0
  20. package/dist/plugin.d.ts +4 -0
  21. package/dist/plugin.d.ts.map +1 -0
  22. package/dist/plugin.js +103 -0
  23. package/dist/plugin.js.map +1 -0
  24. package/dist/resolver.d.ts +25 -0
  25. package/dist/resolver.d.ts.map +1 -0
  26. package/dist/resolver.js +63 -0
  27. package/dist/resolver.js.map +1 -0
  28. package/dist/tokens.d.ts +48 -0
  29. package/dist/tokens.d.ts.map +1 -0
  30. package/dist/tokens.js +103 -0
  31. package/dist/tokens.js.map +1 -0
  32. package/dist/tools/get-approval-status.d.ts +11 -0
  33. package/dist/tools/get-approval-status.d.ts.map +1 -0
  34. package/dist/tools/get-approval-status.js +47 -0
  35. package/dist/tools/get-approval-status.js.map +1 -0
  36. package/dist/tools/index.d.ts +6 -0
  37. package/dist/tools/index.d.ts.map +1 -0
  38. package/dist/tools/index.js +6 -0
  39. package/dist/tools/index.js.map +1 -0
  40. package/dist/tools/list-my-requests.d.ts +11 -0
  41. package/dist/tools/list-my-requests.d.ts.map +1 -0
  42. package/dist/tools/list-my-requests.js +55 -0
  43. package/dist/tools/list-my-requests.js.map +1 -0
  44. package/dist/tools/list-pending-approvals.d.ts +11 -0
  45. package/dist/tools/list-pending-approvals.d.ts.map +1 -0
  46. package/dist/tools/list-pending-approvals.js +62 -0
  47. package/dist/tools/list-pending-approvals.js.map +1 -0
  48. package/dist/tools/request-approval.d.ts +13 -0
  49. package/dist/tools/request-approval.d.ts.map +1 -0
  50. package/dist/tools/request-approval.js +118 -0
  51. package/dist/tools/request-approval.js.map +1 -0
  52. package/dist/tools/respond-to-approval.d.ts +13 -0
  53. package/dist/tools/respond-to-approval.d.ts.map +1 -0
  54. package/dist/tools/respond-to-approval.js +107 -0
  55. package/dist/tools/respond-to-approval.js.map +1 -0
  56. package/package.json +64 -7
@@ -0,0 +1,50 @@
1
+ import type { Inngest } from 'inngest';
2
+ import type { BaseCorePluginOptions } from '@forumone/throughline-plugin-contract';
3
+ export interface ApproverGroup {
4
+ /** Group slug, referenced by the policy.approverGroups field on requests. */
5
+ slug: string;
6
+ /** Human-readable group name shown in admin UI. */
7
+ name: string;
8
+ /** Optional description shown in admin. */
9
+ description?: string;
10
+ }
11
+ export interface ResolvedApprover {
12
+ id: string;
13
+ email: string;
14
+ name?: string;
15
+ }
16
+ export interface GroupResolver {
17
+ /**
18
+ * Given a list of group slugs, returns the users in those groups. Used
19
+ * when an approval is requested to materialize the actual approver list
20
+ * and again if the action endpoint needs to confirm a token-bearing user
21
+ * belongs to one of the request's groups.
22
+ */
23
+ resolveUsers: (groupSlugs: string[]) => Promise<ResolvedApprover[]>;
24
+ }
25
+ export interface ApprovalsPluginOptions extends BaseCorePluginOptions {
26
+ /** Approver groups available in this deployment. */
27
+ groups: ApproverGroup[];
28
+ /** Resolver mapping group slugs to users. Required. */
29
+ groupResolver: GroupResolver;
30
+ /** Inngest client for firing approval/* events. Required. */
31
+ inngest: Inngest;
32
+ /** HMAC signing secret for action tokens. Falls back to APPROVAL_TOKEN_SECRET env var. */
33
+ tokenSecret?: string;
34
+ /** Days before a pending approval expires. Default: 7. */
35
+ expirationDays?: number;
36
+ /** Override the approvals collection slug. Default: 'approvals'. */
37
+ collectionSlug?: string;
38
+ /** Override the users collection slug used for approver/requester relationships. Default: 'users'. */
39
+ usersSlug?: string;
40
+ /** Public base URL used to build inline action links. Defaults to NEXT_PUBLIC_SERVER_URL. */
41
+ publicUrl?: string;
42
+ }
43
+ /**
44
+ * Validates options at load time and resolves the token secret. Throws with
45
+ * a targeted error on each obvious misconfiguration.
46
+ */
47
+ export declare function validateOptions(options: ApprovalsPluginOptions): ApprovalsPluginOptions & {
48
+ tokenSecret: string;
49
+ };
50
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAA;AAElF,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAA;IACZ,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAA;IACZ,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,YAAY,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAA;CACpE;AAED,MAAM,WAAW,sBAAuB,SAAQ,qBAAqB;IACnE,oDAAoD;IACpD,MAAM,EAAE,aAAa,EAAE,CAAA;IACvB,uDAAuD;IACvD,aAAa,EAAE,aAAa,CAAA;IAC5B,6DAA6D;IAC7D,OAAO,EAAE,OAAO,CAAA;IAChB,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,sGAAsG;IACtG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,6FAA6F;IAC7F,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAID;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,GAC9B,sBAAsB,GAAG;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,CA2BlD"}
@@ -0,0 +1,28 @@
1
+ const MIN_TOKEN_SECRET_LENGTH = 32;
2
+ /**
3
+ * Validates options at load time and resolves the token secret. Throws with
4
+ * a targeted error on each obvious misconfiguration.
5
+ */
6
+ export function validateOptions(options) {
7
+ if (!options.groups || options.groups.length === 0) {
8
+ throw new Error('approvalsPlugin requires at least one group in options.groups');
9
+ }
10
+ if (!options.groupResolver) {
11
+ throw new Error('approvalsPlugin requires a groupResolver in options');
12
+ }
13
+ if (!options.inngest) {
14
+ throw new Error('approvalsPlugin requires an Inngest client in options.inngest');
15
+ }
16
+ const secret = options.tokenSecret ?? process.env['APPROVAL_TOKEN_SECRET'];
17
+ if (!secret || secret.length < MIN_TOKEN_SECRET_LENGTH) {
18
+ throw new Error(`approvalsPlugin requires a tokenSecret in options or an APPROVAL_TOKEN_SECRET env var (${MIN_TOKEN_SECRET_LENGTH}+ characters)`);
19
+ }
20
+ // Group slugs must be unique.
21
+ const slugs = options.groups.map((g) => g.slug);
22
+ const dupes = slugs.filter((s, i) => slugs.indexOf(s) !== i);
23
+ if (dupes.length > 0) {
24
+ throw new Error(`approvalsPlugin: duplicate group slug${dupes.length === 1 ? '' : 's'}: ${dupes.join(', ')}`);
25
+ }
26
+ return { ...options, tokenSecret: secret };
27
+ }
28
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AA+CA,MAAM,uBAAuB,GAAG,EAAE,CAAA;AAElC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC7B,OAA+B;IAE/B,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAA;IAClF,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAA;IACxE,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAA;IAClF,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAA;IAC1E,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,uBAAuB,EAAE,CAAC;QACvD,MAAM,IAAI,KAAK,CACb,0FAA0F,uBAAuB,eAAe,CACjI,CAAA;IACH,CAAC;IAED,8BAA8B;IAC9B,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IAC/C,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;IAC5D,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CACb,wCAAwC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC7F,CAAA;IACH,CAAC;IAED,OAAO,EAAE,GAAG,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,CAAA;AAC5C,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { CorePlugin } from '@forumone/throughline-plugin-contract';
2
+ import { type ApprovalsPluginOptions } from './options.js';
3
+ export declare const approvalsPlugin: CorePlugin<ApprovalsPluginOptions>;
4
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAA;AAQvE,OAAO,EAAE,KAAK,sBAAsB,EAAmB,MAAM,cAAc,CAAA;AAmB3E,eAAO,MAAM,eAAe,EAAE,UAAU,CAAC,sBAAsB,CA2G5D,CAAA"}
package/dist/plugin.js ADDED
@@ -0,0 +1,103 @@
1
+ import { getPluginRegistry } from '@forumone/throughline-plugin-contract';
2
+ import { createMcpHandler, createNamedLogger, defaultLogger, getAuditWriter, } from '@forumone/throughline-core';
3
+ import { validateOptions } from './options.js';
4
+ import { createApprovalsCollection } from './collection.js';
5
+ import { attachApprovalResolver, createApprovalResolver } from './resolver.js';
6
+ import { createActionEndpoint } from './endpoints/action.js';
7
+ import { createGetApprovalStatusTool, createListMyRequestsTool, createListPendingApprovalsTool, createRequestApprovalTool, createRespondToApprovalTool, } from './tools/index.js';
8
+ const PLUGIN_ID = '@forumone/throughline-approvals';
9
+ const PLUGIN_VERSION = '0.1.0';
10
+ const MCP_HANDLER_SYMBOL = Symbol.for('@forumone/throughline/approvals-mcp-handler');
11
+ const ACTION_HANDLER_SYMBOL = Symbol.for('@forumone/throughline/approvals-action-handler');
12
+ export const approvalsPlugin = (rawOptions) => (incomingConfig) => {
13
+ if (rawOptions.enabled === false)
14
+ return incomingConfig;
15
+ const options = validateOptions(rawOptions);
16
+ const routePrefix = options.routePrefix ?? '/approvals';
17
+ const logger = createNamedLogger('approvals', options.logger ?? defaultLogger);
18
+ const collection = createApprovalsCollection({
19
+ ...(options.collectionSlug ? { slug: options.collectionSlug } : {}),
20
+ ...(options.usersSlug ? { usersSlug: options.usersSlug } : {}),
21
+ groupSlugs: options.groups.map((g) => g.slug),
22
+ });
23
+ return {
24
+ ...incomingConfig,
25
+ collections: [...(incomingConfig.collections ?? []), collection],
26
+ endpoints: [
27
+ ...(incomingConfig.endpoints ?? []),
28
+ {
29
+ path: `${routePrefix}/mcp`,
30
+ method: 'post',
31
+ handler: async (req) => {
32
+ const handler = req.payload[MCP_HANDLER_SYMBOL];
33
+ if (!handler) {
34
+ return new Response(JSON.stringify({ error: 'Approvals MCP not initialized' }), { status: 503, headers: { 'content-type': 'application/json' } });
35
+ }
36
+ return handler(req);
37
+ },
38
+ },
39
+ {
40
+ path: `${routePrefix}/action`,
41
+ method: 'get',
42
+ handler: async (req) => {
43
+ const actionHandler = req.payload[ACTION_HANDLER_SYMBOL];
44
+ if (!actionHandler) {
45
+ return new Response('Approvals action handler not initialized', { status: 503 });
46
+ }
47
+ return actionHandler(req);
48
+ },
49
+ },
50
+ ],
51
+ onInit: async (payload) => {
52
+ if (incomingConfig.onInit) {
53
+ await incomingConfig.onInit(payload);
54
+ }
55
+ const registry = getPluginRegistry(payload);
56
+ registry.requireCapability('audit-log', PLUGIN_ID);
57
+ const auditWriter = getAuditWriter(payload);
58
+ const collectionSlug = options.collectionSlug;
59
+ const resolver = createApprovalResolver(collectionSlug ? { payload, collectionSlug } : { payload });
60
+ attachApprovalResolver(payload, resolver);
61
+ const actionEndpoint = createActionEndpoint({ options, auditWriter });
62
+ Object.defineProperty(payload, ACTION_HANDLER_SYMBOL, {
63
+ value: actionEndpoint.handler,
64
+ enumerable: false,
65
+ writable: false,
66
+ configurable: false,
67
+ });
68
+ const tools = [
69
+ createRequestApprovalTool({ payload, options, auditWriter }),
70
+ createRespondToApprovalTool({ payload, options, auditWriter }),
71
+ createGetApprovalStatusTool({ payload, options }),
72
+ createListPendingApprovalsTool({ payload, options }),
73
+ createListMyRequestsTool({ payload, options }),
74
+ ];
75
+ const handler = createMcpHandler({
76
+ payload,
77
+ serverName: 'approvals',
78
+ tools,
79
+ logger,
80
+ });
81
+ Object.defineProperty(payload, MCP_HANDLER_SYMBOL, {
82
+ value: handler,
83
+ enumerable: false,
84
+ writable: false,
85
+ configurable: false,
86
+ });
87
+ registry.register({
88
+ id: PLUGIN_ID,
89
+ version: PLUGIN_VERSION,
90
+ capabilities: ['approvals', 'approval-resolver'],
91
+ });
92
+ logger.info('Approvals server ready', {
93
+ groups: options.groups.map((g) => g.slug),
94
+ expirationDays: options.expirationDays ?? 7,
95
+ });
96
+ },
97
+ };
98
+ };
99
+ // `createActionEndpoint` is exported from src/index.ts for clients who want
100
+ // to expose the action handler separately (e.g. behind a branded router).
101
+ // Most consumers won't need it — the plugin already registers the action
102
+ // endpoint at `${routePrefix}/action`.
103
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAA;AACzE,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,cAAc,GACf,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAA+B,eAAe,EAAE,MAAM,cAAc,CAAA;AAC3E,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EACL,2BAA2B,EAC3B,wBAAwB,EACxB,8BAA8B,EAC9B,yBAAyB,EACzB,2BAA2B,GAC5B,MAAM,kBAAkB,CAAA;AAEzB,MAAM,SAAS,GAAG,iCAAiC,CAAA;AACnD,MAAM,cAAc,GAAG,OAAO,CAAA;AAC9B,MAAM,kBAAkB,GAAG,MAAM,CAAC,GAAG,CAAC,6CAA6C,CAAC,CAAA;AACpF,MAAM,qBAAqB,GAAG,MAAM,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAA;AAI1F,MAAM,CAAC,MAAM,eAAe,GAC1B,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,cAAc,EAAE,EAAE;IACjC,IAAI,UAAU,CAAC,OAAO,KAAK,KAAK;QAAE,OAAO,cAAc,CAAA;IAEvD,MAAM,OAAO,GAAG,eAAe,CAAC,UAAU,CAAC,CAAA;IAC3C,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,YAAY,CAAA;IACvD,MAAM,MAAM,GAAG,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,MAAM,IAAI,aAAa,CAAC,CAAA;IAE9E,MAAM,UAAU,GAAG,yBAAyB,CAAC;QAC3C,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnE,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;KAC9C,CAAC,CAAA;IAEF,OAAO;QACL,GAAG,cAAc;QACjB,WAAW,EAAE,CAAC,GAAG,CAAC,cAAc,CAAC,WAAW,IAAI,EAAE,CAAC,EAAE,UAAU,CAAC;QAChE,SAAS,EAAE;YACT,GAAG,CAAC,cAAc,CAAC,SAAS,IAAI,EAAE,CAAC;YACnC;gBACE,IAAI,EAAE,GAAG,WAAW,MAAM;gBAC1B,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;oBACrB,MAAM,OAAO,GAAI,GAAG,CAAC,OAA8C,CACjE,kBAAkB,CACO,CAAA;oBAC3B,IAAI,CAAC,OAAO,EAAE,CAAC;wBACb,OAAO,IAAI,QAAQ,CACjB,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAC,EAC1D,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,EAAE,CACjE,CAAA;oBACH,CAAC;oBACD,OAAO,OAAO,CAAC,GAAyB,CAAC,CAAA;gBAC3C,CAAC;aACF;YACD;gBACE,IAAI,EAAE,GAAG,WAAW,SAAS;gBAC7B,MAAM,EAAE,KAAK;gBACb,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;oBACrB,MAAM,aAAa,GAAI,GAAG,CAAC,OAA8C,CACvE,qBAAqB,CACgC,CAAA;oBACvD,IAAI,CAAC,aAAa,EAAE,CAAC;wBACnB,OAAO,IAAI,QAAQ,CAAC,0CAA0C,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;oBAClF,CAAC;oBACD,OAAO,aAAa,CAAC,GAAG,CAAC,CAAA;gBAC3B,CAAC;aACF;SACF;QACD,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACxB,IAAI,cAAc,CAAC,MAAM,EAAE,CAAC;gBAC1B,MAAM,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;YACtC,CAAC;YAED,MAAM,QAAQ,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAA;YAC3C,QAAQ,CAAC,iBAAiB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;YAElD,MAAM,WAAW,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;YAE3C,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAA;YAC7C,MAAM,QAAQ,GAAG,sBAAsB,CACrC,cAAc,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAC3D,CAAA;YACD,sBAAsB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;YAEzC,MAAM,cAAc,GAAG,oBAAoB,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAA;YACrE,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,qBAAqB,EAAE;gBACpD,KAAK,EAAE,cAAc,CAAC,OAAO;gBAC7B,UAAU,EAAE,KAAK;gBACjB,QAAQ,EAAE,KAAK;gBACf,YAAY,EAAE,KAAK;aACpB,CAAC,CAAA;YAEF,MAAM,KAAK,GAAG;gBACZ,yBAAyB,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;gBAC5D,2BAA2B,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;gBAC9D,2BAA2B,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;gBACjD,8BAA8B,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;gBACpD,wBAAwB,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;aAC/C,CAAA;YAED,MAAM,OAAO,GAAG,gBAAgB,CAAC;gBAC/B,OAAO;gBACP,UAAU,EAAE,WAAW;gBACvB,KAAK;gBACL,MAAM;aACP,CAAC,CAAA;YAEF,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,kBAAkB,EAAE;gBACjD,KAAK,EAAE,OAAO;gBACd,UAAU,EAAE,KAAK;gBACjB,QAAQ,EAAE,KAAK;gBACf,YAAY,EAAE,KAAK;aACpB,CAAC,CAAA;YAEF,QAAQ,CAAC,QAAQ,CAAC;gBAChB,EAAE,EAAE,SAAS;gBACb,OAAO,EAAE,cAAc;gBACvB,YAAY,EAAE,CAAC,WAAW,EAAE,mBAAmB,CAAC;aACjD,CAAC,CAAA;YAEF,MAAM,CAAC,IAAI,CAAC,wBAAwB,EAAE;gBACpC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;gBACzC,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,CAAC;aAC5C,CAAC,CAAA;QACJ,CAAC;KACF,CAAA;AACH,CAAC,CAAA;AAEH,4EAA4E;AAC5E,0EAA0E;AAC1E,yEAAyE;AACzE,uCAAuC"}
@@ -0,0 +1,25 @@
1
+ import type { Payload } from 'payload';
2
+ import type { ApprovalResolver } from '@forumone/throughline-publishing';
3
+ /**
4
+ * Symbol under which the resolver is attached to the Payload instance.
5
+ * Publishing's `approvalStep` looks here when its `options.approvalResolver`
6
+ * is not set; this is the wiring point that lets clients add the approvals
7
+ * plugin without re-configuring publishing.
8
+ */
9
+ export declare const APPROVALS_RESOLVER_SYMBOL: unique symbol;
10
+ export interface CreateApprovalResolverOptions {
11
+ payload: Payload;
12
+ /** Override the approvals collection slug. Default: 'approvals'. */
13
+ collectionSlug?: string;
14
+ }
15
+ /**
16
+ * Builds the resolver that the publishing server consumes. Looks up the
17
+ * latest granted approval whose `targetVersion` matches the document
18
+ * version under consideration. Multiple decisions are sorted by `decidedAt`
19
+ * descending; first-decision-wins semantics mean the latest grant for a
20
+ * given version is what counts.
21
+ */
22
+ export declare function createApprovalResolver(options: CreateApprovalResolverOptions): ApprovalResolver;
23
+ /** Attaches the resolver to a Payload instance under the agreed symbol. */
24
+ export declare function attachApprovalResolver(payload: object, resolver: ApprovalResolver): void;
25
+ //# sourceMappingURL=resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAkB,gBAAgB,EAAE,MAAM,kCAAkC,CAAA;AAGxF;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,eAErC,CAAA;AAED,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,OAAO,CAAA;IAChB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,6BAA6B,GACrC,gBAAgB,CAyClB;AAED,2EAA2E;AAC3E,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,gBAAgB,GACzB,IAAI,CAON"}
@@ -0,0 +1,63 @@
1
+ import { DEFAULT_APPROVALS_SLUG } from './collection.js';
2
+ /**
3
+ * Symbol under which the resolver is attached to the Payload instance.
4
+ * Publishing's `approvalStep` looks here when its `options.approvalResolver`
5
+ * is not set; this is the wiring point that lets clients add the approvals
6
+ * plugin without re-configuring publishing.
7
+ */
8
+ export const APPROVALS_RESOLVER_SYMBOL = Symbol.for('@forumone/throughline/approvals-resolver');
9
+ /**
10
+ * Builds the resolver that the publishing server consumes. Looks up the
11
+ * latest granted approval whose `targetVersion` matches the document
12
+ * version under consideration. Multiple decisions are sorted by `decidedAt`
13
+ * descending; first-decision-wins semantics mean the latest grant for a
14
+ * given version is what counts.
15
+ */
16
+ export function createApprovalResolver(options) {
17
+ const { payload, collectionSlug = DEFAULT_APPROVALS_SLUG } = options;
18
+ return {
19
+ async getActiveApproval(collection, id, version) {
20
+ const result = await payload.find({
21
+ collection: collectionSlug,
22
+ where: {
23
+ and: [
24
+ { targetCollection: { equals: collection } },
25
+ { targetId: { equals: id } },
26
+ { targetVersion: { equals: version } },
27
+ { status: { equals: 'granted' } },
28
+ ],
29
+ },
30
+ limit: 1,
31
+ sort: '-decidedAt',
32
+ });
33
+ const approval = result.docs[0];
34
+ if (!approval)
35
+ return null;
36
+ const decidedAt = approval['decidedAt'];
37
+ if (typeof decidedAt !== 'string')
38
+ return null;
39
+ const decidedByRaw = approval['decidedBy'];
40
+ const decidedBy = typeof decidedByRaw === 'string'
41
+ ? decidedByRaw
42
+ : decidedByRaw && typeof decidedByRaw === 'object' && 'id' in decidedByRaw
43
+ ? String(decidedByRaw.id)
44
+ : '';
45
+ return {
46
+ id: String(approval['id']),
47
+ grantedAt: decidedAt,
48
+ grantedBy: decidedBy,
49
+ version: String(approval['targetVersion']),
50
+ };
51
+ },
52
+ };
53
+ }
54
+ /** Attaches the resolver to a Payload instance under the agreed symbol. */
55
+ export function attachApprovalResolver(payload, resolver) {
56
+ Object.defineProperty(payload, APPROVALS_RESOLVER_SYMBOL, {
57
+ value: resolver,
58
+ enumerable: false,
59
+ writable: false,
60
+ configurable: false,
61
+ });
62
+ }
63
+ //# sourceMappingURL=resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAExD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC,GAAG,CACjD,0CAA0C,CAC3C,CAAA;AAQD;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CACpC,OAAsC;IAEtC,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,sBAAsB,EAAE,GAAG,OAAO,CAAA;IAEpE,OAAO;QACL,KAAK,CAAC,iBAAiB,CAAC,UAAU,EAAE,EAAE,EAAE,OAAO;YAC7C,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;gBAChC,UAAU,EAAE,cAAc;gBAC1B,KAAK,EAAE;oBACL,GAAG,EAAE;wBACH,EAAE,gBAAgB,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE;wBAC5C,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE;wBAC5B,EAAE,aAAa,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;wBACtC,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE;qBAClC;iBACF;gBACD,KAAK,EAAE,CAAC;gBACR,IAAI,EAAE,YAAY;aACnB,CAAC,CAAA;YAEF,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAwC,CAAA;YACtE,IAAI,CAAC,QAAQ;gBAAE,OAAO,IAAI,CAAA;YAE1B,MAAM,SAAS,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAA;YACvC,IAAI,OAAO,SAAS,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAA;YAE9C,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAA;YAC1C,MAAM,SAAS,GACb,OAAO,YAAY,KAAK,QAAQ;gBAC9B,CAAC,CAAC,YAAY;gBACd,CAAC,CAAC,YAAY,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,IAAI,IAAI,YAAY;oBACxE,CAAC,CAAC,MAAM,CAAE,YAAgC,CAAC,EAAE,CAAC;oBAC9C,CAAC,CAAC,EAAE,CAAA;YAEV,OAAO;gBACL,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;gBAC1B,SAAS,EAAE,SAAS;gBACpB,SAAS,EAAE,SAAS;gBACpB,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;aAC3C,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,sBAAsB,CACpC,OAAe,EACf,QAA0B;IAE1B,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,yBAAyB,EAAE;QACxD,KAAK,EAAE,QAAQ;QACf,UAAU,EAAE,KAAK;QACjB,QAAQ,EAAE,KAAK;QACf,YAAY,EAAE,KAAK;KACpB,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * HMAC-signed action tokens used in inline action emails (Approve / Decline /
3
+ * Request changes / Discuss). Each token is bound to one approval, one
4
+ * action, and one approver. Tokens are valid for a configurable window
5
+ * (default 14 days) and consumed via the `consumedTokens` array on the
6
+ * approval record so they can't be replayed.
7
+ *
8
+ * Crypto uses the Web Crypto API (`crypto.subtle`) so the same code runs
9
+ * in Node and edge runtimes.
10
+ */
11
+ export type ActionTokenAction = 'approve' | 'decline' | 'changes' | 'discuss';
12
+ export interface ActionToken {
13
+ approvalId: string;
14
+ action: ActionTokenAction;
15
+ approverId: string;
16
+ /** Issued-at timestamp in milliseconds since epoch. */
17
+ issuedAt: number;
18
+ }
19
+ /**
20
+ * Mints an action token. Returns a base64url-encoded payload with the
21
+ * format `<approvalId>:<action>:<approverId>:<issuedAt>:<signature>`.
22
+ */
23
+ export declare function generateActionToken(token: ActionToken, secret: string): Promise<string>;
24
+ export interface VerifyOptions {
25
+ /** Override the default 14-day token lifetime. */
26
+ maxAgeMs?: number;
27
+ /** Override "now" — useful in tests. Defaults to `Date.now()`. */
28
+ now?: number;
29
+ }
30
+ export type VerifyResult = {
31
+ ok: true;
32
+ token: ActionToken;
33
+ } | {
34
+ ok: false;
35
+ error: string;
36
+ };
37
+ /**
38
+ * Verifies a base64url-encoded action token with constant-time signature
39
+ * comparison. Returns either the parsed token or a structured error.
40
+ */
41
+ export declare function verifyActionToken(encoded: string, secret: string, options?: VerifyOptions): Promise<VerifyResult>;
42
+ /**
43
+ * Returns the absolute URL an approver visits to invoke the action. The
44
+ * server-side endpoint (added by the plugin) verifies the token and
45
+ * presents a confirmation page before recording the decision.
46
+ */
47
+ export declare function buildActionUrl(publicUrl: string, token: string): string;
48
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAA;AAE7E,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,iBAAiB,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAA;CACjB;AAOD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GAChC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhC;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAiDvB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGvE"}
package/dist/tokens.js ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * HMAC-signed action tokens used in inline action emails (Approve / Decline /
3
+ * Request changes / Discuss). Each token is bound to one approval, one
4
+ * action, and one approver. Tokens are valid for a configurable window
5
+ * (default 14 days) and consumed via the `consumedTokens` array on the
6
+ * approval record so they can't be replayed.
7
+ *
8
+ * Crypto uses the Web Crypto API (`crypto.subtle`) so the same code runs
9
+ * in Node and edge runtimes.
10
+ */
11
+ const DEFAULT_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
12
+ const TOKEN_PARTS = 5;
13
+ const VALID_ACTIONS = new Set(['approve', 'decline', 'changes', 'discuss']);
14
+ const ENCODER = new TextEncoder();
15
+ /**
16
+ * Mints an action token. Returns a base64url-encoded payload with the
17
+ * format `<approvalId>:<action>:<approverId>:<issuedAt>:<signature>`.
18
+ */
19
+ export async function generateActionToken(token, secret) {
20
+ ensureSafeForEncoding(token);
21
+ const payload = `${token.approvalId}:${token.action}:${token.approverId}:${token.issuedAt}`;
22
+ const signature = await hmacSignHex(payload, secret);
23
+ return base64UrlEncode(`${payload}:${signature}`);
24
+ }
25
+ /**
26
+ * Verifies a base64url-encoded action token with constant-time signature
27
+ * comparison. Returns either the parsed token or a structured error.
28
+ */
29
+ export async function verifyActionToken(encoded, secret, options = {}) {
30
+ let decoded;
31
+ try {
32
+ decoded = base64UrlDecode(encoded);
33
+ }
34
+ catch {
35
+ return { ok: false, error: 'Token decode failed' };
36
+ }
37
+ const parts = decoded.split(':');
38
+ if (parts.length !== TOKEN_PARTS)
39
+ return { ok: false, error: 'Invalid token format' };
40
+ const [approvalId, action, approverId, issuedAtRaw, providedSignature] = parts;
41
+ if (!approvalId || !action || !approverId || !issuedAtRaw || !providedSignature) {
42
+ return { ok: false, error: 'Invalid token format' };
43
+ }
44
+ const issuedAt = Number.parseInt(issuedAtRaw, 10);
45
+ if (!Number.isFinite(issuedAt)) {
46
+ return { ok: false, error: 'Invalid token timestamp' };
47
+ }
48
+ if (!VALID_ACTIONS.has(action)) {
49
+ return { ok: false, error: 'Invalid action' };
50
+ }
51
+ const expectedSignature = await hmacSignHex(`${approvalId}:${action}:${approverId}:${issuedAt}`, secret);
52
+ if (!constantTimeEqual(providedSignature, expectedSignature)) {
53
+ return { ok: false, error: 'Invalid token signature' };
54
+ }
55
+ const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
56
+ const now = options.now ?? Date.now();
57
+ if (now - issuedAt > maxAge) {
58
+ return { ok: false, error: 'Token has expired' };
59
+ }
60
+ return {
61
+ ok: true,
62
+ token: { approvalId, action: action, approverId, issuedAt },
63
+ };
64
+ }
65
+ /**
66
+ * Returns the absolute URL an approver visits to invoke the action. The
67
+ * server-side endpoint (added by the plugin) verifies the token and
68
+ * presents a confirmation page before recording the decision.
69
+ */
70
+ export function buildActionUrl(publicUrl, token) {
71
+ const base = publicUrl.replace(/\/$/, '');
72
+ return `${base}/api/approvals/action?token=${encodeURIComponent(token)}`;
73
+ }
74
+ async function hmacSignHex(payload, secret) {
75
+ const key = await crypto.subtle.importKey('raw', ENCODER.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
76
+ const signature = await crypto.subtle.sign('HMAC', key, ENCODER.encode(payload));
77
+ return Array.from(new Uint8Array(signature))
78
+ .map((b) => b.toString(16).padStart(2, '0'))
79
+ .join('');
80
+ }
81
+ function constantTimeEqual(a, b) {
82
+ if (a.length !== b.length)
83
+ return false;
84
+ let result = 0;
85
+ for (let i = 0; i < a.length; i++) {
86
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
87
+ }
88
+ return result === 0;
89
+ }
90
+ function ensureSafeForEncoding(token) {
91
+ if (token.approvalId.includes(':') ||
92
+ token.approverId.includes(':') ||
93
+ token.action.includes(':')) {
94
+ throw new Error('Token field contains a reserved colon character');
95
+ }
96
+ }
97
+ function base64UrlEncode(input) {
98
+ return Buffer.from(input, 'utf-8').toString('base64url');
99
+ }
100
+ function base64UrlDecode(encoded) {
101
+ return Buffer.from(encoded, 'base64url').toString('utf-8');
102
+ }
103
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAYH,MAAM,kBAAkB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AACnD,MAAM,WAAW,GAAG,CAAC,CAAA;AACrB,MAAM,aAAa,GAAG,IAAI,GAAG,CAAoB,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,CAAA;AAC9F,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;AAEjC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,KAAkB,EAClB,MAAc;IAEd,qBAAqB,CAAC,KAAK,CAAC,CAAA;IAC5B,MAAM,OAAO,GAAG,GAAG,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAA;IAC3F,MAAM,SAAS,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IACpD,OAAO,eAAe,CAAC,GAAG,OAAO,IAAI,SAAS,EAAE,CAAC,CAAA;AACnD,CAAC;AAaD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAe,EACf,MAAc,EACd,UAAyB,EAAE;IAE3B,IAAI,OAAe,CAAA;IACnB,IAAI,CAAC;QACH,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,CAAA;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAA;IACpD,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAChC,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAA;IAErF,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,iBAAiB,CAAC,GAAG,KAMxE,CAAA;IACD,IAAI,CAAC,UAAU,IAAI,CAAC,MAAM,IAAI,CAAC,UAAU,IAAI,CAAC,WAAW,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAChF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAA;IACrD,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;IACjD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC/B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAA;IACxD,CAAC;IAED,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,MAA2B,CAAC,EAAE,CAAC;QACpD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAA;IAC/C,CAAC;IAED,MAAM,iBAAiB,GAAG,MAAM,WAAW,CACzC,GAAG,UAAU,IAAI,MAAM,IAAI,UAAU,IAAI,QAAQ,EAAE,EACnD,MAAM,CACP,CAAA;IACD,IAAI,CAAC,iBAAiB,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,EAAE,CAAC;QAC7D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAA;IACxD,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,IAAI,kBAAkB,CAAA;IACrD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAA;IACrC,IAAI,GAAG,GAAG,QAAQ,GAAG,MAAM,EAAE,CAAC;QAC5B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAA;IAClD,CAAC;IAED,OAAO;QACL,EAAE,EAAE,IAAI;QACR,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,MAA2B,EAAE,UAAU,EAAE,QAAQ,EAAE;KACjF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB,EAAE,KAAa;IAC7D,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;IACzC,OAAO,GAAG,IAAI,+BAA+B,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAA;AAC1E,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,OAAe,EAAE,MAAc;IACxD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CACvC,KAAK,EACL,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EACtB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EACjC,KAAK,EACL,CAAC,MAAM,CAAC,CACT,CAAA;IACD,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAA;IAChF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC;SACzC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SAC3C,IAAI,CAAC,EAAE,CAAC,CAAA;AACb,CAAC;AAED,SAAS,iBAAiB,CAAC,CAAS,EAAE,CAAS;IAC7C,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACvC,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,MAAM,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IAC7C,CAAC;IACD,OAAO,MAAM,KAAK,CAAC,CAAA;AACrB,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAkB;IAC/C,IACE,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC;QAC9B,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC;QAC9B,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAC1B,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAA;IACpE,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,KAAa;IACpC,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;AAC1D,CAAC;AAED,SAAS,eAAe,CAAC,OAAe;IACtC,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;AAC5D,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { Payload } from 'payload';
2
+ import type { McpToolDefinition } from '@forumone/throughline-plugin-contract';
3
+ import type { ApprovalsPluginOptions } from '../options.js';
4
+ export interface GetApprovalStatusDeps {
5
+ payload: Payload;
6
+ options: ApprovalsPluginOptions & {
7
+ tokenSecret: string;
8
+ };
9
+ }
10
+ export declare function createGetApprovalStatusTool(deps: GetApprovalStatusDeps): McpToolDefinition;
11
+ //# sourceMappingURL=get-approval-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-approval-status.d.ts","sourceRoot":"","sources":["../../src/tools/get-approval-status.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAEtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAA;AAE9E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAE3D,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,sBAAsB,GAAG;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;CAC1D;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,qBAAqB,GAAG,iBAAiB,CAkC1F"}
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ import { withMeta } from '@forumone/throughline-core';
3
+ import { DEFAULT_APPROVALS_SLUG } from '../collection.js';
4
+ export function createGetApprovalStatusTool(deps) {
5
+ const inputSchema = withMeta({
6
+ approvalId: z.string(),
7
+ });
8
+ return {
9
+ name: 'get_approval_status',
10
+ description: "Returns the current status of an approval request including who decided, when, and any notes. Read-only; no audit record is written.",
11
+ inputSchema,
12
+ handler: async (input) => {
13
+ const approval = (await deps.payload.findByID({
14
+ collection: deps.options.collectionSlug ?? DEFAULT_APPROVALS_SLUG,
15
+ id: input.approvalId,
16
+ }));
17
+ if (!approval)
18
+ return { error: 'Approval not found' };
19
+ return {
20
+ approvalId: String(approval['id']),
21
+ status: approval['status'],
22
+ targetCollection: approval['targetCollection'],
23
+ targetId: approval['targetId'],
24
+ targetTitle: approval['targetTitle'],
25
+ targetVersion: approval['targetVersion'],
26
+ requestedBy: unwrapRelationshipId(approval['requestedBy']),
27
+ requestedAt: approval['requestedAt'],
28
+ approverGroups: approval['approverGroups'],
29
+ decidedBy: unwrapRelationshipId(approval['decidedBy']),
30
+ decidedAt: approval['decidedAt'],
31
+ decisionNotes: approval['decisionNotes'],
32
+ expiresAt: approval['expiresAt'],
33
+ };
34
+ },
35
+ };
36
+ }
37
+ function unwrapRelationshipId(value) {
38
+ if (value === null || value === undefined)
39
+ return null;
40
+ if (typeof value === 'string')
41
+ return value;
42
+ if (typeof value === 'object' && 'id' in value) {
43
+ return String(value.id);
44
+ }
45
+ return null;
46
+ }
47
+ //# sourceMappingURL=get-approval-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-approval-status.js","sourceRoot":"","sources":["../../src/tools/get-approval-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAErD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AAQzD,MAAM,UAAU,2BAA2B,CAAC,IAA2B;IACrE,MAAM,WAAW,GAAG,QAAQ,CAAC;QAC3B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;KACvB,CAAC,CAAA;IAEF,OAAO;QACL,IAAI,EAAE,qBAAqB;QAC3B,WAAW,EACT,sIAAsI;QACxI,WAAW;QACX,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;YACvB,MAAM,QAAQ,GAAG,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;gBAC5C,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,cAAc,IAAI,sBAAsB;gBACjE,EAAE,EAAE,KAAK,CAAC,UAAU;aACrB,CAAC,CAAmC,CAAA;YACrC,IAAI,CAAC,QAAQ;gBAAE,OAAO,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAA;YAErD,OAAO;gBACL,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;gBAClC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC;gBAC1B,gBAAgB,EAAE,QAAQ,CAAC,kBAAkB,CAAC;gBAC9C,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC;gBAC9B,WAAW,EAAE,QAAQ,CAAC,aAAa,CAAC;gBACpC,aAAa,EAAE,QAAQ,CAAC,eAAe,CAAC;gBACxC,WAAW,EAAE,oBAAoB,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;gBAC1D,WAAW,EAAE,QAAQ,CAAC,aAAa,CAAC;gBACpC,cAAc,EAAE,QAAQ,CAAC,gBAAgB,CAAC;gBAC1C,SAAS,EAAE,oBAAoB,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;gBACtD,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC;gBAChC,aAAa,EAAE,QAAQ,CAAC,eAAe,CAAC;gBACxC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC;aACjC,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAc;IAC1C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IACtD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,IAAI,IAAI,KAAK,EAAE,CAAC;QAC/C,OAAO,MAAM,CAAE,KAAyB,CAAC,EAAE,CAAC,CAAA;IAC9C,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { createRequestApprovalTool, type RequestApprovalDeps, } from './request-approval.js';
2
+ export { createRespondToApprovalTool, type RespondToApprovalDeps, } from './respond-to-approval.js';
3
+ export { createGetApprovalStatusTool, type GetApprovalStatusDeps, } from './get-approval-status.js';
4
+ export { createListPendingApprovalsTool, type ListPendingApprovalsDeps, } from './list-pending-approvals.js';
5
+ export { createListMyRequestsTool, type ListMyRequestsDeps, } from './list-my-requests.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,KAAK,mBAAmB,GACzB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,2BAA2B,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,2BAA2B,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,8BAA8B,EAC9B,KAAK,wBAAwB,GAC9B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,wBAAwB,EACxB,KAAK,kBAAkB,GACxB,MAAM,uBAAuB,CAAA"}
@@ -0,0 +1,6 @@
1
+ export { createRequestApprovalTool, } from './request-approval.js';
2
+ export { createRespondToApprovalTool, } from './respond-to-approval.js';
3
+ export { createGetApprovalStatusTool, } from './get-approval-status.js';
4
+ export { createListPendingApprovalsTool, } from './list-pending-approvals.js';
5
+ export { createListMyRequestsTool, } from './list-my-requests.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,GAE1B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,2BAA2B,GAE5B,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,2BAA2B,GAE5B,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,8BAA8B,GAE/B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,wBAAwB,GAEzB,MAAM,uBAAuB,CAAA"}
@@ -0,0 +1,11 @@
1
+ import type { Payload } from 'payload';
2
+ import type { McpToolDefinition } from '@forumone/throughline-plugin-contract';
3
+ import type { ApprovalsPluginOptions } from '../options.js';
4
+ export interface ListMyRequestsDeps {
5
+ payload: Payload;
6
+ options: ApprovalsPluginOptions & {
7
+ tokenSecret: string;
8
+ };
9
+ }
10
+ export declare function createListMyRequestsTool(deps: ListMyRequestsDeps): McpToolDefinition;
11
+ //# sourceMappingURL=list-my-requests.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-my-requests.d.ts","sourceRoot":"","sources":["../../src/tools/list-my-requests.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAEtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAA;AAE9E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAE3D,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,sBAAsB,GAAG;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;CAC1D;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,kBAAkB,GAAG,iBAAiB,CAqDpF"}