@wayai/executor-sdk 0.1.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.js ADDED
@@ -0,0 +1,382 @@
1
+ // src/core/hmac.ts
2
+ async function signPayload(secret, payload) {
3
+ const key = await crypto.subtle.importKey(
4
+ "raw",
5
+ new TextEncoder().encode(secret),
6
+ { name: "HMAC", hash: "SHA-256" },
7
+ false,
8
+ ["sign"]
9
+ );
10
+ const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload));
11
+ return arrayBufferToHex(signature);
12
+ }
13
+ function timingSafeEqual(a, b) {
14
+ if (a.length !== b.length) return false;
15
+ let result = 0;
16
+ for (let i = 0; i < a.length; i++) result |= a.charCodeAt(i) ^ b.charCodeAt(i);
17
+ return result === 0;
18
+ }
19
+ function arrayBufferToHex(buffer) {
20
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
21
+ }
22
+
23
+ // src/core/headers.ts
24
+ function headerGetter(headers) {
25
+ if (headers instanceof Headers) return (name) => headers.get(name) ?? void 0;
26
+ const lower = {};
27
+ for (const [k, v] of Object.entries(headers)) lower[k.toLowerCase()] = v;
28
+ return (name) => {
29
+ const v = lower[name];
30
+ if (Array.isArray(v)) return v.length === 1 ? v[0] : void 0;
31
+ return v;
32
+ };
33
+ }
34
+
35
+ // src/core/canonical.ts
36
+ var SIGNING_SCHEME = "v1";
37
+ function buildCanonical(parts) {
38
+ return [SIGNING_SCHEME, parts.id, String(parts.timestamp), parts.method, parts.path, parts.body].join("\n");
39
+ }
40
+ function unixSeconds(nowMs) {
41
+ return Math.floor((nowMs ?? Date.now()) / 1e3);
42
+ }
43
+
44
+ // src/core/verify.ts
45
+ var DATA_HEADERS = {
46
+ signature: "x-data-signature",
47
+ timestamp: "x-data-timestamp",
48
+ id: "x-data-id",
49
+ idempotencyKey: "x-data-idempotency-key",
50
+ deliveryId: "x-data-delivery-id",
51
+ // Executor credential pull: advertised on a proxied dispatch to a source declaring executor_secrets.
52
+ credentialUrl: "x-data-credential-url",
53
+ credentialToken: "x-data-credential-token",
54
+ credentialNames: "x-data-credential-names"
55
+ };
56
+ var MAX_SIG_ENTRIES = 4;
57
+ async function verifySignatureHeader(secret, parts, header) {
58
+ const canonical = buildCanonical(parts);
59
+ let expected;
60
+ let checked = 0;
61
+ let matched = false;
62
+ for (const entry of header.split(" ")) {
63
+ const comma = entry.indexOf(",");
64
+ if (comma <= 0) continue;
65
+ if (entry.slice(0, comma) !== SIGNING_SCHEME) continue;
66
+ const hex = entry.slice(comma + 1);
67
+ if (hex.length === 0) continue;
68
+ if (++checked > MAX_SIG_ENTRIES) break;
69
+ if (expected === void 0) expected = await signPayload(secret, canonical);
70
+ if (timingSafeEqual(expected, hex)) matched = true;
71
+ }
72
+ return matched;
73
+ }
74
+ async function verify(input) {
75
+ const get = headerGetter(input.headers);
76
+ const signature = get(DATA_HEADERS.signature);
77
+ if (!signature) return { ok: false, reason: "missing_signature" };
78
+ const tsRaw = get(DATA_HEADERS.timestamp);
79
+ const id = get(DATA_HEADERS.id);
80
+ if (!tsRaw || !id) return { ok: false, reason: "malformed" };
81
+ if (!/^\d+$/.test(tsRaw)) return { ok: false, reason: "malformed" };
82
+ const timestamp = Number(tsRaw);
83
+ if (!Number.isFinite(timestamp)) return { ok: false, reason: "malformed" };
84
+ const t = input.toleranceSec;
85
+ const tolerance = Number.isFinite(t) ? Math.max(1, Math.floor(t)) : 300;
86
+ const now = unixSeconds(input.now);
87
+ if (Math.abs(now - timestamp) > tolerance) return { ok: false, reason: "stale_timestamp" };
88
+ const parts = { id, timestamp, method: input.method, path: input.path, body: input.rawBody };
89
+ const valid = await verifySignatureHeader(input.secret, parts, signature);
90
+ if (!valid) return { ok: false, reason: "invalid_signature" };
91
+ return {
92
+ ok: true,
93
+ id,
94
+ timestamp,
95
+ idempotencyKey: get(DATA_HEADERS.idempotencyKey),
96
+ deliveryId: get(DATA_HEADERS.deliveryId),
97
+ body: input.rawBody
98
+ };
99
+ }
100
+
101
+ // src/store/memory.ts
102
+ var DEFAULT_SWEEP_EVERY = 1e3;
103
+ var MemoryStore = class {
104
+ map = /* @__PURE__ */ new Map();
105
+ nowMs;
106
+ sweepEvery;
107
+ writes = 0;
108
+ constructor(opts) {
109
+ this.nowMs = opts?.now ?? (() => Date.now());
110
+ this.sweepEvery = opts?.sweepEvery ?? DEFAULT_SWEEP_EVERY;
111
+ }
112
+ /** Number of live entries currently held (after lazily ignoring nothing — counts raw map size). */
113
+ get size() {
114
+ return this.map.size;
115
+ }
116
+ live(key) {
117
+ const e = this.map.get(key);
118
+ if (!e) return void 0;
119
+ if (e.expiresAt <= this.nowMs()) {
120
+ this.map.delete(key);
121
+ return void 0;
122
+ }
123
+ return e;
124
+ }
125
+ /** Amortized O(1)/write: every SWEEP_EVERY writes, drop all expired entries so the map can't grow forever. */
126
+ maybeSweep() {
127
+ if (++this.writes < this.sweepEvery) return;
128
+ this.writes = 0;
129
+ const now = this.nowMs();
130
+ for (const [k, e] of this.map) if (e.expiresAt <= now) this.map.delete(k);
131
+ }
132
+ async claim(key, ttlSec) {
133
+ const e = this.live(key);
134
+ if (!e) {
135
+ this.map.set(key, { state: "pending", expiresAt: this.nowMs() + ttlSec * 1e3 });
136
+ this.maybeSweep();
137
+ return { state: "fresh" };
138
+ }
139
+ if (e.state === "done" && e.result) return { state: "done", result: e.result };
140
+ return { state: "in_progress" };
141
+ }
142
+ async complete(key, result, ttlSec) {
143
+ this.map.set(key, { state: "done", result, expiresAt: this.nowMs() + ttlSec * 1e3 });
144
+ this.maybeSweep();
145
+ }
146
+ async fail(key) {
147
+ this.map.delete(key);
148
+ }
149
+ };
150
+
151
+ // src/errors/envelope.ts
152
+ var ExecutorError = class extends Error {
153
+ code;
154
+ retriable;
155
+ httpStatus;
156
+ constructor(args) {
157
+ super(args.message);
158
+ this.name = "ExecutorError";
159
+ this.code = args.code;
160
+ this.retriable = args.retriable;
161
+ this.httpStatus = args.status;
162
+ }
163
+ toEnvelope() {
164
+ return { status: toHttpStatus(this), code: this.code, message: this.message, retriable: this.retriable };
165
+ }
166
+ };
167
+ function toHttpStatus(e) {
168
+ const s = e.httpStatus;
169
+ if (e.retriable) {
170
+ return s !== void 0 && (s === 429 || s >= 500 && s <= 599) ? s : 503;
171
+ }
172
+ return s !== void 0 && s >= 400 && s <= 499 ? s : 400;
173
+ }
174
+ function jsonResponseBody(env) {
175
+ return JSON.stringify(env);
176
+ }
177
+
178
+ // src/executor.ts
179
+ var JSON_CT = "application/json";
180
+ function errorResponse(env) {
181
+ return { status: env.status, body: jsonResponseBody(env), contentType: JSON_CT };
182
+ }
183
+ function reasonResponse(reason) {
184
+ const status = reason === "malformed" ? 400 : reason === "stale_timestamp" ? 403 : 401;
185
+ return errorResponse({ status, code: reason.toUpperCase(), message: `signature ${reason}`, retriable: false });
186
+ }
187
+ function bypassContext(req) {
188
+ const get = headerGetter(req.headers);
189
+ const id = get(DATA_HEADERS.id);
190
+ const tsRaw = get(DATA_HEADERS.timestamp);
191
+ return {
192
+ id: id ?? "",
193
+ idempotencyKey: get(DATA_HEADERS.idempotencyKey),
194
+ deliveryId: get(DATA_HEADERS.deliveryId),
195
+ timestamp: tsRaw && /^\d+$/.test(tsRaw) ? Number(tsRaw) : 0,
196
+ body: req.rawBody
197
+ };
198
+ }
199
+ function createExecutor(opts) {
200
+ const store = opts.store ?? new MemoryStore();
201
+ const ttl = opts.idempotencyTtlSec ?? 86400;
202
+ async function run(ctx) {
203
+ const key = ctx.idempotencyKey || ctx.id;
204
+ if (!key) return await invoke(ctx);
205
+ const claim = await store.claim(key, ttl);
206
+ if (claim.state === "done") {
207
+ return { status: claim.result.status, body: claim.result.body, contentType: claim.result.contentType ?? JSON_CT };
208
+ }
209
+ if (claim.state === "in_progress") {
210
+ return errorResponse({ status: 503, code: "IN_PROGRESS", message: "duplicate delivery in progress", retriable: true });
211
+ }
212
+ try {
213
+ const res = await invoke(ctx);
214
+ await store.complete(key, { status: res.status, body: res.body, contentType: res.contentType }, ttl);
215
+ return res;
216
+ } catch (err) {
217
+ const env = toEnvelope(err);
218
+ const res = errorResponse(env);
219
+ if (env.retriable) {
220
+ await store.fail(key);
221
+ } else {
222
+ await store.complete(key, { status: res.status, body: res.body, contentType: res.contentType }, ttl);
223
+ }
224
+ if (!(err instanceof ExecutorError)) opts.onError?.(err, ctx);
225
+ return res;
226
+ }
227
+ }
228
+ async function invoke(ctx) {
229
+ const result = await opts.handler(ctx);
230
+ if (!result) return { status: 204, body: "", contentType: JSON_CT };
231
+ return { status: result.status, body: result.body, contentType: result.contentType ?? JSON_CT };
232
+ }
233
+ return {
234
+ async handle(req) {
235
+ const path = opts.pathOverride ? opts.pathOverride(req) : req.path;
236
+ if (opts.devBypass) {
237
+ const ctx = bypassContext(req);
238
+ return ctx ? await run(ctx) : reasonResponse("malformed");
239
+ }
240
+ const result = await verify({
241
+ secret: opts.secret,
242
+ headers: req.headers,
243
+ method: req.method,
244
+ path,
245
+ rawBody: req.rawBody,
246
+ toleranceSec: opts.toleranceSec
247
+ });
248
+ if (!result.ok) return reasonResponse(result.reason);
249
+ return await run({
250
+ id: result.id,
251
+ idempotencyKey: result.idempotencyKey,
252
+ deliveryId: result.deliveryId,
253
+ timestamp: result.timestamp,
254
+ body: result.body
255
+ });
256
+ }
257
+ };
258
+ }
259
+ function toEnvelope(err) {
260
+ if (err instanceof ExecutorError) return err.toEnvelope();
261
+ return { status: toHttpStatus({ retriable: true }), code: "INTERNAL", message: "handler error", retriable: true };
262
+ }
263
+
264
+ // src/adapters/fetch.ts
265
+ function toFetchHandler(executor) {
266
+ return async (req) => {
267
+ const rawBody = await req.text();
268
+ const url = new URL(req.url);
269
+ const res = await executor.handle({
270
+ method: req.method,
271
+ path: url.pathname + url.search,
272
+ headers: req.headers,
273
+ rawBody
274
+ });
275
+ return new Response(res.body || null, { status: res.status, headers: { "content-type": res.contentType } });
276
+ };
277
+ }
278
+
279
+ // src/adapters/node.ts
280
+ function toNodeHandler(executor) {
281
+ return async (req, res) => {
282
+ const chunks = [];
283
+ for await (const chunk of req) {
284
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
285
+ }
286
+ const rawBody = Buffer.concat(chunks).toString("utf8");
287
+ const out = await executor.handle({
288
+ method: req.method ?? "GET",
289
+ path: req.url ?? "/",
290
+ headers: req.headers,
291
+ rawBody
292
+ });
293
+ res.writeHead(out.status, { "content-type": out.contentType });
294
+ res.end(out.body);
295
+ };
296
+ }
297
+
298
+ // src/core/sign.ts
299
+ async function signRequest(input) {
300
+ const id = input.id ?? crypto.randomUUID();
301
+ const timestamp = input.timestamp ?? unixSeconds();
302
+ const body = input.body ?? "";
303
+ const { pathname, search } = new URL(input.url);
304
+ const hex = await signPayload(input.secret, buildCanonical({ id, timestamp, method: input.method, path: pathname + search, body }));
305
+ return {
306
+ "X-Data-Signature": `${SIGNING_SCHEME},${hex}`,
307
+ "X-Data-Timestamp": String(timestamp),
308
+ "X-Data-Id": id
309
+ };
310
+ }
311
+
312
+ // src/core/credential.ts
313
+ function resolveHeaders(source) {
314
+ if (source instanceof Headers) return source;
315
+ const nested = source.headers;
316
+ if (nested instanceof Headers) return nested;
317
+ if (nested && typeof nested === "object") return nested;
318
+ return source;
319
+ }
320
+ async function fetchExecutorCredential(source, name, opts = {}) {
321
+ const get = headerGetter(resolveHeaders(source));
322
+ const url = get(DATA_HEADERS.credentialUrl);
323
+ const token = get(DATA_HEADERS.credentialToken);
324
+ const namesHeader = get(DATA_HEADERS.credentialNames);
325
+ if (!url || !token || namesHeader === void 0) {
326
+ throw new ExecutorError({ code: "no_credential_grant", message: "Request carries no executor credential grant", retriable: false });
327
+ }
328
+ if (opts.allowedOrigins && opts.allowedOrigins.length > 0) {
329
+ let origin;
330
+ try {
331
+ origin = new URL(url).origin;
332
+ } catch {
333
+ throw new ExecutorError({ code: "credential_bad_url", message: "Credential callback URL is not a valid URL", retriable: false });
334
+ }
335
+ const allowed = opts.allowedOrigins.map((o) => {
336
+ try {
337
+ return new URL(o).origin;
338
+ } catch {
339
+ return o;
340
+ }
341
+ });
342
+ if (!allowed.includes(origin)) {
343
+ throw new ExecutorError({ code: "credential_untrusted_url", message: `Credential callback URL origin '${origin}' is not in allowedOrigins`, retriable: false });
344
+ }
345
+ }
346
+ const names = namesHeader.split(",").map((n) => n.trim()).filter(Boolean);
347
+ if (!names.includes(name)) {
348
+ throw new ExecutorError({ code: "undeclared_credential", message: `Credential '${name}' is not in the grant (declared: ${names.join(", ") || "none"})`, retriable: false });
349
+ }
350
+ const doFetch = opts.fetchImpl ?? fetch;
351
+ const body = JSON.stringify({ token, name });
352
+ for (let attempt = 0; attempt < 2; attempt++) {
353
+ let res;
354
+ try {
355
+ res = await doFetch(url, { method: "POST", headers: { "content-type": "application/json" }, body, redirect: "manual" });
356
+ } catch {
357
+ if (attempt === 0) continue;
358
+ throw new ExecutorError({ code: "credential_unreachable", message: "Credential pull is unreachable", retriable: true });
359
+ }
360
+ if (res.status === 403) {
361
+ throw new ExecutorError({ code: "credential_denied", message: `Credential '${name}' pull was denied (grant invalid, expired, or already used)`, retriable: false, status: 403 });
362
+ }
363
+ if (res.status >= 500) {
364
+ if (attempt === 0) continue;
365
+ throw new ExecutorError({ code: "credential_unavailable", message: `Credential pull failed (status ${res.status})`, retriable: true, status: res.status });
366
+ }
367
+ if (!res.ok) {
368
+ throw new ExecutorError({ code: "credential_fetch_failed", message: `Credential pull failed (status ${res.status})`, retriable: false, status: res.status });
369
+ }
370
+ const parsed = await res.json().catch(() => null);
371
+ const data = parsed?.data;
372
+ if (!data || typeof data.value !== "string") {
373
+ throw new ExecutorError({ code: "credential_malformed", message: "Credential pull returned an unexpected body", retriable: false });
374
+ }
375
+ return { name: data.name ?? name, contentType: data.content_type ?? "application/octet-stream", value: data.value };
376
+ }
377
+ throw new ExecutorError({ code: "credential_unavailable", message: "Credential pull failed", retriable: true });
378
+ }
379
+
380
+ export { DATA_HEADERS, ExecutorError, MAX_SIG_ENTRIES, MemoryStore, SIGNING_SCHEME, buildCanonical, createExecutor, fetchExecutorCredential, jsonResponseBody, signRequest, toFetchHandler, toHttpStatus, toNodeHandler, unixSeconds, verify, verifySignatureHeader };
381
+ //# sourceMappingURL=index.js.map
382
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/hmac.ts","../src/core/headers.ts","../src/core/canonical.ts","../src/core/verify.ts","../src/store/memory.ts","../src/errors/envelope.ts","../src/executor.ts","../src/adapters/fetch.ts","../src/adapters/node.ts","../src/core/sign.ts","../src/core/credential.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * HMAC-SHA256 over Web Crypto — pure, dependency-free, runs in any runtime with `crypto.subtle`\n * (Node ≥20, Cloudflare Workers, Deno, Bun, browsers). Ported verbatim from Rekor's backend\n * `workers/shared/src/hmac.ts`; the cross-package conformance vector (test/conformance.test.ts)\n * guards against drift.\n */\n\nexport async function signPayload(secret: string, payload: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload));\n return arrayBufferToHex(signature);\n}\n\n/** Constant-time-ish equality of two hex strings (length-checks first, then XOR-accumulate). */\nexport function timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let result = 0;\n for (let i = 0; i < a.length; i++) result |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return result === 0;\n}\n\nfunction arrayBufferToHex(buffer: ArrayBuffer): string {\n return Array.from(new Uint8Array(buffer))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n","/** Case-insensitive header reader for a Fetch `Headers` or a plain object. A duplicate (array) value is\n * ambiguous and resolves to `undefined` (the signature/timestamp must be unambiguous). Shared by the\n * verify path and the dev-bypass path so they read headers identically. */\nexport type HeaderGetter = (name: string) => string | undefined;\n\nexport function headerGetter(headers: Headers | Record<string, string | string[] | undefined>): HeaderGetter {\n if (headers instanceof Headers) return (name) => headers.get(name) ?? undefined;\n const lower: Record<string, string | string[] | undefined> = {};\n for (const [k, v] of Object.entries(headers)) lower[k.toLowerCase()] = v;\n return (name) => {\n const v = lower[name];\n if (Array.isArray(v)) return v.length === 1 ? v[0] : undefined;\n return v;\n };\n}\n","/**\n * Rekor \"Signing v1\" canonical — the exact string Rekor HMAC-signs for every request it sends\n * (trigger webhooks + signed proxied calls). Ported verbatim from the backend's\n * `workers/shared/src/signing.ts`; kept byte-identical by the cross-package conformance vector.\n *\n * canonical = [\"v1\", id, String(unixTimestamp), method, path, body].join(\"\\n\")\n *\n * SECURITY INVARIANT: newline-joined; the only data-influenced field, `body`, is LAST, and every\n * other field is provably newline-free (the \"v1\" literal, an integer, a uuid/hex id, an HTTP method\n * token, and a URL pathname+search). The signature binds method + path so it can't be replayed as a\n * different request.\n */\n\nexport const SIGNING_SCHEME = 'v1';\n\nexport interface SignedParts {\n /** Stable message id — `X-Data-Id` (= delivery id on webhooks, idempotency key on proxied writes). */\n id: string;\n /** `X-Data-Timestamp`, unix seconds (integer). */\n timestamp: number;\n /** HTTP method (`POST` for trigger webhooks). */\n method: string;\n /** Request path: `new URL(receiverUrl).pathname + search`. */\n path: string;\n /** Exact raw request body string (`\"\"` when none). */\n body: string;\n}\n\n/** Build the canonical signing string. Component order and the `v1` prefix are part of the wire. */\nexport function buildCanonical(parts: SignedParts): string {\n return [SIGNING_SCHEME, parts.id, String(parts.timestamp), parts.method, parts.path, parts.body].join('\\n');\n}\n\n/** Current unix time in whole seconds (Standard Webhooks convention). `nowMs` override for tests. */\nexport function unixSeconds(nowMs?: number): number {\n return Math.floor((nowMs ?? Date.now()) / 1000);\n}\n","/**\n * Verify a Rekor \"Signing v1\" request: HMAC over the canonical, plus timestamp-skew enforcement\n * (the receiver's job — Rekor only emits). All inputs are the raw request bits the adapter extracts.\n */\n\nimport { signPayload, timingSafeEqual } from './hmac.js';\nimport { headerGetter } from './headers.js';\nimport { SIGNING_SCHEME, buildCanonical, unixSeconds, type SignedParts } from './canonical.js';\n\n/** Lowercased header names Rekor emits. */\nexport const DATA_HEADERS = {\n signature: 'x-data-signature',\n timestamp: 'x-data-timestamp',\n id: 'x-data-id',\n idempotencyKey: 'x-data-idempotency-key',\n deliveryId: 'x-data-delivery-id',\n // Executor credential pull: advertised on a proxied dispatch to a source declaring executor_secrets.\n credentialUrl: 'x-data-credential-url',\n credentialToken: 'x-data-credential-token',\n credentialNames: 'x-data-credential-names',\n} as const;\n\n/**\n * Max `scheme,hex` candidates we'll HMAC-check from one `X-Data-Signature` header. Rekor emits one\n * (two during a future key rotation); the cap stops a hostile header with thousands of entries from\n * forcing thousands of HMACs per request.\n */\nexport const MAX_SIG_ENTRIES = 4;\n\n/**\n * Verify a signature header value (`X-Data-Signature`) against the canonical of `parts`. The header\n * is a space-delimited list of `scheme,hex` entries; returns true if any `v1` entry matches (constant-\n * time compare). Malformed/empty/unknown-scheme entries are skipped, never throw. Candidates beyond\n * `MAX_SIG_ENTRIES` are ignored. Does NOT check timestamp skew (see {@link verify}).\n */\nexport async function verifySignatureHeader(secret: string, parts: SignedParts, header: string): Promise<boolean> {\n const canonical = buildCanonical(parts);\n let expected: string | undefined; // loop-invariant HMAC — computed at most once, lazily\n let checked = 0;\n let matched = false;\n for (const entry of header.split(' ')) {\n const comma = entry.indexOf(',');\n if (comma <= 0) continue; // no scheme/hex separator (e.g. \"v1\" or \"\")\n if (entry.slice(0, comma) !== SIGNING_SCHEME) continue;\n const hex = entry.slice(comma + 1);\n if (hex.length === 0) continue;\n if (++checked > MAX_SIG_ENTRIES) break;\n if (expected === undefined) expected = await signPayload(secret, canonical);\n if (timingSafeEqual(expected, hex)) matched = true;\n }\n return matched;\n}\n\nexport type VerifyResult =\n | { ok: true; id: string; timestamp: number; idempotencyKey?: string; deliveryId?: string; body: string }\n | { ok: false; reason: 'missing_signature' | 'invalid_signature' | 'stale_timestamp' | 'malformed' };\n\nexport interface VerifyInput {\n /** The HMAC signing secret shared with Rekor (the trigger/source signing secret). */\n secret: string;\n /** Request headers — a Fetch `Headers` or a plain object (case-insensitive lookup either way). */\n headers: Headers | Record<string, string | string[] | undefined>;\n /** HTTP method of the incoming request. */\n method: string;\n /** Request path the signature was computed over: `pathname + search` (see README path-matching). */\n path: string;\n /** RAW request body bytes as a string — NEVER a re-stringified parsed object. */\n rawBody: string;\n /** Allowed clock skew in seconds (both directions). A finite value is clamped to ≥1 (never 0);\n * unset / NaN / Infinity fall back to the 300s default (a bad value can't disable the window). */\n toleranceSec?: number;\n /** Override \"now\" in ms (tests). */\n now?: number;\n}\n\n/**\n * Verify an inbound Rekor request end to end: signature present + valid, and timestamp within the\n * skew window. Returns a discriminated result; never throws on bad input.\n */\nexport async function verify(input: VerifyInput): Promise<VerifyResult> {\n const get = headerGetter(input.headers);\n const signature = get(DATA_HEADERS.signature);\n if (!signature) return { ok: false, reason: 'missing_signature' };\n\n const tsRaw = get(DATA_HEADERS.timestamp);\n const id = get(DATA_HEADERS.id);\n if (!tsRaw || !id) return { ok: false, reason: 'malformed' };\n if (!/^\\d+$/.test(tsRaw)) return { ok: false, reason: 'malformed' };\n const timestamp = Number(tsRaw);\n if (!Number.isFinite(timestamp)) return { ok: false, reason: 'malformed' };\n\n // Non-finite (unset / NaN / Infinity, e.g. `Number(process.env.X)` with X missing) → the 300s\n // default, so a misconfigured tolerance can never silently disable the replay window. A finite value\n // is clamped to ≥1 (never 0). To accept a wider window, pass an explicit large finite number.\n const t = input.toleranceSec;\n const tolerance = Number.isFinite(t) ? Math.max(1, Math.floor(t as number)) : 300;\n const now = unixSeconds(input.now);\n if (Math.abs(now - timestamp) > tolerance) return { ok: false, reason: 'stale_timestamp' };\n\n const parts: SignedParts = { id, timestamp, method: input.method, path: input.path, body: input.rawBody };\n const valid = await verifySignatureHeader(input.secret, parts, signature);\n if (!valid) return { ok: false, reason: 'invalid_signature' };\n\n return {\n ok: true,\n id,\n timestamp,\n idempotencyKey: get(DATA_HEADERS.idempotencyKey),\n deliveryId: get(DATA_HEADERS.deliveryId),\n body: input.rawBody,\n };\n}\n","import type { ClaimResult, IdempotencyStore, StoredResult } from './types.js';\n\ninterface Entry {\n state: 'pending' | 'done';\n result?: StoredResult;\n expiresAt: number; // epoch ms\n}\n\n/**\n * In-memory idempotency store (per-process). Single-threaded JS gives the atomic first-wins claim for\n * free. An accessed key expires lazily; since every delivery has a unique key, completed entries are\n * otherwise dropped by a periodic full sweep (every {@link SWEEP_EVERY} writes) — so the map is bounded\n * by the deliveries within one TTL window (default 24h), not unbounded. For multiple instances, or to\n * cap memory tightly, implement {@link IdempotencyStore} over a shared backend instead.\n */\nconst DEFAULT_SWEEP_EVERY = 1000;\n\nexport class MemoryStore implements IdempotencyStore {\n private readonly map = new Map<string, Entry>();\n private readonly nowMs: () => number;\n private readonly sweepEvery: number;\n private writes = 0;\n\n constructor(opts?: { now?: () => number; sweepEvery?: number }) {\n this.nowMs = opts?.now ?? (() => Date.now());\n this.sweepEvery = opts?.sweepEvery ?? DEFAULT_SWEEP_EVERY;\n }\n\n /** Number of live entries currently held (after lazily ignoring nothing — counts raw map size). */\n get size(): number {\n return this.map.size;\n }\n\n private live(key: string): Entry | undefined {\n const e = this.map.get(key);\n if (!e) return undefined;\n if (e.expiresAt <= this.nowMs()) {\n this.map.delete(key);\n return undefined;\n }\n return e;\n }\n\n /** Amortized O(1)/write: every SWEEP_EVERY writes, drop all expired entries so the map can't grow forever. */\n private maybeSweep(): void {\n if (++this.writes < this.sweepEvery) return;\n this.writes = 0;\n const now = this.nowMs();\n for (const [k, e] of this.map) if (e.expiresAt <= now) this.map.delete(k);\n }\n\n async claim(key: string, ttlSec: number): Promise<ClaimResult> {\n const e = this.live(key);\n if (!e) {\n this.map.set(key, { state: 'pending', expiresAt: this.nowMs() + ttlSec * 1000 });\n this.maybeSweep();\n return { state: 'fresh' };\n }\n if (e.state === 'done' && e.result) return { state: 'done', result: e.result };\n return { state: 'in_progress' };\n }\n\n async complete(key: string, result: StoredResult, ttlSec: number): Promise<void> {\n this.map.set(key, { state: 'done', result, expiresAt: this.nowMs() + ttlSec * 1000 });\n this.maybeSweep();\n }\n\n async fail(key: string): Promise<void> {\n this.map.delete(key);\n }\n}\n","/**\n * The normalized error envelope an executor returns. The HTTP STATUS is the real contract a sender's\n * retry logic reads (2xx ack, 4xx permanent/no-retry, 5xx & 429 retry); the JSON body is for humans.\n */\nexport interface ErrorEnvelope {\n status: number;\n code: string;\n message: string;\n retriable: boolean;\n}\n\n/**\n * Throw this from a handler to fail with a specific code/retriability. `retriable` decides the status\n * family ({@link toHttpStatus}) so the sender retries (5xx/429) or drops (4xx) correctly.\n */\nexport class ExecutorError extends Error {\n readonly code: string;\n readonly retriable: boolean;\n readonly httpStatus?: number;\n\n constructor(args: { code: string; message: string; retriable: boolean; status?: number }) {\n super(args.message);\n this.name = 'ExecutorError';\n this.code = args.code;\n this.retriable = args.retriable;\n this.httpStatus = args.status;\n }\n\n toEnvelope(): ErrorEnvelope {\n return { status: toHttpStatus(this), code: this.code, message: this.message, retriable: this.retriable };\n }\n}\n\n/**\n * Map an error to an HTTP status. An explicit `status` is honored when it agrees with `retriable`\n * (5xx/429 for retriable, 4xx for permanent); otherwise the family default is used: retriable → 503,\n * permanent → 400.\n */\nexport function toHttpStatus(e: { retriable: boolean; httpStatus?: number }): number {\n const s = e.httpStatus;\n if (e.retriable) {\n return s !== undefined && (s === 429 || (s >= 500 && s <= 599)) ? s : 503;\n }\n return s !== undefined && s >= 400 && s <= 499 ? s : 400;\n}\n\n/** Serialize an envelope to a JSON body string. */\nexport function jsonResponseBody(env: ErrorEnvelope): string {\n return JSON.stringify(env);\n}\n","import { verify, DATA_HEADERS, type VerifyResult } from './core/verify.js';\nimport { headerGetter } from './core/headers.js';\nimport { MemoryStore } from './store/memory.js';\nimport type { IdempotencyStore, StoredResult } from './store/types.js';\nimport { ExecutorError, toHttpStatus, jsonResponseBody, type ErrorEnvelope } from './errors/envelope.js';\n\n/** Framework-neutral request the adapters hand to the executor. `rawBody` is the RAW bytes as a string. */\nexport interface NormalizedRequest {\n method: string;\n path: string;\n headers: Headers | Record<string, string | string[] | undefined>;\n rawBody: string;\n}\n\nexport interface NormalizedResponse {\n status: number;\n body: string;\n contentType: string;\n}\n\n/** Verified request context passed to your handler. Never contains the signing secret. */\nexport interface ExecutorContext {\n id: string;\n idempotencyKey?: string;\n deliveryId?: string;\n timestamp: number;\n body: string;\n}\n\n/** Your business logic. Return a `StoredResult` (cached for idempotent replays) or nothing (→ 204). */\nexport type ExecutorHandler = (ctx: ExecutorContext) => Promise<StoredResult | void> | StoredResult | void;\n\nexport interface CreateExecutorOptions {\n /** HMAC signing secret shared with Rekor. */\n secret: string;\n handler: ExecutorHandler;\n /** Idempotency store. Default: a per-process {@link MemoryStore}. */\n store?: IdempotencyStore;\n /** Clock-skew tolerance (seconds, both directions). Default 300; clamped to ≥1. */\n toleranceSec?: number;\n /** Skip signature/skew verification (LOCAL DEV ONLY). Dedup still applies if headers are present. */\n devBypass?: boolean;\n /** Idempotency record TTL (seconds). Default 86400 (24h). */\n idempotencyTtlSec?: number;\n /** Reports unexpected handler errors. MUST NOT log secrets/PII — it never receives the secret. */\n onError?: (err: unknown, ctx?: ExecutorContext) => void;\n /**\n * Reproduce the path Rekor SIGNED when a reverse proxy rewrites it. Must return the exact\n * `pathname+search` Rekor used (see README path-matching), not necessarily the incoming path.\n */\n pathOverride?: (req: NormalizedRequest) => string;\n}\n\nexport interface Executor {\n handle(req: NormalizedRequest): Promise<NormalizedResponse>;\n}\n\nconst JSON_CT = 'application/json';\n\nfunction errorResponse(env: ErrorEnvelope): NormalizedResponse {\n return { status: env.status, body: jsonResponseBody(env), contentType: JSON_CT };\n}\n\nfunction reasonResponse(reason: Exclude<VerifyResult, { ok: true }>['reason']): NormalizedResponse {\n // missing/invalid → 401 (unauthenticated); stale → 403; malformed → 400.\n const status = reason === 'malformed' ? 400 : reason === 'stale_timestamp' ? 403 : 401;\n return errorResponse({ status, code: reason.toUpperCase(), message: `signature ${reason}`, retriable: false });\n}\n\n/** Read the dedupe context from headers without verifying (dev-bypass path). */\nfunction bypassContext(req: NormalizedRequest): ExecutorContext | null {\n const get = headerGetter(req.headers);\n const id = get(DATA_HEADERS.id);\n const tsRaw = get(DATA_HEADERS.timestamp);\n return {\n id: id ?? '',\n idempotencyKey: get(DATA_HEADERS.idempotencyKey),\n deliveryId: get(DATA_HEADERS.deliveryId),\n timestamp: tsRaw && /^\\d+$/.test(tsRaw) ? Number(tsRaw) : 0,\n body: req.rawBody,\n };\n}\n\nexport function createExecutor(opts: CreateExecutorOptions): Executor {\n const store = opts.store ?? new MemoryStore();\n const ttl = opts.idempotencyTtlSec ?? 86_400;\n\n async function run(ctx: ExecutorContext): Promise<NormalizedResponse> {\n // Dedupe key: the logical idempotency key (proxy writes) or the stable message id (webhooks).\n // Absent both (e.g. dev-bypass with no Rekor headers) → run without dedupe.\n const key = ctx.idempotencyKey || ctx.id;\n if (!key) return await invoke(ctx);\n\n const claim = await store.claim(key, ttl);\n if (claim.state === 'done') {\n return { status: claim.result.status, body: claim.result.body, contentType: claim.result.contentType ?? JSON_CT };\n }\n if (claim.state === 'in_progress') {\n // A concurrent attempt holds this key — tell the sender to retry. Use a retriable (5xx) status,\n // not 409, so it matches `retriable: true` and the sender's retry convention.\n return errorResponse({ status: 503, code: 'IN_PROGRESS', message: 'duplicate delivery in progress', retriable: true });\n }\n try {\n const res = await invoke(ctx);\n await store.complete(key, { status: res.status, body: res.body, contentType: res.contentType }, ttl);\n return res;\n } catch (err) {\n const env = toEnvelope(err);\n const res = errorResponse(env);\n if (env.retriable) {\n await store.fail(key); // retriable: release the key so the retry re-runs (don't cache failures)\n } else {\n // permanent: cache the 4xx so a duplicate delivery replays it instead of re-running.\n await store.complete(key, { status: res.status, body: res.body, contentType: res.contentType }, ttl);\n }\n if (!(err instanceof ExecutorError)) opts.onError?.(err, ctx);\n return res;\n }\n }\n\n async function invoke(ctx: ExecutorContext): Promise<NormalizedResponse> {\n const result = await opts.handler(ctx);\n if (!result) return { status: 204, body: '', contentType: JSON_CT };\n return { status: result.status, body: result.body, contentType: result.contentType ?? JSON_CT };\n }\n\n return {\n async handle(req: NormalizedRequest): Promise<NormalizedResponse> {\n const path = opts.pathOverride ? opts.pathOverride(req) : req.path;\n\n if (opts.devBypass) {\n const ctx = bypassContext(req);\n return ctx ? await run(ctx) : reasonResponse('malformed');\n }\n\n const result = await verify({\n secret: opts.secret,\n headers: req.headers,\n method: req.method,\n path,\n rawBody: req.rawBody,\n toleranceSec: opts.toleranceSec,\n });\n if (!result.ok) return reasonResponse(result.reason);\n\n return await run({\n id: result.id,\n idempotencyKey: result.idempotencyKey,\n deliveryId: result.deliveryId,\n timestamp: result.timestamp,\n body: result.body,\n });\n },\n };\n}\n\nfunction toEnvelope(err: unknown): ErrorEnvelope {\n if (err instanceof ExecutorError) return err.toEnvelope();\n // Unknown error → retriable 500 so the sender retries; message is generic (no secret/PII).\n return { status: toHttpStatus({ retriable: true }), code: 'INTERNAL', message: 'handler error', retriable: true };\n}\n","import type { Executor } from '../executor.js';\n\n/**\n * Wrap an {@link Executor} as a Fetch-API handler `(Request) => Promise<Response>` — works on Hono,\n * Next.js route handlers, Cloudflare Workers, Deno, and Bun.\n *\n * Reads the RAW body via `req.text()` BEFORE any parsing (verifying a re-stringified parsed body would\n * fail). The signed `path` is `new URL(req.url).pathname + search`; if a reverse proxy rewrites the\n * path, pass `pathOverride` to `createExecutor`.\n */\nexport function toFetchHandler(executor: Executor): (req: Request) => Promise<Response> {\n return async (req: Request): Promise<Response> => {\n const rawBody = await req.text();\n const url = new URL(req.url);\n const res = await executor.handle({\n method: req.method,\n path: url.pathname + url.search,\n headers: req.headers,\n rawBody,\n });\n // 204/205/304 are null-body statuses — passing any body (even '') throws in spec-compliant\n // runtimes, so send `null` when the body is empty.\n return new Response(res.body || null, { status: res.status, headers: { 'content-type': res.contentType } });\n };\n}\n","import type { Executor } from '../executor.js';\n\n/**\n * Minimal structural shapes of Node's `http.IncomingMessage` / `http.ServerResponse` (and Express's\n * `req`/`res`). Declared structurally so the package imports no Node built-ins at runtime.\n */\nexport interface NodeReq extends AsyncIterable<unknown> {\n method?: string;\n url?: string;\n headers: Record<string, string | string[] | undefined>;\n}\nexport interface NodeRes {\n writeHead(status: number, headers?: Record<string, string>): unknown;\n end(chunk?: string): unknown;\n}\n\n/**\n * Wrap an {@link Executor} as a Node/Express handler `(req, res) => Promise<void>`.\n *\n * IMPORTANT: this reads the RAW request stream itself, so it MUST run BEFORE any body parser\n * (`express.json()`, `body-parser`, …). If a parser has already consumed the stream, the raw bytes are\n * gone and verification fails. Mount this on the Rekor route before such middleware.\n *\n * The signed `path` comes from `req.url` (already `pathname+search`); use `pathOverride` for proxies.\n */\nexport function toNodeHandler(executor: Executor): (req: NodeReq, res: NodeRes) => Promise<void> {\n return async (req: NodeReq, res: NodeRes): Promise<void> => {\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : (chunk as Buffer));\n }\n const rawBody = Buffer.concat(chunks).toString('utf8');\n\n const out = await executor.handle({\n method: req.method ?? 'GET',\n path: req.url ?? '/',\n headers: req.headers,\n rawBody,\n });\n res.writeHead(out.status, { 'content-type': out.contentType });\n res.end(out.body);\n };\n}\n","/**\n * Sign a request TO Rekor — the inverse of the executor's verify side. Use this when an executor POSTs\n * a result back to a Rekor inbound webhook (the async write-back leg): Rekor's inbound webhook ingest verifies a Signing v1\n * signature, and this produces it from the inbound webhook's shared secret.\n *\n * Returns the three headers to attach (`X-Data-Signature`, `X-Data-Timestamp`, `X-Data-Id`). The\n * canonical is byte-identical to the verify side (the cross-package conformance vector guards drift).\n */\n\nimport { buildCanonical, SIGNING_SCHEME, unixSeconds } from './canonical.js';\nimport { signPayload } from './hmac.js';\n\nexport interface SignRequestInput {\n /** HMAC shared secret for the target Rekor inbound webhook. */\n secret: string;\n /** HTTP method of the request being signed (e.g. 'POST'). */\n method: string;\n /** Full target URL — its `pathname + search` is bound into the signature. */\n url: string;\n /** Raw request body string (default `''`). MUST be the exact bytes sent. */\n body?: string;\n /** Message id → `X-Data-Id`, bound into the signature. Default: a generated UUID. */\n id?: string;\n /** Unix seconds → `X-Data-Timestamp`. Default: now. */\n timestamp?: number;\n}\n\nexport interface SignedHeaders {\n 'X-Data-Signature': string;\n 'X-Data-Timestamp': string;\n 'X-Data-Id': string;\n}\n\n/** Sign a request to Rekor and return the headers to attach. */\nexport async function signRequest(input: SignRequestInput): Promise<SignedHeaders> {\n const id = input.id ?? crypto.randomUUID();\n const timestamp = input.timestamp ?? unixSeconds();\n const body = input.body ?? '';\n const { pathname, search } = new URL(input.url);\n const hex = await signPayload(input.secret, buildCanonical({ id, timestamp, method: input.method, path: pathname + search, body }));\n return {\n 'X-Data-Signature': `${SIGNING_SCHEME},${hex}`,\n 'X-Data-Timestamp': String(timestamp),\n 'X-Data-Id': id,\n };\n}\n","/**\n * Executor credential pull — the client side of Rekor's scoped, single-use binary-credential delivery.\n *\n * When a Rekor source declares `executor_secrets`, every proxied dispatch to the executor carries\n * `X-Data-Credential-Url`/`-Token`/`-Names`. The executor calls `fetchExecutorCredential` with the\n * INBOUND request to pull a declared credential by name (e.g. an mTLS client cert it must present to a\n * downstream upstream). The grant is single-use per name and short-lived, so fetch each credential ONCE\n * per invocation and reuse the value — a re-pull of the same name fails (the grant is already claimed).\n *\n * The returned `value` is delivered verbatim (Rekor stores binary credentials base64-encoded); decode it\n * per `contentType` (e.g. base64-decode an `application/x-pkcs12` bundle). This helper never decodes.\n *\n * SECURITY: call this ONLY from a handler whose inbound request the SDK has already verified (e.g. inside\n * `createExecutor`'s handler). The grant headers are NOT part of the request's HMAC signature, so anything\n * that can rewrite headers in front of your executor (a TLS-terminating proxy/gateway) could swap the\n * callback URL. Pass `allowedOrigins` (your Rekor origin, e.g. `['https://api.rekor.pro']`) to reject a\n * rewritten URL — recommended whenever your executor sits behind such an intermediary.\n */\n\nimport { headerGetter } from './headers.js';\nimport { DATA_HEADERS } from './verify.js';\nimport { ExecutorError } from '../errors/envelope.js';\n\n/** A pulled credential. `value` is delivered verbatim (base64 for binary); decode per `contentType`. */\nexport interface ExecutorCredential {\n name: string;\n contentType: string;\n value: string;\n}\n\nexport interface FetchCredentialOptions {\n /** Override the fetch implementation (defaults to the global `fetch`). */\n fetchImpl?: typeof fetch;\n /**\n * If set (and non-empty), the callback URL's origin must be one of these — rejects a URL rewritten in\n * transit (the grant headers are NOT covered by the request signature). Pass your Rekor origin(s),\n * e.g. `['https://api.rekor.pro']`. Recommended when the executor sits behind a TLS-terminating proxy.\n */\n allowedOrigins?: string[];\n}\n\n/** Anything carrying the inbound request headers: a NormalizedRequest, a Fetch `Request`, or raw headers. */\ntype HeaderSource =\n | Headers\n | Record<string, string | string[] | undefined>\n | { headers: Headers | Record<string, string | string[] | undefined> };\n\nfunction resolveHeaders(source: HeaderSource): Headers | Record<string, string | string[] | undefined> {\n if (source instanceof Headers) return source;\n // A NormalizedRequest / Fetch Request carries headers under `.headers` (a Headers or a record); a bare\n // headers record does not (its `.headers`, if any, would be a string value, not an object) — so guard\n // on the shape rather than `'headers' in source`, which a record with a \"headers\" key would spoof.\n const nested = (source as { headers?: unknown }).headers;\n if (nested instanceof Headers) return nested;\n if (nested && typeof nested === 'object') return nested as Record<string, string | string[] | undefined>;\n return source as Record<string, string | string[] | undefined>;\n}\n\n/**\n * Pull a declared credential by name from Rekor, using the grant advertised on the inbound dispatch.\n * Throws `ExecutorError` if the request carries no grant, the name isn't in the grant, or the pull is\n * denied/fails. A 403 (grant invalid/expired/already-used) is never retried; a transient 5xx/network\n * error is retried once.\n */\nexport async function fetchExecutorCredential(\n source: HeaderSource,\n name: string,\n opts: FetchCredentialOptions = {},\n): Promise<ExecutorCredential> {\n const get = headerGetter(resolveHeaders(source));\n const url = get(DATA_HEADERS.credentialUrl);\n const token = get(DATA_HEADERS.credentialToken);\n const namesHeader = get(DATA_HEADERS.credentialNames);\n if (!url || !token || namesHeader === undefined) {\n throw new ExecutorError({ code: 'no_credential_grant', message: 'Request carries no executor credential grant', retriable: false });\n }\n // The callback URL is transport-trusted, not signature-bound — pin it when an allowlist is supplied so a\n // rewritten URL can't redirect the single-use grant token to an attacker host.\n if (opts.allowedOrigins && opts.allowedOrigins.length > 0) {\n let origin: string;\n try { origin = new URL(url).origin; } catch { throw new ExecutorError({ code: 'credential_bad_url', message: 'Credential callback URL is not a valid URL', retriable: false }); }\n // Normalize each allowlist entry through URL.origin too, so a caller's trailing slash / explicit\n // default port (e.g. 'https://api.rekor.pro/' or ':443') still matches. A malformed entry keeps its\n // raw form and simply never matches (it can't grant), rather than breaking the whole check.\n const allowed = opts.allowedOrigins.map((o) => { try { return new URL(o).origin; } catch { return o; } });\n if (!allowed.includes(origin)) {\n throw new ExecutorError({ code: 'credential_untrusted_url', message: `Credential callback URL origin '${origin}' is not in allowedOrigins`, retriable: false });\n }\n }\n const names = namesHeader.split(',').map((n) => n.trim()).filter(Boolean);\n if (!names.includes(name)) {\n throw new ExecutorError({ code: 'undeclared_credential', message: `Credential '${name}' is not in the grant (declared: ${names.join(', ') || 'none'})`, retriable: false });\n }\n\n const doFetch = opts.fetchImpl ?? fetch;\n const body = JSON.stringify({ token, name });\n\n // Up to 2 attempts: retry once on a transient 5xx/network error, never on a 403 (single-use — a retry\n // would just hit `already_redeemed`). A 5xx that landed AFTER the grant was claimed surfaces as a clean\n // 403 on retry, which is the correct terminal outcome (the credential is consumed; Rekor mints a fresh\n // grant on the next dispatch).\n for (let attempt = 0; attempt < 2; attempt++) {\n let res: Response;\n try {\n // `redirect: 'manual'` so a 3xx from the (pinned) URL can't re-POST the token-bearing body to\n // another host — a redirect surfaces as a non-ok status → credential_fetch_failed below.\n res = await doFetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body, redirect: 'manual' });\n } catch {\n if (attempt === 0) continue; // network error → retry once\n throw new ExecutorError({ code: 'credential_unreachable', message: 'Credential pull is unreachable', retriable: true });\n }\n\n if (res.status === 403) {\n throw new ExecutorError({ code: 'credential_denied', message: `Credential '${name}' pull was denied (grant invalid, expired, or already used)`, retriable: false, status: 403 });\n }\n if (res.status >= 500) {\n if (attempt === 0) continue; // transient → retry once\n throw new ExecutorError({ code: 'credential_unavailable', message: `Credential pull failed (status ${res.status})`, retriable: true, status: res.status });\n }\n if (!res.ok) {\n throw new ExecutorError({ code: 'credential_fetch_failed', message: `Credential pull failed (status ${res.status})`, retriable: false, status: res.status });\n }\n\n const parsed = (await res.json().catch(() => null)) as { data?: { name?: string; content_type?: string; value?: string } } | null;\n const data = parsed?.data;\n if (!data || typeof data.value !== 'string') {\n throw new ExecutorError({ code: 'credential_malformed', message: 'Credential pull returned an unexpected body', retriable: false });\n }\n return { name: data.name ?? name, contentType: data.content_type ?? 'application/octet-stream', value: data.value };\n }\n\n // Unreachable: the loop returns or throws on every path.\n throw new ExecutorError({ code: 'credential_unavailable', message: 'Credential pull failed', retriable: true });\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@wayai/executor-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Receive and verify WayAI Data's HMAC-signed, idempotency-keyed webhooks and proxied calls — the executor side of the WayAI Data executor contract.",
5
+ "keywords": ["wayai", "data", "webhook", "hmac", "signature", "idempotency", "executor", "verify"],
6
+ "license": "MIT",
7
+ "publishConfig": { "access": "public" },
8
+ "homepage": "https://github.com/wayai-pro/rekor/tree/main/sdk#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/wayai-pro/rekor.git",
12
+ "directory": "sdk"
13
+ },
14
+ "type": "module",
15
+ "engines": { "node": ">=24.0.0" },
16
+ "sideEffects": false,
17
+ "main": "./dist/index.cjs",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ },
28
+ "default": "./dist/index.js"
29
+ }
30
+ },
31
+ "files": ["dist"],
32
+ "scripts": {
33
+ "build": "tsup",
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "vitest run",
36
+ "dev": "tsup --watch"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^26.2.0",
40
+ "tsup": "^8.4.0",
41
+ "typescript": "^5.8.2",
42
+ "vitest": "^4.1.10"
43
+ }
44
+ }