@volter/twin-inngest 0.1.0 → 0.1.2

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.
@@ -26,7 +26,7 @@
26
26
  // Poll-fold pattern ported from fal-twin.ts's `progressOnce` (fal-twin.ts:245-255): each call to
27
27
  // `advanceRun` performs AT MOST ONE meaningful state transition (no wall clock, no timers), so a
28
28
  // test suite re-running this file is bit-for-bit reproducible regardless of real elapsed time.
29
- import { applyTwinWrite, projectResources } from '@volter/world-core';
29
+ import { applyTwinWrite, projectResources, worldNow } from '@volter/world-core';
30
30
  import { createHash } from 'node:crypto';
31
31
  const SERVICE = 'inngest';
32
32
  const DEFAULT_MAX_ATTEMPTS = 4; // modeled default (§13 ⚠4 — doc-UNVERIFIED exact vendor default; config.retries overrides it, see inngest-twin.ts)
@@ -34,7 +34,7 @@ function kid(type, id) {
34
34
  return `${type}:${id}`;
35
35
  }
36
36
  export function nowIso(occurredAt) {
37
- return occurredAt ?? new Date().toISOString();
37
+ return occurredAt ?? worldNow();
38
38
  }
39
39
  // ── canonical JSON + deterministic stub hashing (ported convention: fal-twin.ts:186-195) ──────
