amalgm 0.1.245 → 0.1.246

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 (157) hide show
  1. package/README.md +12 -0
  2. package/lib/cli.js +146 -8
  3. package/lib/layout.js +89 -4
  4. package/lib/shared-realtime-tunnel.js +384 -43
  5. package/lib/supervisor.js +0 -3
  6. package/package.json +2 -2
  7. package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
  8. package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
  9. package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
  10. package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
  11. package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
  12. package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
  13. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
  14. package/runtime/scripts/amalgm-mcp/index.js +4 -0
  15. package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
  16. package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
  17. package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
  18. package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
  19. package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
  20. package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
  21. package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
  22. package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
  23. package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
  24. package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
  25. package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
  26. package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
  27. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
  28. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
  29. package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
  30. package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
  31. package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
  32. package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
  33. package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
  34. package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
  35. package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
  36. package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
  37. package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
  38. package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
  39. package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
  40. package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
  41. package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
  42. package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
  43. package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
  44. package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
  45. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  46. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
  47. package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
  48. package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
  49. package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
  50. package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
  51. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
  52. package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
  53. package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
  54. package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
  55. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
  56. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
  57. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
  58. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
  59. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
  60. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
  61. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
  62. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
  63. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
  64. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
  65. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
  66. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
  67. package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
  68. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
  69. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
  70. package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
  71. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
  72. package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
  73. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
  74. package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
  75. package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
  76. package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
  77. package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
  78. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
  79. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
  80. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
  81. package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
  82. package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
  83. package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
  84. package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
  85. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
  86. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
  87. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
  88. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
  89. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
  90. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
  91. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
  92. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
  93. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
  94. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
  95. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
  96. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
  97. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
  98. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
  99. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
  100. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
  101. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
  102. package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
  103. package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
  104. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
  105. package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
  106. package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
  107. package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
  108. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
  109. package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
  110. package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
  111. package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
  112. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
  113. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
  114. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
  115. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
  116. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
  117. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
  118. package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
  119. package/runtime/scripts/local-gateway.js +1 -0
  120. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
  121. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
  122. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
  123. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
  124. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
  125. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
  126. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
  127. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
  128. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
  129. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
  130. package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
  131. package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
  132. package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
  133. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
  134. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  135. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  136. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
  137. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
  138. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
  139. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
  140. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
  141. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
  142. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
  143. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
  144. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
  145. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
  146. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
  147. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
  148. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
  149. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
  150. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
  151. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
  152. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
  153. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
  154. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
  155. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
  156. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
  157. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
