@floegence/flowersec-core 2.5.4 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +46 -18
  2. package/dist/browser/connectSessionV3.d.ts +13 -0
  3. package/dist/browser/connectSessionV3.js +75 -0
  4. package/dist/browser/index.d.ts +3 -2
  5. package/dist/browser/index.js +2 -1
  6. package/dist/browser/v2.d.ts +3 -0
  7. package/dist/browser/v2.js +2 -0
  8. package/dist/facade.d.ts +15 -8
  9. package/dist/facade.js +5 -4
  10. package/dist/interop/proxyServerPeer.js +1 -1
  11. package/dist/interop/serverParityPeer.js +8 -1
  12. package/dist/node/acceptor.d.ts +10 -0
  13. package/dist/node/acceptor.js +45 -2
  14. package/dist/node/acceptorV3.d.ts +57 -0
  15. package/dist/node/acceptorV3.js +227 -0
  16. package/dist/node/connectSessionV3.d.ts +21 -0
  17. package/dist/node/connectSessionV3.js +73 -0
  18. package/dist/node/index.d.ts +15 -10
  19. package/dist/node/index.js +6 -5
  20. package/dist/node/nativeTransportAddon.d.ts +39 -1
  21. package/dist/node/nativeTransportAddon.js +29 -5
  22. package/dist/node/rawQuicAdapterV3.d.ts +10 -0
  23. package/dist/node/rawQuicAdapterV3.js +76 -0
  24. package/dist/node/rawQuicServerV3.d.ts +24 -0
  25. package/dist/node/rawQuicServerV3.js +34 -0
  26. package/dist/node/runtimeAuthorizationV3.d.ts +29 -0
  27. package/dist/node/runtimeAuthorizationV3.js +126 -0
  28. package/dist/node/tunnelRuntimeV3.d.ts +57 -0
  29. package/dist/node/tunnelRuntimeV3.js +857 -0
  30. package/dist/node/v2.d.ts +11 -0
  31. package/dist/node/v2.js +6 -0
  32. package/dist/node/webSocketServerV3.d.ts +27 -0
  33. package/dist/node/webSocketServerV3.js +219 -0
  34. package/dist/public/streamHandlers.d.ts +3 -0
  35. package/dist/public/streamHandlers.js +6 -1
  36. package/dist/v2/index.d.ts +14 -0
  37. package/dist/v2/index.js +7 -0
  38. package/dist/v2/protocol.js +23 -32
  39. package/dist/v2/session.d.ts +5 -0
  40. package/dist/v2/session.js +90 -35
  41. package/dist/v3/admissionError.d.ts +4 -0
  42. package/dist/v3/admissionError.js +8 -0
  43. package/dist/v3/artifact.d.ts +145 -0
  44. package/dist/v3/artifact.js +1162 -0
  45. package/dist/v3/artifactLease.d.ts +17 -0
  46. package/dist/v3/artifactLease.js +109 -0
  47. package/dist/v3/browserRuntime.d.ts +37 -0
  48. package/dist/v3/browserRuntime.js +197 -0
  49. package/dist/v3/browserSessionRuntime.d.ts +2 -0
  50. package/dist/v3/browserSessionRuntime.js +21 -0
  51. package/dist/v3/capability.d.ts +32 -0
  52. package/dist/v3/capability.js +345 -0
  53. package/dist/v3/carrier.d.ts +84 -0
  54. package/dist/v3/carrier.js +411 -0
  55. package/dist/v3/connectionController.d.ts +87 -0
  56. package/dist/v3/connectionController.js +805 -0
  57. package/dist/v3/contract.d.ts +65 -0
  58. package/dist/v3/contract.js +3 -0
  59. package/dist/v3/controller.d.ts +43 -0
  60. package/dist/v3/controller.js +303 -0
  61. package/dist/v3/handshake.d.ts +81 -0
  62. package/dist/v3/handshake.js +471 -0
  63. package/dist/v3/jcs.d.ts +6 -0
  64. package/dist/v3/jcs.js +69 -0
  65. package/dist/v3/jsonPreflight.d.ts +1 -0
  66. package/dist/v3/jsonPreflight.js +251 -0
  67. package/dist/v3/nodeRuntime.d.ts +40 -0
  68. package/dist/v3/nodeRuntime.js +315 -0
  69. package/dist/v3/nodeSessionRuntime.d.ts +2 -0
  70. package/dist/v3/nodeSessionRuntime.js +13 -0
  71. package/dist/v3/protocol.d.ts +99 -0
  72. package/dist/v3/protocol.js +773 -0
  73. package/dist/v3/publicApi.d.ts +12 -0
  74. package/dist/v3/publicApi.js +46 -0
  75. package/dist/v3/publicSession.d.ts +1 -0
  76. package/dist/v3/publicSession.js +281 -0
  77. package/dist/v3/retryDisposition.d.ts +16 -0
  78. package/dist/v3/retryDisposition.js +30 -0
  79. package/dist/v3/runtimeAdapters.d.ts +9 -0
  80. package/dist/v3/runtimeAdapters.js +109 -0
  81. package/dist/v3/security.d.ts +34 -0
  82. package/dist/v3/security.js +100 -0
  83. package/dist/v3/serverAdmission.d.ts +38 -0
  84. package/dist/v3/serverAdmission.js +223 -0
  85. package/dist/v3/session.d.ts +318 -0
  86. package/dist/v3/session.js +2289 -0
  87. package/dist/v3/sessionConnector.d.ts +37 -0
  88. package/dist/v3/sessionConnector.js +440 -0
  89. package/dist/v3/streamLifetimeLedger.d.ts +31 -0
  90. package/dist/v3/streamLifetimeLedger.js +131 -0
  91. package/dist/v3/streamMetadata.d.ts +1 -0
  92. package/dist/v3/streamMetadata.js +7 -0
  93. package/dist/v3/transportConstants.d.ts +56 -0
  94. package/dist/v3/transportConstants.js +59 -0
  95. package/dist/v3/unreliableMessage.d.ts +44 -0
  96. package/dist/v3/unreliableMessage.js +284 -0
  97. package/dist/v3/webSocketCarrier.d.ts +31 -0
  98. package/dist/v3/webSocketCarrier.js +370 -0
  99. package/dist/v3/webTransportCarrier.d.ts +27 -0
  100. package/dist/v3/webTransportCarrier.js +534 -0
  101. package/package.json +4 -4
  102. package/sbom/cyclonedx.json +6 -6
  103. package/sbom/spdx.json +11 -11
