@wenathlan/saddle 1.8.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.
Files changed (167) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +192 -0
  3. package/adapters/forge.js +16 -0
  4. package/adapters/forgejo.js +8 -0
  5. package/adapters/github.js +19 -0
  6. package/adapters/gitlab.js +10 -0
  7. package/adapters/huggingface.js +6 -0
  8. package/adapters/socket.js +14 -0
  9. package/adapters/transport.js +30 -0
  10. package/ai/chunk.js +22 -0
  11. package/ai/llmstxt.js +12 -0
  12. package/ai/provenance.js +18 -0
  13. package/ai/rag.js +14 -0
  14. package/ai/tokens.js +9 -0
  15. package/api/auth.js +13 -0
  16. package/api/contracts.js +17 -0
  17. package/api/control.js +33 -0
  18. package/api/http.js +12 -0
  19. package/api/rate.js +31 -0
  20. package/api/security.js +42 -0
  21. package/api/service.js +36 -0
  22. package/binary/build.js +17 -0
  23. package/bot/adapter.js +8 -0
  24. package/bot/bot.js +39 -0
  25. package/bot/commands.js +18 -0
  26. package/bot/permissions.js +16 -0
  27. package/browser/actions.js +33 -0
  28. package/browser/agent.js +9 -0
  29. package/browser/context.js +52 -0
  30. package/browser/fingerprint.js +12 -0
  31. package/browser/index.js +10 -0
  32. package/browser/recorder.js +15 -0
  33. package/browser/session.js +19 -0
  34. package/browser/snapshot.js +57 -0
  35. package/captcha/contract.js +15 -0
  36. package/captcha/evidence.js +9 -0
  37. package/captcha/guard.js +10 -0
  38. package/cli/main.js +36 -0
  39. package/core/errors.js +37 -0
  40. package/core/events.js +21 -0
  41. package/core/hash.js +73 -0
  42. package/core/ids.js +15 -0
  43. package/crawl/crawler.js +29 -0
  44. package/crawl/frontier.js +34 -0
  45. package/crawl/normalize.js +14 -0
  46. package/crawl/persistent.js +13 -0
  47. package/dispatch/resumable.js +31 -0
  48. package/dispatch/workflow.js +33 -0
  49. package/docs/assets/architecture.svg +45 -0
  50. package/docs/assets/saddlemark.svg +13 -0
  51. package/docs/comparativeaudit.md +63 -0
  52. package/docs/ecosystemplan.md +59 -0
  53. package/docs/enginearchitecture.md +83 -0
  54. package/docs/featureaudit.md +63 -0
  55. package/docs/gapmatrix.md +80 -0
  56. package/docs/libraryapi.md +63 -0
  57. package/docs/modes.md +27 -0
  58. package/docs/productindex.md +28 -0
  59. package/docs/registryresearch.md +56 -0
  60. package/docs/release.md +28 -0
  61. package/docs/release17notes.md +24 -0
  62. package/docs/release181notes.md +15 -0
  63. package/docs/release18notes.md +15 -0
  64. package/docs/roadmapp2p3.md +33 -0
  65. package/docs/toolchains.md +28 -0
  66. package/docs/usage.md +107 -0
  67. package/domain/artifacts.js +13 -0
  68. package/domain/jobs.js +20 -0
  69. package/domain/providers.js +8 -0
  70. package/domain/runtime.js +10 -0
  71. package/domain/sessions.js +34 -0
  72. package/errors/taxonomy.js +18 -0
  73. package/examples/localjob.js +15 -0
  74. package/examples/publicapi.js +7 -0
  75. package/extension/README.md +23 -0
  76. package/extension/content.js +85 -0
  77. package/extension/index.js +5 -0
  78. package/extension/manifest.json +10 -0
  79. package/extension/popup.css +13 -0
  80. package/extension/popup.html +24 -0
  81. package/extension/popup.js +25 -0
  82. package/extension/protocol.js +76 -0
  83. package/extension/serviceworker.js +43 -0
  84. package/extension/worker.js +20 -0
  85. package/format/check.js +21 -0
  86. package/index.js +120 -0
  87. package/library/public.js +83 -0
  88. package/license.md +203 -0
  89. package/license.txt +203 -0
  90. package/mcp/browser.js +12 -0
  91. package/mcp/server.js +28 -0
  92. package/mcp/transport.js +14 -0
  93. package/memory/bridge.js +16 -0
  94. package/memory/engine.js +45 -0
  95. package/memory/modes.js +55 -0
  96. package/memory/objects.js +18 -0
  97. package/memory/targets.js +21 -0
  98. package/memory/transforms.js +15 -0
  99. package/modes/matrix.js +20 -0
  100. package/modes/modes.js +16 -0
  101. package/modes/resolve.js +39 -0
  102. package/package.json +47 -0
  103. package/packager/manifest.js +28 -0
  104. package/packager/publish.js +15 -0
  105. package/persistence/adapter.js +8 -0
  106. package/persistence/drizzle.js +10 -0
  107. package/persistence/memory.js +26 -0
  108. package/persistence/migrations.js +14 -0
  109. package/persistence/prisma.js +23 -0
  110. package/persistence/schema.js +29 -0
  111. package/persistence/sql.js +30 -0
  112. package/protocol/blocks.js +18 -0
  113. package/protocol/json.js +5 -0
  114. package/protocol/ndjson.js +17 -0
  115. package/protocol/sse.js +22 -0
  116. package/proxy/pool.js +12 -0
  117. package/queue/idempotency.js +12 -0
  118. package/queue/persistent.js +44 -0
  119. package/queue/queue.js +50 -0
  120. package/queue/saga.js +13 -0
  121. package/readme.txt +163 -0
  122. package/retry/circuit.js +15 -0
  123. package/retry/policy.js +12 -0
  124. package/runners/health.js +23 -0
  125. package/runners/heartbeat.js +26 -0
  126. package/runners/inprocess.js +19 -0
  127. package/runners/scheduler.js +16 -0
  128. package/runtime/abort.js +10 -0
  129. package/runtime/compatibility.js +13 -0
  130. package/runtime/detect.js +14 -0
  131. package/runtime/engine.js +56 -0
  132. package/runtime/worker.js +18 -0
  133. package/scrape/cache.js +14 -0
  134. package/scrape/extract.js +14 -0
  135. package/scrape/robots.js +32 -0
  136. package/scrape/schema.js +21 -0
  137. package/scrape/scraper.js +40 -0
  138. package/scrape/semantic.js +22 -0
  139. package/server/node.js +34 -0
  140. package/sessions/file.js +13 -0
  141. package/sessions/replay.js +21 -0
  142. package/sessions/store.js +13 -0
  143. package/storage/adapter.js +8 -0
  144. package/storage/cache.js +54 -0
  145. package/storage/checksum.js +17 -0
  146. package/storage/chunked.js +58 -0
  147. package/storage/content.js +42 -0
  148. package/storage/filehosting.js +17 -0
  149. package/storage/githubcontents.js +18 -0
  150. package/storage/index.js +10 -0
  151. package/storage/local.js +35 -0
  152. package/storage/memory.js +28 -0
  153. package/storage/s3compatible.js +23 -0
  154. package/storage/sync.js +55 -0
  155. package/surfaces/adapters.js +48 -0
  156. package/surfaces/controls.js +37 -0
  157. package/surfaces/manifest.js +25 -0
  158. package/surfaces/n8n.js +24 -0
  159. package/surfaces/operations.js +43 -0
  160. package/surfaces/targets.js +16 -0
  161. package/webhook/delivery.js +26 -0
  162. package/webhook/receiver.js +20 -0
  163. package/webhook/signature.js +7 -0
  164. package/workflow/manifest.js +20 -0
  165. package/workflow/registry.js +16 -0
  166. package/workflow/templates.js +18 -0
  167. package/workflow/triggers.js +31 -0
