@fairfox/polly 0.20.1 → 0.22.0

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 (62) hide show
  1. package/README.md +83 -3
  2. package/dist/cli/polly.js +21 -1
  3. package/dist/cli/polly.js.map +3 -3
  4. package/dist/src/background/index.js.map +7 -7
  5. package/dist/src/background/message-router.js.map +7 -7
  6. package/dist/src/elysia/index.d.ts +2 -0
  7. package/dist/src/elysia/index.js +177 -17
  8. package/dist/src/elysia/index.js.map +8 -5
  9. package/dist/src/elysia/peer-repo-plugin.d.ts +79 -0
  10. package/dist/src/elysia/signaling-server-plugin.d.ts +121 -0
  11. package/dist/src/index.d.ts +4 -0
  12. package/dist/src/index.js +90 -1
  13. package/dist/src/index.js.map +15 -13
  14. package/dist/src/mesh.d.ts +29 -0
  15. package/dist/src/mesh.js +1502 -0
  16. package/dist/src/mesh.js.map +22 -0
  17. package/dist/src/peer.d.ts +29 -0
  18. package/dist/src/peer.js +928 -0
  19. package/dist/src/peer.js.map +20 -0
  20. package/dist/src/shared/adapters/index.js.map +6 -6
  21. package/dist/src/shared/lib/_client-only.d.ts +38 -0
  22. package/dist/src/shared/lib/access.d.ts +124 -0
  23. package/dist/src/shared/lib/blob-ref.d.ts +72 -0
  24. package/dist/src/shared/lib/context-helpers.js.map +7 -7
  25. package/dist/src/shared/lib/crdt-specialised.d.ts +129 -0
  26. package/dist/src/shared/lib/crdt-state.d.ts +86 -0
  27. package/dist/src/shared/lib/encryption.d.ts +117 -0
  28. package/dist/src/shared/lib/mesh-network-adapter.d.ts +130 -0
  29. package/dist/src/shared/lib/mesh-signaling-client.d.ts +85 -0
  30. package/dist/src/shared/lib/mesh-state.d.ts +102 -0
  31. package/dist/src/shared/lib/mesh-webrtc-adapter.d.ts +132 -0
  32. package/dist/src/shared/lib/message-bus.js.map +7 -7
  33. package/dist/src/shared/lib/migrate-primitive.d.ts +100 -0
  34. package/dist/src/shared/lib/pairing.d.ts +170 -0
  35. package/dist/src/shared/lib/peer-relay-adapter.d.ts +80 -0
  36. package/dist/src/shared/lib/peer-repo-server.d.ts +83 -0
  37. package/dist/src/shared/lib/peer-state.d.ts +117 -0
  38. package/dist/src/shared/lib/primitive-registry.d.ts +88 -0
  39. package/dist/src/shared/lib/resource.js.map +4 -4
  40. package/dist/src/shared/lib/revocation.d.ts +126 -0
  41. package/dist/src/shared/lib/schema-version.d.ts +129 -0
  42. package/dist/src/shared/lib/signing.d.ts +118 -0
  43. package/dist/src/shared/lib/state.js.map +4 -4
  44. package/dist/src/shared/state/app-state.js.map +5 -5
  45. package/dist/tools/init/src/cli.js.map +1 -1
  46. package/dist/tools/quality/src/cli.js +162 -0
  47. package/dist/tools/quality/src/cli.js.map +11 -0
  48. package/dist/tools/test/src/adapters/index.js.map +2 -2
  49. package/dist/tools/test/src/browser/harness.d.ts +80 -0
  50. package/dist/tools/test/src/browser/index.d.ts +32 -0
  51. package/dist/tools/test/src/browser/index.js +243 -0
  52. package/dist/tools/test/src/browser/index.js.map +10 -0
  53. package/dist/tools/test/src/browser/run.d.ts +26 -0
  54. package/dist/tools/test/src/index.js.map +2 -2
  55. package/dist/tools/verify/specs/tla/MeshState.cfg +21 -0
  56. package/dist/tools/verify/specs/tla/MeshState.tla +247 -0
  57. package/dist/tools/verify/specs/tla/PeerState.cfg +27 -0
  58. package/dist/tools/verify/specs/tla/PeerState.tla +238 -0
  59. package/dist/tools/verify/specs/tla/README.md +27 -3
  60. package/dist/tools/verify/src/cli.js.map +8 -8
  61. package/dist/tools/visualize/src/cli.js.map +7 -7
  62. package/package.json +51 -5
