@solarpunkltd/swarm-id 0.0.0-stage → 0.8.1

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 (111) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +89 -2
  3. package/dist/bus/account-bus.d.ts +104 -0
  4. package/dist/bus/account-delta.d.ts +52 -0
  5. package/dist/bus/bus-context.d.ts +7 -0
  6. package/dist/bus/envelope.d.ts +9 -0
  7. package/dist/bus/messages.d.ts +241 -0
  8. package/dist/bus/presence.d.ts +62 -0
  9. package/dist/bus/signaling-transport.d.ts +79 -0
  10. package/dist/chunk/bmt.d.ts +16 -0
  11. package/dist/chunk/cac.d.ts +17 -0
  12. package/dist/chunk/constants.d.ts +10 -0
  13. package/dist/chunk/encrypted-cac.d.ts +47 -0
  14. package/dist/chunk/encryption.d.ts +85 -0
  15. package/dist/chunk/index.d.ts +5 -0
  16. package/dist/chunks/chat-binding-DO5aGhPl.js +23 -0
  17. package/dist/chunks/chat-binding-DO5aGhPl.js.map +1 -0
  18. package/dist/errors.d.ts +35 -0
  19. package/dist/index.d.ts +27 -0
  20. package/dist/internal.d.ts +49 -0
  21. package/dist/internal.esm.js +2 -0
  22. package/dist/internal.esm.js.map +1 -0
  23. package/dist/proxy/act/act.d.ts +58 -0
  24. package/dist/proxy/act/crypto.d.ts +46 -0
  25. package/dist/proxy/act/grantee-list.d.ts +81 -0
  26. package/dist/proxy/act/history.d.ts +182 -0
  27. package/dist/proxy/act/index.d.ts +114 -0
  28. package/dist/proxy/app-stamp-limits.d.ts +62 -0
  29. package/dist/proxy/chunk-upload-stream.d.ts +37 -0
  30. package/dist/proxy/chunking.d.ts +15 -0
  31. package/dist/proxy/collection.d.ts +38 -0
  32. package/dist/proxy/download-data.d.ts +27 -0
  33. package/dist/proxy/errors.d.ts +8 -0
  34. package/dist/proxy/feed-manifest.d.ts +60 -0
  35. package/dist/proxy/feeds/epochs/async-finder.d.ts +70 -0
  36. package/dist/proxy/feeds/epochs/epoch.d.ts +79 -0
  37. package/dist/proxy/feeds/epochs/finder.d.ts +60 -0
  38. package/dist/proxy/feeds/epochs/index.d.ts +30 -0
  39. package/dist/proxy/feeds/epochs/types.d.ts +110 -0
  40. package/dist/proxy/feeds/epochs/updater.d.ts +76 -0
  41. package/dist/proxy/feeds/epochs/utils.d.ts +21 -0
  42. package/dist/proxy/feeds/index.d.ts +4 -0
  43. package/dist/proxy/feeds/sequence/async-finder.d.ts +14 -0
  44. package/dist/proxy/feeds/sequence/finder.d.ts +11 -0
  45. package/dist/proxy/feeds/sequence/index.d.ts +22 -0
  46. package/dist/proxy/feeds/sequence/types.d.ts +82 -0
  47. package/dist/proxy/feeds/sequence/updater.d.ts +20 -0
  48. package/dist/proxy/index.d.ts +5 -0
  49. package/dist/proxy/manifest-builder.d.ts +164 -0
  50. package/dist/proxy/mantaray.d.ts +76 -0
  51. package/dist/proxy/soc-restamp-slots.d.ts +48 -0
  52. package/dist/proxy/stamp-chunk.d.ts +45 -0
  53. package/dist/proxy/stamp-worker-handler.d.ts +32 -0
  54. package/dist/proxy/stamp-worker-pool.d.ts +52 -0
  55. package/dist/proxy/stamp-worker.d.ts +1 -0
  56. package/dist/proxy/types.d.ts +38 -0
  57. package/dist/proxy/upload.d.ts +207 -0
  58. package/dist/schemas.d.ts +671 -0
  59. package/dist/stamp-worker.iife.js +1 -0
  60. package/dist/storage/debounced-uploader.d.ts +61 -0
  61. package/dist/storage/utilization-store.d.ts +113 -0
  62. package/dist/swarm-id-client.d.ts +1143 -0
  63. package/dist/swarm-id-proxy.d.ts +1079 -0
  64. package/dist/swarm-id.esm.js +2 -0
  65. package/dist/swarm-id.esm.js.map +1 -0
  66. package/dist/sync/batch-write-coordinator.d.ts +348 -0
  67. package/dist/sync/device-roster.d.ts +59 -0
  68. package/dist/sync/device-state.d.ts +199 -0
  69. package/dist/sync/fold-account-from-swarm.d.ts +53 -0
  70. package/dist/sync/index.d.ts +11 -0
  71. package/dist/sync/merge-snapshot.d.ts +57 -0
  72. package/dist/sync/partition-intent.d.ts +218 -0
  73. package/dist/sync/partition-lease.d.ts +531 -0
  74. package/dist/sync/partition-lock.d.ts +218 -0
  75. package/dist/sync/partition-state.d.ts +287 -0
  76. package/dist/sync/session-check.d.ts +29 -0
  77. package/dist/sync/store-interfaces.d.ts +48 -0
  78. package/dist/sync/sync-account.d.ts +45 -0
  79. package/dist/sync/timing-constants.d.ts +32 -0
  80. package/dist/sync/types.d.ts +18 -0
  81. package/dist/types.d.ts +3042 -0
  82. package/dist/utils/account-state-snapshot.d.ts +43 -0
  83. package/dist/utils/active-devices.d.ts +45 -0
  84. package/dist/utils/avatar.d.ts +30 -0
  85. package/dist/utils/batch-utilization.d.ts +840 -0
  86. package/dist/utils/batch-write-lock.d.ts +12 -0
  87. package/dist/utils/bee-error.d.ts +11 -0
  88. package/dist/utils/chat-binding.d.ts +113 -0
  89. package/dist/utils/coalesced-task.d.ts +28 -0
  90. package/dist/utils/collection.d.ts +6 -0
  91. package/dist/utils/connection-info.d.ts +11 -0
  92. package/dist/utils/constants.d.ts +28 -0
  93. package/dist/utils/device-id.d.ts +109 -0
  94. package/dist/utils/hex.d.ts +31 -0
  95. package/dist/utils/json-rpc.d.ts +34 -0
  96. package/dist/utils/key-derivation.d.ts +124 -0
  97. package/dist/utils/lock-soc.d.ts +35 -0
  98. package/dist/utils/postage-contract.d.ts +143 -0
  99. package/dist/utils/postage-stamp-association.d.ts +39 -0
  100. package/dist/utils/promise.d.ts +37 -0
  101. package/dist/utils/sealed-values.d.ts +23 -0
  102. package/dist/utils/seed-encryption.d.ts +17 -0
  103. package/dist/utils/site-allow-list.d.ts +54 -0
  104. package/dist/utils/stamp-lifespan.d.ts +37 -0
  105. package/dist/utils/storage-managers.d.ts +62 -0
  106. package/dist/utils/storage-probe.d.ts +40 -0
  107. package/dist/utils/tag.d.ts +11 -0
  108. package/dist/utils/ttl.d.ts +120 -0
  109. package/dist/utils/url.d.ts +55 -0
  110. package/dist/utils/versioned-storage.d.ts +183 -0
  111. package/package.json +91 -3
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Per-partition lock SOC for multi-device postage-batch sharing.
3
+ *
4
+ * A single shared SOC per partition is the cross-device authority for "who
5
+ * holds this partition". Multiple devices share the `backupSigner` so any of
6
+ * them can write the SOC; concurrent writers are ordered deterministically by
7
+ * a (timestampMs, tiebreaker) fencing token.
8
+ *
9
+ * identifier = keccak256("swarm-id-partition-lock-v1:" || batchIdHex || ":" || partition)
10
+ * owner = backup signer (shared across the account's devices)
11
+ * encryption = swarmEncryptionKey
12
+ *
13
+ * The identifier carries the batch and the partition, but not the account —
14
+ * domain separation across accounts comes from the per-account `owner`
15
+ * (derived from each account's `derivationKey`), so the SOC address still
16
+ * differs per account without accountId in the hash. The batch IS in there:
17
+ * keying on the partition alone made lane p of batch X and lane p of batch Y
18
+ * one address, so two coordinators sharing no slot took turns anyway (#589).
19
+ * See `makePartitionLockIdentifier` in `../utils/lock-soc`.
20
+ *
21
+ * Protocol (acquire / takeover / refresh):
22
+ * 1. Read the lock SOC.
23
+ * 2. If a live foreign holder exists → return "blocked" without writing.
24
+ * 3. Otherwise write our claim, wait `guardMs`, re-read.
25
+ * 4. If our generation is still the latest visible → "acquired".
26
+ * If a higher generation was written during the guard → "lost-race".
27
+ *
28
+ * See the "Multi-Device Postage Batches" page in docs-site for the full design.
29
+ */
30
+ import { Bee, PrivateKey, type BatchId, type Stamper } from "@ethersphere/bee-js";
31
+ import { type PartitionLockGeneration, type PartitionLockPayload } from "../schemas";
32
+ export type { PartitionLockGeneration, PartitionLockPayload };
33
+ export { lockSocAddress, lockSocBucket, makePartitionLockIdentifier, } from "../utils/lock-soc";
34
+ /**
35
+ * Sentinel value placed in `holderDeviceId` to indicate "no current holder"
36
+ * (e.g. after an explicit release).
37
+ */
38
+ export declare const NO_HOLDER_DEVICE_ID = "";
39
+ /**
40
+ * Outcome of an `acquirePartitionLock` call.
41
+ *
42
+ * - `"acquired"` — the lock is now held by this device (write + verify both
43
+ * succeeded).
44
+ * - `"blocked"` — a live foreign holder exists; no write was performed.
45
+ * - `"lost-race"` — we wrote, but a higher-generation writer appeared
46
+ * during the guard window. The verify-read returned that other write.
47
+ * - `"aborted"` — `shouldAbort` flipped before the claim write (the owning
48
+ * lease session closed, e.g. a release started); no write was performed.
49
+ */
50
+ export interface AcquirePartitionLockResult {
51
+ outcome: "acquired" | "blocked" | "lost-race" | "aborted";
52
+ /** Latest payload observed by this call (after the verify-read). */
53
+ payload: PartitionLockPayload | undefined;
54
+ }
55
+ /**
56
+ * Outcome of a `releasePartitionLock` call.
57
+ *
58
+ * - `"released"` — the release sentinel was written.
59
+ * - `"skipped"` — the lock no longer carries the claim being released
60
+ * (successor claim, peer claim, or an existing sentinel); writing the
61
+ * sentinel would clobber state that is not ours to release. The lock is
62
+ * left untouched.
63
+ */
64
+ export interface ReleasePartitionLockResult {
65
+ outcome: "released" | "skipped";
66
+ /** Payload observed by the pre-write read (undefined when missing/unreadable). */
67
+ observed?: PartitionLockPayload;
68
+ }
69
+ /**
70
+ * Compute the per-device tiebreaker: first 8 bytes of `keccak256(deviceId)`
71
+ * as a hex string. Two different deviceIds collide with probability 2⁻⁶⁴.
72
+ */
73
+ export declare function makeDeviceTiebreaker(deviceId: string): string;
74
+ /**
75
+ * Deterministic per-device starting partition: `keccak256(deviceId) mod
76
+ * partitionCount`. Distinct devices map to distinct home partitions with high
77
+ * probability, so they spread across slots even when none can read peers' lock
78
+ * SOCs (the disjoint-gateway frozen-cache case — see
79
+ * docs/Partition-Lock-Hardening-Plan.md). Only changes the selection scan
80
+ * ORDER; visibly-held slots are still skipped, so behaviour under working
81
+ * reads is unchanged.
82
+ *
83
+ * ponytail: probabilistic spread, not mutual exclusion — colliding home
84
+ * partitions still race. The complete cross-gateway fix is the Phase 2
85
+ * intent-SOC protocol in the hardening plan.
86
+ */
87
+ export declare function deviceHomePartition(deviceId: string, partitionCount: number): number;
88
+ /**
89
+ * Lexicographic comparison of two generations: timestamp first, then
90
+ * tiebreaker. Returns -1 / 0 / 1.
91
+ */
92
+ export declare function compareGenerations(a: PartitionLockGeneration, b: PartitionLockGeneration): -1 | 0 | 1;
93
+ /**
94
+ * Read the partition-lock SOC. Returns `undefined` when the SOC has never
95
+ * been written (the underlying chunk is missing on the Bee node).
96
+ */
97
+ export declare function readPartitionLock(opts: {
98
+ bee: Bee;
99
+ backupSigner: PrivateKey;
100
+ swarmEncryptionKey: Uint8Array;
101
+ batchId: BatchId;
102
+ partition: number;
103
+ }): Promise<PartitionLockPayload | undefined>;
104
+ /**
105
+ * Overwrite the partition-lock SOC with `payload`. LWW per SOC identifier
106
+ * — any prior chunk at this address is replaced (modulo Swarm propagation
107
+ * delay).
108
+ */
109
+ export declare function writePartitionLock(opts: {
110
+ bee: Bee;
111
+ stamper: Stamper;
112
+ backupSigner: PrivateKey;
113
+ swarmEncryptionKey: Uint8Array;
114
+ batchId: BatchId;
115
+ partition: number;
116
+ payload: PartitionLockPayload;
117
+ }): Promise<void>;
118
+ /**
119
+ * Acquire (or refresh) the partition lock for this device.
120
+ *
121
+ * @returns `"acquired"` if we now hold the lock,
122
+ * `"blocked"` if a live foreign holder exists (no write performed),
123
+ * `"lost-race"` if a higher-generation writer superseded our write
124
+ * inside the guard window.
125
+ */
126
+ export declare function acquirePartitionLock(opts: {
127
+ bee: Bee;
128
+ stamper: Stamper;
129
+ backupSigner: PrivateKey;
130
+ swarmEncryptionKey: Uint8Array;
131
+ batchId: BatchId;
132
+ partition: number;
133
+ deviceId: string;
134
+ ttlMs: number;
135
+ guardMs: number;
136
+ now?: () => number;
137
+ wait?: (ms: number) => Promise<void>;
138
+ /**
139
+ * Pre-built generation to claim with. Pass the SAME generation already
140
+ * advertised in a Phase-2 intent round so the winner's lock claim and its
141
+ * intent order identically. Defaults to a fresh `(now, tiebreaker)`.
142
+ */
143
+ generation?: PartitionLockGeneration;
144
+ /**
145
+ * Checked immediately before the claim write. When it returns true the
146
+ * acquire aborts WITHOUT writing — used by `PartitionLease` to stop a
147
+ * refresh that overlaps a `release()` from minting a ghost claim the
148
+ * generation-fenced release would then refuse to clear (peers would have
149
+ * to wait out the TTL).
150
+ */
151
+ shouldAbort?: () => boolean;
152
+ /**
153
+ * Skip the pre-write read entirely. Pass when the caller ALREADY read this
154
+ * partition's lock (timeout-bounded) moments ago and classified it as
155
+ * claimable — `PartitionLease.acquire`'s scan — so the duplicate read (an
156
+ * absent chunk that can block for tens of seconds on a gateway) stays off
157
+ * the acquire path. The live-foreign-holder "blocked" check is the caller's
158
+ * responsibility; the guard-window verify below still catches races.
159
+ */
160
+ skipInitialRead?: boolean;
161
+ /**
162
+ * Bound for the initial + verify reads (default {@link SYNC_READ_TIMEOUT_MS}).
163
+ * Bee has no fast authoritative "not found", so an absent lock would
164
+ * otherwise block on peer exhaustion. A timed-out read maps to the same
165
+ * "unobserved" handling as any other read failure: initial → treated absent
166
+ * (claim proceeds; guard+verify arbitrate), verify → the optimistic
167
+ * "couldn't confirm our own write" branch.
168
+ */
169
+ readTimeoutMs?: number;
170
+ }): Promise<AcquirePartitionLockResult>;
171
+ /**
172
+ * Write the release sentinel for a SPECIFIC claim — generation-fenced.
173
+ *
174
+ * A release is an action on the claim identified by `releasedGeneration`,
175
+ * not on the partition as such. The sentinel therefore carries that
176
+ * generation (it does NOT mint a fresh one), and it is only written when the
177
+ * lock still visibly carries the claim being released. Without the fence, a
178
+ * detached release that outlives a re-acquire writes its sentinel LAST —
179
+ * and Bee replaces a same-address SOC whenever the new chunk's postage
180
+ * stamp timestamp is newer, so the stale sentinel would deterministically
181
+ * clobber the successor's claim and peers would treat the partition as free
182
+ * while the successor still believes it holds it (issue #349).
183
+ *
184
+ * Skip rules (never write a sentinel over a claim that is not the one being
185
+ * released):
186
+ * - an existing sentinel (any generation) — already released; rewriting
187
+ * would mint a fresh stamp that could clobber a claim landing in between.
188
+ * Checked BEFORE the generation compare: a prior sentinel for this same
189
+ * claim carries the same generation and would otherwise match "ours".
190
+ * - a foreign claim, live or expired — never clobber a peer; an expired
191
+ * claim already reads as takeable.
192
+ * - our own claim with a NEWER generation — a successor re-acquired while
193
+ * this release was publishing (#349), or a release-overlapping refresh.
194
+ * - our own claim at a non-newer generation writes (`<=`, not `==`): a
195
+ * stale read returning an older refresh of the same holdership must not
196
+ * suppress the release.
197
+ * - a missing/unreadable lock writes (best-effort release; the sentinel is
198
+ * generation-fenced, so readers and successors can order it correctly).
199
+ *
200
+ * Residual (documented, accepted): a skip decision is only as fresh as the
201
+ * pre-write read. The same-device #349 race reads through the same Bee node
202
+ * (read-your-writes holds), so the fence is dependable there; cross-node
203
+ * staleness falls back to the displacement/TTL machinery.
204
+ */
205
+ export declare function releasePartitionLock(opts: {
206
+ bee: Bee;
207
+ stamper: Stamper;
208
+ backupSigner: PrivateKey;
209
+ swarmEncryptionKey: Uint8Array;
210
+ batchId: BatchId;
211
+ partition: number;
212
+ deviceId: string;
213
+ /** Generation of the claim being released — fences the sentinel. */
214
+ releasedGeneration: PartitionLockGeneration;
215
+ /** `acquiredAt` of the claim being released (carried in the sentinel). */
216
+ acquiredAt: number;
217
+ now?: () => number;
218
+ }): Promise<ReleasePartitionLockResult>;
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Partition-state pointer for multi-device postage-batch sharing.
3
+ *
4
+ * The current holder publishes its per-partition counter on each upload (and on
5
+ * release); the next holder reads it on acquire and resumes from exactly that
6
+ * counter, so the two devices never race for the same `(bucket, slot)` pair
7
+ * during an orderly hand-off.
8
+ *
9
+ * The counter is not a single blob: it is split into the same per-`chunkIndex`
10
+ * chunks the utilisation store uses, each uploaded with a random key to the
11
+ * partition's reserved slot (never a data slot), and a single "reference chunk"
12
+ * lists their 64-byte encrypted references (address‖key).
13
+ *
14
+ * The address of the latest reference chunk is published as a **rotating
15
+ * per-epoch state-pointer SOC** (`writeStatePointer`/`readStatePointer`): the
16
+ * identifier is `keccak256(topic ‖ uint64(floor(now / STATE_POINTER_EPOCH_MS)))`,
17
+ * so a reader COMPUTES the address directly — no epoch-tree walk — and the
18
+ * rotating address bypasses a gateway's frozen static-cache. The holder
19
+ * heartbeats the pointer on every refresh, so a taking-over device finds it at
20
+ * the current/previous bucket around `now` or the gone holder's `leasedUntil`.
21
+ * Mirrors the occupancy beacon (`partition-intent.ts`). See
22
+ * docs/Postage-Batch-Partitioning.md (§8 cross-device handoff).
23
+ *
24
+ * Topic = keccak256("swarm-id-partition-state-v1" ‖ batchId ‖ uint32(partition))
25
+ * Owner = backup signer (`deriveSecret(swarmEncryptionKey, BACKUP_KEY_LABEL)`)
26
+ */
27
+ import { Bee, BatchId, EthAddress, PrivateKey, Topic, type Stamper } from "@ethersphere/bee-js";
28
+ import { z } from "zod";
29
+ /**
30
+ * Schema for the *decoded* partition state: the per-bucket counter
31
+ * reconstructed from the binary wire format (reference chunk → counter
32
+ * chunks, see the module doc above for the byte layout). The wire format is
33
+ * versioned by the feed topic domain (`PARTITION_STATE_TOPIC_DOMAIN`), not an
34
+ * in-blob version byte — a future v2 publishes under a v2 topic and never
35
+ * collides with v1 readers.
36
+ *
37
+ * `readPartitionState` validates the assembled counter against this schema
38
+ * before returning it; a failure is reported as `readFailed` (fail-safe —
39
+ * never a zero counter, which would re-issue used slots).
40
+ * `writePartitionState` validates its input against it before publishing.
41
+ */
42
+ export declare const PartitionStateSchemaV1: z.ZodObject<{
43
+ counters: z.ZodCustom<Uint32Array<ArrayBuffer>, Uint32Array<ArrayBuffer>>;
44
+ }, z.core.$strip>;
45
+ export type PartitionState = z.infer<typeof PartitionStateSchemaV1>;
46
+ /**
47
+ * Build the per-partition state feed topic.
48
+ *
49
+ * Hashing rather than concatenating-to-string keeps the topic length
50
+ * fixed at 32 bytes regardless of how many partitions we support later.
51
+ */
52
+ export declare function makePartitionStateTopic(batchId: BatchId, partition: number): Topic;
53
+ /**
54
+ * Granularity of the rotating state-pointer address — the SAME epoch the
55
+ * intent/occupancy beacons rotate on: a reader computes a writer's bucket from
56
+ * this shared constant, so the two must never drift apart (this module already
57
+ * imports from partition-intent; there is no import cycle).
58
+ *
59
+ * COUPLING: the takeover lookup (`readStatePointer`) scans the whole
60
+ * `LEASE_TTL_MS`-wide bucket span. The reads run CONCURRENTLY (each
61
+ * timeout-bounded), so widening the span costs more parallel reads but not more
62
+ * wall-clock — the lookup latency stays ~one read timeout regardless of the
63
+ * `STATE_POINTER_EPOCH_MS` vs `LEASE_TTL_MS` ratio.
64
+ */
65
+ export declare const STATE_POINTER_EPOCH_MS: number;
66
+ /** Result of a `readStatePointer` lookup (see its doc for `inconclusive`). */
67
+ export interface StatePointerLookup {
68
+ pointer?: {
69
+ referenceHex: string;
70
+ };
71
+ inconclusive: boolean;
72
+ }
73
+ /** `floor(nowMs / STATE_POINTER_EPOCH_MS)` — the current state-pointer epoch bucket. */
74
+ export declare function statePointerEpochBucket(nowMs: number): number;
75
+ /**
76
+ * Publish the reference-chunk pointer at the current epoch bucket — a reserved-
77
+ * slot, encrypted SOC (mirrors the occupancy beacon's `writeReservedPartitionSoc`).
78
+ * Reserved-slot routed so it never consumes a data slot; overwrites within the
79
+ * same 30 s bucket. Called per publish AND on the refresh-tick heartbeat so a
80
+ * held partition always has a fresh pointer in the current bucket.
81
+ */
82
+ export declare function writeStatePointer(opts: {
83
+ bee: Bee;
84
+ stamper: Stamper;
85
+ backupSigner: PrivateKey;
86
+ swarmEncryptionKey: Uint8Array;
87
+ batchId: BatchId;
88
+ partition: number;
89
+ referenceHex: string;
90
+ nowMs: number;
91
+ }): Promise<void>;
92
+ /**
93
+ * The 32-byte state-pointer SOC address for `partition` at the epoch bucket
94
+ * covering `nowMs`. Exposed so `writePartitionState` can exclude the pointer's
95
+ * bucket from the reserved-slot buckets its counter/reference chunks may use
96
+ * (both land at slot = partition; a collision would evict one). Same derivation
97
+ * `writeStatePointer` writes at.
98
+ */
99
+ export declare function statePointerAddress(batchId: BatchId, partition: number, owner: EthAddress, nowMs: number): Uint8Array;
100
+ /**
101
+ * Read the latest state pointer by computing bucket addresses directly — no
102
+ * feed walk. Checks (newest-first) the current/previous bucket around `now` and,
103
+ * when given, the gone holder's lease span: every bucket from its last lock
104
+ * refresh (`leasedUntil`) back through `leasedUntil - LEASE_TTL_MS` plus
105
+ * {@link POINTER_LOOKUP_SLACK_BUCKETS} of slack. The holder heartbeats the
106
+ * pointer at `floor(now / EPOCH)` on every refresh, so its last DURABLE pointer
107
+ * lands somewhere in that span; scanning the whole span (rather than just the
108
+ * `leasedUntil` bucket) makes the lookup independent of the
109
+ * `STATE_POINTER_EPOCH_MS` vs `LEASE_TTL_MS` ratio AND tolerant of a couple of
110
+ * dropped final heartbeats. Returns the freshest pointer found (`pointer`), or
111
+ * `pointer: undefined` when none of the candidate buckets carry one.
112
+ * `inconclusive` marks a verdict that is NOT authoritative: with no pointer, a
113
+ * CLEAN scan (every candidate bucket read resolved) proves "no published state"
114
+ * while ≥1 timed-out bucket leaves a pointer we couldn't fetch possible; with a
115
+ * pointer found, a timed-out bucket NEWER than it may hide a fresher resume
116
+ * point (a later acked publish), so the found pointer is only a stale
117
+ * candidate. Either way the caller must fail safe on `inconclusive` rather
118
+ * than resume. Ceiling: a holder dark for longer than
119
+ * `LEASE_TTL_MS + POINTER_LOOKUP_SLACK_BUCKETS·EPOCH` past its last heartbeat
120
+ * resumes from zero.
121
+ */
122
+ export declare function readStatePointer(opts: {
123
+ bee: Bee;
124
+ owner: EthAddress;
125
+ swarmEncryptionKey: Uint8Array;
126
+ batchId: BatchId;
127
+ partition: number;
128
+ nowMs: number;
129
+ holderLeasedUntilMs?: number;
130
+ }): Promise<StatePointerLookup>;
131
+ /**
132
+ * Read the latest partition-state-feed entry and reconstruct the
133
+ * `localCounter` array — the previous holder's per-bucket high-water.
134
+ *
135
+ * Returns a fresh zero-filled array when no prior snapshot exists — i.e.
136
+ * when a fresh partition is being seeded for the first time (Case A on a
137
+ * brand-new account, or Case B when no peer has yet published on the
138
+ * partition we're about to take). We resume at exactly the published
139
+ * counter: an unclean-crash window where a peer stamped past its last
140
+ * publish is not defended against — that data was never durably persisted
141
+ * anyway, so overwriting those slots loses nothing of value.
142
+ *
143
+ * Pass `knownReferenceHex` (the feed reference this device's local counter was
144
+ * last in sync with) to short-circuit: when the feed still points there,
145
+ * returns `{ unchanged: true }` WITHOUT downloading the reference/counter
146
+ * chunks, and the caller reuses its local counter. On a real read it returns
147
+ * the reconstructed `localCounter` plus the current `referenceHex` for the
148
+ * caller to cache.
149
+ */
150
+ export declare function readPartitionState(opts: {
151
+ bee: Bee;
152
+ owner: EthAddress;
153
+ swarmEncryptionKey: Uint8Array;
154
+ batchId: BatchId;
155
+ partition: number;
156
+ batchDepth: number;
157
+ /** Gone holder's `leasedUntil` (from the lock SOC) — locates the pointer of
158
+ * a partition that's been free for a while at its last-heartbeat bucket.
159
+ * A holder heartbeats the pointer up to its `leasedUntil`, so reading that
160
+ * bucket (and the one before) finds a published pointer durably, however
161
+ * long ago: "no pointer found" then reliably means "no state was published". */
162
+ holderLeasedUntilMs?: number;
163
+ /**
164
+ * True when this partition's lock SOC could not be read conclusively (a
165
+ * transient/hanging read, NOT a clean 404). If we then find no published
166
+ * pointer we must NOT assume "no state" and resume from zero — the lock may
167
+ * be hiding an expired holder whose acked slots we'd re-issue. Treat that as
168
+ * `readFailed` (degrade to read-only + retry) instead. A genuinely fresh
169
+ * partition has a cleanly-absent lock (`false`) and still zero-seeds.
170
+ */
171
+ lockUnreadable?: boolean;
172
+ /**
173
+ * Wall-clock ms to anchor the state-pointer lookup to — the reader half of
174
+ * the rendezvous. The lease passes its own clock (`this.now()`, = `Date.now()`
175
+ * in production) so the reader computes the same buckets the writer wrote at.
176
+ * Defaults to `Date.now()` for bare callers. Injectable for deterministic /
177
+ * per-device-skew tests (#385).
178
+ */
179
+ nowMs?: number;
180
+ /**
181
+ * A `readStatePointer` result the caller already fetched (speculatively,
182
+ * overlapped with its lock/beacon scan) for this SAME
183
+ * (owner, batch, partition) with the SAME `nowMs` and NO
184
+ * `holderLeasedUntilMs` span. Only valid when `holderLeasedUntilMs` is
185
+ * undefined here too — identical inputs make it exactly the lookup this
186
+ * function would perform, so all fail-safe classification below applies
187
+ * unchanged. When provided, the pointer lookup is skipped.
188
+ */
189
+ prefetchedPointer?: StatePointerLookup;
190
+ }, knownReferenceHex?: string): Promise<{
191
+ localCounter?: Uint32Array;
192
+ referenceHex?: string;
193
+ unchanged: boolean;
194
+ /**
195
+ * True when the feed HAS an entry but the reference chunk or a counter
196
+ * chunk could not be read. The caller must NOT proceed with a zero
197
+ * counter — the partition has a real resume point we failed to learn,
198
+ * and stamping from zero would re-issue every used slot (each overstamp
199
+ * evicts the existing data chunk from the reserve). Degrade to read-only
200
+ * and retry instead.
201
+ */
202
+ readFailed?: boolean;
203
+ /**
204
+ * Buckets occupied by the published state chunks (counter chunks + the
205
+ * reference chunk) of the entry that was read. The caller hands these to
206
+ * the stamper so utilisation saves avoid overstamping (and evicting) the
207
+ * published resume point. Present only on a successful full read.
208
+ */
209
+ stateBuckets?: number[];
210
+ /**
211
+ * The N per-chunk refs (sentinels included) the resume state was assembled
212
+ * from — i.e. the reference chunk's contents. The caller seeds these as the
213
+ * lease's `publishedReferences` so its FIRST publish is incremental against
214
+ * the resumed state (only the changed chunk + reference chunk), not a full
215
+ * re-publish. Present on a full read and (best-effort) on a cache hit;
216
+ * undefined when no prior state exists.
217
+ */
218
+ references?: Uint8Array[];
219
+ }>;
220
+ /**
221
+ * Publish the current `localCounter` to the partition-state feed.
222
+ *
223
+ * The counter is split into the same per-`chunkIndex` chunks the utilisation
224
+ * store uses; each is uploaded with a random encryption key to this partition's
225
+ * reserved slot (so it never consumes a data slot), yielding a 64-byte
226
+ * encrypted reference (address‖key). A single "reference chunk" lists those
227
+ * references and is itself uploaded the same way; the feed points at it. A
228
+ * taking-over device follows the feed → reference chunk → counter chunks,
229
+ * decrypting each from the embedded key.
230
+ *
231
+ * INCREMENTAL: pass `previousReferences` (the refs the last publish returned)
232
+ * and `previousCounter` (the counter it published) to re-upload ONLY the chunks
233
+ * that changed, reusing the retained refs for the rest. A typical upload changes
234
+ * the data SOC's bucket plus the previous publish's feed-SOC bucket, so the
235
+ * publish is ~3-4 chunks instead of the whole counter. Omit them (release, or
236
+ * the first publish of a session) for a full publish.
237
+ *
238
+ * PARALLEL: every chunk address is content-derived, so all PUTs (counter chunks
239
+ * + reference chunk + state-pointer SOC) are computed first and fired in a
240
+ * single `Promise.all`. The pointer is written at the current rotating epoch
241
+ * bucket — no feed walk to publish, and the reader computes its address directly.
242
+ */
243
+ export declare function writePartitionState(opts: {
244
+ bee: Bee;
245
+ stamper: Stamper;
246
+ batchId: BatchId;
247
+ batchDepth: number;
248
+ partition: number;
249
+ localCounter: Uint32Array;
250
+ backupSigner: PrivateKey;
251
+ swarmEncryptionKey: Uint8Array;
252
+ /** Refs from the previous publish; with `previousCounter`, enables incremental. */
253
+ previousReferences?: Uint8Array[];
254
+ /** The counter the previous publish wrote; diffed to find changed chunks. */
255
+ previousCounter?: Uint32Array;
256
+ /**
257
+ * This device's id. When provided, the publish also avoids the buckets of
258
+ * this device's current/previous-epoch intent beacons (the refresh tick
259
+ * writes them off-lock at the same reserved slot). Omit for read-only / the
260
+ * deviceId-independent paths — the pointer and occupancy buckets are avoided
261
+ * regardless.
262
+ */
263
+ deviceId?: string;
264
+ /**
265
+ * Wall-clock ms the rotating pointer/beacon rendezvous addresses are anchored
266
+ * to. The lease passes its own clock (`this.now()`, = `Date.now()` in
267
+ * production) so writer, reader, and the collision detector all agree (#385);
268
+ * the reader anchors to the same clock. Defaults to `Date.now()` for bare
269
+ * callers (direct unit tests). Injectable so a test can drive rendezvous
270
+ * deterministically / model per-device skew.
271
+ */
272
+ nowMs?: number;
273
+ }): Promise<{
274
+ /** Reference-chunk reference (hex) — the caller's "synced reference". */
275
+ referenceHex: string;
276
+ /**
277
+ * Buckets occupied by the current state chunks (retained + re-uploaded
278
+ * counter chunks + the reference chunk). The caller hands these to the
279
+ * stamper so subsequent utilisation saves avoid overstamping (evicting) the
280
+ * publish.
281
+ */
282
+ stateBuckets: number[];
283
+ /** The full ref set after this publish, for the caller to cache. */
284
+ references: Uint8Array[];
285
+ /** Snapshot of the counter this publish wrote (pass as `previousCounter` next). */
286
+ publishedCounter: Uint32Array;
287
+ }>;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Whether a session restored from storage may still write.
3
+ *
4
+ * A saved session outlives the page that made it, and a revoke made while no
5
+ * page of this site was open is written only to Swarm: the account's published
6
+ * state is the one place a Remove or a Disconnect from another device can be
7
+ * read. So the frame asks it before a restored session's first write, and an
8
+ * answer that never arrived is `unconfirmed`, never "not revoked".
9
+ */
10
+ import { Bee } from "@ethersphere/bee-js";
11
+ /**
12
+ * - `allowed`: the published account still has this site connected, or the
13
+ * account has never published anything (it has no drive, so it cannot have
14
+ * published a revoke either).
15
+ * - `ended`: the published account removed the site, disconnected it after
16
+ * this session connected, or no longer lists it.
17
+ * - `unconfirmed`: the answer could not be read.
18
+ */
19
+ export type SessionVerdict = "allowed" | "ended" | "unconfirmed";
20
+ export declare function checkRestoredSession(opts: {
21
+ bee: Bee;
22
+ derivationKey: string;
23
+ accountId: string;
24
+ appOrigin: string;
25
+ /** When this session's connection was made. Missing reads as 0, so a
26
+ * disconnect stands rather than being explained away by a clock that does
27
+ * not exist. */
28
+ connectedAt: number | undefined;
29
+ }): Promise<SessionVerdict>;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Store Interfaces for Sync Coordinator
3
+ *
4
+ * These interfaces define the minimal contract that stores must implement
5
+ * to be used with the sync coordinator. This allows the coordinator to be
6
+ * used with different store implementations (Svelte stores, plain objects, etc.)
7
+ */
8
+ import type { EthAddress, BatchId, Stamper } from "@ethersphere/bee-js";
9
+ import type { SyncedAccount, PostageStamp } from "../schemas";
10
+ /**
11
+ * Options for creating a stamper with utilization tracking
12
+ */
13
+ export interface StamperOptions {
14
+ owner: EthAddress;
15
+ encryptionKey: Uint8Array;
16
+ }
17
+ /**
18
+ * Extended stamper interface with optional flush capability
19
+ *
20
+ * Some stampers (like UtilizationAwareStamper) support flushing
21
+ * dirty bucket state to cache and binding to a multi-device partition.
22
+ */
23
+ export interface FlushableStamper extends Stamper {
24
+ flush?(): Promise<void>;
25
+ bindPartition?(opts: {
26
+ partition: number;
27
+ partitionCount: number;
28
+ localCounter: Uint32Array;
29
+ }): void;
30
+ buildLeaseLocalCounter?(): Uint32Array;
31
+ }
32
+ /**
33
+ * Interface for accessing account data. The account is the aggregate root and
34
+ * owns its connected apps and postage stamps inline. The sync coordinator
35
+ * publishes to Swarm, so it sees only the `SyncedAccount` projection — the
36
+ * device-local vault never enters this seam.
37
+ */
38
+ export interface AccountsStoreInterface {
39
+ getAccount(id: EthAddress): SyncedAccount | undefined;
40
+ }
41
+ /**
42
+ * Interface for accessing and managing postage stamp data
43
+ */
44
+ export interface PostageStampsStoreInterface {
45
+ getStamp(batchID: BatchId): PostageStamp | undefined;
46
+ getStamper(batchID: BatchId, options?: StamperOptions): Promise<FlushableStamper | undefined>;
47
+ updateStampUtilization(batchID: BatchId, utilization: number): void;
48
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Sync Account
3
+ *
4
+ * Factory function that creates a sync function for syncing account state
5
+ * to Swarm. This integrates store access, key derivation, and utilization
6
+ * tracking.
7
+ */
8
+ import { Bee } from "@ethersphere/bee-js";
9
+ import type { UtilizationStoreDB } from "../storage/utilization-store";
10
+ import type { DebouncedUtilizationUploader } from "../storage/debounced-uploader";
11
+ import type { AccountsStoreInterface, PostageStampsStoreInterface } from "./store-interfaces";
12
+ import type { SyncResult } from "./types";
13
+ /**
14
+ * Options for creating a sync account function
15
+ */
16
+ export interface SyncAccountOptions {
17
+ /** Bee client for Swarm operations */
18
+ bee: Bee;
19
+ /** Store providing account data (the account owns its apps + stamps) */
20
+ accountsStore: AccountsStoreInterface;
21
+ /** Store providing postage stamp runtime access (stamper/utilization) */
22
+ postageStampsStore: PostageStampsStoreInterface;
23
+ /** Utilization store for browser-based utilization tracking */
24
+ utilizationStore: UtilizationStoreDB;
25
+ /** Debounced uploader for batch utilization state */
26
+ utilizationUploader: DebouncedUtilizationUploader;
27
+ /**
28
+ * Devices heard live on the account bus (`PresenceTracker`), unioned into
29
+ * the partition rival set so this one-shot coordinator and the proxy's agree
30
+ * about who the rivals are. Optional: a caller with no bus has only the
31
+ * registry's recent sign-ins to go on.
32
+ */
33
+ liveDeviceIds?: () => string[];
34
+ }
35
+ /**
36
+ * Sync account function type
37
+ */
38
+ export type SyncAccountFunction = (accountId: string) => Promise<SyncResult | undefined>;
39
+ /**
40
+ * Create a sync account function with dependency-injected stores
41
+ *
42
+ * @param options - Configuration options including stores and optional utilization tracking
43
+ * @returns Function that syncs an account to Swarm
44
+ */
45
+ export declare function createSyncAccount(options: SyncAccountOptions): SyncAccountFunction;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Cross-module timing constants for the multi-device sync path.
3
+ *
4
+ * This module has NO imports on purpose: it is the single source of truth for
5
+ * values several sync modules share, so it can be imported by any of them
6
+ * (including `utils/batch-utilization`) without a circular import. Keeping the
7
+ * values here removes the drift risk of the same literal defined independently
8
+ * in each module.
9
+ */
10
+ /**
11
+ * Client-side timeout for a single cross-device SOC/feed read (roster entry,
12
+ * partition-intent, lock SOC, state-pointer). Bee has no fast authoritative
13
+ * "not found" — a retrieval of an absent chunk fails only after exhausting
14
+ * peers — so an absent target would otherwise block for tens of seconds. A
15
+ * present chunk in its neighborhood retrieves well under this; a timed-out read
16
+ * is treated as inconclusive (see each caller's sentinel handling).
17
+ */
18
+ export declare const SYNC_READ_TIMEOUT_MS = 2500;
19
+ /**
20
+ * Partition-lease lifetime. A device holds its partition for this long before
21
+ * the lease must be refreshed; if it crashes without refreshing, peers can
22
+ * reclaim the partition.
23
+ */
24
+ export declare const LEASE_TTL_MS: number;
25
+ /**
26
+ * Epoch length for the rotating intent/occupancy/state-pointer addresses.
27
+ * TTL-sized (`= LEASE_TTL_MS`) so a contention round and its immediate retries
28
+ * share a bucket (and the previous bucket covers the boundary), while every
29
+ * fresh round rotates to an address no node has cached. The two genuinely track
30
+ * each other — sharing this source keeps them from drifting.
31
+ */
32
+ export declare const INTENT_EPOCH_MS: number;