@floegence/flowersec-core 3.2.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +57 -200
  2. package/THIRD_PARTY_NOTICES.md +11 -11
  3. package/dist/browser/index.d.ts +0 -5
  4. package/dist/browser/index.js +0 -3
  5. package/dist/cli.js +93 -64
  6. package/dist/facade.d.ts +0 -17
  7. package/dist/facade.js +0 -11
  8. package/dist/generated/unicode151.d.ts +2 -0
  9. package/dist/generated/unicode151.js +83 -0
  10. package/dist/interop/proxyServerPeer.js +34 -20
  11. package/dist/interop/serverParityPeer.js +11 -252
  12. package/dist/node/acceptor.d.ts +1 -57
  13. package/dist/node/acceptor.js +3 -420
  14. package/dist/node/acceptorV3.js +2 -2
  15. package/dist/node/connectSessionV3.js +2 -2
  16. package/dist/node/index.d.ts +2 -17
  17. package/dist/node/index.js +1 -9
  18. package/dist/node/nativeTransportAddon.d.ts +7 -36
  19. package/dist/node/nativeTransportAddon.js +7 -27
  20. package/dist/node/proxyServer.d.ts +2 -2
  21. package/dist/node/proxyServer.js +2 -2
  22. package/dist/node/rawQuicAdapterV3.d.ts +2 -2
  23. package/dist/node/rawQuicAdapterV3.js +1 -1
  24. package/dist/node/rawQuicServerV3.d.ts +2 -2
  25. package/dist/node/rawQuicServerV3.js +1 -1
  26. package/dist/node/rawQuicTls.d.ts +2 -0
  27. package/dist/node/{rawQuicAdapter.js → rawQuicTls.js} +30 -45
  28. package/dist/node/tunnelRuntimeV3.js +2 -2
  29. package/dist/proxy/integration.d.ts +5 -5
  30. package/dist/proxy/integration.js +2 -2
  31. package/dist/public/streamHandlers.d.ts +0 -3
  32. package/dist/public/streamHandlers.js +2 -4
  33. package/dist/public/streamMetadata.js +2 -2
  34. package/dist/runtime/errors.d.ts +2 -2
  35. package/dist/testSupport/tlsFixture.d.ts +2 -0
  36. package/dist/testSupport/tlsFixture.js +8 -0
  37. package/dist/utils/errors.d.ts +0 -2
  38. package/dist/utils/errors.js +0 -80
  39. package/dist/v3/artifact.js +17 -3
  40. package/dist/v3/carrier.d.ts +2 -2
  41. package/dist/v3/protocol.js +2 -103
  42. package/dist/v3/retryDisposition.d.ts +2 -2
  43. package/dist/v3/retryDisposition.js +0 -7
  44. package/dist/vendor/tr46.d.ts +4 -0
  45. package/dist/vendor/tr46.js +37 -32
  46. package/package.json +19 -17
  47. package/sbom/cyclonedx.json +62 -62
  48. package/sbom/spdx.json +65 -65
  49. package/dist/browser/connectSession.d.ts +0 -13
  50. package/dist/browser/connectSession.js +0 -40
  51. package/dist/browser/runtimeCapability.d.ts +0 -13
  52. package/dist/browser/runtimeCapability.js +0 -27
  53. package/dist/browser/sessionRuntime.d.ts +0 -2
  54. package/dist/browser/sessionRuntime.js +0 -21
  55. package/dist/browser/v2.d.ts +0 -3
  56. package/dist/browser/v2.js +0 -2
  57. package/dist/browser/webTransportClient.d.ts +0 -8
  58. package/dist/browser/webTransportClient.js +0 -55
  59. package/dist/connectionController.d.ts +0 -55
  60. package/dist/connectionController.js +0 -441
  61. package/dist/connector/adapters/rawQuicCandidate.d.ts +0 -5
  62. package/dist/connector/adapters/rawQuicCandidate.js +0 -71
  63. package/dist/connector/adapters/webSocketCandidate.d.ts +0 -7
  64. package/dist/connector/adapters/webSocketCandidate.js +0 -116
  65. package/dist/connector/adapters/webTransportCandidate.d.ts +0 -5
  66. package/dist/connector/adapters/webTransportCandidate.js +0 -73
  67. package/dist/connector/admissionCommit.d.ts +0 -31
  68. package/dist/connector/admissionCommit.js +0 -239
  69. package/dist/connector/sessionAcceptor.d.ts +0 -38
  70. package/dist/connector/sessionAcceptor.js +0 -158
  71. package/dist/connector/sessionConfig.d.ts +0 -3
  72. package/dist/connector/sessionConfig.js +0 -30
  73. package/dist/connector/sessionConnector.d.ts +0 -47
  74. package/dist/connector/sessionConnector.js +0 -606
  75. package/dist/node/connectSession.d.ts +0 -23
  76. package/dist/node/connectSession.js +0 -93
  77. package/dist/node/controlplane.d.ts +0 -137
  78. package/dist/node/controlplane.js +0 -443
  79. package/dist/node/rawQuicAdapter.d.ts +0 -9
  80. package/dist/node/rawQuicServer.d.ts +0 -24
  81. package/dist/node/rawQuicServer.js +0 -37
  82. package/dist/node/runtimeCapability.d.ts +0 -14
  83. package/dist/node/runtimeCapability.js +0 -24
  84. package/dist/node/sessionRuntime.d.ts +0 -2
  85. package/dist/node/sessionRuntime.js +0 -13
  86. package/dist/node/tunnelRuntime.d.ts +0 -42
  87. package/dist/node/tunnelRuntime.js +0 -559
  88. package/dist/node/v2.d.ts +0 -11
  89. package/dist/node/v2.js +0 -6
  90. package/dist/node/webSocketServer.d.ts +0 -24
  91. package/dist/node/webSocketServer.js +0 -135
  92. package/dist/public/artifact.d.ts +0 -11
  93. package/dist/public/artifact.js +0 -44
  94. package/dist/public/artifactLease.d.ts +0 -10
  95. package/dist/public/artifactLease.js +0 -41
  96. package/dist/public/connectError.d.ts +0 -6
  97. package/dist/public/connectError.js +0 -9
  98. package/dist/transport/webSocketAdapter.d.ts +0 -31
  99. package/dist/transport/webSocketAdapter.js +0 -370
  100. package/dist/transport/webTransportAdapter.d.ts +0 -27
  101. package/dist/transport/webTransportAdapter.js +0 -534
  102. package/dist/v2/admissionError.d.ts +0 -4
  103. package/dist/v2/admissionError.js +0 -8
  104. package/dist/v2/artifact.d.ts +0 -128
  105. package/dist/v2/artifact.js +0 -1131
  106. package/dist/v2/artifactLease.d.ts +0 -1
  107. package/dist/v2/artifactLease.js +0 -7
  108. package/dist/v2/capability.d.ts +0 -29
  109. package/dist/v2/capability.js +0 -190
  110. package/dist/v2/carrier.d.ts +0 -84
  111. package/dist/v2/carrier.js +0 -411
  112. package/dist/v2/contract.d.ts +0 -46
  113. package/dist/v2/contract.js +0 -1
  114. package/dist/v2/handshake.d.ts +0 -80
  115. package/dist/v2/handshake.js +0 -470
  116. package/dist/v2/index.d.ts +0 -14
  117. package/dist/v2/index.js +0 -7
  118. package/dist/v2/opaqueArtifact.d.ts +0 -1
  119. package/dist/v2/opaqueArtifact.js +0 -2
  120. package/dist/v2/protocol.d.ts +0 -99
  121. package/dist/v2/protocol.js +0 -737
  122. package/dist/v2/publicSession.d.ts +0 -1
  123. package/dist/v2/publicSession.js +0 -281
  124. package/dist/v2/retryDisposition.d.ts +0 -16
  125. package/dist/v2/retryDisposition.js +0 -30
  126. package/dist/v2/session.d.ts +0 -310
  127. package/dist/v2/session.js +0 -2127
  128. package/dist/v2/streamLifetimeLedger.d.ts +0 -31
  129. package/dist/v2/streamLifetimeLedger.js +0 -131
  130. package/dist/v2/streamMetadata.d.ts +0 -1
  131. package/dist/v2/streamMetadata.js +0 -7
  132. package/dist/v2/unreliableMessage.d.ts +0 -43
  133. package/dist/v2/unreliableMessage.js +0 -254
