@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,129 @@
1
+ /**
2
+ * crdt-specialised — text, counter, and list variants for Polly's peer-first
3
+ * state primitives.
4
+ *
5
+ * Where the base $crdtState binds a signal to a whole Automerge document with
6
+ * naive top-level structural assignment, the specialised variants each own a
7
+ * single field inside their document and use type-specific Automerge
8
+ * operations to mutate it. This is the difference between "your write
9
+ * survives a concurrent edit" and "your write clobbers the other peer's
10
+ * edit," and it matters most for text editing where every keystroke from a
11
+ * concurrent peer would otherwise be lost.
12
+ *
13
+ * - **$crdtText** stores its value in `doc.text` and writes via
14
+ * `Automerge.updateText`, which computes the minimal sequence of character
15
+ * splices between the previous and new strings and records each one as a
16
+ * CRDT operation. Concurrent text edits merge.
17
+ *
18
+ * - **$crdtCounter** stores its value in `doc.count` as an `Automerge.Counter`
19
+ * instance and writes via `counter.increment(delta)`. Increments commute
20
+ * across peers, so two clients each adding 1 to a counter at 5 produce a
21
+ * counter at 7 after merging — last-write-wins semantics on a plain number
22
+ * would lose one of the increments.
23
+ *
24
+ * - **$crdtList** stores its value in `doc.items` and, for the Phase 0 cut,
25
+ * uses naive whole-array replacement on every write. This is correct for
26
+ * single-writer scenarios and good enough to exercise the rest of the
27
+ * pipeline. Phase 1 will replace the write path with proper diff-to-splice
28
+ * logic so that concurrent inserts and removes preserve list ordering.
29
+ *
30
+ * All three variants share a single internal factory that handles the
31
+ * primitive-registry guard, migration-registry guard, schema-version
32
+ * migration, hydration promise, two-way binding with an `updating` flag,
33
+ * and the {@link MigratableState} interface. Variants differ only in the
34
+ * `extractValue` and `applyWrite` hooks they pass.
35
+ */
36
+ import { Counter, type DocHandle } from "@automerge/automerge-repo";
37
+ import type { Access } from "./access";
38
+ import { type MigratableState } from "./migrate-primitive";
39
+ import { type PrimitiveKind } from "./primitive-registry";
40
+ import { type Migrations, type VersionedDoc } from "./schema-version";
41
+ /**
42
+ * Public interface for a specialised primitive. The signal value type V is
43
+ * not the same as the underlying Automerge document — for example, $crdtText
44
+ * exposes a string signal whose value lives at `doc.text` inside the
45
+ * document. Implements {@link MigratableState} so the cross-primitive
46
+ * migration helper can consume it directly.
47
+ */
48
+ export interface SpecialisedPrimitive<V> extends MigratableState<V> {
49
+ readonly key: string;
50
+ readonly primitive: PrimitiveKind;
51
+ value: V;
52
+ readonly loaded: Promise<void>;
53
+ readonly handle: DocHandle<unknown> | undefined;
54
+ }
55
+ /** The Automerge document shape backing a $crdtText primitive. */
56
+ export type TextDoc = VersionedDoc & {
57
+ text?: string;
58
+ };
59
+ /** Options for {@link $crdtText}. */
60
+ export interface CrdtTextOptions {
61
+ /** Primitive kind label. Defaults to "peerState"; Phase 2's $meshState
62
+ * wrapper passes "meshState" instead. */
63
+ primitive?: PrimitiveKind;
64
+ /** Async factory that returns a ready DocHandle for the text document. */
65
+ getHandle: () => Promise<DocHandle<TextDoc>>;
66
+ schemaVersion?: number;
67
+ migrations?: Migrations;
68
+ access?: Access;
69
+ callSite?: string;
70
+ }
71
+ /**
72
+ * Create a CRDT-backed text primitive. The signal exposes a plain string;
73
+ * writes are diffed into character-level splices via `Automerge.updateText`
74
+ * so that concurrent edits from peers merge cleanly rather than clobbering
75
+ * each other.
76
+ */
77
+ export declare function $crdtText(key: string, initialValue: string, options: CrdtTextOptions): SpecialisedPrimitive<string>;
78
+ /** The Automerge document shape backing a $crdtCounter primitive. */
79
+ export type CounterDoc = VersionedDoc & {
80
+ count?: Counter;
81
+ };
82
+ /** Options for {@link $crdtCounter}. */
83
+ export interface CrdtCounterOptions {
84
+ primitive?: PrimitiveKind;
85
+ getHandle: () => Promise<DocHandle<CounterDoc>>;
86
+ schemaVersion?: number;
87
+ migrations?: Migrations;
88
+ access?: Access;
89
+ callSite?: string;
90
+ }
91
+ /**
92
+ * Create a CRDT-backed counter primitive. The signal exposes a plain number;
93
+ * writes compute the delta from the document's current value and call
94
+ * `counter.increment(delta)` on the underlying `Automerge.Counter`. Concurrent
95
+ * increments from peers commute, so two clients each adding 1 to a counter at
96
+ * 5 produce a counter at 7 after merging.
97
+ *
98
+ * Application code that wants to express increments idiomatically can write
99
+ * `counter.value = counter.value + 1`; the signal's reactivity captures the
100
+ * read-then-write pattern and the factory translates it into a proper CRDT
101
+ * increment operation underneath.
102
+ */
103
+ export declare function $crdtCounter(key: string, initialValue: number, options: CrdtCounterOptions): SpecialisedPrimitive<number>;
104
+ /** The Automerge document shape backing a $crdtList primitive. */
105
+ export type ListDoc<T> = VersionedDoc & {
106
+ items?: T[];
107
+ };
108
+ /** Options for {@link $crdtList}. */
109
+ export interface CrdtListOptions<T> {
110
+ primitive?: PrimitiveKind;
111
+ getHandle: () => Promise<DocHandle<ListDoc<T>>>;
112
+ schemaVersion?: number;
113
+ migrations?: Migrations;
114
+ access?: Access;
115
+ callSite?: string;
116
+ }
117
+ /**
118
+ * Create a CRDT-backed list primitive. The signal exposes a plain array;
119
+ * for the Phase 0 cut, writes replace the underlying array wholesale inside
120
+ * an `Automerge.change` block. This is correct for single-writer scenarios
121
+ * and is the simplest way to ship a working list primitive on the same
122
+ * pipeline as text and counter.
123
+ *
124
+ * Phase 1 will replace the write path with proper structural diffing into
125
+ * insert and remove operations so that concurrent edits from peers preserve
126
+ * list ordering. Until then, applications using $crdtList in a multi-writer
127
+ * setting should expect last-writer-wins semantics on the array as a whole.
128
+ */
129
+ export declare function $crdtList<T>(key: string, initialValue: T[], options: CrdtListOptions<T>): SpecialisedPrimitive<T[]>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * crdt-state — base machinery for Polly's peer-first state primitives.
3
+ *
4
+ * This module is transport-agnostic: it takes a caller-supplied async factory
5
+ * that produces a ready {@link DocHandle}, binds it bidirectionally to a
6
+ * Preact signal, runs any pending schema migrations on load, and integrates
7
+ * with the primitive-registry and migration-registry guards. Phase 1's
8
+ * $peerState and Phase 2's $meshState both construct these base primitives
9
+ * with their own handle factories — one over Automerge-Repo's WebSocket
10
+ * client adapter, the other over WebRTC — and the base never knows which.
11
+ *
12
+ * The signal-to-handle binding uses an `updating` guard flag to prevent write
13
+ * loops: when a local signal assignment runs the effect that pushes the value
14
+ * into `handle.change`, the flag is raised so that the 'change' event the
15
+ * handle fires back is ignored. The same flag protects in the other direction
16
+ * when a remote change seeds the signal.
17
+ *
18
+ * For the Phase 0 cut, writes are applied with a naive top-level structural
19
+ * replacement inside the `Automerge.change` block. This is correct for
20
+ * JSON-shaped documents with scalar and flat-object fields and is good enough
21
+ * to exercise the rest of the pipeline. The specialised variants for text,
22
+ * counters, and lists (which require type-specific operation capture to
23
+ * preserve concurrent-edit semantics) land in Phase 1's crdt-specialised.ts.
24
+ */
25
+ import type { DocHandle } from "@automerge/automerge-repo";
26
+ import type { Access } from "./access";
27
+ import { type MigratableState } from "./migrate-primitive";
28
+ import { type PrimitiveKind } from "./primitive-registry";
29
+ import { type Migrations, type VersionedDoc } from "./schema-version";
30
+ /**
31
+ * The interface a Polly peer-first primitive exposes at the call site. It
32
+ * satisfies {@link MigratableState} so that the cross-primitive migration
33
+ * helper can consume it directly.
34
+ */
35
+ export interface CrdtPrimitive<T extends VersionedDoc> extends MigratableState<T> {
36
+ /** Stable logical key the primitive was registered under. */
37
+ readonly key: string;
38
+ /** Primitive kind — one of the {@link PrimitiveKind} labels. */
39
+ readonly primitive: PrimitiveKind;
40
+ /** Current value. Writes push into the backing Automerge document. */
41
+ value: T;
42
+ /** Resolves when the handle is ready and migrations have run. */
43
+ readonly loaded: Promise<void>;
44
+ /** The underlying {@link DocHandle}, populated after {@link loaded} resolves.
45
+ * Intended for advanced escape hatches; most callers should stay at the
46
+ * signal surface. */
47
+ readonly handle: DocHandle<T> | undefined;
48
+ }
49
+ /**
50
+ * Options for constructing a base CRDT-backed primitive. Phase 1 and Phase 2
51
+ * primitive constructors pass a transport-specific {@link getHandle} factory
52
+ * and their own {@link primitive} label; everything else is shared.
53
+ */
54
+ export interface CrdtStateOptions<T extends VersionedDoc> {
55
+ /** Stable logical key identifying this piece of state. */
56
+ key: string;
57
+ /** Primitive kind label for registry and error-message purposes. */
58
+ primitive: PrimitiveKind;
59
+ /** Initial value if no stored document exists yet. Applied by the caller's
60
+ * handle factory; the base module does not create documents itself. */
61
+ initialValue: T;
62
+ /** Async factory that resolves to a ready {@link DocHandle}. The factory is
63
+ * responsible for repo lookup, document creation, and any transport-specific
64
+ * setup. The base module calls this once, during hydration. */
65
+ getHandle: () => Promise<DocHandle<T>>;
66
+ /** Target schema version for the application. If set, migrations run on
67
+ * load to bring the document up to this version before the signal hydrates. */
68
+ schemaVersion?: number;
69
+ /** Migration table. Ignored if {@link schemaVersion} is not set. */
70
+ migrations?: Migrations;
71
+ /** Declarative access predicates. Not consumed by the base module; the
72
+ * transport-specific constructors compile it to their enforcement layer. */
73
+ access?: Access;
74
+ /** Optional free-text call-site label for primitive-registry error messages. */
75
+ callSite?: string;
76
+ }
77
+ /**
78
+ * Construct a base CRDT-backed Polly primitive. Integrates with
79
+ * primitive-registry (for collision detection), migration-registry (for
80
+ * cross-family migration guards), and schema-version (for on-load migrations).
81
+ *
82
+ * @throws {MigrationError} if the source key has been marked as migrated.
83
+ * @throws {PrimitiveCollisionError} if the key is already registered under a
84
+ * different primitive kind.
85
+ */
86
+ export declare function $crdtState<T extends VersionedDoc>(options: CrdtStateOptions<T>): CrdtPrimitive<T>;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * encryption — symmetric authenticated encryption for Polly's $meshState
3
+ * primitive (Phase 2). Wraps tweetnacl's secretbox (XSalsa20-Poly1305) with
4
+ * a small Polly-flavoured API so the rest of the codebase never imports
5
+ * tweetnacl directly.
6
+ *
7
+ * Every $meshState document has a per-document symmetric key that is
8
+ * provisioned to authorised peers at pairing time and never held by any
9
+ * server. Outgoing operations are encrypted under this key before they
10
+ * touch the network adapter; incoming operations are decrypted on receipt.
11
+ * The signing layer in {@link signing.ts} provides authenticity (proof of
12
+ * who sent the message); this layer provides confidentiality (the bytes
13
+ * are unreadable to anything that does not hold the document key).
14
+ *
15
+ * tweetnacl's secretbox uses a 32-byte symmetric key and a 24-byte nonce.
16
+ * The output of `nacl.secretbox` is the ciphertext concatenated with a
17
+ * 16-byte Poly1305 authentication tag. We package the nonce + ciphertext
18
+ * into a single binary blob using a small length-prefixed envelope so the
19
+ * receiver can recover the nonce without out-of-band coordination.
20
+ *
21
+ * - {@link generateDocumentKey} returns a fresh 32-byte symmetric key.
22
+ *
23
+ * - {@link encrypt} produces a sealed blob from a payload and a key.
24
+ *
25
+ * - {@link decrypt} recovers the payload from a sealed blob and a key.
26
+ * Returns undefined if the blob is malformed or the authentication
27
+ * tag does not match (i.e. wrong key or tampered ciphertext) — the
28
+ * undefined signal lets call sites distinguish "wrong key" from
29
+ * "structurally invalid" without throwing.
30
+ *
31
+ * - {@link sealEnvelope} and {@link openEnvelope} are convenience helpers
32
+ * that wrap encrypt/decrypt in a structured EncryptedEnvelope shape so
33
+ * the mesh transport layer can handle the binary plumbing uniformly.
34
+ */
35
+ /** Length in bytes of a secretbox symmetric key. */
36
+ export declare const KEY_BYTES = 32;
37
+ /** Length in bytes of a secretbox nonce. */
38
+ export declare const NONCE_BYTES = 24;
39
+ /** Length in bytes of the Poly1305 authentication tag. */
40
+ export declare const TAG_BYTES = 16;
41
+ /**
42
+ * A sealed blob suitable for storage or network transmission. The wire
43
+ * layout is the concatenation of the nonce and the ciphertext+tag from
44
+ * tweetnacl. Callers should not depend on the exact bytes — round-trip
45
+ * through {@link encrypt} / {@link decrypt} or the envelope helpers.
46
+ */
47
+ export type SealedBytes = Uint8Array;
48
+ /** Errors thrown by the encryption subsystem. */
49
+ export declare class EncryptionError extends Error {
50
+ readonly code: "invalid-key-length" | "decrypt-failed" | "envelope-malformed";
51
+ constructor(message: string, code: EncryptionError["code"]);
52
+ }
53
+ /**
54
+ * Generate a fresh 32-byte symmetric document key. Calls into tweetnacl's
55
+ * CSPRNG.
56
+ */
57
+ export declare function generateDocumentKey(): Uint8Array;
58
+ /**
59
+ * Encrypt a payload under a symmetric key. The returned blob includes a
60
+ * fresh nonce so the receiver does not need any out-of-band coordination
61
+ * to decrypt.
62
+ */
63
+ export declare function encrypt(payload: Uint8Array, key: Uint8Array): SealedBytes;
64
+ /**
65
+ * Decrypt a sealed blob under a symmetric key. Returns the original
66
+ * payload on success. Returns undefined if the blob is too short to
67
+ * contain a nonce and tag, or if the authentication tag does not match
68
+ * (which indicates either a wrong key or a tampered ciphertext).
69
+ *
70
+ * The undefined return is deliberate: call sites typically want to fall
71
+ * back to a different key or surface a "could not decrypt" message rather
72
+ * than catching an exception. Use {@link decryptOrThrow} when an exception
73
+ * is preferred.
74
+ */
75
+ export declare function decrypt(sealed: SealedBytes, key: Uint8Array): Uint8Array | undefined;
76
+ /**
77
+ * Decrypt a sealed blob, throwing {@link EncryptionError} on failure
78
+ * instead of returning undefined.
79
+ */
80
+ export declare function decryptOrThrow(sealed: SealedBytes, key: Uint8Array): Uint8Array;
81
+ /**
82
+ * A high-level structured envelope combining encryption with a small
83
+ * amount of metadata the receiver needs to find the right key. The mesh
84
+ * network adapter uses this shape on the wire.
85
+ */
86
+ export interface EncryptedEnvelope {
87
+ /** Stable identifier for the document this payload belongs to. The
88
+ * receiver looks this up in its local key store to find the right key. */
89
+ documentId: string;
90
+ /** Sealed blob containing the encrypted payload plus its nonce. */
91
+ sealed: SealedBytes;
92
+ }
93
+ /**
94
+ * Encrypt a payload and pack it into an {@link EncryptedEnvelope} along
95
+ * with the document id.
96
+ */
97
+ export declare function sealEnvelope(payload: Uint8Array, documentId: string, key: Uint8Array): EncryptedEnvelope;
98
+ /**
99
+ * Decrypt an {@link EncryptedEnvelope} using the given key. Throws on
100
+ * failure for symmetry with {@link sealEnvelope}.
101
+ */
102
+ export declare function openEnvelope(envelope: EncryptedEnvelope, key: Uint8Array): Uint8Array;
103
+ /**
104
+ * Serialise an {@link EncryptedEnvelope} to a single binary blob.
105
+ *
106
+ * Wire format:
107
+ *
108
+ * [4 bytes BE: documentId byte length]
109
+ * [N bytes: documentId UTF-8]
110
+ * [remaining: sealed blob (nonce + ciphertext + tag)]
111
+ */
112
+ export declare function encodeEncryptedEnvelope(envelope: EncryptedEnvelope): Uint8Array;
113
+ /**
114
+ * Deserialise a binary envelope produced by {@link encodeEncryptedEnvelope}.
115
+ * Throws on malformed input.
116
+ */
117
+ export declare function decodeEncryptedEnvelope(bytes: Uint8Array): EncryptedEnvelope;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * mesh-network-adapter — Phase 2 wrapping NetworkAdapter that adds Polly's
3
+ * mesh-transport semantics on top of any underlying Automerge NetworkAdapter.
4
+ *
5
+ * The mesh transport's job is to make every message between peers signed
6
+ * and encrypted before it reaches the wire. Rather than reimplementing the
7
+ * Automerge sync protocol, this adapter takes a base adapter (in production
8
+ * a real WebRTC or WebSocket adapter; in tests an in-memory loopback) and
9
+ * applies the crypto envelope to every message that flows through.
10
+ *
11
+ * Outgoing path (Repo → wire):
12
+ * 1. The Repo's NetworkSubsystem calls send(message) on this adapter.
13
+ * 2. We serialise the message to bytes, encrypt them under the local
14
+ * keyring's document key, sign the resulting blob with the local
15
+ * identity's secret key, and pack the pair into a MeshFrame.
16
+ * 3. We hand the MeshFrame off to the base adapter, which puts it on
17
+ * whatever wire it owns.
18
+ *
19
+ * Incoming path (wire → Repo):
20
+ * 1. The base adapter emits a 'message' event with bytes from the wire.
21
+ * 2. We unpack the MeshFrame, look up the sender's public key in the
22
+ * keyring, verify the signature, look up the document key, decrypt
23
+ * the payload, and deserialise it back to the original message.
24
+ * 3. We re-emit the 'message' event upward to the Repo's NetworkSubsystem
25
+ * with the decrypted message.
26
+ *
27
+ * The keyring is an injected dependency. In production it's backed by
28
+ * persistent storage and populated through the pairing flow. For tests it
29
+ * is just a Map of publicly-known fixtures that both sides share.
30
+ *
31
+ * Caveat for the Phase 2 first cut: Automerge sync messages don't have a
32
+ * stable "what document does this belong to" field at the wire level (the
33
+ * documentId is part of the message contents). The mesh adapter therefore
34
+ * uses a single per-Repo encryption key for now rather than per-document
35
+ * keys, and stores the key once in the keyring under the well-known id
36
+ * "polly-mesh-default". The plan describes per-document keys as the right
37
+ * end state; that requires either parsing the message to extract the
38
+ * documentId before encrypting (peeking inside the binary protocol) or
39
+ * threading the document context through the network subsystem (which
40
+ * needs upstream support). A follow-up will address this.
41
+ */
42
+ import { type Message, NetworkAdapter, type PeerId, type PeerMetadata } from "@automerge/automerge-repo";
43
+ import { type SigningKeyPair } from "./signing";
44
+ /** The well-known document id used for the Phase 2 first-cut single-key
45
+ * encryption mode. See the file-level comment for the per-document key
46
+ * follow-up. */
47
+ export declare const DEFAULT_MESH_KEY_ID = "polly-mesh-default";
48
+ /**
49
+ * A mesh keyring holds the local peer's signing identity, the public keys
50
+ * of every peer the local node will accept messages from, the symmetric
51
+ * encryption keys for documents the local node has access to, and the set
52
+ * of peers whose keys have been revoked.
53
+ */
54
+ export interface MeshKeyring {
55
+ /** The local peer's signing keypair. The secret never leaves this
56
+ * keyring; the public key is gossiped through the access set. */
57
+ identity: SigningKeyPair;
58
+ /** Map from peer id (string) to that peer's signing public key. The
59
+ * mesh adapter rejects messages from peers not present in this map. */
60
+ knownPeers: Map<string, Uint8Array>;
61
+ /** Map from document key id (typically the documentId, or the well-known
62
+ * default for the single-key first cut) to the symmetric encryption key. */
63
+ documentKeys: Map<string, Uint8Array>;
64
+ /** Set of peer ids whose keys have been revoked. The mesh adapter drops
65
+ * incoming messages from any peer in this set, even if the peer is still
66
+ * present in {@link knownPeers}. Revocation is applied via the revocation
67
+ * module; the set is kept separate from knownPeers so that an application
68
+ * can audit who was once authorised without losing the revocation record. */
69
+ revokedPeers: Set<string>;
70
+ /** Optional set of peer ids authorised to issue revocations. When present
71
+ * and non-empty, `decodeRevocation` accepts a signed record only if the
72
+ * issuer is in this set. When undefined or empty, any signed revocation
73
+ * from a known peer is accepted (the Phase 2 first-cut default). This
74
+ * field layers a "who can revoke whom" check on top of the signature
75
+ * layer without breaking existing callers. */
76
+ revocationAuthority?: Set<string>;
77
+ }
78
+ /**
79
+ * Constructor options for {@link MeshNetworkAdapter}.
80
+ */
81
+ export interface MeshNetworkAdapterOptions {
82
+ /** The underlying NetworkAdapter that puts crypto-wrapped bytes on the
83
+ * wire. In production this is a WebRTC or WebSocket adapter; in tests
84
+ * it's an in-memory loopback. */
85
+ base: NetworkAdapter;
86
+ /** The local node's keyring. The adapter signs every outgoing message
87
+ * with `identity.secretKey` and verifies every incoming message against
88
+ * the public keys in `knownPeers`. */
89
+ keyring: MeshKeyring;
90
+ /** When false, the adapter signs but does not encrypt. Outgoing messages
91
+ * carry a signature envelope but the payload is plaintext; incoming
92
+ * messages are verified against the sender's public key without a
93
+ * decryption step. This mode is used by $peerState's `sign: true`
94
+ * option, where the server must still be able to parse Automerge sync
95
+ * messages. Defaults to true (encrypt + sign, the full $meshState
96
+ * posture). */
97
+ encryptionEnabled?: boolean;
98
+ }
99
+ /**
100
+ * NetworkAdapter that wraps another adapter with Polly's mesh-transport
101
+ * crypto envelope. Every outgoing message is encrypted then signed; every
102
+ * incoming message is verified then decrypted before being forwarded to
103
+ * the Repo's network subsystem.
104
+ *
105
+ * The adapter delegates lifecycle (connect, disconnect, isReady, peer
106
+ * discovery) to the base adapter unchanged. Only the message body is
107
+ * intercepted.
108
+ */
109
+ export declare class MeshNetworkAdapter extends NetworkAdapter {
110
+ readonly base: NetworkAdapter;
111
+ readonly keyring: MeshKeyring;
112
+ readonly encryptionEnabled: boolean;
113
+ constructor(options: MeshNetworkAdapterOptions);
114
+ isReady(): boolean;
115
+ whenReady(): Promise<void>;
116
+ connect(peerId: PeerId, peerMetadata?: PeerMetadata): void;
117
+ disconnect(): void;
118
+ send(message: Message): void;
119
+ /**
120
+ * Wrap an outgoing Automerge message in an encrypt-then-sign envelope.
121
+ * The wrapped payload is returned as a Message with the original sender
122
+ * and target ids and the crypto blob in the `data` field.
123
+ */
124
+ private wrap;
125
+ /**
126
+ * Try to unwrap an incoming crypto-wrapped message. Returns the original
127
+ * Message on success, undefined on verification or decryption failure.
128
+ */
129
+ private tryUnwrap;
130
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * mesh-signaling-client — browser-side client for Polly's signalingServer
3
+ * Elysia plugin. Connects to the signalling WebSocket, registers a peer
4
+ * id, and relays SDP/ICE messages between local WebRTC connections and
5
+ * remote peers.
6
+ *
7
+ * This module is the companion to {@link signalingServer} from the Elysia
8
+ * plugin family. The server accepts "join" and "signal" messages; this
9
+ * client produces them. The protocol matches: opening the connection,
10
+ * sending a "join" with the local peer id, and then using sendSignal()
11
+ * to forward SDP and ICE messages to specific target peers.
12
+ *
13
+ * Because this client is browser-only in its first incarnation — it
14
+ * assumes the global `WebSocket` is available — it cannot be exercised
15
+ * under bun:test the way the server-side plugin is. The first validation
16
+ * of this code path is either a Playwright harness or a human running
17
+ * the browser-side example that consumes it.
18
+ */
19
+ /** A signal message either sent to or received from the signalling server.
20
+ * Matches the wire format produced by the Elysia signalingServer plugin. */
21
+ export interface SignalingMessage {
22
+ type: "join" | "signal" | "error";
23
+ peerId?: string;
24
+ targetPeerId?: string;
25
+ payload?: unknown;
26
+ reason?: "unknown-target" | "not-joined" | "malformed";
27
+ }
28
+ /** Options for constructing a {@link MeshSignalingClient}. */
29
+ export interface MeshSignalingClientOptions {
30
+ /** The signalling server URL (ws:// or wss://). */
31
+ url: string;
32
+ /** The local peer id that this client will register with on join. */
33
+ peerId: string;
34
+ /** Callback invoked whenever a signal message from another peer arrives.
35
+ * The receiver dispatches to the right PeerConnection based on the
36
+ * `fromPeerId`. */
37
+ onSignal: (fromPeerId: string, payload: unknown) => void;
38
+ /** Optional callback invoked when the server returns an error (for
39
+ * diagnostic UI or reconnection logic). */
40
+ onError?: (reason: string, targetPeerId?: string) => void;
41
+ /** Optional callback for the open and close lifecycle events. */
42
+ onOpen?: () => void;
43
+ onClose?: () => void;
44
+ }
45
+ /**
46
+ * Thin wrapper around a WebSocket connection to a Polly signalling server.
47
+ * Handles the join handshake, routes incoming signals to the supplied
48
+ * callback, and exposes a {@link sendSignal} method for outgoing signals.
49
+ *
50
+ * This class is deliberately small. It has no opinion on the signal
51
+ * payload shape (the wire carries it as `unknown`), so it can carry SDP
52
+ * offers, SDP answers, ICE candidates, or any other message the
53
+ * WebRTC adapter wants to exchange with peers.
54
+ */
55
+ export declare class MeshSignalingClient {
56
+ readonly url: string;
57
+ readonly peerId: string;
58
+ private readonly onSignal;
59
+ private readonly onError?;
60
+ private readonly onOpen?;
61
+ private readonly onClose?;
62
+ private socket;
63
+ private joined;
64
+ constructor(options: MeshSignalingClientOptions);
65
+ /**
66
+ * Open the WebSocket and send the join message. Resolves once the
67
+ * connection is open; callers should not send signals before this
68
+ * promise resolves.
69
+ */
70
+ connect(): Promise<void>;
71
+ /**
72
+ * Send a signal to another peer via the signalling server. The server
73
+ * validates the sender (replacing the claimed peerId with the
74
+ * authenticated join id) and routes to the target. Returns true if
75
+ * the message was sent, false if the connection is not open.
76
+ */
77
+ sendSignal(targetPeerId: string, payload: unknown): boolean;
78
+ /**
79
+ * Close the underlying WebSocket connection. The server's close handler
80
+ * will evict this peer from its routing table.
81
+ */
82
+ close(): void;
83
+ /** True if the signalling connection is open and joined. */
84
+ get isConnected(): boolean;
85
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * mesh-state — Phase 2 wrappers exposing $meshState, $meshText, $meshCounter,
3
+ * and $meshList. These are the application-facing constructors for the
4
+ * strongest resilience tier in RFC-041: every device is a full Automerge
5
+ * replica, the server is *not on the data path at all*, and the application
6
+ * functions with zero server uptime once direct peer connections are
7
+ * established.
8
+ *
9
+ * Each primitive wraps the corresponding Phase 0 base ($crdtState, $crdtText,
10
+ * $crdtCounter, $crdtList) with three additions:
11
+ *
12
+ * 1. The `primitive` label is hard-coded to "meshState" so the
13
+ * primitive-registry collision detection knows which family the key
14
+ * belongs to.
15
+ *
16
+ * 2. A handle factory that resolves the application's logical key to an
17
+ * Automerge DocumentId via a per-Repo key map, identical in shape to
18
+ * the $peerState factory but registered against a separate Repo
19
+ * configured for the mesh transport (signed and encrypted at the
20
+ * network layer).
21
+ *
22
+ * 3. Signing and encryption are mandatory, not optional. Where $peerState
23
+ * accepts encrypt/sign as opt-in flags (currently throwing in Phase 1),
24
+ * $meshState requires every operation to be signed by the originating
25
+ * peer's key and encrypted under the document's symmetric key. The
26
+ * mechanism lives in the wrapping MeshNetworkAdapter that the Repo
27
+ * uses for transport.
28
+ *
29
+ * The Repo itself is supplied by the application via {@link configureMeshState}
30
+ * or per-call via the `repo` option. In Phase 2 the production transport will
31
+ * be a WebRTC mesh adapter wrapping signing+encryption around an in-process
32
+ * RTCDataChannel; for tests and for the early Phase 2 cut, an in-memory
33
+ * loopback adapter pair satisfies the same contract.
34
+ */
35
+ import type { Repo } from "@automerge/automerge-repo";
36
+ import type { Access } from "./access";
37
+ import { type SpecialisedPrimitive } from "./crdt-specialised";
38
+ import { type CrdtPrimitive } from "./crdt-state";
39
+ import type { Migrations, VersionedDoc } from "./schema-version";
40
+ /** Common option shape across all four $mesh* primitives. */
41
+ export interface MeshStateOptions {
42
+ /** Override the default Repo for this primitive. The Repo must be
43
+ * configured with the mesh transport (signing and encryption at the
44
+ * network layer). */
45
+ repo?: Repo;
46
+ /** Schema version target for the application. Migrations run on load. */
47
+ schemaVersion?: number;
48
+ /** Migration table keyed by target version. Required if schemaVersion is set. */
49
+ migrations?: Migrations;
50
+ /** Declarative read/write access. The mesh transport compiles this into
51
+ * a public-key set used by the signing layer to verify incoming ops. */
52
+ access?: Access;
53
+ }
54
+ /**
55
+ * Set the default Repo that the $mesh* primitives use when no `repo` option
56
+ * is supplied. Calling this with a new Repo clears the per-Repo key map so
57
+ * that tests start each scenario with a fresh document space.
58
+ *
59
+ * Production code typically calls this once at application startup with a
60
+ * Repo configured for the mesh transport. Tests call it before each
61
+ * scenario with an in-memory or loopback Repo.
62
+ */
63
+ export declare function configureMeshState(repo: Repo): void;
64
+ /**
65
+ * Reset the mesh-state subsystem to its initial unconfigured state.
66
+ * Intended for tests; production code should not call this.
67
+ */
68
+ export declare function resetMeshState(): void;
69
+ /**
70
+ * Create a peer-replicated state primitive backed by Automerge with a mesh
71
+ * transport. Every device holds a full replica; no central server holds a
72
+ * replica. Operations flow peer-to-peer through signed and encrypted
73
+ * messages on the underlying transport.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const journal = $meshState<Journal>("journal", { entries: [] });
78
+ * await journal.loaded;
79
+ * journal.value = { entries: ["my private thoughts"] };
80
+ * ```
81
+ */
82
+ export declare function $meshState<T extends VersionedDoc>(key: string, initialValue: T, options?: MeshStateOptions): CrdtPrimitive<T>;
83
+ /**
84
+ * Create a peer-replicated text primitive backed by a mesh transport.
85
+ * Concurrent character-level edits from peers merge cleanly via Automerge's
86
+ * updateText splicing, and every operation is signed and encrypted before
87
+ * leaving the originating peer.
88
+ */
89
+ export declare function $meshText(key: string, initialValue: string, options?: MeshStateOptions): SpecialisedPrimitive<string>;
90
+ /**
91
+ * Create a peer-replicated counter primitive backed by a mesh transport.
92
+ * Concurrent increments commute, and every operation is signed and
93
+ * encrypted before leaving the originating peer.
94
+ */
95
+ export declare function $meshCounter(key: string, initialValue: number, options?: MeshStateOptions): SpecialisedPrimitive<number>;
96
+ /**
97
+ * Create a peer-replicated list primitive backed by a mesh transport.
98
+ * Phase 0 naive replacement applies to writes for now; Phase 1.1 will
99
+ * refine with structural diff-to-splice for concurrent insert/remove
100
+ * preservation.
101
+ */
102
+ export declare function $meshList<T>(key: string, initialValue: T[], options?: MeshStateOptions): SpecialisedPrimitive<T[]>;