@dicabrio/durable-sdk 0.1.0 → 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/README.md +21 -2
- package/dist/index.cjs +55 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +54 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,8 +54,10 @@ import { onboarding } from "./onboarding.js";
|
|
|
54
54
|
const signingKey = process.env.DURABLE_APP_KEY!;
|
|
55
55
|
|
|
56
56
|
const app = express();
|
|
57
|
-
//
|
|
58
|
-
app.
|
|
57
|
+
// serve() answers POST (invocations) and GET (the function manifest, for pull
|
|
58
|
+
// registration); mount with app.all so both reach it. It verifies the HMAC
|
|
59
|
+
// over the raw body itself — don't add a JSON body parser on this route.
|
|
60
|
+
app.all("/api/durable", serve([onboarding], { signingKey }));
|
|
59
61
|
app.listen(4000);
|
|
60
62
|
|
|
61
63
|
const client = new DurableClient({
|
|
@@ -70,6 +72,23 @@ await client.sync([onboarding]);
|
|
|
70
72
|
await client.send({ name: "user.created", data: { id: "alice" } });
|
|
71
73
|
```
|
|
72
74
|
|
|
75
|
+
## Registration: push or pull
|
|
76
|
+
|
|
77
|
+
`serve()` also answers a signed `GET` with your function manifest, so the
|
|
78
|
+
service can **pull** your registration instead of you pushing it. You give
|
|
79
|
+
Durable the app URL once — `provision()` creates the workspace and sends the
|
|
80
|
+
client's `appUrl` in one call:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// Idempotent by (name, environment); returns this app's id + signing key.
|
|
84
|
+
const { id, key } = await client.provision({ name: "billing", environment: "prod" });
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The service then fetches the manifest from your app on boot, when the URL is
|
|
88
|
+
set or changed, nightly, and from the dashboard's Refresh button. `sync()`
|
|
89
|
+
(push) stays fully supported — both paths produce identical registration, so
|
|
90
|
+
you can use whichever fits (push from CI, pull for hands-off dev).
|
|
91
|
+
|
|
73
92
|
## Step primitives
|
|
74
93
|
|
|
75
94
|
| Primitive | What it does |
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
DurableClient: () => DurableClient,
|
|
24
|
+
SDK_VERSION: () => SDK_VERSION,
|
|
24
25
|
StepInterrupt: () => StepInterrupt,
|
|
25
26
|
createFunction: () => createFunction,
|
|
26
27
|
createStep: () => createStep,
|
|
@@ -123,10 +124,26 @@ async function runInvocation(fn, req) {
|
|
|
123
124
|
return { op: "error", id: null, message: message2, retryable: true };
|
|
124
125
|
}
|
|
125
126
|
}
|
|
127
|
+
function toWindow(w) {
|
|
128
|
+
return w ? { limit: w.limit, periodMs: parseDuration(w.period), key: w.key } : void 0;
|
|
129
|
+
}
|
|
130
|
+
function buildManifest(functions) {
|
|
131
|
+
return functions.map((f) => ({
|
|
132
|
+
id: f.id,
|
|
133
|
+
trigger: f.trigger,
|
|
134
|
+
concurrency: f.concurrency,
|
|
135
|
+
priority: f.priority,
|
|
136
|
+
throttle: toWindow(f.throttle),
|
|
137
|
+
rateLimit: toWindow(f.rateLimit),
|
|
138
|
+
debounce: f.debounce ? { periodMs: parseDuration(f.debounce.period), key: f.debounce.key } : void 0,
|
|
139
|
+
batch: f.batch ? { maxSize: f.batch.maxSize, timeoutMs: parseDuration(f.batch.timeout), key: f.batch.key } : void 0
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
126
142
|
|
|
127
143
|
// ../shared/src/protocol.ts
|
|
128
144
|
var SIGNATURE_HEADER = "x-durable-signature";
|
|
129
145
|
var APP_HEADER = "x-durable-app";
|
|
146
|
+
var TIMESTAMP_HEADER = "x-durable-timestamp";
|
|
130
147
|
|
|
131
148
|
// ../shared/src/hmac.ts
|
|
132
149
|
var import_node_crypto = require("crypto");
|
|
@@ -141,6 +158,9 @@ function verify(body, signature, key) {
|
|
|
141
158
|
return (0, import_node_crypto.timingSafeEqual)(a, b);
|
|
142
159
|
}
|
|
143
160
|
|
|
161
|
+
// src/version.ts
|
|
162
|
+
var SDK_VERSION = true ? "0.2.1" : "dev";
|
|
163
|
+
|
|
144
164
|
// src/serve.ts
|
|
145
165
|
function readBody(req) {
|
|
146
166
|
return new Promise((resolve, reject) => {
|
|
@@ -153,6 +173,24 @@ function readBody(req) {
|
|
|
153
173
|
function serve(functions, opts) {
|
|
154
174
|
return async (req, res) => {
|
|
155
175
|
try {
|
|
176
|
+
if (req.method === "GET") {
|
|
177
|
+
const ts = req.headers[TIMESTAMP_HEADER];
|
|
178
|
+
const sig2 = req.headers[SIGNATURE_HEADER];
|
|
179
|
+
const fresh = typeof ts === "string" && Math.abs(Date.now() - Number(ts)) <= 5 * 60 * 1e3;
|
|
180
|
+
if (!fresh || typeof sig2 !== "string" || !verify(ts, sig2, opts.signingKey)) {
|
|
181
|
+
res.writeHead(401).end("invalid signature");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const out2 = JSON.stringify({
|
|
185
|
+
functions: buildManifest(functions),
|
|
186
|
+
sdkVersion: SDK_VERSION
|
|
187
|
+
});
|
|
188
|
+
res.writeHead(200, {
|
|
189
|
+
"content-type": "application/json",
|
|
190
|
+
[SIGNATURE_HEADER]: sign(out2, opts.signingKey)
|
|
191
|
+
}).end(out2);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
156
194
|
const raw = await readBody(req);
|
|
157
195
|
const sig = req.headers[SIGNATURE_HEADER];
|
|
158
196
|
if (typeof sig !== "string" || !verify(raw, sig, opts.signingKey)) {
|
|
@@ -179,9 +217,6 @@ function serve(functions, opts) {
|
|
|
179
217
|
}
|
|
180
218
|
|
|
181
219
|
// src/client.ts
|
|
182
|
-
function toWindow(w) {
|
|
183
|
-
return w ? { limit: w.limit, periodMs: parseDuration(w.period), key: w.key } : void 0;
|
|
184
|
-
}
|
|
185
220
|
var DurableClient = class {
|
|
186
221
|
constructor(opts) {
|
|
187
222
|
this.opts = opts;
|
|
@@ -208,16 +243,8 @@ var DurableClient = class {
|
|
|
208
243
|
async sync(functions) {
|
|
209
244
|
const payload = {
|
|
210
245
|
url: this.opts.appUrl,
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
}))
|
|
246
|
+
sdkVersion: SDK_VERSION,
|
|
247
|
+
functions: buildManifest(functions)
|
|
221
248
|
};
|
|
222
249
|
await this.post("/fn/sync", payload);
|
|
223
250
|
}
|
|
@@ -225,10 +252,25 @@ var DurableClient = class {
|
|
|
225
252
|
async send(event) {
|
|
226
253
|
await this.post("/e", { name: event.name, data: event.data ?? {} });
|
|
227
254
|
}
|
|
255
|
+
/** Create (or reuse, idempotent by name+env) this app's workspace, sending its appUrl. */
|
|
256
|
+
async provision(opts) {
|
|
257
|
+
const res = await fetch(this.opts.baseUrl + "/api/apps", {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers: { "content-type": "application/json" },
|
|
260
|
+
body: JSON.stringify({
|
|
261
|
+
name: opts.name,
|
|
262
|
+
environment: opts.environment,
|
|
263
|
+
appUrl: this.opts.appUrl
|
|
264
|
+
})
|
|
265
|
+
});
|
|
266
|
+
if (!res.ok) throw new Error(`provision failed: ${res.status} ${await res.text()}`);
|
|
267
|
+
return await res.json();
|
|
268
|
+
}
|
|
228
269
|
};
|
|
229
270
|
// Annotate the CommonJS export names for ESM import in node:
|
|
230
271
|
0 && (module.exports = {
|
|
231
272
|
DurableClient,
|
|
273
|
+
SDK_VERSION,
|
|
232
274
|
StepInterrupt,
|
|
233
275
|
createFunction,
|
|
234
276
|
createStep,
|
package/dist/index.cjs.map
CHANGED
|
@@ -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/version.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\";\nexport { SDK_VERSION } from \"./version.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 /** SDK version reporting in (\"dev\" for the monorepo's tsx path); absent for older/non-JS SDKs. */\n sdkVersion?: 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 /** SDK version reporting in (\"dev\" for the monorepo's tsx path); absent for older/non-JS SDKs. */\n sdkVersion?: string;\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","// Baked in by tsup's esbuild `define` (see tsup.config.ts) from package.json's\n// version at build time. Undefined under the monorepo's zero-build tsx path.\ndeclare const __SDK_VERSION__: string | undefined;\n\nexport const SDK_VERSION =\n typeof __SDK_VERSION__ !== \"undefined\" ? __SDK_VERSION__ : \"dev\";\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\";\nimport { SDK_VERSION } from \"./version.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({\n functions: buildManifest(functions),\n sdkVersion: SDK_VERSION,\n });\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\";\nimport { SDK_VERSION } from \"./version.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 sdkVersion: SDK_VERSION,\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,16 @@ 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
|
+
declare const SDK_VERSION: string;
|
|
128
|
+
|
|
129
|
+
export { DurableClient, type DurableClientOptions, type DurableFunction, type FunctionContext, SDK_VERSION, StepInterrupt, type StepTools, type WaitForEventOptions, createFunction, createStep, parseDuration, runInvocation, serve };
|
package/dist/index.d.ts
CHANGED
|
@@ -114,6 +114,16 @@ 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
|
+
declare const SDK_VERSION: string;
|
|
128
|
+
|
|
129
|
+
export { DurableClient, type DurableClientOptions, type DurableFunction, type FunctionContext, SDK_VERSION, 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";
|
|
@@ -109,6 +125,9 @@ function verify(body, signature, key) {
|
|
|
109
125
|
return timingSafeEqual(a, b);
|
|
110
126
|
}
|
|
111
127
|
|
|
128
|
+
// src/version.ts
|
|
129
|
+
var SDK_VERSION = true ? "0.2.1" : "dev";
|
|
130
|
+
|
|
112
131
|
// src/serve.ts
|
|
113
132
|
function readBody(req) {
|
|
114
133
|
return new Promise((resolve, reject) => {
|
|
@@ -121,6 +140,24 @@ function readBody(req) {
|
|
|
121
140
|
function serve(functions, opts) {
|
|
122
141
|
return async (req, res) => {
|
|
123
142
|
try {
|
|
143
|
+
if (req.method === "GET") {
|
|
144
|
+
const ts = req.headers[TIMESTAMP_HEADER];
|
|
145
|
+
const sig2 = req.headers[SIGNATURE_HEADER];
|
|
146
|
+
const fresh = typeof ts === "string" && Math.abs(Date.now() - Number(ts)) <= 5 * 60 * 1e3;
|
|
147
|
+
if (!fresh || typeof sig2 !== "string" || !verify(ts, sig2, opts.signingKey)) {
|
|
148
|
+
res.writeHead(401).end("invalid signature");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const out2 = JSON.stringify({
|
|
152
|
+
functions: buildManifest(functions),
|
|
153
|
+
sdkVersion: SDK_VERSION
|
|
154
|
+
});
|
|
155
|
+
res.writeHead(200, {
|
|
156
|
+
"content-type": "application/json",
|
|
157
|
+
[SIGNATURE_HEADER]: sign(out2, opts.signingKey)
|
|
158
|
+
}).end(out2);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
124
161
|
const raw = await readBody(req);
|
|
125
162
|
const sig = req.headers[SIGNATURE_HEADER];
|
|
126
163
|
if (typeof sig !== "string" || !verify(raw, sig, opts.signingKey)) {
|
|
@@ -147,9 +184,6 @@ function serve(functions, opts) {
|
|
|
147
184
|
}
|
|
148
185
|
|
|
149
186
|
// src/client.ts
|
|
150
|
-
function toWindow(w) {
|
|
151
|
-
return w ? { limit: w.limit, periodMs: parseDuration(w.period), key: w.key } : void 0;
|
|
152
|
-
}
|
|
153
187
|
var DurableClient = class {
|
|
154
188
|
constructor(opts) {
|
|
155
189
|
this.opts = opts;
|
|
@@ -176,16 +210,8 @@ var DurableClient = class {
|
|
|
176
210
|
async sync(functions) {
|
|
177
211
|
const payload = {
|
|
178
212
|
url: this.opts.appUrl,
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
}))
|
|
213
|
+
sdkVersion: SDK_VERSION,
|
|
214
|
+
functions: buildManifest(functions)
|
|
189
215
|
};
|
|
190
216
|
await this.post("/fn/sync", payload);
|
|
191
217
|
}
|
|
@@ -193,9 +219,24 @@ var DurableClient = class {
|
|
|
193
219
|
async send(event) {
|
|
194
220
|
await this.post("/e", { name: event.name, data: event.data ?? {} });
|
|
195
221
|
}
|
|
222
|
+
/** Create (or reuse, idempotent by name+env) this app's workspace, sending its appUrl. */
|
|
223
|
+
async provision(opts) {
|
|
224
|
+
const res = await fetch(this.opts.baseUrl + "/api/apps", {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: { "content-type": "application/json" },
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
name: opts.name,
|
|
229
|
+
environment: opts.environment,
|
|
230
|
+
appUrl: this.opts.appUrl
|
|
231
|
+
})
|
|
232
|
+
});
|
|
233
|
+
if (!res.ok) throw new Error(`provision failed: ${res.status} ${await res.text()}`);
|
|
234
|
+
return await res.json();
|
|
235
|
+
}
|
|
196
236
|
};
|
|
197
237
|
export {
|
|
198
238
|
DurableClient,
|
|
239
|
+
SDK_VERSION,
|
|
199
240
|
StepInterrupt,
|
|
200
241
|
createFunction,
|
|
201
242
|
createStep,
|
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/version.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 /** SDK version reporting in (\"dev\" for the monorepo's tsx path); absent for older/non-JS SDKs. */\n sdkVersion?: 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 /** SDK version reporting in (\"dev\" for the monorepo's tsx path); absent for older/non-JS SDKs. */\n sdkVersion?: string;\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","// Baked in by tsup's esbuild `define` (see tsup.config.ts) from package.json's\n// version at build time. Undefined under the monorepo's zero-build tsx path.\ndeclare const __SDK_VERSION__: string | undefined;\n\nexport const SDK_VERSION =\n typeof __SDK_VERSION__ !== \"undefined\" ? __SDK_VERSION__ : \"dev\";\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\";\nimport { SDK_VERSION } from \"./version.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({\n functions: buildManifest(functions),\n sdkVersion: SDK_VERSION,\n });\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\";\nimport { SDK_VERSION } from \"./version.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 sdkVersion: SDK_VERSION,\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
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|