@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
@@ -0,0 +1,62 @@
1
+ import { z } from 'zod';
2
+ import { withMeta } from '@forumone/throughline-core';
3
+ import { DEFAULT_APPROVALS_SLUG } from '../collection.js';
4
+ export function createListPendingApprovalsTool(deps) {
5
+ const inputSchema = withMeta({
6
+ limit: z
7
+ .number()
8
+ .int()
9
+ .positive()
10
+ .max(100)
11
+ .optional()
12
+ .describe('Max results. Default: 25'),
13
+ });
14
+ return {
15
+ name: 'list_pending_approvals',
16
+ description: "Returns pending approval requests routed to one of the caller's approver groups. Use to answer 'what do I need to review?'. Read-only.",
17
+ inputSchema,
18
+ handler: async (input, ctx) => {
19
+ if (!ctx.user)
20
+ return { error: 'Must be authenticated to list pending approvals' };
21
+ const userGroups = ctx.user.groups;
22
+ if (userGroups.length === 0)
23
+ return { pending: [] };
24
+ const result = await deps.payload.find({
25
+ collection: deps.options.collectionSlug ?? DEFAULT_APPROVALS_SLUG,
26
+ where: {
27
+ and: [
28
+ { status: { equals: 'pending' } },
29
+ { approverGroups: { in: userGroups } },
30
+ ],
31
+ },
32
+ limit: input.limit ?? 25,
33
+ sort: '-requestedAt',
34
+ });
35
+ const docs = result.docs;
36
+ return {
37
+ pending: docs.map((doc) => ({
38
+ approvalId: String(doc['id']),
39
+ targetCollection: doc['targetCollection'],
40
+ targetId: doc['targetId'],
41
+ targetTitle: doc['targetTitle'],
42
+ changesSummary: doc['changesSummary'],
43
+ requestedBy: unwrapRelationshipId(doc['requestedBy']),
44
+ requestedAt: doc['requestedAt'],
45
+ expiresAt: doc['expiresAt'],
46
+ previewUrl: doc['previewUrl'],
47
+ })),
48
+ };
49
+ },
50
+ };
51
+ }
52
+ function unwrapRelationshipId(value) {
53
+ if (value === null || value === undefined)
54
+ return null;
55
+ if (typeof value === 'string')
56
+ return value;
57
+ if (typeof value === 'object' && 'id' in value) {
58
+ return String(value.id);
59
+ }
60
+ return null;
61
+ }
62
+ //# sourceMappingURL=list-pending-approvals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-pending-approvals.js","sourceRoot":"","sources":["../../src/tools/list-pending-approvals.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,8BAA8B,CAC5C,IAA8B;IAE9B,MAAM,WAAW,GAAG,QAAQ,CAAC;QAC3B,KAAK,EAAE,CAAC;aACL,MAAM,EAAE;aACR,GAAG,EAAE;aACL,QAAQ,EAAE;aACV,GAAG,CAAC,GAAG,CAAC;aACR,QAAQ,EAAE;aACV,QAAQ,CAAC,0BAA0B,CAAC;KACxC,CAAC,CAAA;IAEF,OAAO;QACL,IAAI,EAAE,wBAAwB;QAC9B,WAAW,EACT,wIAAwI;QAC1I,WAAW;QACX,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAC5B,IAAI,CAAC,GAAG,CAAC,IAAI;gBAAE,OAAO,EAAE,KAAK,EAAE,iDAAiD,EAAE,CAAA;YAClF,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAA;YAClC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;YAEnD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;gBACrC,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,cAAc,IAAI,sBAAsB;gBACjE,KAAK,EAAE;oBACL,GAAG,EAAE;wBACH,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE;wBACjC,EAAE,cAAc,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE;qBACvC;iBACF;gBACD,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,EAAE;gBACxB,IAAI,EAAE,cAAc;aACrB,CAAC,CAAA;YAEF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAsC,CAAA;YAC1D,OAAO;gBACL,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;oBAC1B,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBAC7B,gBAAgB,EAAE,GAAG,CAAC,kBAAkB,CAAC;oBACzC,QAAQ,EAAE,GAAG,CAAC,UAAU,CAAC;oBACzB,WAAW,EAAE,GAAG,CAAC,aAAa,CAAC;oBAC/B,cAAc,EAAE,GAAG,CAAC,gBAAgB,CAAC;oBACrC,WAAW,EAAE,oBAAoB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;oBACrD,WAAW,EAAE,GAAG,CAAC,aAAa,CAAC;oBAC/B,SAAS,EAAE,GAAG,CAAC,WAAW,CAAC;oBAC3B,UAAU,EAAE,GAAG,CAAC,YAAY,CAAC;iBAC9B,CAAC,CAAC;aACJ,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,13 @@
1
+ import type { Payload } from 'payload';
2
+ import { type AuditWriter } from '@forumone/throughline-core';
3
+ import type { McpToolDefinition } from '@forumone/throughline-plugin-contract';
4
+ import type { ApprovalsPluginOptions } from '../options.js';
5
+ export interface RequestApprovalDeps {
6
+ payload: Payload;
7
+ options: ApprovalsPluginOptions & {
8
+ tokenSecret: string;
9
+ };
10
+ auditWriter: AuditWriter;
11
+ }
12
+ export declare function createRequestApprovalTool(deps: RequestApprovalDeps): McpToolDefinition;
13
+ //# sourceMappingURL=request-approval.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-approval.d.ts","sourceRoot":"","sources":["../../src/tools/request-approval.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,KAAK,WAAW,EAAY,MAAM,4BAA4B,CAAA;AACvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAA;AAE9E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAE3D,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,sBAAsB,GAAG;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;IACzD,WAAW,EAAE,WAAW,CAAA;CACzB;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,mBAAmB,GAAG,iBAAiB,CA8HtF"}
@@ -0,0 +1,118 @@
1
+ import { z } from 'zod';
2
+ import { withMeta } from '@forumone/throughline-core';
3
+ import { DEFAULT_APPROVALS_SLUG } from '../collection.js';
4
+ export function createRequestApprovalTool(deps) {
5
+ const inputSchema = withMeta({
6
+ collection: z.string(),
7
+ id: z.string(),
8
+ changesSummary: z
9
+ .string()
10
+ .min(20)
11
+ .describe('Clear description of what is changing and why, shown to approvers in notifications'),
12
+ requestReason: z
13
+ .string()
14
+ .optional()
15
+ .describe('Optional context about why the change is being made'),
16
+ approverGroups: z
17
+ .array(z.string())
18
+ .min(1)
19
+ .describe('Group slugs the request should be routed to'),
20
+ });
21
+ return {
22
+ name: 'request_approval',
23
+ description: "Kicks off the approval workflow for a document that requires approval before publishing. Provide a clear changesSummary explaining what changed and why; approvers see this in their notifications. Returns the approval ID, expiration time, and the list of approvers who were notified.",
24
+ inputSchema,
25
+ handler: async (input, ctx) => {
26
+ if (!ctx.user) {
27
+ return { error: 'Approval requests must be made by an authenticated user' };
28
+ }
29
+ const approverGroups = input.approverGroups;
30
+ const validGroupSlugs = new Set(deps.options.groups.map((g) => g.slug));
31
+ const unknownGroups = approverGroups.filter((g) => !validGroupSlugs.has(g));
32
+ if (unknownGroups.length > 0) {
33
+ return { error: `Unknown approver groups: ${unknownGroups.join(', ')}` };
34
+ }
35
+ const document = (await deps.payload.findByID({
36
+ collection: input.collection,
37
+ id: input.id,
38
+ draft: true,
39
+ }));
40
+ if (!document)
41
+ return { error: 'Document not found' };
42
+ const approvers = await deps.options.groupResolver.resolveUsers(approverGroups);
43
+ if (approvers.length === 0) {
44
+ return {
45
+ error: `No approvers found in groups: ${approverGroups.join(', ')}`,
46
+ };
47
+ }
48
+ const targetVersion = String(document['updatedAt'] ?? document['id'] ?? input.id);
49
+ const targetTitle = typeof document['title'] === 'string' ? document['title'] : input.id;
50
+ const slug = typeof document['slug'] === 'string' ? document['slug'] : input.id;
51
+ const previewUrl = buildPreviewUrl(deps.options, input.collection, slug);
52
+ const expiresAt = new Date(Date.now() + (deps.options.expirationDays ?? 7) * 24 * 60 * 60 * 1000).toISOString();
53
+ const created = (await deps.payload.create({
54
+ collection: deps.options.collectionSlug ?? DEFAULT_APPROVALS_SLUG,
55
+ data: {
56
+ targetCollection: input.collection,
57
+ targetId: input.id,
58
+ targetTitle,
59
+ targetVersion,
60
+ previewUrl,
61
+ requestedBy: ctx.user.id,
62
+ requestedAt: new Date().toISOString(),
63
+ ...(input.requestReason ? { requestReason: input.requestReason } : {}),
64
+ changesSummary: input.changesSummary,
65
+ approverGroups: approverGroups,
66
+ status: 'pending',
67
+ expiresAt,
68
+ notifiedApprovers: approvers.map((a) => a.id),
69
+ consumedTokens: [],
70
+ },
71
+ }));
72
+ const approvalId = String(created.id);
73
+ await deps.options.inngest.send({
74
+ name: 'approval/requested',
75
+ data: {
76
+ approvalId,
77
+ targetCollection: input.collection,
78
+ targetId: input.id,
79
+ targetTitle,
80
+ requestedBy: ctx.user.id,
81
+ approverIds: approvers.map((a) => a.id),
82
+ expiresAt,
83
+ },
84
+ });
85
+ await deps.auditWriter({
86
+ actor: {
87
+ type: 'user',
88
+ userId: ctx.user.id,
89
+ userName: ctx.user.name,
90
+ apiKeyName: ctx.apiKeyName,
91
+ },
92
+ action: 'approval.requested',
93
+ mcpServer: 'approvals',
94
+ mcpTool: 'request_approval',
95
+ targetCollection: input.collection,
96
+ targetId: input.id,
97
+ targetTitle,
98
+ prompt: input._meta?.userPrompt,
99
+ reasoning: input._meta?.reasoning,
100
+ changesSummary: input.changesSummary,
101
+ approvalRequestId: approvalId,
102
+ success: true,
103
+ });
104
+ return {
105
+ approvalId,
106
+ status: 'pending',
107
+ expiresAt,
108
+ approvers: approvers.map((a) => ({ id: a.id, name: a.name ?? a.email })),
109
+ previewUrl,
110
+ };
111
+ },
112
+ };
113
+ }
114
+ function buildPreviewUrl(options, collection, slug) {
115
+ const baseUrl = options.publicUrl ?? process.env['NEXT_PUBLIC_SERVER_URL'] ?? '';
116
+ return `${baseUrl.replace(/\/$/, '')}/api/preview?collection=${encodeURIComponent(collection)}&slug=${encodeURIComponent(slug)}`;
117
+ }
118
+ //# sourceMappingURL=request-approval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-approval.js","sourceRoot":"","sources":["../../src/tools/request-approval.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { Payload } from 'payload';
2
+ import { type AuditWriter } from '@forumone/throughline-core';
3
+ import type { McpToolDefinition } from '@forumone/throughline-plugin-contract';
4
+ import type { ApprovalsPluginOptions } from '../options.js';
5
+ export interface RespondToApprovalDeps {
6
+ payload: Payload;
7
+ options: ApprovalsPluginOptions & {
8
+ tokenSecret: string;
9
+ };
10
+ auditWriter: AuditWriter;
11
+ }
12
+ export declare function createRespondToApprovalTool(deps: RespondToApprovalDeps): McpToolDefinition;
13
+ //# sourceMappingURL=respond-to-approval.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"respond-to-approval.d.ts","sourceRoot":"","sources":["../../src/tools/respond-to-approval.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,KAAK,WAAW,EAAY,MAAM,4BAA4B,CAAA;AAEvE,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;IACzD,WAAW,EAAE,WAAW,CAAA;CACzB;AAcD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,qBAAqB,GAAG,iBAAiB,CAgG1F"}
@@ -0,0 +1,107 @@
1
+ import { z } from 'zod';
2
+ import { withMeta } from '@forumone/throughline-core';
3
+ import { DEFAULT_APPROVALS_SLUG } from '../collection.js';
4
+ const decisionToStatus = {
5
+ approve: 'granted',
6
+ decline: 'declined',
7
+ request_changes: 'changes-requested',
8
+ };
9
+ const decisionToAuditAction = {
10
+ approve: 'approval.granted',
11
+ decline: 'approval.declined',
12
+ request_changes: 'approval.changes_requested',
13
+ };
14
+ export function createRespondToApprovalTool(deps) {
15
+ const inputSchema = withMeta({
16
+ approvalId: z.string(),
17
+ decision: z.enum(['approve', 'decline', 'request_changes']),
18
+ notes: z
19
+ .string()
20
+ .optional()
21
+ .describe('Decision rationale, shown to the requester (optional but encouraged for declines)'),
22
+ });
23
+ return {
24
+ name: 'respond_to_approval',
25
+ description: "Records an approver's decision on a pending approval. Valid decisions: approve, decline, request_changes. Approvers can also act through the inline action links in their notification emails; this tool is for when they respond conversationally through Claude.",
26
+ inputSchema,
27
+ handler: async (input, ctx) => {
28
+ if (!ctx.user) {
29
+ return { error: 'Must be authenticated to respond to approvals' };
30
+ }
31
+ const collectionSlug = deps.options.collectionSlug ?? DEFAULT_APPROVALS_SLUG;
32
+ const approval = (await deps.payload.findByID({
33
+ collection: collectionSlug,
34
+ id: input.approvalId,
35
+ }));
36
+ if (!approval)
37
+ return { error: 'Approval not found' };
38
+ if (approval['status'] !== 'pending') {
39
+ return { error: `Approval is already ${String(approval['status'])}` };
40
+ }
41
+ const requesterId = unwrapRelationshipId(approval['requestedBy']);
42
+ if (requesterId === ctx.user.id) {
43
+ return { error: 'You cannot approve your own request' };
44
+ }
45
+ const approverGroups = approval['approverGroups'] ?? [];
46
+ const userGroups = ctx.user.groups;
47
+ const hasAccess = approverGroups.some((g) => userGroups.includes(g));
48
+ if (!hasAccess) {
49
+ return { error: 'You are not in an approver group for this request' };
50
+ }
51
+ const decision = input.decision;
52
+ const newStatus = decisionToStatus[decision];
53
+ const decidedAt = new Date().toISOString();
54
+ await deps.payload.update({
55
+ collection: collectionSlug,
56
+ id: input.approvalId,
57
+ data: {
58
+ status: newStatus,
59
+ decidedBy: ctx.user.id,
60
+ decidedAt,
61
+ ...(input.notes ? { decisionNotes: input.notes } : {}),
62
+ },
63
+ });
64
+ await deps.options.inngest.send({
65
+ name: 'approval/decided',
66
+ data: {
67
+ approvalId: input.approvalId,
68
+ decision: newStatus,
69
+ decidedBy: ctx.user.id,
70
+ decidedAt,
71
+ targetCollection: String(approval['targetCollection']),
72
+ targetId: String(approval['targetId']),
73
+ },
74
+ });
75
+ await deps.auditWriter({
76
+ actor: {
77
+ type: 'user',
78
+ userId: ctx.user.id,
79
+ userName: ctx.user.name,
80
+ apiKeyName: ctx.apiKeyName,
81
+ },
82
+ action: decisionToAuditAction[decision],
83
+ mcpServer: 'approvals',
84
+ mcpTool: 'respond_to_approval',
85
+ targetCollection: String(approval['targetCollection']),
86
+ targetId: String(approval['targetId']),
87
+ targetTitle: typeof approval['targetTitle'] === 'string'
88
+ ? approval['targetTitle']
89
+ : String(approval['targetId']),
90
+ prompt: input._meta?.userPrompt,
91
+ reasoning: input.notes ?? input._meta?.reasoning,
92
+ approvalRequestId: input.approvalId,
93
+ success: true,
94
+ });
95
+ return { success: true, status: newStatus, decidedAt };
96
+ },
97
+ };
98
+ }
99
+ function unwrapRelationshipId(value) {
100
+ if (typeof value === 'string')
101
+ return value;
102
+ if (value && typeof value === 'object' && 'id' in value) {
103
+ return String(value.id);
104
+ }
105
+ return null;
106
+ }
107
+ //# sourceMappingURL=respond-to-approval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"respond-to-approval.js","sourceRoot":"","sources":["../../src/tools/respond-to-approval.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,10 +1,67 @@
1
1
  {
2
2
  "name": "@forumone/throughline-approvals",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @forumone/throughline-approvals",
3
+ "version": "0.2.0",
4
+ "description": "Conversational approval workflow server for Throughline. Provides the approval resolver the publishing server consumes plus MCP tools for requesting and responding to approvals.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "CHANGELOG.md"
18
+ ],
5
19
  "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