@@ -0,0 +1,170 @@
1
+ /**
2
+ * pairing — Phase 2 first-cut pairing flow for $meshState.
3
+ *
4
+ * Two devices that want to share a $meshState document must exchange three
5
+ * things before sync can begin: the issuer's Ed25519 signing public key
6
+ * (so the receiver can verify ops authored by the issuer), the symmetric
7
+ * document encryption key (so both sides can encrypt and decrypt the
8
+ * shared document), and the issuer's stable peer id (so the receiver
9
+ * knows which entry in its keyring the public key belongs to). This
10
+ * module packs all three into a {@link PairingToken}, serialises it to a
11
+ * compact binary format suitable for QR codes or copy-paste, and provides
12
+ * the matching parse-and-apply flow on the receiving side.
13
+ *
14
+ * Threat model: pairing tokens are transmitted over an out-of-band channel
15
+ * that the user can authenticate visually — typically a QR code on the
16
+ * issuer's device, scanned by the receiver. Because anyone with the token
17
+ * can decrypt and impersonate, the OOB channel is the only authentication.
18
+ * The token includes a TTL (default 10 minutes) so that a token displayed
19
+ * briefly and then dismissed cannot be replayed by an attacker who later
20
+ * gains access to a screenshot. A production deployment would layer a
21
+ * Short Authentication String (SAS) on top — both devices display a code
22
+ * derived from the shared state, and the user verifies they match — but
23
+ * that is a follow-up.
24
+ *
25
+ * The pairing flow is one-way in the Phase 2 first cut. The issuer
26
+ * generates a token and displays it; the receiver applies it and picks
27
+ * up the issuer's keys. The receiver's own keys reach the issuer through
28
+ * the access set: when the receiver sends its first signed op, the issuer
29
+ * records the receiver's public key alongside its peer id and adds it to
30
+ * the keyring. A bidirectional pairing flow that exchanges both sides'
31
+ * keys in a single QR exchange is straightforward to add later but adds
32
+ * UX surface area that is not needed for the mesh transport to work.
33
+ */
34
+ import type { MeshKeyring } from "./mesh-network-adapter";
35
+ import { type SigningKeyPair } from "./signing";
36
+ /** Current pairing-token format version. Bumped if the wire format changes. */
37
+ export declare const PAIRING_TOKEN_VERSION = 1;
38
+ /** Magic header bytes for sanity-checking parsed tokens. ASCII "PPT1". */
39
+ export declare const PAIRING_TOKEN_MAGIC: Uint8Array<ArrayBuffer>;
40
+ /** Length of the random nonce embedded in every token. */
41
+ export declare const PAIRING_NONCE_BYTES = 16;
42
+ /** Default TTL applied when {@link createPairingToken} is called without an
43
+ * explicit `ttlMs` option. */
44
+ export declare const DEFAULT_PAIRING_TTL_MS: number;
45
+ /**
46
+ * The contents of a pairing token. Both sides operate on this shape; the
47
+ * binary serialisation is purely for transport.
48
+ */
49
+ export interface PairingToken {
50
+ /** Format version. {@link PAIRING_TOKEN_VERSION} at the time of writing. */
51
+ version: number;
52
+ /** Stable peer id of the issuing device. The receiver records this as
53
+ * the lookup key for the issuer's public key in its keyring. */
54
+ issuerPeerId: string;
55
+ /** Issuer's Ed25519 signing public key (32 bytes). */
56
+ issuerPublicKey: Uint8Array;
57
+ /** Shared document encryption key (32 bytes). The receiver stores this
58
+ * under {@link documentKeyId} in its keyring. */
59
+ documentKey: Uint8Array;
60
+ /** Identifier under which the receiver stores the document key. For the
61
+ * Phase 2 first cut this is typically the well-known DEFAULT_MESH_KEY_ID
62
+ * from mesh-network-adapter; per-document keys (one entry per Automerge
63
+ * document) are a follow-up. */
64
+ documentKeyId: string;
65
+ /** Unix timestamp (milliseconds) after which the token is considered
66
+ * expired and {@link applyPairingToken} refuses to use it. */
67
+ expiresAt: number;
68
+ /** 16-byte random nonce. Carried through serialisation so two tokens
69
+ * with otherwise-identical contents are still distinguishable. */
70
+ nonce: Uint8Array;
71
+ }
72
+ /** Errors thrown by the pairing subsystem. */
73
+ export declare class PairingError extends Error {
74
+ readonly code: "expired" | "wrong-magic" | "unknown-version" | "truncated" | "invalid-public-key" | "invalid-document-key" | "invalid-nonce";
75
+ constructor(message: string, code: PairingError["code"]);
76
+ }
77
+ /**
78
+ * Options for {@link createPairingToken}. The signing identity and the
79
+ * document key are required; everything else is optional with sensible
80
+ * defaults.
81
+ */
82
+ export interface CreatePairingTokenOptions {
83
+ /** The issuing device's signing keypair. Only the public key ends up in
84
+ * the token; the secret never leaves the issuer. */
85
+ identity: SigningKeyPair;
86
+ /** Stable peer id for the issuing device. */
87
+ issuerPeerId: string;
88
+ /** The symmetric document key the receiver should adopt. If omitted, a
89
+ * fresh key is generated and the caller is responsible for using the
90
+ * same key on the issuing side too. */
91
+ documentKey?: Uint8Array;
92
+ /** Identifier under which the receiver stores the document key. */
93
+ documentKeyId: string;
94
+ /** Time-to-live in milliseconds. Defaults to {@link DEFAULT_PAIRING_TTL_MS}. */
95
+ ttlMs?: number;
96
+ /** Override the current time. Intended for tests; production code should
97
+ * not pass this. */
98
+ now?: () => number;
99
+ }
100
+ /**
101
+ * Generate a fresh {@link PairingToken}. The token is ready to be
102
+ * serialised and displayed to the receiver via an OOB channel.
103
+ */
104
+ export declare function createPairingToken(options: CreatePairingTokenOptions): PairingToken;
105
+ /**
106
+ * Generate a fresh pairing token *and* a fresh signing keypair in one call.
107
+ * Convenience for first-time setup where the device has no existing
108
+ * identity yet. Returns both so the caller can persist the keypair and
109
+ * then display the token.
110
+ */
111
+ export declare function createPairingTokenWithFreshIdentity(args: {
112
+ issuerPeerId: string;
113
+ documentKeyId: string;
114
+ ttlMs?: number;
115
+ now?: () => number;
116
+ }): {
117
+ identity: SigningKeyPair;
118
+ token: PairingToken;
119
+ };
120
+ /**
121
+ * Check whether a token has expired against the current wall-clock time
122
+ * (or an injected `now`).
123
+ */
124
+ export declare function isPairingTokenExpired(token: PairingToken, now?: () => number): boolean;
125
+ /**
126
+ * Apply a parsed and validated token to a {@link MeshKeyring}. Mutates the
127
+ * keyring in place: adds the issuer's public key to {@link MeshKeyring.knownPeers}
128
+ * and the document key to {@link MeshKeyring.documentKeys}.
129
+ *
130
+ * Throws {@link PairingError} with code "expired" if the token's TTL has
131
+ * elapsed. The receiver is expected to apply the token promptly after
132
+ * scanning; rejecting expired tokens prevents replay of long-lived
133
+ * captures.
134
+ */
135
+ export declare function applyPairingToken(token: PairingToken, keyring: MeshKeyring, options?: {
136
+ now?: () => number;
137
+ }): void;
138
+ /**
139
+ * Serialise a token to a binary blob. The wire format is:
140
+ *
141
+ * [4 bytes: magic "PPT1"]
142
+ * [1 byte: version]
143
+ * [4 bytes BE: issuer id byte length]
144
+ * [N bytes: issuer id UTF-8]
145
+ * [32 bytes: issuer public key]
146
+ * [32 bytes: document key]
147
+ * [4 bytes BE: document key id byte length]
148
+ * [M bytes: document key id UTF-8]
149
+ * [8 bytes BE: expiresAt (uint64 milliseconds)]
150
+ * [16 bytes: nonce]
151
+ *
152
+ * Use {@link encodePairingToken} to round-trip through a base64 string.
153
+ */
154
+ export declare function serialisePairingToken(token: PairingToken): Uint8Array;
155
+ /**
156
+ * Inverse of {@link serialisePairingToken}. Throws {@link PairingError} on
157
+ * malformed input.
158
+ */
159
+ export declare function parsePairingToken(bytes: Uint8Array): PairingToken;
160
+ /**
161
+ * Serialise a token and base64-encode it for QR-code or copy-paste display.
162
+ * The encoding uses the standard base64 alphabet (not URL-safe) because
163
+ * QR codes encode bytes directly and do not care about URL safety.
164
+ */
165
+ export declare function encodePairingToken(token: PairingToken): string;
166
+ /**
167
+ * Decode a base64-encoded pairing token produced by {@link encodePairingToken}.
168
+ * Throws {@link PairingError} on malformed input.
169
+ */
170
+ export declare function decodePairingToken(encoded: string): PairingToken;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * peer-relay-adapter — Phase 1 client helper for connecting a Polly $peerState
3
+ * application to an Automerge-Repo relay server over WebSocket.
4
+ *
5
+ * The Phase 0 base $crdtState and the Phase 1 $peerState wrapper both consume
6
+ * a caller-supplied `Repo` via `configurePeerState`. This module provides the
7
+ * one-call factory that builds a Repo wired to the relay transport: a
8
+ * `WebSocketClientAdapter` from `@automerge/automerge-repo-network-websocket`
9
+ * pointed at the server URL, an `IndexedDBStorageAdapter` for client-side
10
+ * persistence, and a Polly-shaped connection-state signal that the application
11
+ * can render as a diagnostic UI or feed into reconnection logic.
12
+ *
13
+ * The mirror server-side factory is in {@link peer-repo-server}.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { configurePeerState } from "@fairfox/polly";
18
+ * import { createPeerStateClient } from "@fairfox/polly";
19
+ *
20
+ * const { repo, connectionState } = await createPeerStateClient({
21
+ * url: "wss://yourapp.example.com/polly/peer",
22
+ * });
23
+ * configurePeerState(repo);
24
+ *
25
+ * // connectionState is a Signal<"connecting" | "connected" | "disconnected">
26
+ * ```
27
+ */
28
+ import { Repo } from "@automerge/automerge-repo";
29
+ import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
30
+ import { type Signal } from "@preact/signals";
31
+ import { type MeshKeyring } from "./mesh-network-adapter";
32
+ export type PeerRelayConnectionState = "connecting" | "connected" | "disconnected";
33
+ export interface CreatePeerStateClientOptions {
34
+ /** WebSocket URL of the Polly peer-relay server. Use `ws://` for local
35
+ * development and `wss://` for any deployment that terminates TLS. */
36
+ url: string;
37
+ /** Reconnect interval in milliseconds. Defaults to Automerge-Repo's own
38
+ * default (5 seconds at the time of writing). */
39
+ retryInterval?: number;
40
+ /** Optional storage adapter. Applications running in a browser typically
41
+ * pass an `IndexedDBStorageAdapter`; tests pass nothing for a local-only
42
+ * Repo. The default is no storage, which keeps the client purely in-memory. */
43
+ storage?: ConstructorParameters<typeof Repo>[0] extends infer C ? C extends {
44
+ storage?: infer S;
45
+ } ? S : never : never;
46
+ /** Enable Ed25519 signing on every sync message. Adds Byzantine defence:
47
+ * a compromised client cannot push unsigned writes through the relay.
48
+ * Requires a keyring with the local peer's signing identity and the
49
+ * public keys of peers whose ops should be accepted. The server can
50
+ * still read and mutate document contents because the payload is
51
+ * signed, not encrypted. */
52
+ sign?: boolean;
53
+ /** Keyring for the signing layer. Required when `sign` is true. */
54
+ keyring?: MeshKeyring;
55
+ }
56
+ export interface PeerStateClient {
57
+ /** A configured Repo backed by the WebSocket relay. Pass to
58
+ * {@link configurePeerState}. */
59
+ repo: Repo;
60
+ /** Reactive connection state. Updates as the underlying WebSocket opens,
61
+ * closes, and reconnects. */
62
+ connectionState: Signal<PeerRelayConnectionState>;
63
+ /** The underlying network adapter, exposed for advanced use. */
64
+ adapter: WebSocketClientAdapter;
65
+ /** True if the client was constructed with `sign: true`. Used by
66
+ * $peerState primitives to validate per-primitive sign options. */
67
+ signEnabled: boolean;
68
+ /** Disconnect from the relay and tear down the Repo. Awaiting the
69
+ * returned promise drains the Repo's subsystems cleanly. */
70
+ close: () => Promise<void>;
71
+ }
72
+ /**
73
+ * Construct a Polly-flavoured client for the peer-relay transport.
74
+ *
75
+ * The returned object includes the Repo, a connection-state signal, the
76
+ * underlying network adapter, and a close function. Production code typically
77
+ * passes the Repo to {@link configurePeerState} and renders the connection
78
+ * state somewhere visible.
79
+ */
80
+ export declare function createPeerStateClient(options: CreatePeerStateClientOptions): PeerStateClient;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * peer-repo-server — Phase 1 server-side factory for the Polly peer-relay
3
+ * transport. Constructs an Automerge-Repo `Repo` wired to a WebSocket server
4
+ * and a NodeFS storage backend, ready to relay sync messages between
5
+ * connected $peerState clients.
6
+ *
7
+ * The "always-on peer" role for $peerState lives here. The server holds a
8
+ * full Automerge replica of every document, participates in the sync protocol
9
+ * as an ordinary peer, and persists state to disk so the next process restart
10
+ * picks up where the previous one left off. Server-side cron, HTTP handlers,
11
+ * and other compute can open document handles on the returned Repo and mutate
12
+ * them; mutations propagate to connected clients through the same sync
13
+ * protocol that handles client-to-client traffic.
14
+ *
15
+ * The plan originally called this an "Elysia plugin," but Automerge's
16
+ * `WebSocketServerAdapter` requires an `isomorphic-ws` `WebSocketServer`
17
+ * instance — not Elysia's native WebSocket — so the cleanest first cut is a
18
+ * standalone factory that runs its own `ws` server. Elysia integration for
19
+ * authenticated upgrades is a Phase 1.1 follow-up that wraps this factory.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import { createPeerRepoServer } from "@fairfox/polly";
24
+ *
25
+ * const server = await createPeerRepoServer({
26
+ * port: 3030,
27
+ * storagePath: "./data/polly-peer",
28
+ * });
29
+ *
30
+ * // Open a document handle on the server's Repo for cron or compute work.
31
+ * const handle = server.repo.create({ counter: 0 });
32
+ *
33
+ * // On shutdown:
34
+ * await server.close();
35
+ * ```
36
+ */
37
+ import { Repo } from "@automerge/automerge-repo";
38
+ import { WebSocketServerAdapter } from "@automerge/automerge-repo-network-websocket";
39
+ import { NodeFSStorageAdapter } from "@automerge/automerge-repo-storage-nodefs";
40
+ import * as ws from "ws";
41
+ type WebSocketServer = ws.WebSocketServer;
42
+ export interface CreatePeerRepoServerOptions {
43
+ /** Port to listen on. The factory creates its own `WebSocketServer` and
44
+ * binds to this port. */
45
+ port: number;
46
+ /** Filesystem directory for the NodeFS storage adapter. The directory is
47
+ * created on demand. Defaults to `./automerge-repo-data` (Automerge's own
48
+ * default). */
49
+ storagePath?: string;
50
+ /** Hostname interface to bind to. Defaults to all interfaces. */
51
+ host?: string;
52
+ /** Override the `WebSocketServer` instance entirely. When provided, `port`
53
+ * and `host` are ignored and the caller is responsible for the lifecycle.
54
+ * Useful for tests that want to bind to a random port. */
55
+ webSocketServer?: WebSocketServer;
56
+ }
57
+ export interface PeerRepoServer {
58
+ /** A configured Repo participating as the always-on peer. Server-side
59
+ * cron and HTTP handlers can open document handles on this directly. */
60
+ repo: Repo;
61
+ /** The underlying WebSocket server. Exposed for advanced use such as
62
+ * health checks or graceful shutdown coordination. */
63
+ webSocketServer: WebSocketServer;
64
+ /** The Automerge network adapter wrapping the WebSocket server. */
65
+ adapter: WebSocketServerAdapter;
66
+ /** The NodeFS storage adapter writing to {@link CreatePeerRepoServerOptions.storagePath}. */
67
+ storage: NodeFSStorageAdapter;
68
+ /** Tear down the server: disconnect peers, flush storage, close the
69
+ * underlying WebSocket server. Returns a promise that resolves once the
70
+ * tear-down is complete. */
71
+ close: () => Promise<void>;
72
+ }
73
+ /**
74
+ * Construct a Polly peer-relay server. Returns a Repo that participates as
75
+ * the always-on peer, the underlying WebSocket server and storage adapter
76
+ * for advanced use, and a close function for orderly shutdown.
77
+ *
78
+ * Applications typically call this once at startup, hold the returned
79
+ * `repo` reference for cron and compute work, and wire the close function
80
+ * into their process shutdown signal handlers.
81
+ */
82
+ export declare function createPeerRepoServer(options: CreatePeerRepoServerOptions): Promise<PeerRepoServer>;
83
+ export {};
@@ -0,0 +1,117 @@
1
+ /**
2
+ * peer-state — Phase 1 wrappers exposing $peerState, $peerText, $peerCounter,
3
+ * and $peerList. These are the application-facing constructors for the middle
4
+ * resilience tier in RFC-041: every device is a full Automerge replica, the
5
+ * server included, and server-side code can read and mutate document contents
6
+ * because the server participates in the data plane as an ordinary peer.
7
+ *
8
+ * Each primitive wraps the corresponding Phase 0 base ($crdtState, $crdtText,
9
+ * $crdtCounter, $crdtList) with three additions:
10
+ *
11
+ * 1. The `primitive` label is hard-coded to "peerState" so the
12
+ * primitive-registry collision detection knows which family the key
13
+ * belongs to.
14
+ *
15
+ * 2. A handle factory that resolves the application's logical key to an
16
+ * Automerge DocumentId via a per-Repo key map. The first time a key is
17
+ * registered, the factory creates a new document on the configured Repo
18
+ * and records the mapping. On subsequent constructions of the same key,
19
+ * the factory looks up the existing DocumentId and finds the handle.
20
+ *
21
+ * 3. The `sign` option field validates that the configured Repo was
22
+ * created with signing enabled (via createPeerStateClient with
23
+ * `sign: true`). Signing adds Byzantine defence at the transport
24
+ * level without preventing the server from reading document
25
+ * contents. Encryption is not offered on $peerState because it
26
+ * would prevent the server from participating as an Automerge
27
+ * peer; applications that want encrypted state should use $meshState.
28
+ *
29
+ * The Repo itself is supplied by the application via {@link configurePeerState}
30
+ * or per-call via the `repo` option. There is no transport in this Phase 1
31
+ * cut — applications use a local-only Repo and document operations stay
32
+ * inside the calling process. Phase 1's WebSocket relay adapter will plug in
33
+ * via the same configuration path; Phase 2's mesh adapter does the same for
34
+ * $meshState.
35
+ */
36
+ import type { Repo } from "@automerge/automerge-repo";
37
+ import type { Access } from "./access";
38
+ import { type SpecialisedPrimitive } from "./crdt-specialised";
39
+ import { type CrdtPrimitive } from "./crdt-state";
40
+ import type { Migrations, VersionedDoc } from "./schema-version";
41
+ /** Common option shape across all four $peer* primitives. */
42
+ export interface PeerStateOptions<T> {
43
+ /** Override the default Repo for this primitive. Useful for tests and for
44
+ * applications that maintain multiple Repos (rare). */
45
+ repo?: Repo;
46
+ /** Request per-op Ed25519 signing for this primitive. Signing is a
47
+ * transport-level concern: pass `sign: true` to `createPeerStateClient`
48
+ * to enable it for all primitives on that Repo. Passing `sign: true`
49
+ * here validates that the configured Repo was created with signing
50
+ * enabled and throws if it was not. */
51
+ sign?: boolean;
52
+ /** Schema version target for the application. Migrations run on load. */
53
+ schemaVersion?: number;
54
+ /** Migration table keyed by target version. Required if schemaVersion is set. */
55
+ migrations?: Migrations;
56
+ /** Declarative read/write access. Compiled into a server share policy
57
+ * once the relay transport is wired in. */
58
+ access?: Access;
59
+ /** Initial value used when this primitive's key has not been registered
60
+ * before. Phase 0 callers passed this positionally; Phase 1 application
61
+ * code does the same. */
62
+ initialValue?: T;
63
+ }
64
+ /**
65
+ * Set the default Repo that the $peer* primitives use when no `repo` option
66
+ * is supplied. Calling this with a new Repo clears the per-Repo key map so
67
+ * that tests start each scenario with a fresh document space.
68
+ *
69
+ * Production code typically calls this once at application startup with a
70
+ * Repo configured for the relay transport. Tests call it before each scenario
71
+ * with an in-memory Repo.
72
+ */
73
+ export declare function configurePeerState(repo: Repo, options?: {
74
+ signEnabled?: boolean;
75
+ }): void;
76
+ /**
77
+ * Reset the peer-state subsystem to its initial unconfigured state. Intended
78
+ * for tests; production code should not call this.
79
+ */
80
+ export declare function resetPeerState(): void;
81
+ /**
82
+ * Create a peer-replicated state primitive backed by Automerge. Every device
83
+ * holds a full replica; the server, when one is configured via the relay
84
+ * transport, holds one too and participates in the sync protocol as an
85
+ * ordinary peer. Server-side code can read and mutate document contents.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * const settings = $peerState<Settings>("settings", { theme: "dark" });
90
+ * await settings.loaded;
91
+ * settings.value = { theme: "light" };
92
+ * ```
93
+ */
94
+ export declare function $peerState<T extends VersionedDoc>(key: string, initialValue: T, options?: PeerStateOptions<T>): CrdtPrimitive<T>;
95
+ export interface PeerTextOptions extends Omit<PeerStateOptions<unknown>, "initialValue"> {
96
+ }
97
+ /**
98
+ * Create a peer-replicated text primitive. Concurrent character-level edits
99
+ * from peers merge cleanly via Automerge's updateText splicing.
100
+ */
101
+ export declare function $peerText(key: string, initialValue: string, options?: PeerTextOptions): SpecialisedPrimitive<string>;
102
+ export interface PeerCounterOptions extends Omit<PeerStateOptions<unknown>, "initialValue"> {
103
+ }
104
+ /**
105
+ * Create a peer-replicated counter primitive. Concurrent increments from
106
+ * peers commute, so two clients each adding 1 to a counter at 5 produce a
107
+ * counter at 7 after merging.
108
+ */
109
+ export declare function $peerCounter(key: string, initialValue: number, options?: PeerCounterOptions): SpecialisedPrimitive<number>;
110
+ export interface PeerListOptions extends Omit<PeerStateOptions<unknown>, "initialValue"> {
111
+ }
112
+ /**
113
+ * Create a peer-replicated list primitive. The Phase 0 base uses naive
114
+ * whole-array replacement; Phase 1.1 will refine the write path with
115
+ * structural diff-to-splice for concurrent insert/remove preservation.
116
+ */
117
+ export declare function $peerList<T>(key: string, initialValue: T[], options?: PeerListOptions): SpecialisedPrimitive<T[]>;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * PrimitiveRegistry — runtime namespace collision detection across Polly's
3
+ * synced state primitives.
4
+ *
5
+ * The three primitive families ($sharedState, $peerState, $meshState) each store
6
+ * data under a developer-chosen logical key. If two different primitives both
7
+ * claim the same key, the developer almost certainly has a bug: the on-disk
8
+ * formats are incompatible, no sync happens between them, and whichever primitive
9
+ * resolves first silently "wins" from the developer's perspective. By the time
10
+ * the mistake is noticed, data has diverged.
11
+ *
12
+ * The registry catches the mistake at the first mismatched registration and
13
+ * throws a structured error naming the key, both primitives, and (when available)
14
+ * the call site of each registration. This is run-to-failure by design: a
15
+ * collision is always a bug, and the failure should be loud.
16
+ *
17
+ * Same primitive re-registering the same key is allowed and is a no-op — it
18
+ * supports hot module reloading and component re-mounts without spurious errors.
19
+ * Changing the primitive kind of an existing key is still an error; developers
20
+ * doing that during local HMR should hard-reload to reset the registry.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * primitiveRegistry.register("notes", "sharedState", "src/app.ts:10");
25
+ * primitiveRegistry.register("notes", "peerState", "src/other.ts:22");
26
+ * // throws PrimitiveCollisionError — names both primitives and both call sites
27
+ * ```
28
+ */
29
+ /**
30
+ * Canonical identifiers for Polly's synced state primitives. The registry
31
+ * uses these as opaque labels; nothing else in Polly needs to match them.
32
+ */
33
+ export type PrimitiveKind = "sharedState" | "syncedState" | "persistedState" | "state" | "peerState" | "meshState";
34
+ /**
35
+ * Thrown when a logical key is registered under more than one primitive.
36
+ * The message names the key, both primitives, and (when available) the
37
+ * call site of each registration, so the developer can navigate to both
38
+ * sites from the error output.
39
+ */
40
+ export declare class PrimitiveCollisionError extends Error {
41
+ readonly key: string;
42
+ readonly firstPrimitive: PrimitiveKind;
43
+ readonly firstCallSite: string | undefined;
44
+ readonly secondPrimitive: PrimitiveKind;
45
+ readonly secondCallSite: string | undefined;
46
+ constructor(key: string, firstPrimitive: PrimitiveKind, firstCallSite: string | undefined, secondPrimitive: PrimitiveKind, secondCallSite: string | undefined);
47
+ }
48
+ /**
49
+ * A small Map-backed registry of "logical key → primitive kind". Exported as
50
+ * a class so tests can construct fresh instances without sharing state; the
51
+ * module-level {@link primitiveRegistry} singleton is what application code
52
+ * actually uses.
53
+ */
54
+ export declare class PrimitiveRegistry {
55
+ private readonly entries;
56
+ /**
57
+ * Register a key under a primitive kind. Re-registering the same key under
58
+ * the same primitive is a no-op, which is what hot module reloading and
59
+ * component re-mounts produce.
60
+ *
61
+ * @throws {PrimitiveCollisionError} if the key is already registered under
62
+ * a different primitive kind.
63
+ */
64
+ register(key: string, primitive: PrimitiveKind, callSite?: string): void;
65
+ /**
66
+ * True if the key has been registered (under any primitive kind).
67
+ */
68
+ has(key: string): boolean;
69
+ /**
70
+ * Look up the primitive kind a key is registered under, if any.
71
+ * Returns undefined for unregistered keys.
72
+ */
73
+ kindOf(key: string): PrimitiveKind | undefined;
74
+ /**
75
+ * Drop every registration. Intended for test setup and teardown; application
76
+ * code should not call this.
77
+ */
78
+ clear(): void;
79
+ /**
80
+ * Number of registered keys. Intended for tests.
81
+ */
82
+ get size(): number;
83
+ }
84
+ /**
85
+ * The process-wide primitive registry. Application code registers here
86
+ * implicitly via primitive constructors; tests can reset it with `clear()`.
87
+ */
88
+ export declare const primitiveRegistry: PrimitiveRegistry;