amalgm 0.1.159 → 0.1.160

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.
@@ -26,6 +26,7 @@ function createSharedRealtimeTunnel({
26
26
  const mutationInflight = new Map();
27
27
  const promotionInflight = new Map();
28
28
  const attachmentInflight = new Map();
29
+ const privateEnsureInflight = new Map();
29
30
  const joinInflight = new Map();
30
31
  const inboundChains = new Map();
31
32
 
@@ -40,6 +41,31 @@ function createSharedRealtimeTunnel({
40
41
  let attachmentRetryTimer = null;
41
42
  let localFailures = 0;
42
43
 
44
+ async function ensurePrivateCookieResource() {
45
+ if (!enabled || privateEnsureInflight.size > 0) return;
46
+ try {
47
+ const bootstrap = await localStateJson('GET', '/browser/cookie-jar/cloud/bootstrap');
48
+ if (!bootstrap?.enabled || !bootstrap.resourceId) return;
49
+ const requestId = `private_${crypto.randomBytes(12).toString('hex')}`;
50
+ if (!sendLarge({
51
+ type: 'private.resource.ensure',
52
+ request_id: requestId,
53
+ resource_id: bootstrap.resourceId,
54
+ contract: bootstrap.contract,
55
+ schema_version: bootstrap.schemaVersion,
56
+ snapshot_b64: bootstrap.snapshotBase64,
57
+ snapshot_checksum: bootstrap.snapshotChecksum,
58
+ snapshot_bytes: bootstrap.snapshotBytes,
59
+ })) {
60
+ scheduleMutationDrain(RETRY_MS);
61
+ return;
62
+ }
63
+ privateEnsureInflight.set(requestId, bootstrap.resourceId);
64
+ } catch (error) {
65
+ warn(`private cookie bootstrap unavailable: ${error?.message || error}`);
66
+ }
67
+ }
68
+
43
69
  function localStateJson(method, requestPath, body) {
44
70
  const payload = body === undefined ? null : Buffer.from(JSON.stringify(body));
45
71
  return new Promise((resolve, reject) => {
@@ -323,6 +349,7 @@ function createSharedRealtimeTunnel({
323
349
  function startPump() {
324
350
  if (!enabled) return;
325
351
  startWakeStream();
352
+ void ensurePrivateCookieResource();
326
353
  void joinReplicas();
327
354
  scheduleAttachmentDrain();
328
355
  schedulePromotionDrain();
@@ -375,6 +402,28 @@ function createSharedRealtimeTunnel({
375
402
  });
376
403
  }
377
404
 
405
+ function handlePrivateReady(frame) {
406
+ const resourceId = privateEnsureInflight.get(frame.request_id);
407
+ if (!resourceId || resourceId !== frame.resource_id) return;
408
+ void localStateJson('POST', '/browser/cookie-jar/cloud/install', {
409
+ resourceId: frame.resource_id,
410
+ authorityEpoch: frame.authority_epoch,
411
+ headVersion: frame.head_version,
412
+ snapshotVersion: frame.snapshot_version,
413
+ snapshotChecksum: frame.snapshot_checksum,
414
+ snapshotBase64: frame.snapshot_b64,
415
+ contract: frame.contract,
416
+ schemaVersion: frame.schema_version,
417
+ }).then(() => {
418
+ privateEnsureInflight.delete(frame.request_id);
419
+ void joinReplicas();
420
+ scheduleMutationDrain();
421
+ }).catch((error) => {
422
+ warn(`private cookie snapshot was not installed: ${error?.message || error}`);
423
+ setTimeout(() => enabled && handlePrivateReady(frame), RETRY_MS).unref?.();
424
+ });
425
+ }
426
+
378
427
  function handleAck(frame) {
379
428
  const identity = mutationInflight.get(frame.request_id) || {
380
429
  resourceId: frame.resource_id,
@@ -396,6 +445,15 @@ function createSharedRealtimeTunnel({
396
445
  }
397
446
 
398
447
  function handleError(frame) {
448
+ const privateResource = privateEnsureInflight.get(frame.request_id);
449
+ if (privateResource) {
450
+ privateEnsureInflight.delete(frame.request_id);
451
+ warn(`private cookie bootstrap failed for ${privateResource}: ${frame.code || 'shared_error'}: ${frame.message || ''}`);
452
+ if (frame.retryable !== false) {
453
+ setTimeout(() => enabled && void ensurePrivateCookieResource(), RETRY_MS).unref?.();
454
+ }
455
+ return;
456
+ }
399
457
  const joinedResource = joinInflight.get(frame.request_id);
400
458
  if (joinedResource) {
401
459
  joinInflight.delete(frame.request_id);
@@ -505,6 +563,9 @@ function createSharedRealtimeTunnel({
505
563
  schemaVersion: frame.schema_version,
506
564
  }).catch(() => {});
507
565
  return true;
566
+ case 'private.resource.ready':
567
+ handlePrivateReady(frame);
568
+ return true;
508
569
  default:
509
570
  return false;
510
571
  }
@@ -524,6 +585,7 @@ function createSharedRealtimeTunnel({
524
585
  mutationInflight.clear();
525
586
  promotionInflight.clear();
526
587
  attachmentInflight.clear();
588
+ privateEnsureInflight.clear();
527
589
  joinInflight.clear();
528
590
  inboundChains.clear();
529
591
  if (wakeRequest) wakeRequest.destroy();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.159",
3
+ "version": "0.1.160",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -23,8 +23,12 @@ const {
23
23
  COOKIE_SOURCE_BUNDLE_ID,
24
24
  DEFAULT_COOKIE_SOURCE_PROFILE_ID,
25
25
  } = require('./cookie-source');
26
+ const crypto = require('crypto');
26
27
 
27
28
  const COOKIE_JAR_SCHEMA_VERSION = 1;
29
+ const COOKIE_CLOUD_CONTRACT = 'browser-cookie-jar@1';
30
+ const COOKIE_CLOUD_RESOURCE = 'browser-cookie-jar';
31
+ const COOKIE_CLOUD_PATH = 'amalgm://browser-cookie-jar/default';
28
32
  const MAX_MUTATIONS_PER_MERGE = 10_000;
29
33
  const MAX_TOMBSTONES = 4_096;
30
34
  const DEFAULT_EXCLUDED_HOST_SUFFIXES = [
@@ -224,6 +228,16 @@ function publicSnapshot(jar) {
224
228
  };
225
229
  }
226
230
 
231
+ function cloudSnapshot(jar = loadInternalCookieJar()) {
232
+ return {
233
+ schemaVersion: COOKIE_JAR_SCHEMA_VERSION,
234
+ version: jar.version,
235
+ updatedAt: nowIso(),
236
+ records: [...jar.records.values()].sort((a, b) => a.key.localeCompare(b.key)),
237
+ tombstones: [...jar.tombstones.values()].sort((a, b) => a.key.localeCompare(b.key)),
238
+ };
239
+ }
240
+
227
241
  function readCookieJar() {
228
242
  return publicSnapshot(loadInternalCookieJar());
229
243
  }
@@ -264,7 +278,7 @@ function deleteKeyFor(input) {
264
278
  return cookieKey(input);
265
279
  }
266
280
 
267
- function mergeCookieJar(input = {}) {
281
+ function mergeCookieJarMaterialized(input = {}) {
268
282
  const sourceId = cleanSourceId(input.sourceId);
269
283
  const extraHosts = Array.isArray(input.excludeHosts) ? input.excludeHosts : [];
270
284
  const upserts = Array.isArray(input.upserts) ? input.upserts : [];
@@ -275,7 +289,7 @@ function mergeCookieJar(input = {}) {
275
289
 
276
290
  let jar = loadInternalCookieJar();
277
291
  const nextRevision = jar.version + 1;
278
- const changedAt = nowIso();
292
+ const changedAt = String(input.changedAt || nowIso());
279
293
  let changed = false;
280
294
 
281
295
  for (const inputCookie of upserts) {
@@ -319,6 +333,344 @@ function mergeCookieJar(input = {}) {
319
333
  return publicSnapshot(persistCookieJar(jar));
320
334
  }
321
335
 
336
+ function privateCookieResourceId(forUserId) {
337
+ const clean = String(forUserId || '').trim();
338
+ if (!clean) return null;
339
+ const digest = crypto
340
+ .createHash('sha256')
341
+ .update(`private-user:${clean}:${COOKIE_CLOUD_CONTRACT}`)
342
+ .digest('base64url')
343
+ .slice(0, 32);
344
+ return `pvr_${digest}`;
345
+ }
346
+
347
+ function cookieCloudReplica(options = {}) {
348
+ const { machineId, userId } = require('../workspace/identity');
349
+ const ownerUserId = userId();
350
+ const resourceId = privateCookieResourceId(ownerUserId);
351
+ if (!resourceId) return null;
352
+ const database = options.database || require('../state/db').openLocalDb();
353
+ const { getReplicaByResourceId, registerReplica } = require('../state/replicas');
354
+ const existing = getReplicaByResourceId(resourceId, { database });
355
+ if (existing) return { ...existing, deviceId: machineId(), ownerUserId };
356
+ const registered = registerReplica({
357
+ resourceId,
358
+ localResource: COOKIE_CLOUD_RESOURCE,
359
+ localPath: COOKIE_CLOUD_PATH,
360
+ contract: COOKIE_CLOUD_CONTRACT,
361
+ schemaVersion: COOKIE_JAR_SCHEMA_VERSION,
362
+ authorityEpoch: 1,
363
+ appliedVersion: 0,
364
+ snapshotVersion: 0,
365
+ }, { database });
366
+ return { ...registered.replica, deviceId: machineId(), ownerUserId };
367
+ }
368
+
369
+ function operationForUpsert(inputCookie, sourceId, extraHosts, changedAt) {
370
+ const cookie = normalizeJarCookie(inputCookie);
371
+ if (!validCookie(cookie, extraHosts)) return null;
372
+ if (isExpiredCookie(cookie)) {
373
+ return {
374
+ kind: 'cookie.delete',
375
+ key: cookieKey(cookie),
376
+ sourceId,
377
+ changedAt,
378
+ };
379
+ }
380
+ return {
381
+ kind: 'cookie.upsert',
382
+ key: cookieKey(cookie),
383
+ cookie,
384
+ sourceId,
385
+ changedAt,
386
+ };
387
+ }
388
+
389
+ function operationForDelete(input, sourceId, extraHosts, changedAt) {
390
+ if (input && typeof input === 'object' && hostIsExcluded(input.domain, extraHosts)) return null;
391
+ const key = deleteKeyFor(input);
392
+ if (!key) return null;
393
+ return { kind: 'cookie.delete', key, sourceId, changedAt };
394
+ }
395
+
396
+ function operationChangesJar(operation, snapshot = readCookieJar()) {
397
+ if (!operation) return false;
398
+ if (operation.kind === 'cookie.upsert') {
399
+ const current = (snapshot.cookies || []).find((cookie) => cookieKey(cookie) === operation.key);
400
+ return !current || cookieFingerprint(current) !== cookieFingerprint(operation.cookie);
401
+ }
402
+ return (snapshot.cookies || []).some((cookie) => cookieKey(cookie) === operation.key)
403
+ || !(snapshot.tombstones || []).some((item) => item.key === operation.key);
404
+ }
405
+
406
+ function materializeCookieOperation(operation) {
407
+ if (operation.kind === 'cookie.upsert') {
408
+ return mergeCookieJarMaterialized({
409
+ sourceId: operation.sourceId,
410
+ upserts: [operation.cookie],
411
+ changedAt: operation.changedAt,
412
+ });
413
+ }
414
+ if (operation.kind === 'cookie.delete') {
415
+ return mergeCookieJarMaterialized({
416
+ sourceId: operation.sourceId,
417
+ deletes: [operation.key],
418
+ changedAt: operation.changedAt,
419
+ });
420
+ }
421
+ throw new Error(`Unsupported ${COOKIE_CLOUD_CONTRACT} operation ${JSON.stringify(operation.kind)}`);
422
+ }
423
+
424
+ function cookieEvent(operation, mutationId) {
425
+ return {
426
+ resource: COOKIE_CLOUD_RESOURCE,
427
+ op: operation.kind === 'cookie.delete' ? 'delete' : 'update',
428
+ id: crypto.createHash('sha256').update(operation.key).digest('hex').slice(0, 24),
429
+ value: { protected: true, kind: operation.kind },
430
+ source: `browser-cookie-jar:${operation.sourceId}`,
431
+ clientMutationId: mutationId,
432
+ };
433
+ }
434
+
435
+ function commitCookieOperation(operation, replica = cookieCloudReplica(), options = {}) {
436
+ if (!replica) return materializeCookieOperation(operation);
437
+ const mutationId = options.mutationId || `mut_${crypto.randomBytes(18).toString('base64url')}`;
438
+ const database = options.database || require('../state/db').openLocalDb();
439
+ let snapshot;
440
+ require('../state/mutations').commitLocalMutation({
441
+ channelId: `resource:${replica.resourceId}`,
442
+ localResource: COOKIE_CLOUD_RESOURCE,
443
+ cloudResourceId: replica.resourceId,
444
+ mutationId,
445
+ deviceId: replica.deviceId,
446
+ authorityEpoch: replica.authorityEpoch,
447
+ baseCloudVersion: replica.appliedVersion,
448
+ contract: COOKIE_CLOUD_CONTRACT,
449
+ schemaVersion: COOKIE_JAR_SCHEMA_VERSION,
450
+ operationKind: operation.kind,
451
+ operation,
452
+ }, {
453
+ database,
454
+ materialize() {
455
+ snapshot = materializeCookieOperation(operation);
456
+ },
457
+ event: cookieEvent(operation, mutationId),
458
+ });
459
+ return snapshot || readCookieJar();
460
+ }
461
+
462
+ function mergeCookieJar(input = {}) {
463
+ const replica = cookieCloudReplica();
464
+ if (!replica) return mergeCookieJarMaterialized(input);
465
+ const sourceId = cleanSourceId(input.sourceId);
466
+ const extraHosts = Array.isArray(input.excludeHosts) ? input.excludeHosts : [];
467
+ const upserts = Array.isArray(input.upserts) ? input.upserts : [];
468
+ const deletes = Array.isArray(input.deletes) ? input.deletes : [];
469
+ if (upserts.length + deletes.length > MAX_MUTATIONS_PER_MERGE) {
470
+ throw new Error(`Cookie mutation batch exceeds ${MAX_MUTATIONS_PER_MERGE}`);
471
+ }
472
+
473
+ let snapshot = readCookieJar();
474
+ for (const inputCookie of upserts) {
475
+ const operation = operationForUpsert(inputCookie, sourceId, extraHosts, nowIso());
476
+ if (!operationChangesJar(operation, snapshot)) continue;
477
+ snapshot = commitCookieOperation(operation, replica);
478
+ }
479
+ for (const deletion of deletes) {
480
+ const operation = operationForDelete(deletion, sourceId, extraHosts, nowIso());
481
+ if (!operationChangesJar(operation, snapshot)) continue;
482
+ snapshot = commitCookieOperation(operation, replica);
483
+ }
484
+ return snapshot;
485
+ }
486
+
487
+ function normalizeCloudSnapshot(input) {
488
+ if (!input || input.schemaVersion !== COOKIE_JAR_SCHEMA_VERSION) {
489
+ throw new Error(`Unsupported ${COOKIE_CLOUD_CONTRACT} snapshot`);
490
+ }
491
+ const inputRecords = Array.isArray(input.records) ? input.records : [];
492
+ const inputTombstones = Array.isArray(input.tombstones) ? input.tombstones : [];
493
+ if (inputRecords.length + inputTombstones.length > MAX_MUTATIONS_PER_MERGE + MAX_TOMBSTONES) {
494
+ throw new Error('Cookie cloud snapshot is too large');
495
+ }
496
+ const records = new Map();
497
+ const tombstones = new Map();
498
+ for (const item of inputRecords) {
499
+ const cookie = normalizeJarCookie(item?.cookie || {});
500
+ if (!validCookie(cookie)) continue;
501
+ const key = cookieKey(cookie);
502
+ if (item?.key && item.key !== key) throw new Error('Cookie cloud snapshot key mismatch');
503
+ records.set(key, {
504
+ key,
505
+ cookie,
506
+ revision: Math.max(0, Number(item?.revision) || 0),
507
+ sourceId: cleanSourceId(item?.sourceId),
508
+ updatedAt: String(item?.updatedAt || input.updatedAt || nowIso()),
509
+ });
510
+ }
511
+ for (const item of inputTombstones) {
512
+ const key = String(item?.key || '');
513
+ if (!key) continue;
514
+ tombstones.set(key, {
515
+ key,
516
+ revision: Math.max(0, Number(item?.revision) || 0),
517
+ sourceId: cleanSourceId(item?.sourceId),
518
+ deletedAt: String(item?.deletedAt || input.updatedAt || nowIso()),
519
+ });
520
+ }
521
+ for (const [key, record] of records) {
522
+ const tombstone = tombstones.get(key);
523
+ if (!tombstone) continue;
524
+ if (tombstone.deletedAt >= record.updatedAt) records.delete(key);
525
+ else tombstones.delete(key);
526
+ }
527
+ return {
528
+ schemaVersion: COOKIE_JAR_SCHEMA_VERSION,
529
+ version: Math.max(0, Number(input.version) || 0),
530
+ records,
531
+ tombstones: pruneTombstones(tombstones),
532
+ };
533
+ }
534
+
535
+ function persistCloudSnapshot(input) {
536
+ const normalized = normalizeCloudSnapshot(input);
537
+ return persistCookieJar({
538
+ exists: true,
539
+ version: normalized.version,
540
+ bundle: null,
541
+ records: normalized.records,
542
+ tombstones: normalized.tombstones,
543
+ migratedFromLegacy: false,
544
+ });
545
+ }
546
+
547
+ function timestampForEntry(record, tombstone) {
548
+ return String(record?.updatedAt || tombstone?.deletedAt || '');
549
+ }
550
+
551
+ function localBootstrapOperations(local, remote, pendingKeys) {
552
+ const operations = [];
553
+ const keys = new Set([...local.records.keys(), ...local.tombstones.keys()]);
554
+ for (const key of keys) {
555
+ if (pendingKeys.has(key)) continue;
556
+ const localRecord = local.records.get(key);
557
+ const localTombstone = local.tombstones.get(key);
558
+ const remoteRecord = remote.records.get(key);
559
+ const remoteTombstone = remote.tombstones.get(key);
560
+ const localTime = timestampForEntry(localRecord, localTombstone);
561
+ const remoteTime = timestampForEntry(remoteRecord, remoteTombstone);
562
+ if (!localTime || localTime <= remoteTime) continue;
563
+ if (localRecord) {
564
+ operations.push({
565
+ kind: 'cookie.upsert',
566
+ key,
567
+ cookie: localRecord.cookie,
568
+ sourceId: `${localRecord.sourceId}:bootstrap`,
569
+ changedAt: localRecord.updatedAt,
570
+ });
571
+ } else if (localTombstone) {
572
+ operations.push({
573
+ kind: 'cookie.delete',
574
+ key,
575
+ sourceId: `${localTombstone.sourceId}:bootstrap`,
576
+ changedAt: localTombstone.deletedAt,
577
+ });
578
+ }
579
+ }
580
+ return operations;
581
+ }
582
+
583
+ function getCookieCloudBootstrap(options = {}) {
584
+ const replica = cookieCloudReplica(options);
585
+ if (!replica) return { enabled: false, reason: 'runtime is not enrolled to a user' };
586
+ const bytes = Buffer.from(JSON.stringify(cloudSnapshot()), 'utf8');
587
+ return {
588
+ enabled: true,
589
+ resourceId: replica.resourceId,
590
+ contract: COOKIE_CLOUD_CONTRACT,
591
+ schemaVersion: COOKIE_JAR_SCHEMA_VERSION,
592
+ authorityEpoch: replica.authorityEpoch,
593
+ appliedVersion: replica.appliedVersion,
594
+ snapshotBase64: bytes.toString('base64'),
595
+ snapshotBytes: bytes.length,
596
+ snapshotChecksum: crypto.createHash('sha256').update(bytes).digest('hex'),
597
+ };
598
+ }
599
+
600
+ function installCookieCloudBootstrap(input = {}, options = {}) {
601
+ const database = options.database || require('../state/db').openLocalDb();
602
+ const replica = cookieCloudReplica({ database });
603
+ if (!replica || input.resourceId !== replica.resourceId) {
604
+ throw new Error('Private cookie resource does not belong to this runtime user');
605
+ }
606
+ if (input.contract !== COOKIE_CLOUD_CONTRACT || Number(input.schemaVersion) !== COOKIE_JAR_SCHEMA_VERSION) {
607
+ throw new Error('Private cookie resource contract changed');
608
+ }
609
+ const bytes = Buffer.from(String(input.snapshotBase64 || ''), 'base64');
610
+ const checksum = crypto.createHash('sha256').update(bytes).digest('hex');
611
+ if (checksum !== input.snapshotChecksum) throw new Error('Private cookie snapshot checksum mismatch');
612
+ const local = loadInternalCookieJar();
613
+ const remote = normalizeCloudSnapshot(JSON.parse(bytes.toString('utf8')));
614
+ const pending = require('../state/mutations').listPendingCloudMutations(replica.resourceId, { database });
615
+ const pendingKeys = new Set(pending.map((mutation) => mutation.operation?.key).filter(Boolean));
616
+ const bootstrapOperations = localBootstrapOperations(local, remote, pendingKeys);
617
+
618
+ persistCloudSnapshot({
619
+ schemaVersion: COOKIE_JAR_SCHEMA_VERSION,
620
+ version: remote.version,
621
+ records: [...remote.records.values()],
622
+ tombstones: [...remote.tombstones.values()],
623
+ });
624
+ const { advanceReplicaSnapshot, refreshReplicaAuthority } = require('../state/replicas');
625
+ advanceReplicaSnapshot({
626
+ resourceId: replica.resourceId,
627
+ snapshotVersion: Number(input.snapshotVersion) || 0,
628
+ snapshotChecksum: checksum,
629
+ authorityEpoch: Number(input.authorityEpoch) || 1,
630
+ }, { database });
631
+ refreshReplicaAuthority({
632
+ resourceId: replica.resourceId,
633
+ authorityEpoch: Number(input.authorityEpoch) || 1,
634
+ headVersion: Number(input.headVersion) || 0,
635
+ contract: COOKIE_CLOUD_CONTRACT,
636
+ schemaVersion: COOKIE_JAR_SCHEMA_VERSION,
637
+ }, { database });
638
+
639
+ // Snapshot installation never discards local work. Already-journaled edits
640
+ // replay from their durable rows; pre-cloud legacy entries become ordinary
641
+ // new mutations through the same commit path.
642
+ for (const mutation of pending) materializeCookieOperation(mutation.operation);
643
+ let currentReplica = cookieCloudReplica({ database });
644
+ for (const operation of bootstrapOperations) {
645
+ commitCookieOperation(operation, currentReplica, { database });
646
+ currentReplica = cookieCloudReplica({ database });
647
+ }
648
+ return { replica: currentReplica, jar: readCookieJar(), bootstrappedMutations: bootstrapOperations.length };
649
+ }
650
+
651
+ function applyCloudCookieMutation(input = {}, options = {}) {
652
+ const envelope = input.envelope || {};
653
+ if (envelope.contract !== COOKIE_CLOUD_CONTRACT) {
654
+ throw new Error(`Cannot apply ${String(envelope.contract)} to the cookie jar`);
655
+ }
656
+ const database = options.database || require('../state/db').openLocalDb();
657
+ const mutationId = String(envelope.mutationId || '');
658
+ return require('../state/mutations').ingestCloudMutation({
659
+ localResource: COOKIE_CLOUD_RESOURCE,
660
+ envelope,
661
+ version: input.version,
662
+ committedAt: input.committedAt,
663
+ }, {
664
+ database,
665
+ materialize(mutation) {
666
+ materializeCookieOperation(mutation.operation);
667
+ },
668
+ event(mutation) {
669
+ return cookieEvent(mutation.operation, mutationId);
670
+ },
671
+ });
672
+ }
673
+
322
674
  function diffCookieSnapshots(before = [], after = [], options = {}) {
323
675
  const beforeMap = new Map(normalizeCookieList(before, options.excludeHosts).map((cookie) => [cookieKey(cookie), cookie]));
324
676
  const afterMap = new Map(normalizeCookieList(after, options.excludeHosts).map((cookie) => [cookieKey(cookie), cookie]));
@@ -350,15 +702,21 @@ function cookieJarState(snapshot = readCookieJar()) {
350
702
  }
351
703
 
352
704
  module.exports = {
705
+ COOKIE_CLOUD_CONTRACT,
353
706
  COOKIE_JAR_SCHEMA_VERSION,
354
707
  DEFAULT_EXCLUDED_HOST_SUFFIXES,
708
+ applyCloudCookieMutation,
709
+ cloudSnapshot,
355
710
  cookieFingerprint,
356
711
  cookieJarState,
357
712
  cookieKey,
358
713
  cookiesMissingFromJar,
359
714
  diffCookieSnapshots,
715
+ getCookieCloudBootstrap,
360
716
  hostIsExcluded,
717
+ installCookieCloudBootstrap,
361
718
  mergeCookieJar,
362
719
  normalizeCookieList,
720
+ privateCookieResourceId,
363
721
  readCookieJar,
364
722
  };
@@ -13,6 +13,8 @@ const {
13
13
  createBrowserLoginSession,
14
14
  deleteBrowserAuthBundle,
15
15
  getBrowserAuthBundle,
16
+ getCookieCloudBootstrap,
17
+ installCookieCloudBootstrap,
16
18
  listBrowserLoginSessions,
17
19
  listBrowserAuthBundles,
18
20
  listBrowserProfiles,
@@ -121,6 +123,22 @@ function handleCookieJarMerge(body = {}, sendJson) {
121
123
  sendJson(200, { jar });
122
124
  }
123
125
 
126
+ function handleCookieCloudBootstrap(sendJson) {
127
+ try {
128
+ sendJson(200, getCookieCloudBootstrap());
129
+ } catch (error) {
130
+ sendJson(500, { error: error?.message || 'cookie cloud bootstrap failed' });
131
+ }
132
+ }
133
+
134
+ function handleCookieCloudInstall(body = {}, sendJson) {
135
+ try {
136
+ sendJson(200, installCookieCloudBootstrap(body));
137
+ } catch (error) {
138
+ sendJson(Number(error?.statusCode) || 400, { error: error?.message || 'cookie cloud install failed' });
139
+ }
140
+ }
141
+
124
142
  async function handleAuthBundleExport(body = {}, sendJson) {
125
143
  const result = await exportAuthBundleFromProfile(body);
126
144
  sendJson(200, { bundle: result.bundle });
@@ -333,6 +351,8 @@ module.exports = {
333
351
  handleAuthBundlesList,
334
352
  handleCookieJarGet,
335
353
  handleCookieJarMerge,
354
+ handleCookieCloudBootstrap,
355
+ handleCookieCloudInstall,
336
356
  handleLoginSessionCancel,
337
357
  handleLoginSessionComplete,
338
358
  handleLoginSessionCreate,
@@ -13,6 +13,16 @@ async function handleBrowserRoutes(ctx) {
13
13
  browserRest.handleCookieJarMerge(await ctx.readJsonBody(), ctx.sendJson);
14
14
  return true;
15
15
  }
16
+ if (ctx.pathname === '/browser/cookie-jar/cloud/bootstrap' && ctx.method === 'GET') {
17
+ ctx.res.setHeader('Cache-Control', 'no-store');
18
+ browserRest.handleCookieCloudBootstrap(ctx.sendJson);
19
+ return true;
20
+ }
21
+ if (ctx.pathname === '/browser/cookie-jar/cloud/install' && ctx.method === 'POST') {
22
+ ctx.res.setHeader('Cache-Control', 'no-store');
23
+ browserRest.handleCookieCloudInstall(await ctx.readJsonBody(), ctx.sendJson);
24
+ return true;
25
+ }
16
26
  if (ctx.pathname === '/browser/profiles' && ctx.method === 'GET') {
17
27
  browserRest.handleProfilesList(ctx.sendJson);
18
28
  return true;
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Runtime-side persistence policy for mutation contracts.
5
+ *
6
+ * The mutation journal is the durable cloud outbox, so sensitive operations
7
+ * must be protected before they enter it. Transport callers explicitly decode
8
+ * the row while it is in memory; diagnostics only receive the redacted shape.
9
+ */
10
+
11
+ const { decryptJson, encryptJson } = require('../browser/auth-vault');
12
+
13
+ const COOKIE_CONTRACT = 'browser-cookie-jar@1';
14
+ const PROTECTED_KIND = 'amalgm.encrypted-json@1';
15
+
16
+ function isSensitiveContract(contract) {
17
+ return contract === COOKIE_CONTRACT;
18
+ }
19
+
20
+ function protectOperation(contract, operation) {
21
+ if (!isSensitiveContract(contract)) return operation;
22
+ return {
23
+ kind: PROTECTED_KIND,
24
+ contract,
25
+ envelope: encryptJson(operation).envelope,
26
+ };
27
+ }
28
+
29
+ function revealOperation(contract, stored) {
30
+ if (!isSensitiveContract(contract)) return stored;
31
+ if (
32
+ !stored
33
+ || stored.kind !== PROTECTED_KIND
34
+ || stored.contract !== contract
35
+ || !stored.envelope
36
+ ) {
37
+ throw new Error(`Mutation payload for ${contract} is not protected`);
38
+ }
39
+ return decryptJson(stored.envelope);
40
+ }
41
+
42
+ function diagnosticOperation(contract, operation) {
43
+ if (!isSensitiveContract(contract)) return operation;
44
+ return {
45
+ kind: operation && typeof operation === 'object' ? operation.kind || 'redacted' : 'redacted',
46
+ protected: true,
47
+ };
48
+ }
49
+
50
+ module.exports = {
51
+ COOKIE_CONTRACT,
52
+ diagnosticOperation,
53
+ isSensitiveContract,
54
+ protectOperation,
55
+ revealOperation,
56
+ };
@@ -14,12 +14,17 @@
14
14
 
15
15
  const { cachedStatement, openLocalDb } = require('./db');
16
16
  const { insertStateEvent, publishStateEvent } = require('./events');
17
+ const {
18
+ diagnosticOperation,
19
+ protectOperation,
20
+ revealOperation,
21
+ } = require('./mutation-contracts');
17
22
  const { trace } = require('./trace');
18
23
 
19
24
  const MAX_MUTATION_BYTES = 4 * 1024 * 1024;
20
25
  const DEFAULT_CLAIM_LIMIT = 100;
21
26
  const DEFAULT_CLAIM_LEASE_MS = 30_000;
22
- const SHARED_RESOURCE_ID = /^shr_[A-Za-z0-9_-]{16,128}$/;
27
+ const CLOUD_RESOURCE_ID = /^(?:shr|pvr)_[A-Za-z0-9_-]{16,128}$/;
23
28
  const MUTATION_ID = /^[A-Za-z0-9_-]{16,160}$/;
24
29
  const OPERATION_KIND = /^[a-z][a-z0-9._:-]{0,63}$/;
25
30
  const CONTRACT = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$/;
@@ -96,24 +101,28 @@ function normalizeMutationInput(input) {
96
101
  throw invalid(`operation exceeds the ${MAX_MUTATION_BYTES}-byte durable mutation limit`, 413);
97
102
  }
98
103
 
99
- const sharedResourceId = input.sharedResourceId == null
104
+ // `shared_resource_id` is the installed SQLite compatibility column. The
105
+ // core treats it as a cloud resource id; `shr_` uses shared authority and
106
+ // `pvr_` uses same-user-private authority.
107
+ const cloudResourceInput = input.cloudResourceId ?? input.sharedResourceId;
108
+ const sharedResourceId = cloudResourceInput == null
100
109
  ? null
101
- : cleanText(input.sharedResourceId, 'sharedResourceId', 132);
102
- if (sharedResourceId && !SHARED_RESOURCE_ID.test(sharedResourceId)) {
103
- throw invalid('sharedResourceId must be an opaque id beginning with shr_');
110
+ : cleanText(cloudResourceInput, 'cloudResourceId', 132);
111
+ if (sharedResourceId && !CLOUD_RESOURCE_ID.test(sharedResourceId)) {
112
+ throw invalid('cloudResourceId must be an opaque id beginning with shr_ or pvr_');
104
113
  }
105
114
  if (sharedResourceId && channelId !== `resource:${sharedResourceId}`) {
106
- throw invalid('a shared mutation channelId must be resource:<sharedResourceId>');
115
+ throw invalid('a cloud mutation channelId must be resource:<cloudResourceId>');
107
116
  }
108
117
 
109
118
  const authorityEpoch = nonNegativeInteger(input.authorityEpoch, 'authorityEpoch');
110
119
  const baseCloudVersion = nonNegativeInteger(input.baseCloudVersion, 'baseCloudVersion');
111
120
  const deviceId = input.deviceId == null ? null : cleanText(input.deviceId, 'deviceId', 256);
112
121
  if (sharedResourceId && authorityEpoch < 1) {
113
- throw invalid('shared mutations require authorityEpoch >= 1');
122
+ throw invalid('cloud mutations require authorityEpoch >= 1');
114
123
  }
115
124
  if (sharedResourceId && !deviceId) {
116
- throw invalid('shared mutations require deviceId');
125
+ throw invalid('cloud mutations require deviceId');
117
126
  }
118
127
 
119
128
  const localRevision = input.localRevision == null
@@ -123,6 +132,7 @@ function normalizeMutationInput(input) {
123
132
  return {
124
133
  channelId,
125
134
  localResource,
135
+ cloudResourceId: sharedResourceId,
126
136
  sharedResourceId,
127
137
  mutationId,
128
138
  deviceId,
@@ -137,12 +147,15 @@ function normalizeMutationInput(input) {
137
147
  };
138
148
  }
139
149
 
140
- function normalizeJournalRow(row) {
150
+ function normalizeJournalRow(row, options = {}) {
141
151
  if (!row) return null;
152
+ const storedOperation = safeJsonParse(row.operation_json);
153
+ const operation = revealOperation(row.contract, storedOperation);
142
154
  return {
143
155
  journalId: Number(row.journal_id),
144
156
  channelId: row.channel_id,
145
157
  localResource: row.local_resource,
158
+ cloudResourceId: row.shared_resource_id || null,
146
159
  sharedResourceId: row.shared_resource_id || null,
147
160
  mutationId: row.mutation_id,
148
161
  deviceId: row.device_id || null,
@@ -154,7 +167,9 @@ function normalizeJournalRow(row) {
154
167
  contract: row.contract,
155
168
  schemaVersion: Number(row.schema_version),
156
169
  operationKind: row.operation_kind,
157
- operation: safeJsonParse(row.operation_json),
170
+ operation: options.diagnostic === true
171
+ ? diagnosticOperation(row.contract, operation)
172
+ : operation,
158
173
  origin: row.mutation_origin,
159
174
  state: row.mutation_state,
160
175
  localRevision: row.local_revision == null ? null : Number(row.local_revision),
@@ -177,6 +192,10 @@ function getMutationRow(database, channelId, mutationId) {
177
192
  }
178
193
 
179
194
  function sameMutation(existing, mutation) {
195
+ const existingOperation = revealOperation(
196
+ existing.contract,
197
+ safeJsonParse(existing.operation_json),
198
+ );
180
199
  return existing.local_resource === mutation.localResource
181
200
  && (existing.shared_resource_id || null) === mutation.sharedResourceId
182
201
  && (existing.device_id || null) === mutation.deviceId
@@ -185,11 +204,15 @@ function sameMutation(existing, mutation) {
185
204
  && existing.contract === mutation.contract
186
205
  && Number(existing.schema_version) === mutation.schemaVersion
187
206
  && existing.operation_kind === mutation.operationKind
188
- && existing.operation_json === mutation.operationJson
207
+ && JSON.stringify(existingOperation) === mutation.operationJson
189
208
  && (existing.local_revision == null ? null : Number(existing.local_revision)) === mutation.localRevision;
190
209
  }
191
210
 
192
211
  function sameCloudEnvelope(existing, mutation) {
212
+ const existingOperation = revealOperation(
213
+ existing.contract,
214
+ safeJsonParse(existing.operation_json),
215
+ );
193
216
  return existing.local_resource === mutation.localResource
194
217
  && (existing.shared_resource_id || null) === mutation.sharedResourceId
195
218
  && (existing.device_id || null) === mutation.deviceId
@@ -198,7 +221,7 @@ function sameCloudEnvelope(existing, mutation) {
198
221
  && existing.contract === mutation.contract
199
222
  && Number(existing.schema_version) === mutation.schemaVersion
200
223
  && existing.operation_kind === mutation.operationKind
201
- && existing.operation_json === mutation.operationJson;
224
+ && JSON.stringify(existingOperation) === mutation.operationJson;
202
225
  }
203
226
 
204
227
  function journalMutation(input, options = {}) {
@@ -248,7 +271,7 @@ function journalMutation(input, options = {}) {
248
271
  mutation.contract,
249
272
  mutation.schemaVersion,
250
273
  mutation.operationKind,
251
- mutation.operationJson,
274
+ JSON.stringify(protectOperation(mutation.contract, mutation.operation)),
252
275
  mutation.localRevision,
253
276
  );
254
277
 
@@ -405,7 +428,7 @@ function journalCloudMutation(input, options = {}) {
405
428
  throw invalid(`shared resource ${sharedResourceId} contract changed`, 409);
406
429
  }
407
430
 
408
- const existing = findSharedMutationRow(database, sharedResourceId, mutation.mutationId);
431
+ const existing = findCloudMutationRow(database, sharedResourceId, mutation.mutationId);
409
432
  if (existing) {
410
433
  if (!sameCloudEnvelope(existing, mutation)) {
411
434
  throw invalid(`mutationId ${mutation.mutationId} was reused with different content`, 409);
@@ -499,7 +522,7 @@ function journalCloudMutation(input, options = {}) {
499
522
  mutation.contract,
500
523
  mutation.schemaVersion,
501
524
  mutation.operationKind,
502
- mutation.operationJson,
525
+ JSON.stringify(protectOperation(mutation.contract, mutation.operation)),
503
526
  committedAt,
504
527
  now,
505
528
  now,
@@ -582,15 +605,17 @@ function commitLocalMutation(input, options = {}) {
582
605
 
583
606
  function getMutation(channelId, mutationId, options = {}) {
584
607
  const database = options.database || openLocalDb();
585
- return normalizeJournalRow(getMutationRow(database, channelId, mutationId));
608
+ return normalizeJournalRow(getMutationRow(database, channelId, mutationId), {
609
+ diagnostic: options.revealOperation !== true,
610
+ });
586
611
  }
587
612
 
588
613
  function cloudEnvelope(mutation) {
589
- if (!mutation.sharedResourceId || !mutation.deviceId) {
614
+ if (!mutation.cloudResourceId || !mutation.deviceId) {
590
615
  throw invalid(`mutation ${mutation.mutationId} is not cloud-deliverable`, 409);
591
616
  }
592
617
  return {
593
- resourceId: mutation.sharedResourceId,
618
+ resourceId: mutation.cloudResourceId,
594
619
  authorityEpoch: mutation.authorityEpoch,
595
620
  mutationId: mutation.mutationId,
596
621
  deviceId: mutation.deviceId,
@@ -652,7 +677,7 @@ function claimCloudMutations(options = {}) {
652
677
  })();
653
678
  }
654
679
 
655
- function findSharedMutationRow(database, sharedResourceId, mutationId) {
680
+ function findCloudMutationRow(database, sharedResourceId, mutationId) {
656
681
  return cachedStatement(database, `
657
682
  SELECT * FROM mutation_journal
658
683
  WHERE shared_resource_id = ? AND mutation_id = ?
@@ -671,7 +696,7 @@ function acknowledgeCloudMutation(sharedResourceIdInput, mutationIdInput, acknow
671
696
  const database = options.database || openLocalDb();
672
697
 
673
698
  return database.transaction(() => {
674
- const row = findSharedMutationRow(database, sharedResourceId, mutationId);
699
+ const row = findCloudMutationRow(database, sharedResourceId, mutationId);
675
700
  if (!row) throw invalid(`unknown shared mutation ${mutationId}`, 404);
676
701
  if (row.mutation_state === 'synced') {
677
702
  if (Number(row.official_cloud_version) !== version) {
@@ -715,7 +740,7 @@ function failCloudMutation(sharedResourceIdInput, mutationIdInput, failure, opti
715
740
  ? (failure?.retryAt || new Date(Date.now() + 1000).toISOString())
716
741
  : null;
717
742
  const database = options.database || openLocalDb();
718
- const row = findSharedMutationRow(database, sharedResourceId, mutationId);
743
+ const row = findCloudMutationRow(database, sharedResourceId, mutationId);
719
744
  if (!row) throw invalid(`unknown shared mutation ${mutationId}`, 404);
720
745
  if (row.mutation_state === 'synced') return normalizeJournalRow(row);
721
746
 
@@ -750,7 +775,7 @@ function markMutationConflicted(sharedResourceIdInput, mutationIdInput, messageI
750
775
  WHERE shared_resource_id = ? AND mutation_id = ? AND mutation_state != 'synced'
751
776
  `).run(message, now, sharedResourceId, mutationId);
752
777
  if (!result.changes) throw invalid(`unknown or synced shared mutation ${mutationId}`, 404);
753
- return normalizeJournalRow(findSharedMutationRow(database, sharedResourceId, mutationId));
778
+ return normalizeJournalRow(findCloudMutationRow(database, sharedResourceId, mutationId));
754
779
  }
755
780
 
756
781
  function listMutations(options = {}) {
@@ -768,7 +793,27 @@ function listMutations(options = {}) {
768
793
  ORDER BY journal_id ASC
769
794
  LIMIT ?
770
795
  `).all(limit);
771
- return rows.map(normalizeJournalRow);
796
+ return rows.map((row) => normalizeJournalRow(row, { diagnostic: true }));
797
+ }
798
+
799
+ /**
800
+ * Internal recovery view used by contract materializers after installing a
801
+ * newer snapshot. It reads the journal without claiming rows or changing
802
+ * their retry leases.
803
+ */
804
+ function listPendingCloudMutations(resourceIdInput, options = {}) {
805
+ const resourceId = cleanText(resourceIdInput, 'resourceId', 132);
806
+ if (!CLOUD_RESOURCE_ID.test(resourceId)) {
807
+ throw invalid('resourceId must be an opaque cloud resource id');
808
+ }
809
+ const database = options.database || openLocalDb();
810
+ return cachedStatement(database, `
811
+ SELECT * FROM mutation_journal
812
+ WHERE shared_resource_id = ?
813
+ AND mutation_origin = 'local'
814
+ AND mutation_state != 'synced'
815
+ ORDER BY journal_id ASC
816
+ `).all(resourceId).map((row) => normalizeJournalRow(row));
772
817
  }
773
818
 
774
819
  function pruneMutationJournal(options = {}) {
@@ -814,6 +859,7 @@ module.exports = {
814
859
  ingestCloudMutation,
815
860
  journalCloudMutation,
816
861
  journalMutation,
862
+ listPendingCloudMutations,
817
863
  listMutations,
818
864
  markMutationConflicted,
819
865
  pruneMutationJournal,
@@ -11,7 +11,7 @@
11
11
  const { cachedStatement, openLocalDb } = require('./db');
12
12
  const { trace } = require('./trace');
13
13
 
14
- const SHARED_RESOURCE_ID = /^shr_[A-Za-z0-9_-]{16,128}$/;
14
+ const CLOUD_RESOURCE_ID = /^(?:shr|pvr)_[A-Za-z0-9_-]{16,128}$/;
15
15
  const CONTRACT = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$/;
16
16
 
17
17
  function invalid(message, statusCode = 400) {
@@ -55,8 +55,8 @@ function normalizeReplica(row) {
55
55
 
56
56
  function registerReplica(input, options = {}) {
57
57
  const resourceId = requiredText(input?.resourceId, 'resourceId', 132);
58
- if (!SHARED_RESOURCE_ID.test(resourceId)) {
59
- throw invalid('resourceId must be an opaque id beginning with shr_');
58
+ if (!CLOUD_RESOURCE_ID.test(resourceId)) {
59
+ throw invalid('resourceId must be an opaque id beginning with shr_ or pvr_');
60
60
  }
61
61
  const localResource = requiredText(input?.localResource, 'localResource');
62
62
  const localPath = requiredText(input?.localPath, 'localPath', 4096);
@@ -86,7 +86,7 @@ function registerReplica(input, options = {}) {
86
86
  || normalized.contract !== contract
87
87
  || normalized.schemaVersion !== schemaVersion
88
88
  ) {
89
- throw invalid(`shared resource ${resourceId} is already bound differently`, 409);
89
+ throw invalid(`cloud resource ${resourceId} is already bound differently`, 409);
90
90
  }
91
91
  return { replica: normalized, duplicate: true };
92
92
  }
@@ -123,7 +123,7 @@ function registerReplica(input, options = {}) {
123
123
  );
124
124
  } catch (error) {
125
125
  if (String(error?.code || '').includes('CONSTRAINT')) {
126
- throw invalid('that local file is already bound to another shared resource', 409);
126
+ throw invalid('that local materialization is already bound to another cloud resource', 409);
127
127
  }
128
128
  throw error;
129
129
  }
@@ -267,7 +267,11 @@ async function handleMutationFail(body, sendJson) {
267
267
 
268
268
  async function handleMutationIngest(body, sendJson) {
269
269
  try {
270
- sendJson(200, require('./docs').applyCloudDocMutation(body));
270
+ const contract = body?.envelope?.contract;
271
+ const result = contract === 'browser-cookie-jar@1'
272
+ ? require('../browser/cookie-jar').applyCloudCookieMutation(body)
273
+ : require('./docs').applyCloudDocMutation(body);
274
+ sendJson(200, result);
271
275
  } catch (error) {
272
276
  sendHandlerError(error, sendJson);
273
277
  }
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+
9
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-cookie-cloud-'));
10
+ const userId = '11111111-1111-4111-8111-111111111111';
11
+ process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
12
+ process.env.AMALGM_BROWSER_AUTH_KEY = 'e'.repeat(64);
13
+ fs.mkdirSync(process.env.AMALGM_DIR, { recursive: true });
14
+ fs.writeFileSync(path.join(process.env.AMALGM_DIR, 'computer.json'), JSON.stringify({
15
+ computer_id: 'machine-cookie-a',
16
+ user_id: userId,
17
+ }));
18
+
19
+ const { closeLocalDb, openLocalDb } = require('../state/db');
20
+ const { listEventsAfter } = require('../state/events');
21
+ const {
22
+ acknowledgeCloudMutation,
23
+ claimCloudMutations,
24
+ getMutation,
25
+ } = require('../state/mutations');
26
+ const {
27
+ COOKIE_CLOUD_CONTRACT,
28
+ applyCloudCookieMutation,
29
+ getCookieCloudBootstrap,
30
+ mergeCookieJar,
31
+ privateCookieResourceId,
32
+ readCookieJar,
33
+ } = require('../browser/cookie-jar');
34
+ const { deleteBrowserAuthBundle } = require('../browser/auth-bundles');
35
+ const { COOKIE_SOURCE_BUNDLE_ID } = require('../browser/cookie-source');
36
+
37
+ function cookie(name, value) {
38
+ return {
39
+ name,
40
+ value,
41
+ domain: '.example.com',
42
+ path: '/',
43
+ expires: -1,
44
+ httpOnly: true,
45
+ secure: true,
46
+ sameSite: 'Lax',
47
+ };
48
+ }
49
+
50
+ test.beforeEach(() => {
51
+ const db = openLocalDb();
52
+ deleteBrowserAuthBundle(COOKIE_SOURCE_BUNDLE_ID);
53
+ db.prepare('DELETE FROM event_log').run();
54
+ db.prepare('DELETE FROM mutation_journal').run();
55
+ db.prepare('DELETE FROM shared_resource_replicas').run();
56
+ });
57
+
58
+ test.after(() => {
59
+ closeLocalDb();
60
+ fs.rmSync(tempRoot, { recursive: true, force: true });
61
+ });
62
+
63
+ test('signed-in cookie edits use the common encrypted journal/outbox', () => {
64
+ const secret = 'cloud-cookie-secret';
65
+ const result = mergeCookieJar({ sourceId: 'electron', upserts: [cookie('session', secret)] });
66
+ assert.equal(result.cookies[0].value, secret);
67
+
68
+ const db = openLocalDb();
69
+ const row = db.prepare('SELECT * FROM mutation_journal').get();
70
+ assert.ok(row.shared_resource_id.startsWith('pvr_'));
71
+ assert.equal(row.contract, COOKIE_CLOUD_CONTRACT);
72
+ assert.equal(row.operation_json.includes(secret), false, 'SQLite never stores plaintext cookie values');
73
+
74
+ const [claim] = claimCloudMutations({ database: db });
75
+ assert.equal(claim.envelope.operation.cookie.value, secret, 'the authenticated TLS transport gets the decoded operation');
76
+ assert.deepEqual(getMutation(row.channel_id, row.mutation_id, { database: db }).operation, {
77
+ kind: 'cookie.upsert',
78
+ protected: true,
79
+ });
80
+ assert.equal(JSON.stringify(listEventsAfter(0)).includes(secret), false, 'Local Live events are metadata-only');
81
+ });
82
+
83
+ test('official cookie mutations materialize in order and deletions stay tombstoned', () => {
84
+ mergeCookieJar({ sourceId: 'machine-a', upserts: [cookie('local', 'local-secret')] });
85
+ const db = openLocalDb();
86
+ const [localClaim] = claimCloudMutations({ database: db });
87
+ acknowledgeCloudMutation(localClaim.envelope.resourceId, localClaim.envelope.mutationId, {
88
+ version: 1,
89
+ committedAt: '2026-07-20T20:00:00.000Z',
90
+ }, { database: db });
91
+
92
+ const remoteCookie = cookie('remote', 'remote-secret');
93
+ const key = require('../browser/cookie-jar').cookieKey(remoteCookie);
94
+ const common = {
95
+ resourceId: localClaim.envelope.resourceId,
96
+ authorityEpoch: 1,
97
+ deviceId: 'machine-cookie-b',
98
+ contract: COOKIE_CLOUD_CONTRACT,
99
+ schemaVersion: 1,
100
+ };
101
+ applyCloudCookieMutation({
102
+ version: 2,
103
+ committedAt: '2026-07-20T20:01:00.000Z',
104
+ envelope: {
105
+ ...common,
106
+ mutationId: 'mut_cookie_remote_0001',
107
+ baseVersion: 1,
108
+ operationKind: 'cookie.upsert',
109
+ operation: {
110
+ kind: 'cookie.upsert',
111
+ key,
112
+ cookie: remoteCookie,
113
+ sourceId: 'machine-b',
114
+ changedAt: '2026-07-20T20:01:00.000Z',
115
+ },
116
+ },
117
+ }, { database: db });
118
+ assert.equal(readCookieJar().cookies.some((item) => item.value === 'remote-secret'), true);
119
+
120
+ applyCloudCookieMutation({
121
+ version: 3,
122
+ committedAt: '2026-07-20T20:02:00.000Z',
123
+ envelope: {
124
+ ...common,
125
+ mutationId: 'mut_cookie_remote_0002',
126
+ baseVersion: 2,
127
+ operationKind: 'cookie.delete',
128
+ operation: {
129
+ kind: 'cookie.delete',
130
+ key,
131
+ sourceId: 'machine-b',
132
+ changedAt: '2026-07-20T20:02:00.000Z',
133
+ },
134
+ },
135
+ }, { database: db });
136
+ const deleted = readCookieJar();
137
+ assert.equal(deleted.cookies.some((item) => item.name === 'remote'), false);
138
+ assert.equal(deleted.tombstones.some((item) => item.key === key), true);
139
+ assert.equal(JSON.stringify(listEventsAfter(0)).includes('remote-secret'), false);
140
+ });
141
+
142
+ test('the private resource is deterministic per user and bootstrap is automatic', () => {
143
+ mergeCookieJar({ sourceId: 'headless', upserts: [cookie('bootstrap', 'bootstrap-secret')] });
144
+ const bootstrap = getCookieCloudBootstrap();
145
+ assert.equal(bootstrap.enabled, true);
146
+ assert.equal(bootstrap.resourceId, privateCookieResourceId(userId));
147
+ assert.notEqual(
148
+ bootstrap.resourceId,
149
+ privateCookieResourceId('22222222-2222-4222-8222-222222222222'),
150
+ );
151
+ assert.equal(JSON.parse(Buffer.from(bootstrap.snapshotBase64, 'base64').toString('utf8')).schemaVersion, 1);
152
+ });
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const crypto = require('crypto');
6
+ const http = require('http');
7
+
8
+ const { createSharedRealtimeTunnel } = require('../../../../packages/amalgm/lib/shared-realtime-tunnel');
9
+
10
+ const RESOURCE = 'pvr_private_cookie_tunnel_01';
11
+ const SNAPSHOT = Buffer.from(JSON.stringify({
12
+ schemaVersion: 1,
13
+ version: 0,
14
+ updatedAt: '2026-07-20T20:00:00.000Z',
15
+ records: [],
16
+ tombstones: [],
17
+ }));
18
+
19
+ function sleep(ms) {
20
+ return new Promise((resolve) => { setTimeout(resolve, ms); });
21
+ }
22
+
23
+ async function until(predicate, label) {
24
+ for (let i = 0; i < 100; i += 1) {
25
+ if (predicate()) return;
26
+ await sleep(10);
27
+ }
28
+ assert.fail(`timed out waiting for ${label}`);
29
+ }
30
+
31
+ test('the authenticated tunnel automatically ensures, installs, and joins the private cookie resource', async () => {
32
+ const calls = [];
33
+ const server = http.createServer((req, res) => {
34
+ let body = '';
35
+ req.on('data', (chunk) => { body += chunk; });
36
+ req.on('end', () => {
37
+ calls.push({ method: req.method, path: req.url, body: body ? JSON.parse(body) : null });
38
+ res.setHeader('content-type', 'application/json');
39
+ if (req.url === '/browser/cookie-jar/cloud/bootstrap') {
40
+ res.end(JSON.stringify({
41
+ enabled: true,
42
+ resourceId: RESOURCE,
43
+ contract: 'browser-cookie-jar@1',
44
+ schemaVersion: 1,
45
+ snapshotBase64: SNAPSHOT.toString('base64'),
46
+ snapshotBytes: SNAPSHOT.length,
47
+ snapshotChecksum: crypto.createHash('sha256').update(SNAPSHOT).digest('hex'),
48
+ }));
49
+ return;
50
+ }
51
+ if (req.url === '/state/shared/replicas') {
52
+ res.end(JSON.stringify({ replicas: [{ resourceId: RESOURCE, appliedVersion: 0 }] }));
53
+ return;
54
+ }
55
+ if (req.url?.startsWith('/state/stream')) {
56
+ res.writeHead(200, { 'content-type': 'text/event-stream' });
57
+ res.write('\n');
58
+ return;
59
+ }
60
+ res.end(JSON.stringify({ mutations: [], promotions: [], attachments: [], ok: true }));
61
+ });
62
+ });
63
+ await new Promise((resolve) => { server.listen(0, '127.0.0.1', resolve); });
64
+ const port = server.address().port;
65
+ const sent = [];
66
+ const tunnel = createSharedRealtimeTunnel({
67
+ send(frame) { sent.push(frame); return true; },
68
+ sendLarge(frame) { sent.push(frame); return true; },
69
+ localPort: () => port,
70
+ localHeaders: (headers) => ({ ...headers }),
71
+ warn: () => {},
72
+ });
73
+
74
+ try {
75
+ tunnel.enable(['shared.mutation.v1', 'private.cookie.v1']);
76
+ await until(() => sent.some((frame) => frame.type === 'private.resource.ensure'), 'private ensure frame');
77
+ const ensure = sent.find((frame) => frame.type === 'private.resource.ensure');
78
+ assert.equal(ensure.resource_id, RESOURCE);
79
+ assert.equal(ensure.contract, 'browser-cookie-jar@1');
80
+
81
+ tunnel.handleFrame({
82
+ type: 'private.resource.ready',
83
+ request_id: ensure.request_id,
84
+ resource_id: RESOURCE,
85
+ authority_epoch: 1,
86
+ head_version: 0,
87
+ snapshot_version: 0,
88
+ snapshot_checksum: crypto.createHash('sha256').update(SNAPSHOT).digest('hex'),
89
+ snapshot_b64: SNAPSHOT.toString('base64'),
90
+ contract: 'browser-cookie-jar@1',
91
+ schema_version: 1,
92
+ duplicate: false,
93
+ });
94
+ await until(
95
+ () => calls.some((call) => call.path === '/browser/cookie-jar/cloud/install'),
96
+ 'local private snapshot install',
97
+ );
98
+ const install = calls.find((call) => call.path === '/browser/cookie-jar/cloud/install');
99
+ assert.equal(install.body.resourceId, RESOURCE);
100
+ await until(
101
+ () => sent.filter((frame) => frame.type === 'shared.resource.join' && frame.resource_id === RESOURCE).length >= 2,
102
+ 'post-install private resource join',
103
+ );
104
+ } finally {
105
+ tunnel.stop();
106
+ server.close();
107
+ }
108
+ });