amalgm 0.1.155 → 0.1.156

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.
@@ -0,0 +1,271 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const path = require('path');
5
+ const { cachedStatement, openLocalDb } = require('./db');
6
+ const { appendStateEvent } = require('./events');
7
+ const { registerReplica } = require('./replicas');
8
+
9
+ const SHARED_RESOURCE_ID = /^shr_[A-Za-z0-9_-]{16,128}$/;
10
+ const PROMOTION_ID = /^[A-Za-z0-9_-]{16,160}$/;
11
+
12
+ function invalid(message, statusCode = 400) {
13
+ return Object.assign(new Error(message), { statusCode });
14
+ }
15
+
16
+ function requiredText(value, name, max = 1024) {
17
+ const text = typeof value === 'string' ? value.trim() : '';
18
+ if (!text || text.length > max || /[\u0000-\u001f\u007f]/.test(text)) {
19
+ throw invalid(`${name} must be a non-empty string of at most ${max} characters`);
20
+ }
21
+ return text;
22
+ }
23
+
24
+ function normalizeRow(row) {
25
+ if (!row) return null;
26
+ return {
27
+ promotionId: row.promotion_id,
28
+ resourceId: row.resource_id,
29
+ localResource: row.local_resource,
30
+ localPath: row.local_path,
31
+ contract: row.contract,
32
+ schemaVersion: Number(row.schema_version),
33
+ snapshotBase64: row.snapshot_b64,
34
+ snapshotChecksum: row.snapshot_checksum,
35
+ snapshotBytes: Number(row.snapshot_bytes),
36
+ contentBytes: row.content_bytes == null ? null : Number(row.content_bytes),
37
+ name: row.name || null,
38
+ mediaType: row.media_type || null,
39
+ state: row.promotion_state,
40
+ attemptCount: Number(row.attempt_count),
41
+ nextAttemptAt: row.next_attempt_at || null,
42
+ lastError: row.last_error || null,
43
+ snapshotKey: row.snapshot_key || null,
44
+ createdAt: row.created_at,
45
+ updatedAt: row.updated_at,
46
+ };
47
+ }
48
+
49
+ function enqueuePromotion(input, options = {}) {
50
+ const promotionId = requiredText(input?.promotionId, 'promotionId', 160);
51
+ const resourceId = requiredText(input?.resourceId, 'resourceId', 132);
52
+ if (!PROMOTION_ID.test(promotionId)) throw invalid('promotionId has an invalid format');
53
+ if (!SHARED_RESOURCE_ID.test(resourceId)) throw invalid('resourceId has an invalid format');
54
+ const localResource = requiredText(input?.localResource, 'localResource');
55
+ const localPath = requiredText(input?.localPath, 'localPath', 4096);
56
+ const contract = requiredText(input?.contract || 'text-yjs@1', 'contract', 128);
57
+ const schemaVersion = Number(input?.schemaVersion || 1);
58
+ if (!Number.isSafeInteger(schemaVersion) || schemaVersion < 1) {
59
+ throw invalid('schemaVersion must be a positive integer');
60
+ }
61
+ const snapshotBase64 = requiredText(input?.snapshotBase64, 'snapshotBase64', 8 * 1024 * 1024);
62
+ const snapshot = Buffer.from(snapshotBase64, 'base64');
63
+ const snapshotChecksum = crypto.createHash('sha256').update(snapshot).digest('hex');
64
+ const contentBytes = input?.contentBytes == null ? null : Number(input.contentBytes);
65
+ if (contentBytes != null && (!Number.isSafeInteger(contentBytes) || contentBytes < 0)) {
66
+ throw invalid('contentBytes must be a non-negative integer');
67
+ }
68
+ const database = options.database || openLocalDb();
69
+ const existing = cachedStatement(
70
+ database,
71
+ 'SELECT * FROM shared_resource_promotions WHERE promotion_id = ?',
72
+ ).get(promotionId);
73
+ if (existing) {
74
+ const row = normalizeRow(existing);
75
+ if (
76
+ row.resourceId !== resourceId
77
+ || row.localResource !== localResource
78
+ || row.snapshotChecksum !== snapshotChecksum
79
+ ) {
80
+ throw invalid(`promotionId ${promotionId} was reused with different content`, 409);
81
+ }
82
+ return { promotion: row, duplicate: true };
83
+ }
84
+
85
+ const now = new Date().toISOString();
86
+ cachedStatement(database, `
87
+ INSERT INTO shared_resource_promotions (
88
+ promotion_id,
89
+ resource_id,
90
+ local_resource,
91
+ local_path,
92
+ contract,
93
+ schema_version,
94
+ snapshot_b64,
95
+ snapshot_checksum,
96
+ snapshot_bytes,
97
+ content_bytes,
98
+ name,
99
+ media_type,
100
+ created_at,
101
+ updated_at
102
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
103
+ `).run(
104
+ promotionId,
105
+ resourceId,
106
+ localResource,
107
+ localPath,
108
+ contract,
109
+ schemaVersion,
110
+ snapshotBase64,
111
+ snapshotChecksum,
112
+ snapshot.length,
113
+ contentBytes,
114
+ input?.name || path.basename(localPath),
115
+ input?.mediaType || 'application/vnd.amalgm.text-yjs-snapshot',
116
+ now,
117
+ now,
118
+ );
119
+ const promotion = normalizeRow(
120
+ cachedStatement(database, 'SELECT * FROM shared_resource_promotions WHERE promotion_id = ?')
121
+ .get(promotionId),
122
+ );
123
+
124
+ // Wake the already-open internal Local Live stream used by the tunnel
125
+ // process. This is a control event, not a cloud mutation/version.
126
+ appendStateEvent({
127
+ resource: 'shared:promotions',
128
+ op: 'update',
129
+ id: promotionId,
130
+ sharedResourceId: resourceId,
131
+ value: { state: 'pending' },
132
+ source: 'shared:promotion',
133
+ });
134
+ return { promotion, duplicate: false };
135
+ }
136
+
137
+ function claimPromotions(options = {}) {
138
+ const database = options.database || openLocalDb();
139
+ const limit = Math.max(1, Math.min(Number(options.limit) || 4, 20));
140
+ const leaseMs = Math.max(1000, Number(options.leaseMs) || 30_000);
141
+ const now = new Date().toISOString();
142
+ const leaseUntil = new Date(Date.now() + leaseMs).toISOString();
143
+ return database.transaction(() => {
144
+ const rows = cachedStatement(database, `
145
+ SELECT * FROM shared_resource_promotions
146
+ WHERE promotion_state = 'pending'
147
+ OR (promotion_state = 'syncing' AND (next_attempt_at IS NULL OR next_attempt_at <= ?))
148
+ ORDER BY created_at ASC
149
+ LIMIT ?
150
+ `).all(now, limit);
151
+ const update = cachedStatement(database, `
152
+ UPDATE shared_resource_promotions
153
+ SET promotion_state = 'syncing',
154
+ attempt_count = attempt_count + 1,
155
+ next_attempt_at = ?,
156
+ updated_at = ?
157
+ WHERE promotion_id = ?
158
+ `);
159
+ return rows.map((row) => {
160
+ update.run(leaseUntil, now, row.promotion_id);
161
+ const promotion = normalizeRow(
162
+ cachedStatement(database, 'SELECT * FROM shared_resource_promotions WHERE promotion_id = ?')
163
+ .get(row.promotion_id),
164
+ );
165
+ return {
166
+ promotion,
167
+ frame: {
168
+ promotionId: promotion.promotionId,
169
+ resourceId: promotion.resourceId,
170
+ contract: promotion.contract,
171
+ schemaVersion: promotion.schemaVersion,
172
+ snapshotBase64: promotion.snapshotBase64,
173
+ snapshotChecksum: promotion.snapshotChecksum,
174
+ snapshotBytes: promotion.snapshotBytes,
175
+ contentBytes: promotion.contentBytes,
176
+ name: promotion.name,
177
+ mediaType: promotion.mediaType,
178
+ },
179
+ };
180
+ });
181
+ })();
182
+ }
183
+
184
+ function acknowledgePromotion(promotionIdInput, acknowledgement, options = {}) {
185
+ const promotionId = requiredText(promotionIdInput, 'promotionId', 160);
186
+ const database = options.database || openLocalDb();
187
+ return database.transaction(() => {
188
+ const row = cachedStatement(
189
+ database,
190
+ 'SELECT * FROM shared_resource_promotions WHERE promotion_id = ?',
191
+ ).get(promotionId);
192
+ if (!row) throw invalid(`unknown promotion ${promotionId}`, 404);
193
+ const promotion = normalizeRow(row);
194
+ if (acknowledgement?.resourceId !== promotion.resourceId) {
195
+ throw invalid('promotion acknowledgement resource mismatch', 409);
196
+ }
197
+ if (promotion.state === 'promoted') return { promotion, duplicate: true };
198
+
199
+ registerReplica({
200
+ resourceId: promotion.resourceId,
201
+ localResource: promotion.localResource,
202
+ localPath: promotion.localPath,
203
+ contract: promotion.contract,
204
+ schemaVersion: promotion.schemaVersion,
205
+ authorityEpoch: acknowledgement?.authorityEpoch,
206
+ appliedVersion: acknowledgement?.headVersion ?? 0,
207
+ snapshotVersion: acknowledgement?.snapshotVersion ?? 0,
208
+ snapshotChecksum: acknowledgement?.snapshotChecksum,
209
+ }, { database });
210
+
211
+ const now = new Date().toISOString();
212
+ cachedStatement(database, `
213
+ UPDATE shared_resource_promotions
214
+ SET promotion_state = 'promoted',
215
+ next_attempt_at = NULL,
216
+ last_error = NULL,
217
+ snapshot_key = ?,
218
+ updated_at = ?
219
+ WHERE promotion_id = ?
220
+ `).run(acknowledgement?.snapshotKey || null, now, promotionId);
221
+ return {
222
+ promotion: normalizeRow(
223
+ cachedStatement(database, 'SELECT * FROM shared_resource_promotions WHERE promotion_id = ?')
224
+ .get(promotionId),
225
+ ),
226
+ duplicate: false,
227
+ };
228
+ })();
229
+ }
230
+
231
+ function failPromotion(promotionIdInput, failure, options = {}) {
232
+ const promotionId = requiredText(promotionIdInput, 'promotionId', 160);
233
+ const database = options.database || openLocalDb();
234
+ const retryable = failure?.retryable !== false;
235
+ const now = new Date().toISOString();
236
+ const result = cachedStatement(database, `
237
+ UPDATE shared_resource_promotions
238
+ SET promotion_state = ?,
239
+ next_attempt_at = ?,
240
+ last_error = ?,
241
+ updated_at = ?
242
+ WHERE promotion_id = ? AND promotion_state != 'promoted'
243
+ `).run(
244
+ retryable ? 'syncing' : 'rejected',
245
+ retryable ? new Date(Date.now() + 1000).toISOString() : null,
246
+ String(failure?.message || 'promotion failed').slice(0, 4000),
247
+ now,
248
+ promotionId,
249
+ );
250
+ if (!result.changes) throw invalid(`unknown or completed promotion ${promotionId}`, 404);
251
+ return normalizeRow(
252
+ cachedStatement(database, 'SELECT * FROM shared_resource_promotions WHERE promotion_id = ?')
253
+ .get(promotionId),
254
+ );
255
+ }
256
+
257
+ function getPromotion(promotionId, options = {}) {
258
+ const database = options.database || openLocalDb();
259
+ return normalizeRow(
260
+ cachedStatement(database, 'SELECT * FROM shared_resource_promotions WHERE promotion_id = ?')
261
+ .get(promotionId),
262
+ );
263
+ }
264
+
265
+ module.exports = {
266
+ acknowledgePromotion,
267
+ claimPromotions,
268
+ enqueuePromotion,
269
+ failPromotion,
270
+ getPromotion,
271
+ };
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Local bindings for cloud-authoritative resources.
5
+ *
6
+ * The binding is the answer to "is this edit cloud-backed?". That decision
7
+ * is owned by the runtime and persisted in SQLite; it is never trusted from
8
+ * a browser mutation payload.
9
+ */
10
+
11
+ const { cachedStatement, openLocalDb } = require('./db');
12
+
13
+ const SHARED_RESOURCE_ID = /^shr_[A-Za-z0-9_-]{16,128}$/;
14
+ const CONTRACT = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$/;
15
+
16
+ function invalid(message, statusCode = 400) {
17
+ return Object.assign(new Error(message), { statusCode });
18
+ }
19
+
20
+ function requiredText(value, name, max = 1024) {
21
+ const text = typeof value === 'string' ? value.trim() : '';
22
+ if (!text || text.length > max || /[\u0000-\u001f\u007f]/.test(text)) {
23
+ throw invalid(`${name} must be a non-empty string of at most ${max} characters`);
24
+ }
25
+ return text;
26
+ }
27
+
28
+ function integer(value, name, minimum) {
29
+ const number = Number(value);
30
+ if (!Number.isSafeInteger(number) || number < minimum) {
31
+ throw invalid(`${name} must be an integer >= ${minimum}`);
32
+ }
33
+ return number;
34
+ }
35
+
36
+ function normalizeReplica(row) {
37
+ if (!row) return null;
38
+ return {
39
+ resourceId: row.resource_id,
40
+ localResource: row.local_resource,
41
+ localPath: row.local_path,
42
+ contract: row.contract,
43
+ schemaVersion: Number(row.schema_version),
44
+ authorityEpoch: Number(row.authority_epoch),
45
+ appliedVersion: Number(row.applied_version),
46
+ snapshotVersion: Number(row.snapshot_version),
47
+ snapshotChecksum: row.snapshot_checksum || null,
48
+ syncStatus: row.sync_status,
49
+ lastError: row.last_error || null,
50
+ createdAt: row.created_at,
51
+ updatedAt: row.updated_at,
52
+ };
53
+ }
54
+
55
+ function registerReplica(input, options = {}) {
56
+ const resourceId = requiredText(input?.resourceId, 'resourceId', 132);
57
+ if (!SHARED_RESOURCE_ID.test(resourceId)) {
58
+ throw invalid('resourceId must be an opaque id beginning with shr_');
59
+ }
60
+ const localResource = requiredText(input?.localResource, 'localResource');
61
+ const localPath = requiredText(input?.localPath, 'localPath', 4096);
62
+ const contract = requiredText(input?.contract, 'contract', 128);
63
+ if (!CONTRACT.test(contract)) throw invalid('contract has an invalid format');
64
+ const schemaVersion = integer(input?.schemaVersion, 'schemaVersion', 1);
65
+ const authorityEpoch = integer(input?.authorityEpoch, 'authorityEpoch', 1);
66
+ const appliedVersion = integer(input?.appliedVersion ?? 0, 'appliedVersion', 0);
67
+ const snapshotVersion = integer(input?.snapshotVersion ?? appliedVersion, 'snapshotVersion', 0);
68
+ if (snapshotVersion > appliedVersion) throw invalid('snapshotVersion cannot exceed appliedVersion');
69
+ const snapshotChecksum = input?.snapshotChecksum == null
70
+ ? null
71
+ : requiredText(input.snapshotChecksum, 'snapshotChecksum', 64);
72
+ if (snapshotChecksum && !/^[0-9a-f]{64}$/.test(snapshotChecksum)) {
73
+ throw invalid('snapshotChecksum must be lowercase sha256 hex');
74
+ }
75
+ const database = options.database || openLocalDb();
76
+ const existing = cachedStatement(
77
+ database,
78
+ 'SELECT * FROM shared_resource_replicas WHERE resource_id = ?',
79
+ ).get(resourceId);
80
+ if (existing) {
81
+ const normalized = normalizeReplica(existing);
82
+ if (
83
+ normalized.localResource !== localResource
84
+ || normalized.localPath !== localPath
85
+ || normalized.contract !== contract
86
+ || normalized.schemaVersion !== schemaVersion
87
+ ) {
88
+ throw invalid(`shared resource ${resourceId} is already bound differently`, 409);
89
+ }
90
+ return { replica: normalized, duplicate: true };
91
+ }
92
+
93
+ const now = new Date().toISOString();
94
+ try {
95
+ cachedStatement(database, `
96
+ INSERT INTO shared_resource_replicas (
97
+ resource_id,
98
+ local_resource,
99
+ local_path,
100
+ contract,
101
+ schema_version,
102
+ authority_epoch,
103
+ applied_version,
104
+ snapshot_version,
105
+ snapshot_checksum,
106
+ sync_status,
107
+ created_at,
108
+ updated_at
109
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'synced', ?, ?)
110
+ `).run(
111
+ resourceId,
112
+ localResource,
113
+ localPath,
114
+ contract,
115
+ schemaVersion,
116
+ authorityEpoch,
117
+ appliedVersion,
118
+ snapshotVersion,
119
+ snapshotChecksum,
120
+ now,
121
+ now,
122
+ );
123
+ } catch (error) {
124
+ if (String(error?.code || '').includes('CONSTRAINT')) {
125
+ throw invalid('that local file is already bound to another shared resource', 409);
126
+ }
127
+ throw error;
128
+ }
129
+ return {
130
+ replica: normalizeReplica(
131
+ cachedStatement(database, 'SELECT * FROM shared_resource_replicas WHERE resource_id = ?')
132
+ .get(resourceId),
133
+ ),
134
+ duplicate: false,
135
+ };
136
+ }
137
+
138
+ function getReplicaByResourceId(resourceId, options = {}) {
139
+ const database = options.database || openLocalDb();
140
+ return normalizeReplica(
141
+ cachedStatement(database, 'SELECT * FROM shared_resource_replicas WHERE resource_id = ?')
142
+ .get(resourceId),
143
+ );
144
+ }
145
+
146
+ function getReplicaByLocalResource(localResource, options = {}) {
147
+ const database = options.database || openLocalDb();
148
+ return normalizeReplica(
149
+ cachedStatement(database, 'SELECT * FROM shared_resource_replicas WHERE local_resource = ?')
150
+ .get(localResource),
151
+ );
152
+ }
153
+
154
+ function listReplicas(options = {}) {
155
+ const database = options.database || openLocalDb();
156
+ return cachedStatement(database, 'SELECT * FROM shared_resource_replicas ORDER BY created_at ASC')
157
+ .all()
158
+ .map(normalizeReplica);
159
+ }
160
+
161
+ function refreshReplicaAuthority(input, options = {}) {
162
+ const resourceId = requiredText(input?.resourceId, 'resourceId', 132);
163
+ const authorityEpoch = integer(input?.authorityEpoch, 'authorityEpoch', 1);
164
+ const headVersion = integer(input?.headVersion, 'headVersion', 0);
165
+ const database = options.database || openLocalDb();
166
+ const row = cachedStatement(
167
+ database,
168
+ 'SELECT * FROM shared_resource_replicas WHERE resource_id = ?',
169
+ ).get(resourceId);
170
+ if (!row) throw invalid(`unknown shared replica ${resourceId}`, 404);
171
+ if (
172
+ (input?.contract && input.contract !== row.contract)
173
+ || (input?.schemaVersion && Number(input.schemaVersion) !== Number(row.schema_version))
174
+ ) {
175
+ throw invalid(`shared resource ${resourceId} contract changed`, 409);
176
+ }
177
+ if (authorityEpoch < Number(row.authority_epoch)) {
178
+ return normalizeReplica(row);
179
+ }
180
+ cachedStatement(database, `
181
+ UPDATE shared_resource_replicas
182
+ SET authority_epoch = ?,
183
+ sync_status = CASE WHEN applied_version < ? THEN 'syncing' ELSE sync_status END,
184
+ last_error = CASE
185
+ WHEN applied_version < ? THEN 'official tail recovery required'
186
+ ELSE last_error
187
+ END,
188
+ updated_at = ?
189
+ WHERE resource_id = ?
190
+ `).run(authorityEpoch, headVersion, headVersion, new Date().toISOString(), resourceId);
191
+ return getReplicaByResourceId(resourceId, { database });
192
+ }
193
+
194
+ module.exports = {
195
+ getReplicaByLocalResource,
196
+ getReplicaByResourceId,
197
+ listReplicas,
198
+ refreshReplicaAuthority,
199
+ registerReplica,
200
+ };
@@ -98,6 +98,10 @@ async function handleDocOpen(body, sendJson) {
98
98
 
99
99
  async function handleDocUpdate(body, sendJson) {
100
100
  try {
101
+ if (Array.isArray(body?.mutations)) {
102
+ sendJson(200, require('./docs').applyDocMutations(body?.path, body.mutations));
103
+ return;
104
+ }
101
105
  const updates = body?.updates ?? body?.update;
102
106
  if (updates == null) {
103
107
  sendJson(400, { error: 'body requires "updates" (array of base64 Yjs updates) or "update"' });