@forumone/throughline-approvals 0.0.1 → 0.2.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +12 -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
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @forumone/throughline-approvals
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3ef6f6a: Initial release. Conversational approval workflow server with HMAC-signed single-use action tokens, per-group approver resolution, first-decision-wins semantics, version-bound approvals, seven-day default expiration, an HTML confirmation flow on the action endpoint, and five MCP tools (`request_approval`, `respond_to_approval`, `get_approval_status`, `list_pending_approvals`, `list_my_requests`). The plugin's `onInit` attaches the approval resolver to the Payload instance under `Symbol.for('@forumone/throughline/approvals-resolver')` so the publishing server can look it up automatically.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [3ef6f6a]
12
+ - @forumone/throughline-publishing@0.2.1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Forum One Communications Corporation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,45 +1,103 @@
1
1
  # @forumone/throughline-approvals
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
4
-
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
6
-
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
8
-
9
- ## Purpose
10
-
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@forumone/throughline-approvals`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
15
-
16
- ## What is OIDC Trusted Publishing?
17
-
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
19
-
20
- ## Setup Instructions
21
-
22
- To properly configure OIDC trusted publishing for this package:
23
-
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
-
29
- ## DO NOT USE THIS PACKAGE
30
-
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
-
37
- ## More Information
38
-
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
42
-
43
- ---
44
-
45
- **Maintained for OIDC setup purposes only**
3
+ Conversational approval workflow server for Throughline. Provides the resolver the publishing server consumes plus MCP tools and email-action endpoints for requesting and responding to approvals.
4
+
5
+ ## What this package provides
6
+
7
+ - **Approvals collection** with target / request / decision / workflow-state fields and indexes for the common queries.
8
+ - **Approval resolver** that the publishing server's `approvalStep` calls to check for active granted approvals (auto-attached on the Payload instance via Symbol — no manual wiring needed).
9
+ - **HMAC-signed action tokens** (`generateActionToken` / `verifyActionToken`) for inline-action emails. Single-use enforcement via the per-record `consumedTokens` array prevents replay.
10
+ - **HTTP action endpoint** at `/api/approvals/action` that handles email button clicks: verify token → confirmation page → record decision.
11
+ - **Five MCP tools** served at `/api/approvals/mcp`:
12
+
13
+ | Tool | Audit |
14
+ |---|---|
15
+ | `request_approval` | `approval.requested` |
16
+ | `respond_to_approval` | `approval.granted` / `.declined` / `.changes_requested` |
17
+ | `get_approval_status` | none (read-only) |
18
+ | `list_pending_approvals` | none (read-only) |
19
+ | `list_my_requests` | none (read-only) |
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pnpm add @forumone/throughline-approvals
25
+ ```
26
+
27
+ Peers: `payload@^3.0.0`, `inngest@^4.0.0`. Required runtime peer: `@forumone/throughline-core` (audit log).
28
+
29
+ ## Usage
30
+
31
+ ```ts
32
+ import { buildConfig } from 'payload'
33
+ import { auditPlugin, createInngestClient } from '@forumone/throughline-core'
34
+ import { approvalsPlugin } from '@forumone/throughline-approvals'
35
+ import { publishingPlugin } from '@forumone/throughline-publishing'
36
+
37
+ const inngest = createInngestClient({ id: 'my-site' })
38
+
39
+ export default buildConfig({
40
+ // collections, db, secret...
41
+ plugins: [
42
+ auditPlugin({ inngest }),
43
+ approvalsPlugin({
44
+ inngest,
45
+ groups: [
46
+ { slug: 'editorial', name: 'Editorial review' },
47
+ { slug: 'legal', name: 'Legal review' },
48
+ ],
49
+ groupResolver: {
50
+ async resolveUsers(slugs) {
51
+ // Return users belonging to any of the listed groups.
52
+ return payload.find({
53
+ collection: 'users',
54
+ where: { groups: { in: slugs } },
55
+ })
56
+ },
57
+ },
58
+ tokenSecret: process.env.APPROVAL_TOKEN_SECRET,
59
+ }),
60
+ publishingPlugin({
61
+ inngest,
62
+ collections: [{ slug: 'pages' }],
63
+ // No `approvalResolver` is needed — approvalsPlugin attaches it
64
+ // automatically via Symbol. Pass one explicitly only if you need
65
+ // to override.
66
+ }),
67
+ ],
68
+ })
69
+ ```
70
+
71
+ ## Wiring with the publishing server
72
+
73
+ The publishing server's `approvalStep` does not require an `approvalResolver` in its options. When approvalsPlugin is registered, it attaches the resolver to the Payload instance under `Symbol.for('@forumone/throughline/approvals-resolver')`, and publishing's approval step looks it up at publish time.
74
+
75
+ If you need a custom resolver (e.g. you store approvals in an external system), pass `approvalResolver` directly to `publishingPlugin` — it takes precedence over the symbol lookup.
76
+
77
+ ## Phase 1 semantics
78
+
79
+ - **First-decision-wins.** Multi-party approvals (e.g. legal AND communications must both approve) are deferred to Phase 2. The Phase 1 model handles "any one approver from the configured groups," which covers the most common case.
80
+ - **Approvals are tied to versions.** An approval granted against one draft does not apply to a subsequent edit. The resolver checks `targetVersion` against the document version under consideration.
81
+ - **Action tokens are single-use, 14-day validity.** Once an approver clicks an action link, the token is appended to the request's `consumedTokens` array; reusing it returns an error.
82
+ - **Self-approval is blocked.** The respond_to_approval tool refuses if the caller is the requester.
83
+ - **Group resolution is configurable.** Clients define what "editorial" or "legal" means via the `groupResolver.resolveUsers` callback. Core does not hardcode group membership logic.
84
+
85
+ ## Action endpoint
86
+
87
+ The plugin registers a GET endpoint at `${routePrefix}/action` (default `/api/approvals/action`) that accepts a `?token=` query param. Flow:
88
+
89
+ 1. Verify the HMAC signature and check the token hasn't expired.
90
+ 2. Load the approval; bail if it's already decided or the token has been consumed.
91
+ 3. First hit: render a confirmation page. Second hit (with `?confirm=true`): record the decision, append the token to `consumedTokens`, fire `approval/decided`, write the audit record.
92
+
93
+ The confirmation page is intentionally minimal. Clients that want a branded action page can register their own endpoint that calls `verifyActionToken` and `previewVerification` directly — both are exported from the package entry point.
94
+
95
+ ## The `_meta` payload
96
+
97
+ `request_approval` and `respond_to_approval` accept the framework's `_meta` payload (via `withMeta` from `@forumone/throughline-core`). Audit records carry the prompt and reasoning fields for later "why was this approved?" queries.
98
+
99
+ ## Related packages
100
+
101
+ - `@forumone/throughline-core` — required peer; provides the audit log and MCP handler this plugin builds on
102
+ - `@forumone/throughline-publishing` — peer plugin that consumes the resolver this plugin attaches
103
+ - `@forumone/throughline-email` (C11) — will subscribe to `approval/requested` and `approval/decided` to send notifications
@@ -0,0 +1,18 @@
1
+ import type { CollectionConfig } from 'payload';
2
+ export interface CreateApprovalsCollectionOptions {
3
+ /** Override the collection slug. Default: 'approvals'. */
4
+ slug?: string;
5
+ /** Slug of the users collection that approver/requester relationships point at. Default: 'users'. */
6
+ usersSlug?: string;
7
+ /** Allowed group slugs from the plugin's `groups` option. Used as the select options on the approverGroups field. */
8
+ groupSlugs: string[];
9
+ }
10
+ export declare const DEFAULT_APPROVALS_SLUG = "approvals";
11
+ /**
12
+ * Builds the approvals collection config. The collection is read-mostly:
13
+ * `create` is system-only (the plugin's tools mint records via the local
14
+ * API), `update` is admin-only (so a stuck pending record can be corrected),
15
+ * `delete` is denied (audit trail must remain).
16
+ */
17
+ export declare function createApprovalsCollection(options: CreateApprovalsCollectionOptions): CollectionConfig;
18
+ //# sourceMappingURL=collection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,MAAM,WAAW,gCAAgC;IAC/C,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qGAAqG;IACrG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,qHAAqH;IACrH,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED,eAAO,MAAM,sBAAsB,cAAc,CAAA;AAEjD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gCAAgC,GACxC,gBAAgB,CA4FlB"}
@@ -0,0 +1,95 @@
1
+ export const DEFAULT_APPROVALS_SLUG = 'approvals';
2
+ /**
3
+ * Builds the approvals collection config. The collection is read-mostly:
4
+ * `create` is system-only (the plugin's tools mint records via the local
5
+ * API), `update` is admin-only (so a stuck pending record can be corrected),
6
+ * `delete` is denied (audit trail must remain).
7
+ */
8
+ export function createApprovalsCollection(options) {
9
+ const slug = options.slug ?? DEFAULT_APPROVALS_SLUG;
10
+ const usersSlug = options.usersSlug ?? 'users';
11
+ return {
12
+ slug,
13
+ admin: {
14
+ useAsTitle: 'targetTitle',
15
+ defaultColumns: ['targetTitle', 'status', 'requestedBy', 'requestedAt', 'expiresAt'],
16
+ description: 'Approval workflow state. Read-mostly through the admin; writes happen via the Approvals Server tools and the action endpoint.',
17
+ },
18
+ access: {
19
+ read: ({ req }) => {
20
+ const roles = req.user?.['roles'] ?? [];
21
+ return roles.includes('admin') || roles.includes('editor') || roles.includes('approver');
22
+ },
23
+ create: () => false,
24
+ update: ({ req }) => (req.user?.['roles'] ?? []).includes('admin'),
25
+ delete: () => false,
26
+ },
27
+ fields: [
28
+ // Target
29
+ { name: 'targetCollection', type: 'text', required: true },
30
+ { name: 'targetId', type: 'text', required: true },
31
+ { name: 'targetTitle', type: 'text', required: true },
32
+ {
33
+ name: 'targetVersion',
34
+ type: 'text',
35
+ required: true,
36
+ admin: { description: 'Document version at request time. Approvals are tied to this version.' },
37
+ },
38
+ { name: 'previewUrl', type: 'text' },
39
+ // Request
40
+ { name: 'requestedBy', type: 'relationship', relationTo: usersSlug, required: true },
41
+ {
42
+ name: 'requestedAt',
43
+ type: 'date',
44
+ required: true,
45
+ defaultValue: () => new Date().toISOString(),
46
+ },
47
+ { name: 'requestReason', type: 'textarea' },
48
+ { name: 'changesSummary', type: 'textarea', required: true },
49
+ {
50
+ name: 'approverGroups',
51
+ type: 'select',
52
+ hasMany: true,
53
+ required: true,
54
+ options: options.groupSlugs.map((s) => ({ label: s, value: s })),
55
+ },
56
+ // Decision
57
+ {
58
+ name: 'status',
59
+ type: 'select',
60
+ required: true,
61
+ defaultValue: 'pending',
62
+ options: [
63
+ { label: 'Pending', value: 'pending' },
64
+ { label: 'Granted', value: 'granted' },
65
+ { label: 'Declined', value: 'declined' },
66
+ { label: 'Changes requested', value: 'changes-requested' },
67
+ { label: 'Expired', value: 'expired' },
68
+ ],
69
+ },
70
+ { name: 'decidedBy', type: 'relationship', relationTo: usersSlug },
71
+ { name: 'decidedAt', type: 'date' },
72
+ { name: 'decisionNotes', type: 'textarea' },
73
+ // Workflow state
74
+ {
75
+ name: 'notifiedApprovers',
76
+ type: 'json',
77
+ defaultValue: [],
78
+ admin: { description: 'IDs of users notified for this request.' },
79
+ },
80
+ { name: 'expiresAt', type: 'date', required: true },
81
+ {
82
+ name: 'consumedTokens',
83
+ type: 'json',
84
+ defaultValue: [],
85
+ admin: { description: 'Action tokens already consumed; prevents replay.' },
86
+ },
87
+ ],
88
+ indexes: [
89
+ { fields: ['targetCollection', 'targetId', 'status'] },
90
+ { fields: ['status', 'expiresAt'] },
91
+ { fields: ['requestedBy', 'requestedAt'] },
92
+ ],
93
+ };
94
+ }
95
+ //# sourceMappingURL=collection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection.js","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,sBAAsB,GAAG,WAAW,CAAA;AAEjD;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CACvC,OAAyC;IAEzC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,sBAAsB,CAAA;IACnD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,OAAO,CAAA;IAE9C,OAAO;QACL,IAAI;QACJ,KAAK,EAAE;YACL,UAAU,EAAE,aAAa;YACzB,cAAc,EAAE,CAAC,aAAa,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,CAAC;YACpF,WAAW,EACT,+HAA+H;SAClI;QACD,MAAM,EAAE;YACN,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;gBAChB,MAAM,KAAK,GAAI,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAA0B,IAAI,EAAE,CAAA;gBACjE,OAAO,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;YAC1F,CAAC;YACD,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK;YACnB,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAClB,CAAE,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAA0B,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;YACzE,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK;SACpB;QACD,MAAM,EAAE;YACN,SAAS;YACT,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;YAC1D,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;YAClD,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;YACrD;gBACE,IAAI,EAAE,eAAe;gBACrB,IAAI,EAAE,MAAM;gBACZ,QAAQ,EAAE,IAAI;gBACd,KAAK,EAAE,EAAE,WAAW,EAAE,uEAAuE,EAAE;aAChG;YACD,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE;YAEpC,UAAU;YACV,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE;YACpF;gBACE,IAAI,EAAE,aAAa;gBACnB,IAAI,EAAE,MAAM;gBACZ,QAAQ,EAAE,IAAI;gBACd,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aAC7C;YACD,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,UAAU,EAAE;YAC3C,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE;YAC5D;gBACE,IAAI,EAAE,gBAAgB;gBACtB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,IAAI;gBACb,QAAQ,EAAE,IAAI;gBACd,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;aACjE;YAED,WAAW;YACX;gBACE,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,QAAQ;gBACd,QAAQ,EAAE,IAAI;gBACd,YAAY,EAAE,SAAS;gBACvB,OAAO,EAAE;oBACP,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;oBACtC,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;oBACtC,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE;oBACxC,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,mBAAmB,EAAE;oBAC1D,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;iBACvC;aACF;YACD,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE;YAClE,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE;YACnC,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,UAAU,EAAE;YAE3C,iBAAiB;YACjB;gBACE,IAAI,EAAE,mBAAmB;gBACzB,IAAI,EAAE,MAAM;gBACZ,YAAY,EAAE,EAAE;gBAChB,KAAK,EAAE,EAAE,WAAW,EAAE,yCAAyC,EAAE;aAClE;YACD,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;YACnD;gBACE,IAAI,EAAE,gBAAgB;gBACtB,IAAI,EAAE,MAAM;gBACZ,YAAY,EAAE,EAAE;gBAChB,KAAK,EAAE,EAAE,WAAW,EAAE,kDAAkD,EAAE;aAC3E;SACF;QACD,OAAO,EAAE;YACP,EAAE,MAAM,EAAE,CAAC,kBAAkB,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE;YACtD,EAAE,MAAM,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,EAAE;YACnC,EAAE,MAAM,EAAE,CAAC,aAAa,EAAE,aAAa,CAAC,EAAE;SAC3C;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { Endpoint, Payload } from 'payload';
2
+ import type { AuditWriter } from '@forumone/throughline-core';
3
+ import type { ApprovalsPluginOptions } from '../options.js';
4
+ import { type VerifyResult } from '../tokens.js';
5
+ export interface CreateActionEndpointDeps {
6
+ options: ApprovalsPluginOptions & {
7
+ tokenSecret: string;
8
+ };
9
+ auditWriter: AuditWriter;
10
+ }
11
+ /**
12
+ * Builds the GET endpoint that handles inline action links from emails.
13
+ * The flow is:
14
+ * - First request: render a confirmation page so a single misclicked
15
+ * link can't act on the approval.
16
+ * - Second request (with `confirm=true`): verify the token wasn't
17
+ * already consumed and apply the decision.
18
+ *
19
+ * Uses HTML responses (intentionally minimal styling — clients can drop
20
+ * a custom-branded endpoint in front if they want).
21
+ */
22
+ export declare function createActionEndpoint(deps: CreateActionEndpointDeps): Endpoint;
23
+ /** Lower-level helper exported for test consumers and custom action endpoints. */
24
+ export type ActionVerification = VerifyResult;
25
+ export declare function previewVerification(token: string, payload: Payload, options: ApprovalsPluginOptions & {
26
+ tokenSecret: string;
27
+ }): Promise<{
28
+ verification: VerifyResult;
29
+ approval: Record<string, unknown> | null;
30
+ }>;
31
+ //# sourceMappingURL=action.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../../src/endpoints/action.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAC7D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAE3D,OAAO,EAEL,KAAK,YAAY,EAElB,MAAM,cAAc,CAAA;AAErB,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,sBAAsB,GAAG;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;IACzD,WAAW,EAAE,WAAW,CAAA;CACzB;AAqBD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,wBAAwB,GAAG,QAAQ,CA+H7E;AAED,kFAAkF;AAClF,MAAM,MAAM,kBAAkB,GAAG,YAAY,CAAA;AAE7C,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,sBAAsB,GAAG;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GACxD,OAAO,CAAC;IAAE,YAAY,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAAE,CAAC,CAQnF"}
@@ -0,0 +1,205 @@
1
+ import { DEFAULT_APPROVALS_SLUG } from '../collection.js';
2
+ import { verifyActionToken, } from '../tokens.js';
3
+ const DECISION_STATUS_MAP = {
4
+ approve: 'granted',
5
+ decline: 'declined',
6
+ changes: 'changes-requested',
7
+ };
8
+ const DECISION_AUDIT_MAP = {
9
+ approve: 'approval.granted',
10
+ decline: 'approval.declined',
11
+ changes: 'approval.changes_requested',
12
+ };
13
+ const ACTION_LABEL = {
14
+ approve: 'Approve',
15
+ decline: 'Decline',
16
+ changes: 'Request changes',
17
+ discuss: 'Start discussion',
18
+ };
19
+ /**
20
+ * Builds the GET endpoint that handles inline action links from emails.
21
+ * The flow is:
22
+ * - First request: render a confirmation page so a single misclicked
23
+ * link can't act on the approval.
24
+ * - Second request (with `confirm=true`): verify the token wasn't
25
+ * already consumed and apply the decision.
26
+ *
27
+ * Uses HTML responses (intentionally minimal styling — clients can drop
28
+ * a custom-branded endpoint in front if they want).
29
+ */
30
+ export function createActionEndpoint(deps) {
31
+ return {
32
+ path: '/approvals/action',
33
+ method: 'get',
34
+ handler: async (req) => {
35
+ const url = new URL(req.url ?? 'http://localhost');
36
+ const token = url.searchParams.get('token');
37
+ if (!token)
38
+ return htmlResponse(renderError('Missing action token'), 400);
39
+ const verification = await verifyActionToken(token, deps.options.tokenSecret);
40
+ if (!verification.ok) {
41
+ return htmlResponse(renderError(verification.error), 401);
42
+ }
43
+ const collectionSlug = deps.options.collectionSlug ?? DEFAULT_APPROVALS_SLUG;
44
+ const approval = (await req.payload.findByID({
45
+ collection: collectionSlug,
46
+ id: verification.token.approvalId,
47
+ }));
48
+ if (!approval)
49
+ return htmlResponse(renderError('Approval not found'), 404);
50
+ const status = String(approval['status']);
51
+ if (status !== 'pending') {
52
+ return htmlResponse(renderInfo(`This request was already ${status}.`));
53
+ }
54
+ const consumed = approval['consumedTokens'] ?? [];
55
+ if (consumed.includes(token)) {
56
+ return htmlResponse(renderError('This action link has already been used'), 400);
57
+ }
58
+ const expiresAtRaw = approval['expiresAt'];
59
+ if (typeof expiresAtRaw === 'string' &&
60
+ Date.parse(expiresAtRaw) < Date.now()) {
61
+ return htmlResponse(renderInfo('This approval request has expired.'));
62
+ }
63
+ const action = verification.token.action;
64
+ const confirm = url.searchParams.get('confirm') === 'true';
65
+ if (!confirm) {
66
+ return htmlResponse(renderConfirmation({
67
+ token,
68
+ action,
69
+ targetTitle: stringOrFallback(approval['targetTitle'], 'this document'),
70
+ changesSummary: stringOrFallback(approval['changesSummary'], ''),
71
+ }));
72
+ }
73
+ if (action === 'discuss') {
74
+ // Discuss isn't a decision — surface a message and don't mutate state.
75
+ // C11 listens for the 'approval/discussed' event to thread it into
76
+ // the email reply UX once that lands.
77
+ await deps.options.inngest.send({
78
+ name: 'approval/discussed',
79
+ data: {
80
+ approvalId: verification.token.approvalId,
81
+ approverId: verification.token.approverId,
82
+ },
83
+ });
84
+ await deps.auditWriter({
85
+ actor: { type: 'user', userId: verification.token.approverId, apiKeyName: 'action-token' },
86
+ action: 'approval.discussed',
87
+ mcpServer: 'approvals',
88
+ mcpTool: 'action-endpoint',
89
+ targetCollection: String(approval['targetCollection']),
90
+ targetId: String(approval['targetId']),
91
+ approvalRequestId: verification.token.approvalId,
92
+ success: true,
93
+ });
94
+ return htmlResponse(renderInfo('Thanks — a discussion thread will follow up by email.'));
95
+ }
96
+ const newStatus = DECISION_STATUS_MAP[action];
97
+ const decidedAt = new Date().toISOString();
98
+ await req.payload.update({
99
+ collection: collectionSlug,
100
+ id: verification.token.approvalId,
101
+ data: {
102
+ status: newStatus,
103
+ decidedBy: verification.token.approverId,
104
+ decidedAt,
105
+ consumedTokens: [...consumed, token],
106
+ },
107
+ });
108
+ await deps.options.inngest.send({
109
+ name: 'approval/decided',
110
+ data: {
111
+ approvalId: verification.token.approvalId,
112
+ decision: newStatus,
113
+ decidedBy: verification.token.approverId,
114
+ decidedAt,
115
+ targetCollection: String(approval['targetCollection']),
116
+ targetId: String(approval['targetId']),
117
+ },
118
+ });
119
+ await deps.auditWriter({
120
+ actor: {
121
+ type: 'user',
122
+ userId: verification.token.approverId,
123
+ apiKeyName: 'action-token',
124
+ },
125
+ action: DECISION_AUDIT_MAP[action],
126
+ mcpServer: 'approvals',
127
+ mcpTool: 'action-endpoint',
128
+ targetCollection: String(approval['targetCollection']),
129
+ targetId: String(approval['targetId']),
130
+ targetTitle: stringOrFallback(approval['targetTitle'], String(approval['targetId'])),
131
+ approvalRequestId: verification.token.approvalId,
132
+ success: true,
133
+ });
134
+ return htmlResponse(renderSuccess(ACTION_LABEL[action], stringOrFallback(approval['targetTitle'], 'this document')));
135
+ },
136
+ };
137
+ }
138
+ export async function previewVerification(token, payload, options) {
139
+ const verification = await verifyActionToken(token, options.tokenSecret);
140
+ if (!verification.ok)
141
+ return { verification, approval: null };
142
+ const approval = (await payload.findByID({
143
+ collection: options.collectionSlug ?? DEFAULT_APPROVALS_SLUG,
144
+ id: verification.token.approvalId,
145
+ }));
146
+ return { verification, approval };
147
+ }
148
+ function htmlResponse(html, status = 200) {
149
+ return new Response(html, {
150
+ status,
151
+ headers: { 'content-type': 'text/html; charset=utf-8' },
152
+ });
153
+ }
154
+ function renderConfirmation(args) {
155
+ const label = ACTION_LABEL[args.action];
156
+ const summary = args.changesSummary
157
+ ? `<p><strong>What's changing:</strong> ${escape(args.changesSummary)}</p>`
158
+ : '';
159
+ return htmlPage(`
160
+ <h1>${escape(label)}</h1>
161
+ <p>You're about to <strong>${escape(label.toLowerCase())}</strong> the request for <em>${escape(args.targetTitle)}</em>.</p>
162
+ ${summary}
163
+ <form method="GET">
164
+ <input type="hidden" name="token" value="${escape(args.token)}" />
165
+ <input type="hidden" name="confirm" value="true" />
166
+ <button type="submit">Confirm: ${escape(label)}</button>
167
+ </form>
168
+ `);
169
+ }
170
+ function renderSuccess(label, title) {
171
+ return htmlPage(`<h1>${escape(label)}</h1><p>Your decision on <em>${escape(title)}</em> has been recorded.</p>`);
172
+ }
173
+ function renderError(message) {
174
+ return htmlPage(`<h1>Action could not be completed</h1><p>${escape(message)}</p>`);
175
+ }
176
+ function renderInfo(message) {
177
+ return htmlPage(`<h1>Notice</h1><p>${escape(message)}</p>`);
178
+ }
179
+ function htmlPage(body) {
180
+ return `<!DOCTYPE html>
181
+ <html lang="en">
182
+ <head>
183
+ <meta charset="utf-8" />
184
+ <title>Approval action</title>
185
+ <style>
186
+ body { font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; max-width: 600px; margin: 3rem auto; padding: 0 1rem; color: #18181b; line-height: 1.6; }
187
+ h1 { font-size: 1.5rem; }
188
+ button { background: #18181b; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 0.25rem; font-size: 1rem; cursor: pointer; }
189
+ em { font-style: italic; }
190
+ </style>
191
+ </head>
192
+ <body>${body}</body>
193
+ </html>`;
194
+ }
195
+ function escape(text) {
196
+ return String(text ?? '')
197
+ .replace(/&/g, '&amp;')
198
+ .replace(/</g, '&lt;')
199
+ .replace(/>/g, '&gt;')
200
+ .replace(/"/g, '&quot;');
201
+ }
202
+ function stringOrFallback(value, fallback) {
203
+ return typeof value === 'string' ? value : fallback;
204
+ }
205
+ //# sourceMappingURL=action.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action.js","sourceRoot":"","sources":["../../src/endpoints/action.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export { approvalsPlugin } from './plugin.js';
2
+ export type { ApprovalsPluginOptions, ApproverGroup, GroupResolver, ResolvedApprover, } from './options.js';
3
+ export { DEFAULT_APPROVALS_SLUG, createApprovalsCollection, } from './collection.js';
4
+ export type { CreateApprovalsCollectionOptions } from './collection.js';
5
+ export { APPROVALS_RESOLVER_SYMBOL, attachApprovalResolver, createApprovalResolver, } from './resolver.js';
6
+ export type { CreateApprovalResolverOptions } from './resolver.js';
7
+ export { createActionEndpoint } from './endpoints/action.js';
8
+ export type { CreateActionEndpointDeps } from './endpoints/action.js';
9
+ export { generateActionToken, verifyActionToken, buildActionUrl, } from './tokens.js';
10
+ export type { ActionToken, ActionTokenAction, VerifyOptions, VerifyResult, } from './tokens.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7C,YAAY,EACV,sBAAsB,EACtB,aAAa,EACb,aAAa,EACb,gBAAgB,GACjB,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAA;AACxB,YAAY,EAAE,gCAAgC,EAAE,MAAM,iBAAiB,CAAA;AAEvE,OAAO,EACL,yBAAyB,EACzB,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,eAAe,CAAA;AACtB,YAAY,EAAE,6BAA6B,EAAE,MAAM,eAAe,CAAA;AAElE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAC5D,YAAY,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAErE,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,GACf,MAAM,aAAa,CAAA;AACpB,YAAY,EACV,WAAW,EACX,iBAAiB,EACjB,aAAa,EACb,YAAY,GACb,MAAM,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { approvalsPlugin } from './plugin.js';
2
+ export { DEFAULT_APPROVALS_SLUG, createApprovalsCollection, } from './collection.js';
3
+ export { APPROVALS_RESOLVER_SYMBOL, attachApprovalResolver, createApprovalResolver, } from './resolver.js';
4
+ export { createActionEndpoint } from './endpoints/action.js';
5
+ export { generateActionToken, verifyActionToken, buildActionUrl, } from './tokens.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAS7C,OAAO,EACL,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EACL,yBAAyB,EACzB,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,eAAe,CAAA;AAGtB,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAG5D,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,GACf,MAAM,aAAa,CAAA"}
@@ -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