@optimystic/db-core 0.21.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/btree/btree.d.ts +2 -1
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +1 -1
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/chain/chain.d.ts +1 -1
- package/dist/src/chain/chain.d.ts.map +1 -1
- package/dist/src/chain/chain.js +1 -1
- package/dist/src/chain/chain.js.map +1 -1
- 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 +20 -1
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +31 -4
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/diary/diary.d.ts.map +1 -1
- package/dist/src/collections/diary/diary.js +2 -1
- package/dist/src/collections/diary/diary.js.map +1 -1
- package/dist/src/collections/diary/struct.js +1 -1
- package/dist/src/collections/diary/struct.js.map +1 -1
- package/dist/src/collections/tree/collection-trunk.js +1 -1
- package/dist/src/collections/tree/collection-trunk.js.map +1 -1
- package/dist/src/collections/tree/struct.d.ts +1 -1
- package/dist/src/collections/tree/struct.d.ts.map +1 -1
- package/dist/src/collections/tree/struct.js +2 -1
- package/dist/src/collections/tree/struct.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/log/log.d.ts +1 -1
- package/dist/src/log/log.d.ts.map +1 -1
- package/dist/src/log/log.js +2 -2
- package/dist/src/log/log.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 +133 -12
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +2 -1
- package/dist/src/transaction/coordinator.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 +57 -18
- 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/dist/src/transform/cache-source.js +1 -1
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/helpers.d.ts +6 -1
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +7 -6
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +2 -1
- package/dist/src/transform/tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/btree/btree.ts +2 -1
- package/src/chain/chain.ts +2 -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 +32 -4
- package/src/collections/diary/diary.ts +68 -67
- package/src/collections/diary/struct.ts +1 -1
- package/src/collections/tree/collection-trunk.ts +1 -1
- package/src/collections/tree/readme.md +4 -0
- package/src/collections/tree/struct.ts +3 -1
- package/src/collections/tree/tree.ts +320 -312
- package/src/log/log.ts +2 -2
- 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 -489
- package/src/transaction/coordinator.ts +2 -1
- 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 +58 -19
- package/src/transactor/transactor-source.ts +25 -2
- package/src/transform/atomic-proxy.ts +92 -92
- package/src/transform/cache-source.ts +1 -1
- package/src/transform/helpers.ts +159 -158
- package/src/transform/tracker.ts +2 -1
- 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
|
@@ -1,216 +1,216 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cohort-topic substrate — wire codec.
|
|
3
|
-
*
|
|
4
|
-
* Framing: every message is a 4-byte big-endian unsigned length prefix followed by the UTF-8
|
|
5
|
-
* JSON body, so a framed message is self-delimiting on a stream. Frames whose declared body
|
|
6
|
-
* length exceeds {@link DEFAULT_MAX_MESSAGE_BYTES} (or a caller-supplied ceiling) are rejected
|
|
7
|
-
* with {@link CohortWireError} before any allocation against the declared size.
|
|
8
|
-
*
|
|
9
|
-
* Byte fields travel as base64url (no padding); {@link bytesToB64url} / {@link b64urlToBytes}
|
|
10
|
-
* are the shared, cross-platform (browser/node/RN) helpers — no Node `Buffer`.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { CohortWireError } from "./validate.js";
|
|
14
|
-
import {
|
|
15
|
-
validateChildLinkReplyV1,
|
|
16
|
-
validateChildLinkV1,
|
|
17
|
-
validateCohortGossipV1,
|
|
18
|
-
validateDemotionNoticeV1,
|
|
19
|
-
validateMembershipCertV1,
|
|
20
|
-
validatePromotionNoticeV1,
|
|
21
|
-
validateRegisterReplyV1,
|
|
22
|
-
validateRegisterV1,
|
|
23
|
-
validateRenewReplyV1,
|
|
24
|
-
validateRenewV1,
|
|
25
|
-
validateSignRequestV1,
|
|
26
|
-
validateSignReplyV1,
|
|
27
|
-
} from "./validate.js";
|
|
28
|
-
import type {
|
|
29
|
-
ChildLinkReplyV1,
|
|
30
|
-
ChildLinkV1,
|
|
31
|
-
CohortGossipV1,
|
|
32
|
-
DemotionNoticeV1,
|
|
33
|
-
MembershipCertV1,
|
|
34
|
-
PromotionNoticeV1,
|
|
35
|
-
RegisterReplyV1,
|
|
36
|
-
RegisterV1,
|
|
37
|
-
RenewReplyV1,
|
|
38
|
-
RenewV1,
|
|
39
|
-
SignRequestV1,
|
|
40
|
-
SignReplyV1,
|
|
41
|
-
} from "./types.js";
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Default ceiling on a framed body, in bytes. Sized conservatively at 1 MiB until the
|
|
45
|
-
* simulator-validated cohort-gossip worst case (`topics_max` topic summaries, see
|
|
46
|
-
* `docs/cohort-topic.md` §Configuration) is converted into an exact bound.
|
|
47
|
-
*
|
|
48
|
-
* TODO(cohort-topic): derive the exact ceiling from `topics_max` (2048) × the per-summary JSON
|
|
49
|
-
* size plus the willingness/load header, rather than this flat 1 MiB cap.
|
|
50
|
-
*/
|
|
51
|
-
export const DEFAULT_MAX_MESSAGE_BYTES = 1024 * 1024;
|
|
52
|
-
|
|
53
|
-
const LENGTH_PREFIX_BYTES = 4;
|
|
54
|
-
|
|
55
|
-
const utf8Encoder = new TextEncoder();
|
|
56
|
-
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
57
|
-
|
|
58
|
-
// --- base64url helpers (no padding, cross-platform) ---
|
|
59
|
-
|
|
60
|
-
const B64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
61
|
-
|
|
62
|
-
const B64URL_LOOKUP: ReadonlyArray<number> = ((): number[] => {
|
|
63
|
-
const table = new Array<number>(128).fill(-1);
|
|
64
|
-
for (let i = 0; i < B64URL_ALPHABET.length; i++) {
|
|
65
|
-
table[B64URL_ALPHABET.charCodeAt(i)] = i;
|
|
66
|
-
}
|
|
67
|
-
return table;
|
|
68
|
-
})();
|
|
69
|
-
|
|
70
|
-
/** Encode bytes as base64url with no padding. */
|
|
71
|
-
export function bytesToB64url(b: Uint8Array): string {
|
|
72
|
-
let out = "";
|
|
73
|
-
let i = 0;
|
|
74
|
-
for (; i + 3 <= b.length; i += 3) {
|
|
75
|
-
const n = (b[i]! << 16) | (b[i + 1]! << 8) | b[i + 2]!;
|
|
76
|
-
out += B64URL_ALPHABET[(n >> 18) & 63]! + B64URL_ALPHABET[(n >> 12) & 63]! + B64URL_ALPHABET[(n >> 6) & 63]! + B64URL_ALPHABET[n & 63]!;
|
|
77
|
-
}
|
|
78
|
-
const rem = b.length - i;
|
|
79
|
-
if (rem === 1) {
|
|
80
|
-
const n = b[i]! << 16;
|
|
81
|
-
out += B64URL_ALPHABET[(n >> 18) & 63]! + B64URL_ALPHABET[(n >> 12) & 63]!;
|
|
82
|
-
} else if (rem === 2) {
|
|
83
|
-
const n = (b[i]! << 16) | (b[i + 1]! << 8);
|
|
84
|
-
out += B64URL_ALPHABET[(n >> 18) & 63]! + B64URL_ALPHABET[(n >> 12) & 63]! + B64URL_ALPHABET[(n >> 6) & 63]!;
|
|
85
|
-
}
|
|
86
|
-
return out;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Decode a base64url string (no padding) to bytes. Throws {@link CohortWireError} on bad input. */
|
|
90
|
-
export function b64urlToBytes(s: string): Uint8Array {
|
|
91
|
-
const len = s.length;
|
|
92
|
-
if (len % 4 === 1) {
|
|
93
|
-
throw new CohortWireError("base64url: invalid length");
|
|
94
|
-
}
|
|
95
|
-
const fullGroups = len >> 2;
|
|
96
|
-
const rem = len & 3;
|
|
97
|
-
const outLen = fullGroups * 3 + (rem === 0 ? 0 : rem - 1);
|
|
98
|
-
const out = new Uint8Array(outLen);
|
|
99
|
-
let si = 0;
|
|
100
|
-
let oi = 0;
|
|
101
|
-
const sext = (ch: number): number => {
|
|
102
|
-
const v = ch < 128 ? B64URL_LOOKUP[ch]! : -1;
|
|
103
|
-
if (v < 0) {
|
|
104
|
-
throw new CohortWireError("base64url: invalid character");
|
|
105
|
-
}
|
|
106
|
-
return v;
|
|
107
|
-
};
|
|
108
|
-
for (let g = 0; g < fullGroups; g++) {
|
|
109
|
-
const n = (sext(s.charCodeAt(si)) << 18) | (sext(s.charCodeAt(si + 1)) << 12) | (sext(s.charCodeAt(si + 2)) << 6) | sext(s.charCodeAt(si + 3));
|
|
110
|
-
out[oi++] = (n >> 16) & 0xff;
|
|
111
|
-
out[oi++] = (n >> 8) & 0xff;
|
|
112
|
-
out[oi++] = n & 0xff;
|
|
113
|
-
si += 4;
|
|
114
|
-
}
|
|
115
|
-
if (rem === 2) {
|
|
116
|
-
const n = (sext(s.charCodeAt(si)) << 18) | (sext(s.charCodeAt(si + 1)) << 12);
|
|
117
|
-
out[oi++] = (n >> 16) & 0xff;
|
|
118
|
-
} else if (rem === 3) {
|
|
119
|
-
const n = (sext(s.charCodeAt(si)) << 18) | (sext(s.charCodeAt(si + 1)) << 12) | (sext(s.charCodeAt(si + 2)) << 6);
|
|
120
|
-
out[oi++] = (n >> 16) & 0xff;
|
|
121
|
-
out[oi++] = (n >> 8) & 0xff;
|
|
122
|
-
}
|
|
123
|
-
return out;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// --- framing ---
|
|
127
|
-
|
|
128
|
-
/** Encode a V1 message as a length-prefixed UTF-8 JSON frame. */
|
|
129
|
-
export function encodeCohortMessage<T extends { v: 1 }>(msg: T, maxMessageBytes: number = DEFAULT_MAX_MESSAGE_BYTES): Uint8Array {
|
|
130
|
-
const body = utf8Encoder.encode(JSON.stringify(msg));
|
|
131
|
-
if (body.length > maxMessageBytes) {
|
|
132
|
-
throw new CohortWireError(`message body ${body.length} exceeds max_message_bytes ${maxMessageBytes}`);
|
|
133
|
-
}
|
|
134
|
-
const frame = new Uint8Array(LENGTH_PREFIX_BYTES + body.length);
|
|
135
|
-
const view = new DataView(frame.buffer);
|
|
136
|
-
view.setUint32(0, body.length, false);
|
|
137
|
-
frame.set(body, LENGTH_PREFIX_BYTES);
|
|
138
|
-
return frame;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Strip the length prefix and `JSON.parse` the body. Throws {@link CohortWireError} on a malformed frame. */
|
|
142
|
-
export function decodeCohortMessage(bytes: Uint8Array, maxMessageBytes: number = DEFAULT_MAX_MESSAGE_BYTES): unknown {
|
|
143
|
-
if (bytes.length < LENGTH_PREFIX_BYTES) {
|
|
144
|
-
throw new CohortWireError("frame too short for length prefix");
|
|
145
|
-
}
|
|
146
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
147
|
-
const declared = view.getUint32(0, false);
|
|
148
|
-
if (declared > maxMessageBytes) {
|
|
149
|
-
throw new CohortWireError(`declared body length ${declared} exceeds max_message_bytes ${maxMessageBytes}`);
|
|
150
|
-
}
|
|
151
|
-
if (LENGTH_PREFIX_BYTES + declared !== bytes.length) {
|
|
152
|
-
throw new CohortWireError(`frame length mismatch: declared ${declared}, have ${bytes.length - LENGTH_PREFIX_BYTES}`);
|
|
153
|
-
}
|
|
154
|
-
const body = bytes.subarray(LENGTH_PREFIX_BYTES, LENGTH_PREFIX_BYTES + declared);
|
|
155
|
-
let text: string;
|
|
156
|
-
try {
|
|
157
|
-
text = utf8Decoder.decode(body);
|
|
158
|
-
} catch {
|
|
159
|
-
throw new CohortWireError("frame body is not valid UTF-8");
|
|
160
|
-
}
|
|
161
|
-
try {
|
|
162
|
-
return JSON.parse(text) as unknown;
|
|
163
|
-
} catch {
|
|
164
|
-
throw new CohortWireError("frame body is not valid JSON");
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// --- typed decoders (decode frame → validate → narrow) ---
|
|
169
|
-
|
|
170
|
-
export function decodeRegisterV1(bytes: Uint8Array, maxMessageBytes?: number): RegisterV1 {
|
|
171
|
-
return validateRegisterV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function decodeRegisterReplyV1(bytes: Uint8Array, maxMessageBytes?: number): RegisterReplyV1 {
|
|
175
|
-
return validateRegisterReplyV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function decodeRenewV1(bytes: Uint8Array, maxMessageBytes?: number): RenewV1 {
|
|
179
|
-
return validateRenewV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function decodeRenewReplyV1(bytes: Uint8Array, maxMessageBytes?: number): RenewReplyV1 {
|
|
183
|
-
return validateRenewReplyV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function decodeChildLinkV1(bytes: Uint8Array, maxMessageBytes?: number, minSigs?: number): ChildLinkV1 {
|
|
187
|
-
return validateChildLinkV1(decodeCohortMessage(bytes, maxMessageBytes), minSigs);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export function decodeChildLinkReplyV1(bytes: Uint8Array, maxMessageBytes?: number): ChildLinkReplyV1 {
|
|
191
|
-
return validateChildLinkReplyV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export function decodePromotionNoticeV1(bytes: Uint8Array, maxMessageBytes?: number): PromotionNoticeV1 {
|
|
195
|
-
return validatePromotionNoticeV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function decodeDemotionNoticeV1(bytes: Uint8Array, maxMessageBytes?: number): DemotionNoticeV1 {
|
|
199
|
-
return validateDemotionNoticeV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export function decodeCohortGossipV1(bytes: Uint8Array, maxMessageBytes?: number): CohortGossipV1 {
|
|
203
|
-
return validateCohortGossipV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function decodeMembershipCertV1(bytes: Uint8Array, maxMessageBytes?: number): MembershipCertV1 {
|
|
207
|
-
return validateMembershipCertV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function decodeSignRequestV1(bytes: Uint8Array, maxMessageBytes?: number): SignRequestV1 {
|
|
211
|
-
return validateSignRequestV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function decodeSignReplyV1(bytes: Uint8Array, maxMessageBytes?: number): SignReplyV1 {
|
|
215
|
-
return validateSignReplyV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
216
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — wire codec.
|
|
3
|
+
*
|
|
4
|
+
* Framing: every message is a 4-byte big-endian unsigned length prefix followed by the UTF-8
|
|
5
|
+
* JSON body, so a framed message is self-delimiting on a stream. Frames whose declared body
|
|
6
|
+
* length exceeds {@link DEFAULT_MAX_MESSAGE_BYTES} (or a caller-supplied ceiling) are rejected
|
|
7
|
+
* with {@link CohortWireError} before any allocation against the declared size.
|
|
8
|
+
*
|
|
9
|
+
* Byte fields travel as base64url (no padding); {@link bytesToB64url} / {@link b64urlToBytes}
|
|
10
|
+
* are the shared, cross-platform (browser/node/RN) helpers — no Node `Buffer`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { CohortWireError } from "./validate.js";
|
|
14
|
+
import {
|
|
15
|
+
validateChildLinkReplyV1,
|
|
16
|
+
validateChildLinkV1,
|
|
17
|
+
validateCohortGossipV1,
|
|
18
|
+
validateDemotionNoticeV1,
|
|
19
|
+
validateMembershipCertV1,
|
|
20
|
+
validatePromotionNoticeV1,
|
|
21
|
+
validateRegisterReplyV1,
|
|
22
|
+
validateRegisterV1,
|
|
23
|
+
validateRenewReplyV1,
|
|
24
|
+
validateRenewV1,
|
|
25
|
+
validateSignRequestV1,
|
|
26
|
+
validateSignReplyV1,
|
|
27
|
+
} from "./validate.js";
|
|
28
|
+
import type {
|
|
29
|
+
ChildLinkReplyV1,
|
|
30
|
+
ChildLinkV1,
|
|
31
|
+
CohortGossipV1,
|
|
32
|
+
DemotionNoticeV1,
|
|
33
|
+
MembershipCertV1,
|
|
34
|
+
PromotionNoticeV1,
|
|
35
|
+
RegisterReplyV1,
|
|
36
|
+
RegisterV1,
|
|
37
|
+
RenewReplyV1,
|
|
38
|
+
RenewV1,
|
|
39
|
+
SignRequestV1,
|
|
40
|
+
SignReplyV1,
|
|
41
|
+
} from "./types.js";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Default ceiling on a framed body, in bytes. Sized conservatively at 1 MiB until the
|
|
45
|
+
* simulator-validated cohort-gossip worst case (`topics_max` topic summaries, see
|
|
46
|
+
* `docs/cohort-topic.md` §Configuration) is converted into an exact bound.
|
|
47
|
+
*
|
|
48
|
+
* TODO(cohort-topic): derive the exact ceiling from `topics_max` (2048) × the per-summary JSON
|
|
49
|
+
* size plus the willingness/load header, rather than this flat 1 MiB cap.
|
|
50
|
+
*/
|
|
51
|
+
export const DEFAULT_MAX_MESSAGE_BYTES = 1024 * 1024;
|
|
52
|
+
|
|
53
|
+
const LENGTH_PREFIX_BYTES = 4;
|
|
54
|
+
|
|
55
|
+
const utf8Encoder = new TextEncoder();
|
|
56
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
57
|
+
|
|
58
|
+
// --- base64url helpers (no padding, cross-platform) ---
|
|
59
|
+
|
|
60
|
+
const B64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
61
|
+
|
|
62
|
+
const B64URL_LOOKUP: ReadonlyArray<number> = ((): number[] => {
|
|
63
|
+
const table = new Array<number>(128).fill(-1);
|
|
64
|
+
for (let i = 0; i < B64URL_ALPHABET.length; i++) {
|
|
65
|
+
table[B64URL_ALPHABET.charCodeAt(i)] = i;
|
|
66
|
+
}
|
|
67
|
+
return table;
|
|
68
|
+
})();
|
|
69
|
+
|
|
70
|
+
/** Encode bytes as base64url with no padding. */
|
|
71
|
+
export function bytesToB64url(b: Uint8Array): string {
|
|
72
|
+
let out = "";
|
|
73
|
+
let i = 0;
|
|
74
|
+
for (; i + 3 <= b.length; i += 3) {
|
|
75
|
+
const n = (b[i]! << 16) | (b[i + 1]! << 8) | b[i + 2]!;
|
|
76
|
+
out += B64URL_ALPHABET[(n >> 18) & 63]! + B64URL_ALPHABET[(n >> 12) & 63]! + B64URL_ALPHABET[(n >> 6) & 63]! + B64URL_ALPHABET[n & 63]!;
|
|
77
|
+
}
|
|
78
|
+
const rem = b.length - i;
|
|
79
|
+
if (rem === 1) {
|
|
80
|
+
const n = b[i]! << 16;
|
|
81
|
+
out += B64URL_ALPHABET[(n >> 18) & 63]! + B64URL_ALPHABET[(n >> 12) & 63]!;
|
|
82
|
+
} else if (rem === 2) {
|
|
83
|
+
const n = (b[i]! << 16) | (b[i + 1]! << 8);
|
|
84
|
+
out += B64URL_ALPHABET[(n >> 18) & 63]! + B64URL_ALPHABET[(n >> 12) & 63]! + B64URL_ALPHABET[(n >> 6) & 63]!;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Decode a base64url string (no padding) to bytes. Throws {@link CohortWireError} on bad input. */
|
|
90
|
+
export function b64urlToBytes(s: string): Uint8Array {
|
|
91
|
+
const len = s.length;
|
|
92
|
+
if (len % 4 === 1) {
|
|
93
|
+
throw new CohortWireError("base64url: invalid length");
|
|
94
|
+
}
|
|
95
|
+
const fullGroups = len >> 2;
|
|
96
|
+
const rem = len & 3;
|
|
97
|
+
const outLen = fullGroups * 3 + (rem === 0 ? 0 : rem - 1);
|
|
98
|
+
const out = new Uint8Array(outLen);
|
|
99
|
+
let si = 0;
|
|
100
|
+
let oi = 0;
|
|
101
|
+
const sext = (ch: number): number => {
|
|
102
|
+
const v = ch < 128 ? B64URL_LOOKUP[ch]! : -1;
|
|
103
|
+
if (v < 0) {
|
|
104
|
+
throw new CohortWireError("base64url: invalid character");
|
|
105
|
+
}
|
|
106
|
+
return v;
|
|
107
|
+
};
|
|
108
|
+
for (let g = 0; g < fullGroups; g++) {
|
|
109
|
+
const n = (sext(s.charCodeAt(si)) << 18) | (sext(s.charCodeAt(si + 1)) << 12) | (sext(s.charCodeAt(si + 2)) << 6) | sext(s.charCodeAt(si + 3));
|
|
110
|
+
out[oi++] = (n >> 16) & 0xff;
|
|
111
|
+
out[oi++] = (n >> 8) & 0xff;
|
|
112
|
+
out[oi++] = n & 0xff;
|
|
113
|
+
si += 4;
|
|
114
|
+
}
|
|
115
|
+
if (rem === 2) {
|
|
116
|
+
const n = (sext(s.charCodeAt(si)) << 18) | (sext(s.charCodeAt(si + 1)) << 12);
|
|
117
|
+
out[oi++] = (n >> 16) & 0xff;
|
|
118
|
+
} else if (rem === 3) {
|
|
119
|
+
const n = (sext(s.charCodeAt(si)) << 18) | (sext(s.charCodeAt(si + 1)) << 12) | (sext(s.charCodeAt(si + 2)) << 6);
|
|
120
|
+
out[oi++] = (n >> 16) & 0xff;
|
|
121
|
+
out[oi++] = (n >> 8) & 0xff;
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- framing ---
|
|
127
|
+
|
|
128
|
+
/** Encode a V1 message as a length-prefixed UTF-8 JSON frame. */
|
|
129
|
+
export function encodeCohortMessage<T extends { v: 1 }>(msg: T, maxMessageBytes: number = DEFAULT_MAX_MESSAGE_BYTES): Uint8Array {
|
|
130
|
+
const body = utf8Encoder.encode(JSON.stringify(msg));
|
|
131
|
+
if (body.length > maxMessageBytes) {
|
|
132
|
+
throw new CohortWireError(`message body ${body.length} exceeds max_message_bytes ${maxMessageBytes}`);
|
|
133
|
+
}
|
|
134
|
+
const frame = new Uint8Array(LENGTH_PREFIX_BYTES + body.length);
|
|
135
|
+
const view = new DataView(frame.buffer);
|
|
136
|
+
view.setUint32(0, body.length, false);
|
|
137
|
+
frame.set(body, LENGTH_PREFIX_BYTES);
|
|
138
|
+
return frame;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Strip the length prefix and `JSON.parse` the body. Throws {@link CohortWireError} on a malformed frame. */
|
|
142
|
+
export function decodeCohortMessage(bytes: Uint8Array, maxMessageBytes: number = DEFAULT_MAX_MESSAGE_BYTES): unknown {
|
|
143
|
+
if (bytes.length < LENGTH_PREFIX_BYTES) {
|
|
144
|
+
throw new CohortWireError("frame too short for length prefix");
|
|
145
|
+
}
|
|
146
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
147
|
+
const declared = view.getUint32(0, false);
|
|
148
|
+
if (declared > maxMessageBytes) {
|
|
149
|
+
throw new CohortWireError(`declared body length ${declared} exceeds max_message_bytes ${maxMessageBytes}`);
|
|
150
|
+
}
|
|
151
|
+
if (LENGTH_PREFIX_BYTES + declared !== bytes.length) {
|
|
152
|
+
throw new CohortWireError(`frame length mismatch: declared ${declared}, have ${bytes.length - LENGTH_PREFIX_BYTES}`);
|
|
153
|
+
}
|
|
154
|
+
const body = bytes.subarray(LENGTH_PREFIX_BYTES, LENGTH_PREFIX_BYTES + declared);
|
|
155
|
+
let text: string;
|
|
156
|
+
try {
|
|
157
|
+
text = utf8Decoder.decode(body);
|
|
158
|
+
} catch {
|
|
159
|
+
throw new CohortWireError("frame body is not valid UTF-8");
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
return JSON.parse(text) as unknown;
|
|
163
|
+
} catch {
|
|
164
|
+
throw new CohortWireError("frame body is not valid JSON");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- typed decoders (decode frame → validate → narrow) ---
|
|
169
|
+
|
|
170
|
+
export function decodeRegisterV1(bytes: Uint8Array, maxMessageBytes?: number): RegisterV1 {
|
|
171
|
+
return validateRegisterV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function decodeRegisterReplyV1(bytes: Uint8Array, maxMessageBytes?: number): RegisterReplyV1 {
|
|
175
|
+
return validateRegisterReplyV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function decodeRenewV1(bytes: Uint8Array, maxMessageBytes?: number): RenewV1 {
|
|
179
|
+
return validateRenewV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function decodeRenewReplyV1(bytes: Uint8Array, maxMessageBytes?: number): RenewReplyV1 {
|
|
183
|
+
return validateRenewReplyV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function decodeChildLinkV1(bytes: Uint8Array, maxMessageBytes?: number, minSigs?: number): ChildLinkV1 {
|
|
187
|
+
return validateChildLinkV1(decodeCohortMessage(bytes, maxMessageBytes), minSigs);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function decodeChildLinkReplyV1(bytes: Uint8Array, maxMessageBytes?: number): ChildLinkReplyV1 {
|
|
191
|
+
return validateChildLinkReplyV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function decodePromotionNoticeV1(bytes: Uint8Array, maxMessageBytes?: number): PromotionNoticeV1 {
|
|
195
|
+
return validatePromotionNoticeV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function decodeDemotionNoticeV1(bytes: Uint8Array, maxMessageBytes?: number): DemotionNoticeV1 {
|
|
199
|
+
return validateDemotionNoticeV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function decodeCohortGossipV1(bytes: Uint8Array, maxMessageBytes?: number): CohortGossipV1 {
|
|
203
|
+
return validateCohortGossipV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function decodeMembershipCertV1(bytes: Uint8Array, maxMessageBytes?: number): MembershipCertV1 {
|
|
207
|
+
return validateMembershipCertV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function decodeSignRequestV1(bytes: Uint8Array, maxMessageBytes?: number): SignRequestV1 {
|
|
211
|
+
return validateSignRequestV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function decodeSignReplyV1(bytes: Uint8Array, maxMessageBytes?: number): SignReplyV1 {
|
|
215
|
+
return validateSignReplyV1(decodeCohortMessage(bytes, maxMessageBytes));
|
|
216
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
export * from "./types.js";
|
|
2
|
-
export * from "./codec.js";
|
|
3
|
-
export * from "./payloads.js";
|
|
4
|
-
export { CohortWireError } from "./validate.js";
|
|
5
|
-
export {
|
|
6
|
-
validateRegisterV1,
|
|
7
|
-
validateRegisterReplyV1,
|
|
8
|
-
validateRenewV1,
|
|
9
|
-
validateRenewReplyV1,
|
|
10
|
-
validateChildLinkV1,
|
|
11
|
-
validateChildLinkReplyV1,
|
|
12
|
-
validatePromotionNoticeV1,
|
|
13
|
-
validateDemotionNoticeV1,
|
|
14
|
-
validateCohortGossipV1,
|
|
15
|
-
validateMembershipCertV1,
|
|
16
|
-
validateSignRequestV1,
|
|
17
|
-
validateSignReplyV1,
|
|
18
|
-
} from "./validate.js";
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./codec.js";
|
|
3
|
+
export * from "./payloads.js";
|
|
4
|
+
export { CohortWireError } from "./validate.js";
|
|
5
|
+
export {
|
|
6
|
+
validateRegisterV1,
|
|
7
|
+
validateRegisterReplyV1,
|
|
8
|
+
validateRenewV1,
|
|
9
|
+
validateRenewReplyV1,
|
|
10
|
+
validateChildLinkV1,
|
|
11
|
+
validateChildLinkReplyV1,
|
|
12
|
+
validatePromotionNoticeV1,
|
|
13
|
+
validateDemotionNoticeV1,
|
|
14
|
+
validateCohortGossipV1,
|
|
15
|
+
validateMembershipCertV1,
|
|
16
|
+
validateSignRequestV1,
|
|
17
|
+
validateSignReplyV1,
|
|
18
|
+
} from "./validate.js";
|