@@ -0,0 +1,287 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The local entity API — the runtime half of `amalgm register`. Authenticated
5
+ * by the same boundary as every other local route (server/http-service.js
6
+ * authorizes before dispatch), so this file is only the shape of the
7
+ * request and the shape of the failure.
8
+ *
9
+ * Two failures, two meanings, and the boundary itself says which:
10
+ *
11
+ * 400 a REFUSAL — the request named ground amalgm will not take
12
+ * (registration/refusal.js). The message IS the reason and is
13
+ * meant to be shown to whoever asked.
14
+ * --- anything else is a FAULT: a torn identity store, an unusable
15
+ * database, a bug. The request was fine; the installation is
16
+ * not. It travels on to the server's fault handler, which is
17
+ * what already reports every other route's faults.
18
+ *
19
+ * The split is never made by reading an error's wording. A message is
20
+ * for people; it changes. Deciding on it would mean that the day the
21
+ * identity store tears, the user is told their path is bad.
22
+ */
23
+
24
+ const crypto = require('crypto');
25
+
26
+ const {
27
+ USER_HOME_CLOUD_CONTRACT,
28
+ USER_HOME_CLOUD_SCHEMA_VERSION,
29
+ registrationService,
30
+ } = require('../../registration/service');
31
+ const { isRefusal } = require('../../registration/refusal');
32
+
33
+ function privateUserHomeResourceId(userId) {
34
+ return `pvr_${crypto
35
+ .createHash('sha256')
36
+ .update(`private-user:${userId}:${USER_HOME_CLOUD_CONTRACT}`)
37
+ .digest('base64url')
38
+ .slice(0, 32)}`;
39
+ }
40
+
41
+ function userHomeCloudIdentity() {
42
+ const { userId } = require('../../workspace/identity');
43
+ const ownerUserId = userId();
44
+ if (!ownerUserId) return { enabled: false, reason: 'runtime is not enrolled to a user' };
45
+ return {
46
+ enabled: true,
47
+ resourceId: privateUserHomeResourceId(ownerUserId),
48
+ contract: USER_HOME_CLOUD_CONTRACT,
49
+ schemaVersion: USER_HOME_CLOUD_SCHEMA_VERSION,
50
+ };
51
+ }
52
+
53
+ function snapshotBytes(value) {
54
+ const bytes = Buffer.from(JSON.stringify(value), 'utf8');
55
+ return {
56
+ snapshotBase64: bytes.toString('base64'),
57
+ snapshotBytes: bytes.length,
58
+ snapshotChecksum: crypto.createHash('sha256').update(bytes).digest('hex'),
59
+ };
60
+ }
61
+
62
+ async function handleEntityRoutes(ctx) {
63
+ if (ctx.pathname === '/entities/user-home/cloud/identity' && ctx.method === 'GET') {
64
+ ctx.res.setHeader('Cache-Control', 'no-store');
65
+ ctx.sendJson(200, userHomeCloudIdentity());
66
+ return true;
67
+ }
68
+
69
+ if (ctx.pathname === '/entities/user-home/cloud/bootstrap' && ctx.method === 'GET') {
70
+ ctx.res.setHeader('Cache-Control', 'no-store');
71
+ const identity = userHomeCloudIdentity();
72
+ if (!identity.enabled) {
73
+ ctx.sendJson(200, identity);
74
+ return true;
75
+ }
76
+ const home = await registrationService().bootstrapUserHome();
77
+ ctx.sendJson(200, { ...identity, ...snapshotBytes(home.cloudSnapshot) });
78
+ return true;
79
+ }
80
+
81
+ if (ctx.pathname === '/entities/user-home/cloud/install' && ctx.method === 'POST') {
82
+ ctx.res.setHeader('Cache-Control', 'no-store');
83
+ const identity = userHomeCloudIdentity();
84
+ if (!identity.enabled) {
85
+ ctx.sendJson(409, identity);
86
+ return true;
87
+ }
88
+ const body = await ctx.readJsonBody();
89
+ if (
90
+ body?.resourceId !== identity.resourceId
91
+ || body?.contract !== identity.contract
92
+ || Number(body?.schemaVersion) !== identity.schemaVersion
93
+ ) {
94
+ ctx.sendJson(400, { error: 'cloud user-home identity does not belong to this runtime user' });
95
+ return true;
96
+ }
97
+ const bytes = Buffer.from(String(body.snapshotBase64 || ''), 'base64');
98
+ const checksum = crypto.createHash('sha256').update(bytes).digest('hex');
99
+ if (checksum !== body.snapshotChecksum) {
100
+ ctx.sendJson(400, { error: 'cloud user-home snapshot checksum mismatch' });
101
+ return true;
102
+ }
103
+ let snapshot;
104
+ try {
105
+ snapshot = JSON.parse(bytes.toString('utf8'));
106
+ } catch {
107
+ ctx.sendJson(400, { error: 'cloud user-home snapshot is not JSON' });
108
+ return true;
109
+ }
110
+ const home = await registrationService().bootstrapUserHome({ identitySnapshot: snapshot });
111
+ ctx.sendJson(200, {
112
+ resourceId: identity.resourceId,
113
+ roots: home.roots.map(({ key, record }) => ({ key, uuid: record.uuid })),
114
+ exactManaged: home.exactManaged.map(({ key, record }) => ({ key, uuid: record.uuid })),
115
+ });
116
+ return true;
117
+ }
118
+
119
+ // The universal entity graph has its own private resource. It is kept
120
+ // separate from the user-home declaration: the latter establishes the
121
+ // fixed safe roots and exact governance; this resource carries every
122
+ // logical entity record and its durable mutation tail.
123
+ if (ctx.pathname === '/entities/cloud/identity' && ctx.method === 'GET') {
124
+ ctx.res.setHeader('Cache-Control', 'no-store');
125
+ ctx.sendJson(200, registrationService().entityCloud.identity());
126
+ return true;
127
+ }
128
+
129
+ if (ctx.pathname === '/entities/cloud/bootstrap' && ctx.method === 'GET') {
130
+ ctx.res.setHeader('Cache-Control', 'no-store');
131
+ const service = registrationService();
132
+ const identity = service.entityCloud.identity();
133
+ if (!identity.enabled) {
134
+ ctx.sendJson(200, identity);
135
+ return true;
136
+ }
137
+ // A missing entity resource is first created from the same declared
138
+ // user-home graph that login owns. This is not an alternate registrar.
139
+ await service.bootstrapUserHome();
140
+ ctx.sendJson(200, service.entityCloud.bootstrap(service.registration.registry));
141
+ return true;
142
+ }
143
+
144
+ if (ctx.pathname === '/entities/cloud/install' && ctx.method === 'POST') {
145
+ ctx.res.setHeader('Cache-Control', 'no-store');
146
+ const body = await ctx.readJsonBody();
147
+ const service = registrationService();
148
+ try {
149
+ ctx.sendJson(200, service.entityCloud.install({
150
+ registry: service.registration.registry,
151
+ resourceId: body?.resourceId,
152
+ authorityEpoch: body?.authorityEpoch,
153
+ headVersion: body?.headVersion,
154
+ snapshotBase64: body?.snapshotBase64,
155
+ snapshotChecksum: body?.snapshotChecksum,
156
+ }));
157
+ } catch (error) {
158
+ ctx.sendJson(409, { error: error.message });
159
+ }
160
+ return true;
161
+ }
162
+
163
+ if (ctx.pathname === '/entities/cloud/mutations/claim' && ctx.method === 'POST') {
164
+ const body = await ctx.readJsonBody();
165
+ ctx.sendJson(200, {
166
+ mutations: registrationService().entityCloud.claim({
167
+ limit: body?.limit,
168
+ leaseMs: body?.leaseMs,
169
+ }),
170
+ });
171
+ return true;
172
+ }
173
+
174
+ // Content objects are a separate ordered rail: a file record is not
175
+ // submitted until this endpoint has received the immutable object manifest
176
+ // acknowledgement for its payload head.
177
+ if (ctx.pathname === '/entities/cloud/content/claim' && ctx.method === 'POST') {
178
+ const body = await ctx.readJsonBody();
179
+ ctx.sendJson(200, {
180
+ upload: registrationService().entityCloud.claimContent({ leaseMs: body?.leaseMs }),
181
+ });
182
+ return true;
183
+ }
184
+
185
+ if (ctx.pathname === '/entities/cloud/content/ack' && ctx.method === 'POST') {
186
+ const body = await ctx.readJsonBody();
187
+ try {
188
+ ctx.sendJson(200, registrationService().entityCloud.acknowledgeContent({
189
+ resourceId: body?.resourceId,
190
+ contentHash: body?.contentHash,
191
+ kind: body?.kind,
192
+ partIndex: body?.partIndex,
193
+ }));
194
+ } catch (error) {
195
+ ctx.sendJson(400, { error: error.message });
196
+ }
197
+ return true;
198
+ }
199
+
200
+ if (ctx.pathname === '/entities/cloud/content/fail' && ctx.method === 'POST') {
201
+ const body = await ctx.readJsonBody();
202
+ try {
203
+ ctx.sendJson(200, registrationService().entityCloud.failContent({
204
+ resourceId: body?.resourceId,
205
+ contentHash: body?.contentHash,
206
+ message: body?.message,
207
+ }));
208
+ } catch (error) {
209
+ ctx.sendJson(400, { error: error.message });
210
+ }
211
+ return true;
212
+ }
213
+
214
+ if (ctx.pathname === '/entities/cloud/mutations/ack' && ctx.method === 'POST') {
215
+ const body = await ctx.readJsonBody();
216
+ try {
217
+ ctx.sendJson(200, registrationService().entityCloud.acknowledge({
218
+ resourceId: body?.resourceId,
219
+ mutationId: body?.mutationId,
220
+ version: body?.version,
221
+ committedAt: body?.committedAt,
222
+ }));
223
+ } catch (error) {
224
+ ctx.sendJson(400, { error: error.message });
225
+ }
226
+ return true;
227
+ }
228
+
229
+ if (ctx.pathname === '/entities/cloud/mutations/fail' && ctx.method === 'POST') {
230
+ const body = await ctx.readJsonBody();
231
+ try {
232
+ ctx.sendJson(200, registrationService().entityCloud.fail({
233
+ resourceId: body?.resourceId,
234
+ mutationId: body?.mutationId,
235
+ message: body?.message,
236
+ retryable: body?.retryable,
237
+ }));
238
+ } catch (error) {
239
+ ctx.sendJson(400, { error: error.message });
240
+ }
241
+ return true;
242
+ }
243
+
244
+ if (ctx.pathname === '/entities/cloud/mutations/ingest' && ctx.method === 'POST') {
245
+ const body = await ctx.readJsonBody();
246
+ const service = registrationService();
247
+ try {
248
+ ctx.sendJson(200, service.entityCloud.ingest({
249
+ registry: service.registration.registry,
250
+ envelope: body?.envelope,
251
+ version: body?.version,
252
+ committedAt: body?.committedAt,
253
+ }));
254
+ } catch (error) {
255
+ ctx.sendJson(409, { error: error.message });
256
+ }
257
+ return true;
258
+ }
259
+
260
+ if (ctx.pathname !== '/entities/register' || ctx.method !== 'POST') return false;
261
+
262
+ const body = await ctx.readJsonBody();
263
+ if (body?.path !== undefined && body?.paths !== undefined) {
264
+ ctx.sendJson(400, { error: 'send either path or paths, not both' });
265
+ return true;
266
+ }
267
+ const targets = Array.isArray(body?.paths)
268
+ ? body.paths.map((target) => typeof target === 'string' ? target.trim() : '')
269
+ : [typeof body?.path === 'string' ? body.path.trim() : ''];
270
+ if (targets.length === 0 || targets.some((target) => !target)) {
271
+ ctx.sendJson(400, { error: 'path or paths is required: one or more absolute paths to register' });
272
+ return true;
273
+ }
274
+
275
+ try {
276
+ const results = await registrationService().registerMany(targets);
277
+ // Preserve the one-path response while exposing the full batch result to
278
+ // the CLI and any future SDK. One registration remains a degenerate batch.
279
+ ctx.sendJson(200, results.length === 1 ? { ...results[0], results } : { results });
280
+ } catch (error) {
281
+ if (!isRefusal(error)) throw error;
282
+ ctx.sendJson(400, { error: error.message });
283
+ }
284
+ return true;
285
+ }
286
+
287
+ module.exports = { handleEntityRoutes, privateUserHomeResourceId, userHomeCloudIdentity };
@@ -59,19 +59,6 @@ async function handleStateRoutes(ctx) {
59
59
  ));
