@layervai/qurl 0.7.0 → 3.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 (121) hide show
  1. package/README.md +242 -2
  2. package/dist/cjs/client.d.ts +41 -9
  3. package/dist/cjs/client.js +734 -41
  4. package/dist/cjs/crid.d.ts +2 -1
  5. package/dist/cjs/crid.js +11 -2
  6. package/dist/cjs/errors.d.ts +20 -3
  7. package/dist/cjs/errors.js +31 -4
  8. package/dist/cjs/index.d.ts +2 -2
  9. package/dist/cjs/index.js +3 -1
  10. package/dist/cjs/node/agent-runtime.d.ts +108 -0
  11. package/dist/cjs/node/agent-runtime.js +952 -0
  12. package/dist/cjs/node/agent-state.d.ts +100 -0
  13. package/dist/cjs/node/agent-state.js +311 -0
  14. package/dist/cjs/node/agent-transport.d.ts +17 -0
  15. package/dist/cjs/node/agent-transport.js +118 -0
  16. package/dist/cjs/node/deployment.d.ts +6 -0
  17. package/dist/cjs/node/deployment.js +24 -3
  18. package/dist/cjs/node/file-agent-state.d.ts +15 -0
  19. package/dist/cjs/node/file-agent-state.js +137 -0
  20. package/dist/cjs/node/native-loader.cjs +7 -0
  21. package/dist/cjs/node/native-loader.d.cts +11 -0
  22. package/dist/cjs/node/native-udp.d.ts +8 -3
  23. package/dist/cjs/node/native-udp.js +8 -3
  24. package/dist/cjs/node/nhp-wire.d.ts +9 -0
  25. package/dist/cjs/node/nhp-wire.js +24 -7
  26. package/dist/cjs/node/portal-opener.d.ts +7 -0
  27. package/dist/cjs/node/portal-opener.js +28 -6
  28. package/dist/cjs/node/provider.d.ts +19 -0
  29. package/dist/cjs/node/provider.js +153 -0
  30. package/dist/cjs/node/qv2.d.ts +3 -1
  31. package/dist/cjs/node/qv2.js +16 -2
  32. package/dist/cjs/node/relay.d.ts +12 -0
  33. package/dist/cjs/node/relay.js +111 -0
  34. package/dist/cjs/node/sealed-agent-state.d.ts +19 -0
  35. package/dist/cjs/node/sealed-agent-state.js +193 -0
  36. package/dist/cjs/node/strict-json.d.ts +1 -1
  37. package/dist/cjs/node/strict-json.js +8 -3
  38. package/dist/cjs/node.d.ts +14 -0
  39. package/dist/cjs/node.js +24 -1
  40. package/dist/cjs/types.d.ts +161 -8
  41. package/dist/cjs/version.d.ts +1 -1
  42. package/dist/cjs/version.js +1 -1
  43. package/dist/esm/client.d.ts +41 -9
  44. package/dist/esm/client.d.ts.map +1 -1
  45. package/dist/esm/client.js +735 -42
  46. package/dist/esm/client.js.map +1 -1
  47. package/dist/esm/crid.d.ts +2 -1
  48. package/dist/esm/crid.d.ts.map +1 -1
  49. package/dist/esm/crid.js +10 -2
  50. package/dist/esm/crid.js.map +1 -1
  51. package/dist/esm/errors.d.ts +20 -3
  52. package/dist/esm/errors.d.ts.map +1 -1
  53. package/dist/esm/errors.js +29 -3
  54. package/dist/esm/errors.js.map +1 -1
  55. package/dist/esm/index.d.ts +2 -2
  56. package/dist/esm/index.d.ts.map +1 -1
  57. package/dist/esm/index.js +1 -1
  58. package/dist/esm/index.js.map +1 -1
  59. package/dist/esm/node/agent-runtime.d.ts +109 -0
  60. package/dist/esm/node/agent-runtime.d.ts.map +1 -0
  61. package/dist/esm/node/agent-runtime.js +946 -0
  62. package/dist/esm/node/agent-runtime.js.map +1 -0
  63. package/dist/esm/node/agent-state.d.ts +101 -0
  64. package/dist/esm/node/agent-state.d.ts.map +1 -0
  65. package/dist/esm/node/agent-state.js +298 -0
  66. package/dist/esm/node/agent-state.js.map +1 -0
  67. package/dist/esm/node/agent-transport.d.ts +18 -0
  68. package/dist/esm/node/agent-transport.d.ts.map +1 -0
  69. package/dist/esm/node/agent-transport.js +114 -0
  70. package/dist/esm/node/agent-transport.js.map +1 -0
  71. package/dist/esm/node/deployment.d.ts +6 -0
  72. package/dist/esm/node/deployment.d.ts.map +1 -1
  73. package/dist/esm/node/deployment.js +23 -3
  74. package/dist/esm/node/deployment.js.map +1 -1
  75. package/dist/esm/node/file-agent-state.d.ts +16 -0
  76. package/dist/esm/node/file-agent-state.d.ts.map +1 -0
  77. package/dist/esm/node/file-agent-state.js +134 -0
  78. package/dist/esm/node/file-agent-state.js.map +1 -0
  79. package/dist/esm/node/native-loader.cjs +7 -0
  80. package/dist/esm/node/native-loader.d.cts +11 -0
  81. package/dist/esm/node/native-udp.d.ts +8 -3
  82. package/dist/esm/node/native-udp.d.ts.map +1 -1
  83. package/dist/esm/node/native-udp.js +7 -5
  84. package/dist/esm/node/native-udp.js.map +1 -1
  85. package/dist/esm/node/nhp-wire.d.ts +9 -0
  86. package/dist/esm/node/nhp-wire.d.ts.map +1 -1
  87. package/dist/esm/node/nhp-wire.js +23 -6
  88. package/dist/esm/node/nhp-wire.js.map +1 -1
  89. package/dist/esm/node/portal-opener.d.ts +7 -0
  90. package/dist/esm/node/portal-opener.d.ts.map +1 -1
  91. package/dist/esm/node/portal-opener.js +28 -6
  92. package/dist/esm/node/portal-opener.js.map +1 -1
  93. package/dist/esm/node/provider.d.ts +20 -0
  94. package/dist/esm/node/provider.d.ts.map +1 -0
  95. package/dist/esm/node/provider.js +149 -0
  96. package/dist/esm/node/provider.js.map +1 -0
  97. package/dist/esm/node/qv2.d.ts +3 -1
  98. package/dist/esm/node/qv2.d.ts.map +1 -1
  99. package/dist/esm/node/qv2.js +17 -3
  100. package/dist/esm/node/qv2.js.map +1 -1
  101. package/dist/esm/node/relay.d.ts +13 -0
  102. package/dist/esm/node/relay.d.ts.map +1 -0
  103. package/dist/esm/node/relay.js +105 -0
  104. package/dist/esm/node/relay.js.map +1 -0
  105. package/dist/esm/node/sealed-agent-state.d.ts +20 -0
  106. package/dist/esm/node/sealed-agent-state.d.ts.map +1 -0
  107. package/dist/esm/node/sealed-agent-state.js +190 -0
  108. package/dist/esm/node/sealed-agent-state.js.map +1 -0
  109. package/dist/esm/node/strict-json.d.ts +1 -1
  110. package/dist/esm/node/strict-json.d.ts.map +1 -1
  111. package/dist/esm/node/strict-json.js +8 -3
  112. package/dist/esm/node/strict-json.js.map +1 -1
  113. package/dist/esm/node.d.ts +14 -0
  114. package/dist/esm/node.d.ts.map +1 -1
  115. package/dist/esm/node.js +7 -0
  116. package/dist/esm/node.js.map +1 -1
  117. package/dist/esm/types.d.ts +161 -8
  118. package/dist/esm/types.d.ts.map +1 -1
  119. package/dist/esm/version.d.ts +1 -1
  120. package/dist/esm/version.js +1 -1
  121. package/package.json +16 -6
