sealkeep 0.9.0 → 0.11.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.
Files changed (274) hide show
  1. package/ARCHITECTURE.md +163 -14
  2. package/CHANGELOG.md +223 -1
  3. package/CONTROL_PLANE.md +2 -2
  4. package/LICENSE +1 -1
  5. package/README.md +108 -22
  6. package/THIRD_PARTY.md +2 -2
  7. package/THREAT_MODEL.md +23 -4
  8. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
  9. package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
  10. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
  11. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
  12. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
  13. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
  14. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
  15. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
  16. package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
  17. package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
  18. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
  19. package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
  20. package/dist/site/index.html +1808 -1904
  21. package/dist/site/llms.txt +67 -0
  22. package/dist/site/trust/architecture-data-flow.html +53 -0
  23. package/dist/site/trust/audit-roadmap.html +37 -0
  24. package/dist/site/trust/deployment-responsibility.html +11 -0
  25. package/dist/site/trust/dpa-sample.html +30 -0
  26. package/dist/site/trust/release-provenance.html +21 -0
  27. package/dist/site/trust/subprocessors.html +15 -0
  28. package/dist/site/trust/threat-model.html +34 -0
  29. package/dist/site/trust/toms.html +41 -0
  30. package/dist/site/trust-document.css +32 -0
  31. package/dist/site/trust.html +73 -0
  32. package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
  33. package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
  34. package/dist/site/visual/index.html +18 -0
  35. package/dist/site.zip +0 -0
  36. package/dist/src/activity.d.ts +9 -0
  37. package/dist/src/activity.js +90 -1
  38. package/dist/src/adapters.d.ts +175 -5
  39. package/dist/src/adapters.js +961 -73
  40. package/dist/src/agent-context.d.ts +135 -0
  41. package/dist/src/agent-context.js +1059 -0
  42. package/dist/src/archive-copies.d.ts +47 -0
  43. package/dist/src/archive-copies.js +179 -0
  44. package/dist/src/audit.d.ts +1 -1
  45. package/dist/src/audit.js +29 -4
  46. package/dist/src/autopilot.d.ts +52 -7
  47. package/dist/src/autopilot.js +143 -25
  48. package/dist/src/background-bandwidth.d.ts +46 -0
  49. package/dist/src/background-bandwidth.js +301 -0
  50. package/dist/src/background-cpu.d.ts +82 -0
  51. package/dist/src/background-cpu.js +212 -0
  52. package/dist/src/background-worker-error.d.ts +12 -0
  53. package/dist/src/background-worker-error.js +18 -0
  54. package/dist/src/branding.d.ts +24 -1
  55. package/dist/src/branding.js +31 -1
  56. package/dist/src/bridge.d.ts +233 -0
  57. package/dist/src/bridge.js +604 -0
  58. package/dist/src/byte-stream.d.ts +91 -0
  59. package/dist/src/byte-stream.js +385 -0
  60. package/dist/src/chunk-store.d.ts +41 -8
  61. package/dist/src/chunk-store.js +161 -65
  62. package/dist/src/cli.js +1698 -163
  63. package/dist/src/cloud.d.ts +841 -31
  64. package/dist/src/cloud.js +3196 -277
  65. package/dist/src/context-background.d.ts +37 -0
  66. package/dist/src/context-background.js +309 -0
  67. package/dist/src/context-drain-child.d.ts +1 -0
  68. package/dist/src/context-drain-child.js +98 -0
  69. package/dist/src/context-reader.d.ts +118 -0
  70. package/dist/src/context-reader.js +447 -0
  71. package/dist/src/control-plane/auth.d.ts +32 -4
  72. package/dist/src/control-plane/auth.js +85 -24
  73. package/dist/src/control-plane/server.js +19 -6
  74. package/dist/src/control-plane.d.ts +17 -1
  75. package/dist/src/control-plane.js +32 -6
  76. package/dist/src/crypto.d.ts +1 -1
  77. package/dist/src/crypto.js +5 -5
  78. package/dist/src/daemon-lease.d.ts +70 -0
  79. package/dist/src/daemon-lease.js +420 -0
  80. package/dist/src/daemon.d.ts +94 -1
  81. package/dist/src/daemon.js +1082 -105
  82. package/dist/src/darwin-service-policy.d.ts +41 -0
  83. package/dist/src/darwin-service-policy.js +60 -0
  84. package/dist/src/dashboard-cli.js +15 -15
  85. package/dist/src/device-authorization.d.ts +37 -0
  86. package/dist/src/device-authorization.js +199 -0
  87. package/dist/src/device-enrollment.d.ts +91 -0
  88. package/dist/src/device-enrollment.js +349 -0
  89. package/dist/src/disk.d.ts +17 -12
  90. package/dist/src/disk.js +43 -17
  91. package/dist/src/doctor.d.ts +35 -1
  92. package/dist/src/doctor.js +316 -41
  93. package/dist/src/durable-ticket-lock.d.ts +24 -0
  94. package/dist/src/durable-ticket-lock.js +232 -0
  95. package/dist/src/enroll.d.ts +1 -1
  96. package/dist/src/enroll.js +13 -7
  97. package/dist/src/env.d.ts +10 -1
  98. package/dist/src/env.js +11 -3
  99. package/dist/src/errors.d.ts +8 -8
  100. package/dist/src/errors.js +6 -6
  101. package/dist/src/flush.d.ts +12 -0
  102. package/dist/src/flush.js +37 -0
  103. package/dist/src/heartbeat.d.ts +86 -12
  104. package/dist/src/heartbeat.js +415 -29
  105. package/dist/src/index-background-watchdog.d.ts +1 -0
  106. package/dist/src/index-background-watchdog.js +94 -0
  107. package/dist/src/index-background-work.d.ts +21 -0
  108. package/dist/src/index-background-work.js +25 -0
  109. package/dist/src/index-background.d.ts +64 -0
  110. package/dist/src/index-background.js +394 -0
  111. package/dist/src/index-build-child.d.ts +1 -0
  112. package/dist/src/index-build-child.js +109 -0
  113. package/dist/src/index-manifest.d.ts +52 -0
  114. package/dist/src/index-manifest.js +444 -0
  115. package/dist/src/index-publication-proof.d.ts +84 -0
  116. package/dist/src/index-publication-proof.js +380 -0
  117. package/dist/src/index-publication-state.d.ts +149 -0
  118. package/dist/src/index-publication-state.js +696 -0
  119. package/dist/src/index-publication-verifier.d.ts +89 -0
  120. package/dist/src/index-publication-verifier.js +341 -0
  121. package/dist/src/index-publish.d.ts +62 -0
  122. package/dist/src/index-publish.js +540 -0
  123. package/dist/src/index-scratch-cleanup.d.ts +19 -0
  124. package/dist/src/index-scratch-cleanup.js +166 -0
  125. package/dist/src/index-segment-types.d.ts +132 -0
  126. package/dist/src/index-segment-types.js +21 -0
  127. package/dist/src/index-segments.d.ts +9 -0
  128. package/dist/src/index-segments.js +516 -0
  129. package/dist/src/index-store.d.ts +123 -0
  130. package/dist/src/index-store.js +495 -0
  131. package/dist/src/index-sync.d.ts +91 -15
  132. package/dist/src/index-sync.js +286 -53
  133. package/dist/src/index-upgrade-publication.d.ts +30 -0
  134. package/dist/src/index-upgrade-publication.js +179 -0
  135. package/dist/src/integration-manager.d.ts +32 -0
  136. package/dist/src/integration-manager.js +394 -0
  137. package/dist/src/leakscan.js +1 -1
  138. package/dist/src/local-api.d.ts +56 -15
  139. package/dist/src/local-api.js +4979 -541
  140. package/dist/src/machine-settings.d.ts +51 -0
  141. package/dist/src/machine-settings.js +166 -0
  142. package/dist/src/managed-chunks.d.ts +5 -2
  143. package/dist/src/managed-chunks.js +14 -14
  144. package/dist/src/mcp-install.d.ts +11 -9
  145. package/dist/src/mcp-install.js +73 -28
  146. package/dist/src/mcp-workspace.d.ts +18 -0
  147. package/dist/src/mcp-workspace.js +50 -0
  148. package/dist/src/mcp.js +294 -25
  149. package/dist/src/migrate.js +27 -21
  150. package/dist/src/notify.d.ts +1 -1
  151. package/dist/src/notify.js +5 -5
  152. package/dist/src/offload.d.ts +201 -14
  153. package/dist/src/offload.js +1848 -140
  154. package/dist/src/onboarding.d.ts +8 -1
  155. package/dist/src/onboarding.js +4 -4
  156. package/dist/src/packages.d.ts +2 -2
  157. package/dist/src/packages.js +10 -2
  158. package/dist/src/passkey.d.ts +0 -1
  159. package/dist/src/passkey.js +2 -7
  160. package/dist/src/password-lock.d.ts +2 -2
  161. package/dist/src/password-lock.js +6 -6
  162. package/dist/src/paths.d.ts +2 -0
  163. package/dist/src/paths.js +2 -0
  164. package/dist/src/presence.d.ts +86 -0
  165. package/dist/src/presence.js +240 -0
  166. package/dist/src/progress-deadline.d.ts +21 -0
  167. package/dist/src/progress-deadline.js +91 -0
  168. package/dist/src/project-repair.d.ts +55 -0
  169. package/dist/src/project-repair.js +131 -0
  170. package/dist/src/providers/gcs.d.ts +28 -7
  171. package/dist/src/providers/gcs.js +35 -24
  172. package/dist/src/providers/gdrive.d.ts +71 -8
  173. package/dist/src/providers/gdrive.js +223 -50
  174. package/dist/src/providers/index.d.ts +11 -3
  175. package/dist/src/providers/index.js +30 -10
  176. package/dist/src/providers/s3.d.ts +30 -8
  177. package/dist/src/providers/s3.js +41 -30
  178. package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
  179. package/dist/src/providers/safe-storage-fetch.js +72 -0
  180. package/dist/src/queue.d.ts +136 -19
  181. package/dist/src/queue.js +862 -96
  182. package/dist/src/reclaim-transaction.d.ts +156 -0
  183. package/dist/src/reclaim-transaction.js +1027 -0
  184. package/dist/src/recovery-codes.d.ts +32 -0
  185. package/dist/src/recovery-codes.js +338 -0
  186. package/dist/src/recovery.js +12 -9
  187. package/dist/src/rehydrate.d.ts +25 -22
  188. package/dist/src/rehydrate.js +319 -23
  189. package/dist/src/restore.d.ts +57 -4
  190. package/dist/src/restore.js +272 -36
  191. package/dist/src/resume-stub.d.ts +92 -0
  192. package/dist/src/resume-stub.js +417 -0
  193. package/dist/src/retention.d.ts +98 -7
  194. package/dist/src/retention.js +1066 -62
  195. package/dist/src/rotate.js +3 -3
  196. package/dist/src/search.d.ts +566 -8
  197. package/dist/src/search.js +5897 -290
  198. package/dist/src/secrets.d.ts +51 -7
  199. package/dist/src/secrets.js +316 -24
  200. package/dist/src/service.d.ts +49 -11
  201. package/dist/src/service.js +776 -35
  202. package/dist/src/share.js +3 -3
  203. package/dist/src/shared-spaces.d.ts +98 -0
  204. package/dist/src/shared-spaces.js +214 -0
  205. package/dist/src/source-reader.d.ts +73 -0
  206. package/dist/src/source-reader.js +715 -0
  207. package/dist/src/spool.d.ts +1 -1
  208. package/dist/src/spool.js +1 -1
  209. package/dist/src/start-tui.js +2 -1
  210. package/dist/src/start.js +2 -2
  211. package/dist/src/storage-endpoint.d.ts +21 -0
  212. package/dist/src/storage-endpoint.js +122 -0
  213. package/dist/src/storage-setup.js +12 -12
  214. package/dist/src/storage-targets.d.ts +109 -6
  215. package/dist/src/storage-targets.js +975 -67
  216. package/dist/src/stream-to-cloud.d.ts +5 -1
  217. package/dist/src/stream-to-cloud.js +34 -14
  218. package/dist/src/sync-rules.d.ts +31 -6
  219. package/dist/src/sync-rules.js +153 -14
  220. package/dist/src/team-backfill-scheduling.d.ts +8 -0
  221. package/dist/src/team-backfill-scheduling.js +33 -0
  222. package/dist/src/team-backfill.d.ts +116 -0
  223. package/dist/src/team-backfill.js +1429 -0
  224. package/dist/src/team-index-cache.d.ts +16 -0
  225. package/dist/src/team-index-cache.js +152 -0
  226. package/dist/src/team-offboarding.d.ts +38 -0
  227. package/dist/src/team-offboarding.js +1043 -0
  228. package/dist/src/team-presence.d.ts +127 -0
  229. package/dist/src/team-presence.js +904 -0
  230. package/dist/src/team-publication-policy.d.ts +20 -0
  231. package/dist/src/team-publication-policy.js +140 -0
  232. package/dist/src/team-realtime.d.ts +68 -0
  233. package/dist/src/team-realtime.js +816 -0
  234. package/dist/src/team-source-facts-cache.d.ts +23 -0
  235. package/dist/src/team-source-facts-cache.js +255 -0
  236. package/dist/src/trash.d.ts +1 -1
  237. package/dist/src/trash.js +2 -2
  238. package/dist/src/tui.js +11 -12
  239. package/dist/src/types.d.ts +173 -7
  240. package/dist/src/types.js +20 -0
  241. package/dist/src/ui-server.d.ts +163 -35
  242. package/dist/src/ui-server.js +712 -72
  243. package/dist/src/ui.d.ts +1 -2
  244. package/dist/src/ui.js +1 -2
  245. package/dist/src/upload.d.ts +27 -0
  246. package/dist/src/upload.js +383 -43
  247. package/dist/src/vault.d.ts +226 -30
  248. package/dist/src/vault.js +1776 -192
  249. package/dist/src/watcher.d.ts +7 -1
  250. package/dist/src/watcher.js +198 -55
  251. package/dist/src/worker.d.ts +27 -3
  252. package/dist/src/worker.js +274 -55
  253. package/package.json +33 -12
  254. package/scripts/native-reboot-rehearsal.mjs +90 -0
  255. package/web/app.js +6032 -343
  256. package/web/bootstrap.js +17 -0
  257. package/web/index.html +255 -57
  258. package/web/rail.js +317 -40
  259. package/web/retention.html +2 -2
  260. package/web/rules-view.js +188 -16
  261. package/web/sessions-view.js +485 -62
  262. package/web/sessions.html +2 -2
  263. package/web/setup-api.js +152 -29
  264. package/web/setup-logic.js +68 -9
  265. package/web/setup.html +113 -44
  266. package/web/setup.js +604 -71
  267. package/web/style.css +513 -98
  268. package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
  269. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
  270. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
  271. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
  272. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
  273. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
  274. /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
