@dicabrio/durable-sdk 0.1.0 → 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.
package/dist/index.cjs CHANGED
@@ -123,10 +123,26 @@ async function runInvocation(fn, req) {
123
123
  return { op: "error", id: null, message: message2, retryable: true };
124
124
  }
125
125
  }
126
+ function toWindow(w) {
127
+ return w ? { limit: w.limit, periodMs: parseDuration(w.period), key: w.key } : void 0;
128
+ }
129
+ function buildManifest(functions) {
130
+ return functions.map((f) => ({
131
+ id: f.id,
132
+ trigger: f.trigger,
133
+ concurrency: f.concurrency,
134
+ priority: f.priority,
135
+ throttle: toWindow(f.throttle),
136
+ rateLimit: toWindow(f.rateLimit),
137
+ debounce: f.debounce ? { periodMs: parseDuration(f.debounce.period), key: f.debounce.key } : void 0,
138
+ batch: f.batch ? { maxSize: f.batch.maxSize, timeoutMs: parseDuration(f.batch.timeout), key: f.batch.key } : void 0
139
+ }));
140
+ }
126
141
 
127
142
  // ../shared/src/protocol.ts
128
143
  var SIGNATURE_HEADER = "x-durable-signature";
129
144
  var APP_HEADER = "x-durable-app";
145
+ var TIMESTAMP_HEADER = "x-durable-timestamp";
130
146
 
131
147
  // ../shared/src/hmac.ts
132
148
  var import_node_crypto = require("crypto");