20
+ "throughline",
21
+ "payload",
22
+ "mcp",
23
+ "approvals",
24
+ "workflow"
25
+ ],
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/forumone/throughline.git",
30
+ "directory": "packages/approvals"
31
+ },
32
+ "homepage": "https://github.com/forumone/throughline/tree/main/packages/approvals#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/forumone/throughline/issues"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "peerDependencies": {
40
+ "payload": "^3.0.0",
41
+ "inngest": "^4.0.0"
42
+ },
43
+ "dependencies": {
44
+ "zod": "^3.23.0",
45
+ "@forumone/throughline-core": "0.2.0",
46
+ "@forumone/throughline-plugin-contract": "0.0.0",
47
+ "@forumone/throughline-publishing": "0.2.1"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.17.0",
51
+ "eslint": "^9.15.0",
52
+ "inngest": "^4.2.0",
53
+ "payload": "^3.83.0",
54
+ "typescript": "^5.6.0",
55
+ "vitest": "^2.1.0",
56
+ "@forumone/throughline-eslint-config": "0.0.0",
57
+ "@forumone/throughline-tsconfig": "0.0.0"
58
+ },
59
+ "scripts": {
60
+ "build": "tsc -b",
61
+ "dev": "tsc -b -w",
62
+ "clean": "rm -rf dist .turbo *.tsbuildinfo",
63
+ "typecheck": "tsc --noEmit",
64
+ "lint": "eslint src",
65
+ "test": "vitest run"
66
+ }
67
+ }