@@ -0,0 +1,471 @@
1
+ import { x25519 } from "@noble/curves/ed25519.js";
2
+ import { p256 } from "@noble/curves/nist.js";
3
+ import { expand, extract } from "@noble/hashes/hkdf.js";
4
+ import { hmac } from "@noble/hashes/hmac.js";
5
+ import { sha256 } from "@noble/hashes/sha2.js";
6
+ import { base64urlDecode, base64urlEncode } from "../utils/base64url.js";
7
+ import { concatBytes, readU32be, u32be } from "../utils/bin.js";
8
+ import { CipherSuiteV3, ProtocolV3Error } from "./protocol.js";
9
+ import { FLOWERSEC_V3_CRYPTO_LABELS, FLOWERSEC_V3_PROFILE } from "./transportConstants.js";
10
+ const encoder = new TextEncoder();
11
+ const decoder = new TextDecoder("utf-8", { fatal: true });
12
+ const CONTROL_PREFACE_BYTES = 16;
13
+ const HANDSHAKE_HEADER_BYTES = 12;
14
+ const MAX_HANDSHAKE_PAYLOAD_BYTES = 8_192;
15
+ const registryIDPattern = /^[A-Za-z0-9._~-]+$/;
16
+ const KNOWN_FEATURES_V3 = 0x00000001;
17
+ export var HandshakeMessageTypeV3;
18
+ (function (HandshakeMessageTypeV3) {
19
+ HandshakeMessageTypeV3[HandshakeMessageTypeV3["ClientInit"] = 1] = "ClientInit";
20
+ HandshakeMessageTypeV3[HandshakeMessageTypeV3["ServerFinished"] = 2] = "ServerFinished";
21
+ HandshakeMessageTypeV3[HandshakeMessageTypeV3["ClientFinished"] = 3] = "ClientFinished";
22
+ })(HandshakeMessageTypeV3 || (HandshakeMessageTypeV3 = {}));
23
+ export function encodeControlPrefaceV3() {
24
+ const out = new Uint8Array(CONTROL_PREFACE_BYTES);
25
+ out.set(encoder.encode("FSC3"));
26
+ out[4] = 3;
27
+ out[5] = 1;
28
+ return out;
29
+ }
30
+ export function parseControlPrefaceV3(raw) {
31
+ if (raw.length !== CONTROL_PREFACE_BYTES ||
32
+ raw[0] !== 0x46 || raw[1] !== 0x53 || raw[2] !== 0x43 || raw[3] !== 0x33 ||
33
+ raw[4] !== 3 || raw[5] !== 1 || raw.subarray(6).some((value) => value !== 0)) {
34
+ throw new ProtocolV3Error("invalid FSC3 control preface");
35
+ }
36
+ }
37
+ export function encodeHandshakeFrameV3(type, payload) {
38
+ if (type < HandshakeMessageTypeV3.ClientInit ||
39
+ type > HandshakeMessageTypeV3.ClientFinished ||
40
+ payload.length < 1 ||
41
+ payload.length > MAX_HANDSHAKE_PAYLOAD_BYTES) {
42
+ throw new ProtocolV3Error("invalid FSH3 handshake frame");
43
+ }
44
+ const out = new Uint8Array(HANDSHAKE_HEADER_BYTES + payload.length);
45
+ out.set(encoder.encode("FSH3"));
46
+ out[4] = 3;
47
+ out[5] = type;
48
+ out.set(u32be(payload.length), 8);
49
+ out.set(payload, HANDSHAKE_HEADER_BYTES);
50
+ return out;
51
+ }
52
+ export function decodeHandshakeFrameV3(raw) {
53
+ if (raw.length < HANDSHAKE_HEADER_BYTES ||
54
+ raw[0] !== 0x46 || raw[1] !== 0x53 || raw[2] !== 0x48 || raw[3] !== 0x33 ||
55
+ raw[4] !== 3 || raw[6] !== 0 || raw[7] !== 0) {
56
+ throw new ProtocolV3Error("invalid FSH3 handshake frame");
57
+ }
58
+ const type = raw[5];
59
+ const length = readU32be(raw, 8);
60
+ if (type < HandshakeMessageTypeV3.ClientInit ||
61
+ type > HandshakeMessageTypeV3.ClientFinished ||
62
+ length < 1 || length > MAX_HANDSHAKE_PAYLOAD_BYTES ||
63
+ raw.length !== HANDSHAKE_HEADER_BYTES + length) {
64
+ throw new ProtocolV3Error("invalid FSH3 handshake frame");
65
+ }
66
+ const payload = raw.slice(HANDSHAKE_HEADER_BYTES);
67
+ try {
68
+ decoder.decode(payload);
69
+ }
70
+ catch {
71
+ throw new ProtocolV3Error("invalid FSH3 UTF-8 payload");
72
+ }
73
+ return { type, payload, raw: raw.slice() };
74
+ }
75
+ export function encodeClientInitV3(message) {
76
+ validateClientInit(message);
77
+ return encodeCanonicalHandshake(HandshakeMessageTypeV3.ClientInit, {
78
+ channel_id: message.channelID,
79
+ client_admission_binding_b64u: encode32(message.clientAdmissionBinding),
80
+ client_endpoint_instance_id: message.clientEndpointInstanceID,
81
+ client_eph_pub_b64u: base64urlEncode(message.clientEphemeralPublic),
82
+ client_role: message.clientRole,
83
+ max_inbound_streams: message.maxInboundStreams,
84
+ nonce_c_b64u: encode32(message.nonceC),
85
+ profile: message.profile,
86
+ selected_features: message.selectedFeatures,
87
+ session_contract_hash_b64u: encode32(message.sessionContractHash),
88
+ suite: message.suite,
89
+ });
90
+ }
91
+ export function decodeClientInitV3(raw) {
92
+ const value = decodeCanonicalObject(raw, HandshakeMessageTypeV3.ClientInit, [
93
+ "channel_id", "client_admission_binding_b64u", "client_endpoint_instance_id", "client_eph_pub_b64u",
94
+ "client_role", "max_inbound_streams", "nonce_c_b64u", "profile", "selected_features",
95
+ "session_contract_hash_b64u", "suite",
96
+ ]);
97
+ const message = {
98
+ profile: requireString(value.profile),
99
+ channelID: requireString(value.channel_id),
100
+ sessionContractHash: decode32(value.session_contract_hash_b64u),
101
+ clientRole: requireInteger(value.client_role, 1),
102
+ suite: requireSuite(value.suite),
103
+ clientEphemeralPublic: decodeBase64(value.client_eph_pub_b64u),
104
+ nonceC: decode32(value.nonce_c_b64u),
105
+ selectedFeatures: requireInteger(value.selected_features, 0xffffffff),
106
+ maxInboundStreams: requireInteger(value.max_inbound_streams, 0xffff),
107
+ clientAdmissionBinding: decode32(value.client_admission_binding_b64u),
108
+ clientEndpointInstanceID: requireString(value.client_endpoint_instance_id),
109
+ };
110
+ validateClientInit(message);
111
+ requireCanonical(raw, encodeClientInitV3(message));
112
+ return message;
113
+ }
114
+ export function encodeServerFinishedCoreV3(core, suite) {
115
+ const normalized = { ...core, suite };
116
+ validateServerCore(normalized);
117
+ return encodeCanonicalHandshake(HandshakeMessageTypeV3.ServerFinished, serverCoreWire(normalized));
118
+ }
119
+ export function encodeServerFinishedV3(message, suite) {
120
+ const core = { ...message.core, suite };
121
+ validateServerCore(core);
122
+ assertBytes("server confirm", message.serverConfirm, 32);
123
+ return encodeCanonicalHandshake(HandshakeMessageTypeV3.ServerFinished, {
124
+ handshake_id: base64urlEncode(core.handshakeID),
125
+ max_inbound_streams: core.maxInboundStreams,
126
+ nonce_s_b64u: encode32(core.nonceS),
127
+ selected_features: core.selectedFeatures,
128
+ server_admission_binding_b64u: encode32(core.serverAdmissionBinding),
129
+ server_confirm_b64u: encode32(message.serverConfirm),
130
+ server_endpoint_instance_id: core.serverEndpointInstanceID,
131
+ server_eph_pub_b64u: base64urlEncode(core.serverEphemeralPublic),
132
+ session_contract_hash_b64u: encode32(core.sessionContractHash),
133
+ });
134
+ }
135
+ export function decodeServerFinishedV3(raw, suite) {
136
+ const value = decodeCanonicalObject(raw, HandshakeMessageTypeV3.ServerFinished, [
137
+ "handshake_id", "max_inbound_streams", "nonce_s_b64u", "selected_features",
138
+ "server_admission_binding_b64u", "server_confirm_b64u", "server_endpoint_instance_id",
139
+ "server_eph_pub_b64u", "session_contract_hash_b64u",
140
+ ]);
141
+ const core = {
142
+ suite,
143
+ handshakeID: decodeBase64(value.handshake_id),
144
+ serverEphemeralPublic: decodeBase64(value.server_eph_pub_b64u),
145
+ nonceS: decode32(value.nonce_s_b64u),
146
+ sessionContractHash: decode32(value.session_contract_hash_b64u),
147
+ selectedFeatures: requireInteger(value.selected_features, 0xffffffff),
148
+ maxInboundStreams: requireInteger(value.max_inbound_streams, 0xffff),
149
+ serverAdmissionBinding: decode32(value.server_admission_binding_b64u),
150
+ serverEndpointInstanceID: requireString(value.server_endpoint_instance_id),
151
+ };
152
+ const message = { core, serverConfirm: decode32(value.server_confirm_b64u) };
153
+ validateServerCore(core);
154
+ requireCanonical(raw, encodeServerFinishedV3(message, suite));
155
+ return message;
156
+ }
157
+ export function encodeClientFinishedCoreV3(handshakeID) {
158
+ validateHandshakeID(handshakeID);
159
+ return encodeCanonicalHandshake(HandshakeMessageTypeV3.ClientFinished, {
160
+ handshake_id: base64urlEncode(handshakeID),
161
+ });
162
+ }
163
+ export function encodeClientFinishedV3(message) {
164
+ validateHandshakeID(message.handshakeID);
165
+ assertBytes("client confirm", message.clientConfirm, 32);
166
+ return encodeCanonicalHandshake(HandshakeMessageTypeV3.ClientFinished, {
167
+ client_confirm_b64u: encode32(message.clientConfirm),
168
+ handshake_id: base64urlEncode(message.handshakeID),
169
+ });
170
+ }
171
+ export function decodeClientFinishedV3(raw) {
172
+ const value = decodeCanonicalObject(raw, HandshakeMessageTypeV3.ClientFinished, [
173
+ "client_confirm_b64u", "handshake_id",
174
+ ]);
175
+ const message = {
176
+ handshakeID: decodeBase64(value.handshake_id),
177
+ clientConfirm: decode32(value.client_confirm_b64u),
178
+ };
179
+ validateHandshakeID(message.handshakeID);
180
+ requireCanonical(raw, encodeClientFinishedV3(message));
181
+ return message;
182
+ }
183
+ export function generateEphemeralKeyV3(suite, entropy) {
184
+ let privateKey;
185
+ if (suite === CipherSuiteV3.ChaCha20Poly1305) {
186
+ privateKey = requireEntropy(entropy, 32);
187
+ }
188
+ else if (suite === CipherSuiteV3.AES256GCM) {
189
+ do
190
+ privateKey = requireEntropy(entropy, 32);
191
+ while (!p256.utils.isValidSecretKey(privateKey));
192
+ }
193
+ else {
194
+ return invalidSuite();
195
+ }
196
+ return { privateKey, publicKey: ephemeralPublicKeyV3(suite, privateKey) };
197
+ }
198
+ function requireEntropy(entropy, length) {
199
+ const output = entropy(length);
200
+ assertBytes("session entropy", output, length);
201
+ return output.slice();
202
+ }
203
+ export function ephemeralPublicKeyV3(suite, privateKey) {
204
+ assertBytes("ephemeral private key", privateKey, 32);
205
+ try {
206
+ if (suite === CipherSuiteV3.ChaCha20Poly1305)
207
+ return x25519.getPublicKey(privateKey);
208
+ if (suite === CipherSuiteV3.AES256GCM)
209
+ return p256.getPublicKey(privateKey, false);
210
+ }
211
+ catch {
212
+ throw new ProtocolV3Error("invalid v3 ephemeral private key");
213
+ }
214
+ return invalidSuite();
215
+ }
216
+ export function computeSharedSecretV3(suite, privateKey, peerPublicKey) {
217
+ assertBytes("ephemeral private key", privateKey, 32);
218
+ validateEphemeralPublic(suite, peerPublicKey);
219
+ let shared;
220
+ try {
221
+ if (suite === CipherSuiteV3.ChaCha20Poly1305) {
222
+ shared = x25519.getSharedSecret(privateKey, peerPublicKey);
223
+ }
224
+ else if (suite === CipherSuiteV3.AES256GCM) {
225
+ const point = p256.getSharedSecret(privateKey, peerPublicKey, false);
226
+ if (point.length !== 65 || point[0] !== 4)
227
+ throw new Error("invalid P-256 point");
228
+ shared = point.slice(1, 33);
229
+ }
230
+ else {
231
+ return invalidSuite();
232
+ }
233
+ }
234
+ catch {
235
+ throw new ProtocolV3Error("invalid v3 ECDH shared secret");
236
+ }
237
+ if (shared.length !== 32 || shared.every((value) => value === 0)) {
238
+ throw new ProtocolV3Error("invalid v3 ECDH shared secret");
239
+ }
240
+ return shared;
241
+ }
242
+ export function deriveHandshakePRKV3(psk, sharedSecret) {
243
+ assertBytes("handshake PSK", psk, 32);
244
+ assertBytes("ECDH shared secret", sharedSecret, 32);
245
+ if (sharedSecret.every((value) => value === 0))
246
+ throw new ProtocolV3Error("invalid v3 ECDH shared secret");
247
+ return extract(sha256, sharedSecret, psk);
248
+ }
249
+ export function computeHandshakeH0V3(controlPrefaceRaw, clientInitRaw) {
250
+ parseControlPrefaceV3(controlPrefaceRaw);
251
+ decodeClientInitV3(clientInitRaw);
252
+ return sha256(concatBytes([
253
+ encoder.encode(FLOWERSEC_V3_CRYPTO_LABELS.handshake),
254
+ controlPrefaceRaw,
255
+ lengthPrefixed(clientInitRaw),
256
+ ]));
257
+ }
258
+ export function computeHandshakeH1V3(h0, serverCoreRaw) {
259
+ assertBytes("H0", h0, 32);
260
+ const frame = decodeHandshakeFrameV3(serverCoreRaw);
261
+ if (frame.type !== HandshakeMessageTypeV3.ServerFinished)
262
+ throw new ProtocolV3Error("invalid v3 transcript");
263
+ return sha256(concatBytes([h0, lengthPrefixed(serverCoreRaw)]));
264
+ }
265
+ export function computeServerConfirmV3(handshakePRK, h1) {
266
+ return computeConfirm(handshakePRK, h1, FLOWERSEC_V3_CRYPTO_LABELS["server-finished"]);
267
+ }
268
+ export function computeHandshakeH2V3(h1, serverFinishedRaw, clientCoreRaw) {
269
+ assertBytes("H1", h1, 32);
270
+ if (decodeHandshakeFrameV3(serverFinishedRaw).type !== HandshakeMessageTypeV3.ServerFinished ||
271
+ decodeHandshakeFrameV3(clientCoreRaw).type !== HandshakeMessageTypeV3.ClientFinished) {
272
+ throw new ProtocolV3Error("invalid v3 transcript");
273
+ }
274
+ return sha256(concatBytes([h1, lengthPrefixed(serverFinishedRaw), lengthPrefixed(clientCoreRaw)]));
275
+ }
276
+ export function computeClientConfirmV3(handshakePRK, h2) {
277
+ return computeConfirm(handshakePRK, h2, FLOWERSEC_V3_CRYPTO_LABELS["client-finished"]);
278
+ }
279
+ export function computeHandshakeH3V3(h2, clientFinishedRaw) {
280
+ assertBytes("H2", h2, 32);
281
+ decodeClientFinishedV3(clientFinishedRaw);
282
+ return sha256(concatBytes([h2, lengthPrefixed(clientFinishedRaw)]));
283
+ }
284
+ export function deriveSessionPRKV3(h3, handshakePRK) {
285
+ assertBytes("H3", h3, 32);
286
+ assertBytes("handshake PRK", handshakePRK, 32);
287
+ return extract(sha256, handshakePRK, h3);
288
+ }
289
+ export function validateClientInitV3(message, expected) {
290
+ validateClientInit(message);
291
+ if (message.channelID !== expected.channelID ||
292
+ message.suite !== expected.suite ||
293
+ message.maxInboundStreams !== expected.maxInboundStreams ||
294
+ !bytesEqual(message.sessionContractHash, expected.sessionContractHash) ||
295
+ !validAdmissionBinding(expected.path, message.clientAdmissionBinding, expected.admissionBinding) ||
296
+ !validExpectedEndpoint(expected.path, message.clientEndpointInstanceID, expected.expectedEndpointInstanceID)) {
297
+ throw new ProtocolV3Error("v3 handshake binding mismatch");
298
+ }
299
+ }
300
+ export function validateServerFinishedV3(message, expected) {
301
+ validateServerCore(message.core);
302
+ if (message.core.suite !== expected.suite ||
303
+ message.core.maxInboundStreams !== expected.maxInboundStreams ||
304
+ !bytesEqual(message.core.sessionContractHash, expected.sessionContractHash) ||
305
+ !validAdmissionBinding(expected.path, message.core.serverAdmissionBinding, expected.admissionBinding) ||
306
+ !validExpectedEndpoint(expected.path, message.core.serverEndpointInstanceID, expected.expectedEndpointInstanceID)) {
307
+ throw new ProtocolV3Error("v3 handshake binding mismatch");
308
+ }
309
+ }
310
+ function serverCoreWire(core) {
311
+ return {
312
+ handshake_id: base64urlEncode(core.handshakeID),
313
+ max_inbound_streams: core.maxInboundStreams,
314
+ nonce_s_b64u: encode32(core.nonceS),
315
+ selected_features: core.selectedFeatures,
316
+ server_admission_binding_b64u: encode32(core.serverAdmissionBinding),
317
+ server_endpoint_instance_id: core.serverEndpointInstanceID,
318
+ server_eph_pub_b64u: base64urlEncode(core.serverEphemeralPublic),
319
+ session_contract_hash_b64u: encode32(core.sessionContractHash),
320
+ };
321
+ }
322
+ function validateClientInit(message) {
323
+ if (message.profile !== FLOWERSEC_V3_PROFILE ||
324
+ !validRegistryID(message.channelID, false) ||
325
+ message.clientRole !== 1 ||
326
+ (message.selectedFeatures & ~KNOWN_FEATURES_V3) !== 0 ||
327
+ message.maxInboundStreams < 1 || message.maxInboundStreams > 128 ||
328
+ !validRegistryID(message.clientEndpointInstanceID, true)) {
329
+ throw new ProtocolV3Error("invalid v3 client init");
330
+ }
331
+ assertBytes("session contract hash", message.sessionContractHash, 32);
332
+ assertBytes("client nonce", message.nonceC, 32);
333
+ assertBytes("client admission binding", message.clientAdmissionBinding, 32);
334
+ validateEphemeralPublic(message.suite, message.clientEphemeralPublic);
335
+ }
336
+ function validateServerCore(core) {
337
+ validateHandshakeID(core.handshakeID);
338
+ if ((core.selectedFeatures & ~KNOWN_FEATURES_V3) !== 0 ||
339
+ core.maxInboundStreams < 1 || core.maxInboundStreams > 128 ||
340
+ !validRegistryID(core.serverEndpointInstanceID, true)) {
341
+ throw new ProtocolV3Error("invalid v3 server finished");
342
+ }
343
+ assertBytes("session contract hash", core.sessionContractHash, 32);
344
+ assertBytes("server nonce", core.nonceS, 32);
345
+ assertBytes("server admission binding", core.serverAdmissionBinding, 32);
346
+ validateEphemeralPublic(core.suite, core.serverEphemeralPublic);
347
+ }
348
+ function validateEphemeralPublic(suite, value) {
349
+ try {
350
+ if (suite === CipherSuiteV3.ChaCha20Poly1305) {
351
+ assertBytes("X25519 public key", value, 32);
352
+ return;
353
+ }
354
+ if (suite === CipherSuiteV3.AES256GCM) {
355
+ if (value.length !== 65 || value[0] !== 4)
356
+ throw new Error("invalid P-256 public key");
357
+ p256.Point.fromBytes(value);
358
+ return;
359
+ }
360
+ }
361
+ catch {
362
+ throw new ProtocolV3Error("invalid v3 ephemeral public key");
363
+ }
364
+ invalidSuite();
365
+ }
366
+ function encodeCanonicalHandshake(type, value) {
367
+ return encodeHandshakeFrameV3(type, encoder.encode(JSON.stringify(value)));
368
+ }
369
+ function decodeCanonicalObject(raw, type, fields) {
370
+ const frame = decodeHandshakeFrameV3(raw);
371
+ if (frame.type !== type)
372
+ throw new ProtocolV3Error("unexpected FSH3 message type");
373
+ let value;
374
+ try {
375
+ value = JSON.parse(decoder.decode(frame.payload));
376
+ }
377
+ catch {
378
+ throw new ProtocolV3Error("invalid FSH3 JSON payload");
379
+ }
380
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
381
+ throw new ProtocolV3Error("invalid FSH3 JSON object");
382
+ }
383
+ const object = value;
384
+ const keys = Object.keys(object);
385
+ if (keys.length !== fields.length || fields.some((field) => !Object.hasOwn(object, field))) {
386
+ throw new ProtocolV3Error("invalid FSH3 JSON fields");
387
+ }
388
+ return object;
389
+ }
390
+ function requireCanonical(actual, canonical) {
391
+ if (!bytesEqual(actual, canonical))
392
+ throw new ProtocolV3Error("non-canonical FSH3 message");
393
+ }
394
+ function requireString(value) {
395
+ if (typeof value !== "string")
396
+ throw new ProtocolV3Error("invalid FSH3 string");
397
+ return value;
398
+ }
399
+ function requireInteger(value, max) {
400
+ if (!Number.isInteger(value) || value < 0 || value > max) {
401
+ throw new ProtocolV3Error("invalid FSH3 integer");
402
+ }
403
+ return value;
404
+ }
405
+ function requireSuite(value) {
406
+ if (value !== CipherSuiteV3.ChaCha20Poly1305 && value !== CipherSuiteV3.AES256GCM)
407
+ return invalidSuite();
408
+ return value;
409
+ }
410
+ function decodeBase64(value) {
411
+ const text = requireString(value);
412
+ try {
413
+ return base64urlDecode(text);
414
+ }
415
+ catch {
416
+ throw new ProtocolV3Error("invalid FSH3 base64url");
417
+ }
418
+ }
419
+ function decode32(value) {
420
+ const decoded = decodeBase64(value);
421
+ assertBytes("FSH3 field", decoded, 32);
422
+ return decoded;
423
+ }
424
+ function encode32(value) {
425
+ assertBytes("FSH3 field", value, 32);
426
+ return base64urlEncode(value);
427
+ }
428
+ function validateHandshakeID(value) {
429
+ if (value.length < 16 || value.length > 32)
430
+ throw new ProtocolV3Error("invalid v3 handshake ID");
431
+ }
432
+ function computeConfirm(handshakePRK, transcript, label) {
433
+ assertBytes("handshake PRK", handshakePRK, 32);
434
+ assertBytes("handshake transcript", transcript, 32);
435
+ const key = expand(sha256, handshakePRK, concatBytes([encoder.encode(label), transcript]), 32);
436
+ return hmac(sha256, key, transcript);
437
+ }
438
+ function lengthPrefixed(raw) {
439
+ return concatBytes([u32be(raw.length), raw]);
440
+ }
441
+ function validRegistryID(value, allowEmpty) {
442
+ return (allowEmpty && value === "") || (value.length >= 1 && value.length <= 128 && registryIDPattern.test(value));
443
+ }
444
+ function validAdmissionBinding(path, actual, expected) {
445
+ assertBytes("actual admission binding", actual, 32);
446
+ assertBytes("expected admission binding", expected, 32);
447
+ if (path === "direct" || !expected.every((value) => value === 0))
448
+ return bytesEqual(actual, expected);
449
+ return !actual.every((value) => value === 0);
450
+ }
451
+ function validExpectedEndpoint(path, actual, expected) {
452
+ if (path === "direct")
453
+ return actual === "" && expected === "";
454
+ return validRegistryID(actual, false) && validRegistryID(expected, false) && actual === expected;
455
+ }
456
+ function bytesEqual(left, right) {
457
+ if (left.length !== right.length)
458
+ return false;
459
+ let difference = 0;
460
+ for (let index = 0; index < left.length; index++)
461
+ difference |= left[index] ^ right[index];
462
+ return difference === 0;
463
+ }
464
+ function assertBytes(name, value, length) {
465
+ if (!(value instanceof Uint8Array) || value.length !== length) {
466
+ throw new ProtocolV3Error(`${name} must be ${length} bytes`);
467
+ }
468
+ }
469
+ function invalidSuite() {
470
+ throw new ProtocolV3Error("invalid v3 cipher suite");
471
+ }
@@ -0,0 +1,6 @@
1
+ export type JCSValue = null | boolean | number | string | readonly JCSValue[] | Readonly<{
2
+ [key: string]: JCSValue;
3
+ }>;
4
+ export declare function canonicalizeJCSV3(value: JCSValue): string;
5
+ export declare function jcsUTF8V3(value: JCSValue): Uint8Array;
6
+ export declare function assertJCSValueV3(value: unknown): asserts value is JCSValue;
package/dist/v3/jcs.js ADDED
@@ -0,0 +1,69 @@
1
+ const encoder = new TextEncoder();
2
+ export function canonicalizeJCSV3(value) {
3
+ if (value === null)
4
+ return "null";
5
+ switch (typeof value) {
6
+ case "boolean":
7
+ return value ? "true" : "false";
8
+ case "number":
9
+ if (!Number.isSafeInteger(value) || Object.is(value, -0)) {
10
+ throw new TypeError("Flowersec v3 JCS numbers must be safe integers");
11
+ }
12
+ return String(value);
13
+ case "string":
14
+ assertUnicodeScalarString(value);
15
+ return JSON.stringify(value);
16
+ case "object":
17
+ if (Array.isArray(value))
18
+ return `[${value.map(canonicalizeJCSV3).join(",")}]`;
19
+ const object = value;
20
+ return `{${Object.keys(object).sort(compareUTF16).map((key) => {
21
+ assertUnicodeScalarString(key);
22
+ return `${JSON.stringify(key)}:${canonicalizeJCSV3(object[key])}`;
23
+ }).join(",")}}`;
24
+ }
25
+ }
26
+ export function jcsUTF8V3(value) {
27
+ return encoder.encode(canonicalizeJCSV3(value));
28
+ }
29
+ export function assertJCSValueV3(value) {
30
+ if (value === null || typeof value === "boolean")
31
+ return;
32
+ if (typeof value === "number") {
33
+ if (!Number.isSafeInteger(value) || Object.is(value, -0))
34
+ throw new TypeError("invalid JCS number");
35
+ return;
36
+ }
37
+ if (typeof value === "string") {
38
+ assertUnicodeScalarString(value);
39
+ return;
40
+ }
41
+ if (Array.isArray(value)) {
42
+ for (const item of value)
43
+ assertJCSValueV3(item);
44
+ return;
45
+ }
46
+ if (typeof value !== "object" || value === null)
47
+ throw new TypeError("invalid JCS value");
48
+ for (const [key, item] of Object.entries(value)) {
49
+ assertUnicodeScalarString(key);
50
+ assertJCSValueV3(item);
51
+ }
52
+ }
53
+ function compareUTF16(left, right) {
54
+ return left < right ? -1 : left > right ? 1 : 0;
55
+ }
56
+ function assertUnicodeScalarString(value) {
57
+ for (let index = 0; index < value.length; index += 1) {
58
+ const code = value.charCodeAt(index);
59
+ if (code >= 0xd800 && code <= 0xdbff) {
60
+ const next = value.charCodeAt(index + 1);
61
+ if (next < 0xdc00 || next > 0xdfff)
62
+ throw new TypeError("unpaired UTF-16 surrogate");
63
+ index += 1;
64
+ }
65
+ else if (code >= 0xdc00 && code <= 0xdfff) {
66
+ throw new TypeError("unpaired UTF-16 surrogate");
67
+ }
68
+ }
69
+ }
@@ -0,0 +1 @@
1
+ export declare function preflightJSONV3(text: string, scanScopedPayloads?: boolean): void;