@optimystic/db-core 0.22.0 → 0.24.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.
- package/README.md +336 -336
- package/dist/src/cluster/structs.d.ts +39 -1
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +24 -0
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/collection/collection.d.ts +17 -0
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +24 -2
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +5 -0
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +7 -0
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/network/i-peer-network.d.ts +16 -0
- package/dist/src/network/i-peer-network.d.ts.map +1 -1
- package/dist/src/network/struct.d.ts +39 -2
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js +18 -0
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +95 -8
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +121 -8
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +1 -1
- package/dist/src/transaction/transaction.js +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +48 -13
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +25 -2
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/package.json +1 -1
- package/src/cluster/membership.ts +85 -85
- package/src/cluster/structs.ts +43 -4
- package/src/cohort-topic/addressing.ts +120 -120
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
- package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
- package/src/cohort-topic/antidos/index.ts +5 -5
- package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
- package/src/cohort-topic/antidos/replay-guard.ts +146 -146
- package/src/cohort-topic/antidos/topic-budget.ts +160 -160
- package/src/cohort-topic/antiflood/index.ts +2 -2
- package/src/cohort-topic/antiflood/invariants.ts +108 -108
- package/src/cohort-topic/antiflood/jitter.ts +117 -117
- package/src/cohort-topic/coldstart.ts +237 -237
- package/src/cohort-topic/dmax.ts +88 -88
- package/src/cohort-topic/gossip/bus.ts +254 -254
- package/src/cohort-topic/gossip/index.ts +3 -3
- package/src/cohort-topic/gossip/records.ts +45 -45
- package/src/cohort-topic/gossip/view.ts +91 -91
- package/src/cohort-topic/index.ts +20 -20
- package/src/cohort-topic/load/barometer.ts +134 -134
- package/src/cohort-topic/load/index.ts +1 -1
- package/src/cohort-topic/member-engine.ts +430 -430
- package/src/cohort-topic/membership/index.ts +3 -3
- package/src/cohort-topic/membership/publisher.ts +163 -163
- package/src/cohort-topic/membership/source.ts +41 -41
- package/src/cohort-topic/membership/verifier.ts +461 -461
- package/src/cohort-topic/ports.ts +157 -157
- package/src/cohort-topic/promotion.ts +405 -405
- package/src/cohort-topic/registration/bytes.ts +37 -37
- package/src/cohort-topic/registration/handoff.ts +154 -154
- package/src/cohort-topic/registration/index.ts +6 -6
- package/src/cohort-topic/registration/renewal.ts +495 -495
- package/src/cohort-topic/registration/sharding.ts +61 -61
- package/src/cohort-topic/registration/store.ts +81 -81
- package/src/cohort-topic/registration/types.ts +91 -91
- package/src/cohort-topic/ring-hash.ts +50 -50
- package/src/cohort-topic/service.ts +416 -416
- package/src/cohort-topic/sig/index.ts +2 -2
- package/src/cohort-topic/sig/payloads.ts +59 -59
- package/src/cohort-topic/sig/threshold.ts +64 -64
- package/src/cohort-topic/tiers.ts +74 -74
- package/src/cohort-topic/traffic.ts +233 -233
- package/src/cohort-topic/walk.ts +326 -326
- package/src/cohort-topic/willingness.ts +237 -237
- package/src/cohort-topic/wire/codec.ts +216 -216
- package/src/cohort-topic/wire/index.ts +18 -18
- package/src/cohort-topic/wire/payloads.ts +126 -126
- package/src/cohort-topic/wire/primitives.ts +188 -188
- package/src/cohort-topic/wire/types.ts +475 -475
- package/src/cohort-topic/wire/validate.ts +512 -512
- package/src/collection/collection-type-registry.ts +37 -37
- package/src/collection/collection.ts +25 -2
- package/src/collections/diary/diary.ts +68 -68
- package/src/collections/tree/readme.md +4 -0
- package/src/collections/tree/tree.ts +320 -312
- package/src/matchmaking/capability-filter.ts +45 -45
- package/src/matchmaking/config.ts +98 -98
- package/src/matchmaking/index.ts +21 -21
- package/src/matchmaking/multi-cohort-seeker.ts +234 -234
- package/src/matchmaking/provider.ts +123 -123
- package/src/matchmaking/query-eval.ts +105 -105
- package/src/matchmaking/seeker-walk.ts +127 -127
- package/src/matchmaking/seeker.ts +86 -86
- package/src/matchmaking/topic-anchor.ts +90 -90
- package/src/matchmaking/voting-quorum.ts +394 -394
- package/src/matchmaking/wire.ts +603 -603
- package/src/network/i-peer-network.ts +17 -0
- package/src/network/stale-failure.ts +43 -43
- package/src/network/struct.ts +41 -2
- package/src/network/types.ts +37 -37
- package/src/reactivity/backfill.ts +220 -220
- package/src/reactivity/backpressure.ts +191 -191
- package/src/reactivity/checkpoint.ts +308 -308
- package/src/reactivity/config.ts +172 -172
- package/src/reactivity/dedupe.ts +132 -132
- package/src/reactivity/forwarder.ts +87 -87
- package/src/reactivity/index.ts +34 -34
- package/src/reactivity/notification.ts +123 -123
- package/src/reactivity/policy.ts +79 -79
- package/src/reactivity/push-state.ts +310 -310
- package/src/reactivity/recover.ts +153 -153
- package/src/reactivity/replay-buffer.ts +141 -141
- package/src/reactivity/resume.ts +549 -549
- package/src/reactivity/rotation.ts +415 -415
- package/src/reactivity/subscriber.ts +132 -132
- package/src/reactivity/subscription.ts +66 -66
- package/src/reactivity/topic-anchor.ts +71 -71
- package/src/reactivity/verify.ts +73 -73
- package/src/reactivity/wire-validate.ts +13 -13
- package/src/reactivity/wire.ts +224 -224
- package/src/testing/async-wait.ts +65 -65
- package/src/testing/index.ts +2 -2
- package/src/testing/test-transactor.ts +638 -502
- package/src/transaction/errors.ts +91 -91
- package/src/transaction/operations-hash.ts +196 -196
- package/src/transaction/read-dependency-collector.ts +78 -78
- package/src/transaction/transaction.ts +1 -1
- package/src/transactor/change-notifier.ts +80 -80
- package/src/transactor/index.ts +5 -5
- package/src/transactor/network-transactor.ts +49 -14
- package/src/transactor/transactor-source.ts +25 -2
- package/src/transform/atomic-proxy.ts +92 -92
- package/src/transform/helpers.ts +159 -159
- package/src/utility/backoff.ts +95 -95
- package/src/utility/batch-coordinator.ts +191 -191
- package/dist/src/transaction/context.d.ts +0 -60
- package/dist/src/transaction/context.d.ts.map +0 -1
- package/dist/src/transaction/context.js +0 -91
- package/dist/src/transaction/context.js.map +0 -1
package/src/reactivity/verify.ts
CHANGED
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reactivity — subscriber/forwarder notification verification seam
|
|
3
|
-
* (`docs/reactivity.md` §Propagation, §Delivery, §Authentication).
|
|
4
|
-
*
|
|
5
|
-
* A forwarder and a subscriber both verify a notification's threshold signature against the **tail
|
|
6
|
-
* cohort's** `MembershipCertV1` before trusting it — forwarders never re-sign, so the same end-to-end
|
|
7
|
-
* signature is verified regardless of hop count. The standard cohort-topic membership-snapshot path
|
|
8
|
-
* ({@link MembershipVerifier}) already provides the **one fetch-and-retry** on a stale/missing cached
|
|
9
|
-
* cert, so this module is a thin adapter: derive the tail cohort's `coord_0(_, topicId)` from the
|
|
10
|
-
* notification's `tailId`, then hand `(signers, coord, tier, digest, sig)` to the verifier.
|
|
11
|
-
*
|
|
12
|
-
* The signed payload is the commit `digest` (the commit cert's threshold signature is over the commit
|
|
13
|
-
* hash — see {@link import("./notification.js").buildNotificationV1}). `signers` arrive base64url-encoded
|
|
14
|
-
* as the cohort member-id bytes the verifier compares against `cert.members`; a custom `signersToBytes`
|
|
15
|
-
* seam is exposed for bindings that carry signers in a different encoding.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { createTierAddressing, DEFAULT_FANOUT } from "../cohort-topic/addressing.js";
|
|
19
|
-
import { createRingHash } from "../cohort-topic/ring-hash.js";
|
|
20
|
-
import { b64urlToBytes } from "../cohort-topic/wire/codec.js";
|
|
21
|
-
import { Tier } from "../cohort-topic/tiers.js";
|
|
22
|
-
import type { IRingHash } from "../cohort-topic/ports.js";
|
|
23
|
-
import type { MembershipVerifier, VerifyResult } from "../cohort-topic/membership/verifier.js";
|
|
24
|
-
import { reactivityTopicId } from "./topic-anchor.js";
|
|
25
|
-
import type { NotificationV1 } from "./wire.js";
|
|
26
|
-
|
|
27
|
-
/** Verifies a {@link NotificationV1}'s threshold signature against the tail cohort's membership. */
|
|
28
|
-
export interface NotificationVerifier {
|
|
29
|
-
/** `"verified"` iff `sig` is a valid `≥ minSigs` cohort signature over the commit digest. */
|
|
30
|
-
verify(n: NotificationV1): Promise<VerifyResult>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Construction inputs for the default {@link NotificationVerifier}. */
|
|
34
|
-
export interface NotificationVerifierDeps {
|
|
35
|
-
/** The cohort-topic participant-side membership verifier (owns the one fetch-and-retry). */
|
|
36
|
-
readonly verifier: MembershipVerifier;
|
|
37
|
-
/** Ring hash for `topicId` / `coord_0` derivation. Default db-core 256-bit SHA-256. */
|
|
38
|
-
readonly hash?: IRingHash;
|
|
39
|
-
/** Fan-out `F` for tier addressing. Default {@link DEFAULT_FANOUT}. */
|
|
40
|
-
readonly fanout?: number;
|
|
41
|
-
/** Reactivity runs at T3; overridable for tests. */
|
|
42
|
-
readonly tier?: Tier;
|
|
43
|
-
/** Map a wire signer string to the verifier's member-id bytes. Default base64url decode. */
|
|
44
|
-
readonly signersToBytes?: (signer: string) => Uint8Array;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
class MembershipNotificationVerifier implements NotificationVerifier {
|
|
48
|
-
private readonly addressing: ReturnType<typeof createTierAddressing>;
|
|
49
|
-
private readonly hash: IRingHash;
|
|
50
|
-
private readonly tier: Tier;
|
|
51
|
-
private readonly signersToBytes: (signer: string) => Uint8Array;
|
|
52
|
-
|
|
53
|
-
constructor(private readonly deps: NotificationVerifierDeps) {
|
|
54
|
-
this.hash = deps.hash ?? createRingHash();
|
|
55
|
-
this.addressing = createTierAddressing(this.hash, deps.fanout ?? DEFAULT_FANOUT);
|
|
56
|
-
this.tier = deps.tier ?? Tier.T3;
|
|
57
|
-
this.signersToBytes = deps.signersToBytes ?? b64urlToBytes;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async verify(n: NotificationV1): Promise<VerifyResult> {
|
|
61
|
-
const topicId = reactivityTopicId(b64urlToBytes(n.tailId), this.hash);
|
|
62
|
-
const expectedCoord = this.addressing.coord0(topicId);
|
|
63
|
-
const signers = n.signers.map(this.signersToBytes);
|
|
64
|
-
const payload = b64urlToBytes(n.digest);
|
|
65
|
-
const sig = b64urlToBytes(n.sig);
|
|
66
|
-
return this.deps.verifier.verifyMessage(signers, expectedCoord, this.tier, payload, sig);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Build the default {@link NotificationVerifier} over the cohort-topic {@link MembershipVerifier}. */
|
|
71
|
-
export function createNotificationVerifier(deps: NotificationVerifierDeps): NotificationVerifier {
|
|
72
|
-
return new MembershipNotificationVerifier(deps);
|
|
73
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity — subscriber/forwarder notification verification seam
|
|
3
|
+
* (`docs/reactivity.md` §Propagation, §Delivery, §Authentication).
|
|
4
|
+
*
|
|
5
|
+
* A forwarder and a subscriber both verify a notification's threshold signature against the **tail
|
|
6
|
+
* cohort's** `MembershipCertV1` before trusting it — forwarders never re-sign, so the same end-to-end
|
|
7
|
+
* signature is verified regardless of hop count. The standard cohort-topic membership-snapshot path
|
|
8
|
+
* ({@link MembershipVerifier}) already provides the **one fetch-and-retry** on a stale/missing cached
|
|
9
|
+
* cert, so this module is a thin adapter: derive the tail cohort's `coord_0(_, topicId)` from the
|
|
10
|
+
* notification's `tailId`, then hand `(signers, coord, tier, digest, sig)` to the verifier.
|
|
11
|
+
*
|
|
12
|
+
* The signed payload is the commit `digest` (the commit cert's threshold signature is over the commit
|
|
13
|
+
* hash — see {@link import("./notification.js").buildNotificationV1}). `signers` arrive base64url-encoded
|
|
14
|
+
* as the cohort member-id bytes the verifier compares against `cert.members`; a custom `signersToBytes`
|
|
15
|
+
* seam is exposed for bindings that carry signers in a different encoding.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createTierAddressing, DEFAULT_FANOUT } from "../cohort-topic/addressing.js";
|
|
19
|
+
import { createRingHash } from "../cohort-topic/ring-hash.js";
|
|
20
|
+
import { b64urlToBytes } from "../cohort-topic/wire/codec.js";
|
|
21
|
+
import { Tier } from "../cohort-topic/tiers.js";
|
|
22
|
+
import type { IRingHash } from "../cohort-topic/ports.js";
|
|
23
|
+
import type { MembershipVerifier, VerifyResult } from "../cohort-topic/membership/verifier.js";
|
|
24
|
+
import { reactivityTopicId } from "./topic-anchor.js";
|
|
25
|
+
import type { NotificationV1 } from "./wire.js";
|
|
26
|
+
|
|
27
|
+
/** Verifies a {@link NotificationV1}'s threshold signature against the tail cohort's membership. */
|
|
28
|
+
export interface NotificationVerifier {
|
|
29
|
+
/** `"verified"` iff `sig` is a valid `≥ minSigs` cohort signature over the commit digest. */
|
|
30
|
+
verify(n: NotificationV1): Promise<VerifyResult>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Construction inputs for the default {@link NotificationVerifier}. */
|
|
34
|
+
export interface NotificationVerifierDeps {
|
|
35
|
+
/** The cohort-topic participant-side membership verifier (owns the one fetch-and-retry). */
|
|
36
|
+
readonly verifier: MembershipVerifier;
|
|
37
|
+
/** Ring hash for `topicId` / `coord_0` derivation. Default db-core 256-bit SHA-256. */
|
|
38
|
+
readonly hash?: IRingHash;
|
|
39
|
+
/** Fan-out `F` for tier addressing. Default {@link DEFAULT_FANOUT}. */
|
|
40
|
+
readonly fanout?: number;
|
|
41
|
+
/** Reactivity runs at T3; overridable for tests. */
|
|
42
|
+
readonly tier?: Tier;
|
|
43
|
+
/** Map a wire signer string to the verifier's member-id bytes. Default base64url decode. */
|
|
44
|
+
readonly signersToBytes?: (signer: string) => Uint8Array;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class MembershipNotificationVerifier implements NotificationVerifier {
|
|
48
|
+
private readonly addressing: ReturnType<typeof createTierAddressing>;
|
|
49
|
+
private readonly hash: IRingHash;
|
|
50
|
+
private readonly tier: Tier;
|
|
51
|
+
private readonly signersToBytes: (signer: string) => Uint8Array;
|
|
52
|
+
|
|
53
|
+
constructor(private readonly deps: NotificationVerifierDeps) {
|
|
54
|
+
this.hash = deps.hash ?? createRingHash();
|
|
55
|
+
this.addressing = createTierAddressing(this.hash, deps.fanout ?? DEFAULT_FANOUT);
|
|
56
|
+
this.tier = deps.tier ?? Tier.T3;
|
|
57
|
+
this.signersToBytes = deps.signersToBytes ?? b64urlToBytes;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async verify(n: NotificationV1): Promise<VerifyResult> {
|
|
61
|
+
const topicId = reactivityTopicId(b64urlToBytes(n.tailId), this.hash);
|
|
62
|
+
const expectedCoord = this.addressing.coord0(topicId);
|
|
63
|
+
const signers = n.signers.map(this.signersToBytes);
|
|
64
|
+
const payload = b64urlToBytes(n.digest);
|
|
65
|
+
const sig = b64urlToBytes(n.sig);
|
|
66
|
+
return this.deps.verifier.verifyMessage(signers, expectedCoord, this.tier, payload, sig);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Build the default {@link NotificationVerifier} over the cohort-topic {@link MembershipVerifier}. */
|
|
71
|
+
export function createNotificationVerifier(deps: NotificationVerifierDeps): NotificationVerifier {
|
|
72
|
+
return new MembershipNotificationVerifier(deps);
|
|
73
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reactivity — shared wire-validation primitives.
|
|
3
|
-
*
|
|
4
|
-
* The reactivity message codecs ({@link import("./wire.js")}, {@link import("./push-state.js")},
|
|
5
|
-
* {@link import("./backfill.js")}, {@link import("./resume.js")}, plus checkpoint / rotation / recover)
|
|
6
|
-
* decode untrusted JSON into validated V1 shapes with the same structural checks the rest of the
|
|
7
|
-
* substrate uses. Those generic primitives now live in one place —
|
|
8
|
-
* {@link import("../cohort-topic/wire/primitives.js")} — so a hardening tweak lands once; this module is
|
|
9
|
-
* a thin re-export kept so the reactivity codecs import from a sibling path. Every helper throws
|
|
10
|
-
* {@link CohortWireError} on a defect (base64url byte fields, finite/integer numbers, `v: 1`).
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export * from "../cohort-topic/wire/primitives.js";
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity — shared wire-validation primitives.
|
|
3
|
+
*
|
|
4
|
+
* The reactivity message codecs ({@link import("./wire.js")}, {@link import("./push-state.js")},
|
|
5
|
+
* {@link import("./backfill.js")}, {@link import("./resume.js")}, plus checkpoint / rotation / recover)
|
|
6
|
+
* decode untrusted JSON into validated V1 shapes with the same structural checks the rest of the
|
|
7
|
+
* substrate uses. Those generic primitives now live in one place —
|
|
8
|
+
* {@link import("../cohort-topic/wire/primitives.js")} — so a hardening tweak lands once; this module is
|
|
9
|
+
* a thin re-export kept so the reactivity codecs import from a sibling path. Every helper throws
|
|
10
|
+
* {@link CohortWireError} on a defect (base64url byte fields, finite/integer numbers, `v: 1`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export * from "../cohort-topic/wire/primitives.js";
|
package/src/reactivity/wire.ts
CHANGED
|
@@ -1,224 +1,224 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reactivity — V1 wire types, codecs, and per-message validation.
|
|
3
|
-
*
|
|
4
|
-
* Transcribed from `docs/reactivity.md` §Wire formats. Reactivity reuses the cohort-topic `RegisterV1`
|
|
5
|
-
* / `RenewV1` envelopes **unchanged**; the reactivity-specific shapes live in two places:
|
|
6
|
-
*
|
|
7
|
-
* 1. **Subscribe app payload** ({@link SubscribeAppPayloadV1}) carried opaquely inside
|
|
8
|
-
* `RegisterV1.appPayload`. Serialized to UTF-8 JSON bytes and handed to the cohort-topic
|
|
9
|
-
* {@link import("../cohort-topic/service.js").RegisterRequest}`.appPayload` slot, which base64url-encodes
|
|
10
|
-
* it on the wire. It is therefore **not** length-framed here — the cohort-topic `RegisterV1` frame
|
|
11
|
-
* wraps it. The subscribe RPC's authentication (peer-key signature over `correlationId` + `timestamp`)
|
|
12
|
-
* is the cohort-topic `RegisterV1` envelope's job, so the payload itself carries no signature.
|
|
13
|
-
*
|
|
14
|
-
* 2. **Notification** ({@link NotificationV1}) sent as a standalone RPC over the reactivity application
|
|
15
|
-
* protocol. It rides the same length-prefixed UTF-8 JSON framing as cohort-topic messages
|
|
16
|
-
* ({@link encodeCohortMessage}). Its `sig` is **bit-for-bit** the commit certificate's threshold
|
|
17
|
-
* signature — reactivity never re-signs (`docs/reactivity.md` §Notification origination).
|
|
18
|
-
*
|
|
19
|
-
* Conventions (matching the cohort-topic wire conventions): all JSON, byte fields base64url (no
|
|
20
|
-
* padding), unix-millisecond timestamps, per-message structural validation on decode, byte-fidelity
|
|
21
|
-
* round-trips (encode→decode→encode is stable). `collectionId` / `tailId` travel as the collection's
|
|
22
|
-
* base64url block ids verbatim. The `BackfillV1` / `ResumeV1` codecs belong to the sibling tickets.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
decodeCohortMessage,
|
|
27
|
-
encodeCohortMessage,
|
|
28
|
-
DEFAULT_MAX_MESSAGE_BYTES,
|
|
29
|
-
} from "../cohort-topic/wire/codec.js";
|
|
30
|
-
import {
|
|
31
|
-
asObject,
|
|
32
|
-
b64urlField,
|
|
33
|
-
failWire as fail,
|
|
34
|
-
reqBool,
|
|
35
|
-
reqFiniteNumber,
|
|
36
|
-
reqIntInRange,
|
|
37
|
-
reqString,
|
|
38
|
-
reqStringArray,
|
|
39
|
-
requireV1,
|
|
40
|
-
} from "./wire-validate.js";
|
|
41
|
-
|
|
42
|
-
const utf8Encoder = new TextEncoder();
|
|
43
|
-
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
44
|
-
|
|
45
|
-
/** Ceiling for an opaque subscribe app payload, guarding decode allocation. */
|
|
46
|
-
export const DEFAULT_MAX_SUBSCRIBE_PAYLOAD_BYTES = 64 * 1024;
|
|
47
|
-
|
|
48
|
-
// --- Subscribe app payload (carried inside cohort-topic RegisterV1.appPayload) ---
|
|
49
|
-
|
|
50
|
-
/** A subscriber's registration payload (`RegisterV1.appPayload`, `kind == "reactivity"`). */
|
|
51
|
-
export interface SubscribeAppPayloadV1 {
|
|
52
|
-
kind: "reactivity";
|
|
53
|
-
/** Stable collection identity, base64url (the collection's id block id). */
|
|
54
|
-
collectionId: string;
|
|
55
|
-
/** Tail block id at registration time, base64url (the subscriber-side rotation detector). */
|
|
56
|
-
tailIdAtAttach: string;
|
|
57
|
-
/** Last revision the subscriber already holds; `0` for a fresh subscribe. */
|
|
58
|
-
lastKnownRev: number;
|
|
59
|
-
/** Max delta bytes the subscriber accepts; `0` declines delta payloads (Edge). */
|
|
60
|
-
deltaMaxBytes: number;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// --- Notification (length-framed RPC) ---
|
|
64
|
-
|
|
65
|
-
/** The tail cohort's rotation pre-announce, carried inside (and signed by) a {@link NotificationV1}. */
|
|
66
|
-
export interface RotationHintV1 {
|
|
67
|
-
/** New tail block id the topic anchor is rotating to, base64url. */
|
|
68
|
-
newTailId: string;
|
|
69
|
-
/** Revision at which the new tail becomes effective. */
|
|
70
|
-
effectiveAtRevision: number;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** A signed change notification fanned out through the reactivity tree (`docs/reactivity.md` §Notification origination). */
|
|
74
|
-
export interface NotificationV1 {
|
|
75
|
-
v: 1;
|
|
76
|
-
/** Collection id, base64url. */
|
|
77
|
-
collectionId: string;
|
|
78
|
-
/** Tail block id the reactivity topic is anchored on, base64url. */
|
|
79
|
-
tailId: string;
|
|
80
|
-
/** Per-collection monotonic revision. */
|
|
81
|
-
revision: number;
|
|
82
|
-
/**
|
|
83
|
-
* The commit-vote **signed payload** `utf8(commitHash + ":approve")`, base64url — the exact bytes the
|
|
84
|
-
* threshold signature {@link sig} was computed over. A subscriber threshold-verifies {@link sig} over
|
|
85
|
-
* `b64urlToBytes(digest)`, so this reproduces the signed image against real Ed25519.
|
|
86
|
-
*/
|
|
87
|
-
digest: string;
|
|
88
|
-
/** Optional bounded delta, base64url; omitted when `delta_max == 0` or the collection declines it. */
|
|
89
|
-
delta?: string;
|
|
90
|
-
/** Unix ms. */
|
|
91
|
-
timestamp: number;
|
|
92
|
-
/** Threshold signature, base64url — **= the commit cert**, never re-signed. */
|
|
93
|
-
sig: string;
|
|
94
|
-
/** PeerIds contributing to {@link sig}, base64url of the cohort member-id bytes. */
|
|
95
|
-
signers: string[];
|
|
96
|
-
/** Tail-rotation pre-announce, when this notification carries one (rotation ticket fills it). */
|
|
97
|
-
rotationHint?: RotationHintV1;
|
|
98
|
-
/**
|
|
99
|
-
* `true` iff this notification announces a durable **invalidation** (a reversal of a previously
|
|
100
|
-
* committed action proven invalid by dispute) rather than an ordinary commit
|
|
101
|
-
* (`docs/reactivity.md` §Notification origination, `docs/right-is-right.md` §Durable Invalidation).
|
|
102
|
-
* An invalidation is a committed collection change like any other and rides the same notification
|
|
103
|
-
* path, reusing the **invalidation's** commit cert as {@link sig} (never re-signed) — so it verifies
|
|
104
|
-
* against the tail cohort exactly like a commit notification. The flag is a hint that lets a
|
|
105
|
-
* subscriber react (drop derived results + resubmit) rather than merely refresh; a subscriber that
|
|
106
|
-
* ignores it still re-reads the authoritative reverted state. Omitted (falsy) on commit notifications.
|
|
107
|
-
*/
|
|
108
|
-
invalidation?: boolean;
|
|
109
|
-
/**
|
|
110
|
-
* When {@link invalidation} is set, the original action id that was reversed
|
|
111
|
-
* (`InvalidationEntry.invalidatedActionId`). Lets a client dedup/coalesce the several cascade
|
|
112
|
-
* notifications a single dispute can produce by the action they reverse. Omitted on commits.
|
|
113
|
-
*/
|
|
114
|
-
invalidatedActionId?: string;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// --- validation helpers ---
|
|
118
|
-
// Generic primitives live in ./wire-validate.js (shared across the reactivity codecs); the
|
|
119
|
-
// notification-specific narrowing stays here next to the type it produces.
|
|
120
|
-
|
|
121
|
-
function validateRotationHint(value: unknown, what: string): RotationHintV1 {
|
|
122
|
-
const obj = asObject(value, what);
|
|
123
|
-
return {
|
|
124
|
-
newTailId: b64urlField(reqString(obj, "newTailId", what), "newTailId", what),
|
|
125
|
-
effectiveAtRevision: reqIntInRange(obj, "effectiveAtRevision", what, 0),
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// --- Subscribe app payload (opaque RegisterV1.appPayload bytes) ---
|
|
130
|
-
|
|
131
|
-
/** Narrow an already-parsed value to {@link SubscribeAppPayloadV1}, throwing on any defect. */
|
|
132
|
-
export function validateSubscribeAppPayloadV1(value: unknown): SubscribeAppPayloadV1 {
|
|
133
|
-
const what = "SubscribeAppPayloadV1";
|
|
134
|
-
const obj = asObject(value, what);
|
|
135
|
-
if (obj["kind"] !== "reactivity") {
|
|
136
|
-
fail(`${what}: field "kind" must be exactly "reactivity"`);
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
kind: "reactivity",
|
|
140
|
-
collectionId: b64urlField(reqString(obj, "collectionId", what), "collectionId", what),
|
|
141
|
-
tailIdAtAttach: b64urlField(reqString(obj, "tailIdAtAttach", what), "tailIdAtAttach", what),
|
|
142
|
-
lastKnownRev: reqIntInRange(obj, "lastKnownRev", what, 0),
|
|
143
|
-
deltaMaxBytes: reqIntInRange(obj, "deltaMaxBytes", what, 0),
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Serialize a {@link SubscribeAppPayloadV1} to the opaque UTF-8 JSON bytes for `RegisterV1.appPayload`. */
|
|
148
|
-
export function encodeSubscribeAppPayload(payload: SubscribeAppPayloadV1): Uint8Array {
|
|
149
|
-
return utf8Encoder.encode(JSON.stringify(validateSubscribeAppPayloadV1(payload)));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Decode opaque `RegisterV1.appPayload` bytes back to a validated {@link SubscribeAppPayloadV1}. */
|
|
153
|
-
export function decodeSubscribeAppPayload(bytes: Uint8Array, maxBytes: number = DEFAULT_MAX_SUBSCRIBE_PAYLOAD_BYTES): SubscribeAppPayloadV1 {
|
|
154
|
-
return validateSubscribeAppPayloadV1(parseJsonBytes(bytes, maxBytes, "SubscribeAppPayloadV1"));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Parse opaque (non-framed) UTF-8 JSON payload bytes, rejecting oversized/invalid input. */
|
|
158
|
-
function parseJsonBytes(bytes: Uint8Array, maxBytes: number, what: string): unknown {
|
|
159
|
-
if (bytes.length > maxBytes) {
|
|
160
|
-
fail(`${what}: payload ${bytes.length} exceeds max ${maxBytes} bytes`);
|
|
161
|
-
}
|
|
162
|
-
let text: string;
|
|
163
|
-
try {
|
|
164
|
-
text = utf8Decoder.decode(bytes);
|
|
165
|
-
} catch {
|
|
166
|
-
fail(`${what}: payload is not valid UTF-8`);
|
|
167
|
-
}
|
|
168
|
-
try {
|
|
169
|
-
return JSON.parse(text) as unknown;
|
|
170
|
-
} catch {
|
|
171
|
-
fail(`${what}: payload is not valid JSON`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// --- Notification (length-framed) ---
|
|
176
|
-
|
|
177
|
-
/** Narrow an already-parsed value to {@link NotificationV1}, throwing on any defect. */
|
|
178
|
-
export function validateNotificationV1(value: unknown): NotificationV1 {
|
|
179
|
-
const what = "NotificationV1";
|
|
180
|
-
const obj = asObject(value, what);
|
|
181
|
-
requireV1(obj, what);
|
|
182
|
-
const signers = reqStringArray(obj, "signers", what);
|
|
183
|
-
const out: NotificationV1 = {
|
|
184
|
-
v: 1,
|
|
185
|
-
collectionId: b64urlField(reqString(obj, "collectionId", what), "collectionId", what),
|
|
186
|
-
tailId: b64urlField(reqString(obj, "tailId", what), "tailId", what),
|
|
187
|
-
revision: reqIntInRange(obj, "revision", what, 0),
|
|
188
|
-
digest: b64urlField(reqString(obj, "digest", what), "digest", what),
|
|
189
|
-
timestamp: reqFiniteNumber(obj, "timestamp", what),
|
|
190
|
-
sig: b64urlField(reqString(obj, "sig", what), "sig", what),
|
|
191
|
-
signers: signers.map((s, i) => b64urlField(s, `signers[${i}]`, what)),
|
|
192
|
-
};
|
|
193
|
-
if (obj["delta"] !== undefined) {
|
|
194
|
-
out.delta = b64urlField(reqString(obj, "delta", what), "delta", what);
|
|
195
|
-
}
|
|
196
|
-
if (obj["rotationHint"] !== undefined) {
|
|
197
|
-
out.rotationHint = validateRotationHint(obj["rotationHint"], `${what}.rotationHint`);
|
|
198
|
-
}
|
|
199
|
-
if (obj["invalidation"] !== undefined) {
|
|
200
|
-
out.invalidation = reqBool(obj, "invalidation", what);
|
|
201
|
-
}
|
|
202
|
-
if (obj["invalidatedActionId"] !== undefined) {
|
|
203
|
-
out.invalidatedActionId = reqString(obj, "invalidatedActionId", what);
|
|
204
|
-
}
|
|
205
|
-
return out;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Validate an array of {@link NotificationV1} (the `entries`/`recentEntries` carried in backfill/resume replies). */
|
|
209
|
-
export function validateNotificationArray(value: unknown, what: string): NotificationV1[] {
|
|
210
|
-
if (!Array.isArray(value)) {
|
|
211
|
-
fail(`${what}: field "entries" must be an array`);
|
|
212
|
-
}
|
|
213
|
-
return value.map((entry) => validateNotificationV1(entry));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Encode a {@link NotificationV1} as a length-prefixed UTF-8 JSON frame. */
|
|
217
|
-
export function encodeNotificationV1(msg: NotificationV1, maxMessageBytes: number = DEFAULT_MAX_MESSAGE_BYTES): Uint8Array {
|
|
218
|
-
return encodeCohortMessage(validateNotificationV1(msg), maxMessageBytes);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Decode a length-prefixed {@link NotificationV1} frame. */
|
|
222
|
-
export function decodeNotificationV1(bytes: Uint8Array, maxMessageBytes?: number): NotificationV1 {
|
|
223
|
-
return validateNotificationV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
224
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity — V1 wire types, codecs, and per-message validation.
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from `docs/reactivity.md` §Wire formats. Reactivity reuses the cohort-topic `RegisterV1`
|
|
5
|
+
* / `RenewV1` envelopes **unchanged**; the reactivity-specific shapes live in two places:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Subscribe app payload** ({@link SubscribeAppPayloadV1}) carried opaquely inside
|
|
8
|
+
* `RegisterV1.appPayload`. Serialized to UTF-8 JSON bytes and handed to the cohort-topic
|
|
9
|
+
* {@link import("../cohort-topic/service.js").RegisterRequest}`.appPayload` slot, which base64url-encodes
|
|
10
|
+
* it on the wire. It is therefore **not** length-framed here — the cohort-topic `RegisterV1` frame
|
|
11
|
+
* wraps it. The subscribe RPC's authentication (peer-key signature over `correlationId` + `timestamp`)
|
|
12
|
+
* is the cohort-topic `RegisterV1` envelope's job, so the payload itself carries no signature.
|
|
13
|
+
*
|
|
14
|
+
* 2. **Notification** ({@link NotificationV1}) sent as a standalone RPC over the reactivity application
|
|
15
|
+
* protocol. It rides the same length-prefixed UTF-8 JSON framing as cohort-topic messages
|
|
16
|
+
* ({@link encodeCohortMessage}). Its `sig` is **bit-for-bit** the commit certificate's threshold
|
|
17
|
+
* signature — reactivity never re-signs (`docs/reactivity.md` §Notification origination).
|
|
18
|
+
*
|
|
19
|
+
* Conventions (matching the cohort-topic wire conventions): all JSON, byte fields base64url (no
|
|
20
|
+
* padding), unix-millisecond timestamps, per-message structural validation on decode, byte-fidelity
|
|
21
|
+
* round-trips (encode→decode→encode is stable). `collectionId` / `tailId` travel as the collection's
|
|
22
|
+
* base64url block ids verbatim. The `BackfillV1` / `ResumeV1` codecs belong to the sibling tickets.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
decodeCohortMessage,
|
|
27
|
+
encodeCohortMessage,
|
|
28
|
+
DEFAULT_MAX_MESSAGE_BYTES,
|
|
29
|
+
} from "../cohort-topic/wire/codec.js";
|
|
30
|
+
import {
|
|
31
|
+
asObject,
|
|
32
|
+
b64urlField,
|
|
33
|
+
failWire as fail,
|
|
34
|
+
reqBool,
|
|
35
|
+
reqFiniteNumber,
|
|
36
|
+
reqIntInRange,
|
|
37
|
+
reqString,
|
|
38
|
+
reqStringArray,
|
|
39
|
+
requireV1,
|
|
40
|
+
} from "./wire-validate.js";
|
|
41
|
+
|
|
42
|
+
const utf8Encoder = new TextEncoder();
|
|
43
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
44
|
+
|
|
45
|
+
/** Ceiling for an opaque subscribe app payload, guarding decode allocation. */
|
|
46
|
+
export const DEFAULT_MAX_SUBSCRIBE_PAYLOAD_BYTES = 64 * 1024;
|
|
47
|
+
|
|
48
|
+
// --- Subscribe app payload (carried inside cohort-topic RegisterV1.appPayload) ---
|
|
49
|
+
|
|
50
|
+
/** A subscriber's registration payload (`RegisterV1.appPayload`, `kind == "reactivity"`). */
|
|
51
|
+
export interface SubscribeAppPayloadV1 {
|
|
52
|
+
kind: "reactivity";
|
|
53
|
+
/** Stable collection identity, base64url (the collection's id block id). */
|
|
54
|
+
collectionId: string;
|
|
55
|
+
/** Tail block id at registration time, base64url (the subscriber-side rotation detector). */
|
|
56
|
+
tailIdAtAttach: string;
|
|
57
|
+
/** Last revision the subscriber already holds; `0` for a fresh subscribe. */
|
|
58
|
+
lastKnownRev: number;
|
|
59
|
+
/** Max delta bytes the subscriber accepts; `0` declines delta payloads (Edge). */
|
|
60
|
+
deltaMaxBytes: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Notification (length-framed RPC) ---
|
|
64
|
+
|
|
65
|
+
/** The tail cohort's rotation pre-announce, carried inside (and signed by) a {@link NotificationV1}. */
|
|
66
|
+
export interface RotationHintV1 {
|
|
67
|
+
/** New tail block id the topic anchor is rotating to, base64url. */
|
|
68
|
+
newTailId: string;
|
|
69
|
+
/** Revision at which the new tail becomes effective. */
|
|
70
|
+
effectiveAtRevision: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A signed change notification fanned out through the reactivity tree (`docs/reactivity.md` §Notification origination). */
|
|
74
|
+
export interface NotificationV1 {
|
|
75
|
+
v: 1;
|
|
76
|
+
/** Collection id, base64url. */
|
|
77
|
+
collectionId: string;
|
|
78
|
+
/** Tail block id the reactivity topic is anchored on, base64url. */
|
|
79
|
+
tailId: string;
|
|
80
|
+
/** Per-collection monotonic revision. */
|
|
81
|
+
revision: number;
|
|
82
|
+
/**
|
|
83
|
+
* The commit-vote **signed payload** `utf8(commitHash + ":approve")`, base64url — the exact bytes the
|
|
84
|
+
* threshold signature {@link sig} was computed over. A subscriber threshold-verifies {@link sig} over
|
|
85
|
+
* `b64urlToBytes(digest)`, so this reproduces the signed image against real Ed25519.
|
|
86
|
+
*/
|
|
87
|
+
digest: string;
|
|
88
|
+
/** Optional bounded delta, base64url; omitted when `delta_max == 0` or the collection declines it. */
|
|
89
|
+
delta?: string;
|
|
90
|
+
/** Unix ms. */
|
|
91
|
+
timestamp: number;
|
|
92
|
+
/** Threshold signature, base64url — **= the commit cert**, never re-signed. */
|
|
93
|
+
sig: string;
|
|
94
|
+
/** PeerIds contributing to {@link sig}, base64url of the cohort member-id bytes. */
|
|
95
|
+
signers: string[];
|
|
96
|
+
/** Tail-rotation pre-announce, when this notification carries one (rotation ticket fills it). */
|
|
97
|
+
rotationHint?: RotationHintV1;
|
|
98
|
+
/**
|
|
99
|
+
* `true` iff this notification announces a durable **invalidation** (a reversal of a previously
|
|
100
|
+
* committed action proven invalid by dispute) rather than an ordinary commit
|
|
101
|
+
* (`docs/reactivity.md` §Notification origination, `docs/right-is-right.md` §Durable Invalidation).
|
|
102
|
+
* An invalidation is a committed collection change like any other and rides the same notification
|
|
103
|
+
* path, reusing the **invalidation's** commit cert as {@link sig} (never re-signed) — so it verifies
|
|
104
|
+
* against the tail cohort exactly like a commit notification. The flag is a hint that lets a
|
|
105
|
+
* subscriber react (drop derived results + resubmit) rather than merely refresh; a subscriber that
|
|
106
|
+
* ignores it still re-reads the authoritative reverted state. Omitted (falsy) on commit notifications.
|
|
107
|
+
*/
|
|
108
|
+
invalidation?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* When {@link invalidation} is set, the original action id that was reversed
|
|
111
|
+
* (`InvalidationEntry.invalidatedActionId`). Lets a client dedup/coalesce the several cascade
|
|
112
|
+
* notifications a single dispute can produce by the action they reverse. Omitted on commits.
|
|
113
|
+
*/
|
|
114
|
+
invalidatedActionId?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- validation helpers ---
|
|
118
|
+
// Generic primitives live in ./wire-validate.js (shared across the reactivity codecs); the
|
|
119
|
+
// notification-specific narrowing stays here next to the type it produces.
|
|
120
|
+
|
|
121
|
+
function validateRotationHint(value: unknown, what: string): RotationHintV1 {
|
|
122
|
+
const obj = asObject(value, what);
|
|
123
|
+
return {
|
|
124
|
+
newTailId: b64urlField(reqString(obj, "newTailId", what), "newTailId", what),
|
|
125
|
+
effectiveAtRevision: reqIntInRange(obj, "effectiveAtRevision", what, 0),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- Subscribe app payload (opaque RegisterV1.appPayload bytes) ---
|
|
130
|
+
|
|
131
|
+
/** Narrow an already-parsed value to {@link SubscribeAppPayloadV1}, throwing on any defect. */
|
|
132
|
+
export function validateSubscribeAppPayloadV1(value: unknown): SubscribeAppPayloadV1 {
|
|
133
|
+
const what = "SubscribeAppPayloadV1";
|
|
134
|
+
const obj = asObject(value, what);
|
|
135
|
+
if (obj["kind"] !== "reactivity") {
|
|
136
|
+
fail(`${what}: field "kind" must be exactly "reactivity"`);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
kind: "reactivity",
|
|
140
|
+
collectionId: b64urlField(reqString(obj, "collectionId", what), "collectionId", what),
|
|
141
|
+
tailIdAtAttach: b64urlField(reqString(obj, "tailIdAtAttach", what), "tailIdAtAttach", what),
|
|
142
|
+
lastKnownRev: reqIntInRange(obj, "lastKnownRev", what, 0),
|
|
143
|
+
deltaMaxBytes: reqIntInRange(obj, "deltaMaxBytes", what, 0),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Serialize a {@link SubscribeAppPayloadV1} to the opaque UTF-8 JSON bytes for `RegisterV1.appPayload`. */
|
|
148
|
+
export function encodeSubscribeAppPayload(payload: SubscribeAppPayloadV1): Uint8Array {
|
|
149
|
+
return utf8Encoder.encode(JSON.stringify(validateSubscribeAppPayloadV1(payload)));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Decode opaque `RegisterV1.appPayload` bytes back to a validated {@link SubscribeAppPayloadV1}. */
|
|
153
|
+
export function decodeSubscribeAppPayload(bytes: Uint8Array, maxBytes: number = DEFAULT_MAX_SUBSCRIBE_PAYLOAD_BYTES): SubscribeAppPayloadV1 {
|
|
154
|
+
return validateSubscribeAppPayloadV1(parseJsonBytes(bytes, maxBytes, "SubscribeAppPayloadV1"));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Parse opaque (non-framed) UTF-8 JSON payload bytes, rejecting oversized/invalid input. */
|
|
158
|
+
function parseJsonBytes(bytes: Uint8Array, maxBytes: number, what: string): unknown {
|
|
159
|
+
if (bytes.length > maxBytes) {
|
|
160
|
+
fail(`${what}: payload ${bytes.length} exceeds max ${maxBytes} bytes`);
|
|
161
|
+
}
|
|
162
|
+
let text: string;
|
|
163
|
+
try {
|
|
164
|
+
text = utf8Decoder.decode(bytes);
|
|
165
|
+
} catch {
|
|
166
|
+
fail(`${what}: payload is not valid UTF-8`);
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(text) as unknown;
|
|
170
|
+
} catch {
|
|
171
|
+
fail(`${what}: payload is not valid JSON`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- Notification (length-framed) ---
|
|
176
|
+
|
|
177
|
+
/** Narrow an already-parsed value to {@link NotificationV1}, throwing on any defect. */
|
|
178
|
+
export function validateNotificationV1(value: unknown): NotificationV1 {
|
|
179
|
+
const what = "NotificationV1";
|
|
180
|
+
const obj = asObject(value, what);
|
|
181
|
+
requireV1(obj, what);
|
|
182
|
+
const signers = reqStringArray(obj, "signers", what);
|
|
183
|
+
const out: NotificationV1 = {
|
|
184
|
+
v: 1,
|
|
185
|
+
collectionId: b64urlField(reqString(obj, "collectionId", what), "collectionId", what),
|
|
186
|
+
tailId: b64urlField(reqString(obj, "tailId", what), "tailId", what),
|
|
187
|
+
revision: reqIntInRange(obj, "revision", what, 0),
|
|
188
|
+
digest: b64urlField(reqString(obj, "digest", what), "digest", what),
|
|
189
|
+
timestamp: reqFiniteNumber(obj, "timestamp", what),
|
|
190
|
+
sig: b64urlField(reqString(obj, "sig", what), "sig", what),
|
|
191
|
+
signers: signers.map((s, i) => b64urlField(s, `signers[${i}]`, what)),
|
|
192
|
+
};
|
|
193
|
+
if (obj["delta"] !== undefined) {
|
|
194
|
+
out.delta = b64urlField(reqString(obj, "delta", what), "delta", what);
|
|
195
|
+
}
|
|
196
|
+
if (obj["rotationHint"] !== undefined) {
|
|
197
|
+
out.rotationHint = validateRotationHint(obj["rotationHint"], `${what}.rotationHint`);
|
|
198
|
+
}
|
|
199
|
+
if (obj["invalidation"] !== undefined) {
|
|
200
|
+
out.invalidation = reqBool(obj, "invalidation", what);
|
|
201
|
+
}
|
|
202
|
+
if (obj["invalidatedActionId"] !== undefined) {
|
|
203
|
+
out.invalidatedActionId = reqString(obj, "invalidatedActionId", what);
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Validate an array of {@link NotificationV1} (the `entries`/`recentEntries` carried in backfill/resume replies). */
|
|
209
|
+
export function validateNotificationArray(value: unknown, what: string): NotificationV1[] {
|
|
210
|
+
if (!Array.isArray(value)) {
|
|
211
|
+
fail(`${what}: field "entries" must be an array`);
|
|
212
|
+
}
|
|
213
|
+
return value.map((entry) => validateNotificationV1(entry));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Encode a {@link NotificationV1} as a length-prefixed UTF-8 JSON frame. */
|
|
217
|
+
export function encodeNotificationV1(msg: NotificationV1, maxMessageBytes: number = DEFAULT_MAX_MESSAGE_BYTES): Uint8Array {
|
|
218
|
+
return encodeCohortMessage(validateNotificationV1(msg), maxMessageBytes);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Decode a length-prefixed {@link NotificationV1} frame. */
|
|
222
|
+
export function decodeNotificationV1(bytes: Uint8Array, maxMessageBytes?: number): NotificationV1 {
|
|
223
|
+
return validateNotificationV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
224
|
+
}
|