@@ -1,5 +1,5 @@
1
1
  import { parseCrid, cridKeyMatches } from "./crid.js";
2
- import { ConnectorResourceOutcomeUnknownError, createError, ERROR_CODE_AMBIGUOUS_RESOURCE, ERROR_CODE_CLIENT_VALIDATION, ERROR_CODE_RESOURCE_NOT_FOUND, ERROR_CODE_CONNECTOR_RESOURCE_REVOKED, ERROR_CODE_UNEXPECTED_RESPONSE, ERROR_CODE_UNKNOWN, NetworkError, NotFoundError, QURLError, RuntimeError, TimeoutError, ValidationError, } from "./errors.js";
2
+ import { ConnectorResourceOutcomeUnknownError, createError, DelegatedBatchOutcomeUnknownError, ERROR_CODE_AMBIGUOUS_RESOURCE, ERROR_CODE_CLIENT_VALIDATION, ERROR_CODE_RESOURCE_NOT_FOUND, ERROR_CODE_CONNECTOR_RESOURCE_REVOKED, ERROR_CODE_UNEXPECTED_RESPONSE, ERROR_CODE_UNKNOWN, NetworkError, NotFoundError, QURLError, RuntimeError, TimeoutError, ValidationError, } from "./errors.js";
3
3
  import { stripTrailingSlashes } from "./internal.js";
4
4
  import { ShareLink } from "./share.js";
5
5
  import { VERSION } from "./version.js";
@@ -73,6 +73,7 @@ class ResponseBodyMaterializationError extends Error {
73
73
  }
74
74
  const NO_PASSTHROUGH_STATUSES = [];
75
75
  const BATCH_PASSTHROUGH_STATUSES = [400];
76
+ const DELEGATED_BATCH_PASSTHROUGH_STATUSES = [304];
76
77
  /** Allowlist of known query-param keys for the `list()` endpoint. */