@@ -153,6 +169,21 @@ function readBody(req) {
153
169
  function serve(functions, opts) {
154
170
  return async (req, res) => {
155
171
  try {
172
+ if (req.method === "GET") {
173
+ const ts = req.headers[TIMESTAMP_HEADER];
174
+ const sig2 = req.headers[SIGNATURE_HEADER];
175
+ const fresh = typeof ts === "string" && Math.abs(Date.now() - Number(ts)) <= 5 * 60 * 1e3;
176
+ if (!fresh || typeof sig2 !== "string" || !verify(ts, sig2, opts.signingKey)) {
177
+ res.writeHead(401).end("invalid signature");
178
+ return;
179
+ }
180
+ const out2 = JSON.stringify({ functions: buildManifest(functions) });
181
+ res.writeHead(200, {
182
+ "content-type": "application/json",
183
+ [SIGNATURE_HEADER]: sign(out2, opts.signingKey)
184
+ }).end(out2);
185
+ return;
186
+ }
156
187
  const raw = await readBody(req);
157
188
  const sig = req.headers[SIGNATURE_HEADER];
158
189
  if (typeof sig !== "string" || !verify(raw, sig, opts.signingKey)) {
@@ -179,9 +210,6 @@ function serve(functions, opts) {
179
210
  }
180
211
 
181
212
  // src/client.ts
182
- function toWindow(w) {
183
- return w ? { limit: w.limit, periodMs: parseDuration(w.period), key: w.key } : void 0;
184
- }
185
213
  var DurableClient = class {
186
214
  constructor(opts) {
187
215
  this.opts = opts;
@@ -208,16 +236,7 @@ var DurableClient = class {
208
236
  async sync(functions) {
209
237
  const payload = {
210
238
  url: this.opts.appUrl,
211
- functions: functions.map((f) => ({
212
- id: f.id,
213
- trigger: f.trigger,
214
- concurrency: f.concurrency,
215
- priority: f.priority,
216
- throttle: toWindow(f.throttle),
217
- rateLimit: toWindow(f.rateLimit),
218
- debounce: f.debounce ? { periodMs: parseDuration(f.debounce.period), key: f.debounce.key } : void 0,
219
- batch: f.batch ? { maxSize: f.batch.maxSize, timeoutMs: parseDuration(f.batch.timeout), key: f.batch.key } : void 0
220
- }))
239
+ functions: buildManifest(functions)
221
240
  };
222
241
  await this.post("/fn/sync", payload);
223
242
  }
@@ -225,6 +244,20 @@ var DurableClient = class {
225
244
  async send(event) {
226
245
  await this.post("/e", { name: event.name, data: event.data ?? {} });
227
246
  }
247
+ /** Create (or reuse, idempotent by name+env) this app's workspace, sending its appUrl. */
248
+ async provision(opts) {
249
+ const res = await fetch(this.opts.baseUrl + "/api/apps", {
250
+ method: "POST",
251
+ headers: { "content-type": "application/json" },
252
+ body: JSON.stringify({
253
+ name: opts.name,
254
+ environment: opts.environment,
255
+ appUrl: this.opts.appUrl
256
+ })
257
+ });
258
+ if (!res.ok) throw new Error(`provision failed: ${res.status} ${await res.text()}`);
259
+ return await res.json();
260
+ }
228
261
  };
229
262
  // Annotate the CommonJS export names for ESM import in node:
230
263
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/step.ts","../src/function.ts","../../shared/src/protocol.ts","../../shared/src/hmac.ts","../src/serve.ts","../src/client.ts"],"sourcesContent":["export { createFunction, runInvocation } from \"./function.js\";\nexport type { DurableFunction, FunctionContext } from \"./function.js\";\nexport { createStep, parseDuration, StepInterrupt } from \"./step.js\";\nexport type { StepTools, WaitForEventOptions } from \"./step.js\";\nexport { serve } from \"./serve.js\";\nexport { DurableClient } from \"./client.js\";\nexport type { DurableClientOptions } from \"./client.js\";\n","import type { DurableEvent, InvokeResponse, MemoizedStep } from \"@durable/shared\";\n\n/**\n * Thrown to unwind the handler the moment it reaches a new operation. The\n * carried response is exactly what the SDK reports to the service. Using an\n * exception means at most one *new* step executes per invocation — everything\n * after it is skipped until the service re-invokes with that step memoized.\n */\nexport class StepInterrupt extends Error {\n constructor(public readonly response: InvokeResponse) {\n super(\"durable:step-interrupt\");\n }\n}\n\nexport interface WaitForEventOptions {\n /** The event name to wait for. */\n event: string;\n /** Key/value pairs the incoming event.data must contain to match. */\n match?: Record<string, unknown>;\n /** How long to wait before giving up (e.g. \"7d\", \"1h\", or ms). */\n timeout: string | number;\n}\n\nexport interface StepTools {\n /** Run a side-effecting step once; its result is memoized and replayed. */\n run<T>(id: string, cb: () => Promise<T> | T): Promise<T>;\n /** Pause the run until the duration elapses (e.g. \"10s\", \"2d\", or ms). */\n sleep(id: string, duration: string | number): Promise<void>;\n /**\n * Pause the run until a matching event arrives, or the timeout elapses.\n * Resolves with the matched event, or `null` if it timed out.\n */\n waitForEvent(id: string, opts: WaitForEventOptions): Promise<DurableEvent | null>;\n}\n\nconst UNITS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n};\n\nexport function parseDuration(d: string | number): number {\n if (typeof d === \"number\") return d;\n const m = /^(\\d+)\\s*(ms|s|m|h|d)$/.exec(d.trim());\n if (!m) throw new Error(`invalid duration: ${d}`);\n return Number(m[1]) * UNITS[m[2]];\n}\n\nfunction message(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Build the step tools bound to the steps already memoized for this run. */\nexport function createStep(memoized: Record<string, MemoizedStep>): StepTools {\n return {\n async run<T>(id: string, cb: () => Promise<T> | T): Promise<T> {\n const memo = memoized[id];\n if (memo) {\n if (memo.type !== \"run\") {\n throw new Error(`step \"${id}\" already used as a ${memo.type}`);\n }\n return memo.data as T; // replay: free, no side effect\n }\n let data: T;\n try {\n data = await cb();\n } catch (err) {\n // A genuine failure inside the step: let the service decide on retry.\n throw new StepInterrupt({\n op: \"error\",\n id,\n message: message(err),\n retryable: true,\n });\n }\n throw new StepInterrupt({ op: \"step\", id, data });\n },\n\n async sleep(id: string, duration: string | number): Promise<void> {\n const memo = memoized[id];\n if (memo) {\n if (memo.type !== \"sleep\") {\n throw new Error(`step \"${id}\" already used as a ${memo.type}`);\n }\n return; // sleep already satisfied\n }\n const until = new Date(Date.now() + parseDuration(duration)).toISOString();\n throw new StepInterrupt({ op: \"sleep\", id, until });\n },\n\n async waitForEvent(id, opts): Promise<DurableEvent | null> {\n const memo = memoized[id];\n if (memo) {\n if (memo.type !== \"wait\") {\n throw new Error(`step \"${id}\" already used as a ${memo.type}`);\n }\n return (memo.data as DurableEvent | null) ?? null; // event, or null on timeout\n }\n const until = new Date(Date.now() + parseDuration(opts.timeout)).toISOString();\n throw new StepInterrupt({\n op: \"wait\",\n id,\n event: opts.event,\n match: opts.match ?? null,\n until,\n });\n },\n };\n}\n","import type {\n Concurrency,\n DurableEvent,\n InvokeRequest,\n InvokeResponse,\n Trigger,\n} from \"@durable/shared\";\nimport { createStep, StepInterrupt, type StepTools } from \"./step.js\";\n\nexport interface FunctionContext {\n event: DurableEvent;\n step: StepTools;\n}\n\nexport interface DurableFunction {\n id: string;\n /** Triggered by an event name (`{ event }`) or a cron expression (`{ cron }`). */\n trigger: Trigger;\n /** Optional cap on how many runs execute at once (per key, if given). */\n concurrency?: Concurrency;\n /** Higher runs sooner when jobs compete in the queue (default 0). */\n priority?: number;\n /** Spread run starts over time: max `limit` per `period` (excess delayed). */\n throttle?: { limit: number; period: string | number; key?: string };\n /** Cap new runs: max `limit` per `period` (excess dropped). */\n rateLimit?: { limit: number; period: string | number; key?: string };\n /** Collapse rapid events: one run with the LAST event, once quiet for `period`. */\n debounce?: { period: string | number; key?: string };\n /**\n * Group events: ONE run receives event.data = the whole list, flushed at\n * `maxSize` events or `timeout` after the first (event.name = \"$batch\").\n */\n batch?: { maxSize: number; timeout: string | number; key?: string };\n handler: (ctx: FunctionContext) => Promise<unknown>;\n}\n\nexport function createFunction(def: DurableFunction): DurableFunction {\n return def;\n}\n\n/**\n * Replay a single invocation: run the handler from the top, returning the next\n * operation it reaches (a new step, a sleep, completion, or an error).\n */\nexport async function runInvocation(\n fn: DurableFunction,\n req: InvokeRequest,\n): Promise<InvokeResponse> {\n const step = createStep(req.steps);\n try {\n const data = await fn.handler({ event: req.event, step });\n return { op: \"done\", data };\n } catch (err) {\n if (err instanceof StepInterrupt) return err.response;\n // Error thrown outside any step.run (in the handler body itself).\n const message = err instanceof Error ? err.message : String(err);\n return { op: \"error\", id: null, message, retryable: true };\n }\n}\n","/**\n * Wire protocol between the Durable service and the app SDK.\n *\n * One HTTP round-trip advances a run by at most one *new* step. The service\n * sends the run state (event + already-memoized steps); the SDK replays the\n * cached steps for free, executes the next new operation, and reports it back.\n */\n\n/** An event that triggered (or can trigger) a run. */\nexport interface DurableEvent {\n id: string;\n name: string;\n data: unknown;\n}\n\n/** A previously-completed step, keyed by its user-supplied step id. */\nexport interface MemoizedStep {\n /**\n * \"run\" = a step.run result,\n * \"sleep\" = a satisfied step.sleep marker,\n * \"wait\" = a settled step.waitForEvent (data = the event, or null on timeout).\n */\n type: \"run\" | \"sleep\" | \"wait\";\n /** Output of step.run, or the matched event for \"wait\". Null otherwise. */\n data: unknown;\n}\n\n/** Service -> SDK: \"advance this run\". */\nexport interface InvokeRequest {\n runId: string;\n functionId: string;\n event: DurableEvent;\n /** Completed steps so far, keyed by step id. */\n steps: Record<string, MemoizedStep>;\n}\n\n/**\n * SDK -> service: the next operation the function reached.\n *\n * - step : a new step.run executed; persist `data` under `id`, then re-invoke.\n * - sleep : function wants to pause until `until`; arm a timer.\n * - done : function returned; the run is complete with `data`.\n * - error : the current step threw; retry (if retryable) or fail the run.\n */\nexport type InvokeResponse =\n | { op: \"step\"; id: string; data: unknown }\n | { op: \"sleep\"; id: string; until: string /* ISO timestamp */ }\n | {\n op: \"wait\";\n id: string;\n event: string;\n /** Key/value pairs the incoming event.data must contain; null = any. */\n match: Record<string, unknown> | null;\n until: string /* ISO timestamp timeout */;\n }\n | { op: \"done\"; data: unknown }\n | { op: \"error\"; id: string | null; message: string; retryable: boolean };\n\n/** A function is triggered by either an event name or a cron expression. */\nexport type Trigger = { event: string } | { cron: string };\n\n/** Optional concurrency cap for a function. */\nexport interface Concurrency {\n /** Max runs executing at once (per key, if given). */\n limit: number;\n /** event.data field name to scope the limit by (e.g. \"tenantId\"). */\n key?: string;\n}\n\n/** Wire form of a throttle/rate-limit rule (period already in ms). */\nexport interface WindowLimit {\n limit: number;\n periodMs: number;\n key?: string;\n}\n\n/** Wire form of a debounce rule (period already in ms). */\nexport interface DebounceRule {\n periodMs: number;\n key?: string;\n}\n\n/** Wire form of a batch rule (timeout already in ms). */\nexport interface BatchRule {\n maxSize: number;\n timeoutMs: number;\n key?: string;\n}\n\n/** Registry sync: app -> service, \"here are my functions and where to reach me\". */\nexport interface SyncRequest {\n /** Base URL the service should POST invocations to (callback endpoint). */\n url: string;\n functions: Array<{\n id: string;\n trigger: Trigger;\n concurrency?: Concurrency;\n priority?: number;\n throttle?: WindowLimit;\n rateLimit?: WindowLimit;\n debounce?: DebounceRule;\n batch?: BatchRule;\n }>;\n}\n\n/** Header carrying the HMAC signature on every service<->app request body. */\nexport const SIGNATURE_HEADER = \"x-durable-signature\";\n\n/** Header carrying the app id on app -> service requests. */\nexport const APP_HEADER = \"x-durable-app\";\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\n\n/** Compute the hex HMAC-SHA256 of a raw request body. */\nexport function sign(body: string, key: string): string {\n return createHmac(\"sha256\", key).update(body).digest(\"hex\");\n}\n\n/** Constant-time verify of a signature against a raw body. */\nexport function verify(body: string, signature: string, key: string): boolean {\n const expected = sign(body, key);\n const a = Buffer.from(expected);\n const b = Buffer.from(signature);\n if (a.length !== b.length) return false;\n return timingSafeEqual(a, b);\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport {\n sign,\n verify,\n SIGNATURE_HEADER,\n type InvokeRequest,\n} from \"@durable/shared\";\nimport { runInvocation, type DurableFunction } from \"./function.js\";\n\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (c) => chunks.push(c as Buffer));\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Returns a Node HTTP handler (also usable directly as an Express route) that\n * the Durable service POSTs invocations to. Verifies the HMAC, replays the run,\n * and returns the next operation — itself signed so the service can trust it.\n *\n * Mount this route WITHOUT a JSON body parser; the handler reads the raw stream\n * so it can verify the signature over the exact bytes.\n */\nexport function serve(\n functions: DurableFunction[],\n opts: { signingKey: string },\n) {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n try {\n const raw = await readBody(req);\n const sig = req.headers[SIGNATURE_HEADER];\n if (typeof sig !== \"string\" || !verify(raw, sig, opts.signingKey)) {\n res.writeHead(401).end(\"invalid signature\");\n return;\n }\n\n const body = JSON.parse(raw) as InvokeRequest;\n const fn = functions.find((f) => f.id === body.functionId);\n const response = fn\n ? await runInvocation(fn, body)\n : ({\n op: \"error\" as const,\n id: null,\n message: `unknown function ${body.functionId}`,\n retryable: false,\n });\n\n const out = JSON.stringify(response);\n res\n .writeHead(200, {\n \"content-type\": \"application/json\",\n [SIGNATURE_HEADER]: sign(out, opts.signingKey),\n })\n .end(out);\n } catch (err) {\n res.writeHead(500).end(err instanceof Error ? err.message : String(err));\n }\n };\n}\n","import {\n sign,\n SIGNATURE_HEADER,\n APP_HEADER,\n type SyncRequest,\n type WindowLimit,\n} from \"@durable/shared\";\nimport type { DurableFunction } from \"./function.js\";\nimport { parseDuration } from \"./step.js\";\n\nfunction toWindow(w?: { limit: number; period: string | number; key?: string }): WindowLimit | undefined {\n return w ? { limit: w.limit, periodMs: parseDuration(w.period), key: w.key } : undefined;\n}\n\nexport interface DurableClientOptions {\n /** Base URL of the Durable service. */\n baseUrl: string;\n /** This app's id (from the dashboard). */\n appId: string;\n /** This app's signing key (from the dashboard). */\n signingKey: string;\n /** This app's callback base URL, where the service POSTs invocations. */\n appUrl: string;\n}\n\nexport class DurableClient {\n constructor(private readonly opts: DurableClientOptions) {}\n\n private async post(path: string, payload: unknown): Promise<void> {\n const body = JSON.stringify(payload);\n const res = await fetch(this.opts.baseUrl + path, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [APP_HEADER]: this.opts.appId,\n [SIGNATURE_HEADER]: sign(body, this.opts.signingKey),\n },\n body,\n });\n if (!res.ok) {\n throw new Error(\n `durable ${path} failed: ${res.status} ${await res.text()}`,\n );\n }\n }\n\n /** Register this app's functions and callback URL with the service. */\n async sync(functions: DurableFunction[]): Promise<void> {\n const payload: SyncRequest = {\n url: this.opts.appUrl,\n functions: functions.map((f) => ({\n id: f.id,\n trigger: f.trigger,\n concurrency: f.concurrency,\n priority: f.priority,\n throttle: toWindow(f.throttle),\n rateLimit: toWindow(f.rateLimit),\n debounce: f.debounce\n ? { periodMs: parseDuration(f.debounce.period), key: f.debounce.key }\n : undefined,\n batch: f.batch\n ? { maxSize: f.batch.maxSize, timeoutMs: parseDuration(f.batch.timeout), key: f.batch.key }\n : undefined,\n })),\n };\n await this.post(\"/fn/sync\", payload);\n }\n\n /** Send an event to the service. */\n async send(event: { name: string; data?: unknown }): Promise<void> {\n await this.post(\"/e\", { name: event.name, data: event.data ?? {} });\n }\n}\n"],"mappings":"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","names":["message"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/step.ts","../src/function.ts","../../shared/src/protocol.ts","../../shared/src/hmac.ts","../src/serve.ts","../src/client.ts"],"sourcesContent":["export { createFunction, runInvocation } from \"./function.js\";\nexport type { DurableFunction, FunctionContext } from \"./function.js\";\nexport { createStep, parseDuration, StepInterrupt } from \"./step.js\";\nexport type { StepTools, WaitForEventOptions } from \"./step.js\";\nexport { serve } from \"./serve.js\";\nexport { DurableClient } from \"./client.js\";\nexport type { DurableClientOptions } from \"./client.js\";\n","import type { DurableEvent, InvokeResponse, MemoizedStep } from \"@durable/shared\";\n\n/**\n * Thrown to unwind the handler the moment it reaches a new operation. The\n * carried response is exactly what the SDK reports to the service. Using an\n * exception means at most one *new* step executes per invocation — everything\n * after it is skipped until the service re-invokes with that step memoized.\n */\nexport class StepInterrupt extends Error {\n constructor(public readonly response: InvokeResponse) {\n super(\"durable:step-interrupt\");\n }\n}\n\nexport interface WaitForEventOptions {\n /** The event name to wait for. */\n event: string;\n /** Key/value pairs the incoming event.data must contain to match. */\n match?: Record<string, unknown>;\n /** How long to wait before giving up (e.g. \"7d\", \"1h\", or ms). */\n timeout: string | number;\n}\n\nexport interface StepTools {\n /** Run a side-effecting step once; its result is memoized and replayed. */\n run<T>(id: string, cb: () => Promise<T> | T): Promise<T>;\n /** Pause the run until the duration elapses (e.g. \"10s\", \"2d\", or ms). */\n sleep(id: string, duration: string | number): Promise<void>;\n /**\n * Pause the run until a matching event arrives, or the timeout elapses.\n * Resolves with the matched event, or `null` if it timed out.\n */\n waitForEvent(id: string, opts: WaitForEventOptions): Promise<DurableEvent | null>;\n}\n\nconst UNITS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n};\n\nexport function parseDuration(d: string | number): number {\n if (typeof d === \"number\") return d;\n const m = /^(\\d+)\\s*(ms|s|m|h|d)$/.exec(d.trim());\n if (!m) throw new Error(`invalid duration: ${d}`);\n return Number(m[1]) * UNITS[m[2]];\n}\n\nfunction message(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Build the step tools bound to the steps already memoized for this run. */\nexport function createStep(memoized: Record<string, MemoizedStep>): StepTools {\n return {\n async run<T>(id: string, cb: () => Promise<T> | T): Promise<T> {\n const memo = memoized[id];\n if (memo) {\n if (memo.type !== \"run\") {\n throw new Error(`step \"${id}\" already used as a ${memo.type}`);\n }\n return memo.data as T; // replay: free, no side effect\n }\n let data: T;\n try {\n data = await cb();\n } catch (err) {\n // A genuine failure inside the step: let the service decide on retry.\n throw new StepInterrupt({\n op: \"error\",\n id,\n message: message(err),\n retryable: true,\n });\n }\n throw new StepInterrupt({ op: \"step\", id, data });\n },\n\n async sleep(id: string, duration: string | number): Promise<void> {\n const memo = memoized[id];\n if (memo) {\n if (memo.type !== \"sleep\") {\n throw new Error(`step \"${id}\" already used as a ${memo.type}`);\n }\n return; // sleep already satisfied\n }\n const until = new Date(Date.now() + parseDuration(duration)).toISOString();\n throw new StepInterrupt({ op: \"sleep\", id, until });\n },\n\n async waitForEvent(id, opts): Promise<DurableEvent | null> {\n const memo = memoized[id];\n if (memo) {\n if (memo.type !== \"wait\") {\n throw new Error(`step \"${id}\" already used as a ${memo.type}`);\n }\n return (memo.data as DurableEvent | null) ?? null; // event, or null on timeout\n }\n const until = new Date(Date.now() + parseDuration(opts.timeout)).toISOString();\n throw new StepInterrupt({\n op: \"wait\",\n id,\n event: opts.event,\n match: opts.match ?? null,\n until,\n });\n },\n };\n}\n","import type {\n Concurrency,\n DurableEvent,\n InvokeRequest,\n InvokeResponse,\n Trigger,\n SyncRequest,\n WindowLimit,\n} from \"@durable/shared\";\nimport { createStep, StepInterrupt, type StepTools } from \"./step.js\";\nimport { parseDuration } from \"./step.js\";\n\nexport interface FunctionContext {\n event: DurableEvent;\n step: StepTools;\n}\n\nexport interface DurableFunction {\n id: string;\n /** Triggered by an event name (`{ event }`) or a cron expression (`{ cron }`). */\n trigger: Trigger;\n /** Optional cap on how many runs execute at once (per key, if given). */\n concurrency?: Concurrency;\n /** Higher runs sooner when jobs compete in the queue (default 0). */\n priority?: number;\n /** Spread run starts over time: max `limit` per `period` (excess delayed). */\n throttle?: { limit: number; period: string | number; key?: string };\n /** Cap new runs: max `limit` per `period` (excess dropped). */\n rateLimit?: { limit: number; period: string | number; key?: string };\n /** Collapse rapid events: one run with the LAST event, once quiet for `period`. */\n debounce?: { period: string | number; key?: string };\n /**\n * Group events: ONE run receives event.data = the whole list, flushed at\n * `maxSize` events or `timeout` after the first (event.name = \"$batch\").\n */\n batch?: { maxSize: number; timeout: string | number; key?: string };\n handler: (ctx: FunctionContext) => Promise<unknown>;\n}\n\nexport function createFunction(def: DurableFunction): DurableFunction {\n return def;\n}\n\n/**\n * Replay a single invocation: run the handler from the top, returning the next\n * operation it reaches (a new step, a sleep, completion, or an error).\n */\nexport async function runInvocation(\n fn: DurableFunction,\n req: InvokeRequest,\n): Promise<InvokeResponse> {\n const step = createStep(req.steps);\n try {\n const data = await fn.handler({ event: req.event, step });\n return { op: \"done\", data };\n } catch (err) {\n if (err instanceof StepInterrupt) return err.response;\n // Error thrown outside any step.run (in the handler body itself).\n const message = err instanceof Error ? err.message : String(err);\n return { op: \"error\", id: null, message, retryable: true };\n }\n}\n\nfunction toWindow(w?: { limit: number; period: string | number; key?: string }): WindowLimit | undefined {\n return w ? { limit: w.limit, periodMs: parseDuration(w.period), key: w.key } : undefined;\n}\n\n/** Build the wire manifest for a set of functions (shared by push sync and pull GET). */\nexport function buildManifest(functions: DurableFunction[]): SyncRequest[\"functions\"] {\n return functions.map((f) => ({\n id: f.id,\n trigger: f.trigger,\n concurrency: f.concurrency,\n priority: f.priority,\n throttle: toWindow(f.throttle),\n rateLimit: toWindow(f.rateLimit),\n debounce: f.debounce\n ? { periodMs: parseDuration(f.debounce.period), key: f.debounce.key }\n : undefined,\n batch: f.batch\n ? { maxSize: f.batch.maxSize, timeoutMs: parseDuration(f.batch.timeout), key: f.batch.key }\n : undefined,\n }));\n}\n","/**\n * Wire protocol between the Durable service and the app SDK.\n *\n * One HTTP round-trip advances a run by at most one *new* step. The service\n * sends the run state (event + already-memoized steps); the SDK replays the\n * cached steps for free, executes the next new operation, and reports it back.\n */\n\n/** An event that triggered (or can trigger) a run. */\nexport interface DurableEvent {\n id: string;\n name: string;\n data: unknown;\n}\n\n/** A previously-completed step, keyed by its user-supplied step id. */\nexport interface MemoizedStep {\n /**\n * \"run\" = a step.run result,\n * \"sleep\" = a satisfied step.sleep marker,\n * \"wait\" = a settled step.waitForEvent (data = the event, or null on timeout).\n */\n type: \"run\" | \"sleep\" | \"wait\";\n /** Output of step.run, or the matched event for \"wait\". Null otherwise. */\n data: unknown;\n}\n\n/** Service -> SDK: \"advance this run\". */\nexport interface InvokeRequest {\n runId: string;\n functionId: string;\n event: DurableEvent;\n /** Completed steps so far, keyed by step id. */\n steps: Record<string, MemoizedStep>;\n}\n\n/**\n * SDK -> service: the next operation the function reached.\n *\n * - step : a new step.run executed; persist `data` under `id`, then re-invoke.\n * - sleep : function wants to pause until `until`; arm a timer.\n * - done : function returned; the run is complete with `data`.\n * - error : the current step threw; retry (if retryable) or fail the run.\n */\nexport type InvokeResponse =\n | { op: \"step\"; id: string; data: unknown }\n | { op: \"sleep\"; id: string; until: string /* ISO timestamp */ }\n | {\n op: \"wait\";\n id: string;\n event: string;\n /** Key/value pairs the incoming event.data must contain; null = any. */\n match: Record<string, unknown> | null;\n until: string /* ISO timestamp timeout */;\n }\n | { op: \"done\"; data: unknown }\n | { op: \"error\"; id: string | null; message: string; retryable: boolean };\n\n/** A function is triggered by either an event name or a cron expression. */\nexport type Trigger = { event: string } | { cron: string };\n\n/** Optional concurrency cap for a function. */\nexport interface Concurrency {\n /** Max runs executing at once (per key, if given). */\n limit: number;\n /** event.data field name to scope the limit by (e.g. \"tenantId\"). */\n key?: string;\n}\n\n/** Wire form of a throttle/rate-limit rule (period already in ms). */\nexport interface WindowLimit {\n limit: number;\n periodMs: number;\n key?: string;\n}\n\n/** Wire form of a debounce rule (period already in ms). */\nexport interface DebounceRule {\n periodMs: number;\n key?: string;\n}\n\n/** Wire form of a batch rule (timeout already in ms). */\nexport interface BatchRule {\n maxSize: number;\n timeoutMs: number;\n key?: string;\n}\n\n/** Registry sync: app -> service, \"here are my functions and where to reach me\". */\nexport interface SyncRequest {\n /** Base URL the service should POST invocations to (callback endpoint). */\n url: string;\n functions: Array<{\n id: string;\n trigger: Trigger;\n concurrency?: Concurrency;\n priority?: number;\n throttle?: WindowLimit;\n rateLimit?: WindowLimit;\n debounce?: DebounceRule;\n batch?: BatchRule;\n }>;\n}\n\n/** Service <- app (pull): the app's function manifest, same shape as SyncRequest.functions. */\nexport interface ManifestResponse {\n functions: SyncRequest[\"functions\"];\n}\n\n/** Header carrying the HMAC signature on every service<->app request body. */\nexport const SIGNATURE_HEADER = \"x-durable-signature\";\n\n/** Header carrying the app id on app -> service requests. */\nexport const APP_HEADER = \"x-durable-app\";\n\n/** Header carrying the millisecond timestamp a signed GET (manifest pull) was made at. */\nexport const TIMESTAMP_HEADER = \"x-durable-timestamp\";\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\n\n/** Compute the hex HMAC-SHA256 of a raw request body. */\nexport function sign(body: string, key: string): string {\n return createHmac(\"sha256\", key).update(body).digest(\"hex\");\n}\n\n/** Constant-time verify of a signature against a raw body. */\nexport function verify(body: string, signature: string, key: string): boolean {\n const expected = sign(body, key);\n const a = Buffer.from(expected);\n const b = Buffer.from(signature);\n if (a.length !== b.length) return false;\n return timingSafeEqual(a, b);\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport {\n sign,\n verify,\n SIGNATURE_HEADER,\n TIMESTAMP_HEADER,\n type InvokeRequest,\n} from \"@durable/shared\";\nimport { runInvocation, buildManifest, type DurableFunction } from \"./function.js\";\n\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (c) => chunks.push(c as Buffer));\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Returns a Node HTTP handler (also usable directly as an Express route) that\n * the Durable service POSTs invocations to. Verifies the HMAC, replays the run,\n * and returns the next operation — itself signed so the service can trust it.\n *\n * Mount this route WITHOUT a JSON body parser; the handler reads the raw stream\n * so it can verify the signature over the exact bytes.\n */\nexport function serve(\n functions: DurableFunction[],\n opts: { signingKey: string },\n) {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n try {\n if (req.method === \"GET\") {\n const ts = req.headers[TIMESTAMP_HEADER];\n const sig = req.headers[SIGNATURE_HEADER];\n const fresh = typeof ts === \"string\" && Math.abs(Date.now() - Number(ts)) <= 5 * 60 * 1000;\n if (!fresh || typeof sig !== \"string\" || !verify(ts as string, sig, opts.signingKey)) {\n res.writeHead(401).end(\"invalid signature\");\n return;\n }\n const out = JSON.stringify({ functions: buildManifest(functions) });\n res\n .writeHead(200, {\n \"content-type\": \"application/json\",\n [SIGNATURE_HEADER]: sign(out, opts.signingKey),\n })\n .end(out);\n return;\n }\n\n const raw = await readBody(req);\n const sig = req.headers[SIGNATURE_HEADER];\n if (typeof sig !== \"string\" || !verify(raw, sig, opts.signingKey)) {\n res.writeHead(401).end(\"invalid signature\");\n return;\n }\n\n const body = JSON.parse(raw) as InvokeRequest;\n const fn = functions.find((f) => f.id === body.functionId);\n const response = fn\n ? await runInvocation(fn, body)\n : ({\n op: \"error\" as const,\n id: null,\n message: `unknown function ${body.functionId}`,\n retryable: false,\n });\n\n const out = JSON.stringify(response);\n res\n .writeHead(200, {\n \"content-type\": \"application/json\",\n [SIGNATURE_HEADER]: sign(out, opts.signingKey),\n })\n .end(out);\n } catch (err) {\n res.writeHead(500).end(err instanceof Error ? err.message : String(err));\n }\n };\n}\n","import {\n sign,\n SIGNATURE_HEADER,\n APP_HEADER,\n type SyncRequest,\n} from \"@durable/shared\";\nimport type { DurableFunction } from \"./function.js\";\nimport { buildManifest } from \"./function.js\";\n\nexport interface DurableClientOptions {\n /** Base URL of the Durable service. */\n baseUrl: string;\n /** This app's id (from the dashboard). */\n appId: string;\n /** This app's signing key (from the dashboard). */\n signingKey: string;\n /** This app's callback base URL, where the service POSTs invocations. */\n appUrl: string;\n}\n\nexport class DurableClient {\n constructor(private readonly opts: DurableClientOptions) {}\n\n private async post(path: string, payload: unknown): Promise<void> {\n const body = JSON.stringify(payload);\n const res = await fetch(this.opts.baseUrl + path, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [APP_HEADER]: this.opts.appId,\n [SIGNATURE_HEADER]: sign(body, this.opts.signingKey),\n },\n body,\n });\n if (!res.ok) {\n throw new Error(\n `durable ${path} failed: ${res.status} ${await res.text()}`,\n );\n }\n }\n\n /** Register this app's functions and callback URL with the service. */\n async sync(functions: DurableFunction[]): Promise<void> {\n const payload: SyncRequest = {\n url: this.opts.appUrl,\n functions: buildManifest(functions),\n };\n await this.post(\"/fn/sync\", payload);\n }\n\n /** Send an event to the service. */\n async send(event: { name: string; data?: unknown }): Promise<void> {\n await this.post(\"/e\", { name: event.name, data: event.data ?? {} });\n }\n\n /** Create (or reuse, idempotent by name+env) this app's workspace, sending its appUrl. */\n async provision(opts: { name: string; environment?: string }): Promise<{ id: string; key: string }> {\n const res = await fetch(this.opts.baseUrl + \"/api/apps\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n name: opts.name,\n environment: opts.environment,\n appUrl: this.opts.appUrl,\n }),\n });\n if (!res.ok) throw new Error(`provision failed: ${res.status} ${await res.text()}`);\n return (await res.json()) as { id: string; key: string };\n }\n}\n"],"mappings":"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","names":["message","sig","out"]}
package/dist/index.d.cts CHANGED
@@ -114,6 +114,14 @@ declare class DurableClient {
114
114
  name: string;
115
115
  data?: unknown;
116
116
  }): Promise<void>;
117
+ /** Create (or reuse, idempotent by name+env) this app's workspace, sending its appUrl. */
118
+ provision(opts: {
119
+ name: string;
120
+ environment?: string;
121
+ }): Promise<{
122
+ id: string;
123
+ key: string;
124
+ }>;
117
125
  }
118
126
 
119
127
  export { DurableClient, type DurableClientOptions, type DurableFunction, type FunctionContext, StepInterrupt, type StepTools, type WaitForEventOptions, createFunction, createStep, parseDuration, runInvocation, serve };
package/dist/index.d.ts CHANGED
@@ -114,6 +114,14 @@ declare class DurableClient {
114
114
  name: string;
115
115
  data?: unknown;
116
116
  }): Promise<void>;
117
+ /** Create (or reuse, idempotent by name+env) this app's workspace, sending its appUrl. */
118
+ provision(opts: {
119
+ name: string;
120
+ environment?: string;
121
+ }): Promise<{
122
+ id: string;
123
+ key: string;
124
+ }>;
117
125
  }
118
126
 
119
127
  export { DurableClient, type DurableClientOptions, type DurableFunction, type FunctionContext, StepInterrupt, type StepTools, type WaitForEventOptions, createFunction, createStep, parseDuration, runInvocation, serve };
package/dist/index.js CHANGED
@@ -91,10 +91,26 @@ async function runInvocation(fn, req) {
91
91
  return { op: "error", id: null, message: message2, retryable: true };
92
92
  }
93
93
  }
94
+ function toWindow(w) {
95
+ return w ? { limit: w.limit, periodMs: parseDuration(w.period), key: w.key } : void 0;
96
+ }
97
+ function buildManifest(functions) {
98
+ return functions.map((f) => ({
99
+ id: f.id,
100
+ trigger: f.trigger,
101
+ concurrency: f.concurrency,
102
+ priority: f.priority,
103
+ throttle: toWindow(f.throttle),
104
+ rateLimit: toWindow(f.rateLimit),
105
+ debounce: f.debounce ? { periodMs: parseDuration(f.debounce.period), key: f.debounce.key } : void 0,
106
+ batch: f.batch ? { maxSize: f.batch.maxSize, timeoutMs: parseDuration(f.batch.timeout), key: f.batch.key } : void 0
107
+ }));
108
+ }
94
109
 
95
110
  // ../shared/src/protocol.ts
96
111
  var SIGNATURE_HEADER = "x-durable-signature";
97
112
  var APP_HEADER = "x-durable-app";
113
+ var TIMESTAMP_HEADER = "x-durable-timestamp";
98
114
 
99
115
  // ../shared/src/hmac.ts
100
116
  import { createHmac, timingSafeEqual } from "crypto";
@@ -121,6 +137,21 @@ function readBody(req) {
121
137
  function serve(functions, opts) {
122
138
  return async (req, res) => {
123
139
  try {
140
+ if (req.method === "GET") {
141
+ const ts = req.headers[TIMESTAMP_HEADER];
142
+ const sig2 = req.headers[SIGNATURE_HEADER];
143
+ const fresh = typeof ts === "string" && Math.abs(Date.now() - Number(ts)) <= 5 * 60 * 1e3;
144
+ if (!fresh || typeof sig2 !== "string" || !verify(ts, sig2, opts.signingKey)) {
145
+ res.writeHead(401).end("invalid signature");
146
+ return;
147
+ }
148
+ const out2 = JSON.stringify({ functions: buildManifest(functions) });
149
+ res.writeHead(200, {
150
+ "content-type": "application/json",
151
+ [SIGNATURE_HEADER]: sign(out2, opts.signingKey)
152
+ }).end(out2);
153
+ return;
154
+ }
124
155
  const raw = await readBody(req);
125
156
  const sig = req.headers[SIGNATURE_HEADER];
126
157
  if (typeof sig !== "string" || !verify(raw, sig, opts.signingKey)) {
@@ -147,9 +178,6 @@ function serve(functions, opts) {
147
178
  }
148
179
 
149
180
  // src/client.ts
150
- function toWindow(w) {
151
- return w ? { limit: w.limit, periodMs: parseDuration(w.period), key: w.key } : void 0;
152
- }
153
181
  var DurableClient = class {
154
182
  constructor(opts) {
155
183
  this.opts = opts;
@@ -176,16 +204,7 @@ var DurableClient = class {
176
204
  async sync(functions) {
177
205
  const payload = {
178
206
  url: this.opts.appUrl,
179
- functions: functions.map((f) => ({
180
- id: f.id,
181
- trigger: f.trigger,
182
- concurrency: f.concurrency,
183
- priority: f.priority,
184
- throttle: toWindow(f.throttle),
185
- rateLimit: toWindow(f.rateLimit),
186
- debounce: f.debounce ? { periodMs: parseDuration(f.debounce.period), key: f.debounce.key } : void 0,
187
- batch: f.batch ? { maxSize: f.batch.maxSize, timeoutMs: parseDuration(f.batch.timeout), key: f.batch.key } : void 0
188
- }))
207
+ functions: buildManifest(functions)
189
208
  };
190
209
  await this.post("/fn/sync", payload);
191
210
  }
@@ -193,6 +212,20 @@ var DurableClient = class {
193
212
  async send(event) {
194
213
  await this.post("/e", { name: event.name, data: event.data ?? {} });
195
214
  }
215
+ /** Create (or reuse, idempotent by name+env) this app's workspace, sending its appUrl. */
216
+ async provision(opts) {
217
+ const res = await fetch(this.opts.baseUrl + "/api/apps", {
218
+ method: "POST",
219
+ headers: { "content-type": "application/json" },
220
+ body: JSON.stringify({
221
+ name: opts.name,
222
+ environment: opts.environment,
223
+ appUrl: this.opts.appUrl
224
+ })
225
+ });
226
+ if (!res.ok) throw new Error(`provision failed: ${res.status} ${await res.text()}`);
227
+ return await res.json();
228
+ }
196
229
  };
197
230
  export {
198
231
  DurableClient,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/step.ts","../src/function.ts","../../shared/src/protocol.ts","../../shared/src/hmac.ts","../src/serve.ts","../src/client.ts"],"sourcesContent":["import type { DurableEvent, InvokeResponse, MemoizedStep } from \"@durable/shared\";\n\n/**\n * Thrown to unwind the handler the moment it reaches a new operation. The\n * carried response is exactly what the SDK reports to the service. Using an\n * exception means at most one *new* step executes per invocation — everything\n * after it is skipped until the service re-invokes with that step memoized.\n */\nexport class StepInterrupt extends Error {\n constructor(public readonly response: InvokeResponse) {\n super(\"durable:step-interrupt\");\n }\n}\n\nexport interface WaitForEventOptions {\n /** The event name to wait for. */\n event: string;\n /** Key/value pairs the incoming event.data must contain to match. */\n match?: Record<string, unknown>;\n /** How long to wait before giving up (e.g. \"7d\", \"1h\", or ms). */\n timeout: string | number;\n}\n\nexport interface StepTools {\n /** Run a side-effecting step once; its result is memoized and replayed. */\n run<T>(id: string, cb: () => Promise<T> | T): Promise<T>;\n /** Pause the run until the duration elapses (e.g. \"10s\", \"2d\", or ms). */\n sleep(id: string, duration: string | number): Promise<void>;\n /**\n * Pause the run until a matching event arrives, or the timeout elapses.\n * Resolves with the matched event, or `null` if it timed out.\n */\n waitForEvent(id: string, opts: WaitForEventOptions): Promise<DurableEvent | null>;\n}\n\nconst UNITS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n};\n\nexport function parseDuration(d: string | number): number {\n if (typeof d === \"number\") return d;\n const m = /^(\\d+)\\s*(ms|s|m|h|d)$/.exec(d.trim());\n if (!m) throw new Error(`invalid duration: ${d}`);\n return Number(m[1]) * UNITS[m[2]];\n}\n\nfunction message(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Build the step tools bound to the steps already memoized for this run. */\nexport function createStep(memoized: Record<string, MemoizedStep>): StepTools {\n return {\n async run<T>(id: string, cb: () => Promise<T> | T): Promise<T> {\n const memo = memoized[id];\n if (memo) {\n if (memo.type !== \"run\") {\n throw new Error(`step \"${id}\" already used as a ${memo.type}`);\n }\n return memo.data as T; // replay: free, no side effect\n }\n let data: T;\n try {\n data = await cb();\n } catch (err) {\n // A genuine failure inside the step: let the service decide on retry.\n throw new StepInterrupt({\n op: \"error\",\n id,\n message: message(err),\n retryable: true,\n });\n }\n throw new StepInterrupt({ op: \"step\", id, data });\n },\n\n async sleep(id: string, duration: string | number): Promise<void> {\n const memo = memoized[id];\n if (memo) {\n if (memo.type !== \"sleep\") {\n throw new Error(`step \"${id}\" already used as a ${memo.type}`);\n }\n return; // sleep already satisfied\n }\n const until = new Date(Date.now() + parseDuration(duration)).toISOString();\n throw new StepInterrupt({ op: \"sleep\", id, until });\n },\n\n async waitForEvent(id, opts): Promise<DurableEvent | null> {\n const memo = memoized[id];\n if (memo) {\n if (memo.type !== \"wait\") {\n throw new Error(`step \"${id}\" already used as a ${memo.type}`);\n }\n return (memo.data as DurableEvent | null) ?? null; // event, or null on timeout\n }\n const until = new Date(Date.now() + parseDuration(opts.timeout)).toISOString();\n throw new StepInterrupt({\n op: \"wait\",\n id,\n event: opts.event,\n match: opts.match ?? null,\n until,\n });\n },\n };\n}\n","import type {\n Concurrency,\n DurableEvent,\n InvokeRequest,\n InvokeResponse,\n Trigger,\n} from \"@durable/shared\";\nimport { createStep, StepInterrupt, type StepTools } from \"./step.js\";\n\nexport interface FunctionContext {\n event: DurableEvent;\n step: StepTools;\n}\n\nexport interface DurableFunction {\n id: string;\n /** Triggered by an event name (`{ event }`) or a cron expression (`{ cron }`). */\n trigger: Trigger;\n /** Optional cap on how many runs execute at once (per key, if given). */\n concurrency?: Concurrency;\n /** Higher runs sooner when jobs compete in the queue (default 0). */\n priority?: number;\n /** Spread run starts over time: max `limit` per `period` (excess delayed). */\n throttle?: { limit: number; period: string | number; key?: string };\n /** Cap new runs: max `limit` per `period` (excess dropped). */\n rateLimit?: { limit: number; period: string | number; key?: string };\n /** Collapse rapid events: one run with the LAST event, once quiet for `period`. */\n debounce?: { period: string | number; key?: string };\n /**\n * Group events: ONE run receives event.data = the whole list, flushed at\n * `maxSize` events or `timeout` after the first (event.name = \"$batch\").\n */\n batch?: { maxSize: number; timeout: string | number; key?: string };\n handler: (ctx: FunctionContext) => Promise<unknown>;\n}\n\nexport function createFunction(def: DurableFunction): DurableFunction {\n return def;\n}\n\n/**\n * Replay a single invocation: run the handler from the top, returning the next\n * operation it reaches (a new step, a sleep, completion, or an error).\n */\nexport async function runInvocation(\n fn: DurableFunction,\n req: InvokeRequest,\n): Promise<InvokeResponse> {\n const step = createStep(req.steps);\n try {\n const data = await fn.handler({ event: req.event, step });\n return { op: \"done\", data };\n } catch (err) {\n if (err instanceof StepInterrupt) return err.response;\n // Error thrown outside any step.run (in the handler body itself).\n const message = err instanceof Error ? err.message : String(err);\n return { op: \"error\", id: null, message, retryable: true };\n }\n}\n","/**\n * Wire protocol between the Durable service and the app SDK.\n *\n * One HTTP round-trip advances a run by at most one *new* step. The service\n * sends the run state (event + already-memoized steps); the SDK replays the\n * cached steps for free, executes the next new operation, and reports it back.\n */\n\n/** An event that triggered (or can trigger) a run. */\nexport interface DurableEvent {\n id: string;\n name: string;\n data: unknown;\n}\n\n/** A previously-completed step, keyed by its user-supplied step id. */\nexport interface MemoizedStep {\n /**\n * \"run\" = a step.run result,\n * \"sleep\" = a satisfied step.sleep marker,\n * \"wait\" = a settled step.waitForEvent (data = the event, or null on timeout).\n */\n type: \"run\" | \"sleep\" | \"wait\";\n /** Output of step.run, or the matched event for \"wait\". Null otherwise. */\n data: unknown;\n}\n\n/** Service -> SDK: \"advance this run\". */\nexport interface InvokeRequest {\n runId: string;\n functionId: string;\n event: DurableEvent;\n /** Completed steps so far, keyed by step id. */\n steps: Record<string, MemoizedStep>;\n}\n\n/**\n * SDK -> service: the next operation the function reached.\n *\n * - step : a new step.run executed; persist `data` under `id`, then re-invoke.\n * - sleep : function wants to pause until `until`; arm a timer.\n * - done : function returned; the run is complete with `data`.\n * - error : the current step threw; retry (if retryable) or fail the run.\n */\nexport type InvokeResponse =\n | { op: \"step\"; id: string; data: unknown }\n | { op: \"sleep\"; id: string; until: string /* ISO timestamp */ }\n | {\n op: \"wait\";\n id: string;\n event: string;\n /** Key/value pairs the incoming event.data must contain; null = any. */\n match: Record<string, unknown> | null;\n until: string /* ISO timestamp timeout */;\n }\n | { op: \"done\"; data: unknown }\n | { op: \"error\"; id: string | null; message: string; retryable: boolean };\n\n/** A function is triggered by either an event name or a cron expression. */\nexport type Trigger = { event: string } | { cron: string };\n\n/** Optional concurrency cap for a function. */\nexport interface Concurrency {\n /** Max runs executing at once (per key, if given). */\n limit: number;\n /** event.data field name to scope the limit by (e.g. \"tenantId\"). */\n key?: string;\n}\n\n/** Wire form of a throttle/rate-limit rule (period already in ms). */\nexport interface WindowLimit {\n limit: number;\n periodMs: number;\n key?: string;\n}\n\n/** Wire form of a debounce rule (period already in ms). */\nexport interface DebounceRule {\n periodMs: number;\n key?: string;\n}\n\n/** Wire form of a batch rule (timeout already in ms). */\nexport interface BatchRule {\n maxSize: number;\n timeoutMs: number;\n key?: string;\n}\n\n/** Registry sync: app -> service, \"here are my functions and where to reach me\". */\nexport interface SyncRequest {\n /** Base URL the service should POST invocations to (callback endpoint). */\n url: string;\n functions: Array<{\n id: string;\n trigger: Trigger;\n concurrency?: Concurrency;\n priority?: number;\n throttle?: WindowLimit;\n rateLimit?: WindowLimit;\n debounce?: DebounceRule;\n batch?: BatchRule;\n }>;\n}\n\n/** Header carrying the HMAC signature on every service<->app request body. */\nexport const SIGNATURE_HEADER = \"x-durable-signature\";\n\n/** Header carrying the app id on app -> service requests. */\nexport const APP_HEADER = \"x-durable-app\";\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\n\n/** Compute the hex HMAC-SHA256 of a raw request body. */\nexport function sign(body: string, key: string): string {\n return createHmac(\"sha256\", key).update(body).digest(\"hex\");\n}\n\n/** Constant-time verify of a signature against a raw body. */\nexport function verify(body: string, signature: string, key: string): boolean {\n const expected = sign(body, key);\n const a = Buffer.from(expected);\n const b = Buffer.from(signature);\n if (a.length !== b.length) return false;\n return timingSafeEqual(a, b);\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport {\n sign,\n verify,\n SIGNATURE_HEADER,\n type InvokeRequest,\n} from \"@durable/shared\";\nimport { runInvocation, type DurableFunction } from \"./function.js\";\n\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (c) => chunks.push(c as Buffer));\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Returns a Node HTTP handler (also usable directly as an Express route) that\n * the Durable service POSTs invocations to. Verifies the HMAC, replays the run,\n * and returns the next operation — itself signed so the service can trust it.\n *\n * Mount this route WITHOUT a JSON body parser; the handler reads the raw stream\n * so it can verify the signature over the exact bytes.\n */\nexport function serve(\n functions: DurableFunction[],\n opts: { signingKey: string },\n) {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n try {\n const raw = await readBody(req);\n const sig = req.headers[SIGNATURE_HEADER];\n if (typeof sig !== \"string\" || !verify(raw, sig, opts.signingKey)) {\n res.writeHead(401).end(\"invalid signature\");\n return;\n }\n\n const body = JSON.parse(raw) as InvokeRequest;\n const fn = functions.find((f) => f.id === body.functionId);\n const response = fn\n ? await runInvocation(fn, body)\n : ({\n op: \"error\" as const,\n id: null,\n message: `unknown function ${body.functionId}`,\n retryable: false,\n });\n\n const out = JSON.stringify(response);\n res\n .writeHead(200, {\n \"content-type\": \"application/json\",\n [SIGNATURE_HEADER]: sign(out, opts.signingKey),\n })\n .end(out);\n } catch (err) {\n res.writeHead(500).end(err instanceof Error ? err.message : String(err));\n }\n };\n}\n","import {\n sign,\n SIGNATURE_HEADER,\n APP_HEADER,\n type SyncRequest,\n type WindowLimit,\n} from \"@durable/shared\";\nimport type { DurableFunction } from \"./function.js\";\nimport { parseDuration } from \"./step.js\";\n\nfunction toWindow(w?: { limit: number; period: string | number; key?: string }): WindowLimit | undefined {\n return w ? { limit: w.limit, periodMs: parseDuration(w.period), key: w.key } : undefined;\n}\n\nexport interface DurableClientOptions {\n /** Base URL of the Durable service. */\n baseUrl: string;\n /** This app's id (from the dashboard). */\n appId: string;\n /** This app's signing key (from the dashboard). */\n signingKey: string;\n /** This app's callback base URL, where the service POSTs invocations. */\n appUrl: string;\n}\n\nexport class DurableClient {\n constructor(private readonly opts: DurableClientOptions) {}\n\n private async post(path: string, payload: unknown): Promise<void> {\n const body = JSON.stringify(payload);\n const res = await fetch(this.opts.baseUrl + path, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [APP_HEADER]: this.opts.appId,\n [SIGNATURE_HEADER]: sign(body, this.opts.signingKey),\n },\n body,\n });\n if (!res.ok) {\n throw new Error(\n `durable ${path} failed: ${res.status} ${await res.text()}`,\n );\n }\n }\n\n /** Register this app's functions and callback URL with the service. */\n async sync(functions: DurableFunction[]): Promise<void> {\n const payload: SyncRequest = {\n url: this.opts.appUrl,\n functions: functions.map((f) => ({\n id: f.id,\n trigger: f.trigger,\n concurrency: f.concurrency,\n priority: f.priority,\n throttle: toWindow(f.throttle),\n rateLimit: toWindow(f.rateLimit),\n debounce: f.debounce\n ? { periodMs: parseDuration(f.debounce.period), key: f.debounce.key }\n : undefined,\n batch: f.batch\n ? { maxSize: f.batch.maxSize, timeoutMs: parseDuration(f.batch.timeout), key: f.batch.key }\n : undefined,\n })),\n };\n await this.post(\"/fn/sync\", payload);\n }\n\n /** Send an event to the service. */\n async send(event: { name: string; data?: unknown }): Promise<void> {\n await this.post(\"/e\", { name: event.name, data: event.data ?? {} });\n }\n}\n"],"mappings":"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","names":["message"]}
1
+ {"version":3,"sources":["../src/step.ts","../src/function.ts","../../shared/src/protocol.ts","../../shared/src/hmac.ts","../src/serve.ts","../src/client.ts"],"sourcesContent":["import type { DurableEvent, InvokeResponse, MemoizedStep } from \"@durable/shared\";\n\n/**\n * Thrown to unwind the handler the moment it reaches a new operation. The\n * carried response is exactly what the SDK reports to the service. Using an\n * exception means at most one *new* step executes per invocation — everything\n * after it is skipped until the service re-invokes with that step memoized.\n */\nexport class StepInterrupt extends Error {\n constructor(public readonly response: InvokeResponse) {\n super(\"durable:step-interrupt\");\n }\n}\n\nexport interface WaitForEventOptions {\n /** The event name to wait for. */\n event: string;\n /** Key/value pairs the incoming event.data must contain to match. */\n match?: Record<string, unknown>;\n /** How long to wait before giving up (e.g. \"7d\", \"1h\", or ms). */\n timeout: string | number;\n}\n\nexport interface StepTools {\n /** Run a side-effecting step once; its result is memoized and replayed. */\n run<T>(id: string, cb: () => Promise<T> | T): Promise<T>;\n /** Pause the run until the duration elapses (e.g. \"10s\", \"2d\", or ms). */\n sleep(id: string, duration: string | number): Promise<void>;\n /**\n * Pause the run until a matching event arrives, or the timeout elapses.\n * Resolves with the matched event, or `null` if it timed out.\n */\n waitForEvent(id: string, opts: WaitForEventOptions): Promise<DurableEvent | null>;\n}\n\nconst UNITS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n};\n\nexport function parseDuration(d: string | number): number {\n if (typeof d === \"number\") return d;\n const m = /^(\\d+)\\s*(ms|s|m|h|d)$/.exec(d.trim());\n if (!m) throw new Error(`invalid duration: ${d}`);\n return Number(m[1]) * UNITS[m[2]];\n}\n\nfunction message(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Build the step tools bound to the steps already memoized for this run. */\nexport function createStep(memoized: Record<string, MemoizedStep>): StepTools {\n return {\n async run<T>(id: string, cb: () => Promise<T> | T): Promise<T> {\n const memo = memoized[id];\n if (memo) {\n if (memo.type !== \"run\") {\n throw new Error(`step \"${id}\" already used as a ${memo.type}`);\n }\n return memo.data as T; // replay: free, no side effect\n }\n let data: T;\n try {\n data = await cb();\n } catch (err) {\n // A genuine failure inside the step: let the service decide on retry.\n throw new StepInterrupt({\n op: \"error\",\n id,\n message: message(err),\n retryable: true,\n });\n }\n throw new StepInterrupt({ op: \"step\", id, data });\n },\n\n async sleep(id: string, duration: string | number): Promise<void> {\n const memo = memoized[id];\n if (memo) {\n if (memo.type !== \"sleep\") {\n throw new Error(`step \"${id}\" already used as a ${memo.type}`);\n }\n return; // sleep already satisfied\n }\n const until = new Date(Date.now() + parseDuration(duration)).toISOString();\n throw new StepInterrupt({ op: \"sleep\", id, until });\n },\n\n async waitForEvent(id, opts): Promise<DurableEvent | null> {\n const memo = memoized[id];\n if (memo) {\n if (memo.type !== \"wait\") {\n throw new Error(`step \"${id}\" already used as a ${memo.type}`);\n }\n return (memo.data as DurableEvent | null) ?? null; // event, or null on timeout\n }\n const until = new Date(Date.now() + parseDuration(opts.timeout)).toISOString();\n throw new StepInterrupt({\n op: \"wait\",\n id,\n event: opts.event,\n match: opts.match ?? null,\n until,\n });\n },\n };\n}\n","import type {\n Concurrency,\n DurableEvent,\n InvokeRequest,\n InvokeResponse,\n Trigger,\n SyncRequest,\n WindowLimit,\n} from \"@durable/shared\";\nimport { createStep, StepInterrupt, type StepTools } from \"./step.js\";\nimport { parseDuration } from \"./step.js\";\n\nexport interface FunctionContext {\n event: DurableEvent;\n step: StepTools;\n}\n\nexport interface DurableFunction {\n id: string;\n /** Triggered by an event name (`{ event }`) or a cron expression (`{ cron }`). */\n trigger: Trigger;\n /** Optional cap on how many runs execute at once (per key, if given). */\n concurrency?: Concurrency;\n /** Higher runs sooner when jobs compete in the queue (default 0). */\n priority?: number;\n /** Spread run starts over time: max `limit` per `period` (excess delayed). */\n throttle?: { limit: number; period: string | number; key?: string };\n /** Cap new runs: max `limit` per `period` (excess dropped). */\n rateLimit?: { limit: number; period: string | number; key?: string };\n /** Collapse rapid events: one run with the LAST event, once quiet for `period`. */\n debounce?: { period: string | number; key?: string };\n /**\n * Group events: ONE run receives event.data = the whole list, flushed at\n * `maxSize` events or `timeout` after the first (event.name = \"$batch\").\n */\n batch?: { maxSize: number; timeout: string | number; key?: string };\n handler: (ctx: FunctionContext) => Promise<unknown>;\n}\n\nexport function createFunction(def: DurableFunction): DurableFunction {\n return def;\n}\n\n/**\n * Replay a single invocation: run the handler from the top, returning the next\n * operation it reaches (a new step, a sleep, completion, or an error).\n */\nexport async function runInvocation(\n fn: DurableFunction,\n req: InvokeRequest,\n): Promise<InvokeResponse> {\n const step = createStep(req.steps);\n try {\n const data = await fn.handler({ event: req.event, step });\n return { op: \"done\", data };\n } catch (err) {\n if (err instanceof StepInterrupt) return err.response;\n // Error thrown outside any step.run (in the handler body itself).\n const message = err instanceof Error ? err.message : String(err);\n return { op: \"error\", id: null, message, retryable: true };\n }\n}\n\nfunction toWindow(w?: { limit: number; period: string | number; key?: string }): WindowLimit | undefined {\n return w ? { limit: w.limit, periodMs: parseDuration(w.period), key: w.key } : undefined;\n}\n\n/** Build the wire manifest for a set of functions (shared by push sync and pull GET). */\nexport function buildManifest(functions: DurableFunction[]): SyncRequest[\"functions\"] {\n return functions.map((f) => ({\n id: f.id,\n trigger: f.trigger,\n concurrency: f.concurrency,\n priority: f.priority,\n throttle: toWindow(f.throttle),\n rateLimit: toWindow(f.rateLimit),\n debounce: f.debounce\n ? { periodMs: parseDuration(f.debounce.period), key: f.debounce.key }\n : undefined,\n batch: f.batch\n ? { maxSize: f.batch.maxSize, timeoutMs: parseDuration(f.batch.timeout), key: f.batch.key }\n : undefined,\n }));\n}\n","/**\n * Wire protocol between the Durable service and the app SDK.\n *\n * One HTTP round-trip advances a run by at most one *new* step. The service\n * sends the run state (event + already-memoized steps); the SDK replays the\n * cached steps for free, executes the next new operation, and reports it back.\n */\n\n/** An event that triggered (or can trigger) a run. */\nexport interface DurableEvent {\n id: string;\n name: string;\n data: unknown;\n}\n\n/** A previously-completed step, keyed by its user-supplied step id. */\nexport interface MemoizedStep {\n /**\n * \"run\" = a step.run result,\n * \"sleep\" = a satisfied step.sleep marker,\n * \"wait\" = a settled step.waitForEvent (data = the event, or null on timeout).\n */\n type: \"run\" | \"sleep\" | \"wait\";\n /** Output of step.run, or the matched event for \"wait\". Null otherwise. */\n data: unknown;\n}\n\n/** Service -> SDK: \"advance this run\". */\nexport interface InvokeRequest {\n runId: string;\n functionId: string;\n event: DurableEvent;\n /** Completed steps so far, keyed by step id. */\n steps: Record<string, MemoizedStep>;\n}\n\n/**\n * SDK -> service: the next operation the function reached.\n *\n * - step : a new step.run executed; persist `data` under `id`, then re-invoke.\n * - sleep : function wants to pause until `until`; arm a timer.\n * - done : function returned; the run is complete with `data`.\n * - error : the current step threw; retry (if retryable) or fail the run.\n */\nexport type InvokeResponse =\n | { op: \"step\"; id: string; data: unknown }\n | { op: \"sleep\"; id: string; until: string /* ISO timestamp */ }\n | {\n op: \"wait\";\n id: string;\n event: string;\n /** Key/value pairs the incoming event.data must contain; null = any. */\n match: Record<string, unknown> | null;\n until: string /* ISO timestamp timeout */;\n }\n | { op: \"done\"; data: unknown }\n | { op: \"error\"; id: string | null; message: string; retryable: boolean };\n\n/** A function is triggered by either an event name or a cron expression. */\nexport type Trigger = { event: string } | { cron: string };\n\n/** Optional concurrency cap for a function. */\nexport interface Concurrency {\n /** Max runs executing at once (per key, if given). */\n limit: number;\n /** event.data field name to scope the limit by (e.g. \"tenantId\"). */\n key?: string;\n}\n\n/** Wire form of a throttle/rate-limit rule (period already in ms). */\nexport interface WindowLimit {\n limit: number;\n periodMs: number;\n key?: string;\n}\n\n/** Wire form of a debounce rule (period already in ms). */\nexport interface DebounceRule {\n periodMs: number;\n key?: string;\n}\n\n/** Wire form of a batch rule (timeout already in ms). */\nexport interface BatchRule {\n maxSize: number;\n timeoutMs: number;\n key?: string;\n}\n\n/** Registry sync: app -> service, \"here are my functions and where to reach me\". */\nexport interface SyncRequest {\n /** Base URL the service should POST invocations to (callback endpoint). */\n url: string;\n functions: Array<{\n id: string;\n trigger: Trigger;\n concurrency?: Concurrency;\n priority?: number;\n throttle?: WindowLimit;\n rateLimit?: WindowLimit;\n debounce?: DebounceRule;\n batch?: BatchRule;\n }>;\n}\n\n/** Service <- app (pull): the app's function manifest, same shape as SyncRequest.functions. */\nexport interface ManifestResponse {\n functions: SyncRequest[\"functions\"];\n}\n\n/** Header carrying the HMAC signature on every service<->app request body. */\nexport const SIGNATURE_HEADER = \"x-durable-signature\";\n\n/** Header carrying the app id on app -> service requests. */\nexport const APP_HEADER = \"x-durable-app\";\n\n/** Header carrying the millisecond timestamp a signed GET (manifest pull) was made at. */\nexport const TIMESTAMP_HEADER = \"x-durable-timestamp\";\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\n\n/** Compute the hex HMAC-SHA256 of a raw request body. */\nexport function sign(body: string, key: string): string {\n return createHmac(\"sha256\", key).update(body).digest(\"hex\");\n}\n\n/** Constant-time verify of a signature against a raw body. */\nexport function verify(body: string, signature: string, key: string): boolean {\n const expected = sign(body, key);\n const a = Buffer.from(expected);\n const b = Buffer.from(signature);\n if (a.length !== b.length) return false;\n return timingSafeEqual(a, b);\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport {\n sign,\n verify,\n SIGNATURE_HEADER,\n TIMESTAMP_HEADER,\n type InvokeRequest,\n} from \"@durable/shared\";\nimport { runInvocation, buildManifest, type DurableFunction } from \"./function.js\";\n\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (c) => chunks.push(c as Buffer));\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Returns a Node HTTP handler (also usable directly as an Express route) that\n * the Durable service POSTs invocations to. Verifies the HMAC, replays the run,\n * and returns the next operation — itself signed so the service can trust it.\n *\n * Mount this route WITHOUT a JSON body parser; the handler reads the raw stream\n * so it can verify the signature over the exact bytes.\n */\nexport function serve(\n functions: DurableFunction[],\n opts: { signingKey: string },\n) {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n try {\n if (req.method === \"GET\") {\n const ts = req.headers[TIMESTAMP_HEADER];\n const sig = req.headers[SIGNATURE_HEADER];\n const fresh = typeof ts === \"string\" && Math.abs(Date.now() - Number(ts)) <= 5 * 60 * 1000;\n if (!fresh || typeof sig !== \"string\" || !verify(ts as string, sig, opts.signingKey)) {\n res.writeHead(401).end(\"invalid signature\");\n return;\n }\n const out = JSON.stringify({ functions: buildManifest(functions) });\n res\n .writeHead(200, {\n \"content-type\": \"application/json\",\n [SIGNATURE_HEADER]: sign(out, opts.signingKey),\n })\n .end(out);\n return;\n }\n\n const raw = await readBody(req);\n const sig = req.headers[SIGNATURE_HEADER];\n if (typeof sig !== \"string\" || !verify(raw, sig, opts.signingKey)) {\n res.writeHead(401).end(\"invalid signature\");\n return;\n }\n\n const body = JSON.parse(raw) as InvokeRequest;\n const fn = functions.find((f) => f.id === body.functionId);\n const response = fn\n ? await runInvocation(fn, body)\n : ({\n op: \"error\" as const,\n id: null,\n message: `unknown function ${body.functionId}`,\n retryable: false,\n });\n\n const out = JSON.stringify(response);\n res\n .writeHead(200, {\n \"content-type\": \"application/json\",\n [SIGNATURE_HEADER]: sign(out, opts.signingKey),\n })\n .end(out);\n } catch (err) {\n res.writeHead(500).end(err instanceof Error ? err.message : String(err));\n }\n };\n}\n","import {\n sign,\n SIGNATURE_HEADER,\n APP_HEADER,\n type SyncRequest,\n} from \"@durable/shared\";\nimport type { DurableFunction } from \"./function.js\";\nimport { buildManifest } from \"./function.js\";\n\nexport interface DurableClientOptions {\n /** Base URL of the Durable service. */\n baseUrl: string;\n /** This app's id (from the dashboard). */\n appId: string;\n /** This app's signing key (from the dashboard). */\n signingKey: string;\n /** This app's callback base URL, where the service POSTs invocations. */\n appUrl: string;\n}\n\nexport class DurableClient {\n constructor(private readonly opts: DurableClientOptions) {}\n\n private async post(path: string, payload: unknown): Promise<void> {\n const body = JSON.stringify(payload);\n const res = await fetch(this.opts.baseUrl + path, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n [APP_HEADER]: this.opts.appId,\n [SIGNATURE_HEADER]: sign(body, this.opts.signingKey),\n },\n body,\n });\n if (!res.ok) {\n throw new Error(\n `durable ${path} failed: ${res.status} ${await res.text()}`,\n );\n }\n }\n\n /** Register this app's functions and callback URL with the service. */\n async sync(functions: DurableFunction[]): Promise<void> {\n const payload: SyncRequest = {\n url: this.opts.appUrl,\n functions: buildManifest(functions),\n };\n await this.post(\"/fn/sync\", payload);\n }\n\n /** Send an event to the service. */\n async send(event: { name: string; data?: unknown }): Promise<void> {\n await this.post(\"/e\", { name: event.name, data: event.data ?? {} });\n }\n\n /** Create (or reuse, idempotent by name+env) this app's workspace, sending its appUrl. */\n async provision(opts: { name: string; environment?: string }): Promise<{ id: string; key: string }> {\n const res = await fetch(this.opts.baseUrl + \"/api/apps\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n name: opts.name,\n environment: opts.environment,\n appUrl: this.opts.appUrl,\n }),\n });\n if (!res.ok) throw new Error(`provision failed: ${res.status} ${await res.text()}`);\n return (await res.json()) as { id: string; key: string };\n }\n}\n"],"mappings":"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","names":["message","sig","out"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dicabrio/durable-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "TypeScript SDK for the Durable execution engine: define durable functions, sync them, send events, and serve the replay callback.",
6
6
  "license": "MIT",