@latchway/react-native 0.0.0-bootstrap.0 → 1.1.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 (108) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/LatchwayReactNative.podspec +33 -0
  3. package/NOTICE +7 -0
  4. package/README.md +293 -3
  5. package/SECURITY.md +69 -0
  6. package/android/build.gradle.kts +62 -0
  7. package/android/consumer-rules.pro +2 -0
  8. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  9. package/android/gradle/wrapper/gradle-wrapper.properties +9 -0
  10. package/android/gradle.properties +4 -0
  11. package/android/gradlew +251 -0
  12. package/android/gradlew.bat +94 -0
  13. package/android/settings.gradle.kts +33 -0
  14. package/android/src/main/AndroidManifest.xml +3 -0
  15. package/android/src/main/java/dev/latchway/reactnative/LatchwayReactNativePackage.kt +25 -0
  16. package/android/src/main/java/dev/latchway/reactnative/NativeLatchwayModule.kt +964 -0
  17. package/android/src/test/java/dev/latchway/reactnative/NativeLatchwayModuleTest.kt +521 -0
  18. package/babel.cjs +22 -0
  19. package/babel.d.cts +7 -0
  20. package/contract.lock +7 -0
  21. package/docs/architecture.md +145 -0
  22. package/docs/conformance.md +68 -0
  23. package/docs/langchain.md +135 -0
  24. package/docs/native-installation.md +172 -0
  25. package/docs/physical-device-evidence.md +559 -0
  26. package/docs/releasing.md +360 -0
  27. package/docs/security.md +55 -0
  28. package/ios/LatchwayNativeBridge.swift +1551 -0
  29. package/ios/RCTNativeLatchway.h +9 -0
  30. package/ios/RCTNativeLatchway.mm +175 -0
  31. package/lib/client.d.ts +29 -0
  32. package/lib/client.d.ts.map +1 -0
  33. package/lib/client.js +939 -0
  34. package/lib/client.js.map +1 -0
  35. package/lib/component-client.d.ts +15 -0
  36. package/lib/component-client.d.ts.map +1 -0
  37. package/lib/component-client.js +141 -0
  38. package/lib/component-client.js.map +1 -0
  39. package/lib/config.d.ts +25 -0
  40. package/lib/config.d.ts.map +1 -0
  41. package/lib/config.js +261 -0
  42. package/lib/config.js.map +1 -0
  43. package/lib/coordinator.d.ts +19 -0
  44. package/lib/coordinator.d.ts.map +1 -0
  45. package/lib/coordinator.js +167 -0
  46. package/lib/coordinator.js.map +1 -0
  47. package/lib/errors.d.ts +5 -0
  48. package/lib/errors.d.ts.map +1 -0
  49. package/lib/errors.js +201 -0
  50. package/lib/errors.js.map +1 -0
  51. package/lib/index.d.ts +8 -0
  52. package/lib/index.d.ts.map +1 -0
  53. package/lib/index.js +13 -0
  54. package/lib/index.js.map +1 -0
  55. package/lib/native/NativeLatchway.d.ts +25 -0
  56. package/lib/native/NativeLatchway.d.ts.map +1 -0
  57. package/lib/native/NativeLatchway.js +3 -0
  58. package/lib/native/NativeLatchway.js.map +1 -0
  59. package/lib/native/bridge.d.ts +5 -0
  60. package/lib/native/bridge.d.ts.map +1 -0
  61. package/lib/native/bridge.js +17 -0
  62. package/lib/native/bridge.js.map +1 -0
  63. package/lib/native-output.d.ts +3 -0
  64. package/lib/native-output.d.ts.map +1 -0
  65. package/lib/native-output.js +43 -0
  66. package/lib/native-output.js.map +1 -0
  67. package/lib/polyfills.d.ts +8 -0
  68. package/lib/polyfills.d.ts.map +1 -0
  69. package/lib/polyfills.js +56 -0
  70. package/lib/polyfills.js.map +1 -0
  71. package/lib/request-id.d.ts +2 -0
  72. package/lib/request-id.d.ts.map +1 -0
  73. package/lib/request-id.js +5 -0
  74. package/lib/request-id.js.map +1 -0
  75. package/lib/runtime-symbols.d.ts +2 -0
  76. package/lib/runtime-symbols.d.ts.map +1 -0
  77. package/lib/runtime-symbols.js +9 -0
  78. package/lib/runtime-symbols.js.map +1 -0
  79. package/lib/testing.d.ts +7 -0
  80. package/lib/testing.d.ts.map +1 -0
  81. package/lib/testing.js +9 -0
  82. package/lib/testing.js.map +1 -0
  83. package/lib/types.d.ts +200 -0
  84. package/lib/types.d.ts.map +1 -0
  85. package/lib/types.js +2 -0
  86. package/lib/types.js.map +1 -0
  87. package/lib/version.d.ts +8 -0
  88. package/lib/version.d.ts.map +1 -0
  89. package/lib/version.js +8 -0
  90. package/lib/version.js.map +1 -0
  91. package/package.json +152 -6
  92. package/react-native.config.cjs +7 -0
  93. package/release-compatibility.json +64 -0
  94. package/src/client.ts +1022 -0
  95. package/src/component-client.ts +158 -0
  96. package/src/config.ts +368 -0
  97. package/src/coordinator.ts +195 -0
  98. package/src/errors.ts +225 -0
  99. package/src/index.ts +53 -0
  100. package/src/native/NativeLatchway.ts +75 -0
  101. package/src/native/bridge.ts +23 -0
  102. package/src/native-output.ts +43 -0
  103. package/src/polyfills.ts +50 -0
  104. package/src/request-id.ts +5 -0
  105. package/src/runtime-symbols.ts +9 -0
  106. package/src/testing.ts +11 -0
  107. package/src/types.ts +241 -0
  108. package/src/version.ts +7 -0
