@forumone/throughline-workflows 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # @forumone/throughline-workflows
2
+
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [a4b5108]
8
+ - @forumone/throughline-core@0.2.1
9
+
10
+ ## 0.2.0
11
+
12
+ ### Minor Changes
13
+
14
+ - 28f5af4: Initial release of the workflows package. Five composable Inngest function factories for the common async work in the framework: `createRevalidateOnPublishFunction` (Next.js cache invalidation on publish), `createExecuteScheduledPublishesFunction` (cron-driven scheduled publishes that go through the Publishing Server's MCP for full pipeline coverage), `createExpireStaleApprovalsFunction` (daily approval expiration with audit + `approval/expired` event), `createAuditEventEchoFunction` (fan-out for approval lifecycle plus pluggable custom handlers), `createHealthcheckFunction` (configurable checks with `onFailure` routing and a `system/healthcheck` heartbeat). Plus reusable `createPayloadReachableCheck` and `createManifestReachableCheck` helpers. No Payload plugin — factories only; client apps merge the functions into their Inngest endpoint. `next` is an optional peer dependency.
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,140 @@
1
1
  # @forumone/throughline-workflows
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ Composable Inngest function factories for the Throughline framework. Client apps import the factories they need and merge the functions into their Inngest endpoint.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ This package has **no Payload plugin** it exports factories only. Workflows subscribe to events the server packages emit; they don't modify Payload configuration.
6
6
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
7
+ ## What this package provides
8
8
 
9
- ## Purpose
10
-
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@forumone/throughline-workflows`
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**
9
+ | Factory | What it does |
10
+ |---|---|
11
+ | `createRevalidateOnPublishFunction` | Invalidates Next.js caches for the page, listings, and sitemap when content publishes |
12
+ | `createExecuteScheduledPublishesFunction` | Cron-driven scheduled publish executor that calls through the Publishing Server's MCP |
13
+ | `createExpireStaleApprovalsFunction` | Daily cron that flips pending approvals past `expiresAt` to `expired` and fires an `approval/expired` event |
14
+ | `createAuditEventEchoFunction` | Fan-out point: turns `audit/event.recorded` into `notification/send-approval-*` events plus custom handlers |
15
+ | `createHealthcheckFunction` | Periodic health monitoring with configurable checks |
16
+
17
+ Plus two reusable check helpers used with the healthcheck factory:
18
+
19
+ - `createPayloadReachableCheck(slug?)` — verifies Payload can `find` from a collection
20
+ - `createManifestReachableCheck(url)` — verifies an HTTPS manifest URL responds 2xx
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pnpm add @forumone/throughline-workflows
26
+ ```
27
+
28
+ Peers: `payload@^3.0.0`, `inngest@^4.0.0`. `next` is an **optional** peer — install it only if you use `createRevalidateOnPublishFunction` with the default revalidator.
29
+
30
+ ## Usage
31
+
32
+ In your Next.js app's Inngest endpoint:
33
+
34
+ ```typescript
35
+ // src/app/api/inngest/route.ts
36
+ import { serve } from 'inngest/next'
37
+ import { getPayload } from 'payload'
38
+ import config from '@payload-config'
39
+ import { inngest } from '@/lib/inngest'
40
+ import {
41
+ createRevalidateOnPublishFunction,
42
+ createExecuteScheduledPublishesFunction,
43
+ createExpireStaleApprovalsFunction,
44
+ createAuditEventEchoFunction,
45
+ createHealthcheckFunction,
46
+ createPayloadReachableCheck,
47
+ } from '@forumone/throughline-workflows'
48
+
49
+ const payload = await getPayload({ config })
50
+
51
+ export const { GET, POST, PUT } = serve({
52
+ client: inngest,
53
+ functions: [
54
+ createRevalidateOnPublishFunction({ inngest, payload }),
55
+ createExecuteScheduledPublishesFunction({
56
+ inngest,
57
+ payload,
58
+ collections: [{ slug: 'pages' }, { slug: 'posts' }],
59
+ publishingServerUrl: process.env.NEXT_PUBLIC_SERVER_URL!,
60
+ // PUBLISHING_SYSTEM_API_KEY env var carries the api key
61
+ }),
62
+ createExpireStaleApprovalsFunction({ inngest, payload }),
63
+ createAuditEventEchoFunction({ inngest }),
64
+ createHealthcheckFunction({
65
+ inngest,
66
+ payload,
67
+ checks: [createPayloadReachableCheck()],
68
+ }),
69
+ ],
70
+ })
71
+ ```
72
+
73
+ ## Why this lives outside the server packages
74
+
75
+ By C10, every server package fires Inngest events when consequential things happen. This package is the **subscriber side** — the functions that react to those events. Splitting the subscriber side from the publisher packages keeps the server packages from accumulating "and also runs this revalidation logic" appendices, and it lets clients pick which workflows they want without dragging in everything.
76
+
77
+ The factories-only shape (no Payload plugin) is the same reasoning. A workflow that wants to revalidate Next.js pages doesn't need to register a Payload collection. A scheduled-publish executor doesn't need a Payload hook. They're cron / event handlers that happen to read from Payload.
78
+
79
+ ## Non-Next.js frontends
80
+
81
+ The default `revalidate` function dynamically imports `next/cache` and is a no-op anywhere `revalidatePath` / `revalidateTag` aren't available. For other frameworks supply your own:
82
+
83
+ ```typescript
84
+ createRevalidateOnPublishFunction({
85
+ inngest,
86
+ payload,
87
+ revalidate: async ({ path, tags }) => {
88
+ if (path) await myCdn.purge(path)
89
+ for (const tag of tags) await myCdn.purgeTag(tag)
90
+ },
91
+ })
92
+ ```
93
+
94
+ `next` is declared as an optional peer (`peerDependenciesMeta.next.optional = true`) so non-Next consumers don't see a missing-peer warning.
95
+
96
+ ## Why scheduled publishes call through the MCP
97
+
98
+ `createExecuteScheduledPublishesFunction` calls `POST /api/publishing/mcp` with a Bearer token rather than calling `payload.update` directly. That means scheduled publishes go through the same composition / accessibility / approval pipeline as Claude-initiated publishes — a composition error in a scheduled publish gets the same treatment as one caught interactively.
99
+
100
+ A composition error fails the publish loudly (audit log + `lastError`) instead of silently writing through.
101
+
102
+ ## Customizing audit fan-out
103
+
104
+ The audit echo function wires the approval workflow by default. Add custom handlers for any other action:
105
+
106
+ ```typescript
107
+ createAuditEventEchoFunction({
108
+ inngest,
109
+ handlers: [
110
+ {
111
+ match: (e) => e.action === 'integration.failed',
112
+ handle: async (e) => {
113
+ await inngest.send({
114
+ name: 'notification/send-alert',
115
+ data: { severity: 'error', summary: 'Integration failure', ...e.data },
116
+ })
117
+ },
118
+ },
119
+ ],
120
+ })
121
+ ```
122
+
123
+ Each handler runs in its own `step.run`, so failures isolate.
124
+
125
+ ## Options reference
126
+
127
+ Every factory takes a typed options object. See `src/types.ts` for the full surface — defaults documented there:
128
+
129
+ - `RevalidateOnPublishOptions` — `revalidate?`, `urlBuilders?`, `collectionTags?`, `id?`
130
+ - `ExecuteScheduledPublishesOptions` — `collections[]`, `publishingServerUrl`, `publishingApiKey?` (env fallback), `schedule?` (default `*/5 * * * *`), `id?`
131
+ - `ExpireStaleApprovalsOptions` — `collectionSlug?` (default `approvals`), `schedule?` (default `0 2 * * *`), `id?`
132
+ - `AuditEventEchoOptions` — `handlers?`, `id?`
133
+ - `HealthcheckOptions` — `checks[]`, `schedule?` (default `*/15 * * * *`), `onFailure?`, `id?`
134
+
135
+ ## Related packages
136
+
137
+ - `@forumone/throughline-core` — required peer; provides the audit writer the approval-expiration cron uses
138
+ - `@forumone/throughline-publishing` — emits the publishing events the revalidation cron subscribes to
139
+ - `@forumone/throughline-approvals` — owns the approvals collection the expiration cron reads
140
+ - `@forumone/throughline-email` (C11) — will subscribe to `notification/send-approval-*`
@@ -0,0 +1,14 @@
1
+ import type { InngestFunction } from 'inngest';
2
+ import type { AuditEventEchoOptions } from './types.js';
3
+ /**
4
+ * Subscribes to `audit/event.recorded` and fans out to notification
5
+ * workflows. The built-in handlers cover the approval lifecycle (which
6
+ * the email package in C11 subscribes to); custom handlers in
7
+ * `options.handlers` add other fan-outs (e.g. "post integration.failed
8
+ * to a #alerts Slack channel").
9
+ *
10
+ * Each handler runs in its own `step.run` so a failure in one fan-out
11
+ * does not affect the others.
12
+ */
13
+ export declare function createAuditEventEchoFunction(options: AuditEventEchoOptions): InngestFunction.Any;
14
+ //# sourceMappingURL=audit-event-echo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-event-echo.d.ts","sourceRoot":"","sources":["../src/audit-event-echo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAiBvD;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,qBAAqB,GAAG,eAAe,CAAC,GAAG,CA0ChG"}
@@ -0,0 +1,53 @@
1
+ const APPROVAL_DECISION_ACTIONS = new Set([
2
+ 'approval.granted',
3
+ 'approval.declined',
4
+ 'approval.changes_requested',
5
+ ]);
6
+ /**
7
+ * Subscribes to `audit/event.recorded` and fans out to notification
8
+ * workflows. The built-in handlers cover the approval lifecycle (which
9
+ * the email package in C11 subscribes to); custom handlers in
10
+ * `options.handlers` add other fan-outs (e.g. "post integration.failed
11
+ * to a #alerts Slack channel").
12
+ *
13
+ * Each handler runs in its own `step.run` so a failure in one fan-out
14
+ * does not affect the others.
15
+ */
16
+ export function createAuditEventEchoFunction(options) {
17
+ return options.inngest.createFunction({
18
+ id: options.id ?? 'audit-event-echo',
19
+ triggers: [{ event: 'audit/event.recorded' }],
20
+ }, async ({ event, step, logger }) => {
21
+ const data = (event.data ?? {});
22
+ logger.info('Audit event echoed', { action: data.action });
23
+ await step.run('handle-approval-requested', async () => {
24
+ if (data.action === 'approval.requested' && data.approvalRequestId) {
25
+ await options.inngest.send({
26
+ name: 'notification/send-approval-request',
27
+ data: { approvalId: data.approvalRequestId },
28
+ });
29
+ }
30
+ });
31
+ await step.run('handle-approval-decided', async () => {
32
+ if (APPROVAL_DECISION_ACTIONS.has(data.action) && data.approvalRequestId) {
33
+ await options.inngest.send({
34
+ name: 'notification/send-approval-decision',
35
+ data: { approvalId: data.approvalRequestId, decision: data.action },
36
+ });
37
+ }
38
+ });
39
+ const handlers = options.handlers ?? [];
40
+ for (let index = 0; index < handlers.length; index += 1) {
41
+ const handler = handlers[index];
42
+ await step.run(`custom-handler-${index}`, async () => {
43
+ if (handler.match({ action: data.action })) {
44
+ await handler.handle({
45
+ action: data.action,
46
+ data: data,
47
+ });
48
+ }
49
+ });
50
+ }
51
+ });
52
+ }
53
+ //# sourceMappingURL=audit-event-echo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-event-echo.js","sourceRoot":"","sources":["../src/audit-event-echo.ts"],"names":[],"mappings":"AAYA,MAAM,yBAAyB,GAAG,IAAI,GAAG,CAAC;IACxC,kBAAkB;IAClB,mBAAmB;IACnB,4BAA4B;CAC7B,CAAC,CAAA;AAEF;;;;;;;;;GASG;AACH,MAAM,UAAU,4BAA4B,CAAC,OAA8B;IACzE,OAAO,OAAO,CAAC,OAAO,CAAC,cAAc,CACnC;QACE,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,kBAAkB;QACpC,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC;KAC9C,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE;QAChC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,CAAsB,CAAA;QACpD,MAAM,CAAC,IAAI,CAAC,oBAAoB,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;QAE1D,MAAM,IAAI,CAAC,GAAG,CAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;YACrD,IAAI,IAAI,CAAC,MAAM,KAAK,oBAAoB,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBACnE,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;oBACzB,IAAI,EAAE,oCAAoC;oBAC1C,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,iBAAiB,EAAE;iBAC7C,CAAC,CAAA;YACJ,CAAC;QACH,CAAC,CAAC,CAAA;QAEF,MAAM,IAAI,CAAC,GAAG,CAAC,yBAAyB,EAAE,KAAK,IAAI,EAAE;YACnD,IAAI,yBAAyB,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBACzE,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;oBACzB,IAAI,EAAE,qCAAqC;oBAC3C,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,iBAAiB,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE;iBACpE,CAAC,CAAA;YACJ,CAAC;QACH,CAAC,CAAC,CAAA;QAEF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAA;QACvC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;YACxD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAE,CAAA;YAChC,MAAM,IAAI,CAAC,GAAG,CAAC,kBAAkB,KAAK,EAAE,EAAE,KAAK,IAAI,EAAE;gBACnD,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;oBAC3C,MAAM,OAAO,CAAC,MAAM,CAAC;wBACnB,MAAM,EAAE,IAAI,CAAC,MAAM;wBACnB,IAAI,EAAE,IAA0C;qBACjD,CAAC,CAAA;gBACJ,CAAC;YACH,CAAC,CAAC,CAAA;QACJ,CAAC;IACH,CAAC,CACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { InngestFunction } from 'inngest';
2
+ import type { ExecuteScheduledPublishesOptions } from './types.js';
3
+ /**
4
+ * Cron-driven scheduled publish executor. Every tick (default: every 5
5
+ * minutes) the function looks for documents in any of the configured
6
+ * collections whose `_status` is still `draft` and whose
7
+ * `scheduledPublishAt` has passed. For each, it calls the publishing
8
+ * server's MCP `publish` tool — going through the full publishing pipeline
9
+ * means scheduled publishes get the same composition / accessibility /
10
+ * approval checks as interactive publishes.
11
+ *
12
+ * The `publishingApiKey` (or `PUBLISHING_SYSTEM_API_KEY` env var) must
13
+ * carry `publishing.execute` scope. The factory throws at construction if
14
+ * neither is present so misconfigurations surface immediately.
15
+ *
16
+ * Failures from the publishing MCP are logged but do not throw; the
17
+ * unpublished document stays at `_status: draft` until either an admin
18
+ * intervenes or the next tick succeeds. Throwing would retry indefinitely
19
+ * on a permanent error (e.g. composition check failure), which is wrong
20
+ * for cron-style work.
21
+ */
22
+ export declare function createExecuteScheduledPublishesFunction(options: ExecuteScheduledPublishesOptions): InngestFunction.Any;
23
+ //# sourceMappingURL=execute-scheduled-publishes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute-scheduled-publishes.d.ts","sourceRoot":"","sources":["../src/execute-scheduled-publishes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,YAAY,CAAA;AAelE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uCAAuC,CACrD,OAAO,EAAE,gCAAgC,GACxC,eAAe,CAAC,GAAG,CA4DrB"}
@@ -0,0 +1,119 @@
1
+ const DEFAULT_SCHEDULE = '*/5 * * * *';
2
+ /**
3
+ * Cron-driven scheduled publish executor. Every tick (default: every 5
4
+ * minutes) the function looks for documents in any of the configured
5
+ * collections whose `_status` is still `draft` and whose
6
+ * `scheduledPublishAt` has passed. For each, it calls the publishing
7
+ * server's MCP `publish` tool — going through the full publishing pipeline
8
+ * means scheduled publishes get the same composition / accessibility /
9
+ * approval checks as interactive publishes.
10
+ *
11
+ * The `publishingApiKey` (or `PUBLISHING_SYSTEM_API_KEY` env var) must
12
+ * carry `publishing.execute` scope. The factory throws at construction if
13
+ * neither is present so misconfigurations surface immediately.
14
+ *
15
+ * Failures from the publishing MCP are logged but do not throw; the
16
+ * unpublished document stays at `_status: draft` until either an admin
17
+ * intervenes or the next tick succeeds. Throwing would retry indefinitely
18
+ * on a permanent error (e.g. composition check failure), which is wrong
19
+ * for cron-style work.
20
+ */
21
+ export function createExecuteScheduledPublishesFunction(options) {
22
+ const apiKey = options.publishingApiKey ?? process.env['PUBLISHING_SYSTEM_API_KEY'];
23
+ if (!apiKey) {
24
+ throw new Error('createExecuteScheduledPublishesFunction requires options.publishingApiKey or the PUBLISHING_SYSTEM_API_KEY env var.');
25
+ }
26
+ const schedule = options.schedule ?? DEFAULT_SCHEDULE;
27
+ const baseUrl = options.publishingServerUrl.replace(/\/$/, '');
28
+ return options.inngest.createFunction({
29
+ id: options.id ?? 'execute-scheduled-publishes',
30
+ triggers: [{ cron: schedule }],
31
+ }, async ({ step, logger }) => {
32
+ const nowIso = new Date().toISOString();
33
+ let publishedCount = 0;
34
+ let blockedCount = 0;
35
+ for (const config of options.collections) {
36
+ const statusField = config.statusField ?? '_status';
37
+ const scheduledField = config.scheduledField ?? 'scheduledPublishAt';
38
+ const due = await step.run(`find-due-${config.slug}`, async () => {
39
+ const result = await options.payload.find({
40
+ collection: config.slug,
41
+ where: {
42
+ and: [
43
+ { [statusField]: { equals: 'draft' } },
44
+ { [scheduledField]: { exists: true } },
45
+ { [scheduledField]: { less_than_equal: nowIso } },
46
+ ],
47
+ },
48
+ limit: 100,
49
+ });
50
+ return result.docs.map((doc) => ({
51
+ id: String(doc['id']),
52
+ title: String(doc['title'] ?? doc['id']),
53
+ collection: config.slug,
54
+ }));
55
+ });
56
+ for (const doc of due) {
57
+ const outcome = await step.run(`publish-${config.slug}-${doc.id}`, async () => {
58
+ return executePublish({ doc, baseUrl, apiKey, logger });
59
+ });
60
+ if (outcome === 'published')
61
+ publishedCount += 1;
62
+ if (outcome === 'blocked')
63
+ blockedCount += 1;
64
+ }
65
+ }
66
+ logger.info('Scheduled publish tick complete', { publishedCount, blockedCount });
67
+ return { publishedCount, blockedCount };
68
+ });
69
+ }
70
+ async function executePublish({ doc, baseUrl, apiKey, logger, }) {
71
+ let response;
72
+ try {
73
+ response = await fetch(`${baseUrl}/api/publishing/mcp`, {
74
+ method: 'POST',
75
+ headers: {
76
+ authorization: `Bearer ${apiKey}`,
77
+ 'content-type': 'application/json',
78
+ },
79
+ body: JSON.stringify({
80
+ jsonrpc: '2.0',
81
+ id: 1,
82
+ method: 'tools/call',
83
+ params: {
84
+ name: 'publish',
85
+ arguments: {
86
+ collection: doc.collection,
87
+ id: doc.id,
88
+ _meta: { reasoning: 'Scheduled publish executed by workflow cron' },
89
+ },
90
+ },
91
+ }),
92
+ });
93
+ }
94
+ catch (error) {
95
+ logger.error('Scheduled publish HTTP error', {
96
+ document: doc.title,
97
+ error: error instanceof Error ? error.message : String(error),
98
+ });
99
+ return 'error';
100
+ }
101
+ if (!response.ok) {
102
+ logger.error('Scheduled publish HTTP non-2xx', {
103
+ document: doc.title,
104
+ status: response.status,
105
+ });
106
+ return 'error';
107
+ }
108
+ const body = (await response.json());
109
+ if (body.error) {
110
+ logger.warn('Scheduled publish blocked by policy', {
111
+ document: doc.title,
112
+ reason: body.error.message,
113
+ });
114
+ return 'blocked';
115
+ }
116
+ logger.info('Scheduled publish succeeded', { document: doc.title });
117
+ return 'published';
118
+ }
119
+ //# sourceMappingURL=execute-scheduled-publishes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute-scheduled-publishes.js","sourceRoot":"","sources":["../src/execute-scheduled-publishes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { InngestFunction } from 'inngest';
2
+ import type { ExpireStaleApprovalsOptions } from './types.js';
3
+ /**
4
+ * Cron that finds pending approvals whose `expiresAt` has passed and flips
5
+ * them to `status: 'expired'`. For each expired record it writes an
6
+ * `approval.expired` audit event and fires an `approval/expired` Inngest
7
+ * event so notification workflows (e.g. C11 email) can let the requester
8
+ * know without polling.
9
+ */
10
+ export declare function createExpireStaleApprovalsFunction(options: ExpireStaleApprovalsOptions): InngestFunction.Any;
11
+ //# sourceMappingURL=expire-stale-approvals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expire-stale-approvals.d.ts","sourceRoot":"","sources":["../src/expire-stale-approvals.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAA;AAa7D;;;;;;GAMG;AACH,wBAAgB,kCAAkC,CAChD,OAAO,EAAE,2BAA2B,GACnC,eAAe,CAAC,GAAG,CA0ErB"}
@@ -0,0 +1,83 @@
1
+ import { getAuditWriter } from '@forumone/throughline-core';
2
+ const DEFAULT_SCHEDULE = '0 2 * * *'; // daily at 2am UTC
3
+ const DEFAULT_SLUG = 'approvals';
4
+ /**
5
+ * Cron that finds pending approvals whose `expiresAt` has passed and flips
6
+ * them to `status: 'expired'`. For each expired record it writes an
7
+ * `approval.expired` audit event and fires an `approval/expired` Inngest
8
+ * event so notification workflows (e.g. C11 email) can let the requester
9
+ * know without polling.
10
+ */
11
+ export function createExpireStaleApprovalsFunction(options) {
12
+ const schedule = options.schedule ?? DEFAULT_SCHEDULE;
13
+ const collectionSlug = options.collectionSlug ?? DEFAULT_SLUG;
14
+ return options.inngest.createFunction({
15
+ id: options.id ?? 'expire-stale-approvals',
16
+ triggers: [{ cron: schedule }],
17
+ }, async ({ step, logger }) => {
18
+ const now = new Date().toISOString();
19
+ const expired = await step.run('find-expired', async () => {
20
+ const result = await options.payload.find({
21
+ collection: collectionSlug,
22
+ where: {
23
+ and: [
24
+ { status: { equals: 'pending' } },
25
+ { expiresAt: { less_than: now } },
26
+ ],
27
+ },
28
+ limit: 500,
29
+ });
30
+ return result.docs.map((doc) => ({
31
+ id: String(doc['id']),
32
+ targetCollection: String(doc['targetCollection'] ?? ''),
33
+ targetId: String(doc['targetId'] ?? ''),
34
+ targetTitle: String(doc['targetTitle'] ?? doc['targetId'] ?? '(unknown)'),
35
+ requesterId: extractRequesterId(doc['requestedBy']),
36
+ }));
37
+ });
38
+ if (expired.length === 0) {
39
+ logger.info('No stale approvals to expire');
40
+ return { expiredCount: 0 };
41
+ }
42
+ const auditWriter = getAuditWriter(options.payload);
43
+ for (const approval of expired) {
44
+ await step.run(`expire-${approval.id}`, async () => {
45
+ await options.payload.update({
46
+ collection: collectionSlug,
47
+ id: approval.id,
48
+ data: { status: 'expired' },
49
+ });
50
+ await auditWriter({
51
+ actor: { type: 'system', apiKeyName: 'workflow:expire-stale-approvals' },
52
+ action: 'approval.expired',
53
+ mcpServer: 'approvals',
54
+ mcpTool: 'expire-stale-approvals',
55
+ targetCollection: approval.targetCollection,
56
+ targetId: approval.targetId,
57
+ targetTitle: approval.targetTitle,
58
+ approvalRequestId: approval.id,
59
+ });
60
+ await options.inngest.send({
61
+ name: 'approval/expired',
62
+ data: {
63
+ approvalId: approval.id,
64
+ requesterId: approval.requesterId,
65
+ targetCollection: approval.targetCollection,
66
+ targetId: approval.targetId,
67
+ },
68
+ });
69
+ });
70
+ }
71
+ logger.info('Expired stale approvals', { count: expired.length });
72
+ return { expiredCount: expired.length };
73
+ });
74
+ }
75
+ function extractRequesterId(value) {
76
+ if (typeof value === 'string')
77
+ return value;
78
+ if (value && typeof value === 'object' && 'id' in value) {
79
+ return String(value.id);
80
+ }
81
+ return null;
82
+ }
83
+ //# sourceMappingURL=expire-stale-approvals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expire-stale-approvals.js","sourceRoot":"","sources":["../src/expire-stale-approvals.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { InngestFunction } from 'inngest';
2
+ import type { HealthcheckDefinition, HealthcheckOptions } from './types.js';
3
+ /**
4
+ * Cron healthcheck. Runs every check in its own `step.run` so one
5
+ * misbehaving probe can't take down the rest, collects failures, and
6
+ * routes them to `onFailure` (defaults to `console.error`). Always fires
7
+ * a `system/healthcheck` Inngest event so external dashboards can
8
+ * observe heartbeats independently of failure routing.
9
+ */
10
+ export declare function createHealthcheckFunction(options: HealthcheckOptions): InngestFunction.Any;
11
+ /**
12
+ * Check that Payload is reachable and able to query a collection. Defaults
13
+ * to `users` because every Payload deployment has it; pass a different
14
+ * slug if `users` isn't always present (rare).
15
+ */
16
+ export declare function createPayloadReachableCheck(collectionSlug?: string): HealthcheckDefinition;
17
+ /**
18
+ * Check that a manifest URL is reachable. Aimed at the design-system
19
+ * manifest the components server consults, but works for any HTTPS
20
+ * endpoint that responds 2xx to a GET.
21
+ */
22
+ export declare function createManifestReachableCheck(manifestUrl: string): HealthcheckDefinition;
23
+ //# sourceMappingURL=healthcheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healthcheck.d.ts","sourceRoot":"","sources":["../src/healthcheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAqB,MAAM,YAAY,CAAA;AAK9F;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,kBAAkB,GAAG,eAAe,CAAC,GAAG,CA4D1F;AAWD;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,cAAc,GAAE,MAAgB,GAC/B,qBAAqB,CAevB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,MAAM,GAAG,qBAAqB,CAkBvF"}
@@ -0,0 +1,117 @@
1
+ const DEFAULT_SCHEDULE = '*/15 * * * *';
2
+ const REACHABLE_TIMEOUT_MS = 5_000;
3
+ /**
4
+ * Cron healthcheck. Runs every check in its own `step.run` so one
5
+ * misbehaving probe can't take down the rest, collects failures, and
6
+ * routes them to `onFailure` (defaults to `console.error`). Always fires
7
+ * a `system/healthcheck` Inngest event so external dashboards can
8
+ * observe heartbeats independently of failure routing.
9
+ */
10
+ export function createHealthcheckFunction(options) {
11
+ const schedule = options.schedule ?? DEFAULT_SCHEDULE;
12
+ const onFailure = options.onFailure ?? defaultOnFailure;
13
+ return options.inngest.createFunction({
14
+ id: options.id ?? 'healthcheck',
15
+ triggers: [{ cron: schedule }],
16
+ }, async ({ step, logger }) => {
17
+ const results = [];
18
+ for (const check of options.checks) {
19
+ const outcome = await step.run(`check-${check.name}`, async () => {
20
+ try {
21
+ const result = await check.run({ payload: options.payload });
22
+ return { name: check.name, ...result };
23
+ }
24
+ catch (error) {
25
+ return {
26
+ name: check.name,
27
+ ok: false,
28
+ details: error instanceof Error ? error.message : 'Unknown error',
29
+ };
30
+ }
31
+ });
32
+ results.push(outcome);
33
+ }
34
+ const failures = results
35
+ .filter((r) => !r.ok)
36
+ .map((r) => {
37
+ const entry = { name: r.name };
38
+ if (r.details)
39
+ entry.details = r.details;
40
+ return entry;
41
+ });
42
+ if (failures.length > 0) {
43
+ await step.run('report-failures', async () => {
44
+ await onFailure(failures);
45
+ });
46
+ }
47
+ await step.run('emit-heartbeat', async () => {
48
+ await options.inngest.send({
49
+ name: 'system/healthcheck',
50
+ data: {
51
+ source: 'workflow',
52
+ timestamp: new Date().toISOString(),
53
+ },
54
+ });
55
+ });
56
+ logger.info('Healthcheck complete', {
57
+ totalChecks: results.length,
58
+ failures: failures.length,
59
+ });
60
+ return { results, failureCount: failures.length };
61
+ });
62
+ }
63
+ async function defaultOnFailure(failures) {
64
+ // eslint-disable-next-line no-console
65
+ console.error(`[healthcheck] ${failures.length} check(s) failed: ${failures
66
+ .map((f) => `${f.name}: ${f.details ?? 'no details'}`)
67
+ .join('; ')}`);
68
+ }
69
+ /**
70
+ * Check that Payload is reachable and able to query a collection. Defaults
71
+ * to `users` because every Payload deployment has it; pass a different
72
+ * slug if `users` isn't always present (rare).
73
+ */
74
+ export function createPayloadReachableCheck(collectionSlug = 'users') {
75
+ return {
76
+ name: 'payload-reachable',
77
+ run: async ({ payload }) => {
78
+ try {
79
+ await payload.find({ collection: collectionSlug, limit: 1 });
80
+ return { ok: true };
81
+ }
82
+ catch (error) {
83
+ return {
84
+ ok: false,
85
+ details: error instanceof Error ? error.message : 'Unknown error',
86
+ };
87
+ }
88
+ },
89
+ };
90
+ }
91
+ /**
92
+ * Check that a manifest URL is reachable. Aimed at the design-system
93
+ * manifest the components server consults, but works for any HTTPS
94
+ * endpoint that responds 2xx to a GET.
95
+ */
96
+ export function createManifestReachableCheck(manifestUrl) {
97
+ return {
98
+ name: 'manifest-reachable',
99
+ run: async () => {
100
+ try {
101
+ const response = await fetch(manifestUrl, {
102
+ signal: AbortSignal.timeout(REACHABLE_TIMEOUT_MS),
103
+ });
104
+ if (response.ok)
105
+ return { ok: true };
106
+ return { ok: false, details: `HTTP ${response.status}` };
107
+ }
108
+ catch (error) {
109
+ return {
110
+ ok: false,
111
+ details: error instanceof Error ? error.message : 'Unknown error',
112
+ };
113
+ }
114
+ },
115
+ };
116
+ }
117
+ //# sourceMappingURL=healthcheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healthcheck.js","sourceRoot":"","sources":["../src/healthcheck.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export { createRevalidateOnPublishFunction } from './revalidate-on-publish.js';
2
+ export { createExecuteScheduledPublishesFunction } from './execute-scheduled-publishes.js';
3
+ export { createExpireStaleApprovalsFunction } from './expire-stale-approvals.js';
4
+ export { createAuditEventEchoFunction } from './audit-event-echo.js';
5
+ export { createHealthcheckFunction, createPayloadReachableCheck, createManifestReachableCheck, } from './healthcheck.js';
6
+ export type { BaseWorkflowOptions, RevalidateFn, RevalidatePathsInput, RevalidateOnPublishOptions, ScheduledCollectionConfig, ExecuteScheduledPublishesOptions, ExpireStaleApprovalsOptions, AuditEchoEvent, AuditEchoHandler, AuditEventEchoOptions, HealthcheckDefinition, HealthcheckOptions, HealthcheckResult, } from './types.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iCAAiC,EAAE,MAAM,4BAA4B,CAAA;AAC9E,OAAO,EAAE,uCAAuC,EAAE,MAAM,kCAAkC,CAAA;AAC1F,OAAO,EAAE,kCAAkC,EAAE,MAAM,6BAA6B,CAAA;AAChF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,kBAAkB,CAAA;AAEzB,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EACpB,0BAA0B,EAC1B,yBAAyB,EACzB,gCAAgC,EAChC,2BAA2B,EAC3B,cAAc,EACd,gBAAgB,EAChB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // Other factory exports land in subsequent commits.
2
+ export { createRevalidateOnPublishFunction } from './revalidate-on-publish.js';
3
+ export { createExecuteScheduledPublishesFunction } from './execute-scheduled-publishes.js';
4
+ export { createExpireStaleApprovalsFunction } from './expire-stale-approvals.js';
5
+ export { createAuditEventEchoFunction } from './audit-event-echo.js';
6
+ export { createHealthcheckFunction, createPayloadReachableCheck, createManifestReachableCheck, } from './healthcheck.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oDAAoD;AAEpD,OAAO,EAAE,iCAAiC,EAAE,MAAM,4BAA4B,CAAA;AAC9E,OAAO,EAAE,uCAAuC,EAAE,MAAM,kCAAkC,CAAA;AAC1F,OAAO,EAAE,kCAAkC,EAAE,MAAM,6BAA6B,CAAA;AAChF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,13 @@
1
+ import type { InngestFunction } from 'inngest';
2
+ import type { RevalidateOnPublishOptions } from './types.js';
3
+ /**
4
+ * Builds the `revalidate-on-publish` Inngest function. Subscribes to the
5
+ * publishing taxonomy and revalidates Next.js cache entries for the affected
6
+ * page, the listing routes, and the sitemap.
7
+ *
8
+ * The default revalidate function dynamically imports `next/cache`, so the
9
+ * package is safe to install in non-Next.js contexts. Pass `options.revalidate`
10
+ * to use a different cache invalidation strategy.
11
+ */
12
+ export declare function createRevalidateOnPublishFunction(options: RevalidateOnPublishOptions): InngestFunction.Any;
13
+ //# sourceMappingURL=revalidate-on-publish.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revalidate-on-publish.d.ts","sourceRoot":"","sources":["../src/revalidate-on-publish.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,KAAK,EAAgB,0BAA0B,EAAwB,MAAM,YAAY,CAAA;AAOhG;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,0BAA0B,GAClC,eAAe,CAAC,GAAG,CAuCrB"}
@@ -0,0 +1,59 @@
1
+ const DEFAULT_URL_BUILDERS = {
2
+ pages: (slug) => (slug === 'home' || slug === '' ? '/' : `/${slug}`),
3
+ posts: (slug) => `/blog/${slug}`,
4
+ };
5
+ /**
6
+ * Builds the `revalidate-on-publish` Inngest function. Subscribes to the
7
+ * publishing taxonomy and revalidates Next.js cache entries for the affected
8
+ * page, the listing routes, and the sitemap.
9
+ *
10
+ * The default revalidate function dynamically imports `next/cache`, so the
11
+ * package is safe to install in non-Next.js contexts. Pass `options.revalidate`
12
+ * to use a different cache invalidation strategy.
13
+ */
14
+ export function createRevalidateOnPublishFunction(options) {
15
+ const urlBuilders = { ...DEFAULT_URL_BUILDERS, ...options.urlBuilders };
16
+ const collectionTags = options.collectionTags ?? {};
17
+ const revalidate = options.revalidate ?? defaultRevalidate;
18
+ return options.inngest.createFunction({
19
+ id: options.id ?? 'revalidate-on-publish',
20
+ retries: 5,
21
+ triggers: [
22
+ { event: 'content/page.published' },
23
+ { event: 'content/page.unpublished' },
24
+ { event: 'content/page.rolled_back' },
25
+ ],
26
+ }, async ({ event, step, logger }) => {
27
+ const data = (event.data ?? {});
28
+ const collection = data.collection ?? 'pages';
29
+ const slug = data.slug ?? data.id ?? '';
30
+ const tags = collectionTags[collection] ?? [collection];
31
+ await step.run('revalidate-page-path', async () => {
32
+ const builder = urlBuilders[collection] ?? ((s) => `/${s}`);
33
+ const path = builder(slug);
34
+ await revalidate({ path, tags });
35
+ logger.info('Revalidated page path', { path, tags });
36
+ });
37
+ await step.run('revalidate-listings', async () => {
38
+ await revalidate({ path: '', tags });
39
+ });
40
+ await step.run('revalidate-sitemap', async () => {
41
+ await revalidate({ path: '/sitemap.xml', tags: ['sitemap'] });
42
+ });
43
+ return { collection, slug, revalidated: true };
44
+ });
45
+ }
46
+ /**
47
+ * Default revalidator: dynamic-imports `next/cache` so the package can be
48
+ * imported in environments without Next.js (e.g. test runners that don't
49
+ * stub the module). Calls `revalidatePath` only when `path` is non-empty
50
+ * because `revalidatePath('')` triggers a noisy Next.js warning.
51
+ */
52
+ const defaultRevalidate = async ({ path, tags }) => {
53
+ const { revalidatePath, revalidateTag } = (await import('next/cache'));
54
+ if (path)
55
+ revalidatePath(path);
56
+ for (const tag of tags)
57
+ revalidateTag(tag);
58
+ };
59
+ //# sourceMappingURL=revalidate-on-publish.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revalidate-on-publish.js","sourceRoot":"","sources":["../src/revalidate-on-publish.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ import type { Inngest } from 'inngest';
2
+ import type { Payload } from 'payload';
3
+ /**
4
+ * Common dependencies for workflow factories. Every factory takes an
5
+ * Inngest client and a Payload instance — the workflows-as-factories shape
6
+ * means the client app is responsible for instantiating both and passing
7
+ * them in.
8
+ */
9
+ export interface BaseWorkflowOptions {
10
+ inngest: Inngest;
11
+ payload: Payload;
12
+ }
13
+ export interface RevalidatePathsInput {
14
+ /** A page path (`/about`, `/blog/post-1`, or `''` for layout-level only). */
15
+ path: string;
16
+ /** Tag list passed through to Next.js's `revalidateTag`. */
17
+ tags: string[];
18
+ }
19
+ export type RevalidateFn = (input: RevalidatePathsInput) => Promise<void>;
20
+ export interface RevalidateOnPublishOptions extends BaseWorkflowOptions {
21
+ /**
22
+ * Custom revalidation function. Defaults to Next.js `revalidatePath` +
23
+ * `revalidateTag` (loaded via dynamic import so non-Next.js consumers
24
+ * don't fail to import the package). Supply your own for non-Next.js
25
+ * frontends or richer cache invalidation.
26
+ */
27
+ revalidate?: RevalidateFn;
28
+ /**
29
+ * Per-collection URL builders. Built-in defaults: pages → /slug
30
+ * (with `home` mapped to `/`), posts → /blog/slug. Other collections
31
+ * fall back to /<slug> unless you provide a builder.
32
+ */
33
+ urlBuilders?: Record<string, (slug: string) => string>;
34
+ /**
35
+ * Per-collection cache tags. Defaults to `[<collection>]`. Multiple tags
36
+ * supported for collections that participate in shared listings (e.g.
37
+ * `programs` → `['programs', 'sitemap']`).
38
+ */
39
+ collectionTags?: Record<string, string[]>;
40
+ /**
41
+ * Override the function id. Default: `revalidate-on-publish`.
42
+ * Useful if multiple instances of the workflow run in the same Inngest
43
+ * deployment (rare).
44
+ */
45
+ id?: string;
46
+ }
47
+ export interface ScheduledCollectionConfig {
48
+ slug: string;
49
+ /**
50
+ * Field name carrying the publish status. Default: `_status`.
51
+ * Override only if the collection uses a non-default status field.
52
+ */
53
+ statusField?: string;
54
+ /**
55
+ * Field name carrying the scheduled-publish timestamp. Default: `scheduledPublishAt`.
56
+ */
57
+ scheduledField?: string;
58
+ }
59
+ export interface ExecuteScheduledPublishesOptions extends BaseWorkflowOptions {
60
+ /** Collections that participate in scheduled publishing. */
61
+ collections: ScheduledCollectionConfig[];
62
+ /** Cron schedule. Default: `*\/5 * * * *` (every 5 minutes). */
63
+ schedule?: string;
64
+ /**
65
+ * Base URL of the deployment's publishing server. Scheduled publishes
66
+ * call through the publishing MCP so the full pipeline runs (composition
67
+ * checks, accessibility, approval gating). Direct Payload writes would
68
+ * skip those checks.
69
+ */
70
+ publishingServerUrl: string;
71
+ /**
72
+ * API-key value for `Authorization: Bearer <key>` against the publishing
73
+ * MCP. Falls back to `process.env.PUBLISHING_SYSTEM_API_KEY` when omitted.
74
+ * Required at construction time — the factory throws if neither is set.
75
+ */
76
+ publishingApiKey?: string;
77
+ /** Override the function id. Default: `execute-scheduled-publishes`. */
78
+ id?: string;
79
+ }
80
+ export interface ExpireStaleApprovalsOptions extends BaseWorkflowOptions {
81
+ /** Approvals collection slug. Default: 'approvals'. */
82
+ collectionSlug?: string;
83
+ /** Cron schedule. Default: `0 2 * * *` (daily at 2am UTC). */
84
+ schedule?: string;
85
+ /** Override the function id. Default: `expire-stale-approvals`. */
86
+ id?: string;
87
+ }
88
+ export interface AuditEchoEvent {
89
+ action: string;
90
+ data: Record<string, unknown>;
91
+ }
92
+ export interface AuditEchoHandler {
93
+ /** Predicate. Return true to run `handle`. */
94
+ match: (event: {
95
+ action: string;
96
+ }) => boolean;
97
+ /** Side-effects (e.g. fire a follow-on Inngest event). */
98
+ handle: (event: AuditEchoEvent) => Promise<void>;
99
+ }
100
+ export interface AuditEventEchoOptions {
101
+ inngest: Inngest;
102
+ /**
103
+ * Additional handlers run after the built-in approval fan-out. Each
104
+ * handler is wrapped in its own `step.run` so failures retry independently.
105
+ */
106
+ handlers?: AuditEchoHandler[];
107
+ /** Override the function id. Default: `audit-event-echo`. */
108
+ id?: string;
109
+ }
110
+ export interface HealthcheckResult {
111
+ ok: boolean;
112
+ details?: string;
113
+ }
114
+ export interface HealthcheckDefinition {
115
+ name: string;
116
+ run: (ctx: {
117
+ payload: Payload;
118
+ }) => Promise<HealthcheckResult>;
119
+ }
120
+ export interface HealthcheckOptions extends BaseWorkflowOptions {
121
+ checks: HealthcheckDefinition[];
122
+ /** Cron schedule. Default: `*\/15 * * * *`. */
123
+ schedule?: string;
124
+ /**
125
+ * Called once per run when at least one check fails. Defaults to
126
+ * `console.error`; production deployments route this to monitoring.
127
+ */
128
+ onFailure?: (failures: Array<{
129
+ name: string;
130
+ details?: string;
131
+ }>) => Promise<void>;
132
+ /** Override the function id. Default: `healthcheck`. */
133
+ id?: string;
134
+ }
135
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,10 +1,70 @@
1
1
  {
2
2
  "name": "@forumone/throughline-workflows",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @forumone/throughline-workflows",
3
+ "version": "0.2.1",
4
+ "description": "Composable Inngest function factories for the Throughline framework — revalidation, scheduled publishing, approval expiration, audit fan-out, healthcheck.",
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
+ "inngest",
23
+ "workflows"
24
+ ],
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/forumone/throughline.git",
29
+ "directory": "packages/workflows"
30
+ },
31
+ "homepage": "https://github.com/forumone/throughline/tree/main/packages/workflows#readme",
32
+ "bugs": {
33
+ "url": "https://github.com/forumone/throughline/issues"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "peerDependencies": {
39
+ "inngest": "^4.0.0",
40
+ "next": ">=15.0.0",
41
+ "payload": "^3.0.0"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "next": {
45
+ "optional": true
46
+ }
47
+ },
48
+ "dependencies": {
49
+ "@forumone/throughline-core": "0.2.1"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^20.17.0",
53
+ "eslint": "^9.15.0",
54
+ "inngest": "^4.2.0",
55
+ "next": "^16.2.4",
56
+ "payload": "^3.83.0",
57
+ "typescript": "^5.6.0",
58
+ "vitest": "^2.1.0",
59
+ "@forumone/throughline-eslint-config": "0.0.0",
60
+ "@forumone/throughline-tsconfig": "0.0.0"
61
+ },
62
+ "scripts": {
63
+ "build": "tsc -b",
64
+ "dev": "tsc -b -w",
65
+ "clean": "rm -rf dist .turbo *.tsbuildinfo",
66
+ "typecheck": "tsc --noEmit",
67
+ "lint": "eslint src",
68
+ "test": "vitest run"
69
+ }
70
+ }