@artenx/dshb 0.1.18 → 0.1.20

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 (112) hide show
  1. package/lib/auth/decorate-webserver.d.ts +2 -1
  2. package/lib/auth/decorate-webserver.js +21 -10
  3. package/lib/auth/decorate-webserver.js.map +1 -1
  4. package/lib/auth/gate.d.ts +3 -1
  5. package/lib/auth/gate.js +18 -1
  6. package/lib/auth/gate.js.map +1 -1
  7. package/lib/auth/index.js +11 -3
  8. package/lib/auth/index.js.map +1 -1
  9. package/lib/auth/webserver.d.ts +3 -1
  10. package/lib/auth/webserver.js +10 -1
  11. package/lib/auth/webserver.js.map +1 -1
  12. package/lib/client.js +2157 -567
  13. package/lib/client.js.map +1 -1
  14. package/lib/core/index.js +72 -4
  15. package/lib/core/index.js.map +1 -1
  16. package/lib/core/node-registry.d.ts +2 -108
  17. package/lib/core/node-registry.js +43 -2
  18. package/lib/core/node-registry.js.map +1 -1
  19. package/lib/core/routes.d.ts +4 -3
  20. package/lib/core/routes.js +67 -23
  21. package/lib/core/routes.js.map +1 -1
  22. package/lib/core/sync/coordinator.d.ts +48 -0
  23. package/lib/core/sync/coordinator.js +247 -0
  24. package/lib/core/sync/coordinator.js.map +1 -0
  25. package/lib/core/sync/host-key.d.ts +2 -0
  26. package/lib/core/sync/host-key.js +71 -0
  27. package/lib/core/sync/host-key.js.map +1 -0
  28. package/lib/core/sync/internal-routes.d.ts +9 -0
  29. package/lib/core/sync/internal-routes.js +236 -0
  30. package/lib/core/sync/internal-routes.js.map +1 -0
  31. package/lib/core/sync/model.d.ts +2 -0
  32. package/lib/core/sync/model.js +106 -0
  33. package/lib/core/sync/model.js.map +1 -0
  34. package/lib/core/sync/node-sync.d.ts +2 -0
  35. package/lib/core/sync/node-sync.js +660 -0
  36. package/lib/core/sync/node-sync.js.map +1 -0
  37. package/lib/core/sync/offline-archive.d.ts +55 -0
  38. package/lib/core/sync/offline-archive.js +673 -0
  39. package/lib/core/sync/offline-archive.js.map +1 -0
  40. package/lib/core/sync/offline-journal.d.ts +22 -0
  41. package/lib/core/sync/offline-journal.js +74 -0
  42. package/lib/core/sync/offline-journal.js.map +1 -0
  43. package/lib/core/sync/offline-routes.d.ts +7 -0
  44. package/lib/core/sync/offline-routes.js +192 -0
  45. package/lib/core/sync/offline-routes.js.map +1 -0
  46. package/lib/core/sync/peer-manager.d.ts +56 -0
  47. package/lib/core/sync/peer-manager.js +258 -0
  48. package/lib/core/sync/peer-manager.js.map +1 -0
  49. package/lib/core/sync/protocol.d.ts +2 -0
  50. package/lib/core/sync/protocol.js +124 -0
  51. package/lib/core/sync/protocol.js.map +1 -0
  52. package/lib/core/sync/remote-helper.d.ts +5 -0
  53. package/lib/core/sync/remote-helper.js +160 -0
  54. package/lib/core/sync/remote-helper.js.map +1 -0
  55. package/lib/core/sync/revision.d.ts +2 -0
  56. package/lib/core/sync/revision.js +77 -0
  57. package/lib/core/sync/revision.js.map +1 -0
  58. package/lib/core/sync/routes.d.ts +9 -0
  59. package/lib/core/sync/routes.js +339 -0
  60. package/lib/core/sync/routes.js.map +1 -0
  61. package/lib/core/sync/scheduler.d.ts +14 -0
  62. package/lib/core/sync/scheduler.js +81 -0
  63. package/lib/core/sync/scheduler.js.map +1 -0
  64. package/lib/core/sync/session-journal.d.ts +2 -0
  65. package/lib/core/sync/session-journal.js +191 -0
  66. package/lib/core/sync/session-journal.js.map +1 -0
  67. package/lib/core/sync/session-routes.d.ts +7 -0
  68. package/lib/core/sync/session-routes.js +67 -0
  69. package/lib/core/sync/session-routes.js.map +1 -0
  70. package/lib/core/sync/session-sync.d.ts +2 -0
  71. package/lib/core/sync/session-sync.js +2 -0
  72. package/lib/core/sync/ssh-transport.d.ts +2 -0
  73. package/lib/core/sync/ssh-transport.js +408 -0
  74. package/lib/core/sync/ssh-transport.js.map +1 -0
  75. package/lib/core/sync/stores.d.ts +3 -0
  76. package/lib/core/sync/stores.js +385 -0
  77. package/lib/core/sync/stores.js.map +1 -0
  78. package/lib/core/test.d.ts +1 -1
  79. package/lib/core/workspace-bindings.d.ts +2 -28
  80. package/lib/core/workspace-bindings.js +14 -14
  81. package/lib/core/workspace-bindings.js.map +1 -1
  82. package/lib/core/workspace-instruction-context.d.ts +3 -2260
  83. package/lib/core/workspace-instruction-context.js +104 -2
  84. package/lib/core/workspace-instruction-context.js.map +1 -0
  85. package/lib/core/workspace-instruction-routes.d.ts +1 -1
  86. package/lib/core/workspace-instructions.d.ts +2 -68
  87. package/lib/core/workspace-instructions.js +13 -0
  88. package/lib/core/workspace-instructions.js.map +1 -1
  89. package/lib/core/workspaces.d.ts +4 -3
  90. package/lib/core/workspaces.js +17 -1
  91. package/lib/core/workspaces.js.map +1 -1
  92. package/lib/host-key-C5afhAUd.d.ts +30 -0
  93. package/lib/index-VF8bcBeX.d.ts +2293 -0
  94. package/lib/{workspace-instruction-context-DytAmaPY.js → lib-gdtU_WKT.js} +159 -102
  95. package/lib/lib-gdtU_WKT.js.map +1 -0
  96. package/lib/model-BP7Gw1yk.d.ts +104 -0
  97. package/lib/node-registry-BTnnM5-k.d.ts +112 -0
  98. package/lib/node-sync-3Z9QLz2n.d.ts +89 -0
  99. package/lib/protocol-CV7LGE44.d.ts +35 -0
  100. package/lib/revision-C9sOlDZ6.d.ts +25 -0
  101. package/lib/router/router-fs.d.ts +12 -3
  102. package/lib/router/router-fs.js.map +1 -1
  103. package/lib/session-journal-pgPqcWn9.d.ts +40 -0
  104. package/lib/session-sync-EdfwUON5.js +2177 -0
  105. package/lib/session-sync-EdfwUON5.js.map +1 -0
  106. package/lib/session-sync-g6qKZzjV.d.ts +1252 -0
  107. package/lib/ssh-transport-BcIiLxsX.d.ts +55 -0
  108. package/lib/stores-CKyEt0yk.d.ts +31 -0
  109. package/lib/workspace-bindings-CsB0TIJB.d.ts +28 -0
  110. package/lib/workspace-instructions-DirYYiku.d.ts +69 -0
  111. package/package.json +20 -1
  112. package/lib/workspace-instruction-context-DytAmaPY.js.map +0 -1