@@ -1,1131 +0,0 @@
1
- import { sha256 } from "@noble/hashes/sha2.js";
2
- import { base64urlDecode, base64urlEncode } from "../utils/base64url.js";
3
- import { concatBytes, readU32be, u32be } from "../utils/bin.js";
4
- import { toASCII } from "../vendor/tr46.js";
5
- export var AdmissionStatusV2;
6
- (function (AdmissionStatusV2) {
7
- AdmissionStatusV2[AdmissionStatusV2["Success"] = 0] = "Success";
8
- AdmissionStatusV2[AdmissionStatusV2["Reject"] = 1] = "Reject";
9
- AdmissionStatusV2[AdmissionStatusV2["Retryable"] = 2] = "Retryable";
10
- })(AdmissionStatusV2 || (AdmissionStatusV2 = {}));
11
- export class ArtifactV2Error extends Error {
12
- code;
13
- constructor(code, message) {
14
- super(message);
15
- this.name = "ArtifactV2Error";
16
- this.code = code;
17
- }
18
- }
19
- const PROFILE = "flowersec/2";
20
- const MAX_ARTIFACT_JSON_BYTES = 65_536;
21
- const MAX_CANDIDATES = 4;
22
- const MAX_CANONICAL_CANDIDATE_BYTES = 2_304;
23
- const MAX_CANONICAL_CANDIDATE_SET_BYTES = 12 * 1_024;
24
- const MAX_CANONICAL_FSB2_PAYLOAD = 32_768;
25
- const MAX_ADMISSION_REASON_BYTES = 64;
26
- const MAX_ADMISSION_CREDENTIAL_BYTES = 8_192;
27
- const FSB2_HEADER_BYTES = 12;
28
- const FSA2_HEADER_BYTES = 8;
29
- const encoder = new TextEncoder();
30
- const decoder = new TextDecoder("utf-8", { fatal: true });
31
- const registryIDPattern = /^[A-Za-z0-9._~-]+$/;
32
- const candidateIDPattern = /^[a-z0-9][a-z0-9._-]*$/;
33
- const scopePattern = /^[a-z][a-z0-9._-]{0,63}$/;
34
- const correlationKeyPattern = /^[a-z][a-z0-9._-]{0,31}$/;
35
- export function computeSessionContractHashV2(session) {
36
- validateSession(session);
37
- const canonicalJSON = JSON.stringify({
38
- allowed_suites: session.allowed_suites,
39
- channel_id: session.channel_id,
40
- default_suite: session.default_suite,
41
- establish_timeout_seconds: session.establish_timeout_seconds,
42
- idle_timeout_seconds: session.idle_timeout_seconds,
43
- max_inbound_streams: session.max_inbound_streams,
44
- profile: PROFILE,
45
- rekey_completion_timeout_seconds: session.rekey_completion_timeout_seconds,
46
- rekey_prepare_timeout_seconds: session.rekey_prepare_timeout_seconds,
47
- selected_features: session.selected_features,
48
- });
49
- return labeledHash("flowersec-v2-session-contract\0", canonicalJSON);
50
- }
51
- export function canonicalizeCandidatesV2(kind, candidates) {
52
- if (kind !== "direct" && kind !== "tunnel")
53
- throw invalidCandidate("path kind");
54
- if (!Array.isArray(candidates) || candidates.length < 1 || candidates.length > MAX_CANDIDATES) {
55
- throw invalidCandidate("candidate count");
56
- }
57
- const ids = new Set();
58
- const tuples = new Set();
59
- const canonical = [];
60
- for (const candidate of candidates) {
61
- validateCandidateShape(candidate);
62
- if (!candidateIDPattern.test(candidate.id) || utf8Length(candidate.id) > 64) {
63
- throw invalidCandidate("candidate id");
64
- }
65
- if (ids.has(candidate.id))
66
- throw invalidCandidate("duplicate candidate id");
67
- ids.add(candidate.id);
68
- if (utf8Length(candidate.url) < 1 || utf8Length(candidate.url) > 2_048) {
69
- throw invalidCandidate("candidate URL length");
70
- }
71
- const normalizedURL = normalizeCandidateURL(kind, candidate.carrier, candidate.url);
72
- if (utf8Length(normalizedURL) > 2_048)
73
- throw invalidCandidate("normalized URL length");
74
- if (candidate.normalized_url !== undefined && candidate.normalized_url !== normalizedURL) {
75
- throw invalidCandidate("normalized URL mismatch");
76
- }
77
- const wireProfile = `flowersec-${kind}/2`;
78
- if (candidate.wire_profile !== wireProfile)
79
- throw invalidCandidate("wire profile");
80
- const tuple = `${candidate.carrier}\0${normalizedURL}\0${candidate.wire_profile}`;
81
- if (tuples.has(tuple))
82
- throw invalidCandidate("duplicate normalized tuple");
83
- tuples.add(tuple);
84
- const item = {
85
- carrier: candidate.carrier,
86
- id: candidate.id,
87
- normalized_url: normalizedURL,
88
- wire_profile: candidate.wire_profile,
89
- };
90
- if (utf8Length(JSON.stringify(item)) > MAX_CANONICAL_CANDIDATE_BYTES) {
91
- throw invalidCandidate("canonical candidate too large");
92
- }
93
- canonical.push(item);
94
- }
95
- canonical.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
96
- const canonicalJSON = JSON.stringify(canonical);
97
- if (utf8Length(canonicalJSON) > MAX_CANONICAL_CANDIDATE_SET_BYTES) {
98
- throw invalidCandidate("canonical candidate set too large");
99
- }
100
- return { candidates: canonical, ...labeledHash("flowersec-v2-candidates\0", canonicalJSON) };
101
- }
102
- export function decodeArtifactV2JSON(raw) {
103
- const text = decodeBoundedJSON(raw);
104
- let value;
105
- try {
106
- value = parseStrictJSON(text);
107
- }
108
- catch (error) {
109
- throw invalidArtifact(`strict JSON: ${errorMessage(error)}`);
110
- }
111
- const artifact = decodeArtifactValue(value);
112
- const canonicalCandidates = validateArtifactV2(artifact);
113
- const normalizedByID = new Map(canonicalCandidates.candidates.map((candidate) => [candidate.id, candidate.normalized_url]));
114
- return {
115
- ...artifact,
116
- path: {
117
- ...artifact.path,
118
- candidates: artifact.path.candidates.map((candidate) => ({
119
- ...candidate,
120
- normalized_url: normalizedByID.get(candidate.id),
121
- })),
122
- },
123
- };
124
- }
125
- export function encodeArtifactV2JSON(artifact) {
126
- validateArtifactV2(artifact);
127
- const wire = {
128
- v: artifact.v,
129
- profile: artifact.profile,
130
- session: artifactSessionWire(artifact.session),
131
- path: artifact.path.kind === "direct" ? directPathWire(artifact.path) : tunnelPathWire(artifact.path),
132
- scoped: artifact.scoped.map((scope) => ({
133
- scope: scope.scope,
134
- scope_version: scope.scope_version,
135
- critical: scope.critical,
136
- payload: scope.payload,
137
- })),
138
- correlation: {
139
- v: artifact.correlation.v,
140
- tags: artifact.correlation.tags.map((tag) => ({ key: tag.key, value: tag.value })),
141
- },
142
- };
143
- const raw = encoder.encode(JSON.stringify(wire));
144
- if (raw.length > MAX_ARTIFACT_JSON_BYTES) {
145
- throw new ArtifactV2Error("artifact_too_large", "Flowersec v2 artifact is too large");
146
- }
147
- return raw;
148
- }
149
- export function validateArtifactV2(artifact) {
150
- validateArtifactShape(artifact);
151
- if (artifact.v !== 2 || artifact.profile !== PROFILE)
152
- throw invalidArtifact("version or profile");
153
- const sessionHash = computeSessionContractHashV2(artifact.session);
154
- if (sessionHash.hashBase64URL !== artifact.session.contract_hash_b64u) {
155
- throw invalidArtifact("session contract hash");
156
- }
157
- const candidateSet = canonicalizeCandidatesV2(artifact.path.kind, artifact.path.candidates);
158
- if (!validRegistryID(artifact.path.rendezvous_group_id, 128) || !validRegistryID(artifact.path.listener_audience, 128)) {
159
- throw invalidArtifact("rendezvous group or listener audience");
160
- }
161
- if (artifact.path.kind === "direct") {
162
- if (!validASCII(artifact.path.routing_token, MAX_ADMISSION_CREDENTIAL_BYTES)) {
163
- throw invalidArtifact("direct path variant");
164
- }
165
- }
166
- else {
167
- if ((artifact.path.role !== 1 && artifact.path.role !== 2) ||
168
- !validRegistryID(artifact.path.local_endpoint_instance_id, 128) ||
169
- !validRegistryID(artifact.path.expected_peer_endpoint_instance_id, 128) ||
170
- artifact.path.local_endpoint_instance_id === artifact.path.expected_peer_endpoint_instance_id ||
171
- !validASCII(artifact.path.token, MAX_ADMISSION_CREDENTIAL_BYTES)) {
172
- throw invalidArtifact("tunnel path variant");
173
- }
174
- }
175
- validateScopes(artifact.scoped);
176
- validateCorrelationShape(artifact.correlation);
177
- for (const candidate of candidateSet.candidates) {
178
- const request = requestFromValidatedArtifact(artifact, candidateSet, candidate.id);
179
- if (marshalFSB2Payload(request).length > MAX_CANONICAL_FSB2_PAYLOAD) {
180
- throw new ArtifactV2Error("fsb2_payload_too_large", "FSB2 canonical payload is too large");
181
- }
182
- }
183
- return candidateSet;
184
- }
185
- export function buildFSB2RequestV2(artifact, chosenCandidateID) {
186
- const candidateSet = validateArtifactV2(artifact);
187
- const request = requestFromValidatedArtifact(artifact, candidateSet, chosenCandidateID);
188
- validateFSB2Request(request);
189
- return request;
190
- }
191
- export function encodeFSB2RequestV2(request) {
192
- validateFSB2Request(request);
193
- const payload = marshalFSB2Payload(request);
194
- if (payload.length > MAX_CANONICAL_FSB2_PAYLOAD) {
195
- throw new ArtifactV2Error("fsb2_payload_too_large", "FSB2 canonical payload is too large");
196
- }
197
- const out = new Uint8Array(FSB2_HEADER_BYTES + payload.length);
198
- out.set(encoder.encode("FSB2"), 0);
199
- out[4] = 2;
200
- out[5] = request.pathKind === "direct" ? 1 : 2;
201
- out.set(u32be(payload.length), 8);
202
- out.set(payload, FSB2_HEADER_BYTES);
203
- return out;
204
- }
205
- export function decodeFSB2RequestV2(raw) {
206
- if (raw.length < FSB2_HEADER_BYTES)
207
- throw invalidFSB2("truncated header");
208
- if (raw[0] !== 0x46 ||
209
- raw[1] !== 0x53 ||
210
- raw[2] !== 0x42 ||
211
- raw[3] !== 0x32 ||
212
- raw[4] !== 2 ||
213
- raw[6] !== 0 ||
214
- raw[7] !== 0) {
215
- throw invalidFSB2("header");
216
- }
217
- const pathKind = pathKindFromCode(raw[5]);
218
- const payloadLength = readU32be(raw, 8);
219
- if (payloadLength > MAX_CANONICAL_FSB2_PAYLOAD) {
220
- throw new ArtifactV2Error("fsb2_payload_too_large", "FSB2 canonical payload is too large");
221
- }
222
- if (payloadLength === 0 || raw.length !== FSB2_HEADER_BYTES + payloadLength) {
223
- throw invalidFSB2("payload length");
224
- }
225
- const payload = raw.subarray(FSB2_HEADER_BYTES);
226
- let text;
227
- let value;
228
- try {
229
- text = decoder.decode(payload);
230
- value = parseStrictJSON(text);
231
- }
232
- catch (error) {
233
- throw invalidFSB2(`strict JSON: ${errorMessage(error)}`);
234
- }
235
- const request = decodeFSB2Value(pathKind, value);
236
- validateFSB2Request(request);
237
- if (!bytesEqual(payload, marshalFSB2Payload(request))) {
238
- throw new ArtifactV2Error("noncanonical_fsb2", "FSB2 payload is not canonical");
239
- }
240
- const copied = new Uint8Array(raw);
241
- return {
242
- request,
243
- raw: copied,
244
- localAdmissionBinding: admissionBindingV2(copied),
245
- };
246
- }
247
- export function admissionBindingV2(rawFSB2) {
248
- return sha256(concatBytes([encoder.encode("flowersec-v2-admission\0"), rawFSB2]));
249
- }
250
- // The server authorizer selects rejection reasons; the codec enforces only the
251
- // bounded canonical token required by the FSA2 wire contract.
252
- export function encodeFSA2ResponseV2(response) {
253
- validateFSA2Response(response);
254
- const reason = encoder.encode(response.reason);
255
- const out = new Uint8Array(FSA2_HEADER_BYTES + reason.length);
256
- out.set(encoder.encode("FSA2"), 0);
257
- out[4] = 2;
258
- out[5] = response.status;
259
- new DataView(out.buffer).setUint16(6, reason.length, false);
260
- out.set(reason, FSA2_HEADER_BYTES);
261
- return out;
262
- }
263
- // Clients accept any canonical bounded rejection token. Deployment policy is
264
- // server-owned and is not duplicated as a client-side reason registry.
265
- export function decodeFSA2ResponseV2(raw) {
266
- if (raw.length < FSA2_HEADER_BYTES)
267
- throw invalidFSA2("truncated header");
268
- if (raw[0] !== 0x46 || raw[1] !== 0x53 || raw[2] !== 0x41 || raw[3] !== 0x32 || raw[4] !== 2) {
269
- throw invalidFSA2("header");
270
- }
271
- const reasonLength = new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint16(6, false);
272
- if (reasonLength > MAX_ADMISSION_REASON_BYTES || raw.length !== FSA2_HEADER_BYTES + reasonLength) {
273
- throw invalidFSA2("reason length");
274
- }
275
- let reason;
276
- try {
277
- reason = decoder.decode(raw.subarray(FSA2_HEADER_BYTES));
278
- }
279
- catch {
280
- throw invalidFSA2("reason encoding");
281
- }
282
- const response = { status: raw[5], reason };
283
- validateFSA2Response(response);
284
- return response;
285
- }
286
- function validateSession(session) {
287
- assertRecord(session, "session", "invalid_artifact");
288
- assertExactKeys(session, [
289
- "channel_id",
290
- "init_expire_at_unix_s",
291
- "idle_timeout_seconds",
292
- "establish_timeout_seconds",
293
- "rekey_prepare_timeout_seconds",
294
- "rekey_completion_timeout_seconds",
295
- "max_inbound_streams",
296
- "e2ee_psk_b64u",
297
- "allowed_suites",
298
- "default_suite",
299
- "selected_features",
300
- "contract_hash_b64u",
301
- ], "session", "invalid_artifact");
302
- if (!validRegistryID(session.channel_id, 128))
303
- throw invalidArtifact("channel_id");
304
- assertSafeInteger(session.init_expire_at_unix_s, 1, Number.MAX_SAFE_INTEGER, "init expiry");
305
- assertSafeInteger(session.idle_timeout_seconds, 0, 0xffff_ffff, "idle timeout");
306
- if (session.establish_timeout_seconds !== 30 ||
307
- session.rekey_prepare_timeout_seconds !== 10 ||
308
- session.rekey_completion_timeout_seconds !== 30) {
309
- throw invalidArtifact("fixed session timing");
310
- }
311
- assertSafeInteger(session.max_inbound_streams, 1, 128, "max inbound streams");
312
- assertCanonical32(session.e2ee_psk_b64u, "e2ee PSK", "invalid_artifact");
313
- assertCanonical32(session.contract_hash_b64u, "contract hash", "invalid_artifact");
314
- if (!Array.isArray(session.allowed_suites) || session.allowed_suites.length < 1) {
315
- throw invalidArtifact("allowed suites");
316
- }
317
- let previous = 0;
318
- const suites = new Set();
319
- for (const suite of session.allowed_suites) {
320
- if ((suite !== 1 && suite !== 2) || suite <= previous || suites.has(suite)) {
321
- throw invalidArtifact("allowed suites");
322
- }
323
- previous = suite;
324
- suites.add(suite);
325
- }
326
- if (!suites.has(session.default_suite))
327
- throw invalidArtifact("default suite");
328
- if (session.selected_features !== 0)
329
- throw invalidArtifact("selected features");
330
- }
331
- function validateArtifactShape(artifact) {
332
- assertRecord(artifact, "artifact", "invalid_artifact");
333
- assertExactKeys(artifact, ["v", "profile", "session", "path", "scoped", "correlation"], "artifact", "invalid_artifact");
334
- validateSession(artifact.session);
335
- validatePathShape(artifact.path, true);
336
- if (!Array.isArray(artifact.scoped))
337
- throw invalidArtifact("scoped");
338
- validateCorrelationShape(artifact.correlation);
339
- }
340
- function validatePathShape(path, allowNormalized) {
341
- assertRecord(path, "path", "invalid_artifact");
342
- if (path.kind === "direct") {
343
- assertExactKeys(path, ["kind", "rendezvous_group_id", "listener_audience", "routing_token", "candidates"], "direct path", "invalid_artifact");
344
- }
345
- else if (path.kind === "tunnel") {
346
- assertExactKeys(path, [
347
- "kind",
348
- "rendezvous_group_id",
349
- "listener_audience",
350
- "role",
351
- "local_endpoint_instance_id",
352
- "expected_peer_endpoint_instance_id",
353
- "token",
354
- "candidates",
355
- ], "tunnel path", "invalid_artifact");
356
- }
357
- else {
358
- throw invalidArtifact("path kind");
359
- }
360
- if (!Array.isArray(path.candidates))
361
- throw invalidArtifact("candidates");
362
- for (const candidate of path.candidates)
363
- validateCandidateShape(candidate, allowNormalized);
364
- }
365
- function validateCandidateShape(candidate, allowNormalized = true) {
366
- assertRecord(candidate, "candidate", "invalid_candidate");
367
- const keys = allowNormalized
368
- ? ["id", "carrier", "url", "wire_profile", ...(candidate.normalized_url === undefined ? [] : ["normalized_url"])]
369
- : ["id", "carrier", "url", "wire_profile"];
370
- assertExactKeys(candidate, keys, "candidate", "invalid_candidate");
371
- if (typeof candidate.id !== "string" ||
372
- typeof candidate.carrier !== "string" ||
373
- typeof candidate.url !== "string" ||
374
- typeof candidate.wire_profile !== "string" ||
375
- (candidate.normalized_url !== undefined && typeof candidate.normalized_url !== "string")) {
376
- throw invalidCandidate("candidate field type");
377
- }
378
- }
379
- function validateScopes(scopes) {
380
- if (!Array.isArray(scopes) || scopes.length > 8)
381
- throw invalidArtifact("scoped");
382
- const seen = new Set();
383
- for (const scope of scopes) {
384
- assertRecord(scope, "scope", "invalid_artifact");
385
- assertExactKeys(scope, ["scope", "scope_version", "critical", "payload"], "scope", "invalid_artifact");
386
- const scopeVersion = scope.scope_version;
387
- if (typeof scope.scope !== "string" ||
388
- !scopePattern.test(scope.scope) ||
389
- typeof scopeVersion !== "number" ||
390
- !Number.isInteger(scopeVersion) ||
391
- scopeVersion < 1 ||
392
- scopeVersion > 0xffff ||
393
- typeof scope.critical !== "boolean" ||
394
- !isRecord(scope.payload) ||
395
- utf8Length(JSON.stringify(scope.payload)) > 4_096) {
396
- throw invalidArtifact("scope metadata");
397
- }
398
- if (seen.has(scope.scope))
399
- throw invalidArtifact("duplicate scope");
400
- seen.add(scope.scope);
401
- }
402
- }
403
- function validateCorrelationShape(correlation) {
404
- assertRecord(correlation, "correlation", "invalid_artifact");
405
- assertExactKeys(correlation, ["v", "tags"], "correlation", "invalid_artifact");
406
- if (correlation.v !== 2 || !Array.isArray(correlation.tags) || correlation.tags.length > 8) {
407
- throw invalidArtifact("correlation");
408
- }
409
- const seen = new Set();
410
- for (const tag of correlation.tags) {
411
- assertRecord(tag, "correlation tag", "invalid_artifact");
412
- assertExactKeys(tag, ["key", "value"], "correlation tag", "invalid_artifact");
413
- if (typeof tag.key !== "string" ||
414
- typeof tag.value !== "string" ||
415
- !correlationKeyPattern.test(tag.key) ||
416
- !validASCII(tag.value, 128) ||
417
- seen.has(tag.key)) {
418
- throw invalidArtifact("correlation tag");
419
- }
420
- seen.add(tag.key);
421
- }
422
- }
423
- function normalizeCandidateURL(kind, carrier, raw) {
424
- if (/[\\?#%]/.test(raw))
425
- throw invalidCandidate("forbidden URL component");
426
- const separator = raw.indexOf("://");
427
- if (separator <= 0)
428
- throw invalidCandidate("absolute URL");
429
- const scheme = raw.slice(0, separator).toLowerCase();
430
- const remainder = raw.slice(separator + 3);
431
- const pathAt = remainder.indexOf("/");
432
- const authority = pathAt < 0 ? remainder : remainder.slice(0, pathAt);
433
- let path = pathAt < 0 ? "" : remainder.slice(pathAt);
434
- if (authority === "" || authority.includes("@"))
435
- throw invalidCandidate("URL authority");
436
- const normalizedAuthority = normalizeAuthority(authority);
437
- let expectedScheme;
438
- let expectedPath = "";
439
- switch (carrier) {
440
- case "websocket":
441
- expectedScheme = "wss";
442
- expectedPath = `/flowersec/v2/${kind}`;
443
- break;
444
- case "raw_quic":
445
- expectedScheme = "quic";
446
- if (path !== "" && path !== "/")
447
- throw invalidCandidate("raw QUIC path");
448
- path = "";
449
- break;
450
- case "webtransport":
451
- expectedScheme = "https";
452
- expectedPath = `/flowersec/webtransport/v2/${kind}`;
453
- break;
454
- default:
455
- throw invalidCandidate("carrier registry");
456
- }
457
- const loopbackPlaintext = carrier === "websocket" && scheme === "ws" && kind === "direct" && isLoopbackHost(normalizedAuthority);
458
- if (scheme !== expectedScheme && !loopbackPlaintext)
459
- throw invalidCandidate("carrier scheme");
460
- if (carrier !== "raw_quic" && path !== expectedPath)
461
- throw invalidCandidate("carrier URL path");
462
- return `${scheme}://${normalizedAuthority}${path}`;
463
- }
464
- function isLoopbackHost(authority) {
465
- const host = authority.startsWith("[") ? authority.slice(1, authority.indexOf("]")) : authority.split(":")[0];
466
- return host === "127.0.0.1" || host === "::1";
467
- }
468
- function normalizeAuthority(authority) {
469
- let host;
470
- let portText = "";
471
- if (authority.startsWith("[")) {
472
- const closing = authority.indexOf("]");
473
- if (closing < 0 || authority.indexOf("]", closing + 1) >= 0)
474
- throw invalidCandidate("IPv6 authority");
475
- host = authority.slice(1, closing);
476
- const tail = authority.slice(closing + 1);
477
- if (tail !== "") {
478
- if (!tail.startsWith(":") || tail.length === 1)
479
- throw invalidCandidate("IPv6 port");
480
- portText = tail.slice(1);
481
- }
482
- if (host.includes("."))
483
- throw invalidCandidate("IPv6 dotted subset");
484
- let parsed;
485
- try {
486
- parsed = new URL(`http://[${host}]/`);
487
- }
488
- catch {
489
- throw invalidCandidate("IPv6 host");
490
- }
491
- const canonical = parsed.hostname;
492
- if (!canonical.startsWith("[") || !canonical.endsWith("]"))
493
- throw invalidCandidate("IPv6 host");
494
- host = canonical.toLowerCase();
495
- }
496
- else {
497
- if ((authority.match(/:/g)?.length ?? 0) > 1)
498
- throw invalidCandidate("unbracketed IPv6");
499
- const colon = authority.lastIndexOf(":");
500
- if (colon >= 0) {
501
- host = authority.slice(0, colon);
502
- portText = authority.slice(colon + 1);
503
- if (portText === "")
504
- throw invalidCandidate("empty port");
505
- }
506
- else {
507
- host = authority;
508
- }
509
- host = normalizeDNSOrIPv4(host);
510
- }
511
- if (portText === "")
512
- return host;
513
- if (!/^\d+$/.test(portText))
514
- throw invalidCandidate("port");
515
- const port = Number(portText);
516
- if (!Number.isInteger(port) || port < 1 || port > 0xffff)
517
- throw invalidCandidate("port");
518
- return port === 443 ? host : `${host}:${port}`;
519
- }
520
- function normalizeDNSOrIPv4(host) {
521
- if (host === "" || host.endsWith("."))
522
- throw invalidCandidate("DNS host");
523
- const lower = host.toLowerCase();
524
- if (/^[0-9.]+$/.test(lower)) {
525
- const parts = lower.split(".");
526
- if (parts.length !== 4 ||
527
- parts.some((part) => !/^(0|[1-9]\d{0,2})$/.test(part) || Number(part) > 255)) {
528
- throw invalidCandidate("IPv4 host");
529
- }
530
- return parts.map((part) => String(Number(part))).join(".");
531
- }
532
- const ascii = toASCII(host, {
533
- checkHyphens: true,
534
- checkBidi: true,
535
- checkJoiners: true,
536
- useSTD3ASCIIRules: true,
537
- verifyDNSLength: true,
538
- transitionalProcessing: false,
539
- });
540
- if (ascii === null || ascii.endsWith("."))
541
- throw invalidCandidate("DNS label");
542
- return ascii.toLowerCase();
543
- }
544
- function requestFromValidatedArtifact(artifact, candidateSet, chosenCandidateID) {
545
- const common = {
546
- profile: PROFILE,
547
- channel_id: artifact.session.channel_id,
548
- session_contract_hash_b64u: artifact.session.contract_hash_b64u,
549
- rendezvous_group_id: artifact.path.rendezvous_group_id,
550
- candidates: candidateSet.candidates,
551
- candidate_set_hash_b64u: candidateSet.hashBase64URL,
552
- chosen_candidate_id: chosenCandidateID,
553
- listener_audience: artifact.path.listener_audience,
554
- };
555
- if (artifact.path.kind === "direct") {
556
- return { pathKind: "direct", ...common, routing_token: artifact.path.routing_token };
557
- }
558
- return {
559
- pathKind: "tunnel",
560
- ...common,
561
- role: artifact.path.role,
562
- endpoint_instance_id: artifact.path.local_endpoint_instance_id,
563
- attach_token: artifact.path.token,
564
- };
565
- }
566
- function validateFSB2Request(request) {
567
- try {
568
- if (request.pathKind !== "direct" && request.pathKind !== "tunnel")
569
- throw new Error("path kind");
570
- if (request.profile !== PROFILE)
571
- throw new Error("profile");
572
- if (!validRegistryID(request.channel_id, 128) ||
573
- !validRegistryID(request.rendezvous_group_id, 128) ||
574
- !validRegistryID(request.listener_audience, 128)) {
575
- throw new Error("registry id");
576
- }
577
- assertCanonical32(request.session_contract_hash_b64u, "session hash", "invalid_fsb2");
578
- assertCanonical32(request.candidate_set_hash_b64u, "candidate hash", "invalid_fsb2");
579
- const source = request.candidates.map((candidate) => ({
580
- id: candidate.id,
581
- carrier: candidate.carrier,
582
- url: candidate.normalized_url,
583
- normalized_url: candidate.normalized_url,
584
- wire_profile: candidate.wire_profile,
585
- }));
586
- const canonical = canonicalizeCandidatesV2(request.pathKind, source);
587
- if (canonical.hashBase64URL !== request.candidate_set_hash_b64u ||
588
- JSON.stringify(canonical.candidates) !== JSON.stringify(request.candidates)) {
589
- throw new Error("candidate hash or ordering");
590
- }
591
- if (!request.candidates.some((candidate) => candidate.id === request.chosen_candidate_id)) {
592
- throw new Error("chosen candidate");
593
- }
594
- if (request.pathKind === "direct") {
595
- if (!validASCII(request.routing_token, MAX_ADMISSION_CREDENTIAL_BYTES))
596
- throw new Error("direct variant");
597
- }
598
- else if ((request.role !== 1 && request.role !== 2) ||
599
- !validRegistryID(request.endpoint_instance_id, 128) ||
600
- !validASCII(request.attach_token, MAX_ADMISSION_CREDENTIAL_BYTES)) {
601
- throw new Error("tunnel variant");
602
- }
603
- }
604
- catch (error) {
605
- if (error instanceof ArtifactV2Error && error.code === "invalid_fsb2")
606
- throw error;
607
- throw invalidFSB2(errorMessage(error));
608
- }
609
- }
610
- function marshalFSB2Payload(request) {
611
- const wire = request.pathKind === "direct"
612
- ? {
613
- candidate_set_hash_b64u: request.candidate_set_hash_b64u,
614
- candidates: request.candidates,
615
- channel_id: request.channel_id,
616
- chosen_candidate_id: request.chosen_candidate_id,
617
- listener_audience: request.listener_audience,
618
- profile: request.profile,
619
- rendezvous_group_id: request.rendezvous_group_id,
620
- routing_token: request.routing_token,
621
- session_contract_hash_b64u: request.session_contract_hash_b64u,
622
- }
623
- : {
624
- attach_token: request.attach_token,
625
- candidate_set_hash_b64u: request.candidate_set_hash_b64u,
626
- candidates: request.candidates,
627
- channel_id: request.channel_id,
628
- chosen_candidate_id: request.chosen_candidate_id,
629
- endpoint_instance_id: request.endpoint_instance_id,
630
- listener_audience: request.listener_audience,
631
- profile: request.profile,
632
- rendezvous_group_id: request.rendezvous_group_id,
633
- role: request.role,
634
- session_contract_hash_b64u: request.session_contract_hash_b64u,
635
- };
636
- return encoder.encode(JSON.stringify(wire));
637
- }
638
- function decodeArtifactValue(value) {
639
- const top = requireRecord(value, "artifact");
640
- assertExactKeys(top, ["v", "profile", "session", "path", "scoped", "correlation"], "artifact", "invalid_artifact");
641
- if (top.v !== 2 || top.profile !== PROFILE)
642
- throw invalidArtifact("version or profile");
643
- const session = decodeSessionValue(top.session);
644
- const path = decodePathValue(top.path);
645
- const scoped = decodeScopesValue(top.scoped);
646
- const correlation = decodeCorrelationValue(top.correlation);
647
- return { v: 2, profile: PROFILE, session, path, scoped, correlation };
648
- }
649
- function decodeSessionValue(value) {
650
- const session = requireRecord(value, "session");
651
- assertExactKeys(session, [
652
- "channel_id",
653
- "init_expire_at_unix_s",
654
- "idle_timeout_seconds",
655
- "establish_timeout_seconds",
656
- "rekey_prepare_timeout_seconds",
657
- "rekey_completion_timeout_seconds",
658
- "max_inbound_streams",
659
- "e2ee_psk_b64u",
660
- "allowed_suites",
661
- "default_suite",
662
- "selected_features",
663
- "contract_hash_b64u",
664
- ], "session", "invalid_artifact");
665
- const decoded = session;
666
- validateSession(decoded);
667
- return decoded;
668
- }
669
- function decodePathValue(value) {
670
- const path = requireRecord(value, "path");
671
- if (path.kind === "direct") {
672
- assertExactKeys(path, ["kind", "rendezvous_group_id", "listener_audience", "routing_token", "candidates"], "direct path", "invalid_artifact");
673
- return {
674
- kind: "direct",
675
- rendezvous_group_id: requireString(path.rendezvous_group_id, "rendezvous group"),
676
- listener_audience: requireString(path.listener_audience, "listener audience"),
677
- routing_token: requireString(path.routing_token, "routing token"),
678
- candidates: decodeCandidatesValue(path.candidates),
679
- };
680
- }
681
- if (path.kind === "tunnel") {
682
- assertExactKeys(path, [
683
- "kind",
684
- "rendezvous_group_id",
685
- "listener_audience",
686
- "role",
687
- "local_endpoint_instance_id",
688
- "expected_peer_endpoint_instance_id",
689
- "token",
690
- "candidates",
691
- ], "tunnel path", "invalid_artifact");
692
- if (path.role !== 1 && path.role !== 2)
693
- throw invalidArtifact("tunnel role");
694
- return {
695
- kind: "tunnel",
696
- rendezvous_group_id: requireString(path.rendezvous_group_id, "rendezvous group"),
697
- listener_audience: requireString(path.listener_audience, "listener audience"),
698
- role: path.role,
699
- local_endpoint_instance_id: requireString(path.local_endpoint_instance_id, "local endpoint"),
700
- expected_peer_endpoint_instance_id: requireString(path.expected_peer_endpoint_instance_id, "peer endpoint"),
701
- token: requireString(path.token, "attach token"),
702
- candidates: decodeCandidatesValue(path.candidates),
703
- };
704
- }
705
- throw invalidArtifact("path kind");
706
- }
707
- function decodeCandidatesValue(value) {
708
- if (!Array.isArray(value))
709
- throw invalidArtifact("candidates");
710
- return value.map((item) => {
711
- const candidate = requireRecord(item, "candidate");
712
- assertExactKeys(candidate, ["id", "carrier", "url", "wire_profile"], "candidate", "invalid_artifact");
713
- return {
714
- id: requireString(candidate.id, "candidate id"),
715
- carrier: requireString(candidate.carrier, "candidate carrier"),
716
- url: requireString(candidate.url, "candidate URL"),
717
- wire_profile: requireString(candidate.wire_profile, "wire profile"),
718
- };
719
- });
720
- }
721
- function decodeScopesValue(value) {
722
- if (!Array.isArray(value))
723
- throw invalidArtifact("scoped");
724
- return value.map((item) => {
725
- const scope = requireRecord(item, "scope");
726
- assertExactKeys(scope, ["scope", "scope_version", "critical", "payload"], "scope", "invalid_artifact");
727
- if (typeof scope.critical !== "boolean" || !isRecord(scope.payload))
728
- throw invalidArtifact("scope metadata");
729
- return {
730
- scope: requireString(scope.scope, "scope name"),
731
- scope_version: requireNumber(scope.scope_version, "scope version"),
732
- critical: scope.critical,
733
- payload: scope.payload,
734
- };
735
- });
736
- }
737
- function decodeCorrelationValue(value) {
738
- const correlation = requireRecord(value, "correlation");
739
- assertExactKeys(correlation, ["v", "tags"], "correlation", "invalid_artifact");
740
- if (correlation.v !== 2 || !Array.isArray(correlation.tags))
741
- throw invalidArtifact("correlation");
742
- return {
743
- v: 2,
744
- tags: correlation.tags.map((item) => {
745
- const tag = requireRecord(item, "correlation tag");
746
- assertExactKeys(tag, ["key", "value"], "correlation tag", "invalid_artifact");
747
- return {
748
- key: requireString(tag.key, "correlation key"),
749
- value: requireString(tag.value, "correlation value"),
750
- };
751
- }),
752
- };
753
- }
754
- function decodeFSB2Value(pathKind, value) {
755
- const wire = requireRecord(value, "FSB2 payload", "invalid_fsb2");
756
- const commonKeys = [
757
- "candidate_set_hash_b64u",
758
- "candidates",
759
- "channel_id",
760
- "chosen_candidate_id",
761
- "listener_audience",
762
- "profile",
763
- "rendezvous_group_id",
764
- "session_contract_hash_b64u",
765
- ];
766
- if (pathKind === "direct") {
767
- assertExactKeys(wire, [...commonKeys, "routing_token"], "direct FSB2", "invalid_fsb2");
768
- }
769
- else {
770
- assertExactKeys(wire, [...commonKeys, "attach_token", "endpoint_instance_id", "role"], "tunnel FSB2", "invalid_fsb2");
771
- }
772
- const candidates = decodeCanonicalCandidatesValue(wire.candidates);
773
- const common = {
774
- profile: requireString(wire.profile, "profile", "invalid_fsb2"),
775
- channel_id: requireString(wire.channel_id, "channel ID", "invalid_fsb2"),
776
- session_contract_hash_b64u: requireString(wire.session_contract_hash_b64u, "session hash", "invalid_fsb2"),
777
- rendezvous_group_id: requireString(wire.rendezvous_group_id, "rendezvous group", "invalid_fsb2"),
778
- candidates,
779
- candidate_set_hash_b64u: requireString(wire.candidate_set_hash_b64u, "candidate hash", "invalid_fsb2"),
780
- chosen_candidate_id: requireString(wire.chosen_candidate_id, "chosen candidate", "invalid_fsb2"),
781
- listener_audience: requireString(wire.listener_audience, "listener audience", "invalid_fsb2"),
782
- };
783
- if (pathKind === "direct") {
784
- return {
785
- pathKind,
786
- ...common,
787
- routing_token: requireString(wire.routing_token, "routing token", "invalid_fsb2"),
788
- };
789
- }
790
- if (wire.role !== 1 && wire.role !== 2)
791
- throw invalidFSB2("role");
792
- return {
793
- pathKind,
794
- ...common,
795
- role: wire.role,
796
- endpoint_instance_id: requireString(wire.endpoint_instance_id, "endpoint instance", "invalid_fsb2"),
797
- attach_token: requireString(wire.attach_token, "attach token", "invalid_fsb2"),
798
- };
799
- }
800
- function decodeCanonicalCandidatesValue(value) {
801
- if (!Array.isArray(value))
802
- throw invalidFSB2("candidates");
803
- return value.map((item) => {
804
- const candidate = requireRecord(item, "canonical candidate", "invalid_fsb2");
805
- assertExactKeys(candidate, ["carrier", "id", "normalized_url", "wire_profile"], "canonical candidate", "invalid_fsb2");
806
- return {
807
- carrier: requireString(candidate.carrier, "carrier", "invalid_fsb2"),
808
- id: requireString(candidate.id, "candidate ID", "invalid_fsb2"),
809
- normalized_url: requireString(candidate.normalized_url, "normalized URL", "invalid_fsb2"),
810
- wire_profile: requireString(candidate.wire_profile, "wire profile", "invalid_fsb2"),
811
- };
812
- });
813
- }
814
- function artifactSessionWire(session) {
815
- return {
816
- channel_id: session.channel_id,
817
- init_expire_at_unix_s: session.init_expire_at_unix_s,
818
- idle_timeout_seconds: session.idle_timeout_seconds,
819
- establish_timeout_seconds: session.establish_timeout_seconds,
820
- rekey_prepare_timeout_seconds: session.rekey_prepare_timeout_seconds,
821
- rekey_completion_timeout_seconds: session.rekey_completion_timeout_seconds,
822
- max_inbound_streams: session.max_inbound_streams,
823
- e2ee_psk_b64u: session.e2ee_psk_b64u,
824
- allowed_suites: session.allowed_suites,
825
- default_suite: session.default_suite,
826
- selected_features: session.selected_features,
827
- contract_hash_b64u: session.contract_hash_b64u,
828
- };
829
- }
830
- function directPathWire(path) {
831
- return {
832
- kind: path.kind,
833
- rendezvous_group_id: path.rendezvous_group_id,
834
- listener_audience: path.listener_audience,
835
- routing_token: path.routing_token,
836
- candidates: path.candidates.map(candidateWire),
837
- };
838
- }
839
- function tunnelPathWire(path) {
840
- return {
841
- kind: path.kind,
842
- rendezvous_group_id: path.rendezvous_group_id,
843
- listener_audience: path.listener_audience,
844
- role: path.role,
845
- local_endpoint_instance_id: path.local_endpoint_instance_id,
846
- expected_peer_endpoint_instance_id: path.expected_peer_endpoint_instance_id,
847
- token: path.token,
848
- candidates: path.candidates.map(candidateWire),
849
- };
850
- }
851
- function candidateWire(candidate) {
852
- return {
853
- id: candidate.id,
854
- carrier: candidate.carrier,
855
- url: candidate.url,
856
- wire_profile: candidate.wire_profile,
857
- };
858
- }
859
- function validateFSA2Response(response) {
860
- switch (response.status) {
861
- case AdmissionStatusV2.Success:
862
- if (response.reason !== "")
863
- throw invalidFSA2("success reason");
864
- return;
865
- case AdmissionStatusV2.Reject:
866
- case AdmissionStatusV2.Retryable:
867
- if (!/^[a-z][a-z0-9_]*$/.test(response.reason) || utf8Length(response.reason) > MAX_ADMISSION_REASON_BYTES) {
868
- throw invalidFSA2("reason token");
869
- }
870
- return;
871
- default:
872
- throw invalidFSA2("status");
873
- }
874
- }
875
- function labeledHash(label, canonicalJSON) {
876
- const canonical = encoder.encode(canonicalJSON);
877
- const hash = sha256(concatBytes([encoder.encode(label), u32be(canonical.length), canonical]));
878
- return { canonicalJSON, hash, hashBase64URL: base64urlEncode(hash) };
879
- }
880
- function assertCanonical32(value, name, errorCode) {
881
- if (typeof value !== "string")
882
- throw codecError(errorCode, `${name} type`);
883
- try {
884
- const decoded = base64urlDecode(value);
885
- if (decoded.length !== 32 || base64urlEncode(decoded) !== value)
886
- throw new Error("length");
887
- return decoded;
888
- }
889
- catch {
890
- throw codecError(errorCode, `${name} encoding`);
891
- }
892
- }
893
- function decodeBoundedJSON(raw) {
894
- if (typeof raw === "string") {
895
- if (utf8Length(raw) > MAX_ARTIFACT_JSON_BYTES) {
896
- throw new ArtifactV2Error("artifact_too_large", "Flowersec v2 artifact is too large");
897
- }
898
- return raw;
899
- }
900
- if (raw.length > MAX_ARTIFACT_JSON_BYTES) {
901
- throw new ArtifactV2Error("artifact_too_large", "Flowersec v2 artifact is too large");
902
- }
903
- try {
904
- return decoder.decode(raw);
905
- }
906
- catch {
907
- throw invalidArtifact("UTF-8");
908
- }
909
- }
910
- function parseStrictJSON(text) {
911
- new DuplicateJSONKeyScanner(text).scan();
912
- return JSON.parse(text);
913
- }
914
- class DuplicateJSONKeyScanner {
915
- text;
916
- index = 0;
917
- constructor(text) {
918
- this.text = text;
919
- }
920
- scan() {
921
- this.value(0);
922
- this.whitespace();
923
- if (this.index !== this.text.length)
924
- throw new Error("trailing JSON value");
925
- }
926
- value(depth) {
927
- if (depth > 128)
928
- throw new Error("JSON nesting is too deep");
929
- this.whitespace();
930
- const char = this.text[this.index];
931
- if (char === "{")
932
- return this.object(depth + 1);
933
- if (char === "[")
934
- return this.array(depth + 1);
935
- if (char === '"') {
936
- this.string();
937
- return;
938
- }
939
- if (char === "t")
940
- return this.literal("true");
941
- if (char === "f")
942
- return this.literal("false");
943
- if (char === "n")
944
- return this.literal("null");
945
- this.number();
946
- }
947
- object(depth) {
948
- this.index += 1;
949
- this.whitespace();
950
- const seen = new Set();
951
- if (this.text[this.index] === "}") {
952
- this.index += 1;
953
- return;
954
- }
955
- while (true) {
956
- this.whitespace();
957
- if (this.text[this.index] !== '"')
958
- throw new Error("JSON object key is not a string");
959
- const key = this.string();
960
- if (seen.has(key))
961
- throw new Error(`duplicate JSON field ${JSON.stringify(key)}`);
962
- seen.add(key);
963
- this.whitespace();
964
- if (this.text[this.index] !== ":")
965
- throw new Error("missing JSON colon");
966
- this.index += 1;
967
- this.value(depth);
968
- this.whitespace();
969
- const next = this.text[this.index];
970
- if (next === "}") {
971
- this.index += 1;
972
- return;
973
- }
974
- if (next !== ",")
975
- throw new Error("invalid JSON object separator");
976
- this.index += 1;
977
- }
978
- }
979
- array(depth) {
980
- this.index += 1;
981
- this.whitespace();
982
- if (this.text[this.index] === "]") {
983
- this.index += 1;
984
- return;
985
- }
986
- while (true) {
987
- this.value(depth);
988
- this.whitespace();
989
- const next = this.text[this.index];
990
- if (next === "]") {
991
- this.index += 1;
992
- return;
993
- }
994
- if (next !== ",")
995
- throw new Error("invalid JSON array separator");
996
- this.index += 1;
997
- }
998
- }
999
- string() {
1000
- const start = this.index;
1001
- this.index += 1;
1002
- while (this.index < this.text.length) {
1003
- const code = this.text.charCodeAt(this.index);
1004
- if (code === 0x22) {
1005
- this.index += 1;
1006
- return JSON.parse(this.text.slice(start, this.index));
1007
- }
1008
- if (code < 0x20)
1009
- throw new Error("control character in JSON string");
1010
- if (code === 0x5c) {
1011
- this.index += 1;
1012
- const escaped = this.text[this.index];
1013
- if (escaped === "u") {
1014
- const digits = this.text.slice(this.index + 1, this.index + 5);
1015
- if (!/^[0-9a-fA-F]{4}$/.test(digits))
1016
- throw new Error("invalid JSON unicode escape");
1017
- this.index += 5;
1018
- continue;
1019
- }
1020
- if (escaped === undefined || !'"\\/bfnrt'.includes(escaped)) {
1021
- throw new Error("invalid JSON escape");
1022
- }
1023
- }
1024
- this.index += 1;
1025
- }
1026
- throw new Error("unterminated JSON string");
1027
- }
1028
- number() {
1029
- const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(this.text.slice(this.index));
1030
- if (match === null)
1031
- throw new Error("invalid JSON value");
1032
- this.index += match[0].length;
1033
- }
1034
- literal(value) {
1035
- if (!this.text.startsWith(value, this.index))
1036
- throw new Error("invalid JSON literal");
1037
- this.index += value.length;
1038
- }
1039
- whitespace() {
1040
- while (/[\u0009\u000a\u000d\u0020]/.test(this.text[this.index] ?? ""))
1041
- this.index += 1;
1042
- }
1043
- }
1044
- function assertExactKeys(value, expected, name, code) {
1045
- const actual = Object.keys(value);
1046
- if (actual.length !== expected.length || expected.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) {
1047
- throw codecError(code, `${name} fields`);
1048
- }
1049
- }
1050
- function assertRecord(value, name, code) {
1051
- if (!isRecord(value))
1052
- throw codecError(code, `${name} object`);
1053
- }
1054
- function requireRecord(value, name, code = "invalid_artifact") {
1055
- if (!isRecord(value))
1056
- throw codecError(code, `${name} object`);
1057
- return value;
1058
- }
1059
- function requireString(value, name, code = "invalid_artifact") {
1060
- if (typeof value !== "string")
1061
- throw codecError(code, `${name} string`);
1062
- return value;
1063
- }
1064
- function requireNumber(value, name) {
1065
- if (typeof value !== "number")
1066
- throw invalidArtifact(`${name} number`);
1067
- return value;
1068
- }
1069
- function isRecord(value) {
1070
- return typeof value === "object" && value !== null && !Array.isArray(value);
1071
- }
1072
- function assertSafeInteger(value, minimum, maximum, name) {
1073
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
1074
- throw invalidArtifact(name);
1075
- }
1076
- }
1077
- function validRegistryID(value, maximum) {
1078
- return typeof value === "string" && utf8Length(value) >= 1 && utf8Length(value) <= maximum && registryIDPattern.test(value);
1079
- }
1080
- function validASCII(value, maximum) {
1081
- if (typeof value !== "string" || value.length < 1 || value.length > maximum)
1082
- return false;
1083
- for (let index = 0; index < value.length; index += 1) {
1084
- if (value.charCodeAt(index) > 0x7f)
1085
- return false;
1086
- }
1087
- return true;
1088
- }
1089
- function utf8Length(value) {
1090
- return encoder.encode(value).length;
1091
- }
1092
- function bytesEqual(left, right) {
1093
- if (left.length !== right.length)
1094
- return false;
1095
- let difference = 0;
1096
- for (let index = 0; index < left.length; index += 1)
1097
- difference |= left[index] ^ right[index];
1098
- return difference === 0;
1099
- }
1100
- function pathKindFromCode(code) {
1101
- if (code === 1)
1102
- return "direct";
1103
- if (code === 2)
1104
- return "tunnel";
1105
- throw invalidFSB2("path code");
1106
- }
1107
- function codecError(code, detail) {
1108
- switch (code) {
1109
- case "invalid_artifact":
1110
- return invalidArtifact(detail);
1111
- case "invalid_candidate":
1112
- return invalidCandidate(detail);
1113
- case "invalid_fsb2":
1114
- return invalidFSB2(detail);
1115
- }
1116
- }
1117
- function invalidArtifact(detail) {
1118
- return new ArtifactV2Error("invalid_artifact", `invalid Flowersec v2 artifact: ${detail}`);
1119
- }
1120
- function invalidCandidate(detail) {
1121
- return new ArtifactV2Error("invalid_candidate", `invalid Flowersec v2 candidate: ${detail}`);
1122
- }
1123
- function invalidFSB2(detail) {
1124
- return new ArtifactV2Error("invalid_fsb2", `invalid FSB2 admission request: ${detail}`);
1125
- }
1126
- function invalidFSA2(detail) {
1127
- return new ArtifactV2Error("invalid_fsa2", `invalid FSA2 admission response: ${detail}`);
1128
- }
1129
- function errorMessage(error) {
1130
- return error instanceof Error ? error.message : String(error);
1131
- }