package/lib/client.js ADDED
@@ -0,0 +1,939 @@
1
+ import { LatchwayError } from "@latchway/client";
2
+ import { ReadableStream as PonyfillReadableStream } from "web-streams-polyfill";
3
+ import { encodeIOSComponentDescriptor, encodeIOSComponentDescriptors } from "./config.js";
4
+ import { acquire } from "./coordinator.js";
5
+ import { parseComponentDiagnostics } from "./component-client.js";
6
+ import { abortError, fromNativeError } from "./errors.js";
7
+ import { assertNoCredentialFields } from "./native-output.js";
8
+ import { CONTRACT_VERSION, PROTOCOL_VERSION, SDK_VERSION } from "./version.js";
9
+ const MAXIMUM_REQUEST_BODY_BYTES = 8 * 1024 * 1024;
10
+ const MAXIMUM_NATIVE_REQUEST_BYTES = 12 * 1024 * 1024;
11
+ const MAXIMUM_RESPONSE_CHUNK_BYTES = 32 * 1024;
12
+ const MAXIMUM_HEADERS = 128;
13
+ const MAXIMUM_HEADER_BYTES = 128 * 1024;
14
+ const allowedDataPlanePaths = new Set([
15
+ "/v1/responses",
16
+ "/v1/chat/completions",
17
+ "/v1/embeddings",
18
+ "/v1/messages",
19
+ ]);
20
+ const opaqueDataPlaneMethods = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
21
+ const forbiddenCredentialHeaders = new Set([
22
+ "authorization",
23
+ "proxy-authorization",
24
+ "api-key",
25
+ "api_key",
26
+ "apikey",
27
+ "x-api-key",
28
+ "openai-api-key",
29
+ "openai_api_key",
30
+ "x-openai-api-key",
31
+ "anthropic-api-key",
32
+ "anthropic_api_key",
33
+ "x-goog-api-key",
34
+ "x-goog_api_key",
35
+ "access_token",
36
+ "auth_token",
37
+ "x-auth-token",
38
+ "cookie",
39
+ "connection",
40
+ "content-length",
41
+ "expect",
42
+ "host",
43
+ "key",
44
+ "proxy-connection",
45
+ "te",
46
+ "trailer",
47
+ "transfer-encoding",
48
+ "token",
49
+ "upgrade",
50
+ "x-amz-credential",
51
+ "x-amz-security-token",
52
+ "x-amz-signature",
53
+ "x-goog-credential",
54
+ "x-goog-signature",
55
+ "dpop",
56
+ "dpop-nonce",
57
+ "x-latchway-feature",
58
+ "x-latchway-framework",
59
+ "x-latchway-framework-version",
60
+ "x-latchway-protocol-version",
61
+ "x-latchway-request-id",
62
+ "x-latchway-sdk",
63
+ "x-latchway-sdk-version",
64
+ ]);
65
+ const forbiddenCredentialQueryNames = new Set([
66
+ ...forbiddenCredentialHeaders,
67
+ "refresh_token",
68
+ "identity_token",
69
+ "private_key",
70
+ "client_data_hash",
71
+ "request_hash",
72
+ "integrity_token",
73
+ ]);
74
+ const forbiddenCredentialNameFragments = [
75
+ "authorization",
76
+ "dpop",
77
+ "apikey",
78
+ "accesstoken",
79
+ "authtoken",
80
+ "refreshtoken",
81
+ "identitytoken",
82
+ "integritytoken",
83
+ "sessiontoken",
84
+ "privatekey",
85
+ "clientsecret",
86
+ "credential",
87
+ "attestationevidence",
88
+ "clientdatahash",
89
+ "requesthash",
90
+ "xamzsignature",
91
+ "xgoogsignature",
92
+ ];
93
+ const forbiddenExactCredentialNames = new Set([
94
+ "key", "token", "secret", "bearer", "cookie", "password", "passwd",
95
+ ]);
96
+ const safeResponseHeaderNames = new Set([
97
+ "accept-ranges",
98
+ "age",
99
+ "cache-control",
100
+ "content-encoding",
101
+ "content-language",
102
+ "content-length",
103
+ "content-range",
104
+ "content-type",
105
+ "date",
106
+ "etag",
107
+ "expires",
108
+ "last-modified",
109
+ "request-id",
110
+ "retry-after",
111
+ "server-timing",
112
+ "vary",
113
+ "x-request-id",
114
+ "x-latchway-operation-id",
115
+ "x-latchway-request-id",
116
+ "x-latchway-server-version",
117
+ ]);
118
+ const nativeControlledHeaders = new Set([
119
+ "x-latchway-feature",
120
+ "x-latchway-framework",
121
+ "x-latchway-framework-version",
122
+ "x-latchway-protocol-version",
123
+ "x-latchway-request-id",
124
+ "x-latchway-sdk",
125
+ "x-latchway-sdk-version",
126
+ ]);
127
+ let nextOperationID = 1;
128
+ export class DefaultLatchwayClient {
129
+ config;
130
+ gatewayURL;
131
+ ready;
132
+ lease;
133
+ disposed = false;
134
+ constructor(config) {
135
+ this.config = config;
136
+ this.gatewayURL = config.baseURL.origin;
137
+ this.lease = acquire(config);
138
+ this.ready = this.lease.then(async (lease) => { await lease.ready; });
139
+ }
140
+ async fetch(input, init = {}) {
141
+ this.assertActive();
142
+ const { latchwayFeature, ...requestInit } = init;
143
+ const bodyExpected = requestBodyExpected(input, requestInit);
144
+ const request = this.createRequest(input, requestInit);
145
+ const feature = latchwayFeature ?? request.headers.get("X-Latchway-Feature") ?? undefined;
146
+ assertFeature(feature);
147
+ this.assertGatewayTarget(request.url, request.method, feature);
148
+ if (request.bodyUsed) {
149
+ throw new LatchwayError("request_not_replayable", "The request body has already been consumed.");
150
+ }
151
+ const lease = await this.lease;
152
+ await lease.ready;
153
+ const signal = request.signal;
154
+ const headers = sanitizedRequestHeaders(request.headers);
155
+ const bodyBase64 = await encodedRequestBody(request, signal, bodyExpected);
156
+ const requestJSON = JSON.stringify({
157
+ url: request.url,
158
+ method: request.method.toUpperCase(),
159
+ feature,
160
+ headers,
161
+ bodyBase64,
162
+ });
163
+ if (new TextEncoder().encode(requestJSON).byteLength > MAXIMUM_NATIVE_REQUEST_BYTES) {
164
+ throw new LatchwayError("request_invalid", "The Latchway request exceeds the native bridge limit.");
165
+ }
166
+ const operationID = makeOperationID();
167
+ const identityToken = await token(this.config.getIdentityToken, signal);
168
+ const start = lease.module.startRequest(lease.clientID, operationID, identityToken, requestJSON);
169
+ const observedStart = start.then(async (value) => {
170
+ if (signal.aborted) {
171
+ const responseID = recoverResponseID(value);
172
+ if (responseID !== undefined)
173
+ await ignoreFailure(lease.module.closeResponse(lease.clientID, responseID));
174
+ }
175
+ return value;
176
+ });
177
+ const encoded = await abortable(observedStart, signal, () => { lease.module.cancel(lease.clientID, operationID); });
178
+ let metadata;
179
+ try {
180
+ metadata = parseResponseMetadata(encoded);
181
+ }
182
+ catch (cause) {
183
+ const responseID = recoverResponseID(encoded);
184
+ if (responseID !== undefined)
185
+ await ignoreFailure(lease.module.closeResponse(lease.clientID, responseID));
186
+ throw cause;
187
+ }
188
+ if (signal.aborted) {
189
+ await ignoreFailure(lease.module.closeResponse(lease.clientID, metadata.responseID));
190
+ throw abortError();
191
+ }
192
+ const hasBody = request.method.toUpperCase() !== "HEAD" &&
193
+ metadata.status !== 204 && metadata.status !== 205 && metadata.status !== 304;
194
+ const body = hasBody ? nativeResponseBody(lease, metadata.responseID, signal) : null;
195
+ if (!hasBody)
196
+ await ignoreFailure(lease.module.closeResponse(lease.clientID, metadata.responseID));
197
+ return responseWithNativeBody(body, {
198
+ status: metadata.status,
199
+ statusText: metadata.statusText,
200
+ headers: metadata.headers,
201
+ });
202
+ }
203
+ fetchFor(feature) {
204
+ this.assertActive();
205
+ assertFeature(feature);
206
+ return async (input, init = {}) => aliasFrameworkRequestID(await this.fetch(input, { ...init, latchwayFeature: feature }));
207
+ }
208
+ async quota(feature) {
209
+ this.assertActive();
210
+ assertFeature(feature);
211
+ const encoded = await this.nativeString("quota", undefined, feature);
212
+ return parseQuota(encoded, feature);
213
+ }
214
+ async diagnostics() {
215
+ this.assertActive();
216
+ const lease = await this.lease;
217
+ const compatibility = await lease.ready;
218
+ const encoded = await this.nativeString("diagnostics");
219
+ return parseDiagnostics(encoded, compatibility.platform, compatibility.nativeSDKVersion);
220
+ }
221
+ async refresh() {
222
+ this.assertActive();
223
+ await this.nativeVoid("refresh");
224
+ }
225
+ async prepareComponents(components) {
226
+ this.assertActive();
227
+ const componentsJSON = encodeIOSComponentDescriptors(components, this.config.appleSharedKeychainAccessGroups);
228
+ const snapshot = JSON.parse(componentsJSON);
229
+ const encoded = await this.nativeComponentOperation("prepare", componentsJSON);
230
+ return parsePreparedComponents(encoded, snapshot);
231
+ }
232
+ async revokeComponent(component) {
233
+ this.assertActive();
234
+ const componentJSON = encodeIOSComponentDescriptor(component, this.config.appleSharedKeychainAccessGroups);
235
+ await this.nativeComponentOperation("revokeComponent", componentJSON);
236
+ }
237
+ async replaceComponent(component) {
238
+ this.assertActive();
239
+ const componentJSON = encodeIOSComponentDescriptor(component, this.config.appleSharedKeychainAccessGroups);
240
+ const snapshot = JSON.parse(componentJSON);
241
+ const encoded = await this.nativeComponentOperation("replace", componentJSON);
242
+ return parseComponentDiagnostics(encoded, snapshot);
243
+ }
244
+ async componentDiagnostics(component) {
245
+ this.assertActive();
246
+ const componentJSON = encodeIOSComponentDescriptor(component, this.config.appleSharedKeychainAccessGroups);
247
+ const snapshot = JSON.parse(componentJSON);
248
+ const lease = await this.lease;
249
+ await lease.ready;
250
+ const operationID = makeOperationID();
251
+ const operation = lease.module.rootComponentDiagnostics(lease.clientID, operationID, componentJSON);
252
+ const encoded = await abortable(operation, undefined, () => {
253
+ lease.module.cancel(lease.clientID, operationID);
254
+ });
255
+ return parseComponentDiagnostics(encoded, snapshot);
256
+ }
257
+ async revokeCurrentInstallation() {
258
+ this.assertActive();
259
+ await this.nativeVoid("revoke");
260
+ }
261
+ async revokeCurrentInstallationFamily(retiring = []) {
262
+ this.assertActive();
263
+ if (retiring.length === 0) {
264
+ await this.nativeVoid("revokeFamily");
265
+ return;
266
+ }
267
+ const componentsJSON = encodeIOSComponentDescriptors(retiring, this.config.appleSharedKeychainAccessGroups);
268
+ await this.nativeComponentOperation("revokeFamily", componentsJSON);
269
+ }
270
+ async dispose() {
271
+ if (this.disposed)
272
+ return;
273
+ this.disposed = true;
274
+ const lease = await this.lease;
275
+ await lease.release();
276
+ }
277
+ async nativeString(method, signal, argument) {
278
+ const lease = await this.lease;
279
+ await lease.ready;
280
+ const operationID = makeOperationID();
281
+ const identityToken = await token(this.config.getIdentityToken, signal);
282
+ const operation = method === "quota"
283
+ ? lease.module.quota(lease.clientID, operationID, identityToken, argument ?? "")
284
+ : lease.module.diagnostics(lease.clientID, operationID, identityToken);
285
+ return abortable(operation, signal, () => { lease.module.cancel(lease.clientID, operationID); });
286
+ }
287
+ async nativeVoid(method, signal) {
288
+ const lease = await this.lease;
289
+ await lease.ready;
290
+ const operationID = makeOperationID();
291
+ const identityToken = await token(this.config.getIdentityToken, signal);
292
+ const operation = method === "refresh"
293
+ ? lease.module.refresh(lease.clientID, operationID, identityToken)
294
+ : method === "revoke"
295
+ ? lease.module.revoke(lease.clientID, operationID, identityToken)
296
+ : lease.module.revokeFamily(lease.clientID, operationID, identityToken);
297
+ await abortable(operation, signal, () => { lease.module.cancel(lease.clientID, operationID); });
298
+ }
299
+ async nativeComponentOperation(method, encodedDescriptor, signal) {
300
+ const lease = await this.lease;
301
+ await lease.ready;
302
+ const operationID = makeOperationID();
303
+ const identityToken = await token(this.config.getIdentityToken, signal);
304
+ const operation = method === "prepare"
305
+ ? lease.module.prepareComponents(lease.clientID, operationID, identityToken, encodedDescriptor)
306
+ : method === "replace"
307
+ ? lease.module.replaceComponent(lease.clientID, operationID, identityToken, encodedDescriptor)
308
+ : method === "revokeComponent"
309
+ ? lease.module.revokeComponent(lease.clientID, operationID, identityToken, encodedDescriptor)
310
+ .then(() => "")
311
+ : lease.module.revokeFamilyWithComponents(lease.clientID, operationID, identityToken, encodedDescriptor)
312
+ .then(() => "");
313
+ return abortable(operation, signal, () => { lease.module.cancel(lease.clientID, operationID); });
314
+ }
315
+ createRequest(input, init) {
316
+ if (input instanceof Request && Reflect.ownKeys(init).length === 0)
317
+ return input;
318
+ if (input instanceof Request)
319
+ return new Request(input, init);
320
+ const resolved = input instanceof URL ? input : new URL(input, this.config.baseURL);
321
+ return new Request(resolved, init);
322
+ }
323
+ assertGatewayTarget(input, method, feature) {
324
+ const target = new URL(input);
325
+ const pathname = policyPathname(input, target);
326
+ if (target.origin !== this.config.baseURL.origin || target.hash !== "") {
327
+ throw new LatchwayError("client_configuration_invalid", "Latchway only dispatches requests to the configured gateway origin.");
328
+ }
329
+ if (!isAllowedDataPlaneTarget(target, pathname, method, feature)) {
330
+ throw new LatchwayError("transport_destination_not_allowed", "Latchway only authorizes methods and paths declared by the client contract.");
331
+ }
332
+ target.searchParams.forEach((_value, name) => {
333
+ if (isForbiddenCredentialName(decodedCredentialName(name))) {
334
+ throw new LatchwayError("request_invalid", "Upstream provider credentials must not be supplied in the request URL.");
335
+ }
336
+ });
337
+ }
338
+ assertActive() {
339
+ if (this.disposed) {
340
+ throw new LatchwayError("client_configuration_invalid", "This Latchway client has been disposed.");
341
+ }
342
+ }
343
+ }
344
+ function parsePreparedComponents(encoded, expected) {
345
+ let value;
346
+ try {
347
+ value = JSON.parse(encoded);
348
+ }
349
+ catch {
350
+ throw invalidPreparedComponents();
351
+ }
352
+ if (typeof value !== "object" || value === null || Array.isArray(value) ||
353
+ Reflect.ownKeys(value).length !== 1 || !("components" in value) ||
354
+ !Array.isArray(value.components) || value.components.length !== expected.length) {
355
+ throw invalidPreparedComponents();
356
+ }
357
+ try {
358
+ return value.components.map((diagnostics, index) => {
359
+ const component = expected[index];
360
+ if (component === undefined)
361
+ throw invalidPreparedComponents();
362
+ return parseComponentDiagnostics(JSON.stringify(diagnostics), component);
363
+ });
364
+ }
365
+ catch (cause) {
366
+ if (cause instanceof LatchwayError)
367
+ throw cause;
368
+ throw invalidPreparedComponents();
369
+ }
370
+ }
371
+ function invalidPreparedComponents() {
372
+ return new LatchwayError("protocol_response_invalid", "Latchway native component output was invalid.");
373
+ }
374
+ /**
375
+ * Provider SDKs conventionally read `X-Request-ID`. Preserve Latchway's
376
+ * canonical header and add the alias without consuming or buffering the body.
377
+ */
378
+ function aliasFrameworkRequestID(response) {
379
+ const requestID = response.headers.get("X-Latchway-Request-ID");
380
+ if (requestID === null || response.headers.get("X-Request-ID") === requestID)
381
+ return response;
382
+ response.headers.set("X-Request-ID", requestID);
383
+ return response;
384
+ }
385
+ function responseWithNativeBody(body, init) {
386
+ let response;
387
+ try {
388
+ response = new Response(body, init);
389
+ }
390
+ catch {
391
+ // Some React Native Response implementations reject ponyfill streams even
392
+ // though the instance body is attached below and consumed directly.
393
+ response = new Response(null, init);
394
+ }
395
+ if (body === null)
396
+ return response;
397
+ const exposed = response.body;
398
+ if (exposed !== undefined && exposed !== null && typeof exposed.getReader === "function") {
399
+ return response;
400
+ }
401
+ // React Native 0.82's built-in Response accepts the stream but does not
402
+ // expose it through `body`. Restore the exact native-owned pull stream on
403
+ // the instance; no bytes are buffered and the native cancel/close lifecycle
404
+ // remains authoritative.
405
+ Object.defineProperty(response, "body", {
406
+ configurable: true,
407
+ enumerable: true,
408
+ value: body,
409
+ });
410
+ return response;
411
+ }
412
+ function policyPathname(input, target) {
413
+ // React Native's built-in URL polyfill appends `/` when an absolute URL has
414
+ // no query or fragment. Request.url is already serialized at this point, so
415
+ // recognize only that exact mutation instead of broadening the route set to
416
+ // accept genuinely trailing-slash destinations.
417
+ if (target.pathname.length > 1 && !input.includes("?") && !input.includes("#") &&
418
+ !input.endsWith("/") && target.href === `${input}/`) {
419
+ return target.pathname.slice(0, -1);
420
+ }
421
+ return target.pathname;
422
+ }
423
+ function isAllowedDataPlaneTarget(target, pathname, method, feature) {
424
+ const normalizedMethod = method.toUpperCase();
425
+ if (normalizedMethod === "POST" && allowedDataPlanePaths.has(pathname))
426
+ return true;
427
+ const prefix = `/proxy/${encodeURIComponent(feature)}/`;
428
+ if (!opaqueDataPlaneMethods.has(normalizedMethod) || target.search !== "" ||
429
+ !pathname.startsWith(prefix))
430
+ return false;
431
+ const remaining = pathname.slice(prefix.length);
432
+ const lowerRemaining = remaining.toLowerCase();
433
+ return remaining.length >= 1 && remaining.length <= 2_048 &&
434
+ remaining.split("/").every((segment) => segment !== "" && segment !== "." && segment !== "..") &&
435
+ !lowerRemaining.includes("%2e") && !lowerRemaining.includes("%2f") &&
436
+ !lowerRemaining.includes("%5c") && !remaining.includes("\\") &&
437
+ !lowerRemaining.startsWith("http:") && !lowerRemaining.startsWith("https:");
438
+ }
439
+ function sanitizedRequestHeaders(source) {
440
+ const result = [];
441
+ let size = 0;
442
+ source.forEach((value, name) => {
443
+ const normalized = name.toLowerCase();
444
+ if (nativeControlledHeaders.has(normalized))
445
+ return;
446
+ if (isForbiddenCredentialName(normalized))
447
+ return;
448
+ if (!isHeaderName(normalized) || !isHeaderValue(value)) {
449
+ throw new LatchwayError("request_invalid", "The request contains an invalid header.");
450
+ }
451
+ size += normalized.length + value.length;
452
+ if (result.length >= MAXIMUM_HEADERS || size > MAXIMUM_HEADER_BYTES) {
453
+ throw new LatchwayError("request_invalid", "The request headers exceed the native bridge limit.");
454
+ }
455
+ result.push([normalized, value]);
456
+ });
457
+ return result;
458
+ }
459
+ function requestBodyExpected(input, init) {
460
+ if (Object.prototype.hasOwnProperty.call(init, "body"))
461
+ return init.body !== null && init.body !== undefined;
462
+ if (!(input instanceof Request))
463
+ return false;
464
+ const body = input.body;
465
+ if (body !== null && body !== undefined)
466
+ return true;
467
+ return input._bodyInit !== null &&
468
+ input._bodyInit !== undefined;
469
+ }
470
+ async function encodedRequestBody(request, signal, bodyExpected) {
471
+ const body = request.body;
472
+ if (body === null || (body === undefined && !bodyExpected))
473
+ return null;
474
+ if (body === undefined || typeof body.getReader !== "function") {
475
+ try {
476
+ const encoded = await abortable(request.arrayBuffer(), signal, () => { });
477
+ const bytes = new Uint8Array(encoded);
478
+ if (bytes.byteLength > MAXIMUM_REQUEST_BODY_BYTES) {
479
+ throw new LatchwayError("request_invalid", "The request body exceeds the 8 MiB native transport limit.");
480
+ }
481
+ return bytesToBase64(bytes);
482
+ }
483
+ catch (cause) {
484
+ if (cause instanceof LatchwayError || isAbort(cause))
485
+ throw cause;
486
+ throw new LatchwayError("request_not_replayable", "The request body could not be read for native dispatch.", { cause });
487
+ }
488
+ }
489
+ const reader = body.getReader();
490
+ const chunks = [];
491
+ let size = 0;
492
+ try {
493
+ for (;;) {
494
+ const result = await readWithAbort(reader, signal);
495
+ if (result.done)
496
+ break;
497
+ size += result.value.byteLength;
498
+ if (size > MAXIMUM_REQUEST_BODY_BYTES) {
499
+ await reader.cancel("Latchway request body limit exceeded");
500
+ throw new LatchwayError("request_invalid", "The request body exceeds the 8 MiB native transport limit.");
501
+ }
502
+ chunks.push(result.value);
503
+ }
504
+ }
505
+ catch (cause) {
506
+ if (cause instanceof LatchwayError || isAbort(cause))
507
+ throw cause;
508
+ throw new LatchwayError("request_not_replayable", "The request body could not be read for native dispatch.", { cause });
509
+ }
510
+ finally {
511
+ reader.releaseLock();
512
+ }
513
+ const bytes = new Uint8Array(size);
514
+ let offset = 0;
515
+ for (const chunk of chunks) {
516
+ bytes.set(chunk, offset);
517
+ offset += chunk.byteLength;
518
+ }
519
+ return bytesToBase64(bytes);
520
+ }
521
+ async function readWithAbort(reader, signal) {
522
+ if (signal.aborted) {
523
+ void reader.cancel();
524
+ throw abortError();
525
+ }
526
+ let listener;
527
+ const aborted = new Promise((_resolve, reject) => {
528
+ listener = () => {
529
+ void reader.cancel();
530
+ reject(abortError());
531
+ };
532
+ signal.addEventListener("abort", listener, { once: true });
533
+ });
534
+ try {
535
+ return await Promise.race([reader.read(), aborted]);
536
+ }
537
+ finally {
538
+ if (listener !== undefined)
539
+ signal.removeEventListener("abort", listener);
540
+ }
541
+ }
542
+ function nativeResponseBody(lease, responseID, signal) {
543
+ let finished = false;
544
+ let nativeClosed = false;
545
+ let activeOperationID;
546
+ let abortListener;
547
+ const closeNative = async () => {
548
+ if (nativeClosed)
549
+ return;
550
+ nativeClosed = true;
551
+ await ignoreFailure(lease.module.closeResponse(lease.clientID, responseID));
552
+ };
553
+ const finish = () => {
554
+ if (finished)
555
+ return;
556
+ finished = true;
557
+ if (abortListener !== undefined)
558
+ signal.removeEventListener("abort", abortListener);
559
+ };
560
+ const ReadableStreamConstructor = typeof globalThis.ReadableStream === "function"
561
+ ? globalThis.ReadableStream
562
+ : PonyfillReadableStream;
563
+ return new ReadableStreamConstructor({
564
+ start(controller) {
565
+ abortListener = () => {
566
+ if (finished)
567
+ return;
568
+ finish();
569
+ void closeNative();
570
+ controller.error(abortError());
571
+ };
572
+ signal.addEventListener("abort", abortListener, { once: true });
573
+ },
574
+ async pull(controller) {
575
+ if (finished)
576
+ return;
577
+ if (signal.aborted) {
578
+ finish();
579
+ await closeNative();
580
+ controller.error(abortError());
581
+ return;
582
+ }
583
+ const operationID = makeOperationID();
584
+ activeOperationID = operationID;
585
+ try {
586
+ const encoded = await abortable(lease.module.readResponseChunk(lease.clientID, operationID, responseID, MAXIMUM_RESPONSE_CHUNK_BYTES), signal, () => { lease.module.cancel(lease.clientID, operationID); });
587
+ const chunk = parseResponseChunk(encoded);
588
+ if (chunk.done) {
589
+ finish();
590
+ controller.close();
591
+ await closeNative();
592
+ }
593
+ else if (chunk.chunk !== undefined) {
594
+ controller.enqueue(chunk.chunk);
595
+ }
596
+ }
597
+ catch (cause) {
598
+ if (!finished) {
599
+ finish();
600
+ controller.error(isAbort(cause) ? abortError() : cause);
601
+ }
602
+ await closeNative();
603
+ }
604
+ finally {
605
+ if (activeOperationID === operationID)
606
+ activeOperationID = undefined;
607
+ }
608
+ },
609
+ async cancel() {
610
+ finish();
611
+ if (activeOperationID !== undefined)
612
+ lease.module.cancel(lease.clientID, activeOperationID);
613
+ await closeNative();
614
+ },
615
+ }, { highWaterMark: 0 });
616
+ }
617
+ function parseResponseMetadata(encoded) {
618
+ const value = parseRecord(encoded, "native response metadata");
619
+ assertNoCredentialFields(value);
620
+ if (!hasOnlyKeys(value, ["responseID", "status", "statusText", "headers"]) ||
621
+ typeof value.responseID !== "string" || !/^rsp_[A-Za-z0-9_-]{16,96}$/u.test(value.responseID) ||
622
+ !Number.isInteger(value.status) || value.status < 200 || value.status > 599 ||
623
+ typeof value.statusText !== "string" || value.statusText.length > 128 || /\p{Cc}/u.test(value.statusText) ||
624
+ !Array.isArray(value.headers) || value.headers.length > MAXIMUM_HEADERS) {
625
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned invalid native response metadata.");
626
+ }
627
+ const headers = [];
628
+ let headerBytes = 0;
629
+ for (const header of value.headers) {
630
+ if (!Array.isArray(header) || header.length !== 2 || typeof header[0] !== "string" ||
631
+ typeof header[1] !== "string") {
632
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned invalid native response headers.");
633
+ }
634
+ const name = header[0].toLowerCase();
635
+ const headerValue = header[1];
636
+ if (!isSafeResponseHeader(name) || !isHeaderValue(headerValue)) {
637
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned unsafe native response headers.");
638
+ }
639
+ headerBytes += name.length + headerValue.length;
640
+ if (headerBytes > MAXIMUM_HEADER_BYTES) {
641
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned oversized native response headers.");
642
+ }
643
+ headers.push([name, headerValue]);
644
+ }
645
+ return {
646
+ responseID: value.responseID,
647
+ status: value.status,
648
+ statusText: value.statusText,
649
+ headers,
650
+ };
651
+ }
652
+ function parseResponseChunk(encoded) {
653
+ if (encoded.length > MAXIMUM_RESPONSE_CHUNK_BYTES * 2) {
654
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned an oversized native response chunk.");
655
+ }
656
+ const value = parseRecord(encoded, "native response chunk");
657
+ assertNoCredentialFields(value);
658
+ if (!hasOnlyKeys(value, ["done", "chunk"]) || typeof value.done !== "boolean") {
659
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned an invalid native response chunk.");
660
+ }
661
+ if (value.done) {
662
+ if (value.chunk !== undefined && value.chunk !== null) {
663
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned an invalid final native response chunk.");
664
+ }
665
+ return { done: true };
666
+ }
667
+ if (typeof value.chunk !== "string") {
668
+ throw new LatchwayError("protocol_response_invalid", "Latchway omitted native response bytes.");
669
+ }
670
+ const chunk = base64ToBytes(value.chunk);
671
+ if (chunk.byteLength === 0 || chunk.byteLength > MAXIMUM_RESPONSE_CHUNK_BYTES) {
672
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned invalid native response bytes.");
673
+ }
674
+ return { done: false, chunk };
675
+ }
676
+ function parseQuota(encoded, expectedFeature) {
677
+ const value = parseRecord(encoded, "quota snapshot");
678
+ assertNoCredentialFields(value);
679
+ if (!hasOnlyKeys(value, ["feature", "observed_at", "limits"]) ||
680
+ value.feature !== expectedFeature || typeof value.observed_at !== "string" || !Array.isArray(value.limits)) {
681
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned an invalid quota snapshot.");
682
+ }
683
+ const limits = value.limits.map((item) => {
684
+ if (!isRecord(item) ||
685
+ !hasOnlyKeys(item, ["metric", "maximum", "used", "reserved", "remaining", "resets_at", "hard"]) ||
686
+ typeof item.metric !== "string" || typeof item.hard !== "boolean") {
687
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned an invalid quota limit.");
688
+ }
689
+ for (const field of ["maximum", "used", "reserved", "remaining"]) {
690
+ const counter = item[field];
691
+ if (counter !== undefined && (!Number.isSafeInteger(counter) || counter < 0)) {
692
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned an invalid quota counter.");
693
+ }
694
+ }
695
+ if (item.resets_at !== undefined && typeof item.resets_at !== "string") {
696
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned an invalid quota reset time.");
697
+ }
698
+ return item;
699
+ });
700
+ return { feature: expectedFeature, observed_at: value.observed_at, limits };
701
+ }
702
+ function parseDiagnostics(encoded, platform, nativeSDKVersion) {
703
+ const value = parseRecord(encoded, "diagnostics");
704
+ assertNoCredentialFields(value);
705
+ if (!hasOnlyKeys(value, [
706
+ "contractVersion", "protocolVersion", "keyStorage", "attestation", "session", "installation", "server",
707
+ "lastErrorCode",
708
+ ]) || value.contractVersion !== CONTRACT_VERSION || value.protocolVersion !== PROTOCOL_VERSION ||
709
+ typeof value.keyStorage !== "string" || !isRecord(value.attestation) || !isRecord(value.session) ||
710
+ !isRecord(value.installation) || !isRecord(value.server)) {
711
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned invalid native diagnostics.");
712
+ }
713
+ if (!hasOnlyKeys(value.attestation, ["support", "provider", "trustLevel", "lastOperation"]) ||
714
+ !hasOnlyKeys(value.session, ["state", "expiresAt", "refreshAvailable"]) ||
715
+ !hasOnlyKeys(value.installation, ["id", "status"]) ||
716
+ !hasOnlyKeys(value.server, ["version", "lastRequestID"])) {
717
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned unexpected native diagnostics.");
718
+ }
719
+ const support = value.attestation.support;
720
+ const state = value.session.state;
721
+ if ((support !== "supported" && support !== "unsupported" && support !== "unknown") ||
722
+ typeof state !== "string" ||
723
+ !new Set(["absent", "establishing", "active", "refreshing", "expired", "revoked", "failed"]).has(state)) {
724
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned invalid native diagnostic state.");
725
+ }
726
+ const provider = optionalString(value.attestation.provider);
727
+ const trustLevel = optionalString(value.attestation.trustLevel);
728
+ const lastOperation = optionalString(value.attestation.lastOperation);
729
+ const expiresAt = optionalString(value.session.expiresAt);
730
+ const installationID = optionalString(value.installation.id);
731
+ const installationStatus = optionalString(value.installation.status);
732
+ const serverVersion = optionalString(value.server.version);
733
+ const lastRequestID = optionalString(value.server.lastRequestID);
734
+ const lastErrorCode = optionalString(value.lastErrorCode);
735
+ return {
736
+ sdkVersion: SDK_VERSION,
737
+ nativeSDKVersion,
738
+ contractVersion: CONTRACT_VERSION,
739
+ protocolVersion: PROTOCOL_VERSION,
740
+ platform,
741
+ keyStorage: value.keyStorage,
742
+ attestation: {
743
+ support,
744
+ ...(provider === undefined ? {} : { provider }),
745
+ ...(trustLevel === undefined ? {} : { trustLevel }),
746
+ ...(lastOperation === undefined ? {} : { lastOperation }),
747
+ },
748
+ session: {
749
+ state: state,
750
+ ...(expiresAt === undefined ? {} : { expiresAt }),
751
+ ...(typeof value.session.refreshAvailable === "boolean" ? { refreshAvailable: value.session.refreshAvailable } : {}),
752
+ },
753
+ installation: {
754
+ ...(installationID === undefined ? {} : { id: installationID }),
755
+ ...(installationStatus === undefined ? {} : { status: installationStatus }),
756
+ },
757
+ server: {
758
+ ...(serverVersion === undefined ? {} : { version: serverVersion }),
759
+ ...(lastRequestID === undefined ? {} : { lastRequestID }),
760
+ },
761
+ ...(lastErrorCode === undefined ? {} : { lastErrorCode }),
762
+ };
763
+ }
764
+ async function token(provider, signal) {
765
+ let result;
766
+ try {
767
+ result = await abortable(Promise.resolve().then(provider), signal);
768
+ }
769
+ catch (cause) {
770
+ if (isAbort(cause))
771
+ throw cause;
772
+ throw new LatchwayError("identity_token_invalid", "The identity token provider failed.", { cause });
773
+ }
774
+ if (typeof result !== "string" || result.length === 0 || result.length > 65_536 || /\p{Cc}/u.test(result)) {
775
+ throw new LatchwayError("identity_token_invalid", "The identity token provider returned an invalid token.");
776
+ }
777
+ return result;
778
+ }
779
+ async function abortable(operation, signal, cancel) {
780
+ if (signal === undefined) {
781
+ try {
782
+ return await operation;
783
+ }
784
+ catch (cause) {
785
+ throw fromNativeError(cause);
786
+ }
787
+ }
788
+ if (signal.aborted) {
789
+ cancel?.();
790
+ throw abortError();
791
+ }
792
+ let listener;
793
+ const aborted = new Promise((_resolve, reject) => {
794
+ listener = () => {
795
+ cancel?.();
796
+ reject(abortError());
797
+ };
798
+ signal.addEventListener("abort", listener, { once: true });
799
+ });
800
+ try {
801
+ return await Promise.race([operation.catch((cause) => { throw fromNativeError(cause); }), aborted]);
802
+ }
803
+ finally {
804
+ if (listener !== undefined)
805
+ signal.removeEventListener("abort", listener);
806
+ }
807
+ }
808
+ function makeOperationID() {
809
+ return `op-${nextOperationID++}`;
810
+ }
811
+ function assertFeature(value) {
812
+ if (value === undefined || !validIdentifier(value)) {
813
+ throw new LatchwayError("client_configuration_invalid", "A valid latchwayFeature is required.");
814
+ }
815
+ }
816
+ function validIdentifier(value) {
817
+ return /^[a-z][a-z0-9_-]{0,62}$/u.test(value);
818
+ }
819
+ function parseRecord(encoded, label) {
820
+ try {
821
+ const value = JSON.parse(encoded);
822
+ if (isRecord(value))
823
+ return value;
824
+ }
825
+ catch {
826
+ // Native output is intentionally not reflected into the safe error.
827
+ }
828
+ throw new LatchwayError("protocol_response_invalid", `Latchway returned invalid ${label}.`);
829
+ }
830
+ function hasOnlyKeys(value, names) {
831
+ const expected = new Set(names);
832
+ return Object.keys(value).every((name) => expected.has(name));
833
+ }
834
+ function isSafeResponseHeader(name) {
835
+ return safeResponseHeaderNames.has(name) || name.startsWith("x-ratelimit-") || name.startsWith("ratelimit-");
836
+ }
837
+ function recoverResponseID(encoded) {
838
+ if (encoded.length > 256 * 1024)
839
+ return undefined;
840
+ try {
841
+ const value = JSON.parse(encoded);
842
+ if (isRecord(value) && typeof value.responseID === "string" && /^rsp_[A-Za-z0-9_-]{16,96}$/u.test(value.responseID)) {
843
+ return value.responseID;
844
+ }
845
+ }
846
+ catch {
847
+ // Invalid output has no safely recoverable response handle.
848
+ }
849
+ return undefined;
850
+ }
851
+ function isHeaderName(value) {
852
+ return /^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$/u.test(value);
853
+ }
854
+ function isHeaderValue(value) {
855
+ if (value.length > 8_192)
856
+ return false;
857
+ for (const character of value) {
858
+ const scalar = character.codePointAt(0) ?? 0;
859
+ if (scalar <= 0x08 || (scalar >= 0x0a && scalar <= 0x1f) || scalar === 0x7f)
860
+ return false;
861
+ }
862
+ return true;
863
+ }
864
+ function decodedCredentialName(value) {
865
+ let decoded = value;
866
+ for (let attempt = 0; attempt < 4; attempt += 1) {
867
+ try {
868
+ const next = decodeURIComponent(decoded);
869
+ if (next === decoded)
870
+ break;
871
+ decoded = next;
872
+ }
873
+ catch {
874
+ break;
875
+ }
876
+ }
877
+ // Fail closed on deeper nested escapes instead of letting an attacker move a
878
+ // credential name past the bounded decoder with another layer of `%25`.
879
+ if (/%[0-9A-Fa-f]{2}/u.test(decoded))
880
+ return "credential-encoded-name";
881
+ return decoded.toLowerCase();
882
+ }
883
+ function isForbiddenCredentialName(value) {
884
+ const normalized = value.toLowerCase();
885
+ if (forbiddenCredentialHeaders.has(normalized) || forbiddenCredentialQueryNames.has(normalized))
886
+ return true;
887
+ const compact = normalized.replace(/[^a-z0-9]/gu, "");
888
+ return forbiddenExactCredentialNames.has(compact) ||
889
+ forbiddenCredentialNameFragments.some((fragment) => compact.includes(fragment));
890
+ }
891
+ function bytesToBase64(bytes) {
892
+ let binary = "";
893
+ for (let offset = 0; offset < bytes.length; offset += 16_384) {
894
+ const end = Math.min(offset + 16_384, bytes.length);
895
+ for (let index = offset; index < end; index += 1)
896
+ binary += String.fromCharCode(bytes[index] ?? 0);
897
+ }
898
+ return btoa(binary);
899
+ }
900
+ function base64ToBytes(encoded) {
901
+ if (encoded.length === 0 || encoded.length % 4 !== 0 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(encoded)) {
902
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned malformed native response bytes.");
903
+ }
904
+ let binary;
905
+ try {
906
+ binary = atob(encoded);
907
+ }
908
+ catch {
909
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned malformed native response bytes.");
910
+ }
911
+ const bytes = new Uint8Array(binary.length);
912
+ for (let index = 0; index < binary.length; index += 1)
913
+ bytes[index] = binary.charCodeAt(index);
914
+ if (bytesToBase64(bytes) !== encoded) {
915
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned non-canonical native response bytes.");
916
+ }
917
+ return bytes;
918
+ }
919
+ function optionalString(value) {
920
+ if (value === undefined || value === null)
921
+ return undefined;
922
+ if (typeof value !== "string" || value.length === 0 || value.length > 512 || /\p{Cc}/u.test(value)) {
923
+ throw new LatchwayError("protocol_response_invalid", "Latchway returned an invalid native diagnostic value.");
924
+ }
925
+ return value;
926
+ }
927
+ function isRecord(value) {
928
+ return typeof value === "object" && value !== null && !Array.isArray(value);
929
+ }
930
+ function isAbort(value) {
931
+ return value instanceof Error && value.name === "AbortError";
932
+ }
933
+ async function ignoreFailure(operation) {
934
+ try {
935
+ await operation;
936
+ }
937
+ catch { /* cleanup is idempotent and best-effort */ }
938
+ }
939
+ //# sourceMappingURL=client.js.map