@@ -0,0 +1,191 @@
1
+ import { homedir } from "node:os";
2
+ import { chmodSync, closeSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createHash, randomBytes } from "node:crypto";
5
+ //#region src/core/sync/session-journal.ts
6
+ function sessionHome(home) {
7
+ return home ?? process.env.DSH_HOME ?? join(homedir(), ".dsh");
8
+ }
9
+ function validBatch(value) {
10
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
11
+ const batch = value;
12
+ return typeof batch.peerId === "string" && /^p-[a-f0-9]{24}$/.test(batch.peerId) && typeof batch.sourceName === "string" && batch.sourceName.length <= 128 && !/[\u0000-\u001f\u007f]/.test(batch.sourceName) && typeof batch.bundleDigest === "string" && /^sha256:[a-f0-9]{64}$/.test(batch.bundleDigest) && typeof batch.startedAt === "string" && batch.startedAt.length <= 64 && Array.isArray(batch.recordKeys) && batch.recordKeys.every((key) => typeof key === "string" && key.length <= 8192) && (batch.importedVia === void 0 || batch.importedVia === "peer" || batch.importedVia === "offline") && (batch.allowSameInstance === void 0 || typeof batch.allowSameInstance === "boolean");
13
+ }
14
+ function validateJournal(value) {
15
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid sync session journal");
16
+ const journal = value;
17
+ if (journal.version !== 1 || journal.batches === null || typeof journal.batches !== "object" || Array.isArray(journal.batches)) throw new Error("Invalid sync session journal");
18
+ const batches = Object.create(null);
19
+ for (const [id, batch] of Object.entries(journal.batches)) {
20
+ if (!/^b-[a-f0-9]{32}$/.test(id) || !validBatch(batch)) throw new Error("Invalid sync session journal batch");
21
+ batches[id] = {
22
+ ...batch,
23
+ importedVia: batch.importedVia ?? "peer",
24
+ allowSameInstance: batch.allowSameInstance ?? false,
25
+ recordKeys: [...batch.recordKeys]
26
+ };
27
+ }
28
+ return {
29
+ version: 1,
30
+ batches
31
+ };
32
+ }
33
+ function atomicWrite(path, value) {
34
+ mkdirSync(join(path, ".."), {
35
+ recursive: true,
36
+ mode: 448
37
+ });
38
+ const temporary = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
39
+ let descriptor;
40
+ try {
41
+ descriptor = openSync(temporary, "wx", 384);
42
+ writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`, "utf8");
43
+ fsyncSync(descriptor);
44
+ closeSync(descriptor);
45
+ descriptor = void 0;
46
+ renameSync(temporary, path);
47
+ chmodSync(path, 384);
48
+ } catch (error) {
49
+ if (descriptor !== void 0) closeSync(descriptor);
50
+ try {
51
+ unlinkSync(temporary);
52
+ } catch (cleanupError) {
53
+ if (!(cleanupError instanceof Error) || !("code" in cleanupError) || cleanupError.code !== "ENOENT") throw new AggregateError([error, cleanupError], "session journal write and cleanup failed");
54
+ }
55
+ throw error;
56
+ }
57
+ }
58
+ var SyncSessionJournalStore = class {
59
+ home;
60
+ filePath;
61
+ data;
62
+ constructor(home) {
63
+ this.home = sessionHome(home);
64
+ this.filePath = join(this.home, "dshb", "sync-session-journal.json");
65
+ }
66
+ begin(peerId, sourceName, bundleDigest, recordKeys, startedAt = (/* @__PURE__ */ new Date()).toISOString(), importedVia = "peer", allowSameInstance = false) {
67
+ const batchId = `b-${createHash("sha256").update(JSON.stringify([peerId, bundleDigest])).digest("hex").slice(0, 32)}`;
68
+ const data = this.load();
69
+ const existing = data.batches[batchId];
70
+ const batch = {
71
+ peerId,
72
+ sourceName,
73
+ bundleDigest,
74
+ startedAt: existing?.startedAt ?? startedAt,
75
+ recordKeys: [.../* @__PURE__ */ new Set([...existing?.recordKeys ?? [], ...recordKeys])],
76
+ importedVia,
77
+ allowSameInstance
78
+ };
79
+ data.batches[batchId] = batch;
80
+ this.save();
81
+ return batchId;
82
+ }
83
+ beginWithBundle(peerId, sourceName, bundleDigest, recordKeys, bundle, startedAt = (/* @__PURE__ */ new Date()).toISOString(), importedVia = "peer", allowSameInstance = false) {
84
+ const batchId = `b-${createHash("sha256").update(JSON.stringify([peerId, bundleDigest])).digest("hex").slice(0, 32)}`;
85
+ this.writeBundle(batchId, bundle);
86
+ if (this.begin(peerId, sourceName, bundleDigest, recordKeys, startedAt, importedVia, allowSameInstance) !== batchId) throw new Error("Session journal batch identity changed");
87
+ return batchId;
88
+ }
89
+ pendingPeerIds() {
90
+ return [...new Set(Object.values(this.load().batches).map((batch) => batch.peerId))];
91
+ }
92
+ pendingBatches() {
93
+ return Object.entries(this.load().batches).map(([batchId, batch]) => ({
94
+ batchId,
95
+ peerId: batch.peerId,
96
+ sourceName: batch.sourceName,
97
+ bundleDigest: batch.bundleDigest,
98
+ importedVia: batch.importedVia ?? "peer",
99
+ allowSameInstance: batch.allowSameInstance ?? false
100
+ }));
101
+ }
102
+ findBatch(peerId, bundleDigest) {
103
+ const match = Object.entries(this.load().batches).find(([, batch]) => batch.peerId === peerId && batch.bundleDigest === bundleDigest);
104
+ if (!match) return void 0;
105
+ const [batchId, batch] = match;
106
+ return {
107
+ batchId,
108
+ peerId: batch.peerId,
109
+ sourceName: batch.sourceName,
110
+ bundleDigest: batch.bundleDigest,
111
+ importedVia: batch.importedVia ?? "peer",
112
+ allowSameInstance: batch.allowSameInstance ?? false
113
+ };
114
+ }
115
+ writeBundle(batchId, value) {
116
+ if (!/^b-[a-f0-9]{32}$/.test(batchId)) throw new Error("Invalid sync session batch ID");
117
+ atomicWrite(this.bundlePath(batchId), value);
118
+ }
119
+ readBundle(batchId) {
120
+ try {
121
+ return JSON.parse(readFileSync(this.bundlePath(batchId), "utf8"));
122
+ } catch (error) {
123
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
124
+ throw error;
125
+ }
126
+ }
127
+ writePrepared(batchId, recordKey, value) {
128
+ if (!/^b-[a-f0-9]{32}$/.test(batchId)) throw new Error("Invalid sync session batch ID");
129
+ atomicWrite(this.preparedPath(batchId, recordKey), value);
130
+ }
131
+ readPrepared(batchId, recordKey) {
132
+ const file = this.preparedPath(batchId, recordKey);
133
+ try {
134
+ return JSON.parse(readFileSync(file, "utf8"));
135
+ } catch (error) {
136
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
137
+ throw error;
138
+ }
139
+ }
140
+ complete(batchId) {
141
+ const data = this.load();
142
+ delete data.batches[batchId];
143
+ this.save();
144
+ rmSync(join(this.home, "dshb", "sync-session-staging", batchId), {
145
+ recursive: true,
146
+ force: true
147
+ });
148
+ }
149
+ cleanupOrphanedStaging() {
150
+ const root = join(this.home, "dshb", "sync-session-staging");
151
+ let entries;
152
+ try {
153
+ entries = readdirSync(root, { withFileTypes: true });
154
+ } catch (error) {
155
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
156
+ throw error;
157
+ }
158
+ const activeBatches = new Set(Object.keys(this.load().batches));
159
+ for (const entry of entries) if (entry.isDirectory() && /^b-[a-f0-9]{32}$/.test(entry.name) && !activeBatches.has(entry.name)) rmSync(join(root, entry.name), {
160
+ recursive: true,
161
+ force: true
162
+ });
163
+ }
164
+ preparedPath(batchId, recordKey) {
165
+ const digest = createHash("sha256").update(recordKey).digest("hex");
166
+ return join(this.home, "dshb", "sync-session-staging", batchId, `${digest}.json`);
167
+ }
168
+ bundlePath(batchId) {
169
+ return join(this.home, "dshb", "sync-session-staging", batchId, "bundle.json");
170
+ }
171
+ load() {
172
+ if (this.data) return this.data;
173
+ try {
174
+ this.data = validateJournal(JSON.parse(readFileSync(this.filePath, "utf8")));
175
+ } catch (error) {
176
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") this.data = {
177
+ version: 1,
178
+ batches: {}
179
+ };
180
+ else throw error;
181
+ }
182
+ return this.data;
183
+ }
184
+ save() {
185
+ atomicWrite(this.filePath, this.load());
186
+ }
187
+ };
188
+ //#endregion
189
+ export { SyncSessionJournalStore };
190
+
191
+ //# sourceMappingURL=session-journal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-journal.js","names":[],"sources":["../../../src/core/sync/session-journal.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto'\nimport { chmodSync, closeSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { Dirent } from 'node:fs'\nimport type { SyncImportMethod } from './model.js'\n\ninterface JournalBatch {\n peerId: string\n sourceName: string\n bundleDigest: string\n startedAt: string\n recordKeys: string[]\n importedVia?: SyncImportMethod\n allowSameInstance?: boolean\n}\n\ninterface JournalData {\n version: 1\n batches: Record<string, JournalBatch>\n}\n\nfunction sessionHome(home?: string): string {\n return home ?? process.env.DSH_HOME ?? join(homedir(), '.dsh')\n}\n\nfunction validBatch(value: unknown): value is JournalBatch {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return false\n const batch = value as Record<string, unknown>\n return typeof batch.peerId === 'string' && /^p-[a-f0-9]{24}$/.test(batch.peerId) &&\n typeof batch.sourceName === 'string' && batch.sourceName.length <= 128 && !/[\\u0000-\\u001f\\u007f]/.test(batch.sourceName) &&\n typeof batch.bundleDigest === 'string' && /^sha256:[a-f0-9]{64}$/.test(batch.bundleDigest) &&\n typeof batch.startedAt === 'string' && batch.startedAt.length <= 64 &&\n Array.isArray(batch.recordKeys) && batch.recordKeys.every((key) => typeof key === 'string' && key.length <= 8192) &&\n (batch.importedVia === undefined || batch.importedVia === 'peer' || batch.importedVia === 'offline') &&\n (batch.allowSameInstance === undefined || typeof batch.allowSameInstance === 'boolean')\n}\n\nfunction validateJournal(value: unknown): JournalData {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid sync session journal')\n const journal = value as Record<string, unknown>\n if (journal.version !== 1 || journal.batches === null || typeof journal.batches !== 'object' || Array.isArray(journal.batches)) {\n throw new Error('Invalid sync session journal')\n }\n const batches = Object.create(null) as Record<string, JournalBatch>\n for (const [id, batch] of Object.entries(journal.batches)) {\n if (!/^b-[a-f0-9]{32}$/.test(id) || !validBatch(batch)) throw new Error('Invalid sync session journal batch')\n batches[id] = {\n ...batch,\n importedVia: batch.importedVia ?? 'peer',\n allowSameInstance: batch.allowSameInstance ?? false,\n recordKeys: [...batch.recordKeys],\n }\n }\n return { version: 1, batches }\n}\n\nfunction atomicWrite(path: string, value: unknown): void {\n mkdirSync(join(path, '..'), { recursive: true, mode: 0o700 })\n const temporary = `${path}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`\n let descriptor: number | undefined\n try {\n descriptor = openSync(temporary, 'wx', 0o600)\n writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\\n`, 'utf8')\n fsyncSync(descriptor)\n closeSync(descriptor)\n descriptor = undefined\n renameSync(temporary, path)\n chmodSync(path, 0o600)\n } catch (error) {\n if (descriptor !== undefined) closeSync(descriptor)\n try {\n unlinkSync(temporary)\n } catch (cleanupError) {\n if (!(cleanupError instanceof Error) || !('code' in cleanupError) || cleanupError.code !== 'ENOENT') {\n throw new AggregateError([error, cleanupError], 'session journal write and cleanup failed')\n }\n }\n throw error\n }\n}\n\nexport class SyncSessionJournalStore {\n private readonly home: string\n private readonly filePath: string\n private data: JournalData | undefined\n\n constructor(home?: string) {\n this.home = sessionHome(home)\n this.filePath = join(this.home, 'dshb', 'sync-session-journal.json')\n }\n\n begin(\n peerId: string,\n sourceName: string,\n bundleDigest: string,\n recordKeys: readonly string[],\n startedAt = new Date().toISOString(),\n importedVia: SyncImportMethod = 'peer',\n allowSameInstance = false,\n ): string {\n const batchId = `b-${createHash('sha256').update(JSON.stringify([peerId, bundleDigest])).digest('hex').slice(0, 32)}`\n const data = this.load()\n const existing = data.batches[batchId]\n const batch: JournalBatch = {\n peerId,\n sourceName,\n bundleDigest,\n startedAt: existing?.startedAt ?? startedAt,\n recordKeys: [...new Set([...(existing?.recordKeys ?? []), ...recordKeys])],\n importedVia,\n allowSameInstance,\n }\n data.batches[batchId] = batch\n this.save()\n return batchId\n }\n\n beginWithBundle(\n peerId: string,\n sourceName: string,\n bundleDigest: string,\n recordKeys: readonly string[],\n bundle: unknown,\n startedAt = new Date().toISOString(),\n importedVia: SyncImportMethod = 'peer',\n allowSameInstance = false,\n ): string {\n const batchId = `b-${createHash('sha256').update(JSON.stringify([peerId, bundleDigest])).digest('hex').slice(0, 32)}`\n this.writeBundle(batchId, bundle)\n const registered = this.begin(\n peerId,\n sourceName,\n bundleDigest,\n recordKeys,\n startedAt,\n importedVia,\n allowSameInstance,\n )\n if (registered !== batchId) throw new Error('Session journal batch identity changed')\n return batchId\n }\n\n pendingPeerIds(): string[] {\n return [...new Set(Object.values(this.load().batches).map((batch) => batch.peerId))]\n }\n\n pendingBatches(): Array<{ batchId: string; peerId: string; sourceName: string; bundleDigest: string; importedVia: SyncImportMethod; allowSameInstance: boolean }> {\n return Object.entries(this.load().batches).map(([batchId, batch]) => ({\n batchId,\n peerId: batch.peerId,\n sourceName: batch.sourceName,\n bundleDigest: batch.bundleDigest,\n importedVia: batch.importedVia ?? 'peer',\n allowSameInstance: batch.allowSameInstance ?? false,\n }))\n }\n\n findBatch(peerId: string, bundleDigest: string): {\n batchId: string\n peerId: string\n sourceName: string\n bundleDigest: string\n importedVia: SyncImportMethod\n allowSameInstance: boolean\n } | undefined {\n const match = Object.entries(this.load().batches).find(([, batch]) =>\n batch.peerId === peerId && batch.bundleDigest === bundleDigest,\n )\n if (!match) return undefined\n const [batchId, batch] = match\n return {\n batchId,\n peerId: batch.peerId,\n sourceName: batch.sourceName,\n bundleDigest: batch.bundleDigest,\n importedVia: batch.importedVia ?? 'peer',\n allowSameInstance: batch.allowSameInstance ?? false,\n }\n }\n\n writeBundle(batchId: string, value: unknown): void {\n if (!/^b-[a-f0-9]{32}$/.test(batchId)) throw new Error('Invalid sync session batch ID')\n atomicWrite(this.bundlePath(batchId), value)\n }\n\n readBundle(batchId: string): unknown | undefined {\n try {\n return JSON.parse(readFileSync(this.bundlePath(batchId), 'utf8')) as unknown\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return undefined\n throw error\n }\n }\n\n writePrepared(batchId: string, recordKey: string, value: unknown): void {\n if (!/^b-[a-f0-9]{32}$/.test(batchId)) throw new Error('Invalid sync session batch ID')\n const file = this.preparedPath(batchId, recordKey)\n atomicWrite(file, value)\n }\n\n readPrepared(batchId: string, recordKey: string): unknown | undefined {\n const file = this.preparedPath(batchId, recordKey)\n try {\n return JSON.parse(readFileSync(file, 'utf8')) as unknown\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return undefined\n throw error\n }\n }\n\n complete(batchId: string): void {\n const data = this.load()\n delete data.batches[batchId]\n this.save()\n rmSync(join(this.home, 'dshb', 'sync-session-staging', batchId), { recursive: true, force: true })\n }\n\n cleanupOrphanedStaging(): void {\n const root = join(this.home, 'dshb', 'sync-session-staging')\n let entries: Dirent[]\n try {\n entries = readdirSync(root, { withFileTypes: true })\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return\n throw error\n }\n const activeBatches = new Set(Object.keys(this.load().batches))\n for (const entry of entries) {\n if (entry.isDirectory() && /^b-[a-f0-9]{32}$/.test(entry.name) && !activeBatches.has(entry.name)) {\n rmSync(join(root, entry.name), { recursive: true, force: true })\n }\n }\n }\n\n private preparedPath(batchId: string, recordKey: string): string {\n const digest = createHash('sha256').update(recordKey).digest('hex')\n return join(this.home, 'dshb', 'sync-session-staging', batchId, `${digest}.json`)\n }\n\n private bundlePath(batchId: string): string {\n return join(this.home, 'dshb', 'sync-session-staging', batchId, 'bundle.json')\n }\n\n private load(): JournalData {\n if (this.data) return this.data\n try {\n this.data = validateJournal(JSON.parse(readFileSync(this.filePath, 'utf8')) as unknown)\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {\n this.data = { version: 1, batches: {} }\n } else {\n throw error\n }\n }\n return this.data\n }\n\n private save(): void {\n atomicWrite(this.filePath, this.load())\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { c as SyncSessionService } from "../../session-sync-g6qKZzjV.js";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+ //#region src/core/sync/session-routes.d.ts
4
+ declare function registerSyncSessionRoutes(ctx: Context, sessions: SyncSessionService): void;
5
+ //#endregion
6
+ export { registerSyncSessionRoutes };
7
+ //# sourceMappingURL=session-routes.d.ts.map
@@ -0,0 +1,67 @@
1
+ import { t as SyncSessionError } from "../../session-sync-EdfwUON5.js";
2
+ //#region src/core/sync/session-routes.ts
3
+ function sendJson(res, status, body) {
4
+ res.writeHead(status, {
5
+ "content-type": "application/json",
6
+ "cache-control": "no-store"
7
+ });
8
+ res.end(JSON.stringify(body));
9
+ }
10
+ function pathSegments(req) {
11
+ return new URL(req.url ?? "/", "http://x").pathname.split("/").filter(Boolean);
12
+ }
13
+ function registerSyncSessionRoutes(ctx, sessions) {
14
+ ctx.effect(() => ctx.webServer.register({
15
+ kind: "prefix",
16
+ path: "/api/dshb/sync/archives",
17
+ handler: async (req, res) => {
18
+ const segments = pathSegments(req);
19
+ if (req.method === "GET" && segments.length === 4) {
20
+ try {
21
+ sendJson(res, 200, {
22
+ ok: true,
23
+ archives: sessions.listArchives()
24
+ });
25
+ } catch {
26
+ sendJson(res, 500, {
27
+ ok: false,
28
+ error: "session archive list unavailable"
29
+ });
30
+ }
31
+ return;
32
+ }
33
+ if (req.method === "POST" && segments.length === 6 && segments[5] === "restore") {
34
+ const branchId = segments[4];
35
+ if (!/^b-[a-f0-9]{32}$/.test(branchId)) {
36
+ sendJson(res, 400, {
37
+ ok: false,
38
+ error: "invalid session branch ID"
39
+ });
40
+ return;
41
+ }
42
+ try {
43
+ sendJson(res, 200, {
44
+ ok: true,
45
+ sessionId: await sessions.restoreArchive(branchId)
46
+ });
47
+ } catch (error) {
48
+ const code = error instanceof SyncSessionError ? error.code : "session-restore-failed";
49
+ sendJson(res, code === "archive-not-found" ? 404 : 409, {
50
+ ok: false,
51
+ code,
52
+ error: error instanceof SyncSessionError ? error.message : "session archive restore failed"
53
+ });
54
+ }
55
+ return;
56
+ }
57
+ sendJson(res, 404, {
58
+ ok: false,
59
+ error: "not found"
60
+ });
61
+ }
62
+ }));
63
+ }
64
+ //#endregion
65
+ export { registerSyncSessionRoutes };
66
+
67
+ //# sourceMappingURL=session-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-routes.js","names":[],"sources":["../../../src/core/sync/session-routes.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { SyncSessionError, SyncSessionService } from './session-sync.js'\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' })\n res.end(JSON.stringify(body))\n}\n\nfunction pathSegments(req: IncomingMessage): string[] {\n return new URL(req.url ?? '/', 'http://x').pathname.split('/').filter(Boolean)\n}\n\nexport function registerSyncSessionRoutes(ctx: Context, sessions: SyncSessionService): void {\n ctx.effect(() =>\n ctx.webServer.register({\n kind: 'prefix',\n path: '/api/dshb/sync/archives',\n handler: async (req, res) => {\n const segments = pathSegments(req)\n if (req.method === 'GET' && segments.length === 4) {\n try {\n sendJson(res, 200, { ok: true, archives: sessions.listArchives() })\n } catch {\n sendJson(res, 500, { ok: false, error: 'session archive list unavailable' })\n }\n return\n }\n if (req.method === 'POST' && segments.length === 6 && segments[5] === 'restore') {\n const branchId = segments[4]\n if (!/^b-[a-f0-9]{32}$/.test(branchId)) {\n sendJson(res, 400, { ok: false, error: 'invalid session branch ID' })\n return\n }\n try {\n const sessionId = await sessions.restoreArchive(branchId)\n sendJson(res, 200, { ok: true, sessionId })\n } catch (error) {\n const code = error instanceof SyncSessionError ? error.code : 'session-restore-failed'\n sendJson(res, code === 'archive-not-found' ? 404 : 409, {\n ok: false,\n code,\n error: error instanceof SyncSessionError ? error.message : 'session archive restore failed',\n })\n }\n return\n }\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n"],"mappings":";;AAKA,SAAS,SAAS,KAAqB,QAAgB,MAAqB;CAC1E,IAAI,UAAU,QAAQ;EAAE,gBAAgB;EAAoB,iBAAiB;CAAW,CAAC;CACzF,IAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC9B;AAEA,SAAS,aAAa,KAAgC;CACpD,OAAO,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;AAC/E;AAEA,SAAgB,0BAA0B,KAAc,UAAoC;CAC1F,IAAI,aACF,IAAI,UAAU,SAAS;EACrB,MAAM;EACN,MAAM;EACN,SAAS,OAAO,KAAK,QAAQ;GAC3B,MAAM,WAAW,aAAa,GAAG;GACjC,IAAI,IAAI,WAAW,SAAS,SAAS,WAAW,GAAG;IACjD,IAAI;KACF,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,UAAU,SAAS,aAAa;KAAE,CAAC;IACpE,QAAQ;KACN,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAAmC,CAAC;IAC7E;IACA;GACF;GACA,IAAI,IAAI,WAAW,UAAU,SAAS,WAAW,KAAK,SAAS,OAAO,WAAW;IAC/E,MAAM,WAAW,SAAS;IAC1B,IAAI,CAAC,mBAAmB,KAAK,QAAQ,GAAG;KACtC,SAAS,KAAK,KAAK;MAAE,IAAI;MAAO,OAAO;KAA4B,CAAC;KACpE;IACF;IACA,IAAI;KAEF,SAAS,KAAK,KAAK;MAAE,IAAI;MAAM,WAAA,MADP,SAAS,eAAe,QAAQ;KACf,CAAC;IAC5C,SAAS,OAAO;KACd,MAAM,OAAO,iBAAiB,mBAAmB,MAAM,OAAO;KAC9D,SAAS,KAAK,SAAS,sBAAsB,MAAM,KAAK;MACtD,IAAI;MACJ;MACA,OAAO,iBAAiB,mBAAmB,MAAM,UAAU;KAC7D,CAAC;IACH;IACA;GACF;GACA,SAAS,KAAK,KAAK;IAAE,IAAI;IAAO,OAAO;GAAY,CAAC;EACtD;CACF,CAAC,CACH;AACF"}
@@ -0,0 +1,2 @@
1
+ import { a as SyncSessionError, c as SyncSessionService, d as WorkspaceRegistryLike, f as makeSyncSessionBundle, i as SyncSessionEntry, l as SyncSessionTransferPlan, m as validateSyncSessionBundle, n as SyncSessionApplyResult, o as SyncSessionInstructionSnapshot, p as planSessionTransfers, r as SyncSessionBundle, s as SyncSessionObject, t as SyncSessionApplyOptions, u as SyncSessionWorkspace } from "../../session-sync-g6qKZzjV.js";
2
+ export { SyncSessionApplyOptions, SyncSessionApplyResult, SyncSessionBundle, SyncSessionEntry, SyncSessionError, SyncSessionInstructionSnapshot, SyncSessionObject, SyncSessionService, SyncSessionTransferPlan, SyncSessionWorkspace, WorkspaceRegistryLike, makeSyncSessionBundle, planSessionTransfers, validateSyncSessionBundle };
@@ -0,0 +1,2 @@
1
+ import { a as validateSyncSessionBundle, i as planSessionTransfers, n as SyncSessionService, r as makeSyncSessionBundle, t as SyncSessionError } from "../../session-sync-EdfwUON5.js";
2
+ export { SyncSessionError, SyncSessionService, makeSyncSessionBundle, planSessionTransfers, validateSyncSessionBundle };
@@ -0,0 +1,2 @@
1
+ import { a as SyncSshTransport, i as SyncSshSession, n as SyncSshErrorCategory, o as buildSyncSshConnectConfig, r as SyncSshProbeError, t as SyncRemoteHandshake } from "../../ssh-transport-BcIiLxsX.js";
2
+ export { SyncRemoteHandshake, SyncSshErrorCategory, SyncSshProbeError, SyncSshSession, SyncSshTransport, buildSyncSshConnectConfig };