@@ -0,0 +1,46 @@
1
+ import { type ByteStream } from "./byte-stream.js";
2
+ import { type DaemonProcessIdentityLookup, type DaemonWorkerLease } from "./daemon-lease.js";
3
+ type Clock = {
4
+ now?: () => number;
5
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
6
+ };
7
+ export type BackgroundBandwidthReservationOptions = {
8
+ signal?: AbortSignal;
9
+ burstBytes?: number;
10
+ /** A busy governor fails closed after this bound; it never steals a stale-looking lock. */
11
+ lockWaitMs?: number;
12
+ /** Test/embedding seam. Production uses the kernel-backed daemon process generation. */
13
+ processIdentityLookup?: DaemonProcessIdentityLookup;
14
+ identityLookupTimeoutMs?: number;
15
+ } & Clock;
16
+ export type BackgroundBandwidthStartupRecoveryOptions = {
17
+ /** In-lifetime supervised cleanup must touch only the just-exited child. */
18
+ expectedOwner?: {
19
+ pid: number;
20
+ processIdentity: string;
21
+ };
22
+ /** Test/embedding seam. Production uses the kernel-backed daemon process generation. */
23
+ processIdentityLookup?: DaemonProcessIdentityLookup;
24
+ identityLookupTimeoutMs?: number;
25
+ /** Adversarial ordering seam: invoked after stale ownership is proved, before the exact final check. */
26
+ beforeFinalLockCheck?: () => void | Promise<void>;
27
+ };
28
+ /** Cross-process reservation shared by the daemon and its index child. */
29
+ export declare function reserveBackgroundBytes(dataDir: string, bytes: number, bytesPerSecond: number, options?: BackgroundBandwidthReservationOptions): Promise<number>;
30
+ /**
31
+ * Recovers a crash-left bandwidth mutex before a daemon starts any lane.
32
+ *
33
+ * This is deliberately unavailable to ordinary reservations. The daemon's
34
+ * process-generation lease supplies the mutual exclusion that a POSIX pathname
35
+ * alone cannot: once stale ownership is proved, no legitimate worker may
36
+ * recreate the fixed lock name between the final inode/content check and
37
+ * unlink. Unknown, live, malformed, or concurrently replaced locks fail closed.
38
+ */
39
+ export declare function recoverBackgroundBandwidthLockAtDaemonStartup(dataDir: string, daemonLease: Pick<DaemonWorkerLease, "assertOwned">, options?: BackgroundBandwidthStartupRecoveryOptions): Promise<"absent" | "recovered" | "not-owned">;
40
+ /** One aggregate background transfer lane for a vault, even across processes. */
41
+ export declare function paceBackgroundByteStream(dataDir: string, chunks: ByteStream, bytesPerSecond: number, options?: {
42
+ signal?: AbortSignal;
43
+ burstBytes?: number;
44
+ onProgress?: (bytes: number) => void;
45
+ } & Clock): ByteStream;
46
+ export {};
@@ -0,0 +1,301 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { link, lstat, mkdir, open, readFile, rename, rm, unlink } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { ARCHIVE_IO_CHUNK_BYTES, paceByteStream } from "./byte-stream.js";
6
+ import { daemonProcessIdentity, classifyDaemonProcessGeneration, } from "./daemon-lease.js";
7
+ const DEFAULT_LOCK_WAIT_MS = 2_000;
8
+ const DEFAULT_IDENTITY_LOOKUP_TIMEOUT_MS = 1_000;
9
+ function errorCode(error) {
10
+ return typeof error === "object" && error !== null && "code" in error
11
+ ? String(error.code)
12
+ : undefined;
13
+ }
14
+ function lockFingerprint(details) {
15
+ return {
16
+ dev: String(details.dev),
17
+ ino: String(details.ino),
18
+ bytes: String(details.size),
19
+ links: String(details.nlink),
20
+ mtimeNs: "mtimeNs" in details ? String(details.mtimeNs) : String(details.mtimeMs),
21
+ ctimeNs: "ctimeNs" in details ? String(details.ctimeNs) : String(details.ctimeMs),
22
+ };
23
+ }
24
+ function sameLock(left, right) {
25
+ return left.raw === right.raw
26
+ && left.fingerprint.dev === right.fingerprint.dev
27
+ && left.fingerprint.ino === right.fingerprint.ino
28
+ && left.fingerprint.bytes === right.fingerprint.bytes
29
+ && left.fingerprint.links === right.fingerprint.links
30
+ && left.fingerprint.mtimeNs === right.fingerprint.mtimeNs
31
+ && left.fingerprint.ctimeNs === right.fingerprint.ctimeNs;
32
+ }
33
+ function parseLockOwner(raw) {
34
+ try {
35
+ const value = JSON.parse(raw);
36
+ if (value.version !== 1
37
+ || typeof value.token !== "string" || !/^[A-Za-z0-9_-]{16,128}$/.test(value.token)
38
+ || !Number.isSafeInteger(value.pid) || (value.pid ?? 0) <= 0
39
+ || typeof value.processIdentity !== "string" || value.processIdentity.length < 1 || value.processIdentity.length > 512
40
+ || !Number.isFinite(value.createdAtMs))
41
+ return null;
42
+ return value;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ async function observeBandwidthLock(path) {
49
+ const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
50
+ let handle;
51
+ try {
52
+ handle = await open(path, fsConstants.O_RDONLY | noFollow);
53
+ const [details, pathDetails, raw] = await Promise.all([
54
+ handle.stat({ bigint: true }),
55
+ lstat(path, { bigint: true }),
56
+ handle.readFile("utf8"),
57
+ ]);
58
+ if (!details.isFile() || !pathDetails.isFile()
59
+ || details.dev !== pathDetails.dev || details.ino !== pathDetails.ino) {
60
+ throw new Error("Background bandwidth lock is not one exact regular file");
61
+ }
62
+ const owner = parseLockOwner(raw);
63
+ if (!owner)
64
+ throw new Error("Background bandwidth lock is malformed; refusing unsafe automatic recovery");
65
+ return { raw, owner, fingerprint: lockFingerprint(details) };
66
+ }
67
+ catch (error) {
68
+ if (errorCode(error) === "ENOENT")
69
+ return null;
70
+ throw error;
71
+ }
72
+ finally {
73
+ await handle?.close().catch(() => undefined);
74
+ }
75
+ }
76
+ async function boundedIdentityLookup(lookup, pid, timeoutMs) {
77
+ let timer;
78
+ const deadline = new Promise((resolve) => {
79
+ timer = setTimeout(() => resolve({ state: "unknown" }), Math.max(1, timeoutMs));
80
+ });
81
+ try {
82
+ const result = await Promise.race([Promise.resolve().then(() => lookup(pid)), deadline]);
83
+ if (result.state === "dead" || result.state === "unknown")
84
+ return result;
85
+ if (result.state === "identified" && typeof result.identity === "string"
86
+ && result.identity.length > 0 && result.identity.length <= 512)
87
+ return result;
88
+ return { state: "unknown" };
89
+ }
90
+ catch {
91
+ return { state: "unknown" };
92
+ }
93
+ finally {
94
+ if (timer)
95
+ clearTimeout(timer);
96
+ }
97
+ }
98
+ async function publishBandwidthLock(runtime, lockPath, owner) {
99
+ const temporary = join(runtime, `.background-bandwidth.${owner.token}.${randomUUID()}.pending`);
100
+ const raw = `${JSON.stringify(owner)}\n`;
101
+ let handle;
102
+ try {
103
+ handle = await open(temporary, "wx", 0o600);
104
+ await handle.writeFile(raw, "utf8");
105
+ await handle.sync();
106
+ await handle.close();
107
+ handle = undefined;
108
+ try {
109
+ // The public lock name appears only after complete, fsynced owner data
110
+ // exists. A crash during publication therefore leaves an ignorable temp,
111
+ // never a malformed permanent lock.
112
+ await link(temporary, lockPath);
113
+ }
114
+ catch (error) {
115
+ if (errorCode(error) === "EEXIST")
116
+ return null;
117
+ throw error;
118
+ }
119
+ // Drop the publication name before fingerprinting ownership. Removing a
120
+ // hard link changes nlink/ctime; recording the two-link intermediate state
121
+ // would make the owner's later exact release reject its own lock.
122
+ await rm(temporary, { force: true });
123
+ const observed = await observeBandwidthLock(lockPath);
124
+ if (!observed || observed.raw !== raw) {
125
+ throw new Error("Background bandwidth lock publication could not be verified");
126
+ }
127
+ return observed;
128
+ }
129
+ finally {
130
+ await handle?.close().catch(() => undefined);
131
+ await rm(temporary, { force: true }).catch(() => undefined);
132
+ }
133
+ }
134
+ async function releaseBandwidthLock(path, owned) {
135
+ const current = await observeBandwidthLock(path);
136
+ if (!current || !sameLock(owned, current)) {
137
+ throw new Error("Background bandwidth lock ownership changed before release");
138
+ }
139
+ // Per-reservation code never removes or replaces an existing lock. Therefore
140
+ // legitimate contenders cannot change this name between the exact check and
141
+ // unlink; supervised cleanup runs only before a daemon starts any workers.
142
+ await unlink(path);
143
+ }
144
+ async function syncDirectory(path) {
145
+ let handle;
146
+ try {
147
+ handle = await open(path, "r");
148
+ await handle.sync();
149
+ }
150
+ catch (error) {
151
+ if (process.platform !== "win32")
152
+ throw error;
153
+ }
154
+ finally {
155
+ await handle?.close().catch(() => undefined);
156
+ }
157
+ }
158
+ const sleepAbortable = async (ms, signal) => {
159
+ signal?.throwIfAborted();
160
+ await new Promise((resolve, reject) => {
161
+ let timer;
162
+ const aborted = () => {
163
+ if (timer)
164
+ clearTimeout(timer);
165
+ signal?.removeEventListener("abort", aborted);
166
+ reject(signal?.reason instanceof Error ? signal.reason : new DOMException("Background transfer cancelled", "AbortError"));
167
+ };
168
+ timer = setTimeout(() => { signal?.removeEventListener("abort", aborted); resolve(); }, ms);
169
+ signal?.addEventListener("abort", aborted, { once: true });
170
+ });
171
+ };
172
+ /** Cross-process reservation shared by the daemon and its index child. */
173
+ export async function reserveBackgroundBytes(dataDir, bytes, bytesPerSecond, options = {}) {
174
+ if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0)
175
+ throw new Error("Background byte rate must be positive");
176
+ if (!Number.isFinite(bytes) || bytes < 0)
177
+ throw new Error("Background byte reservation must be non-negative");
178
+ const now = options.now ?? Date.now;
179
+ const sleep = options.sleep ?? sleepAbortable;
180
+ const runtime = join(dataDir, "runtime");
181
+ const statePath = join(runtime, "background-bandwidth.json");
182
+ const lockPath = join(runtime, "background-bandwidth.lock");
183
+ await mkdir(runtime, { recursive: true, mode: 0o700 });
184
+ const lookup = options.processIdentityLookup ?? daemonProcessIdentity;
185
+ const identity = await boundedIdentityLookup(lookup, process.pid, options.identityLookupTimeoutMs ?? DEFAULT_IDENTITY_LOOKUP_TIMEOUT_MS);
186
+ if (identity.state !== "identified") {
187
+ throw new Error("Sealkeep could not identify this process, so background bandwidth reservation failed closed");
188
+ }
189
+ const owner = {
190
+ version: 1,
191
+ token: randomUUID(),
192
+ pid: process.pid,
193
+ processIdentity: identity.identity,
194
+ createdAtMs: Date.now(),
195
+ };
196
+ const deadline = performance.now() + Math.max(0, options.lockWaitMs ?? DEFAULT_LOCK_WAIT_MS);
197
+ let lock = null;
198
+ for (;;) {
199
+ options.signal?.throwIfAborted();
200
+ lock = await publishBandwidthLock(runtime, lockPath, owner);
201
+ if (lock)
202
+ break;
203
+ const remaining = deadline - performance.now();
204
+ if (remaining <= 0) {
205
+ throw new Error("Background bandwidth governor is busy; refusing to bypass its shared transfer limit");
206
+ }
207
+ await sleep(Math.min(10, remaining), options.signal);
208
+ }
209
+ try {
210
+ const current = await readFile(statePath, "utf8")
211
+ .then((raw) => JSON.parse(raw))
212
+ .catch(() => ({}));
213
+ const observed = now();
214
+ const burst = Math.max(0, options.burstBytes ?? ARCHIVE_IO_CHUNK_BYTES);
215
+ const previousAt = Number.isFinite(current.at) ? Number(current.at) : observed;
216
+ const previousTokens = Number.isFinite(current.tokens) ? Math.max(0, Number(current.tokens)) : burst;
217
+ const baseAt = Math.max(observed, previousAt);
218
+ const available = previousAt <= observed
219
+ ? Math.min(burst, previousTokens + (observed - previousAt) / 1_000 * bytesPerSecond)
220
+ : 0;
221
+ const deficit = Math.max(0, bytes - available);
222
+ const reservedAt = baseAt + deficit / bytesPerSecond * 1_000;
223
+ const tokens = deficit > 0 ? 0 : available - bytes;
224
+ const temp = `${statePath}.${process.pid}.${randomUUID()}.tmp`;
225
+ const state = await open(temp, "wx", 0o600);
226
+ try {
227
+ await state.writeFile(JSON.stringify({ at: reservedAt, tokens }));
228
+ await state.sync();
229
+ }
230
+ finally {
231
+ await state.close();
232
+ }
233
+ await rename(temp, statePath);
234
+ return Math.max(0, reservedAt - observed);
235
+ }
236
+ finally {
237
+ await releaseBandwidthLock(lockPath, lock);
238
+ }
239
+ }
240
+ /**
241
+ * Recovers a crash-left bandwidth mutex before a daemon starts any lane.
242
+ *
243
+ * This is deliberately unavailable to ordinary reservations. The daemon's
244
+ * process-generation lease supplies the mutual exclusion that a POSIX pathname
245
+ * alone cannot: once stale ownership is proved, no legitimate worker may
246
+ * recreate the fixed lock name between the final inode/content check and
247
+ * unlink. Unknown, live, malformed, or concurrently replaced locks fail closed.
248
+ */
249
+ export async function recoverBackgroundBandwidthLockAtDaemonStartup(dataDir, daemonLease, options = {}) {
250
+ await daemonLease.assertOwned();
251
+ const runtime = join(dataDir, "runtime");
252
+ const lockPath = join(runtime, "background-bandwidth.lock");
253
+ const observed = await observeBandwidthLock(lockPath);
254
+ if (!observed)
255
+ return "absent";
256
+ if (options.expectedOwner && (observed.owner.pid !== options.expectedOwner.pid
257
+ || observed.owner.processIdentity !== options.expectedOwner.processIdentity))
258
+ return "not-owned";
259
+ const owner = await boundedIdentityLookup(options.processIdentityLookup ?? daemonProcessIdentity, observed.owner.pid, options.identityLookupTimeoutMs ?? DEFAULT_IDENTITY_LOOKUP_TIMEOUT_MS);
260
+ const generation = classifyDaemonProcessGeneration(owner, observed.owner.processIdentity);
261
+ if (generation === "unknown") {
262
+ throw new Error("Background bandwidth lock owner could not be verified; daemon startup failed closed");
263
+ }
264
+ if (generation === "current") {
265
+ throw new Error("A live background transfer still owns the bandwidth governor; daemon startup failed closed");
266
+ }
267
+ await options.beforeFinalLockCheck?.();
268
+ await daemonLease.assertOwned();
269
+ const current = await observeBandwidthLock(lockPath);
270
+ if (!current || !sameLock(observed, current)) {
271
+ throw new Error("Background bandwidth lock changed during supervised recovery; it was left intact");
272
+ }
273
+ await unlink(lockPath);
274
+ await syncDirectory(runtime);
275
+ return "recovered";
276
+ }
277
+ /** One aggregate background transfer lane for a vault, even across processes. */
278
+ export function paceBackgroundByteStream(dataDir, chunks, bytesPerSecond, options = {}) {
279
+ // Deterministic transport tests inject their own clock. Production uses the
280
+ // durable cross-process bucket above.
281
+ const paced = options.now || options.sleep
282
+ ? paceByteStream(chunks, bytesPerSecond, options)
283
+ : {
284
+ async *[Symbol.asyncIterator]() {
285
+ for await (const chunk of chunks) {
286
+ const delay = await reserveBackgroundBytes(dataDir, chunk.length, bytesPerSecond, options);
287
+ if (delay > 0)
288
+ await sleepAbortable(delay, options.signal);
289
+ yield chunk;
290
+ }
291
+ },
292
+ };
293
+ return {
294
+ async *[Symbol.asyncIterator]() {
295
+ for await (const chunk of paced) {
296
+ options.onProgress?.(chunk.length);
297
+ yield chunk;
298
+ }
299
+ },
300
+ };
301
+ }
@@ -0,0 +1,82 @@
1
+ /** Pure duty-cycle calculation shared by every low-priority CPU lane. */
2
+ export declare function backgroundCpuDelayMs(cpuMs: number, wallMs: number, target: number, maximumMs?: number): number;
3
+ export declare const BACKGROUND_CPU_TOTAL_TARGET = 0.3;
4
+ /** Only accepts the already-owned ChildProcess handle, never an arbitrary PID.
5
+ * Policy refusal is not proof of exit; callers retain their close/barrier fence. */
6
+ export declare function signalOwnedBackgroundProcess(child: {
7
+ kill(signal: NodeJS.Signals): boolean;
8
+ }, signal: NodeJS.Signals): boolean;
9
+ /** These workers own no stdout/stderr pipe (only ignore/inherit plus IPC).
10
+ * A denied kill can prevent Node's later `close` bookkeeping even after the
11
+ * exact child emits `exit`. Either event proves this captured child ended;
12
+ * callers still perform their generation-dead barrier retirement. */
13
+ export declare function onOwnedBackgroundProcessExit(child: Pick<ChildProcess, "once">, listener: (code: number | null, signal: NodeJS.Signals | null) => void): void;
14
+ /** One share is reserved for the responsive parent process's cooperative
15
+ * sealing/retention work. Child shares are released only when governing stops.
16
+ * This intentionally leaves some capacity unused when the parent is idle. */
17
+ export declare function createBackgroundCpuBudget(totalTarget?: number): {
18
+ parentTarget: (requested: number) => number;
19
+ register(): {
20
+ target: (requested: number) => number;
21
+ release(): void;
22
+ };
23
+ };
24
+ export type BackgroundCpuBudget = ReturnType<typeof createBackgroundCpuBudget>;
25
+ /** Shared by all background work hosted by this daemon process. Never pauses
26
+ * the daemon itself, so heartbeat, cancellation and hook I/O remain runnable. */
27
+ export declare const backgroundDaemonCpuBudget: {
28
+ parentTarget: (requested: number) => number;
29
+ register(): {
30
+ target: (requested: number) => number;
31
+ release(): void;
32
+ };
33
+ };
34
+ /**
35
+ * Creates one cooperative CPU lane. Each checkpoint accounts only for the work
36
+ * since the previous checkpoint, so slow network I/O cannot build up a credit
37
+ * that later permits a long compression burst at full core speed.
38
+ */
39
+ export declare function createBackgroundCpuYield(options: {
40
+ target: number;
41
+ signal?: AbortSignal;
42
+ cpuUsage?: typeof process.cpuUsage;
43
+ now?: () => number;
44
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
45
+ maximumSleepMs?: number;
46
+ /** Owned children already have the parent governor: they explicitly opt out
47
+ * of this process-local parent accounting instead of paying twice. */
48
+ budget?: BackgroundCpuBudget | null;
49
+ }): () => Promise<void>;
50
+ export type BackgroundProcessCpuGovernor = {
51
+ /** Stop governing and make sure the owned process is runnable again. */
52
+ stop(): void;
53
+ };
54
+ /**
55
+ * Enforces a short wall-clock duty cycle from a process other than the worker.
56
+ *
57
+ * Cooperative checkpoints can account for JavaScript, compression and crypto,
58
+ * but they cannot interrupt a stop-the-world collection: the event loop that
59
+ * would choose to sleep is the thing being collected. A historical worker is
60
+ * already an owned child, so its parent can impose the missing hard boundary
61
+ * with 30 ms runnable / 70 ms paused windows on POSIX. This bounds even native
62
+ * V8 work; Windows retains the cooperative controller (and deliberately does
63
+ * not receive Unix signals).
64
+ */
65
+ export declare function startBackgroundProcessCpuGovernor(options: {
66
+ target: number;
67
+ /** Must identify only the caller's owned child process. */
68
+ signal: (signal: NodeJS.Signals) => boolean;
69
+ platform?: NodeJS.Platform;
70
+ periodMs?: number;
71
+ setTimer?: (callback: () => void, ms: number) => NodeJS.Timeout;
72
+ clearTimer?: (timer: NodeJS.Timeout) => void;
73
+ /** Explicit aggregate allocation; standalone governor diagnostics omit it. */
74
+ budget?: BackgroundCpuBudget;
75
+ /** Owning workers probe both controls before sending any secret/work IPC. */
76
+ verifySignals?: boolean;
77
+ /** A lost governor is a failed pass, never permission to run at full speed. */
78
+ onFailure?: (error: Error & {
79
+ code: string;
80
+ }) => void;
81
+ }): BackgroundProcessCpuGovernor;
82
+ import type { ChildProcess } from "node:child_process";
@@ -0,0 +1,212 @@
1
+ /** Pure duty-cycle calculation shared by every low-priority CPU lane. */
2
+ export function backgroundCpuDelayMs(cpuMs, wallMs, target, maximumMs = 1_000) {
3
+ if (!Number.isFinite(cpuMs) || cpuMs <= 0 || !Number.isFinite(wallMs) || wallMs <= 0)
4
+ return 0;
5
+ if (!Number.isFinite(target) || target <= 0 || target > 1)
6
+ throw new Error("Background CPU target must be between 0 and 1");
7
+ const delay = cpuMs / target - wallMs;
8
+ return Math.max(0, Math.min(maximumMs, Math.ceil(delay)));
9
+ }
10
+ export const BACKGROUND_CPU_TOTAL_TARGET = 0.30;
11
+ /** Only accepts the already-owned ChildProcess handle, never an arbitrary PID.
12
+ * Policy refusal is not proof of exit; callers retain their close/barrier fence. */
13
+ export function signalOwnedBackgroundProcess(child, signal) {
14
+ try {
15
+ return child.kill(signal);
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /** These workers own no stdout/stderr pipe (only ignore/inherit plus IPC).
22
+ * A denied kill can prevent Node's later `close` bookkeeping even after the
23
+ * exact child emits `exit`. Either event proves this captured child ended;
24
+ * callers still perform their generation-dead barrier retirement. */
25
+ export function onOwnedBackgroundProcessExit(child, listener) {
26
+ let ended = false;
27
+ const done = (code, signal) => {
28
+ if (ended)
29
+ return;
30
+ ended = true;
31
+ listener(code, signal);
32
+ };
33
+ child.once("exit", done);
34
+ // Failed spawn may emit close without an exit; it cannot own live work.
35
+ child.once("close", done);
36
+ }
37
+ /** One share is reserved for the responsive parent process's cooperative
38
+ * sealing/retention work. Child shares are released only when governing stops.
39
+ * This intentionally leaves some capacity unused when the parent is idle. */
40
+ export function createBackgroundCpuBudget(totalTarget = BACKGROUND_CPU_TOTAL_TARGET) {
41
+ backgroundCpuDelayMs(1, 1, totalTarget);
42
+ const members = new Set();
43
+ const target = (requested) => Math.min(requested, totalTarget / (members.size + 1));
44
+ return {
45
+ parentTarget: target,
46
+ register() {
47
+ const id = Symbol("owned-background-child");
48
+ members.add(id);
49
+ return { target, release() { members.delete(id); } };
50
+ },
51
+ };
52
+ }
53
+ /** Shared by all background work hosted by this daemon process. Never pauses
54
+ * the daemon itself, so heartbeat, cancellation and hook I/O remain runnable. */
55
+ export const backgroundDaemonCpuBudget = createBackgroundCpuBudget();
56
+ const sleepAbortable = async (ms, signal) => {
57
+ signal?.throwIfAborted();
58
+ await new Promise((resolve, reject) => {
59
+ let timer;
60
+ const aborted = () => {
61
+ if (timer)
62
+ clearTimeout(timer);
63
+ signal?.removeEventListener("abort", aborted);
64
+ reject(signal?.reason instanceof Error ? signal.reason : new DOMException("Background work cancelled", "AbortError"));
65
+ };
66
+ timer = setTimeout(() => { signal?.removeEventListener("abort", aborted); resolve(); }, ms);
67
+ signal?.addEventListener("abort", aborted, { once: true });
68
+ });
69
+ };
70
+ /**
71
+ * Creates one cooperative CPU lane. Each checkpoint accounts only for the work
72
+ * since the previous checkpoint, so slow network I/O cannot build up a credit
73
+ * that later permits a long compression burst at full core speed.
74
+ */
75
+ export function createBackgroundCpuYield(options) {
76
+ const cpuUsage = options.cpuUsage ?? process.cpuUsage;
77
+ const now = options.now ?? Date.now;
78
+ const sleep = options.sleep ?? sleepAbortable;
79
+ // Explicit foreground/embedding targets above the automatic .30 policy
80
+ // retain their old meaning. Only automatic low-priority work joins by
81
+ // default; an owned child opts out because its parent already governs it.
82
+ const budget = options.budget === undefined
83
+ ? (options.target <= BACKGROUND_CPU_TOTAL_TARGET ? backgroundDaemonCpuBudget : null)
84
+ : options.budget;
85
+ let previousCpu = cpuUsage();
86
+ let previousWall = now();
87
+ // Validate before work begins rather than after the first expensive chunk.
88
+ backgroundCpuDelayMs(1, 1, options.target, options.maximumSleepMs);
89
+ return async () => {
90
+ options.signal?.throwIfAborted();
91
+ // V8 may finish concurrent marking/sweeping while JavaScript is asleep.
92
+ // Sampling only before the sleep and resetting the baseline afterwards
93
+ // silently forgave that CPU. On a large index this produced isolated
94
+ // 80%+ process spikes despite a nominal 30% lane. Keep the same window
95
+ // open until work performed by *all* process threads, including work that
96
+ // happened during the repayment sleep, has actually been repaid.
97
+ for (;;) {
98
+ const observedAt = now();
99
+ const used = cpuUsage(previousCpu);
100
+ const cpuMs = (used.user + used.system) / 1_000;
101
+ const wallMs = Math.max(1, observedAt - previousWall);
102
+ const delay = backgroundCpuDelayMs(cpuMs, wallMs, budget?.parentTarget(options.target) ?? options.target, options.maximumSleepMs);
103
+ if (delay <= 1)
104
+ break;
105
+ await sleep(delay, options.signal);
106
+ options.signal?.throwIfAborted();
107
+ }
108
+ options.signal?.throwIfAborted();
109
+ previousCpu = cpuUsage();
110
+ previousWall = now();
111
+ };
112
+ }
113
+ /**
114
+ * Enforces a short wall-clock duty cycle from a process other than the worker.
115
+ *
116
+ * Cooperative checkpoints can account for JavaScript, compression and crypto,
117
+ * but they cannot interrupt a stop-the-world collection: the event loop that
118
+ * would choose to sleep is the thing being collected. A historical worker is
119
+ * already an owned child, so its parent can impose the missing hard boundary
120
+ * with 30 ms runnable / 70 ms paused windows on POSIX. This bounds even native
121
+ * V8 work; Windows retains the cooperative controller (and deliberately does
122
+ * not receive Unix signals).
123
+ */
124
+ export function startBackgroundProcessCpuGovernor(options) {
125
+ backgroundCpuDelayMs(1, 1, options.target);
126
+ const allocation = options.budget?.register();
127
+ if ((options.platform ?? process.platform) === "win32")
128
+ return { stop() { allocation?.release(); } };
129
+ const periodMs = Math.max(20, Math.floor(options.periodMs ?? 100));
130
+ // Timers and SIGSTOP are not real-time primitives: a runnable window can
131
+ // overrun by a few milliseconds before the kernel observes the stop. Keep a
132
+ // small fixed headroom so the measured process stays near the requested
133
+ // target instead of routinely landing several points above it.
134
+ const runnableMs = () => Math.max(1, Math.min(periodMs - 1, Math.floor(periodMs * (allocation?.target(options.target) ?? options.target) * 0.90)));
135
+ let lastRunnableMs = runnableMs();
136
+ const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
137
+ const clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer));
138
+ let timer;
139
+ let stopped = false;
140
+ let paused = false;
141
+ const send = (signal) => {
142
+ try {
143
+ return options.signal(signal);
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ };
149
+ const failed = () => {
150
+ stopped = true;
151
+ allocation?.release();
152
+ options.onFailure?.(Object.assign(new Error("Background CPU process control is unavailable"), {
153
+ code: "background_cpu_control_unavailable",
154
+ }));
155
+ };
156
+ const run = () => {
157
+ if (stopped)
158
+ return;
159
+ if (paused) {
160
+ if (!send("SIGCONT")) {
161
+ failed();
162
+ return;
163
+ }
164
+ paused = false;
165
+ }
166
+ lastRunnableMs = runnableMs();
167
+ timer = setTimer(pause, lastRunnableMs);
168
+ timer.unref?.();
169
+ };
170
+ const pause = () => {
171
+ if (stopped)
172
+ return;
173
+ if (!send("SIGSTOP")) {
174
+ failed();
175
+ return;
176
+ }
177
+ paused = true;
178
+ timer = setTimer(run, periodMs - lastRunnableMs);
179
+ timer.unref?.();
180
+ };
181
+ if (options.verifySignals) {
182
+ if (!send("SIGSTOP"))
183
+ failed();
184
+ else {
185
+ paused = true;
186
+ if (!send("SIGCONT"))
187
+ failed();
188
+ else
189
+ paused = false;
190
+ }
191
+ }
192
+ if (!stopped) {
193
+ timer = setTimer(pause, lastRunnableMs);
194
+ timer.unref?.();
195
+ }
196
+ return {
197
+ stop() {
198
+ if (stopped)
199
+ return;
200
+ stopped = true;
201
+ allocation?.release();
202
+ if (timer)
203
+ clearTimer(timer);
204
+ timer = undefined;
205
+ // Never strand the child stopped if its owner cancels, times out, or
206
+ // receives the result between two pulses.
207
+ if (paused)
208
+ send("SIGCONT");
209
+ paused = false;
210
+ },
211
+ };
212
+ }
@@ -0,0 +1,12 @@
1
+ /** Bounded machine-readable code only; never serialize provider details or
2
+ * arbitrary error properties across a background worker boundary. */
3
+ export type BackgroundWorkerError = {
4
+ name?: string;
5
+ message: string;
6
+ stack?: string;
7
+ code?: string;
8
+ };
9
+ export declare function backgroundWorkerErrorToWire(cause: unknown): BackgroundWorkerError;
10
+ export declare function backgroundWorkerErrorFromWire(wire: BackgroundWorkerError): Error & {
11
+ code?: string;
12
+ };
@@ -0,0 +1,18 @@
1
+ function safeCode(value) {
2
+ return typeof value === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(value) ? value : undefined;
3
+ }
4
+ export function backgroundWorkerErrorToWire(cause) {
5
+ const error = cause instanceof Error ? cause : new Error(String(cause));
6
+ const code = safeCode(error.code);
7
+ return { name: error.name, message: error.message, stack: error.stack, ...(code ? { code } : {}) };
8
+ }
9
+ export function backgroundWorkerErrorFromWire(wire) {
10
+ const error = new Error(wire.message);
11
+ error.name = wire.name ?? "Error";
12
+ if (wire.stack)
13
+ error.stack = wire.stack;
14
+ const code = safeCode(wire.code);
15
+ if (code)
16
+ error.code = code;
17
+ return error;
18
+ }