@@ -0,0 +1,8 @@
1
+ /**
2
+ * storage adapters keep remote services out of the engine core.
3
+ */
4
+ export function storageadapter(methods) {
5
+ const required = ["put", "get", "head", "delete", "list"];
6
+ for (const name of required) if (typeof methods?.[name] !== "function") throw new TypeError(`storage adapter requires ${name}`);
7
+ return methods;
8
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * tiered cache keeps a bounded hot tier and an optional persistent cold tier with stale-while-revalidate.
3
+ */
4
+
5
+ /** Creates a cache with caller supplied encode, decode, clock and persistent storage policies. */
6
+ export function tieredcache(options = {}) {
7
+ const hot = new Map();
8
+ const maxentries = Number(options.maxentries ?? 256);
9
+ const ttl = Number(options.ttl ?? 300000);
10
+ const stale = Number(options.stale ?? ttl);
11
+ const now = options.now ?? (() => Date.now());
12
+ const cold = options.storage;
13
+ const encode = options.encode ?? ((value) => new TextEncoder().encode(JSON.stringify(value)));
14
+ const decode = options.decode ?? ((bytes) => JSON.parse(new TextDecoder().decode(bytes)));
15
+ const stats = { hits: 0, misses: 0, stalehits: 0, evictions: 0, revalidations: 0 };
16
+
17
+ function readhot(key) {
18
+ const item = hot.get(key);
19
+ if (!item) return null;
20
+ const current = now();
21
+ if (current <= item.expires) { stats.hits += 1; return { value: item.value, fresh: true }; }
22
+ if (current <= item.stale) { stats.stalehits += 1; return { value: item.value, fresh: false }; }
23
+ hot.delete(key);
24
+ return null;
25
+ }
26
+
27
+ function writehot(key, value, options = {}) {
28
+ if (hot.size >= maxentries && !hot.has(key)) { hot.delete(hot.keys().next().value); stats.evictions += 1; }
29
+ const current = now();
30
+ hot.set(key, { value, expires: current + Number(options.ttl ?? ttl), stale: current + Number(options.stale ?? stale) });
31
+ return value;
32
+ }
33
+
34
+ async function get(key, options = {}) {
35
+ const hotvalue = readhot(key);
36
+ if (hotvalue?.fresh || (hotvalue && options.allowstale !== false)) return hotvalue.value;
37
+ if (!cold) { stats.misses += 1; return null; }
38
+ try { const value = decode(await cold.get(key)); writehot(key, value, options); return value; } catch { stats.misses += 1; return null; }
39
+ }
40
+
41
+ async function set(key, value, valueoptions = {}) { writehot(key, value, valueoptions); if (cold) await cold.put({ key, data: encode(value), contenttype: "application/json" }); return value; }
42
+ async function getorload(key, loader, options = {}) {
43
+ if (typeof loader !== "function") throw new TypeError("cache loader must be a function");
44
+ const current = readhot(key);
45
+ if (current?.fresh) return current.value;
46
+ if (current && options.allowstale !== false) { stats.revalidations += 1; Promise.resolve(loader()).then((value) => set(key, value, options)).catch(() => undefined); return current.value; }
47
+ const value = await loader();
48
+ return set(key, value, options);
49
+ }
50
+ async function remove(key) { hot.delete(key); await cold?.delete?.(key); }
51
+ function clear() { hot.clear(); }
52
+ function inspect() { return { ...stats, hotentries: hot.size, maxentries, ttl, stale }; }
53
+ return { get, set, getorload, delete: remove, clear, inspect };
54
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * byte collection and hashing are grouped because every storage adapter uses them.
3
+ */
4
+ import { sha256 } from "../core/hash.js";
5
+
6
+ export { sha256 } from "../core/hash.js";
7
+
8
+ export async function collectbytes(input) {
9
+ if (input instanceof Uint8Array) return input;
10
+ const chunks = [];
11
+ let size = 0;
12
+ for await (const chunk of input) { chunks.push(chunk); size += chunk.byteLength; }
13
+ const output = new Uint8Array(size);
14
+ let offset = 0;
15
+ for (const chunk of chunks) { output.set(chunk, offset); offset += chunk.byteLength; }
16
+ return output;
17
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * chunked storage keeps large payloads split without forcing a database blob.
3
+ */
4
+ import { collectbytes, sha256 } from "./checksum.js";
5
+
6
+ export function chunkedstorage(storage, options = {}) {
7
+ const chunkbytes = options.chunkbytes;
8
+ if (!Number.isInteger(chunkbytes) || chunkbytes < 1) throw new TypeError("chunkbytes must be a positive integer");
9
+ return {
10
+ async put(input) {
11
+ const data = await collectbytes(input.data);
12
+ const chunks = [];
13
+ for (let offset = 0; offset < data.byteLength; offset += chunkbytes) {
14
+ const index = chunks.length;
15
+ const key = `${input.key}/chunk${String(index).padStart(8, "0")}`;
16
+ const part = data.slice(offset, Math.min(offset + chunkbytes, data.byteLength));
17
+ const manifest = await storage.put({ key, data: part, contenttype: input.contenttype, metadata: { parent: input.key, index: String(index) } });
18
+ chunks.push({ key, sizebytes: part.byteLength, sha256: manifest.sha256 });
19
+ }
20
+ const manifest = { version: 1, key: input.key, sizebytes: data.byteLength, chunks, chunkbytes, sha256: sha256(data), contenttype: input.contenttype ?? "application/octet-stream", status: "complete", completed: chunks.length, updatedat: Date.now(), metadata: { ...(input.metadata ?? {}) } };
21
+ await storage.put({ key: `${input.key}/manifest`, data: new TextEncoder().encode(JSON.stringify(manifest)), contenttype: "application/json" });
22
+ return manifest;
23
+ },
24
+ async get(key) {
25
+ const raw = await storage.get(`${key}/manifest`);
26
+ const manifest = JSON.parse(new TextDecoder().decode(raw));
27
+ const parts = await Promise.all(manifest.chunks.map((chunk) => storage.get(chunk.key)));
28
+ const output = new Uint8Array(manifest.sizebytes);
29
+ let offset = 0;
30
+ for (const part of parts) { output.set(part, offset); offset += part.byteLength; }
31
+ return output;
32
+ },
33
+ async getrange(key, start = 0, end) {
34
+ const manifest = await this.head(key);
35
+ if (!manifest) throw new Error(`chunk manifest not found: ${key}`);
36
+ const from = Math.max(0, Number(start));
37
+ const to = Math.min(manifest.sizebytes, end === undefined ? manifest.sizebytes : Number(end));
38
+ if (!Number.isInteger(from) || !Number.isInteger(to) || from > to) throw new TypeError("chunk range is invalid");
39
+ if (from === to) return new Uint8Array();
40
+ const first = Math.floor(from / manifest.chunkbytes);
41
+ const last = Math.ceil(to / manifest.chunkbytes) - 1;
42
+ const parts = await Promise.all(manifest.chunks.slice(first, last + 1).map((chunk) => storage.get(chunk.key)));
43
+ const output = new Uint8Array(to - from);
44
+ let offset = 0;
45
+ for (let index = 0; index < parts.length; index += 1) {
46
+ const chunkstart = (first + index) * manifest.chunkbytes;
47
+ const begin = Math.max(from, chunkstart);
48
+ const finish = Math.min(to, chunkstart + parts[index].byteLength);
49
+ output.set(parts[index].slice(begin - chunkstart, finish - chunkstart), offset);
50
+ offset += finish - begin;
51
+ }
52
+ return output;
53
+ },
54
+ async head(key) { const raw = await storage.get(`${key}/manifest`).catch(() => null); return raw ? JSON.parse(new TextDecoder().decode(raw)) : null; },
55
+ async delete(key) { const manifest = await this.head(key); if (!manifest) return; for (const chunk of manifest.chunks) await storage.delete(chunk.key); await storage.delete(`${key}/manifest`); },
56
+ async list(prefix) { return storage.list(prefix); }
57
+ };
58
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * content addressed storage deduplicates immutable bytes while keeping logical references separate.
3
+ */
4
+
5
+ import { collectbytes, sha256 } from "./checksum.js";
6
+
7
+ /** Creates a content-addressed view over a caller supplied storage adapter. */
8
+ export function contentstorage(storage, options = {}) {
9
+ const objectprefix = options.objectprefix ?? "objects";
10
+ const refprefix = options.refprefix ?? "refs";
11
+ const encode = options.encode ?? ((value) => new TextEncoder().encode(JSON.stringify(value)));
12
+ const decode = options.decode ?? ((bytes) => JSON.parse(new TextDecoder().decode(bytes)));
13
+ if (typeof storage?.put !== "function" || typeof storage?.get !== "function" || typeof storage?.head !== "function") throw new TypeError("content storage requires put, get and head");
14
+
15
+ async function put(input = {}) {
16
+ const data = await collectbytes(input.data);
17
+ const digest = sha256(data);
18
+ const objectkey = `${objectprefix}/${digest}`;
19
+ if (!(await storage.head(objectkey))) await storage.put({ key: objectkey, data, contenttype: input.contenttype, metadata: { ...(input.metadata ?? {}), immutable: "true", sha256: digest } });
20
+ const reference = { version: 1, key: String(input.key), objectkey, sha256: digest, sizebytes: data.byteLength, contenttype: input.contenttype ?? "application/octet-stream", metadata: { ...(input.metadata ?? {}) } };
21
+ await storage.put({ key: `${refprefix}/${input.key}`, data: encode(reference), contenttype: "application/json" });
22
+ return reference;
23
+ }
24
+
25
+ async function get(key) {
26
+ const reference = decode(await storage.get(`${refprefix}/${key}`));
27
+ return storage.get(reference.objectkey);
28
+ }
29
+
30
+ async function head(key) {
31
+ try { return decode(await storage.get(`${refprefix}/${key}`)); } catch { return null; }
32
+ }
33
+
34
+ async function remove(key) {
35
+ const reference = await head(key);
36
+ if (!reference) return false;
37
+ await storage.delete?.(`${refprefix}/${key}`);
38
+ return true;
39
+ }
40
+
41
+ return { put, get, head, delete: remove, capabilities: { immutableobjects: true, dedupe: true, references: true } };
42
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * file hosting adapter accepts a caller supplied request function for s3compatible or webdav.
3
+ */
4
+ import { collectbytes, sha256 } from "./checksum.js";
5
+ import { storageadapter } from "./adapter.js";
6
+
7
+ export function filehosting(options = {}) {
8
+ if (!options.host || typeof options.request !== "function") throw new TypeError("file hosting requires host and request");
9
+ const method = options.method ?? "s3compatible";
10
+ return storageadapter({
11
+ async put(input) { const data = await collectbytes(input.data); await options.request({ method: method === "webdav" ? "PUT" : "put", url: new URL(input.key, options.host).href, data, headers: { "content-type": input.contenttype ?? "application/octet-stream" } }); return { key: input.key, sizebytes: data.byteLength, sha256: sha256(data), contenttype: input.contenttype ?? "application/octet-stream", createdat: Date.now(), metadata: input.metadata ?? {} }; },
12
+ async get(key) { const result = await options.request({ method: method === "webdav" ? "GET" : "get", url: new URL(key, options.host).href }); return result.data ?? result; },
13
+ async head(key) { try { const result = await options.request({ method: "head", url: new URL(key, options.host).href }); return { key, sizebytes: Number(result.headers?.["content-length"] ?? 0), sha256: result.headers?.etag ?? "", contenttype: result.headers?.["content-type"] ?? "application/octet-stream", createdat: Date.now(), metadata: {} }; } catch { return null; } },
14
+ async delete(key) { await options.request({ method: method === "webdav" ? "DELETE" : "delete", url: new URL(key, options.host).href }); },
15
+ async list(prefix = "") { const result = await options.request({ method: method === "webdav" ? "PROPFIND" : "list", url: new URL(prefix, options.host).href }); return result.items ?? []; }
16
+ });
17
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * github contents storage maps artifacts to repository files through an injected token.
3
+ */
4
+ import { collectbytes, sha256 } from "./checksum.js";
5
+ import { storageadapter } from "./adapter.js";
6
+
7
+ export function githubcontents(options = {}) {
8
+ if (!options.baseurl || !options.owner || !options.repo || typeof options.token !== "function") throw new TypeError("github contents requires baseurl owner repo and token");
9
+ const fetcher = options.fetcher ?? fetch;
10
+ async function request(method, key, body) { const token = await options.token(); const response = await fetcher(new URL(`/repos/${options.owner}/${options.repo}/contents/${key}`, options.baseurl), { method, headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json" }, body: body ? JSON.stringify(body) : undefined }); if (!response.ok) throw new Error(`github contents request failed with ${response.status}`); return response; }
11
+ return storageadapter({
12
+ async put(input) { const data = await collectbytes(input.data); let sha; try { sha = (await (await request("GET", input.key)).json()).sha; } catch {} const response = await request("PUT", input.key, { message: input.message ?? `saddle write ${input.key}`, content: Buffer.from(data).toString("base64"), branch: options.branch ?? "main", sha }); const result = await response.json(); return { key: input.key, sizebytes: data.byteLength, sha256: sha256(data), contenttype: input.contenttype ?? "application/octet-stream", createdat: Date.now(), metadata: { url: result.content?.download_url, commit: result.commit?.sha, ...(input.metadata ?? {}) } }; },
13
+ async get(key) { const result = await (await request("GET", key)).json(); return new Uint8Array(Buffer.from(result.content.replaceAll("\n", ""), "base64")); },
14
+ async head(key) { try { const result = await (await request("GET", key)).json(); return { key, sizebytes: result.size, sha256: result.sha ?? "", contenttype: "application/octet-stream", createdat: Date.now(), metadata: { url: result.download_url } }; } catch { return null; } },
15
+ async delete(key) { const result = await (await request("GET", key)).json(); await request("DELETE", key, { message: `saddle delete ${key}`, sha: result.sha, branch: options.branch ?? "main" }); },
16
+ async list(prefix = "") { const result = await (await request("GET", prefix)).json(); return (Array.isArray(result) ? result : []).filter((item) => item.type === "file").map((item) => ({ key: item.path, sizebytes: item.size, sha256: item.sha, contenttype: "application/octet-stream", createdat: Date.now(), metadata: { url: item.download_url } })); }
17
+ });
18
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * storage surface exports adapters and working-set helpers without selecting a vendor backend.
3
+ */
4
+ export * from "./adapter.js";
5
+ export * from "./cache.js";
6
+ export * from "./checksum.js";
7
+ export * from "./chunked.js";
8
+ export * from "./content.js";
9
+ export * from "./memory.js";
10
+ export * from "./sync.js";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * local storage is explicit, temporary friendly, and protected against traversal.
3
+ */
4
+ import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
5
+ import { dirname, join, relative, resolve, sep } from "node:path";
6
+ import { artifactnotfound, validationerror } from "../core/errors.js";
7
+ import { artifactmanifest } from "../domain/artifacts.js";
8
+ import { collectbytes, sha256 } from "./checksum.js";
9
+ import { storageadapter } from "./adapter.js";
10
+
11
+ export function localstorage(root) {
12
+ const base = resolve(root);
13
+ function safepath(key) {
14
+ const normalized = String(key ?? "").replaceAll("\\", "/");
15
+ if (!normalized || normalized.startsWith("/") || normalized.split("/").includes("..")) throw validationerror("storage key is not safe", { key });
16
+ const path = resolve(base, normalized);
17
+ if (path !== base && !path.startsWith(`${base}${sep}`)) throw validationerror("storage key escapes adapter root", { key });
18
+ return path;
19
+ }
20
+ function manifest(key, data, contenttype, metadata) {
21
+ return artifactmanifest({ key, sizebytes: data.byteLength, sha256: sha256(data), contenttype, createdat: Date.now(), metadata });
22
+ }
23
+ async function walk(directory, keys) {
24
+ let entries;
25
+ try { entries = await readdir(directory, { withFileTypes: true }); } catch (error) { if (error.code === "ENOENT") return; throw error; }
26
+ for (const entry of entries) { const path = join(directory, entry.name); if (entry.isDirectory()) await walk(path, keys); else keys.push(relative(base, path).split(sep).join("/")); }
27
+ }
28
+ return storageadapter({
29
+ async put(input) { const path = safepath(input.key); const data = await collectbytes(input.data); await mkdir(dirname(path), { recursive: true }); await writeFile(path, data); return manifest(input.key, data, input.contenttype ?? "application/octet-stream", input.metadata); },
30
+ async get(key) { try { return await readFile(safepath(key)); } catch (error) { if (error.code === "ENOENT") throw artifactnotfound(key); throw error; } },
31
+ async head(key) { try { const data = await this.get(key); const file = await stat(safepath(key)); return manifest(key, data, "application/octet-stream", { mtime: String(file.mtimeMs) }); } catch (error) { if (error.name === "saddleerror" && error.code === "ARTIFACT_NOT_FOUND") return null; throw error; } },
32
+ async delete(key) { try { await rm(safepath(key)); } catch (error) { if (error.code !== "ENOENT") throw error; } },
33
+ async list(prefix = "") { const keys = []; await walk(safepath(prefix || "."), keys); return Promise.all(keys.map((key) => this.head(key))); }
34
+ });
35
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * memory storage keeps bytes in a bounded process-local map for browser workers, Deno, Bun and tests.
3
+ */
4
+
5
+ import { artifactmanifest } from "../domain/artifacts.js";
6
+ import { sha256 } from "./checksum.js";
7
+ import { storageadapter } from "./adapter.js";
8
+
9
+ /** Creates a transport-neutral in-memory storage adapter with optional capacity limits. */
10
+ export function memorystorage(options = {}) {
11
+ const values = new Map();
12
+ const maxbytes = Number(options.maxbytes ?? Number.POSITIVE_INFINITY);
13
+ if (!Number.isFinite(maxbytes) || maxbytes < 0) throw new TypeError("memory storage maxbytes is invalid");
14
+ let usedbytes = 0;
15
+
16
+ function manifest(key, data, input = {}) { return artifactmanifest({ key, sizebytes: data.byteLength, sha256: sha256(data), contenttype: input.contenttype ?? "application/octet-stream", createdat: input.createdat ?? Date.now(), metadata: input.metadata }); }
17
+ function copy(value) { return new Uint8Array(value); }
18
+ function ensurecapacity(previous, next) { if (usedbytes - previous + next > maxbytes) { const error = new Error("memory storage capacity exceeded"); error.code = "STORAGE_CAPACITY"; throw error; } }
19
+
20
+ return storageadapter({
21
+ async put(input = {}) { const key = String(input.key ?? ""); if (!key) throw new TypeError("memory storage key is required"); const data = copy(input.data ?? new Uint8Array()); const previous = values.get(key)?.data?.byteLength ?? 0; ensurecapacity(previous, data.byteLength); const item = { data, manifest: manifest(key, data, input) }; values.set(key, item); usedbytes += data.byteLength - previous; return { ...item.manifest }; },
22
+ async get(key) { const item = values.get(String(key)); if (!item) { const error = new Error(`artifact not found: ${key}`); error.code = "ARTIFACT_NOT_FOUND"; throw error; } return copy(item.data); },
23
+ async head(key) { const item = values.get(String(key)); return item ? { ...item.manifest } : null; },
24
+ async delete(key) { const value = values.get(String(key)); if (!value) return false; values.delete(String(key)); usedbytes -= value.data.byteLength; return true; },
25
+ async list(prefix = "") { return [...values].filter(([key]) => key.startsWith(String(prefix))).map(([, value]) => ({ ...value.manifest })); },
26
+ usage() { return { usedbytes, maxbytes }; }
27
+ });
28
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * s3 compatible storage stays open by accepting a caller supplied signer.
3
+ */
4
+ import { storageadapter } from "./adapter.js";
5
+
6
+ export function s3compatible(options = {}) {
7
+ if (!options.endpoint || !options.bucket || typeof options.sign !== "function") throw new TypeError("s3 compatible storage requires endpoint bucket and sign");
8
+ const endpoint = new URL(options.endpoint);
9
+ const fetcher = options.fetcher ?? fetch;
10
+ async function request(method, key, body) {
11
+ const signed = await options.sign({ method, endpoint, bucket: options.bucket, key, body });
12
+ const response = await fetcher(signed.url, { method, headers: signed.headers, body });
13
+ if (!response.ok) throw new Error(`storage request failed with ${response.status}`);
14
+ return response;
15
+ }
16
+ return storageadapter({
17
+ async put(input) { const data = await (input.data instanceof Uint8Array ? input.data : (await import("./checksum.js")).collectbytes(input.data)); await request("PUT", input.key, data); return { key: input.key, sizebytes: data.byteLength, sha256: (await import("./checksum.js")).sha256(data), contenttype: input.contenttype ?? "application/octet-stream", createdat: Date.now(), metadata: { ...(input.metadata ?? {}) } }; },
18
+ async get(key) { const response = await request("GET", key); return new Uint8Array(await response.arrayBuffer()); },
19
+ async head(key) { try { const response = await request("HEAD", key); return { key, sizebytes: Number(response.headers.get("content-length") ?? 0), sha256: response.headers.get("etag") ?? "", contenttype: response.headers.get("content-type") ?? "application/octet-stream", createdat: Date.now(), metadata: {} }; } catch { return null; } },
20
+ async delete(key) { await request("DELETE", key); },
21
+ async list() { throw new Error("s3 compatible list requires a provider specific implementation"); }
22
+ });
23
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * storage synchronization compares versioned manifests and keeps conflict policy explicit.
3
+ */
4
+
5
+ import { collectbytes, sha256 } from "./checksum.js";
6
+
7
+ /** Describes capabilities exposed by a storage adapter without forcing optional methods. */
8
+ export function storagecapabilities(storage) {
9
+ return Object.freeze({ range: typeof storage?.getrange === "function", conditional: typeof storage?.putifmatch === "function", metadata: typeof storage?.head === "function", delete: typeof storage?.delete === "function" });
10
+ }
11
+
12
+ /** Creates a comparable object manifest from bytes and caller metadata. */
13
+ export async function objectmanifest(key, data, options = {}) {
14
+ const bytes = await collectbytes(data);
15
+ return { version: 1, key: String(key), sizebytes: bytes.byteLength, sha256: sha256(bytes), updatedat: Number(options.updatedat ?? Date.now()), etag: options.etag ?? sha256(bytes), metadata: { ...(options.metadata ?? {}) } };
16
+ }
17
+
18
+ /** Compares two manifests and classifies an update or conflict. */
19
+ export function comparemanifests(local, remote) {
20
+ if (!local && !remote) return { state: "empty" };
21
+ if (!local) return { state: "remoteonly", remote };
22
+ if (!remote) return { state: "localonly", local };
23
+ if (local.sha256 === remote.sha256) return { state: "identical", local, remote };
24
+ if (local.updatedat > remote.updatedat) return { state: "localnewer", local, remote };
25
+ if (remote.updatedat > local.updatedat) return { state: "remotenewer", local, remote };
26
+ return { state: "conflict", local, remote };
27
+ }
28
+
29
+ /** Synchronizes one logical object between two adapters with explicit conflict handling. */
30
+ export async function syncobject(source, target, key, options = {}) {
31
+ if (typeof source?.head !== "function" || typeof source?.get !== "function" || typeof target?.head !== "function" || typeof target?.put !== "function") throw new TypeError("sync requires source and target head, get and put");
32
+ const sourcehead = await source.head(key);
33
+ const targethead = await target.head(key);
34
+ const comparison = comparemanifests(normalizemanifest(sourcehead, key), normalizemanifest(targethead, key));
35
+ if (["empty", "identical"].includes(comparison.state)) return { key: String(key), state: comparison.state, manifest: comparison.local ?? comparison.remote };
36
+ if (comparison.state === "conflict" && typeof options.resolve !== "function") { const error = new Error(`storage conflict for key: ${key}`); error.code = "STORAGE_CONFLICT"; error.retryable = false; throw error; }
37
+ if (comparison.state === "conflict") { const choice = await options.resolve(comparison); if (!["source", "target"].includes(choice)) throw new TypeError("sync conflict resolver must return source or target"); if (choice === "target") return { key: String(key), state: "kepttarget", manifest: comparison.remote }; }
38
+ const data = await source.get(key);
39
+ const manifest = await objectmanifest(key, data, { updatedat: comparison.local?.updatedat ?? Date.now(), metadata: sourcehead?.metadata });
40
+ await target.put({ key: String(key), data, contenttype: sourcehead?.contenttype, metadata: { ...(sourcehead?.metadata ?? {}), sha256: manifest.sha256, updatedat: String(manifest.updatedat) } });
41
+ return { key: String(key), state: comparison.state === "conflict" ? "resolvedsource" : "copied", manifest };
42
+ }
43
+
44
+ /** Synchronizes one object to multiple backends and returns each outcome. */
45
+ export async function syncbackends(source, targets = [], key, options = {}) {
46
+ if (!Array.isArray(targets)) throw new TypeError("sync targets must be an array");
47
+ const results = [];
48
+ for (const target of targets) {
49
+ try { results.push(await syncobject(source, target, key, options)); }
50
+ catch (error) { results.push({ key: String(key), state: "failed", code: error.code ?? "STORAGE_SYNC_FAILED", message: error.message }); if (options.stoponerror) break; }
51
+ }
52
+ return results;
53
+ }
54
+
55
+ function normalizemanifest(value, key) { return value ? { ...value, key: String(value.key ?? key), sha256: value.sha256 ?? value.etag, updatedat: Number(value.updatedat ?? 0) } : null; }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * surface adapters define caller-owned desktop and mobile operations without selecting a vendor runtime.
3
+ */
4
+
5
+ const profiles = Object.freeze({
6
+ desktop: { capabilities: ["window", "file", "notification"], formats: ["appimage", "dmg", "msi"] },
7
+ mobile: { capabilities: ["screen", "storage", "network"], formats: ["apk", "ipa"] }
8
+ });
9
+
10
+ /** Creates a transport-neutral adapter contract for a desktop or mobile surface. */
11
+ export function surfaceadapter(options = {}) {
12
+ const target = String(options.target ?? "");
13
+ const profile = profiles[target];
14
+ if (!profile) throw new TypeError(`unsupported surface adapter: ${target}`);
15
+ const operations = [...new Set(options.operations ?? ["open", "close", "invoke", "status"])]
16
+ .map((operation) => String(operation));
17
+ if (!operations.length || operations.some((operation) => !/^[a-z][a-z0-9]*$/.test(operation))) throw new TypeError("surface adapter operations are invalid");
18
+ const handlers = { ...(options.handlers ?? {}) };
19
+ for (const [operation, handler] of Object.entries(handlers)) if (typeof handler !== "function") throw new TypeError(`surface handler is not callable: ${operation}`);
20
+
21
+ async function invoke(operation, input, context = {}) {
22
+ const name = String(operation ?? "");
23
+ if (!operations.includes(name)) throw new TypeError(`unsupported surface operation: ${name}`);
24
+ const handler = handlers[name];
25
+ if (!handler) return { target, operation: name, supported: false, result: undefined };
26
+ try {
27
+ return { target, operation: name, supported: true, result: await handler(input, context) };
28
+ } catch (error) {
29
+ return { target, operation: name, supported: true, ok: false, code: String(error?.code ?? "SURFACE_OPERATION_FAILED"), message: String(error?.message ?? error) };
30
+ }
31
+ }
32
+
33
+ return {
34
+ target,
35
+ version: 1,
36
+ capabilities: [...(options.capabilities ?? profile.capabilities)],
37
+ formats: [...(options.formats ?? profile.formats)],
38
+ operations,
39
+ invoke,
40
+ status: () => ({ target, ready: true, operations: [...operations], capabilities: [...(options.capabilities ?? profile.capabilities)] })
41
+ };
42
+ }
43
+
44
+ /** Creates a desktop adapter contract with caller-owned handlers. */
45
+ export function desktopadapter(options = {}) { return surfaceadapter({ ...options, target: "desktop" }); }
46
+
47
+ /** Creates a mobile adapter contract with caller-owned handlers. */
48
+ export function mobileadapter(options = {}) { return surfaceadapter({ ...options, target: "mobile" }); }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * control surfaces provide an operator contract for jobs, sessions, storage, runners, permissions, logs and artifacts.
3
+ */
4
+
5
+ export const controlresources = Object.freeze(["jobs", "sessions", "storage", "runners", "permissions", "logs", "artifacts"]);
6
+ export const controloperations = Object.freeze(["list", "get", "create", "update", "cancel", "retry", "delete", "check"]);
7
+
8
+ /** Creates a caller-owned operator surface with resource handlers and optional audit recording. */
9
+ export function controlsurface(options = {}) {
10
+ const adapters = { ...(options.adapters ?? {}) };
11
+ const audit = options.audit;
12
+ if (audit !== undefined && typeof audit !== "function") throw new TypeError("control audit must be callable");
13
+ for (const resource of Object.keys(adapters)) if (!controlresources.includes(resource) || !adapters[resource] || typeof adapters[resource] !== "object") throw new TypeError(`unsupported control resource: ${resource}`);
14
+
15
+ async function execute(request = {}) {
16
+ const resource = String(request.resource ?? "");
17
+ const operation = String(request.operation ?? "");
18
+ if (!controlresources.includes(resource) || !controloperations.includes(operation)) throw new TypeError("control request is invalid");
19
+ const handler = adapters[resource]?.[operation];
20
+ const context = { requestid: String(request.requestid ?? `control${Date.now().toString(36)}`), resource, operation };
21
+ if (typeof handler !== "function") return respond({ ok: false, code: "UNSUPPORTED_CONTROL", message: `${resource}.${operation} is not configured`, context, audit });
22
+ try {
23
+ const result = await handler(request.input ?? {}, context);
24
+ return respond({ ok: true, result, context, audit });
25
+ } catch (error) {
26
+ return respond({ ok: false, code: String(error?.code ?? "CONTROL_FAILED"), message: String(error?.message ?? error), context, audit });
27
+ }
28
+ }
29
+
30
+ return { version: 1, resources: [...controlresources], operations: [...controloperations], execute, describe: () => ({ version: 1, resources: [...controlresources], configured: Object.keys(adapters).filter((resource) => controlresources.includes(resource)) }) };
31
+ }
32
+
33
+ async function respond({ ok, result, code, message, context, audit }) {
34
+ const response = { version: 1, ok, ...context, ...(ok ? { result } : { code, message }) };
35
+ if (audit) try { await audit(response); } catch (error) { response.auditerror = String(error?.message ?? error); }
36
+ return response;
37
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * surface manifests describe repackaging targets without adding target code to the engine.
3
+ */
4
+ export const surfaces = Object.freeze(["browser", "extension", "desktop", "mobile", "n8n", "cli", "binary", "library"]);
5
+
6
+ export const surfaceformats = Object.freeze({ desktop: Object.freeze(["appimage", "dmg", "msi"]), mobile: Object.freeze(["apk", "ipa"]) });
7
+
8
+ export function surfacemanifest(options = {}) {
9
+ const target = options.target ?? "library";
10
+ if (!surfaces.includes(target)) throw new TypeError(`unsupported surface: ${target}`);
11
+ return { target, name: options.name ?? "saddle", entry: options.entry ?? "index.js", runtime: options.runtime ?? "caller", formats: [...(options.formats ?? surfaceformats[target] ?? [])], permissions: options.permissions ?? [], capabilities: options.capabilities ?? ["memory", "scrape", "workflow"], metadata: options.metadata ?? {} };
12
+ }
13
+
14
+ /** Creates a desktop surface manifest with caller-selected packaging formats. */
15
+ export function desktopmanifest(options = {}) { return surfacemanifest({ ...options, target: "desktop", formats: options.formats ?? surfaceformats.desktop }); }
16
+
17
+ /** Creates a mobile surface manifest with caller-selected packaging formats. */
18
+ export function mobilemanifest(options = {}) { return surfacemanifest({ ...options, target: "mobile", formats: options.formats ?? surfaceformats.mobile }); }
19
+
20
+ /** Describes installation without performing a package or platform mutation. */
21
+ export function surfacebundle(manifest) {
22
+ if (!manifest?.target || !surfaces.includes(manifest.target)) throw new TypeError("surface bundle requires a valid manifest");
23
+ const install = { n8n: "n8n import", browser: "import by url", extension: "load unpacked", desktop: "caller desktop bundle", mobile: "caller mobile bundle" }[manifest.target] ?? "npm install";
24
+ return { ...manifest, files: [...new Set([manifest.entry, ...(manifest.files ?? [])])], install };
25
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * n8n node metadata keeps workflow automation as a packaging surface.
3
+ */
4
+ export function n8nnode(options = {}) {
5
+ const triggers = normalize(options.triggers ?? n8ntriggers);
6
+ const actions = normalize(options.actions ?? n8nactions);
7
+ return { name: options.name ?? "saddle", displayname: options.displayname ?? "Saddle", description: options.description ?? "Saddle engine operation", version: 1, inputs: options.inputs ?? ["main"], outputs: options.outputs ?? ["main"], triggers, actions, properties: options.properties ?? [{ displayname: "trigger", name: "trigger", type: "options", options: triggers.map((value) => ({ name: value, value })) }, { displayname: "command", name: "command", type: "options", options: actions.map((value) => ({ name: value, value })), default: "status" }] };
8
+ }
9
+
10
+ export const n8ntriggers = Object.freeze(["manual", "dispatch", "webhook", "schedule", "retry", "heartbeat"]);
11
+ export const n8nactions = Object.freeze(["status", "scrape", "crawl", "extract", "batch", "browser", "memory", "sync"]);
12
+
13
+ /** Matches an incoming event against the trigger declarations of a node. */
14
+ export function n8nmatch(node, event = {}) { const type = String(event.type ?? "manual"); return { matched: Boolean(node?.triggers?.includes(type)), type, requestid: event.requestid ?? `${node?.name ?? "saddle"}/${type}` }; }
15
+
16
+ /** Executes a declared n8n action through a caller-owned handler. */
17
+ export async function n8nexecute(node, input, handler) {
18
+ if (typeof handler !== "function") throw new TypeError("n8n handler is required");
19
+ const action = String(input?.action ?? input?.command ?? "status");
20
+ if (!node?.actions?.includes(action)) throw new TypeError(`unsupported n8n action: ${action}`);
21
+ try { return await handler({ node, input: { ...(input ?? {}), action } }); } catch (error) { const failure = new Error(`n8n action failed: ${error?.message ?? error}`, { cause: error }); failure.code = String(error?.code ?? "N8N_ACTION_FAILED"); throw failure; }
22
+ }
23
+
24
+ function normalize(values) { const list = Array.isArray(values) ? values : [values]; if (!list.length || list.some((value) => typeof value !== "string" || !value)) throw new TypeError("n8n declarations are invalid"); return [...new Set(list)]; }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * operations describe bounded telemetry, retention, recovery and threat boundaries without selecting storage or telemetry vendors.
3
+ */
4
+
5
+ export const operationmetrics = Object.freeze(["latency", "retries", "queuedepth", "runnerselection", "storagebytes", "failures"]);
6
+
7
+ /** Binds the standard operational metric names to an injected metric collector. */
8
+ export function operationsmetrics(options = {}) {
9
+ const collector = options.collector;
10
+ if (!collector || typeof collector.count !== "function" || typeof collector.observe !== "function" || typeof collector.snapshot !== "function") throw new TypeError("operations metrics requires a collector");
11
+ function record(name, value = 1, labels = {}) {
12
+ if (!operationmetrics.includes(name)) throw new TypeError(`unsupported operation metric: ${name}`);
13
+ return name === "latency" ? collector.observe(name, value, labels) : collector.count(name, value, labels);
14
+ }
15
+ return { names: [...operationmetrics], record, snapshot: () => collector.snapshot() };
16
+ }
17
+
18
+ /** Creates a deterministic retention policy for caller-owned records. */
19
+ export function retentionpolicy(options = {}) {
20
+ const days = Number(options.days ?? 30);
21
+ const maxbytes = Number(options.maxbytes ?? Number.POSITIVE_INFINITY);
22
+ if (!Number.isFinite(days) || days < 0 || !Number.isFinite(maxbytes) || maxbytes < 0) throw new TypeError("retention policy limits are invalid");
23
+ return { days, maxbytes, cutoff(now = Date.now()) { return Number(now) - days * 86400000; }, keeps(record, now = Date.now()) { return Number(record?.updatedat ?? record?.createdat ?? 0) >= this.cutoff(now) && Number(record?.bytes ?? 0) <= maxbytes; } };
24
+ }
25
+
26
+ /** Defines caller-owned backup and restore functions with explicit capability errors. */
27
+ export function backupplan(options = {}) {
28
+ const backup = options.backup;
29
+ const restore = options.restore;
30
+ return { version: 1, backup: async (input) => execute(backup, input, "backup"), restore: async (input) => execute(restore, input, "restore") };
31
+ }
32
+
33
+ /** Records the security boundary and prevents the core from claiming threat coverage it does not own. */
34
+ export function threatmodel(options = {}) {
35
+ const boundaries = [...new Set((options.boundaries ?? ["credentials", "network", "permissions", "persistence"]).map(String))];
36
+ if (!boundaries.length) throw new TypeError("threat model requires boundaries");
37
+ return { version: 1, boundaries, owner: options.owner ?? "caller", controls: [...new Set((options.controls ?? []).map(String))], disclaimer: "The caller remains responsible for deployment, credentials, service terms and abuse response." };
38
+ }
39
+
40
+ async function execute(handler, input, name) {
41
+ if (typeof handler !== "function") { const error = new Error(`${name} handler is not configured`); error.code = "UNSUPPORTED_OPERATION"; throw error; }
42
+ try { return await handler(input); } catch (error) { const failure = new Error(`${name} failed: ${error?.message ?? error}`, { cause: error }); failure.code = String(error?.code ?? "OPERATION_FAILED"); throw failure; }
43
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * target profiles keep platform packaging open and describe capability boundaries.
3
+ */
4
+ export const targetprofiles = Object.freeze({
5
+ computer: { runtime: "unknown", entry: "index.js", capabilities: ["memory", "storage", "runner"] },
6
+ desktopapp: { runtime: "unknown", entry: "index.js", capabilities: ["memory", "file", "visible"] },
7
+ mobileapp: { runtime: "browser", entry: "index.js", capabilities: ["memory", "network", "headless"] },
8
+ extension: { runtime: "browser", entry: "index.js", capabilities: ["browser", "network", "visible"] },
9
+ internet: { runtime: "unknown", entry: "index.js", capabilities: ["network", "webhook", "api"] }
10
+ });
11
+
12
+ /** Creates a surface target with caller supplied entry and capabilities. */
13
+ export function targetmanifest(target, options = {}) { const profile = targetprofiles[target]; if (!profile) throw new TypeError(`unsupported target: ${target}`); return { target, runtime: options.runtime ?? profile.runtime, entry: options.entry ?? profile.entry, capabilities: options.capabilities ?? [...profile.capabilities], permissions: options.permissions ?? [], metadata: options.metadata ?? {} }; }
14
+
15
+ /** Returns all supported target profiles for documentation and tooling. */
16
+ export function targetcatalog() { return Object.fromEntries(Object.entries(targetprofiles).map(([key, value]) => [key, { ...value, capabilities: [...value.capabilities] }])); }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * webhook delivery tracks attempts and dead letters without choosing a queue vendor.
3
+ */
4
+
5
+ /** Creates a sequential delivery queue with retryable failure handling. */
6
+ export function deliveryqueue(options = {}) {
7
+ const maxattempts = Number(options.maxattempts ?? 3);
8
+ const records = new Map();
9
+ const dead = [];
10
+ async function deliver(input = {}, handler) {
11
+ if (typeof handler !== "function") throw new TypeError("delivery handler must be a function");
12
+ const id = String(input.id ?? `delivery${Date.now().toString(36)}${records.size}`);
13
+ const record = { id, status: "pending", attempts: 0, createdat: Date.now(), event: input.event, metadata: { ...(input.metadata ?? {}) } };
14
+ records.set(id, record);
15
+ while (record.attempts < maxattempts) {
16
+ record.attempts += 1;
17
+ try { record.result = await handler(input.event, input); record.status = "delivered"; record.updatedat = Date.now(); return { ...record }; }
18
+ catch (error) { record.error = { code: String(error.code ?? "DELIVERY_FAILED"), message: String(error.message ?? error), retryable: Boolean(error.retryable) }; if (!error.retryable) break; }
19
+ }
20
+ record.status = "dead";
21
+ dead.push({ ...record });
22
+ record.updatedat = Date.now();
23
+ return { ...record };
24
+ }
25
+ return { deliver, get(id) { return records.get(String(id)); }, list() { return [...records.values()].map((record) => ({ ...record })); }, deadletters() { return dead.map((record) => ({ ...record })); } };
26
+ }