@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,258 @@
1
+ import { parseSyncPeer, parseSyncPeerDraft } from "./model.js";
2
+ import { credentialKey } from "@deepseek-ai/dsh-credentials";
3
+ //#region src/core/sync/peer-manager.ts
4
+ const SECRET_FIELDS = /* @__PURE__ */ new Set([
5
+ "password",
6
+ "privateKey",
7
+ "passphrase"
8
+ ]);
9
+ const PATCH_FIELDS = /* @__PURE__ */ new Set([
10
+ "name",
11
+ "host",
12
+ "port",
13
+ "username",
14
+ "authKind",
15
+ "remoteDshHome",
16
+ "remoteProfile",
17
+ "remoteWebPort",
18
+ "direction",
19
+ "schedule",
20
+ "syncSessions",
21
+ "enabled"
22
+ ]);
23
+ const MAX_SECRET_BYTES = 65536;
24
+ var SyncPeerInputError = class extends Error {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = "SyncPeerInputError";
28
+ }
29
+ };
30
+ var SyncPeerNotFoundError = class extends Error {
31
+ constructor(id) {
32
+ super(`sync peer ${id} not found`);
33
+ this.name = "SyncPeerNotFoundError";
34
+ }
35
+ };
36
+ function asRecord(value, label) {
37
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new SyncPeerInputError(`${label} must be an object`);
38
+ return value;
39
+ }
40
+ function parseSecrets(value) {
41
+ if (value === void 0) return void 0;
42
+ const input = asRecord(value, "secrets");
43
+ for (const key of Object.keys(input)) if (!SECRET_FIELDS.has(key)) throw new SyncPeerInputError(`unsupported secret field: ${key}`);
44
+ const output = {};
45
+ for (const field of [
46
+ "password",
47
+ "privateKey",
48
+ "passphrase"
49
+ ]) {
50
+ const secret = input[field];
51
+ if (secret === void 0 || secret === "") continue;
52
+ if (typeof secret !== "string" || Buffer.byteLength(secret, "utf8") > MAX_SECRET_BYTES || secret.includes("\0")) throw new SyncPeerInputError(`${field} is invalid`);
53
+ output[field] = secret;
54
+ }
55
+ return Object.keys(output).length === 0 ? void 0 : output;
56
+ }
57
+ function payloadFromRecord(value) {
58
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return {};
59
+ const payload = value.payload;
60
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) return {};
61
+ const source = payload;
62
+ return {
63
+ ...typeof source.password === "string" ? { password: source.password } : {},
64
+ ...typeof source.privateKey === "string" ? { privateKey: source.privateKey } : {},
65
+ ...typeof source.passphrase === "string" ? { passphrase: source.passphrase } : {}
66
+ };
67
+ }
68
+ function toView(peer, secrets) {
69
+ return {
70
+ ...peer,
71
+ hasSecret: {
72
+ hasPassword: Boolean(secrets.password),
73
+ hasKey: Boolean(secrets.privateKey),
74
+ hasPassphrase: Boolean(secrets.passphrase)
75
+ }
76
+ };
77
+ }
78
+ function validateAuthSecrets(authKind, supplied, effective) {
79
+ if (authKind === "password") {
80
+ if (supplied?.privateKey !== void 0 || supplied?.passphrase !== void 0) throw new SyncPeerInputError("key credentials do not match password authentication");
81
+ if (!effective.password) throw new SyncPeerInputError("password authentication requires a saved password");
82
+ return;
83
+ }
84
+ if (supplied?.password !== void 0) throw new SyncPeerInputError("password does not match key authentication");
85
+ if (!effective.privateKey) throw new SyncPeerInputError("key authentication requires a saved private key");
86
+ }
87
+ function secretsForAuth(authKind, secrets) {
88
+ return authKind === "password" ? secrets.password ? { password: secrets.password } : {} : {
89
+ ...secrets.privateKey ? { privateKey: secrets.privateKey } : {},
90
+ ...secrets.passphrase ? { passphrase: secrets.passphrase } : {}
91
+ };
92
+ }
93
+ function asInputError(error) {
94
+ return error instanceof SyncPeerInputError ? error : new SyncPeerInputError(error instanceof Error ? error.message : "invalid sync peer");
95
+ }
96
+ var SyncPeerManager = class {
97
+ peers;
98
+ state;
99
+ credentials;
100
+ listeners = /* @__PURE__ */ new Set();
101
+ constructor(peers, state, credentials) {
102
+ this.peers = peers;
103
+ this.state = state;
104
+ this.credentials = credentials;
105
+ }
106
+ async list() {
107
+ return Promise.all(this.peers.list().map((peer) => this.view(peer)));
108
+ }
109
+ subscribe(listener) {
110
+ this.listeners.add(listener);
111
+ return () => {
112
+ this.listeners.delete(listener);
113
+ };
114
+ }
115
+ updateProgress(id, patch) {
116
+ if (!this.peers.get(id)) throw new SyncPeerNotFoundError(id);
117
+ this.state.update((state) => {
118
+ state.peers[id] = {
119
+ ...state.peers[id],
120
+ ...patch
121
+ };
122
+ });
123
+ }
124
+ async get(id) {
125
+ const peer = this.peers.get(id);
126
+ return peer ? this.view(peer) : void 0;
127
+ }
128
+ async connectionData(id) {
129
+ const peer = this.peers.get(id);
130
+ if (!peer) throw new SyncPeerNotFoundError(id);
131
+ return {
132
+ peer,
133
+ secrets: payloadFromRecord(await this.credentials.readRecord(this.credentialKey(id)))
134
+ };
135
+ }
136
+ async pinHostKey(id, fingerprint) {
137
+ if (!/^SHA256:[A-Za-z0-9+/]{43}$/.test(fingerprint)) throw new SyncPeerInputError("invalid SSH host-key fingerprint");
138
+ const peer = this.peers.update(id, { hostKeyFingerprint: fingerprint });
139
+ return this.view(peer);
140
+ }
141
+ async create(draftValue, secretsValue) {
142
+ let draft;
143
+ let secrets;
144
+ try {
145
+ draft = parseSyncPeerDraft(draftValue);
146
+ secrets = parseSecrets(secretsValue);
147
+ validateAuthSecrets(draft.authKind, secrets, secrets ?? {});
148
+ } catch (error) {
149
+ throw asInputError(error);
150
+ }
151
+ const peer = this.peers.create(draft);
152
+ let attemptedSecrets = false;
153
+ try {
154
+ if (secrets) {
155
+ attemptedSecrets = true;
156
+ await this.writeSecrets(peer.id, secretsForAuth(peer.authKind, secrets));
157
+ }
158
+ const view = await this.view(peer);
159
+ this.notify(peer.id, peer);
160
+ return view;
161
+ } catch (error) {
162
+ this.peers.remove(peer.id);
163
+ if (attemptedSecrets) try {
164
+ await this.credentials.deleteRecord(this.credentialKey(peer.id));
165
+ } catch {}
166
+ throw error;
167
+ }
168
+ }
169
+ async update(id, patchValue, secretsValue) {
170
+ const current = this.peers.get(id);
171
+ if (!current) throw new SyncPeerNotFoundError(id);
172
+ let patch;
173
+ let secrets;
174
+ try {
175
+ const input = asRecord(patchValue, "sync peer patch");
176
+ for (const key of Object.keys(input)) if (!PATCH_FIELDS.has(key)) throw new SyncPeerInputError(`unsupported peer field: ${key}`);
177
+ const { id: _id, hostKeyFingerprint: _fingerprint, ...validatedPatch } = parseSyncPeer({
178
+ ...current,
179
+ ...input,
180
+ id
181
+ });
182
+ patch = validatedPatch;
183
+ secrets = parseSecrets(secretsValue);
184
+ } catch (error) {
185
+ throw asInputError(error);
186
+ }
187
+ const credentialKeyValue = this.credentialKey(id);
188
+ const previousRecord = await this.credentials.readRecord(credentialKeyValue);
189
+ const effectiveSecrets = {
190
+ ...payloadFromRecord(previousRecord),
191
+ ...secrets
192
+ };
193
+ const authKind = patch.authKind ?? current.authKind;
194
+ validateAuthSecrets(authKind, secrets, effectiveSecrets);
195
+ if (secrets || authKind !== current.authKind) try {
196
+ await this.writeSecrets(id, secretsForAuth(authKind, effectiveSecrets));
197
+ } catch (error) {
198
+ await this.restoreCredentialRecord(credentialKeyValue, previousRecord);
199
+ throw error;
200
+ }
201
+ let updated;
202
+ try {
203
+ updated = this.peers.update(id, patch);
204
+ } catch (error) {
205
+ await this.restoreCredentialRecord(credentialKeyValue, previousRecord);
206
+ throw error;
207
+ }
208
+ this.notify(id, updated);
209
+ return this.view(updated);
210
+ }
211
+ async remove(id) {
212
+ if (!this.peers.get(id)) return false;
213
+ this.peers.remove(id);
214
+ this.notify(id, void 0);
215
+ await this.credentials.deleteRecord(this.credentialKey(id));
216
+ this.state.update((state) => {
217
+ delete state.peers[id];
218
+ });
219
+ return true;
220
+ }
221
+ async view(peer) {
222
+ const record = await this.credentials.readRecord(this.credentialKey(peer.id));
223
+ const progress = this.state.read().peers[peer.id];
224
+ return {
225
+ ...toView(peer, payloadFromRecord(record)),
226
+ ...progress ? { progress } : {}
227
+ };
228
+ }
229
+ notify(peerId, peer) {
230
+ for (const listener of this.listeners) try {
231
+ listener(peerId, peer);
232
+ } catch {}
233
+ }
234
+ async writeSecrets(id, secrets) {
235
+ const key = this.credentialKey(id);
236
+ await this.credentials.modifyRecord(key, async (_current) => ({
237
+ kind: "grant",
238
+ payload: secrets
239
+ }));
240
+ }
241
+ async restoreCredentialRecord(key, previous) {
242
+ try {
243
+ if (previous === void 0) await this.credentials.deleteRecord(key);
244
+ else await this.credentials.modifyRecord(key, async () => previous);
245
+ } catch {}
246
+ }
247
+ credentialKey(id) {
248
+ try {
249
+ return credentialKey("dshb-core", id);
250
+ } catch {
251
+ throw new Error("credentials service unavailable");
252
+ }
253
+ }
254
+ };
255
+ //#endregion
256
+ export { SyncPeerInputError, SyncPeerManager, SyncPeerNotFoundError };
257
+
258
+ //# sourceMappingURL=peer-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-manager.js","names":[],"sources":["../../../src/core/sync/peer-manager.ts"],"sourcesContent":["import { credentialKey, type CredentialKey, type CredentialRecord } from '@deepseek-ai/dsh-credentials'\nimport { parseSyncPeer, parseSyncPeerDraft, type SyncPeer, type SyncPeerDraft, type SyncPeerProgress } from './model.js'\nimport { SyncPeerStore, SyncStateStore } from './stores.js'\n\nexport interface SyncPeerSecrets {\n password?: string\n privateKey?: string\n passphrase?: string\n}\n\nexport interface SyncPeerView extends SyncPeer {\n hasSecret: { hasPassword: boolean; hasKey: boolean; hasPassphrase: boolean }\n progress?: SyncPeerProgress\n}\n\nexport type SyncPeerChangeListener = (peerId: string, peer: SyncPeer | undefined) => void\n\ninterface CredentialsLike {\n readRecord(key: CredentialKey): Promise<unknown>\n modifyRecord(key: CredentialKey, mutate: (current: unknown) => Promise<unknown>): Promise<unknown>\n deleteRecord(key: CredentialKey): Promise<void>\n}\n\nconst SECRET_FIELDS = new Set(['password', 'privateKey', 'passphrase'])\nconst PATCH_FIELDS = new Set([\n 'name',\n 'host',\n 'port',\n 'username',\n 'authKind',\n 'remoteDshHome',\n 'remoteProfile',\n 'remoteWebPort',\n 'direction',\n 'schedule',\n 'syncSessions',\n 'enabled',\n])\nconst MAX_SECRET_BYTES = 64 * 1024\n\nexport class SyncPeerInputError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SyncPeerInputError'\n }\n}\n\nexport class SyncPeerNotFoundError extends Error {\n constructor(id: string) {\n super(`sync peer ${id} not found`)\n this.name = 'SyncPeerNotFoundError'\n }\n}\n\nfunction asRecord(value: unknown, label: string): Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new SyncPeerInputError(`${label} must be an object`)\n }\n return value as Record<string, unknown>\n}\n\nfunction parseSecrets(value: unknown): SyncPeerSecrets | undefined {\n if (value === undefined) return undefined\n const input = asRecord(value, 'secrets')\n for (const key of Object.keys(input)) {\n if (!SECRET_FIELDS.has(key)) throw new SyncPeerInputError(`unsupported secret field: ${key}`)\n }\n\n const output: SyncPeerSecrets = {}\n for (const field of ['password', 'privateKey', 'passphrase'] as const) {\n const secret = input[field]\n if (secret === undefined || secret === '') continue\n if (typeof secret !== 'string' || Buffer.byteLength(secret, 'utf8') > MAX_SECRET_BYTES || secret.includes('\\0')) {\n throw new SyncPeerInputError(`${field} is invalid`)\n }\n output[field] = secret\n }\n return Object.keys(output).length === 0 ? undefined : output\n}\n\nfunction payloadFromRecord(value: unknown): SyncPeerSecrets {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return {}\n const payload = (value as { payload?: unknown }).payload\n if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return {}\n const source = payload as Record<string, unknown>\n return {\n ...(typeof source.password === 'string' ? { password: source.password } : {}),\n ...(typeof source.privateKey === 'string' ? { privateKey: source.privateKey } : {}),\n ...(typeof source.passphrase === 'string' ? { passphrase: source.passphrase } : {}),\n }\n}\n\nfunction toView(peer: SyncPeer, secrets: SyncPeerSecrets): SyncPeerView {\n return {\n ...peer,\n hasSecret: {\n hasPassword: Boolean(secrets.password),\n hasKey: Boolean(secrets.privateKey),\n hasPassphrase: Boolean(secrets.passphrase),\n },\n }\n}\n\nfunction validateAuthSecrets(\n authKind: SyncPeer['authKind'],\n supplied: SyncPeerSecrets | undefined,\n effective: SyncPeerSecrets,\n): void {\n if (authKind === 'password') {\n if (supplied?.privateKey !== undefined || supplied?.passphrase !== undefined) {\n throw new SyncPeerInputError('key credentials do not match password authentication')\n }\n if (!effective.password) throw new SyncPeerInputError('password authentication requires a saved password')\n return\n }\n if (supplied?.password !== undefined) throw new SyncPeerInputError('password does not match key authentication')\n if (!effective.privateKey) throw new SyncPeerInputError('key authentication requires a saved private key')\n}\n\nfunction secretsForAuth(authKind: SyncPeer['authKind'], secrets: SyncPeerSecrets): SyncPeerSecrets {\n return authKind === 'password'\n ? (secrets.password ? { password: secrets.password } : {})\n : {\n ...(secrets.privateKey ? { privateKey: secrets.privateKey } : {}),\n ...(secrets.passphrase ? { passphrase: secrets.passphrase } : {}),\n }\n}\n\nfunction asInputError(error: unknown): SyncPeerInputError {\n return error instanceof SyncPeerInputError\n ? error\n : new SyncPeerInputError(error instanceof Error ? error.message : 'invalid sync peer')\n}\n\nexport class SyncPeerManager {\n private readonly listeners = new Set<SyncPeerChangeListener>()\n\n constructor(\n private readonly peers: SyncPeerStore,\n private readonly state: SyncStateStore,\n private readonly credentials: CredentialsLike,\n ) {}\n\n async list(): Promise<SyncPeerView[]> {\n return Promise.all(this.peers.list().map((peer) => this.view(peer)))\n }\n\n subscribe(listener: SyncPeerChangeListener): () => void {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n updateProgress(id: string, patch: SyncPeerProgress): void {\n if (!this.peers.get(id)) throw new SyncPeerNotFoundError(id)\n this.state.update((state) => {\n state.peers[id] = { ...state.peers[id], ...patch }\n })\n }\n\n async get(id: string): Promise<SyncPeerView | undefined> {\n const peer = this.peers.get(id)\n return peer ? this.view(peer) : undefined\n }\n\n async connectionData(id: string): Promise<{ peer: SyncPeer; secrets: SyncPeerSecrets }> {\n const peer = this.peers.get(id)\n if (!peer) throw new SyncPeerNotFoundError(id)\n const record = await this.credentials.readRecord(this.credentialKey(id))\n return { peer, secrets: payloadFromRecord(record) }\n }\n\n async pinHostKey(id: string, fingerprint: string): Promise<SyncPeerView> {\n if (!/^SHA256:[A-Za-z0-9+/]{43}$/.test(fingerprint)) {\n throw new SyncPeerInputError('invalid SSH host-key fingerprint')\n }\n const peer = this.peers.update(id, { hostKeyFingerprint: fingerprint })\n return this.view(peer)\n }\n\n async create(draftValue: unknown, secretsValue?: unknown): Promise<SyncPeerView> {\n let draft: SyncPeerDraft\n let secrets: SyncPeerSecrets | undefined\n try {\n draft = parseSyncPeerDraft(draftValue)\n secrets = parseSecrets(secretsValue)\n validateAuthSecrets(draft.authKind, secrets, secrets ?? {})\n } catch (error) {\n throw asInputError(error)\n }\n\n const peer = this.peers.create(draft)\n let attemptedSecrets = false\n try {\n if (secrets) {\n attemptedSecrets = true\n await this.writeSecrets(peer.id, secretsForAuth(peer.authKind, secrets))\n }\n const view = await this.view(peer)\n this.notify(peer.id, peer)\n return view\n } catch (error) {\n this.peers.remove(peer.id)\n if (attemptedSecrets) {\n try {\n await this.credentials.deleteRecord(this.credentialKey(peer.id))\n } catch {}\n }\n throw error\n }\n }\n\n async update(id: string, patchValue: unknown, secretsValue?: unknown): Promise<SyncPeerView> {\n const current = this.peers.get(id)\n if (!current) throw new SyncPeerNotFoundError(id)\n\n let patch: Partial<Omit<SyncPeer, 'id' | 'hostKeyFingerprint'>>\n let secrets: SyncPeerSecrets | undefined\n try {\n const input = asRecord(patchValue, 'sync peer patch')\n for (const key of Object.keys(input)) {\n if (!PATCH_FIELDS.has(key)) throw new SyncPeerInputError(`unsupported peer field: ${key}`)\n }\n const validated = parseSyncPeer({ ...current, ...input, id })\n const { id: _id, hostKeyFingerprint: _fingerprint, ...validatedPatch } = validated\n void _id\n void _fingerprint\n patch = validatedPatch\n secrets = parseSecrets(secretsValue)\n } catch (error) {\n throw asInputError(error)\n }\n\n const credentialKeyValue = this.credentialKey(id)\n const previousRecord = await this.credentials.readRecord(credentialKeyValue)\n const effectiveSecrets = { ...payloadFromRecord(previousRecord), ...secrets }\n const authKind = patch.authKind ?? current.authKind\n validateAuthSecrets(authKind, secrets, effectiveSecrets)\n if (secrets || authKind !== current.authKind) {\n try {\n await this.writeSecrets(id, secretsForAuth(authKind, effectiveSecrets))\n } catch (error) {\n await this.restoreCredentialRecord(credentialKeyValue, previousRecord)\n throw error\n }\n }\n\n let updated: SyncPeer\n try {\n updated = this.peers.update(id, patch)\n } catch (error) {\n await this.restoreCredentialRecord(credentialKeyValue, previousRecord)\n throw error\n }\n this.notify(id, updated)\n return this.view(updated)\n }\n\n async remove(id: string): Promise<boolean> {\n if (!this.peers.get(id)) return false\n this.peers.remove(id)\n this.notify(id, undefined)\n await this.credentials.deleteRecord(this.credentialKey(id))\n this.state.update((state) => {\n delete state.peers[id]\n })\n return true\n }\n\n private async view(peer: SyncPeer): Promise<SyncPeerView> {\n const record = await this.credentials.readRecord(this.credentialKey(peer.id))\n const progress = this.state.read().peers[peer.id]\n return { ...toView(peer, payloadFromRecord(record)), ...(progress ? { progress } : {}) }\n }\n\n private notify(peerId: string, peer: SyncPeer | undefined): void {\n for (const listener of this.listeners) {\n try {\n listener(peerId, peer)\n } catch {}\n }\n }\n\n private async writeSecrets(id: string, secrets: SyncPeerSecrets): Promise<void> {\n const key = this.credentialKey(id)\n await this.credentials.modifyRecord(key, async (_current) => ({\n kind: 'grant',\n payload: secrets,\n }) as CredentialRecord)\n }\n\n private async restoreCredentialRecord(key: CredentialKey, previous: unknown): Promise<void> {\n try {\n if (previous === undefined) await this.credentials.deleteRecord(key)\n else await this.credentials.modifyRecord(key, async () => previous)\n } catch {}\n }\n\n private credentialKey(id: string): CredentialKey {\n try {\n return credentialKey('dshb-core', id)\n } catch {\n throw new Error('credentials service unavailable')\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { a as SyncFrame, c as encodeSyncControlFrame, i as SYNC_PROTOCOL_VERSION, l as encodeSyncDataFrame, n as MAX_SYNC_DATA_CHUNK_BYTES, o as SyncFrameDecoder, r as MAX_SYNC_TRANSFER_BYTES, s as SyncProtocolError, t as MAX_SYNC_CONTROL_FRAME_BYTES } from "../../protocol-CV7LGE44.js";
2
+ export { MAX_SYNC_CONTROL_FRAME_BYTES, MAX_SYNC_DATA_CHUNK_BYTES, MAX_SYNC_TRANSFER_BYTES, SYNC_PROTOCOL_VERSION, SyncFrame, SyncFrameDecoder, SyncProtocolError, encodeSyncControlFrame, encodeSyncDataFrame };
@@ -0,0 +1,124 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ //#region src/core/sync/protocol.ts
3
+ const SYNC_PROTOCOL_VERSION = 1;
4
+ const MAX_SYNC_CONTROL_FRAME_BYTES = 65536;
5
+ const MAX_SYNC_DATA_CHUNK_BYTES = 1048576;
6
+ const MAX_SYNC_TRANSFER_BYTES = 107374182400;
7
+ const FRAME_HEADER_BYTES = 5;
8
+ const CONTROL_FRAME = 0;
9
+ const DATA_FRAME = 1;
10
+ const DATA_METADATA_BYTES = 36;
11
+ var SyncProtocolError = class extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "SyncProtocolError";
15
+ }
16
+ };
17
+ function makeHeader(type, payloadLength) {
18
+ const header = Buffer.allocUnsafe(FRAME_HEADER_BYTES);
19
+ header[0] = type;
20
+ header.writeUInt32BE(payloadLength, 1);
21
+ return header;
22
+ }
23
+ function encodeSyncControlFrame(value) {
24
+ const payload = Buffer.from(`${JSON.stringify(value)}\n`, "utf8");
25
+ if (payload.length > 65536) throw new SyncProtocolError("control frame exceeds size limit");
26
+ return Buffer.concat([makeHeader(CONTROL_FRAME, payload.length), payload]);
27
+ }
28
+ function encodeSyncDataFrame(sequence, data) {
29
+ if (!Number.isSafeInteger(sequence) || sequence < 0 || sequence > 4294967295) throw new SyncProtocolError("data sequence is invalid");
30
+ if (data.length > 1048576) throw new SyncProtocolError("data chunk exceeds size limit");
31
+ const metadata = Buffer.allocUnsafe(DATA_METADATA_BYTES);
32
+ metadata.writeUInt32BE(sequence, 0);
33
+ createHash("sha256").update(data).digest().copy(metadata, 4);
34
+ return Buffer.concat([
35
+ makeHeader(DATA_FRAME, metadata.length + data.length),
36
+ metadata,
37
+ data
38
+ ]);
39
+ }
40
+ var SyncFrameDecoder = class {
41
+ header = Buffer.allocUnsafe(FRAME_HEADER_BYTES);
42
+ headerOffset = 0;
43
+ frameType = -1;
44
+ payload;
45
+ payloadOffset = 0;
46
+ nextDataSequence = 0;
47
+ ended = false;
48
+ push(chunk) {
49
+ if (this.ended) throw new SyncProtocolError("frame decoder is already finished");
50
+ const frames = [];
51
+ let inputOffset = 0;
52
+ while (inputOffset < chunk.length) {
53
+ if (!this.payload) {
54
+ const copied = Math.min(FRAME_HEADER_BYTES - this.headerOffset, chunk.length - inputOffset);
55
+ chunk.copy(this.header, this.headerOffset, inputOffset, inputOffset + copied);
56
+ this.headerOffset += copied;
57
+ inputOffset += copied;
58
+ if (this.headerOffset < FRAME_HEADER_BYTES) continue;
59
+ this.frameType = this.header[0];
60
+ const payloadLength = this.header.readUInt32BE(1);
61
+ if (this.frameType === CONTROL_FRAME) {
62
+ if (payloadLength < 2 || payloadLength > 65536) throw new SyncProtocolError("control frame length is invalid");
63
+ } else if (this.frameType === DATA_FRAME) {
64
+ if (payloadLength < DATA_METADATA_BYTES || payloadLength > 1048612) throw new SyncProtocolError("data frame length is invalid");
65
+ } else throw new SyncProtocolError("unknown frame type");
66
+ this.payload = Buffer.allocUnsafe(payloadLength);
67
+ this.payloadOffset = 0;
68
+ }
69
+ const payload = this.payload;
70
+ const copied = Math.min(payload.length - this.payloadOffset, chunk.length - inputOffset);
71
+ chunk.copy(payload, this.payloadOffset, inputOffset, inputOffset + copied);
72
+ this.payloadOffset += copied;
73
+ inputOffset += copied;
74
+ if (this.payloadOffset === payload.length) {
75
+ frames.push(this.decodeFrame(this.frameType, payload));
76
+ this.resetFrame();
77
+ }
78
+ }
79
+ return frames;
80
+ }
81
+ finish() {
82
+ if (this.headerOffset !== 0 || this.payload !== void 0) throw new SyncProtocolError("truncated frame stream");
83
+ this.ended = true;
84
+ }
85
+ decodeFrame(type, payload) {
86
+ if (type === CONTROL_FRAME) {
87
+ if (payload[payload.length - 1] !== 10) throw new SyncProtocolError("control frame must end with a newline");
88
+ let value;
89
+ try {
90
+ value = JSON.parse(payload.subarray(0, -1).toString("utf8"));
91
+ } catch {
92
+ throw new SyncProtocolError("control frame contains invalid JSON");
93
+ }
94
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new SyncProtocolError("control frame must contain an object");
95
+ return {
96
+ kind: "control",
97
+ value
98
+ };
99
+ }
100
+ const sequence = payload.readUInt32BE(0);
101
+ if (sequence !== this.nextDataSequence) throw new SyncProtocolError("data sequence is out of order");
102
+ const expectedDigest = payload.subarray(4, DATA_METADATA_BYTES);
103
+ const data = payload.subarray(DATA_METADATA_BYTES);
104
+ const actualDigest = createHash("sha256").update(data).digest();
105
+ if (!timingSafeEqual(expectedDigest, actualDigest)) throw new SyncProtocolError("data chunk digest mismatch");
106
+ this.nextDataSequence += 1;
107
+ return {
108
+ kind: "data",
109
+ sequence,
110
+ digest: actualDigest.toString("hex"),
111
+ data: Buffer.from(data)
112
+ };
113
+ }
114
+ resetFrame() {
115
+ this.headerOffset = 0;
116
+ this.frameType = -1;
117
+ this.payload = void 0;
118
+ this.payloadOffset = 0;
119
+ }
120
+ };
121
+ //#endregion
122
+ export { MAX_SYNC_CONTROL_FRAME_BYTES, MAX_SYNC_DATA_CHUNK_BYTES, MAX_SYNC_TRANSFER_BYTES, SYNC_PROTOCOL_VERSION, SyncFrameDecoder, SyncProtocolError, encodeSyncControlFrame, encodeSyncDataFrame };
123
+
124
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","names":[],"sources":["../../../src/core/sync/protocol.ts"],"sourcesContent":["import { createHash, timingSafeEqual } from 'node:crypto'\n\nexport const SYNC_PROTOCOL_VERSION = 1\nexport const MAX_SYNC_CONTROL_FRAME_BYTES = 64 * 1024\nexport const MAX_SYNC_DATA_CHUNK_BYTES = 1024 * 1024\nexport const MAX_SYNC_TRANSFER_BYTES = 100 * 1024 * 1024 * 1024\n\nconst FRAME_HEADER_BYTES = 5\nconst CONTROL_FRAME = 0\nconst DATA_FRAME = 1\nconst DATA_METADATA_BYTES = 36\n\nexport type SyncFrame =\n | { kind: 'control'; value: Record<string, unknown> }\n | { kind: 'data'; sequence: number; digest: string; data: Buffer }\n\nexport class SyncProtocolError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SyncProtocolError'\n }\n}\n\nfunction makeHeader(type: number, payloadLength: number): Buffer {\n const header = Buffer.allocUnsafe(FRAME_HEADER_BYTES)\n header[0] = type\n header.writeUInt32BE(payloadLength, 1)\n return header\n}\n\nexport function encodeSyncControlFrame(value: Record<string, unknown>): Buffer {\n const payload = Buffer.from(`${JSON.stringify(value)}\\n`, 'utf8')\n if (payload.length > MAX_SYNC_CONTROL_FRAME_BYTES) throw new SyncProtocolError('control frame exceeds size limit')\n return Buffer.concat([makeHeader(CONTROL_FRAME, payload.length), payload])\n}\n\nexport function encodeSyncDataFrame(sequence: number, data: Buffer): Buffer {\n if (!Number.isSafeInteger(sequence) || sequence < 0 || sequence > 0xffffffff) {\n throw new SyncProtocolError('data sequence is invalid')\n }\n if (data.length > MAX_SYNC_DATA_CHUNK_BYTES) throw new SyncProtocolError('data chunk exceeds size limit')\n const metadata = Buffer.allocUnsafe(DATA_METADATA_BYTES)\n metadata.writeUInt32BE(sequence, 0)\n createHash('sha256').update(data).digest().copy(metadata, 4)\n return Buffer.concat([makeHeader(DATA_FRAME, metadata.length + data.length), metadata, data])\n}\n\nexport class SyncFrameDecoder {\n private readonly header = Buffer.allocUnsafe(FRAME_HEADER_BYTES)\n private headerOffset = 0\n private frameType = -1\n private payload: Buffer | undefined\n private payloadOffset = 0\n private nextDataSequence = 0\n private ended = false\n\n push(chunk: Buffer): SyncFrame[] {\n if (this.ended) throw new SyncProtocolError('frame decoder is already finished')\n const frames: SyncFrame[] = []\n let inputOffset = 0\n\n while (inputOffset < chunk.length) {\n if (!this.payload) {\n const copied = Math.min(FRAME_HEADER_BYTES - this.headerOffset, chunk.length - inputOffset)\n chunk.copy(this.header, this.headerOffset, inputOffset, inputOffset + copied)\n this.headerOffset += copied\n inputOffset += copied\n if (this.headerOffset < FRAME_HEADER_BYTES) continue\n\n this.frameType = this.header[0]\n const payloadLength = this.header.readUInt32BE(1)\n if (this.frameType === CONTROL_FRAME) {\n if (payloadLength < 2 || payloadLength > MAX_SYNC_CONTROL_FRAME_BYTES) {\n throw new SyncProtocolError('control frame length is invalid')\n }\n } else if (this.frameType === DATA_FRAME) {\n if (payloadLength < DATA_METADATA_BYTES || payloadLength > DATA_METADATA_BYTES + MAX_SYNC_DATA_CHUNK_BYTES) {\n throw new SyncProtocolError('data frame length is invalid')\n }\n } else {\n throw new SyncProtocolError('unknown frame type')\n }\n this.payload = Buffer.allocUnsafe(payloadLength)\n this.payloadOffset = 0\n }\n\n const payload = this.payload\n const copied = Math.min(payload.length - this.payloadOffset, chunk.length - inputOffset)\n chunk.copy(payload, this.payloadOffset, inputOffset, inputOffset + copied)\n this.payloadOffset += copied\n inputOffset += copied\n\n if (this.payloadOffset === payload.length) {\n frames.push(this.decodeFrame(this.frameType, payload))\n this.resetFrame()\n }\n }\n return frames\n }\n\n finish(): void {\n if (this.headerOffset !== 0 || this.payload !== undefined) throw new SyncProtocolError('truncated frame stream')\n this.ended = true\n }\n\n private decodeFrame(type: number, payload: Buffer): SyncFrame {\n if (type === CONTROL_FRAME) {\n if (payload[payload.length - 1] !== 0x0a) throw new SyncProtocolError('control frame must end with a newline')\n let value: unknown\n try {\n value = JSON.parse(payload.subarray(0, -1).toString('utf8'))\n } catch {\n throw new SyncProtocolError('control frame contains invalid JSON')\n }\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new SyncProtocolError('control frame must contain an object')\n }\n return { kind: 'control', value: value as Record<string, unknown> }\n }\n\n const sequence = payload.readUInt32BE(0)\n if (sequence !== this.nextDataSequence) throw new SyncProtocolError('data sequence is out of order')\n const expectedDigest = payload.subarray(4, DATA_METADATA_BYTES)\n const data = payload.subarray(DATA_METADATA_BYTES)\n const actualDigest = createHash('sha256').update(data).digest()\n if (!timingSafeEqual(expectedDigest, actualDigest)) throw new SyncProtocolError('data chunk digest mismatch')\n this.nextDataSequence += 1\n return { kind: 'data', sequence, digest: actualDigest.toString('hex'), data: Buffer.from(data) }\n }\n\n private resetFrame(): void {\n this.headerOffset = 0\n this.frameType = -1\n this.payload = undefined\n this.payloadOffset = 0\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ //#region src/core/sync/remote-helper.d.ts
2
+ declare const REMOTE_SYNC_HELPER_COMMAND: string;
3
+ //#endregion
4
+ export { REMOTE_SYNC_HELPER_COMMAND };
5
+ //# sourceMappingURL=remote-helper.d.ts.map
@@ -0,0 +1,160 @@
1
+ import { Buffer } from "node:buffer";
2
+ //#region src/core/sync/remote-helper.ts
3
+ const REMOTE_HELPER_SOURCE = String.raw`'use strict'
4
+ const { createHash, timingSafeEqual } = require('node:crypto')
5
+ const MAX_CONTROL = 65536
6
+ const MAX_CHUNK = 1048576
7
+ const MAX_TRANSFER = 107374182400
8
+ const PATHS = {
9
+ handshake: { path: '/api/dshb/sync/internal/handshake', method: 'GET' },
10
+ 'node-manifest': { path: '/api/dshb/sync/internal/nodes/manifest', method: 'GET' },
11
+ 'node-apply': { path: '/api/dshb/sync/internal/nodes/apply', method: 'POST' },
12
+ 'session-bundle': { path: '/api/dshb/sync/internal/sessions/bundle', method: 'GET' },
13
+ 'session-apply': { path: '/api/dshb/sync/internal/sessions/apply', method: 'POST' },
14
+ }
15
+ const input = []
16
+ let inputBytes = 0
17
+
18
+ function encodeControl(value) {
19
+ const body = Buffer.from(JSON.stringify(value) + '\n', 'utf8')
20
+ if (body.length > MAX_CONTROL) throw new Error('control frame too large')
21
+ const header = Buffer.alloc(5)
22
+ header[0] = 0
23
+ header.writeUInt32BE(body.length, 1)
24
+ return Buffer.concat([header, body])
25
+ }
26
+
27
+ function encodeData(sequence, data) {
28
+ const digest = createHash('sha256').update(data).digest()
29
+ const metadata = Buffer.alloc(36)
30
+ metadata.writeUInt32BE(sequence, 0)
31
+ digest.copy(metadata, 4)
32
+ const payload = Buffer.concat([metadata, data])
33
+ const header = Buffer.alloc(5)
34
+ header[0] = 1
35
+ header.writeUInt32BE(payload.length, 1)
36
+ return Buffer.concat([header, payload])
37
+ }
38
+
39
+ function decodeFrames(buffer) {
40
+ const frames = []
41
+ let offset = 0
42
+ let sequence = 0
43
+ let totalData = 0
44
+ while (offset < buffer.length) {
45
+ if (buffer.length - offset < 5) throw new Error('truncated frame header')
46
+ const kind = buffer[offset]
47
+ const length = buffer.readUInt32BE(offset + 1)
48
+ offset += 5
49
+ const maxLength = kind === 0 ? MAX_CONTROL : 36 + MAX_CHUNK
50
+ if ((kind !== 0 && kind !== 1) || length > maxLength || length > buffer.length - offset) {
51
+ throw new Error('invalid frame length')
52
+ }
53
+ const payload = buffer.subarray(offset, offset + length)
54
+ offset += length
55
+ if (kind === 0) {
56
+ if (length < 2 || payload[length - 1] !== 10) throw new Error('invalid control frame')
57
+ frames.push({ kind: 'control', value: JSON.parse(payload.subarray(0, -1).toString('utf8')) })
58
+ } else {
59
+ if (length < 36) throw new Error('invalid data frame')
60
+ const currentSequence = payload.readUInt32BE(0)
61
+ const expected = payload.subarray(4, 36)
62
+ const data = payload.subarray(36)
63
+ const actual = createHash('sha256').update(data).digest()
64
+ if (currentSequence !== sequence || !timingSafeEqual(expected, actual)) throw new Error('invalid data chunk')
65
+ sequence += 1
66
+ totalData += data.length
67
+ if (totalData > MAX_TRANSFER) throw new Error('transfer exceeds size limit')
68
+ frames.push({ kind: 'data', data })
69
+ }
70
+ }
71
+ return frames
72
+ }
73
+
74
+ function parseRequest(buffer) {
75
+ const frames = decodeFrames(buffer)
76
+ const first = frames[0]
77
+ if (!first || first.kind !== 'control') throw new Error('request control frame required')
78
+ const request = first.value
79
+ if (!request || request.kind !== 'request' || request.protocolVersion !== 1 || !Object.prototype.hasOwnProperty.call(PATHS, request.operation)) {
80
+ throw new Error('unsupported helper request')
81
+ }
82
+ if (Object.keys(request).some((key) => !['kind', 'protocolVersion', 'operation', 'port'].includes(key))) {
83
+ throw new Error('unsupported helper request field')
84
+ }
85
+ if (!Number.isInteger(request.port) || request.port < 1 || request.port > 65535) throw new Error('invalid loopback port')
86
+ const operation = PATHS[request.operation]
87
+ if (request.operation === 'node-apply' || request.operation === 'session-apply') {
88
+ const last = frames[frames.length - 1]
89
+ if (!last || last.kind !== 'control' || last.value.kind !== 'request-end') throw new Error('apply request terminator required')
90
+ if (frames.slice(1, -1).some((frame) => frame.kind !== 'data')) throw new Error('invalid apply request body')
91
+ const chunks = frames.slice(1, -1).map((frame) => frame.data)
92
+ return { request, operation, body: Buffer.concat(chunks) }
93
+ }
94
+ if (frames.length !== 1) throw new Error('unexpected helper request frames')
95
+ return { request, operation }
96
+ }
97
+
98
+ async function run() {
99
+ const buffer = Buffer.concat(input, inputBytes)
100
+ const { request, operation, body } = parseRequest(buffer)
101
+ const url = 'http://127.0.0.1:' + request.port + operation.path
102
+ const response = await fetch(url, {
103
+ method: operation.method,
104
+ redirect: 'error',
105
+ headers: { accept: 'application/json', ...(body ? { 'content-type': 'application/json' } : {}) },
106
+ ...(body ? { body } : {}),
107
+ })
108
+
109
+ function writeStdout(data) {
110
+ return new Promise(function(resolve, reject) {
111
+ if (process.stdout.write(data)) resolve()
112
+ else process.stdout.once('drain', resolve)
113
+ })
114
+ }
115
+
116
+ if (request.operation === 'node-manifest' || request.operation === 'session-bundle') {
117
+ await writeStdout(encodeControl({ kind: 'response', status: response.status, bodyMode: 'data' }))
118
+ if (!response.body) throw new Error('manifest response body missing')
119
+ var sequence = 0
120
+ var total = 0
121
+ for await (var chunk of response.body) {
122
+ var data = Buffer.from(chunk)
123
+ total += data.length
124
+ if (total > MAX_TRANSFER) throw new Error('manifest exceeds size limit')
125
+ for (var offset = 0; offset < data.length; offset += MAX_CHUNK) {
126
+ await writeStdout(encodeData(sequence++, data.subarray(offset, Math.min(offset + MAX_CHUNK, data.length))))
127
+ }
128
+ }
129
+ await writeStdout(encodeControl({ kind: 'response-end' }))
130
+ if (!response.ok) process.exitCode = 1
131
+ return
132
+ }
133
+
134
+ var text = await response.text()
135
+ if (Buffer.byteLength(text, 'utf8') > MAX_CONTROL) throw new Error('response control frame too large')
136
+ await writeStdout(encodeControl({ kind: 'response', status: response.status, body: JSON.parse(text) }))
137
+ if (!response.ok) process.exitCode = 1
138
+ }
139
+
140
+ process.stdin.on('data', (chunk) => {
141
+ inputBytes += chunk.length
142
+ if (inputBytes > MAX_TRANSFER + 2 * MAX_CONTROL) {
143
+ process.exitCode = 1
144
+ process.stdin.destroy()
145
+ return
146
+ }
147
+ input.push(Buffer.from(chunk))
148
+ })
149
+ process.stdin.on('end', () => {
150
+ run().catch(() => {
151
+ try { process.stdout.write(encodeControl({ kind: 'error', code: 'helper-failed' })) } catch {}
152
+ process.exitCode = 1
153
+ })
154
+ })
155
+ `;
156
+ const REMOTE_SYNC_HELPER_COMMAND = `node -e 'eval(Buffer.from("${Buffer.from(REMOTE_HELPER_SOURCE, "utf8").toString("base64")}","base64").toString("utf8"))'`;
157
+ //#endregion
158
+ export { REMOTE_SYNC_HELPER_COMMAND };
159
+
160
+ //# sourceMappingURL=remote-helper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-helper.js","names":[],"sources":["../../../src/core/sync/remote-helper.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\n\nconst REMOTE_HELPER_SOURCE = String.raw`'use strict'\nconst { createHash, timingSafeEqual } = require('node:crypto')\nconst MAX_CONTROL = 65536\nconst MAX_CHUNK = 1048576\nconst MAX_TRANSFER = 107374182400\nconst PATHS = {\n handshake: { path: '/api/dshb/sync/internal/handshake', method: 'GET' },\n 'node-manifest': { path: '/api/dshb/sync/internal/nodes/manifest', method: 'GET' },\n 'node-apply': { path: '/api/dshb/sync/internal/nodes/apply', method: 'POST' },\n 'session-bundle': { path: '/api/dshb/sync/internal/sessions/bundle', method: 'GET' },\n 'session-apply': { path: '/api/dshb/sync/internal/sessions/apply', method: 'POST' },\n}\nconst input = []\nlet inputBytes = 0\n\nfunction encodeControl(value) {\n const body = Buffer.from(JSON.stringify(value) + '\\n', 'utf8')\n if (body.length > MAX_CONTROL) throw new Error('control frame too large')\n const header = Buffer.alloc(5)\n header[0] = 0\n header.writeUInt32BE(body.length, 1)\n return Buffer.concat([header, body])\n}\n\nfunction encodeData(sequence, data) {\n const digest = createHash('sha256').update(data).digest()\n const metadata = Buffer.alloc(36)\n metadata.writeUInt32BE(sequence, 0)\n digest.copy(metadata, 4)\n const payload = Buffer.concat([metadata, data])\n const header = Buffer.alloc(5)\n header[0] = 1\n header.writeUInt32BE(payload.length, 1)\n return Buffer.concat([header, payload])\n}\n\nfunction decodeFrames(buffer) {\n const frames = []\n let offset = 0\n let sequence = 0\n let totalData = 0\n while (offset < buffer.length) {\n if (buffer.length - offset < 5) throw new Error('truncated frame header')\n const kind = buffer[offset]\n const length = buffer.readUInt32BE(offset + 1)\n offset += 5\n const maxLength = kind === 0 ? MAX_CONTROL : 36 + MAX_CHUNK\n if ((kind !== 0 && kind !== 1) || length > maxLength || length > buffer.length - offset) {\n throw new Error('invalid frame length')\n }\n const payload = buffer.subarray(offset, offset + length)\n offset += length\n if (kind === 0) {\n if (length < 2 || payload[length - 1] !== 10) throw new Error('invalid control frame')\n frames.push({ kind: 'control', value: JSON.parse(payload.subarray(0, -1).toString('utf8')) })\n } else {\n if (length < 36) throw new Error('invalid data frame')\n const currentSequence = payload.readUInt32BE(0)\n const expected = payload.subarray(4, 36)\n const data = payload.subarray(36)\n const actual = createHash('sha256').update(data).digest()\n if (currentSequence !== sequence || !timingSafeEqual(expected, actual)) throw new Error('invalid data chunk')\n sequence += 1\n totalData += data.length\n if (totalData > MAX_TRANSFER) throw new Error('transfer exceeds size limit')\n frames.push({ kind: 'data', data })\n }\n }\n return frames\n}\n\nfunction parseRequest(buffer) {\n const frames = decodeFrames(buffer)\n const first = frames[0]\n if (!first || first.kind !== 'control') throw new Error('request control frame required')\n const request = first.value\n if (!request || request.kind !== 'request' || request.protocolVersion !== 1 || !Object.prototype.hasOwnProperty.call(PATHS, request.operation)) {\n throw new Error('unsupported helper request')\n }\n if (Object.keys(request).some((key) => !['kind', 'protocolVersion', 'operation', 'port'].includes(key))) {\n throw new Error('unsupported helper request field')\n }\n if (!Number.isInteger(request.port) || request.port < 1 || request.port > 65535) throw new Error('invalid loopback port')\n const operation = PATHS[request.operation]\n if (request.operation === 'node-apply' || request.operation === 'session-apply') {\n const last = frames[frames.length - 1]\n if (!last || last.kind !== 'control' || last.value.kind !== 'request-end') throw new Error('apply request terminator required')\n if (frames.slice(1, -1).some((frame) => frame.kind !== 'data')) throw new Error('invalid apply request body')\n const chunks = frames.slice(1, -1).map((frame) => frame.data)\n return { request, operation, body: Buffer.concat(chunks) }\n }\n if (frames.length !== 1) throw new Error('unexpected helper request frames')\n return { request, operation }\n}\n\nasync function run() {\n const buffer = Buffer.concat(input, inputBytes)\n const { request, operation, body } = parseRequest(buffer)\n const url = 'http://127.0.0.1:' + request.port + operation.path\n const response = await fetch(url, {\n method: operation.method,\n redirect: 'error',\n headers: { accept: 'application/json', ...(body ? { 'content-type': 'application/json' } : {}) },\n ...(body ? { body } : {}),\n })\n\n function writeStdout(data) {\n return new Promise(function(resolve, reject) {\n if (process.stdout.write(data)) resolve()\n else process.stdout.once('drain', resolve)\n })\n }\n\n if (request.operation === 'node-manifest' || request.operation === 'session-bundle') {\n await writeStdout(encodeControl({ kind: 'response', status: response.status, bodyMode: 'data' }))\n if (!response.body) throw new Error('manifest response body missing')\n var sequence = 0\n var total = 0\n for await (var chunk of response.body) {\n var data = Buffer.from(chunk)\n total += data.length\n if (total > MAX_TRANSFER) throw new Error('manifest exceeds size limit')\n for (var offset = 0; offset < data.length; offset += MAX_CHUNK) {\n await writeStdout(encodeData(sequence++, data.subarray(offset, Math.min(offset + MAX_CHUNK, data.length))))\n }\n }\n await writeStdout(encodeControl({ kind: 'response-end' }))\n if (!response.ok) process.exitCode = 1\n return\n }\n\n var text = await response.text()\n if (Buffer.byteLength(text, 'utf8') > MAX_CONTROL) throw new Error('response control frame too large')\n await writeStdout(encodeControl({ kind: 'response', status: response.status, body: JSON.parse(text) }))\n if (!response.ok) process.exitCode = 1\n}\n\nprocess.stdin.on('data', (chunk) => {\n inputBytes += chunk.length\n if (inputBytes > MAX_TRANSFER + 2 * MAX_CONTROL) {\n process.exitCode = 1\n process.stdin.destroy()\n return\n }\n input.push(Buffer.from(chunk))\n})\nprocess.stdin.on('end', () => {\n run().catch(() => {\n try { process.stdout.write(encodeControl({ kind: 'error', code: 'helper-failed' })) } catch {}\n process.exitCode = 1\n })\n})\n`\n\nconst REMOTE_HELPER_BASE64 = Buffer.from(REMOTE_HELPER_SOURCE, 'utf8').toString('base64')\n\nexport const REMOTE_SYNC_HELPER_COMMAND =\n `node -e 'eval(Buffer.from(\"${REMOTE_HELPER_BASE64}\",\"base64\").toString(\"utf8\"))'`\n"],"mappings":";;AAEA,MAAM,uBAAuB,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4JvC,MAAa,6BACX,8BAH2B,OAAO,KAAK,sBAAsB,MAAM,CAAC,CAAC,SAAS,QAGhD,EAAqB"}
@@ -0,0 +1,2 @@
1
+ import { i as compareSyncRevision, n as HybridLogicalClock, r as SyncRevision, t as HlcSnapshot } from "../../revision-C9sOlDZ6.js";
2
+ export { HlcSnapshot, HybridLogicalClock, SyncRevision, compareSyncRevision };