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