40
40
  export function canonicalJson(v) {
@@ -13,9 +13,11 @@
13
13
  import { serveHttp } from '@volter/world-core';
14
14
  import { handleInngestTwinRequest } from "./inngest-twin.js";
15
15
  import { rows } from "./inngest-runtime.js";
16
- import { createTwinFetchFromHandler, statefulTwinManifest } from '@volter/world-core';
16
+ import { createTwinFetchFromHandler, statefulTwinManifest, withRequestScopes } from '@volter/world-core';
17
17
  export function createInngestTwinFetch(options = {}) {
18
- return createTwinFetchFromHandler(handleInngestTwinRequest, {
18
+ // a read-only request (x-volter-read-only) is a request to a read-only twin: a run poll answers the run
19
+ // as it stands without running a step, nothing is written (withRequestScopes: the pack opts in)
20
+ return withRequestScopes(createTwinFetchFromHandler(handleInngestTwinRequest, {
19
21
  ...options,
20
22
  manifest: statefulTwinManifest({ vendor: 'inngest', twinOf: 'the Inngest durable-execution surface', stores: 'events, registered functions and their kernel-persisted run state' }),
21
23
  // THE STORE DOOR (R5c). The vendor's own read surface is addressed BY ID
@@ -29,7 +31,7 @@ export function createInngestTwinFetch(options = {}) {
29
31
  runs: () => rows('run', options.root),
30
32
  },
31
33
  extras: (_request, url) => ({ host: url.host }),
32
- });
34
+ }), { refuse: () => Response.json({ error: 'read_only', hint: 'this twin was started read-only; omit readOnly to accept writes' }, { status: 405 }) });
33
35
  }
34
36
  export async function createInngestTwinServer(options = {}) {
35
37
  const server = await serveHttp({
@@ -62,7 +62,7 @@
62
62
  //
63
63
  // `node:crypto` is required LAZILY (server-only), same convention as fal-webhooks.ts, so this
64
64
  // module stays browser-bundle safe.
65
- import { applyTwinWriteAtomic, projectResources, nodeBuiltin } from '@volter/world-core';
65
+ import { applyTwinWriteAtomic, projectResources, nodeBuiltin, worldNow } from '@volter/world-core';
66
66
  function nodeCrypto() {
67
67
  // eslint-disable-next-line @typescript-eslint/no-require-imports
68
68
  return nodeBuiltin('node:crypto');
@@ -160,7 +160,7 @@ function parseHeader(header) {
160
160
  * `now`), or a signature mismatch (constant-time compare). Pure — never touches the handler. */
161
161
  export function verifyInngestSignature(body, header, signingKey, opts = {}) {
162
162
  const { t, s } = parseHeader(header);
163
- const now = opts.now ?? Math.floor(Date.now() / 1000);
163
+ const now = opts.now ?? Math.floor(Date.parse(worldNow()) / 1000);
164
164
  const tolerance = opts.toleranceSec ?? 300;
165
165
  if (Math.abs(now - t) > tolerance)
166
166
  throw new InngestSignatureError(`Signature timestamp outside the ${tolerance}s tolerance.`);
@@ -39,6 +39,7 @@
39
39
  // Honesty (D2): an unmodeled route returns Inngest's real v2 error envelope
40
40
  // (`{errors:[{code,message}]}`, GROUNDED from the fetched v2 OpenAPI `v2ErrorResponse` schema),
41
41
  // never a fabricated success. `readOnly` rejects writes with 405.
42
+ import { worldNow } from '@volter/world-core';
42
43
  import { advanceRun, cancelRunsForFunction, createRun, functionsCancelledByEvent, functionsMatchingEvent, getRow, listFunctions, recordEvent, registerFunction, runsForEvent, } from "./inngest-runtime.js";
43
44
  const SERVICE = 'inngest';
44
45
  // The subject types this twin stores and serves — the mirror of the pack descriptor's
@@ -87,7 +88,7 @@ function parseJson(body) {
87
88
  }
88
89
  }
89
90
  function nowIso(occurredAt) {
90
- return occurredAt ?? new Date().toISOString();
91
+ return occurredAt ?? worldNow();
91
92
  }
92
93
  // ── views ──────────────────────────────────────────────────────────────────────────────────
93
94
  const RUN_STATUS_MAP = { QUEUED: 'QUEUED', RUNNING: 'RUNNING', COMPLETED: 'COMPLETED', FAILED: 'FAILED', CANCELLED: 'CANCELLED' };
@@ -244,7 +245,9 @@ async function handleGetRun(runId, req) {
244
245
  const existing = getRow('run', runId, req.root);
245
246
  if (!existing)
246
247
  return apiError(404, 'run_not_found', `No run with id "${runId}".`);
247
- const result = await advanceRun(runId, req.root, req.occurredAt); // poll-fold advance on read (fal-twin.ts progressOnce pattern)
248
+ // poll-fold advance on read (fal-twin.ts progressOnce pattern). A read-only twin (a World's read
249
+ // token) answers the run as it stands and never runs a step: the app's next poll sees what it would have.
250
+ const result = req.readOnly ? undefined : await advanceRun(runId, req.root, req.occurredAt);
248
251
  const row = result?.run ?? existing;
249
252
  return { status: 200, body: { data: runView(row), metadata: { fetchedAt: nowIso(req.occurredAt) } } };
250
253
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/twin-inngest",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Local Inngest twin for the durable-execution workflow-orchestration surface: event send/batch (Event API), function register/introspect (SDK exec protocol), and a REAL kernel-persisted step-machine (step.run memoize/replay, sleep/sleepUntil instant-advance, waitForEvent gating, exact retry-attempt counters), run status/get/list/cancel-via-event, HMAC-SHA256 executor<->SDK request signing, built on @volter/world-core.",
5
5
  "author": "Volter (https://github.com/volter-ai)",
6
6
  "license": "Apache-2.0",
@@ -35,12 +35,12 @@
35
35
  "postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
36
36
  },
37
37
  "peerDependencies": {
38
- "@volter/world-core": "2.0.0"
38
+ "@volter/world-core": "2.0.2"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/bun": "^1.2.20",
42
42
  "@types/node": "^24.0.0",
43
- "@volter/world-core": "2.0.0",
43
+ "@volter/world-core": "2.0.2",
44
44
  "@volter/world-tooling": "0.1.0",
45
45
  "inngest": "4.12.0",
46
46
  "typescript": "^5.9.0"
@@ -26,7 +26,7 @@
26
26
  // Poll-fold pattern ported from fal-twin.ts's `progressOnce` (fal-twin.ts:245-255): each call to
27
27
  // `advanceRun` performs AT MOST ONE meaningful state transition (no wall clock, no timers), so a
28
28
  // test suite re-running this file is bit-for-bit reproducible regardless of real elapsed time.
29
- import { applyTwinWrite, projectResources } from '@volter/world-core';
29
+ import { applyTwinWrite, projectResources, worldNow } from '@volter/world-core';
30
30
  import { createHash } from 'node:crypto';
31
31
 
32
32
  const SERVICE = 'inngest';
@@ -71,7 +71,7 @@ function kid(type: string, id: string): string {
71
71
  return `${type}:${id}`;
72
72
  }
73
73
  export function nowIso(occurredAt?: string): string {
74
- return occurredAt ?? new Date().toISOString();
74
+ return occurredAt ?? worldNow();
75
75
  }
76
76
 
77
77
  // ── canonical JSON + deterministic stub hashing (ported convention: fal-twin.ts:186-195) ──────
@@ -13,7 +13,7 @@
13
13
  import { serveHttp } from '@volter/world-core';
14
14
  import { handleInngestTwinRequest } from './inngest-twin.ts';
15
15
  import { rows } from './inngest-runtime.ts';
16
- import { createTwinFetchFromHandler, statefulTwinManifest } from '@volter/world-core';
16
+ import { createTwinFetchFromHandler, statefulTwinManifest, withRequestScopes } from '@volter/world-core';
17
17
 
18
18
  /** Options every Inngest-twin HTTP surface needs, independent of who owns the socket. */
19
19
  export interface InngestTwinFetchOptions {
@@ -22,7 +22,9 @@ export interface InngestTwinFetchOptions {
22
22
  }
23
23
 
24
24
  export function createInngestTwinFetch(options: InngestTwinFetchOptions = {}): (request: Request) => Promise<Response> {
25
- return createTwinFetchFromHandler(handleInngestTwinRequest, {
25
+ // a read-only request (x-volter-read-only) is a request to a read-only twin: a run poll answers the run
26
+ // as it stands without running a step, nothing is written (withRequestScopes: the pack opts in)
27
+ return withRequestScopes(createTwinFetchFromHandler(handleInngestTwinRequest, {
26
28
  ...options,
27
29
  manifest: statefulTwinManifest({ vendor: 'inngest', twinOf: 'the Inngest durable-execution surface', stores: 'events, registered functions and their kernel-persisted run state' }),
28
30
  // THE STORE DOOR (R5c). The vendor's own read surface is addressed BY ID
@@ -36,7 +38,7 @@ export function createInngestTwinFetch(options: InngestTwinFetchOptions = {}): (
36
38
  runs: () => rows('run', options.root),
37
39
  },
38
40
  extras: (_request, url) => ({ host: url.host }),
39
- });
41
+ }), { refuse: () => Response.json({ error: 'read_only', hint: 'this twin was started read-only; omit readOnly to accept writes' }, { status: 405 }) });
40
42
  }
41
43
 
42
44
  export async function createInngestTwinServer(options: { root?: string; port?: number; readOnly?: boolean } = {}): Promise<{ port: number; stop: () => void }> {
@@ -62,7 +62,7 @@
62
62
  //
63
63
  // `node:crypto` is required LAZILY (server-only), same convention as fal-webhooks.ts, so this
64
64
  // module stays browser-bundle safe.
65
- import { applyTwinWriteAtomic, projectResources, nodeBuiltin } from '@volter/world-core';
65
+ import { applyTwinWriteAtomic, projectResources, nodeBuiltin, worldNow } from '@volter/world-core';
66
66
 
67
67
  type NodeCrypto = typeof import('node:crypto');
68
68
  function nodeCrypto(): NodeCrypto {
@@ -169,7 +169,7 @@ function parseHeader(header: string): { t: number; s: string } {
169
169
  * `now`), or a signature mismatch (constant-time compare). Pure — never touches the handler. */
170
170
  export function verifyInngestSignature(body: unknown, header: string, signingKey: string, opts: { now?: number; toleranceSec?: number } = {}): void {
171
171
  const { t, s } = parseHeader(header);
172
- const now = opts.now ?? Math.floor(Date.now() / 1000);
172
+ const now = opts.now ?? Math.floor(Date.parse(worldNow()) / 1000);
173
173
  const tolerance = opts.toleranceSec ?? 300;
174
174
  if (Math.abs(now - t) > tolerance) throw new InngestSignatureError(`Signature timestamp outside the ${tolerance}s tolerance.`);
175
175
  const expected = signInngestRequest(body, t, signingKey);
@@ -39,6 +39,7 @@
39
39
  // Honesty (D2): an unmodeled route returns Inngest's real v2 error envelope
40
40
  // (`{errors:[{code,message}]}`, GROUNDED from the fetched v2 OpenAPI `v2ErrorResponse` schema),
41
41
  // never a fabricated success. `readOnly` rejects writes with 405.
42
+ import { worldNow } from '@volter/world-core';
42
43
  import {
43
44
  advanceRun,
44
45
  cancelRunsForFunction,
@@ -120,7 +121,7 @@ function parseJson(body?: string): unknown {
120
121
  }
121
122
  }
122
123
  function nowIso(occurredAt?: string): string {
123
- return occurredAt ?? new Date().toISOString();
124
+ return occurredAt ?? worldNow();
124
125
  }
125
126
 
126
127
  // ── views ──────────────────────────────────────────────────────────────────────────────────
@@ -273,7 +274,9 @@ async function handleExecInvoke(rawBody: string | undefined, req: InngestRequest
273
274
  async function handleGetRun(runId: string, req: InngestRequest): Promise<InngestResponse> {
274
275
  const existing = getRow('run', runId, req.root);
275
276
  if (!existing) return apiError(404, 'run_not_found', `No run with id "${runId}".`);
276
- const result = await advanceRun(runId, req.root, req.occurredAt); // poll-fold advance on read (fal-twin.ts progressOnce pattern)
277
+ // poll-fold advance on read (fal-twin.ts progressOnce pattern). A read-only twin (a World's read
278
+ // token) answers the run as it stands and never runs a step: the app's next poll sees what it would have.
279
+ const result = req.readOnly ? undefined : await advanceRun(runId, req.root, req.occurredAt);
277
280
  const row = result?.run ?? existing;
278
281
  return { status: 200, body: { data: runView(row), metadata: { fetchedAt: nowIso(req.occurredAt) } } };
279
282
  }