77
78
  const LIST_PARAM_KEYS = [
78
79
  "limit",
@@ -86,6 +87,24 @@ const LIST_PARAM_KEYS = [
86
87
  "expires_after",
87
88
  ];
88
89
  const REQUEST_OPTION_KEYS = ["idempotencyKey"];
90
+ const DELEGATED_BATCH_INPUT_KEYS = [
91
+ "mint_capability",
92
+ "grants",
93
+ ];
94
+ const DELEGATED_BATCH_GRANT_KEYS = [
95
+ "expires_in",
96
+ "label",
97
+ "one_time_use",
98
+ "max_sessions",
99
+ "session_duration",
100
+ "access_policy",
101
+ ];
102
+ const DELEGATED_BATCH_CREATE_OPTION_KEYS = [
103
+ "idempotencyKey",
104
+ ];
105
+ const DELEGATED_BATCH_GET_OPTION_KEYS = [
106
+ "etag",
107
+ ];
89
108
  // Compile-time witness: `Exclude<keyof X, (typeof KEYS)[number]>` is
90
109
  // `never` iff KEYS lists every key of X. Paired with the
91
110
  // `satisfies readonly (keyof X)[]` clause on the array (which catches
@@ -96,6 +115,10 @@ function assertExhaustive(_) {
96
115
  }
97
116
  assertExhaustive(true);
98
117
  assertExhaustive(true);
118
+ assertExhaustive(true);
119
+ assertExhaustive(true);
120
+ assertExhaustive(true);
121
+ assertExhaustive(true);
99
122
  const CREATE_FIELD_KEYS = [
100
123
  "type",
101
124
  "target_url",
@@ -148,6 +171,8 @@ const RESOURCE_LIST_PARAM_KEYS = [
148
171
  ];
149
172
  assertExhaustive(true);
150
173
  const CREATE_RESOURCE_FIELD_KEYS = [
174
+ "private",
175
+ "consumer_access",
151
176
  "type",
152
177
  "target_url",
153
178
  "description",
@@ -159,6 +184,8 @@ const CREATE_RESOURCE_FIELD_KEYS = [
159
184
  ];
160
185
  assertExhaustive(true);
161
186
  const UPDATE_RESOURCE_FIELD_KEYS = [
187
+ "consumer_access",
188
+ "expected_consumer_access_version",
162
189
  "description",
163
190
  "tags",
164
191
  "custom_domain",
@@ -187,6 +214,8 @@ assertExhaustive(true);
187
214
  * fields the REST-shaped methods send.
188
215
  */
189
216
  const PROTECT_URL_OPTION_KEYS = [
217
+ "private",
218
+ "consumerAccess",
190
219
  "description",
191
220
  "tags",
192
221
  "customDomain",
@@ -763,6 +792,14 @@ const MAX_TARGET_PATH = 2048;
763
792
  const MAX_TAGS = 10;
764
793
  const MAX_TAG_LENGTH = 50;
765
794
  const MAX_AUTO_PAGINATION_PAGES = 10_000;
795
+ const MIN_DELEGATED_BATCH_IDEMPOTENCY_KEY = 32;
796
+ const MAX_DELEGATED_MINT_CAPABILITY = 8192;
797
+ const MAX_DELEGATED_ETAG = 96;
798
+ const MAX_DELEGATED_BATCH_ITEMS = 100;
799
+ const DELEGATED_BATCH_ID_PATTERN = /^dqb_[A-Za-z0-9_-]{22}$/;
800
+ const DELEGATED_QURL_ID_PATTERN = /^q_[0-9a-f]{11}$/;
801
+ const STRONG_ETAG_PATTERN = /^"[\x21\x23-\x7e]+"$/;
802
+ const UTC_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
766
803
  // Keep these identity contracts aligned with qurl-go and the public API.
767
804
  const CONNECTOR_SLUG_PATTERN = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
768
805
  // Current service schemas intentionally share this grammar, but keep the
@@ -1248,6 +1285,289 @@ function requireValidAccessPolicy(policy) {
1248
1285
  }
1249
1286
  }
1250
1287
  }
1288
+ function validateCreateDelegatedQurlBatchInput(input) {
1289
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
1290
+ throw clientValidationError(`createDelegatedQurlBatch: input must be an object (got ${describeShape(input)})`);
1291
+ }
1292
+ const typed = input;
1293
+ requireNoUnknownFields(typed, DELEGATED_BATCH_INPUT_KEYS, "createDelegatedQurlBatch");
1294
+ if (typeof typed.mint_capability !== "string" ||
1295
+ typed.mint_capability.length < 1 ||
1296
+ typed.mint_capability.length > MAX_DELEGATED_MINT_CAPABILITY) {
1297
+ throw clientValidationError(`createDelegatedQurlBatch: mint_capability must be a string of 1-${MAX_DELEGATED_MINT_CAPABILITY} characters`);
1298
+ }
1299
+ if (!Array.isArray(typed.grants) ||
1300
+ typed.grants.length < 1 ||
1301
+ typed.grants.length > MAX_DELEGATED_BATCH_ITEMS) {
1302
+ throw clientValidationError(`createDelegatedQurlBatch: grants must contain 1-${MAX_DELEGATED_BATCH_ITEMS} items`);
1303
+ }
1304
+ for (const [index, grant] of typed.grants.entries()) {
1305
+ const field = `createDelegatedQurlBatch: grants[${index}]`;
1306
+ if (typeof grant !== "object" || grant === null || Array.isArray(grant)) {
1307
+ throw clientValidationError(`${field}: must be an object (got ${describeShape(grant)})`);
1308
+ }
1309
+ requireNoUnknownFields(grant, DELEGATED_BATCH_GRANT_KEYS, field);
1310
+ requireMaxUtf8Bytes(grant.label, `${field}.label`, MAX_LABEL);
1311
+ requireBooleanIfPresent(grant.one_time_use, `${field}.one_time_use`);
1312
+ requireMaxSessionsInRange(grant.max_sessions, `${field}.max_sessions`);
1313
+ for (const durationField of ["expires_in", "session_duration"]) {
1314
+ const value = grant[durationField];
1315
+ if (value !== undefined && (typeof value !== "string" || value.length === 0)) {
1316
+ throw clientValidationError(`${field}.${durationField}: must be a non-empty string`);
1317
+ }
1318
+ }
1319
+ if (grant.access_policy !== undefined) {
1320
+ requireValidAccessPolicy(grant.access_policy);
1321
+ }
1322
+ }
1323
+ }
1324
+ function validateDelegatedBatchCreateOptions(options) {
1325
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
1326
+ throw clientValidationError("createDelegatedQurlBatch: options are required");
1327
+ }
1328
+ requireNoUnknownFields(options, DELEGATED_BATCH_CREATE_OPTION_KEYS, "createDelegatedQurlBatch options");
1329
+ const key = options.idempotencyKey;
1330
+ if (typeof key !== "string" ||
1331
+ key.length < MIN_DELEGATED_BATCH_IDEMPOTENCY_KEY ||
1332
+ key.length > MAX_IDEMPOTENCY_KEY ||
1333
+ !/^[!-~]+$/.test(key)) {
1334
+ throw clientValidationError(`createDelegatedQurlBatch: idempotencyKey must contain ${MIN_DELEGATED_BATCH_IDEMPOTENCY_KEY}-${MAX_IDEMPOTENCY_KEY} visible ASCII characters`);
1335
+ }
1336
+ }
1337
+ function validateGetDelegatedQurlBatchOptions(options) {
1338
+ if (options === undefined)
1339
+ return;
1340
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
1341
+ throw clientValidationError(`getDelegatedQurlBatch: options must be an object (got ${describeShape(options)})`);
1342
+ }
1343
+ requireNoUnknownFields(options, DELEGATED_BATCH_GET_OPTION_KEYS, "getDelegatedQurlBatch options");
1344
+ const etag = options.etag;
1345
+ if (etag !== undefined &&
1346
+ (typeof etag !== "string" ||
1347
+ etag.length > MAX_DELEGATED_ETAG ||
1348
+ !STRONG_ETAG_PATTERN.test(etag))) {
1349
+ throw clientValidationError("getDelegatedQurlBatch: etag must be a valid strong ETag");
1350
+ }
1351
+ }
1352
+ function delegatedResponseError(status, detail, requestId) {
1353
+ return unexpectedResponseError(`Delegated qURL batch ${detail}`, { status, requestId });
1354
+ }
1355
+ function delegatedResponseObject(value, status, label, requestId) {
1356
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1357
+ throw delegatedResponseError(status, `${label} is not an object`, requestId);
1358
+ }
1359
+ return value;
1360
+ }
1361
+ function delegatedEnvelope(envelope, status) {
1362
+ if (typeof envelope.meta !== "object" || envelope.meta === null || Array.isArray(envelope.meta)) {
1363
+ throw delegatedResponseError(status, "response meta is not an object");
1364
+ }
1365
+ const meta = envelope.meta;
1366
+ if (typeof meta.request_id !== "string") {
1367
+ throw delegatedResponseError(status, "response meta has invalid request_id");
1368
+ }
1369
+ const data = delegatedResponseObject(envelope.data, status, "response data", meta.request_id);
1370
+ return { data, requestId: meta.request_id };
1371
+ }
1372
+ function requiredDelegatedHeader(envelope, name, status, requestId) {
1373
+ const headers = envelope.__http_headers;
1374
+ if (!headers || typeof headers.get !== "function") {
1375
+ throw delegatedResponseError(status, "response headers are unavailable", requestId);
1376
+ }
1377
+ const value = headers.get(name);
1378
+ if (value === null) {
1379
+ throw delegatedResponseError(status, `response is missing ${name}`, requestId);
1380
+ }
1381
+ return value;
1382
+ }
1383
+ function hasDelegatedNoStore(value) {
1384
+ if (value === null)
1385
+ return false;
1386
+ const directives = new Set(value.split(",").map((directive) => directive.trim().split("=", 1)[0].toLowerCase()));
1387
+ return directives.has("private") && directives.has("no-store");
1388
+ }
1389
+ function delegatedResponseHeaders(envelope, status, requestId) {
1390
+ const cacheControl = envelope.__http_headers?.get("Cache-Control") ?? null;
1391
+ if (cacheControl !== null) {
1392
+ if (!hasDelegatedNoStore(cacheControl)) {
1393
+ throw delegatedResponseError(status, "response has invalid Cache-Control", requestId);
1394
+ }
1395
+ }
1396
+ else if (status !== 304) {
1397
+ requiredDelegatedHeader(envelope, "Cache-Control", status, requestId);
1398
+ }
1399
+ const etag = envelope.__http_headers?.get("ETag") ?? undefined;
1400
+ if (etag !== undefined && (etag.length > MAX_DELEGATED_ETAG || !STRONG_ETAG_PATTERN.test(etag))) {
1401
+ throw delegatedResponseError(status, "response has invalid strong ETag", requestId);
1402
+ }
1403
+ const rawRetryAfter = envelope.__http_headers?.get("Retry-After") ?? "";
1404
+ const trimmed = rawRetryAfter.trim();
1405
+ const seconds = /^\d+$/.test(trimmed)
1406
+ ? Number(trimmed)
1407
+ : /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(trimmed)
1408
+ ? Math.ceil((Date.parse(trimmed) - Date.now()) / 1000)
1409
+ : NaN;
1410
+ const retryAfter = Number.isSafeInteger(seconds) && seconds > 0 && seconds <= RETRY_AFTER_PARSE_LIMIT_S
1411
+ ? seconds
1412
+ : undefined;
1413
+ return { etag, retryAfter };
1414
+ }
1415
+ function isUtcTimestamp(value) {
1416
+ return (typeof value === "string" &&
1417
+ UTC_TIMESTAMP_PATTERN.test(value) &&
1418
+ Number.isFinite(Date.parse(value)));
1419
+ }
1420
+ function delegatedBatchBase(data, status, requestId, expectedBatchId) {
1421
+ if (typeof data.batch_id !== "string" ||
1422
+ !DELEGATED_BATCH_ID_PATTERN.test(data.batch_id) ||
1423
+ (expectedBatchId !== undefined && data.batch_id !== expectedBatchId)) {
1424
+ throw delegatedResponseError(status, "response has invalid batch_id", requestId);
1425
+ }
1426
+ if (!Number.isInteger(data.item_count) ||
1427
+ Number(data.item_count) < 1 ||
1428
+ Number(data.item_count) > MAX_DELEGATED_BATCH_ITEMS) {
1429
+ throw delegatedResponseError(status, "response has invalid item_count", requestId);
1430
+ }
1431
+ if (!isUtcTimestamp(data.submitted_at)) {
1432
+ throw delegatedResponseError(status, "response has invalid submitted_at", requestId);
1433
+ }
1434
+ return {
1435
+ batchId: data.batch_id,
1436
+ itemCount: Number(data.item_count),
1437
+ submittedAt: data.submitted_at,
1438
+ };
1439
+ }
1440
+ function validateDelegatedLocation(value, baseUrl, batchId, status, requestId) {
1441
+ let location;
1442
+ try {
1443
+ const base = new URL(`${baseUrl}/`);
1444
+ const relative = value.startsWith("/v1/delegated-qurl-batches/")
1445
+ ? `${base.pathname.replace(/\/$/, "")}${value}`
1446
+ : value;
1447
+ location = new URL(relative, base);
1448
+ }
1449
+ catch {
1450
+ throw delegatedResponseError(status, "response has invalid Location", requestId);
1451
+ }
1452
+ const expected = new URL(`${baseUrl}/v1/delegated-qurl-batches/${batchId}`);
1453
+ if (location.username !== "" ||
1454
+ location.password !== "" ||
1455
+ location.search !== "" ||
1456
+ location.hash !== "" ||
1457
+ location.href !== expected.href) {
1458
+ throw delegatedResponseError(status, "response has invalid Location", requestId);
1459
+ }
1460
+ return location.href;
1461
+ }
1462
+ function validateDelegatedQurlLink(value) {
1463
+ if (typeof value !== "string")
1464
+ return false;
1465
+ try {
1466
+ const link = new URL(value);
1467
+ return (link.protocol === "https:" &&
1468
+ link.username === "" &&
1469
+ link.password === "" &&
1470
+ link.hash.length > 1);
1471
+ }
1472
+ catch {
1473
+ return false;
1474
+ }
1475
+ }
1476
+ function validateDelegatedQurl(value, expectedQurlId, status, requestId) {
1477
+ const data = delegatedResponseObject(value, status, "response data", requestId);
1478
+ if (data.qurl_id !== expectedQurlId) {
1479
+ throw delegatedResponseError(status, "response has invalid qurl_id", requestId);
1480
+ }
1481
+ if (data.status !== "active" &&
1482
+ data.status !== "consumed" &&
1483
+ data.status !== "expired" &&
1484
+ data.status !== "revoked") {
1485
+ throw delegatedResponseError(status, "response has invalid status", requestId);
1486
+ }
1487
+ if (!isUtcTimestamp(data.expires_at) ||
1488
+ typeof data.one_time_use !== "boolean" ||
1489
+ !Number.isInteger(data.max_sessions) ||
1490
+ Number(data.max_sessions) < 0 ||
1491
+ !Number.isInteger(data.session_duration) ||
1492
+ Number(data.session_duration) <= 0 ||
1493
+ (data.label !== undefined && typeof data.label !== "string") ||
1494
+ (data.access_policy !== undefined &&
1495
+ (typeof data.access_policy !== "object" ||
1496
+ data.access_policy === null ||
1497
+ Array.isArray(data.access_policy)))) {
1498
+ throw delegatedResponseError(status, "response has invalid qURL metadata", requestId);
1499
+ }
1500
+ return { ...data, request_id: requestId };
1501
+ }
1502
+ function validateDelegatedBatchResults(value, itemCount, batchStatus, status, requestId) {
1503
+ if (!Array.isArray(value) || value.length !== itemCount) {
1504
+ throw delegatedResponseError(status, "terminal response has invalid results", requestId);
1505
+ }
1506
+ let succeeded = 0;
1507
+ const qurlIds = new Set();
1508
+ const links = new Set();
1509
+ const results = value.map((raw, index) => {
1510
+ const item = delegatedResponseObject(raw, status, `result ${index}`, requestId);
1511
+ if (item.index !== index) {
1512
+ throw delegatedResponseError(status, `result ${index} has invalid index`, requestId);
1513
+ }
1514
+ if (item.status === "succeeded") {
1515
+ if (item.error !== undefined && item.error !== null) {
1516
+ throw delegatedResponseError(status, `result ${index} has conflicting fields`, requestId);
1517
+ }
1518
+ const qurl = delegatedResponseObject(item.qurl, status, `result ${index} qurl`, requestId);
1519
+ if (typeof qurl.qurl_id !== "string" ||
1520
+ !DELEGATED_QURL_ID_PATTERN.test(qurl.qurl_id) ||
1521
+ !validateDelegatedQurlLink(qurl.qurl_link) ||
1522
+ !isUtcTimestamp(qurl.expires_at) ||
1523
+ qurlIds.has(qurl.qurl_id) ||
1524
+ links.has(qurl.qurl_link)) {
1525
+ throw delegatedResponseError(status, `result ${index} has invalid qurl`, requestId);
1526
+ }
1527
+ qurlIds.add(qurl.qurl_id);
1528
+ links.add(qurl.qurl_link);
1529
+ succeeded++;
1530
+ return {
1531
+ index,
1532
+ status: "succeeded",
1533
+ qurl: {
1534
+ qurl_id: qurl.qurl_id,
1535
+ qurl_link: qurl.qurl_link,
1536
+ expires_at: qurl.expires_at,
1537
+ },
1538
+ };
1539
+ }
1540
+ if (item.status !== "failed" || (item.qurl !== undefined && item.qurl !== null)) {
1541
+ throw delegatedResponseError(status, `result ${index} has invalid status`, requestId);
1542
+ }
1543
+ const error = delegatedResponseObject(item.error, status, `result ${index} error`, requestId);
1544
+ if (typeof error.code !== "string" ||
1545
+ error.code.length === 0 ||
1546
+ typeof error.message !== "string") {
1547
+ throw delegatedResponseError(status, `result ${index} has invalid error`, requestId);
1548
+ }
1549
+ return {
1550
+ index,
1551
+ status: "failed",
1552
+ error: { code: error.code, message: error.message },
1553
+ };
1554
+ });
1555
+ if ((batchStatus === "succeeded" && succeeded !== itemCount) ||
1556
+ (batchStatus === "failed" && succeeded !== 0) ||
1557
+ (batchStatus === "partially_failed" && (succeeded === 0 || succeeded === itemCount))) {
1558
+ throw delegatedResponseError(status, "terminal status does not match results", requestId);
1559
+ }
1560
+ return results;
1561
+ }
1562
+ function classifyDelegatedBatchCreateFailure(error, batchId) {
1563
+ if (!(error instanceof QURLError)) {
1564
+ throw new DelegatedBatchOutcomeUnknownError(new RuntimeError("Unexpected failure after delegated batch dispatch", { cause: error }), batchId);
1565
+ }
1566
+ if ((error.status >= 400 && error.status < 500 && error.status !== 408) || error.status === 501) {
1567
+ throw error;
1568
+ }
1569
+ throw new DelegatedBatchOutcomeUnknownError(error, batchId);
1570
+ }
1251
1571
  // `format: uri` in the OpenAPI spec allows schemes the SDK doesn't