60
60
  return true;
61
61
  }
62
- if (ctx.pathname === '/state/repo-objects/uploads/fail') {
63
- const body = await ctx.readJsonBody();
64
- ctx.sendJson(200, require('../../workspace/objects').failUpload(
65
- body?.resourceId,
66
- body?.sha256,
67
- {
68
- retryable: body?.retryable,
69
- code: body?.code,
70
- message: body?.message,
71
- },
72
- ));
73
- return true;
74
- }
75
62
  if (ctx.pathname === '/state/repo-objects/downloads/claim') {
76
63
  ctx.sendJson(200, { download: require('../../workspace/objects').claimDownload() });
77
64
  return true;
@@ -4,7 +4,7 @@ const crypto = require('crypto');
4
4
 
5
5
  const { cachedStatement, openLocalDb } = require('./db');
6
6
  const { isSharedResourceId } = require('./cloud-resource-id');
7
- const { appendStateEvent, insertStateEvent } = require('./events');
7
+ const { appendStateEvent } = require('./events');
8
8
 
9
9
  const ATTACHMENT_ID = /^[A-Za-z0-9_-]{16,160}$/;
10
10
 
@@ -30,10 +30,6 @@ function normalizeRow(row) {
30
30
  attemptCount: Number(row.attempt_count),
31
31
  nextAttemptAt: row.next_attempt_at || null,
32
32
  lastError: row.last_error || null,
33
- // Legacy requests predate the fence. Their target was required to be
34
- // empty, so zero is the only conservative boundary: every surviving or
35
- // newly captured local mutation remains overlay.
36
- journalCursor: row.journal_cursor == null ? 0 : Number(row.journal_cursor),
37
33
  createdAt: row.created_at,
38
34
  updatedAt: row.updated_at,
39
35
  };
@@ -58,16 +54,12 @@ function enqueueAttachment(input, options = {}) {
58
54
  return { attachment, duplicate: true };
59
55
  }
60
56
  const now = new Date().toISOString();
61
- const journalCursor = Number(
62
- cachedStatement(database, 'SELECT COALESCE(MAX(journal_id), 0) AS cursor FROM mutation_journal')
63
- .get().cursor,
64
- );
65
57
  try {
66
58
  cachedStatement(database, `
67
59
  INSERT INTO shared_resource_attachments (
68
- attachment_id, resource_id, local_path, journal_cursor, created_at, updated_at
69
- ) VALUES (?, ?, ?, ?, ?, ?)
70
- `).run(attachmentId, resourceId, localPath, journalCursor, now, now);
60
+ attachment_id, resource_id, local_path, created_at, updated_at
61
+ ) VALUES (?, ?, ?, ?, ?)
62
+ `).run(attachmentId, resourceId, localPath, now, now);
71
63
  } catch (error) {
72
64
  if (String(error?.code || '').includes('CONSTRAINT')) {
73
65
  throw invalid('that resource or local path already has an attachment request', 409);
@@ -78,22 +70,14 @@ function enqueueAttachment(input, options = {}) {
78
70
  cachedStatement(database, 'SELECT * FROM shared_resource_attachments WHERE attachment_id = ?')
79
71
  .get(attachmentId),
80
72
  );
81
- const eventInput = {
73
+ appendStateEvent({
82
74
  resource: 'shared:attachments',
83
75
  op: 'update',
84
76
  id: attachmentId,
85
77
  sharedResourceId: resourceId,
86
78
  value: { state: 'pending' },
87
79
  source: 'shared:attachment',
88
- };
89
- if (options.deferEvent === true) {
90
- return {
91
- attachment,
92
- duplicate: false,
93
- deferredEvent: insertStateEvent(database, eventInput),
94
- };
95
- }
96
- appendStateEvent(eventInput);
80
+ });
97
81
  return { attachment, duplicate: false };
98
82
  }
99
83
 
@@ -145,16 +129,7 @@ function acknowledgeAttachment(attachmentIdInput, payload, options = {}) {
145
129
 
146
130
  const installed = payload?.contract === 'workspace-tree@1'
147
131
  ? require('../workspace/tree-store').installCloudTreeSnapshot(attachment.localPath, payload, { database })
148
- : payload?.contract === 'code-project-stream@2'
149
- ? require('../workspace/project-stream').installSnapshot(
150
- attachment.localPath,
151
- payload,
152
- { database },
153
- )
154
- : require('./docs').installCloudDocSnapshot(attachment.localPath, {
155
- ...payload,
156
- attachmentJournalCursor: attachment.journalCursor,
157
- });
132
+ : require('./docs').installCloudDocSnapshot(attachment.localPath, payload);
158
133
  const now = new Date().toISOString();
159
134
  cachedStatement(database, `
160
135
  UPDATE shared_resource_attachments
@@ -219,30 +194,21 @@ function requestRehydration(input, options = {}) {
219
194
  let attachmentId;
220
195
  if (existing) {
221
196
  attachmentId = existing.attachment_id;
222
- const journalCursor = Number(
223
- cachedStatement(database, 'SELECT COALESCE(MAX(journal_id), 0) AS cursor FROM mutation_journal')
224
- .get().cursor,
225
- );
226
197
  cachedStatement(database, `
227
198
  UPDATE shared_resource_attachments
228
199
  SET attachment_state = 'pending',
229
200
  next_attempt_at = NULL,
230
201
  last_error = NULL,
231
- journal_cursor = ?,
232
202
  updated_at = ?
233
203
  WHERE attachment_id = ?
234
- `).run(journalCursor, now, attachmentId);
204
+ `).run(now, attachmentId);
235
205
  } else {
236
206
  attachmentId = `att_${crypto.randomBytes(16).toString('base64url')}`;
237
- const journalCursor = Number(
238
- cachedStatement(database, 'SELECT COALESCE(MAX(journal_id), 0) AS cursor FROM mutation_journal')
239
- .get().cursor,
240
- );
241
207
  cachedStatement(database, `
242
208
  INSERT INTO shared_resource_attachments (
243
- attachment_id, resource_id, local_path, journal_cursor, created_at, updated_at
244
- ) VALUES (?, ?, ?, ?, ?, ?)
245
- `).run(attachmentId, resourceId, replica.localPath, journalCursor, now, now);
209
+ attachment_id, resource_id, local_path, created_at, updated_at
210
+ ) VALUES (?, ?, ?, ?, ?)
211
+ `).run(attachmentId, resourceId, replica.localPath, now, now);
246
212
  }
247
213
  const attachment = normalizeRow(
248
214
  cachedStatement(database, 'SELECT * FROM shared_resource_attachments WHERE attachment_id = ?')