1252
1572
  // usefully support (`ftp://`, `file://`, `javascript:`, …). This is a
1253
1573
  // cheap client-side sanity check — the server is still the
@@ -1422,6 +1742,13 @@ function normalizePatchFields(input, fields, options = {}) {
1422
1742
  }
1423
1743
  return normalized;
1424
1744
  }
1745
+ function requireConsumerDecision(id, version, allowNew = false) {
1746
+ if (typeof id !== "string" ||
1747
+ (!/^[a-f0-9]{64}$/.test(id) && !(allowNew && id === "new")) ||
1748
+ !Number.isSafeInteger(version) ||
1749
+ version < (allowNew ? 0 : 1))
1750
+ throw clientValidationError("consumer decision: invalid id or expected version");
1751
+ }
1425
1752
  function validateResourceWriteFields(input, options = {}) {
1426
1753
  const errors = [];
1427
1754
  const collect = (fn) => {
@@ -1436,6 +1763,50 @@ function validateResourceWriteFields(input, options = {}) {
1436
1763
  throw err;
1437
1764
  }
1438
1765
  };
1766
+ collect(() => {
1767
+ if (input.private !== undefined && typeof input.private !== "boolean") {
1768
+ throw clientValidationError("private: must be a boolean");
1769
+ }
1770
+ if (input.consumer_access !== undefined) {
1771
+ const policy = input.consumer_access;
1772
+ requireObjectInput(policy, "consumer_access");
1773
+ const p = policy;
1774
+ requireNoUnknownFields(p, ["mode", "version", "contract_id"], "consumer_access");
1775
+ if (!["keys", "invite_only", "email_capture", "review", "contract"].includes(p.mode) ||
1776
+ !Number.isSafeInteger(p.version) ||
1777
+ p.version < 1) {
1778
+ throw clientValidationError("consumer_access: invalid mode or version");
1779
+ }
1780
+ if (p.mode === "contract") {
1781
+ if (typeof p.contract_id !== "string" ||
1782
+ !p.contract_id.trim() ||
1783
+ new TextEncoder().encode(p.contract_id).length > 128) {
1784
+ throw clientValidationError("consumer_access: contract mode requires a bounded contract_id");
1785
+ }
1786
+ }
1787
+ else if (p.contract_id !== undefined) {
1788
+ throw clientValidationError("consumer_access: contract_id requires contract mode");
1789
+ }
1790
+ if (input.private === false)
1791
+ throw clientValidationError("consumer_access: requires a private resource");
1792
+ if (options.requireUrlTarget) {
1793
+ if (p.version !== 1 || input.private !== true)
1794
+ throw clientValidationError("consumer_access: creation requires private true and version one");
1795
+ }
1796
+ else if (options.allowAliasClear) {
1797
+ const expected = input.expected_consumer_access_version;
1798
+ if (!Number.isSafeInteger(expected) ||
1799
+ expected < 0 ||
1800
+ p.version !== expected + 1 ||
1801
+ Object.keys(input).some((key) => key !== "consumer_access" && key !== "expected_consumer_access_version")) {
1802
+ throw clientValidationError("consumer_access: update requires its expected version and no metadata changes");
1803
+ }
1804
+ }
1805
+ }
1806
+ else if (input.expected_consumer_access_version !== undefined) {
1807
+ throw clientValidationError("expected_consumer_access_version: requires consumer_access");
1808
+ }
1809
+ });
1439
1810
  const resourceType = input.type;
1440
1811
  if (resourceType !== undefined && typeof resourceType !== "string") {
1441
1812
  errors.push(`type: must be a string (got ${resourceType === null ? "null" : typeof resourceType})`);
@@ -2108,6 +2479,12 @@ export class QURLClient {
2108
2479
  // SDK-wide normalizePatchFields convention.
2109
2480
  const normalized = normalizePatchFields(opts, PROTECT_URL_OPTION_KEYS);
2110
2481
  const body = { target_url: targetUrl };
2482
+ if (normalized.private !== undefined)
2483
+ body.private = normalized.private;
2484
+ if (normalized.consumerAccess !== undefined) {
2485
+ body.private = normalized.private ?? true;
2486
+ body.consumer_access = normalized.consumerAccess;
2487
+ }
2111
2488
  if (normalized.description !== undefined)
2112
2489
  body.description = normalized.description;
2113
2490
  if (normalized.tags !== undefined)
@@ -2116,9 +2493,19 @@ export class QURLClient {
2116
2493
  body.custom_domain = normalized.customDomain;
2117
2494
  if (normalized.alias !== undefined)
2118
2495
  body.alias = normalized.alias;
2119
- validateResourceWriteFields(body);
2120
- const resource = await this.request("POST", "/v1/resources", body, requestOptions);
2496
+ validateResourceWriteFields(body, { requireUrlTarget: true });
2497
+ const response = await this.rawRequest("POST", "/v1/resources", body, {
2498
+ requestOptions,
2499
+ });
2500
+ if (response.__http_status === 204)
2501
+ throw unexpectedResponseError("protectUrl: expected a resource response body");
2502
+ const resource = response.data;
2121
2503
  await verifyResourceIdentity(resource, "protectUrl");
2504
+ if (normalized.consumerAccess &&
2505
+ (resource.private !== true ||
2506
+ !sameConsumerPolicy(resource.consumer_access, normalized.consumerAccess, response.meta?.found_existing === true))) {
2507
+ throw unexpectedResponseError("protectUrl: private policy was not confirmed");
2508
+ }
2122
2509
  return new ProtectedResource(this, resource.crid,
2123
2510
  // Some resource types redact target_url in responses; fall back to
2124
2511
  // the caller-supplied URL so the handle stays useful.
@@ -2144,8 +2531,13 @@ export class QURLClient {
2144
2531
  return new ProtectedResource(this, id);
2145
2532
  }
2146
2533
  /** Find or create the active qURL Connector resource for an immutable slug. */
2147
- async ensureConnectorResource(slug, requestOptions) {
2534
+ async ensureConnectorResource(slug, requestOptions, consumerAccess) {
2148
2535
  requireConnectorSlug(slug, "ensureConnectorResource");
2536
+ if (consumerAccess !== undefined) {
2537
+ validateResourceWriteFields({ consumer_access: consumerAccess, private: true });
2538
+ if (consumerAccess.version !== 1)
2539
+ throw clientValidationError("consumer_access: creation requires version one");
2540
+ }
2149
2541
  // Response validation imports the producer's P-256 resource key. Preflight
2150
2542
  // before POST so a missing runtime capability cannot orphan a committed row.
2151
2543
  requireConnectorSubtleCrypto("ensureConnectorResource");
@@ -2160,12 +2552,18 @@ export class QURLClient {
2160
2552
  type: "tunnel",
2161
2553
  slug,
2162
2554
  find_or_create: true,
2555
+ ...(consumerAccess ? { private: true, consumer_access: consumerAccess } : {}),
2163
2556
  }, { requestOptions, retry: false, generateIdempotencyKey: false });
2164
2557
  // The find-or-create endpoint returns 201 for both newly-created
2165
2558
  // and found-existing rows; meta.found_existing distinguishes the arms.
2166
2559
  if (response.__http_status !== 201) {
2167
2560
  throw unexpectedResponseError(`ensureConnectorResource: expected HTTP 201, got ${response.__http_status ?? "unknown"}`);
2168
2561
  }
2562
+ if (consumerAccess &&
2563
+ (response.data.private !== true ||
2564
+ !sameConsumerPolicy(response.data.consumer_access, consumerAccess, response.meta?.found_existing === true))) {
2565
+ throw unexpectedResponseError("ensureConnectorResource: private policy was not confirmed");
2566
+ }
2169
2567
  resource = await parseConnectorResource(this, response.data, "ensureConnectorResource", {
2170
2568
  slug,
2171
2569
  });
@@ -2502,6 +2900,195 @@ export class QURLClient {
2502
2900
  }
2503
2901
  return requestId !== undefined ? { ...result, request_id: requestId } : result;
2504
2902
  }
2903
+ /**
2904
+ * Start one durable delegated qURL batch. The SDK sends this mutation once.
2905
+ * If the result may have committed, retry explicitly with the same key and
2906
+ * semantic body.
2907
+ */
2908
+ async createDelegatedQurlBatch(input, options) {
2909
+ validateCreateDelegatedQurlBatchInput(input);
2910
+ validateDelegatedBatchCreateOptions(options);
2911
+ let dispatched = false;
2912
+ let acceptedBatchId;
2913
+ try {
2914
+ const envelope = await this.rawRequest("POST", "/v1/delegated-qurl-batches", input, {
2915
+ requestOptions: options,
2916
+ retry: false,
2917
+ generateIdempotencyKey: false,
2918
+ captureResponseHeaders: true,
2919
+ onDispatch: () => {
2920
+ dispatched = true;
2921
+ },
2922
+ });
2923
+ const status = envelope.__http_status ?? 0;
2924
+ if (status !== 202) {
2925
+ throw delegatedResponseError(status, "create returned an unexpected success status");
2926
+ }
2927
+ const rawData = envelope.data;
2928
+ if (typeof rawData === "object" && rawData !== null && !Array.isArray(rawData)) {
2929
+ const candidate = rawData.batch_id;
2930
+ if (typeof candidate === "string" && DELEGATED_BATCH_ID_PATTERN.test(candidate)) {
2931
+ acceptedBatchId = candidate;
2932
+ }
2933
+ }
2934
+ const { data, requestId } = delegatedEnvelope(envelope, status);
2935
+ const base = delegatedBatchBase(data, status, requestId);
2936
+ if (data.status !== "queued" || base.itemCount !== input.grants.length) {
2937
+ throw delegatedResponseError(status, "acceptance data is inconsistent", requestId);
2938
+ }
2939
+ const responseHeaders = delegatedResponseHeaders(envelope, status, requestId);
2940
+ const location = validateDelegatedLocation(requiredDelegatedHeader(envelope, "Location", status, requestId), this.baseUrl, base.batchId, status, requestId);
2941
+ return {
2942
+ http_status: 202,
2943
+ batch_id: base.batchId,
2944
+ status: "queued",
2945
+ item_count: base.itemCount,
2946
+ submitted_at: base.submittedAt,
2947
+ etag: responseHeaders.etag,
2948
+ retry_after: responseHeaders.retryAfter,
2949
+ location,
2950
+ request_id: requestId,
2951
+ };
2952
+ }
2953
+ catch (error) {
2954
+ if (!dispatched)
2955
+ throw error;
2956
+ classifyDelegatedBatchCreateFailure(error, acceptedBatchId);
2957
+ }
2958
+ }
2959
+ /**
2960
+ * Read one delegated qURL batch state. One call performs one logical read;
2961
+ * callers retain control of the poll count and total deadline.
2962
+ */
2963
+ async getDelegatedQurlBatch(batchId, options) {
2964
+ if (typeof batchId !== "string" || !DELEGATED_BATCH_ID_PATTERN.test(batchId)) {
2965
+ throw clientValidationError("getDelegatedQurlBatch: batchId must match dqb_ plus 22 URL-safe characters");
2966
+ }
2967
+ validateGetDelegatedQurlBatchOptions(options);
2968
+ const envelope = await this.rawRequest("GET", `/v1/delegated-qurl-batches/${encodeURIComponent(batchId)}`, undefined, {
2969
+ passthroughStatuses: DELEGATED_BATCH_PASSTHROUGH_STATUSES,
2970
+ allowEmptyPassthroughBody: true,
2971
+ ifNoneMatch: options?.etag,
2972
+ retry: false,
2973
+ captureResponseHeaders: true,
2974
+ });
2975
+ const status = envelope.__http_status ?? 0;
2976
+ if (status === 304) {
2977
+ if (envelope.__http_body_empty !== true || options?.etag === undefined) {
2978
+ throw delegatedResponseError(status, "304 response is inconsistent");
2979
+ }
2980
+ const responseHeaders = delegatedResponseHeaders(envelope, status);
2981
+ if (responseHeaders.etag !== undefined && responseHeaders.etag !== options.etag) {
2982
+ throw delegatedResponseError(status, "304 response changed the ETag");
2983
+ }
2984
+ return {
2985
+ http_status: 304,
2986
+ etag: responseHeaders.etag ?? options.etag,
2987
+ ...(responseHeaders.retryAfter === undefined
2988
+ ? {}
2989
+ : { retry_after: responseHeaders.retryAfter }),
2990
+ };
2991
+ }
2992
+ if (status !== 200 && status !== 202) {
2993
+ throw delegatedResponseError(status, "read returned an unexpected success status");
2994
+ }
2995
+ const { data, requestId } = delegatedEnvelope(envelope, status);
2996
+ const base = delegatedBatchBase(data, status, requestId, batchId);
2997
+ if (status === 202) {
2998
+ if (data.status !== "queued" && data.status !== "running") {
2999
+ throw delegatedResponseError(status, "pending data has a terminal status", requestId);
3000
+ }
3001
+ const responseHeaders = delegatedResponseHeaders(envelope, status, requestId);
3002
+ return {
3003
+ http_status: 202,
3004
+ batch_id: base.batchId,
3005
+ status: data.status,
3006
+ item_count: base.itemCount,
3007
+ submitted_at: base.submittedAt,
3008
+ etag: responseHeaders.etag,
3009
+ retry_after: responseHeaders.retryAfter,
3010
+ request_id: requestId,
3011
+ };
3012
+ }
3013
+ if (data.status !== "succeeded" &&
3014
+ data.status !== "partially_failed" &&
3015
+ data.status !== "failed") {
3016
+ throw delegatedResponseError(status, "terminal data has a non-terminal status", requestId);
3017
+ }
3018
+ let results;
3019
+ try {
3020
+ if (!isUtcTimestamp(data.completed_at)) {
3021
+ throw delegatedResponseError(status, "terminal data has invalid completed_at", requestId);
3022
+ }
3023
+ results = validateDelegatedBatchResults(data.results, base.itemCount, data.status, status, requestId);
3024
+ }
3025
+ catch (error) {
3026
+ const partialQurlIds = Array.isArray(data.results)
3027
+ ? [
3028
+ ...new Set(data.results.flatMap((item) => item &&
3029
+ typeof item === "object" &&
3030
+ item.status === "succeeded" &&
3031
+ typeof item.qurl?.qurl_id === "string" &&
3032
+ DELEGATED_QURL_ID_PATTERN.test(item.qurl.qurl_id)
3033
+ ? [item.qurl.qurl_id]
3034
+ : [])),
3035
+ ]
3036
+ : [];
3037
+ if (error instanceof QURLError)
3038
+ Object.defineProperty(error, "partialQurlIds", {
3039
+ value: Object.freeze(partialQurlIds),
3040
+ enumerable: true,
3041
+ });
3042
+ throw error;
3043
+ }
3044
+ // This is the caller's only copy of the bearer links. Do not discard a
3045
+ // valid terminal body because an intermediary removed a cache validator;
3046
+ // qurl-service owns the required no-store response policy.
3047
+ const cacheControl = envelope.__http_headers?.get("Cache-Control") ?? null;
3048
+ if (!hasDelegatedNoStore(cacheControl)) {
3049
+ this.log("delegated batch terminal response is missing private, no-store", {
3050
+ batch_id: base.batchId,
3051
+ });
3052
+ }
3053
+ const etag = envelope.__http_headers?.get("ETag") ?? undefined;
3054
+ const terminalEtag = etag !== undefined && etag.length <= MAX_DELEGATED_ETAG && STRONG_ETAG_PATTERN.test(etag)
3055
+ ? etag
3056
+ : undefined;
3057
+ return {
3058
+ http_status: 200,
3059
+ batch_id: base.batchId,
3060
+ status: data.status,
3061
+ item_count: base.itemCount,
3062
+ submitted_at: base.submittedAt,
3063
+ completed_at: data.completed_at,
3064
+ results,
3065
+ ...(terminalEtag === undefined ? {} : { etag: terminalEtag }),
3066
+ request_id: requestId,
3067
+ };
3068
+ }
3069
+ /** Read owner-visible metadata for one delegated qURL. */
3070
+ async getDelegatedQurl(qurlId) {
3071
+ if (typeof qurlId !== "string" || !DELEGATED_QURL_ID_PATTERN.test(qurlId)) {
3072
+ throw clientValidationError("getDelegatedQurl: qurlId must match q_ plus 11 lowercase hexadecimal characters");
3073
+ }
3074
+ const envelope = await this.rawRequest("GET", `/v1/delegated-qurls/${encodeURIComponent(qurlId)}`);
3075
+ const status = envelope.__http_status ?? 0;
3076
+ if (status !== 200) {
3077
+ throw delegatedResponseError(status, "read returned an unexpected success status");
3078
+ }
3079
+ const { data, requestId } = delegatedEnvelope(envelope, status);
3080
+ return validateDelegatedQurl(data, qurlId, status, requestId);
3081
+ }
3082
+ /**
3083
+ * Revoke one delegated qURL. DELETE is never retried automatically. A 503
3084
+ * `mutation_outcome_unknown` is safe to retry with the same qURL ID.
3085
+ */
3086
+ async deleteDelegatedQurl(qurlId) {
3087
+ if (typeof qurlId !== "string" || !DELEGATED_QURL_ID_PATTERN.test(qurlId)) {
3088
+ throw clientValidationError("deleteDelegatedQurl: qurlId must match q_ plus 11 lowercase hexadecimal characters");
3089
+ }
3090
+ await this.requestNoContent(`/v1/delegated-qurls/${encodeURIComponent(qurlId)}`);
3091
+ }
2505
3092
  /**
2506
3093
  * Get a qURL resource and its access tokens.
2507
3094
  *
@@ -2763,6 +3350,76 @@ export class QURLClient {
2763
3350
  });
2764
3351
  return this.request("PATCH", `/v1/resources/${encodeURIComponent(id)}`, validationInput, options);
2765
3352
  }
3353
+ /** Send a publisher-only invitation. Mail delivery does not prove enrollment. */
3354
+ async inviteConsumer(id, email) {
3355
+ requireResourceCrid(id, "inviteConsumer");
3356
+ requireNonEmptyIfPresent(email, "email");
3357
+ if (typeof email !== "string" || email.length > 320)
3358
+ throw clientValidationError("email: must be a bounded string");
3359
+ return this.request("POST", `/v1/resources/${encodeURIComponent(id)}/invitations`, { email });
3360
+ }
3361
+ async listConsumerInvitations(id, input = {}) {
3362
+ requireResourceCrid(id, "listConsumerInvitations");
3363
+ const { data, meta } = await this.rawRequest("GET", appendQuery(`/v1/resources/${encodeURIComponent(id)}/invitations`, input, ["cursor", "limit"], "listConsumerInvitations"));
3364
+ return pageFromMeta({ invitations: data ?? [] }, meta);
3365
+ }
3366
+ async listConsumerGrants(id, input = {}) {
3367
+ requireResourceCrid(id, "listConsumerGrants");
3368
+ const { data, meta } = await this.rawRequest("GET", appendQuery(`/v1/resources/${encodeURIComponent(id)}/consumer-grants`, input, ["cursor", "limit"], "listConsumerGrants"));
3369
+ return pageFromMeta({ grants: data ?? [] }, meta);
3370
+ }
3371
+ async resendConsumerInvitation(id, invitationId, expectedVersion) {
3372
+ requireResourceCrid(id, "resendConsumerInvitation");
3373
+ requireConsumerDecision(invitationId, expectedVersion);
3374
+ return this.request("POST", `/v1/resources/${encodeURIComponent(id)}/invitations/${encodeURIComponent(invitationId)}/resend`, { expected_version: expectedVersion });
3375
+ }
3376
+ /** Remove the invitation and its grant. Inspect session_cleanup before reporting stream removal. */
3377
+ async revokeConsumerInvitation(id, invitationId, expectedVersion) {
3378
+ requireResourceCrid(id, "revokeConsumerInvitation");
3379
+ requireConsumerDecision(invitationId, expectedVersion);
3380
+ return this.request("DELETE", `/v1/resources/${encodeURIComponent(id)}/invitations/${encodeURIComponent(invitationId)}?expected_version=${expectedVersion}`);
3381
+ }
3382
+ /** Review approval, key opt-in with grantId new, or trusted publisher entitlement. */
3383
+ async approveConsumerGrant(id, grantId, input) {
3384
+ requireResourceCrid(id, "approveConsumerGrant");
3385
+ requireObjectInput(input, "approveConsumerGrant");
3386
+ requireNoUnknownFields(input, [
3387
+ "expected_version",
3388
+ "credential_kind",
3389
+ "credential_id",
3390
+ "credential_public_key_b64",
3391
+ "entitlement_id",
3392
+ "expires_at",
3393
+ ], "approveConsumerGrant");
3394
+ requireConsumerDecision(grantId, input.expected_version, grantId === "new");
3395
+ return this.request("POST", `/v1/resources/${encodeURIComponent(id)}/consumer-grants/${encodeURIComponent(grantId)}/approve`, input);
3396
+ }
3397
+ async denyConsumerGrant(id, grantId, expectedVersion) {
3398
+ requireResourceCrid(id, "denyConsumerGrant");
3399
+ requireConsumerDecision(grantId, expectedVersion);
3400
+ return this.request("POST", `/v1/resources/${encodeURIComponent(id)}/consumer-grants/${encodeURIComponent(grantId)}/deny`, { expected_version: expectedVersion });
3401
+ }
3402
+ async revokeConsumerGrant(id, grantId, expectedVersion) {
3403
+ requireResourceCrid(id, "revokeConsumerGrant");
3404
+ requireConsumerDecision(grantId, expectedVersion);
3405
+ return this.request("DELETE", `/v1/resources/${encodeURIComponent(id)}/consumer-grants/${encodeURIComponent(grantId)}?expected_version=${expectedVersion}`);
3406
+ }
3407
+ /**
3408
+ * Revoke an API resource identifier retained by a client migrating to CRIDs.
3409
+ * New callers should use deleteResource with the resource CRID.
3410
+ */
3411
+ async deleteResourceByIdentifier(id) {
3412
+ if (typeof id !== "string")
3413
+ throw clientValidationError("id: invalid resource identifier");
3414
+ if (/^r_[a-z0-9_-]{11}$/.test(id)) {
3415
+ await this.requestNoContent(`/v1/qurls/${encodeURIComponent(id)}`);
3416
+ return;
3417
+ }
3418
+ const publicKey = /^[A-Za-z0-9_-]{107,214}$/.test(id) && id.length % 4 !== 1;
3419
+ if (!publicKey)
3420
+ requireResourceCrid(id, "deleteResourceByIdentifier");
3421
+ await this.requestNoContent(`/v1/resources/${encodeURIComponent(id)}`);
3422
+ }
2766
3423
  /** Revoke a resource and all of its qURLs. */
2767
3424
  async deleteResource(id) {
2768
3425
  requireResourceCrid(id, "deleteResource");
@@ -2869,43 +3526,63 @@ export class QURLClient {
2869
3526
  validateResourceQurlUpdateInput(normalized);
2870
3527
  return this.request("PATCH", `/v1/resources/${encodeURIComponent(id)}/qurls/${encodeURIComponent(qurlId)}`, normalized, options);
2871
3528
  }
2872
- /**
2873
- * List active access sessions for a resource.
2874
- *
2875
- * The current API contract has no cursor query params for this endpoint.
2876
- * If the service later advertises pagination metadata, the SDK returns
2877
- * this page and emits a debug log rather than pretending it fetched all pages.
2878
- */
3529
+ /** List every active session, following bounded service pages. */
2879
3530
  async listResourceSessions(id) {
2880
3531
  requireResourceCrid(id, "listResourceSessions");
2881
- const { data, meta } = await this.rawRequest("GET", `/v1/resources/${encodeURIComponent(id)}/sessions`);
2882
- if (meta?.has_more || meta?.next_cursor) {
2883
- this.log("listResourceSessions: pagination metadata surfaced on unpaginated endpoint", {
2884
- has_more: meta.has_more,
2885
- next_cursor: meta.next_cursor,
2886
- });
3532
+ const sessions = [];
3533
+ let request_id;
3534
+ let page_size;
3535
+ for await (const session of this.paginateAll("listResourceSessions", {}, async ({ cursor }) => {
3536
+ const { data, meta } = await this.rawRequest("GET", appendQuery(`/v1/resources/${encodeURIComponent(id)}/sessions`, { paged: "true", cursor }, ["paged", "cursor"], "listResourceSessions"));
3537
+ if (meta?.has_more && !meta.next_cursor) {
3538
+ throw unexpectedResponseError("Session page has no continuation cursor", {
3539
+ requestId: meta.request_id,
3540
+ });
3541
+ }
3542
+ request_id = meta?.request_id;
3543
+ page_size = meta?.page_size;
3544
+ return { ...meta, sessions: data ?? [] };
3545
+ }, (page) => page.sessions)) {
3546
+ sessions.push(session);
2887
3547
  }
2888
- return {
2889
- sessions: data ?? [],
2890
- request_id: meta?.request_id,
2891
- has_more: false,
2892
- page_size: meta?.page_size,
2893
- };
3548
+ return { sessions, request_id, has_more: false, page_size };
2894
3549
  }
2895
- /** Terminate all active sessions for a resource and return the server count. */
3550
+ /**
3551
+ * End every session page. This is not atomic: a failed page throws after earlier
3552
+ * pages may have closed sessions. List sessions to reconcile, then retry the call.
3553
+ * A retry can close newly admitted sessions and counts only its own transitions.
3554
+ */
2896
3555
  async terminateAllResourceSessions(id) {
2897
3556
  requireResourceCrid(id, "terminateAllResourceSessions");
2898
3557
  const path = `/v1/resources/${encodeURIComponent(id)}/sessions`;
2899
- const { data, meta, __http_status } = await this.rawRequest("DELETE", path);
2900
- if (__http_status === 204) {
2901
- throw unexpectedResponseError(`Unexpected 204 No Content from DELETE ${path}; expected response body`);
2902
- }
2903
- if (data?.terminated === undefined) {
2904
- this.log("terminateAllResourceSessions: missing terminated count; defaulting to zero", {
2905
- request_id: meta?.request_id,
2906
- });
3558
+ let terminated = 0;
3559
+ let request_id;
3560
+ for await (const count of this.paginateAll("terminateAllResourceSessions", {}, async ({ cursor }) => {
3561
+ const { data, meta, __http_status } = await this.rawRequest("DELETE", appendQuery(path, { paged: "true", cursor }, ["paged", "cursor"], "terminateAllResourceSessions"));
3562
+ if (__http_status === 204) {
3563
+ throw unexpectedResponseError(`Unexpected 204 No Content from DELETE ${path}; expected response body`, { status: 204, requestId: meta?.request_id });
3564
+ }
3565
+ if (meta?.has_more && !meta.next_cursor) {
3566
+ throw unexpectedResponseError("Session page has no continuation cursor", {
3567
+ requestId: meta.request_id,
3568
+ });
3569
+ }
3570
+ if (data?.terminated === undefined) {
3571
+ this.log("terminateAllResourceSessions: missing terminated count; defaulting to zero", {
3572
+ request_id: meta?.request_id,
3573
+ });
3574
+ }
3575
+ else if (!Number.isSafeInteger(data.terminated) || data.terminated < 0) {
3576
+ throw unexpectedResponseError("terminateAllResourceSessions: invalid terminated count", {
3577
+ requestId: meta?.request_id,
3578
+ });
3579
+ }
3580
+ request_id = meta?.request_id;
3581
+ return { ...meta, counts: [data?.terminated ?? 0] };
3582
+ }, (page) => page.counts)) {
3583
+ terminated += count;
2907
3584
  }
2908
- return { terminated: data?.terminated ?? 0, request_id: meta?.request_id };
3585
+ return { terminated, request_id };
2909
3586
  }
2910
3587
  /** Terminate a specific resource session. */
2911
3588
  async terminateResourceSession(id, sessionId) {
@@ -3254,7 +3931,7 @@ export class QURLClient {
3254
3931
  */
3255
3932
  async rawRequest(method, path, body, rawOptions = {}) {
3256
3933
  const url = `${this.baseUrl}${path}`;
3257
- const { passthroughStatuses = NO_PASSTHROUGH_STATUSES, requestOptions, allowEmptySuccessBody = false, retry = true, generateIdempotencyKey = true, } = rawOptions;
3934
+ const { passthroughStatuses = NO_PASSTHROUGH_STATUSES, requestOptions, allowEmptySuccessBody = false, allowEmptyPassthroughBody = false, retry = true, generateIdempotencyKey = true, ifNoneMatch, captureResponseHeaders = false, onDispatch, } = rawOptions;
3258
3935
  const idempotencyKey = idempotencyKeyForRequest(method, requestOptions, generateIdempotencyKey);
3259
3936
  const headers = {
3260
3937
  Authorization: `Bearer ${this.apiKey}`,
@@ -3267,6 +3944,9 @@ export class QURLClient {
3267
3944
  if (idempotencyKey !== undefined) {
3268
3945
  headers["Idempotency-Key"] = idempotencyKey;
3269
3946
  }
3947
+ if (ifNoneMatch !== undefined) {
3948
+ headers["If-None-Match"] = ifNoneMatch;
3949
+ }
3270
3950
  // Reads may retry transient statuses and transport failures. POST/PATCH
3271
3951
  // keep status-code retries limited to rate limits and reuse the same
3272
3952
  // Idempotency-Key for fetch-level failures. DELETE is deliberately never
@@ -3303,6 +3983,7 @@ export class QURLClient {
3303
3983
  // attempt shouldn't poison the next try's deadline. Total
3304
3984
  // request bound: roughly `timeout * (maxRetries + 1) +
3305
3985
  // sum(retryDelay)`.
3986
+ onDispatch?.();
3306
3987
  response = await this.fetchFn(url, {
3307
3988
  method,
3308
3989
  headers,
@@ -3478,16 +4159,20 @@ export class QURLClient {
3478
4159
  data: undefined,
3479
4160
  __http_status: response.status,
3480
4161
  __http_body_empty: responseBody.length === 0,
4162
+ __http_headers: captureResponseHeaders ? response.headers : undefined,
3481
4163
  };
3482
4164
  }
3483
4165
  // Exact no-content helpers must inspect both axes of the success
3484
4166
  // contract. Let only those callers observe an empty non-204 response;
3485
4167
  // body-returning methods still reject it as non-JSON below.
3486
- if (response.ok && allowEmptySuccessBody && responseBody.length === 0) {
4168
+ if (((response.ok && allowEmptySuccessBody) ||
4169
+ (isPassthrough && allowEmptyPassthroughBody)) &&
4170
+ responseBody.length === 0) {
3487
4171
  return {
3488
4172
  data: undefined,
3489
4173
  __http_status: response.status,
3490
4174
  __http_body_empty: true,
4175
+ __http_headers: captureResponseHeaders ? response.headers : undefined,
3491
4176
  };
3492
4177
  }
3493
4178
  try {
@@ -3497,6 +4182,7 @@ export class QURLClient {
3497
4182
  __http_status: response.status,
3498
4183
  // JSON.parse("") throws, so parsed JSON is necessarily non-empty.
3499
4184
  __http_body_empty: false,
4185
+ __http_headers: captureResponseHeaders ? response.headers : undefined,
3500
4186
  };
3501
4187
  }
3502
4188
  catch {
@@ -3607,14 +4293,13 @@ export class QURLClient {
3607
4293
  const header = response.headers.get("Retry-After");
3608
4294
  if (!header)
3609
4295
  return undefined;
3610
- // TODO: parse HTTP-date format per RFC 7231 §7.1.3 — currently only
3611
- // handles delta-seconds; HTTP-date headers fall through to the
3612
- // exponential-backoff branch in `retryDelay`. Tracked in #61.
3613
- //
3614
- // Digit-only pre-check rather than `parseInt` alone: `parseInt("60abc")`
3615
- // returns `60` and would silently honor a malformed header. The
3616
- // strict check makes any deviation observable (debug log) instead.
3617
4296
  const trimmed = header.trim();
4297
+ if (/^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(trimmed)) {
4298
+ const seconds = Math.ceil((Date.parse(trimmed) - Date.now()) / 1000);
4299
+ return Number.isSafeInteger(seconds) && seconds > 0 && seconds <= RETRY_AFTER_PARSE_LIMIT_S
4300
+ ? seconds
4301
+ : undefined;
4302
+ }
3618
4303
  if (!/^\d+$/.test(trimmed)) {
3619
4304
  this.log("Retry-After header was non-numeric, using exponential backoff", {
3620
4305
  header_value: header.slice(0, 100),
@@ -3776,4 +4461,12 @@ export class ConnectorResource {
3776
4461
  return this.#client.createPortal(this.crid, opts, requestOptions);
3777
4462
  }
3778
4463
  }
4464
+ function sameConsumerPolicy(actual, expected, foundExisting) {
4465
+ return (actual?.mode === expected.mode &&
4466
+ (actual?.version === expected.version ||
4467
+ (foundExisting &&
4468
+ Number.isSafeInteger(actual?.version) &&
4469
+ Number(actual?.version) > expected.version)) &&
4470
+ actual?.contract_id === expected.contract_id);
4471
+ }
3779
4472
  //# sourceMappingURL=client.js.map