@layervai/qurl 0.6.0 → 2.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.
- package/README.md +332 -15
- package/dist/cjs/client.d.ts +62 -31
- package/dist/cjs/client.js +932 -163
- package/dist/cjs/crid.d.ts +3 -0
- package/dist/cjs/crid.js +62 -0
- package/dist/cjs/errors.d.ts +30 -5
- package/dist/cjs/errors.js +41 -6
- package/dist/cjs/index.d.ts +5 -3
- package/dist/cjs/index.js +11 -1
- package/dist/cjs/node/agent-runtime.d.ts +108 -0
- package/dist/cjs/node/agent-runtime.js +952 -0
- package/dist/cjs/node/agent-state.d.ts +100 -0
- package/dist/cjs/node/agent-state.js +311 -0
- package/dist/cjs/node/agent-transport.d.ts +17 -0
- package/dist/cjs/node/agent-transport.js +118 -0
- package/dist/cjs/node/deployment.d.ts +6 -0
- package/dist/cjs/node/deployment.js +24 -3
- package/dist/cjs/node/file-agent-state.d.ts +15 -0
- package/dist/cjs/node/file-agent-state.js +137 -0
- package/dist/cjs/node/native-loader.cjs +7 -0
- package/dist/cjs/node/native-loader.d.cts +11 -0
- package/dist/cjs/node/native-udp.d.ts +8 -3
- package/dist/cjs/node/native-udp.js +8 -3
- package/dist/cjs/node/nhp-wire.d.ts +9 -0
- package/dist/cjs/node/nhp-wire.js +24 -7
- package/dist/cjs/node/portal-opener.d.ts +7 -0
- package/dist/cjs/node/portal-opener.js +28 -6
- package/dist/cjs/node/provider.d.ts +19 -0
- package/dist/cjs/node/provider.js +153 -0
- package/dist/cjs/node/qv2.d.ts +3 -1
- package/dist/cjs/node/qv2.js +16 -2
- package/dist/cjs/node/relay.d.ts +12 -0
- package/dist/cjs/node/relay.js +111 -0
- package/dist/cjs/node/sealed-agent-state.d.ts +19 -0
- package/dist/cjs/node/sealed-agent-state.js +193 -0
- package/dist/cjs/node/strict-json.d.ts +1 -1
- package/dist/cjs/node/strict-json.js +8 -3
- package/dist/cjs/node.d.ts +14 -0
- package/dist/cjs/node.js +24 -1
- package/dist/cjs/share.d.ts +56 -0
- package/dist/cjs/share.js +113 -0
- package/dist/cjs/types.d.ts +173 -13
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/client.d.ts +62 -31
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +932 -164
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/crid.d.ts +4 -0
- package/dist/esm/crid.d.ts.map +1 -0
- package/dist/esm/crid.js +58 -0
- package/dist/esm/crid.js.map +1 -0
- package/dist/esm/errors.d.ts +30 -5
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +39 -5
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +5 -3
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +3 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/node/agent-runtime.d.ts +109 -0
- package/dist/esm/node/agent-runtime.d.ts.map +1 -0
- package/dist/esm/node/agent-runtime.js +946 -0
- package/dist/esm/node/agent-runtime.js.map +1 -0
- package/dist/esm/node/agent-state.d.ts +101 -0
- package/dist/esm/node/agent-state.d.ts.map +1 -0
- package/dist/esm/node/agent-state.js +298 -0
- package/dist/esm/node/agent-state.js.map +1 -0
- package/dist/esm/node/agent-transport.d.ts +18 -0
- package/dist/esm/node/agent-transport.d.ts.map +1 -0
- package/dist/esm/node/agent-transport.js +114 -0
- package/dist/esm/node/agent-transport.js.map +1 -0
- package/dist/esm/node/deployment.d.ts +6 -0
- package/dist/esm/node/deployment.d.ts.map +1 -1
- package/dist/esm/node/deployment.js +23 -3
- package/dist/esm/node/deployment.js.map +1 -1
- package/dist/esm/node/file-agent-state.d.ts +16 -0
- package/dist/esm/node/file-agent-state.d.ts.map +1 -0
- package/dist/esm/node/file-agent-state.js +134 -0
- package/dist/esm/node/file-agent-state.js.map +1 -0
- package/dist/esm/node/native-loader.cjs +7 -0
- package/dist/esm/node/native-loader.d.cts +11 -0
- package/dist/esm/node/native-udp.d.ts +8 -3
- package/dist/esm/node/native-udp.d.ts.map +1 -1
- package/dist/esm/node/native-udp.js +7 -5
- package/dist/esm/node/native-udp.js.map +1 -1
- package/dist/esm/node/nhp-wire.d.ts +9 -0
- package/dist/esm/node/nhp-wire.d.ts.map +1 -1
- package/dist/esm/node/nhp-wire.js +23 -6
- package/dist/esm/node/nhp-wire.js.map +1 -1
- package/dist/esm/node/portal-opener.d.ts +7 -0
- package/dist/esm/node/portal-opener.d.ts.map +1 -1
- package/dist/esm/node/portal-opener.js +28 -6
- package/dist/esm/node/portal-opener.js.map +1 -1
- package/dist/esm/node/provider.d.ts +20 -0
- package/dist/esm/node/provider.d.ts.map +1 -0
- package/dist/esm/node/provider.js +149 -0
- package/dist/esm/node/provider.js.map +1 -0
- package/dist/esm/node/qv2.d.ts +3 -1
- package/dist/esm/node/qv2.d.ts.map +1 -1
- package/dist/esm/node/qv2.js +17 -3
- package/dist/esm/node/qv2.js.map +1 -1
- package/dist/esm/node/relay.d.ts +13 -0
- package/dist/esm/node/relay.d.ts.map +1 -0
- package/dist/esm/node/relay.js +105 -0
- package/dist/esm/node/relay.js.map +1 -0
- package/dist/esm/node/sealed-agent-state.d.ts +20 -0
- package/dist/esm/node/sealed-agent-state.d.ts.map +1 -0
- package/dist/esm/node/sealed-agent-state.js +190 -0
- package/dist/esm/node/sealed-agent-state.js.map +1 -0
- package/dist/esm/node/strict-json.d.ts +1 -1
- package/dist/esm/node/strict-json.d.ts.map +1 -1
- package/dist/esm/node/strict-json.js +8 -3
- package/dist/esm/node/strict-json.js.map +1 -1
- package/dist/esm/node.d.ts +14 -0
- package/dist/esm/node.d.ts.map +1 -1
- package/dist/esm/node.js +7 -0
- package/dist/esm/node.js.map +1 -1
- package/dist/esm/share.d.ts +57 -0
- package/dist/esm/share.d.ts.map +1 -0
- package/dist/esm/share.js +109 -0
- package/dist/esm/share.js.map +1 -0
- package/dist/esm/types.d.ts +173 -13
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +17 -8
package/dist/cjs/client.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ConnectorResource = exports.ProtectedResource = exports.QURLClient = exports.SIGNED_FRAGMENT_RE = void 0;
|
|
4
|
+
exports.isApiKeyRequestScope = isApiKeyRequestScope;
|
|
5
|
+
const crid_js_1 = require("./crid.js");
|
|
4
6
|
const errors_js_1 = require("./errors.js");
|
|
5
7
|
const internal_js_1 = require("./internal.js");
|
|
8
|
+
const share_js_1 = require("./share.js");
|
|
6
9
|
const version_js_1 = require("./version.js");
|
|
7
10
|
const DEFAULT_BASE_URL = "https://api.layerv.ai";
|
|
8
11
|
const DEFAULT_MAX_RETRIES = 3;
|
|
@@ -74,6 +77,7 @@ class ResponseBodyMaterializationError extends Error {
|
|
|
74
77
|
}
|
|
75
78
|
const NO_PASSTHROUGH_STATUSES = [];
|
|
76
79
|
const BATCH_PASSTHROUGH_STATUSES = [400];
|
|
80
|
+
const DELEGATED_BATCH_PASSTHROUGH_STATUSES = [304];
|
|
77
81
|
/** Allowlist of known query-param keys for the `list()` endpoint. */
|
|
78
82
|
const LIST_PARAM_KEYS = [
|
|
79
83
|
"limit",
|
|
@@ -87,6 +91,24 @@ const LIST_PARAM_KEYS = [
|
|
|
87
91
|
"expires_after",
|
|
88
92
|
];
|
|
89
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
|
+
];
|
|
90
112
|
// Compile-time witness: `Exclude<keyof X, (typeof KEYS)[number]>` is
|
|
91
113
|
// `never` iff KEYS lists every key of X. Paired with the
|
|
92
114
|
// `satisfies readonly (keyof X)[]` clause on the array (which catches
|
|
@@ -97,6 +119,10 @@ function assertExhaustive(_) {
|
|
|
97
119
|
}
|
|
98
120
|
assertExhaustive(true);
|
|
99
121
|
assertExhaustive(true);
|
|
122
|
+
assertExhaustive(true);
|
|
123
|
+
assertExhaustive(true);
|
|
124
|
+
assertExhaustive(true);
|
|
125
|
+
assertExhaustive(true);
|
|
100
126
|
const CREATE_FIELD_KEYS = [
|
|
101
127
|
"type",
|
|
102
128
|
"target_url",
|
|
@@ -176,6 +202,10 @@ const CREATE_QURL_FOR_RESOURCE_FIELD_KEYS = [
|
|
|
176
202
|
"access_policy",
|
|
177
203
|
"target_path",
|
|
178
204
|
];
|
|
205
|
+
const SHARE_RESOURCE_OPTION_KEYS = [
|
|
206
|
+
"ttlSeconds",
|
|
207
|
+
];
|
|
208
|
+
assertExhaustive(true);
|
|
179
209
|
assertExhaustive(true);
|
|
180
210
|
/**
|
|
181
211
|
* Options accepted by the portal-verb `protectUrl` / `createPortal`
|
|
@@ -234,11 +264,12 @@ const UPDATE_WEBHOOK_FIELD_KEYS = [
|
|
|
234
264
|
];
|
|
235
265
|
assertExhaustive(true);
|
|
236
266
|
const CREATE_API_KEY_FIELD_KEYS = [
|
|
267
|
+
"kind",
|
|
237
268
|
"name",
|
|
238
269
|
"scopes",
|
|
270
|
+
"target",
|
|
271
|
+
"claims",
|
|
239
272
|
"expires_in",
|
|
240
|
-
"purpose",
|
|
241
|
-
"tunnel_slug",
|
|
242
273
|
];
|
|
243
274
|
assertExhaustive(true);
|
|
244
275
|
const UPDATE_API_KEY_FIELD_KEYS = [
|
|
@@ -718,12 +749,55 @@ const MAX_DESCRIPTION = 500;
|
|
|
718
749
|
const MAX_CUSTOM_DOMAIN = 253;
|
|
719
750
|
const MAX_MAX_SESSIONS = 1000;
|
|
720
751
|
const MAX_API_KEY_NAME = 100;
|
|
752
|
+
const API_KEY_REQUEST_SCOPE_VALUES = [
|
|
753
|
+
"qurl:read",
|
|
754
|
+
"qurl:write",
|
|
755
|
+
"qurl:resolve",
|
|
756
|
+
"qurl:agent",
|
|
757
|
+
];
|
|
758
|
+
assertExhaustive(true);
|
|
759
|
+
const API_KEY_REQUEST_SCOPES = new Set(API_KEY_REQUEST_SCOPE_VALUES);
|
|
760
|
+
/** Narrow a response scope before reusing it in a create/update request. */
|
|
761
|
+
function isApiKeyRequestScope(scope) {
|
|
762
|
+
return API_KEY_REQUEST_SCOPES.has(scope);
|
|
763
|
+
}
|
|
764
|
+
const CREDENTIAL_TARGET_VALUES = [
|
|
765
|
+
"agent",
|
|
766
|
+
"connector",
|
|
767
|
+
];
|
|
768
|
+
assertExhaustive(true);
|
|
769
|
+
const CREDENTIAL_TARGETS = new Set(CREDENTIAL_TARGET_VALUES);
|
|
770
|
+
const CREDENTIAL_CLAIM_TYPE_VALUES = [
|
|
771
|
+
"connector",
|
|
772
|
+
];
|
|
773
|
+
assertExhaustive(true);
|
|
774
|
+
const CREDENTIAL_CLAIM_TYPES = new Set(CREDENTIAL_CLAIM_TYPE_VALUES);
|
|
775
|
+
// Mirrors CreateApiKeyRequest.expires_in in the service OpenAPI. Composite
|
|
776
|
+
// durations are deliberately invalid on this endpoint. The 24h cap permits
|
|
777
|
+
// 1d, but no positive whole-week duration.
|
|
778
|
+
const CREDENTIAL_DURATION_PATTERN = /^([0-9]+)([smhdw])$/;
|
|
779
|
+
const CREDENTIAL_DURATION_MULTIPLIERS = {
|
|
780
|
+
s: 1n,
|
|
781
|
+
m: 60n,
|
|
782
|
+
h: 3600n,
|
|
783
|
+
d: 86400n,
|
|
784
|
+
w: 604800n,
|
|
785
|
+
};
|
|
786
|
+
const MAX_ENROLLMENT_TOKEN_TTL_SECONDS = 86400n;
|
|
721
787
|
const MAX_ALIAS = 64;
|
|
722
788
|
const MAX_SLUG = 64;
|
|
723
789
|
const MAX_TARGET_PATH = 2048;
|
|
724
790
|
const MAX_TAGS = 10;
|
|
725
791
|
const MAX_TAG_LENGTH = 50;
|
|
726
792
|
const MAX_AUTO_PAGINATION_PAGES = 10_000;
|
|
793
|
+
const MIN_DELEGATED_BATCH_IDEMPOTENCY_KEY = 32;
|
|
794
|
+
const MAX_DELEGATED_MINT_CAPABILITY = 8192;
|
|
795
|
+
const MAX_DELEGATED_ETAG = 96;
|
|
796
|
+
const MAX_DELEGATED_BATCH_ITEMS = 100;
|
|
797
|
+
const DELEGATED_BATCH_ID_PATTERN = /^dqb_[A-Za-z0-9_-]{22}$/;
|
|
798
|
+
const DELEGATED_QURL_ID_PATTERN = /^q_[0-9a-f]{11}$/;
|
|
799
|
+
const STRONG_ETAG_PATTERN = /^"[\x21\x23-\x7e]+"$/;
|
|
800
|
+
const UTC_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
|
|
727
801
|
// Keep these identity contracts aligned with qurl-go and the public API.
|
|
728
802
|
const CONNECTOR_SLUG_PATTERN = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
|
|
729
803
|
// Current service schemas intentionally share this grammar, but keep the
|
|
@@ -735,7 +809,6 @@ const CONNECTOR_ROUTING_ID_PATTERN = /^c-[a-z2-7]{51}[aq]$/;
|
|
|
735
809
|
// CreateQurlRequest.target_url pattern is loose (just a URI) but
|
|
736
810
|
// UpdateQurlRequest.tags pattern is specific — enforce it here.
|
|
737
811
|
const TAG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9 _-]*$/;
|
|
738
|
-
const RESOURCE_ID_PREFIX = "r_";
|
|
739
812
|
function requireMaxLength(value, field, max) {
|
|
740
813
|
if (value === undefined)
|
|
741
814
|
return;
|
|
@@ -836,22 +909,129 @@ function pageFromMeta(out, meta) {
|
|
|
836
909
|
page_size: meta?.page_size,
|
|
837
910
|
};
|
|
838
911
|
}
|
|
912
|
+
// Best-effort defence-in-depth against today's access-token grammar. The
|
|
913
|
+
// service remains authoritative because resource IDs and credentials are both
|
|
914
|
+
// opaque and future credential formats may not retain the `at_` prefix. The
|
|
915
|
+
// delimiter requirement cannot collide with current base64url public keys,
|
|
916
|
+
// standard-base64 keys, or base32 CRIDs.
|
|
917
|
+
const PATH_EMBEDDED_ACCESS_TOKEN_RE = /[?&#=:\s]at_/i;
|
|
918
|
+
const PATH_PREFIXED_ACCESS_TOKEN_RE = /\/at_/i;
|
|
919
|
+
const QURL_LINK_URL_RE = /^(?:https?:\/\/)?(?:[a-z0-9-]+\.)*qurl\.link(?::\d{1,5})?(?:\/|[?#])/i;
|
|
920
|
+
// ponytail: today's 22-character token suffix only improves URL-paste diagnostics;
|
|
921
|
+
// other lengths are still rejected as URLs without echoing the input.
|
|
922
|
+
const URL_EMBEDDED_ACCESS_TOKEN_RE = /[?&#=:/]at_[a-z0-9_-]{22}(?:$|[?&#:/])/i;
|
|
923
|
+
// A host without a slash can be a legitimate domain identifier. Require the
|
|
924
|
+
// path boundary so shared validation does not break the domain namespace.
|
|
925
|
+
const PATH_SCHEMELESS_URL_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)+(?::\d{1,5})?\//i;
|
|
926
|
+
const QURL_DISPLAY_ID_PREFIX_RE = /^q_/i;
|
|
927
|
+
const MAX_PATH_ID_DECODE_PASSES = 3;
|
|
928
|
+
const MAX_PATH_ID_LENGTH = 4096;
|
|
929
|
+
function isUrlLikePathId(value) {
|
|
930
|
+
return /^https?:\/\//i.test(value) || PATH_SCHEMELESS_URL_RE.test(value);
|
|
931
|
+
}
|
|
932
|
+
const REVOKE_INDIVIDUAL_QURL_RECOVERY = "revoke individual tokens with revokeResourceQurl(crid, qurlId)";
|
|
933
|
+
const DELETE_RESOURCE_RECOVERY = "pass a CRID returned by the API; to revoke one qURL, call revokeResourceQurl(crid, qurlId)";
|
|
934
|
+
const RESOURCE_OR_QURL_ID_PATH_OPTIONS = {
|
|
935
|
+
requireCridUnlessQurlDisplayId: true,
|
|
936
|
+
};
|
|
937
|
+
const QURL_ID_PATH_OPTIONS = {
|
|
938
|
+
rejectBareAccessToken: true,
|
|
939
|
+
accessTokenRecovery: "pass the qURL display ID, not its access token",
|
|
940
|
+
};
|
|
839
941
|
/**
|
|
840
942
|
* Validates that a path-parameter argument is a non-empty string. Some callers
|
|
841
943
|
* pass resource/qURL display IDs, while newer endpoints also pass domains,
|
|
842
944
|
* session IDs, webhook IDs, and API-key IDs, so this intentionally enforces
|
|
843
|
-
* only basic shape and leaves endpoint-specific
|
|
945
|
+
* only basic shape plus transport-safety rules and leaves endpoint-specific
|
|
946
|
+
* identifier grammar to the service.
|
|
844
947
|
*/
|
|
845
|
-
function
|
|
948
|
+
function validatePathId(id, method, field = "id", options = {}) {
|
|
949
|
+
if (options.requireCridUnlessQurlDisplayId && typeof id === "string" && !id.startsWith("q_")) {
|
|
950
|
+
requireResourceCrid(id, method, field);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
846
953
|
// `.trim()` catches whitespace-only and padded IDs before they round-trip as
|
|
847
954
|
// `%20...%20` paths that the server can only reject with a less useful 404.
|
|
848
955
|
// The pre-flight error is more actionable than the 404.
|
|
849
|
-
if (typeof id !== "string"
|
|
956
|
+
if (typeof id !== "string") {
|
|
957
|
+
const observedType = id === null ? "null" : typeof id;
|
|
958
|
+
throw clientValidationError(`${method}: ${field} is required (got ${observedType})`);
|
|
959
|
+
}
|
|
960
|
+
if (id.trim() === "") {
|
|
850
961
|
throw clientValidationError(`${method}: ${field} is required`);
|
|
851
962
|
}
|
|
852
963
|
if (id.trim() !== id) {
|
|
853
964
|
throw clientValidationError(`${method}: ${field} must not include leading or trailing whitespace`);
|
|
854
965
|
}
|
|
966
|
+
if (id.length > MAX_PATH_ID_LENGTH) {
|
|
967
|
+
throw clientValidationError(`${method}: ${field} must be ${MAX_PATH_ID_LENGTH} characters or fewer`);
|
|
968
|
+
}
|
|
969
|
+
const decodedIds = decodedPathIdForms(id);
|
|
970
|
+
// URL resolvers and intermediaries can normalize raw or pre-encoded dot
|
|
971
|
+
// segments before routing. Reject every decoded form we inspected so an
|
|
972
|
+
// item operation cannot be retargeted to a collection endpoint. Embedded
|
|
973
|
+
// `a/../b` remains an opaque single segment because the later URL builder
|
|
974
|
+
// percent-encodes its slash.
|
|
975
|
+
if (decodedIds.some((value) => value === "." || value === "..")) {
|
|
976
|
+
throw clientValidationError(`${method}: ${field} is an invalid URL path segment`);
|
|
977
|
+
}
|
|
978
|
+
// `/` is deliberately outside the general delimiter class so a URL path
|
|
979
|
+
// such as `/at_a_glance` is not misread as a credential. For non-URL input,
|
|
980
|
+
// `/at_` remains a strong pasted-path signal. Fragment, query, header, and
|
|
981
|
+
// pasted `Bearer at_...` / `token:at_...` forms are also strong signals.
|
|
982
|
+
// Bare `at_...` values are rejected only for resource/qURL identifiers:
|
|
983
|
+
// those current formats have fixed non-colliding prefixes, while domain,
|
|
984
|
+
// webhook, session, and API-key IDs are deliberately opaque/open contracts.
|
|
985
|
+
// Full URLs and delimiter/path-prefixed credentials remain rejected in
|
|
986
|
+
// every namespace.
|
|
987
|
+
const containsUrl = decodedIds.some(isUrlLikePathId);
|
|
988
|
+
const containsAccessToken = decodedIds.some((value) => {
|
|
989
|
+
const valueIsUrl = isUrlLikePathId(value);
|
|
990
|
+
return ((QURL_LINK_URL_RE.test(value) &&
|
|
991
|
+
(PATH_EMBEDDED_ACCESS_TOKEN_RE.test(value) || PATH_PREFIXED_ACCESS_TOKEN_RE.test(value))) ||
|
|
992
|
+
(valueIsUrl
|
|
993
|
+
? URL_EMBEDDED_ACCESS_TOKEN_RE.test(value)
|
|
994
|
+
: PATH_EMBEDDED_ACCESS_TOKEN_RE.test(value) || PATH_PREFIXED_ACCESS_TOKEN_RE.test(value)) ||
|
|
995
|
+
(options.rejectBareAccessToken === true && value.slice(0, 3).toLowerCase() === "at_"));
|
|
996
|
+
});
|
|
997
|
+
if (containsAccessToken) {
|
|
998
|
+
const recovery = options.accessTokenRecovery ?? "pass the identifier returned by the API";
|
|
999
|
+
throw clientValidationError(`${method}: ${field} must not contain an access token; ${recovery}`);
|
|
1000
|
+
}
|
|
1001
|
+
// A full target/qURL is a common argument mix-up, but it is distinct from
|
|
1002
|
+
// passing an access-token credential. Diagnose it accurately without
|
|
1003
|
+
// echoing the URL, which may itself contain sensitive query parameters.
|
|
1004
|
+
if (containsUrl) {
|
|
1005
|
+
throw clientValidationError(`${method}: ${field} must be an identifier, not a URL`);
|
|
1006
|
+
}
|
|
1007
|
+
if (options.rejectQurlDisplayId === true &&
|
|
1008
|
+
decodedIds.some((value) => QURL_DISPLAY_ID_PREFIX_RE.test(value))) {
|
|
1009
|
+
throw clientValidationError(`${method}: ${field} must not be a qURL display ID; ${REVOKE_INDIVIDUAL_QURL_RECOVERY}`);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
function decodedPathIdForms(id) {
|
|
1013
|
+
// Probe repeatedly encoded input too. Callers sometimes pre-encode a copied
|
|
1014
|
+
// link before passing it through a URL builder that encodes it again. Three
|
|
1015
|
+
// passes cover the common accidental cases without allowing hostile input
|
|
1016
|
+
// to create unbounded work. More deeply encoded or malformed-percent input
|
|
1017
|
+
// deliberately fails open to the authoritative service after safe segment
|
|
1018
|
+
// encoding; this heuristic is not an identifier parser.
|
|
1019
|
+
const decodedIds = [id];
|
|
1020
|
+
let decodedId = id;
|
|
1021
|
+
for (let pass = 0; pass < MAX_PATH_ID_DECODE_PASSES; pass += 1) {
|
|
1022
|
+
try {
|
|
1023
|
+
const next = decodeURIComponent(decodedId);
|
|
1024
|
+
if (next === decodedId)
|
|
1025
|
+
break;
|
|
1026
|
+
decodedIds.push(next);
|
|
1027
|
+
decodedId = next;
|
|
1028
|
+
}
|
|
1029
|
+
catch {
|
|
1030
|
+
// Malformed percent escapes remain opaque and are left to the service.
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return decodedIds;
|
|
855
1035
|
}
|
|
856
1036
|
function requireConnectorSlug(slug, method) {
|
|
857
1037
|
if (typeof slug !== "string" || !CONNECTOR_SLUG_PATTERN.test(slug)) {
|
|
@@ -898,29 +1078,23 @@ function decodeCanonicalBase64Url(value) {
|
|
|
898
1078
|
return undefined;
|
|
899
1079
|
}
|
|
900
1080
|
}
|
|
901
|
-
async function isValidConnectorResourceId(value) {
|
|
902
|
-
const der = decodeCanonicalBase64Url(value);
|
|
903
|
-
if (!der)
|
|
904
|
-
return false;
|
|
905
|
-
if (!globalThis.crypto?.subtle) {
|
|
906
|
-
throw new errors_js_1.RuntimeError("qURL Connector resource validation requires the Web Crypto SubtleCrypto API");
|
|
907
|
-
}
|
|
908
|
-
return importsAsConnectorPublicKey(der);
|
|
909
|
-
}
|
|
910
1081
|
function requireConnectorSubtleCrypto(method) {
|
|
911
1082
|
if (!globalThis.crypto?.subtle) {
|
|
912
1083
|
throw new errors_js_1.RuntimeError(`${method}: requires the Web Crypto SubtleCrypto API`);
|
|
913
1084
|
}
|
|
914
1085
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1086
|
+
function requireResourceCrid(crid, method, field = "id") {
|
|
1087
|
+
if ((0, crid_js_1.parseCrid)(crid))
|
|
1088
|
+
return;
|
|
1089
|
+
// Keep actionable paste errors without inspecting valid CRIDs twice.
|
|
1090
|
+
validatePathId(crid, method, field, {
|
|
1091
|
+
rejectBareAccessToken: true,
|
|
1092
|
+
rejectQurlDisplayId: true,
|
|
1093
|
+
accessTokenRecovery: method === "delete" || method === "deleteResource"
|
|
1094
|
+
? DELETE_RESOURCE_RECOVERY
|
|
1095
|
+
: "pass a CRID returned by the API",
|
|
1096
|
+
});
|
|
1097
|
+
throw clientValidationError(`${method}: requires a valid CRID`);
|
|
924
1098
|
}
|
|
925
1099
|
async function importsAsConnectorPublicKey(der) {
|
|
926
1100
|
try {
|
|
@@ -939,23 +1113,28 @@ function containsControlCharacter(value) {
|
|
|
939
1113
|
return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
940
1114
|
});
|
|
941
1115
|
}
|
|
942
|
-
async function
|
|
943
|
-
if (typeof
|
|
944
|
-
throw unexpectedResponseError(`${method}: response
|
|
1116
|
+
async function verifyResourceIdentity(resource, method, expectedCrid) {
|
|
1117
|
+
if (!resource || typeof resource.resource_id !== "string") {
|
|
1118
|
+
throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
|
|
945
1119
|
}
|
|
946
|
-
const
|
|
947
|
-
|
|
1120
|
+
const resourceKey = decodeCanonicalBase64Url(resource.resource_id);
|
|
1121
|
+
requireConnectorSubtleCrypto(method);
|
|
1122
|
+
if (!resourceKey || !(await importsAsConnectorPublicKey(resourceKey))) {
|
|
948
1123
|
throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
|
|
949
1124
|
}
|
|
950
|
-
if (
|
|
951
|
-
throw unexpectedResponseError(`${method}: response
|
|
1125
|
+
if (expectedCrid !== undefined && resource.crid !== expectedCrid) {
|
|
1126
|
+
throw unexpectedResponseError(`${method}: response crid does not match the request`);
|
|
952
1127
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
if (expectation.resourceId === undefined &&
|
|
956
|
-
!(await isValidConnectorResourceId(resource.resource_id))) {
|
|
957
|
-
throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
|
|
1128
|
+
if (!(await (0, crid_js_1.cridKeyMatches)(resource.crid, resourceKey))) {
|
|
1129
|
+
throw unexpectedResponseError(`${method}: response has missing, invalid, or public-key-mismatched crid`);
|
|
958
1130
|
}
|
|
1131
|
+
}
|
|
1132
|
+
async function parseConnectorResource(client, value, method, expectation = {}) {
|
|
1133
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1134
|
+
throw unexpectedResponseError(`${method}: response is missing connector resource data`);
|
|
1135
|
+
}
|
|
1136
|
+
const resource = value;
|
|
1137
|
+
await verifyResourceIdentity(resource, method, expectation.crid);
|
|
959
1138
|
if (typeof resource.connector_routing_id !== "string" ||
|
|
960
1139
|
!CONNECTOR_ROUTING_ID_PATTERN.test(resource.connector_routing_id)) {
|
|
961
1140
|
throw unexpectedResponseError(`${method}: response has missing or invalid connector_routing_id`);
|
|
@@ -988,12 +1167,6 @@ async function parseConnectorResource(client, value, method, expectation = {}) {
|
|
|
988
1167
|
// could not create or update.
|
|
989
1168
|
throw unexpectedResponseError(`${method}: response has invalid alias`);
|
|
990
1169
|
}
|
|
991
|
-
if (resource.crid !== undefined && typeof resource.crid !== "string") {
|
|
992
|
-
throw unexpectedResponseError(`${method}: response has invalid crid`);
|
|
993
|
-
}
|
|
994
|
-
// Match qurl-go: ConnectorResource carries an optional producer CRID
|
|
995
|
-
// verbatim. Consumers that possess a trusted delivered key can verify the
|
|
996
|
-
// binding separately; this management-plane row is not itself a trust root.
|
|
997
1170
|
if (resource.status === "revoked") {
|
|
998
1171
|
if (expectation.revokedIsLifecycleError !== true) {
|
|
999
1172
|
throw unexpectedResponseError(`${method}: active-only operation returned a revoked connector resource`);
|
|
@@ -1110,6 +1283,289 @@ function requireValidAccessPolicy(policy) {
|
|
|
1110
1283
|
}
|
|
1111
1284
|
}
|
|
1112
1285
|
}
|
|
1286
|
+
function validateCreateDelegatedQurlBatchInput(input) {
|
|
1287
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
1288
|
+
throw clientValidationError(`createDelegatedQurlBatch: input must be an object (got ${describeShape(input)})`);
|
|
1289
|
+
}
|
|
1290
|
+
const typed = input;
|
|
1291
|
+
requireNoUnknownFields(typed, DELEGATED_BATCH_INPUT_KEYS, "createDelegatedQurlBatch");
|
|
1292
|
+
if (typeof typed.mint_capability !== "string" ||
|
|
1293
|
+
typed.mint_capability.length < 1 ||
|
|
1294
|
+
typed.mint_capability.length > MAX_DELEGATED_MINT_CAPABILITY) {
|
|
1295
|
+
throw clientValidationError(`createDelegatedQurlBatch: mint_capability must be a string of 1-${MAX_DELEGATED_MINT_CAPABILITY} characters`);
|
|
1296
|
+
}
|
|
1297
|
+
if (!Array.isArray(typed.grants) ||
|
|
1298
|
+
typed.grants.length < 1 ||
|
|
1299
|
+
typed.grants.length > MAX_DELEGATED_BATCH_ITEMS) {
|
|
1300
|
+
throw clientValidationError(`createDelegatedQurlBatch: grants must contain 1-${MAX_DELEGATED_BATCH_ITEMS} items`);
|
|
1301
|
+
}
|
|
1302
|
+
for (const [index, grant] of typed.grants.entries()) {
|
|
1303
|
+
const field = `createDelegatedQurlBatch: grants[${index}]`;
|
|
1304
|
+
if (typeof grant !== "object" || grant === null || Array.isArray(grant)) {
|
|
1305
|
+
throw clientValidationError(`${field}: must be an object (got ${describeShape(grant)})`);
|
|
1306
|
+
}
|
|
1307
|
+
requireNoUnknownFields(grant, DELEGATED_BATCH_GRANT_KEYS, field);
|
|
1308
|
+
requireMaxUtf8Bytes(grant.label, `${field}.label`, MAX_LABEL);
|
|
1309
|
+
requireBooleanIfPresent(grant.one_time_use, `${field}.one_time_use`);
|
|
1310
|
+
requireMaxSessionsInRange(grant.max_sessions, `${field}.max_sessions`);
|
|
1311
|
+
for (const durationField of ["expires_in", "session_duration"]) {
|
|
1312
|
+
const value = grant[durationField];
|
|
1313
|
+
if (value !== undefined && (typeof value !== "string" || value.length === 0)) {
|
|
1314
|
+
throw clientValidationError(`${field}.${durationField}: must be a non-empty string`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (grant.access_policy !== undefined) {
|
|
1318
|
+
requireValidAccessPolicy(grant.access_policy);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function validateDelegatedBatchCreateOptions(options) {
|
|
1323
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
1324
|
+
throw clientValidationError("createDelegatedQurlBatch: options are required");
|
|
1325
|
+
}
|
|
1326
|
+
requireNoUnknownFields(options, DELEGATED_BATCH_CREATE_OPTION_KEYS, "createDelegatedQurlBatch options");
|
|
1327
|
+
const key = options.idempotencyKey;
|
|
1328
|
+
if (typeof key !== "string" ||
|
|
1329
|
+
key.length < MIN_DELEGATED_BATCH_IDEMPOTENCY_KEY ||
|
|
1330
|
+
key.length > MAX_IDEMPOTENCY_KEY ||
|
|
1331
|
+
!/^[!-~]+$/.test(key)) {
|
|
1332
|
+
throw clientValidationError(`createDelegatedQurlBatch: idempotencyKey must contain ${MIN_DELEGATED_BATCH_IDEMPOTENCY_KEY}-${MAX_IDEMPOTENCY_KEY} visible ASCII characters`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
function validateGetDelegatedQurlBatchOptions(options) {
|
|
1336
|
+
if (options === undefined)
|
|
1337
|
+
return;
|
|
1338
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
1339
|
+
throw clientValidationError(`getDelegatedQurlBatch: options must be an object (got ${describeShape(options)})`);
|
|
1340
|
+
}
|
|
1341
|
+
requireNoUnknownFields(options, DELEGATED_BATCH_GET_OPTION_KEYS, "getDelegatedQurlBatch options");
|
|
1342
|
+
const etag = options.etag;
|
|
1343
|
+
if (etag !== undefined &&
|
|
1344
|
+
(typeof etag !== "string" ||
|
|
1345
|
+
etag.length > MAX_DELEGATED_ETAG ||
|
|
1346
|
+
!STRONG_ETAG_PATTERN.test(etag))) {
|
|
1347
|
+
throw clientValidationError("getDelegatedQurlBatch: etag must be a valid strong ETag");
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function delegatedResponseError(status, detail, requestId) {
|
|
1351
|
+
return unexpectedResponseError(`Delegated qURL batch ${detail}`, { status, requestId });
|
|
1352
|
+
}
|
|
1353
|
+
function delegatedResponseObject(value, status, label, requestId) {
|
|
1354
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1355
|
+
throw delegatedResponseError(status, `${label} is not an object`, requestId);
|
|
1356
|
+
}
|
|
1357
|
+
return value;
|
|
1358
|
+
}
|
|
1359
|
+
function delegatedEnvelope(envelope, status) {
|
|
1360
|
+
if (typeof envelope.meta !== "object" || envelope.meta === null || Array.isArray(envelope.meta)) {
|
|
1361
|
+
throw delegatedResponseError(status, "response meta is not an object");
|
|
1362
|
+
}
|
|
1363
|
+
const meta = envelope.meta;
|
|
1364
|
+
if (typeof meta.request_id !== "string") {
|
|
1365
|
+
throw delegatedResponseError(status, "response meta has invalid request_id");
|
|
1366
|
+
}
|
|
1367
|
+
const data = delegatedResponseObject(envelope.data, status, "response data", meta.request_id);
|
|
1368
|
+
return { data, requestId: meta.request_id };
|
|
1369
|
+
}
|
|
1370
|
+
function requiredDelegatedHeader(envelope, name, status, requestId) {
|
|
1371
|
+
const headers = envelope.__http_headers;
|
|
1372
|
+
if (!headers || typeof headers.get !== "function") {
|
|
1373
|
+
throw delegatedResponseError(status, "response headers are unavailable", requestId);
|
|
1374
|
+
}
|
|
1375
|
+
const value = headers.get(name);
|
|
1376
|
+
if (value === null) {
|
|
1377
|
+
throw delegatedResponseError(status, `response is missing ${name}`, requestId);
|
|
1378
|
+
}
|
|
1379
|
+
return value;
|
|
1380
|
+
}
|
|
1381
|
+
function hasDelegatedNoStore(value) {
|
|
1382
|
+
if (value === null)
|
|
1383
|
+
return false;
|
|
1384
|
+
const directives = new Set(value.split(",").map((directive) => directive.trim().split("=", 1)[0].toLowerCase()));
|
|
1385
|
+
return directives.has("private") && directives.has("no-store");
|
|
1386
|
+
}
|
|
1387
|
+
function delegatedResponseHeaders(envelope, status, requestId) {
|
|
1388
|
+
const cacheControl = envelope.__http_headers?.get("Cache-Control") ?? null;
|
|
1389
|
+
if (cacheControl !== null) {
|
|
1390
|
+
if (!hasDelegatedNoStore(cacheControl)) {
|
|
1391
|
+
throw delegatedResponseError(status, "response has invalid Cache-Control", requestId);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
else if (status !== 304) {
|
|
1395
|
+
requiredDelegatedHeader(envelope, "Cache-Control", status, requestId);
|
|
1396
|
+
}
|
|
1397
|
+
const etag = envelope.__http_headers?.get("ETag") ?? undefined;
|
|
1398
|
+
if (etag !== undefined && (etag.length > MAX_DELEGATED_ETAG || !STRONG_ETAG_PATTERN.test(etag))) {
|
|
1399
|
+
throw delegatedResponseError(status, "response has invalid strong ETag", requestId);
|
|
1400
|
+
}
|
|
1401
|
+
const rawRetryAfter = envelope.__http_headers?.get("Retry-After") ?? "";
|
|
1402
|
+
const trimmed = rawRetryAfter.trim();
|
|
1403
|
+
const seconds = /^\d+$/.test(trimmed)
|
|
1404
|
+
? Number(trimmed)
|
|
1405
|
+
: /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(trimmed)
|
|
1406
|
+
? Math.ceil((Date.parse(trimmed) - Date.now()) / 1000)
|
|
1407
|
+
: NaN;
|
|
1408
|
+
const retryAfter = Number.isSafeInteger(seconds) && seconds > 0 && seconds <= RETRY_AFTER_PARSE_LIMIT_S
|
|
1409
|
+
? seconds
|
|
1410
|
+
: undefined;
|
|
1411
|
+
return { etag, retryAfter };
|
|
1412
|
+
}
|
|
1413
|
+
function isUtcTimestamp(value) {
|
|
1414
|
+
return (typeof value === "string" &&
|
|
1415
|
+
UTC_TIMESTAMP_PATTERN.test(value) &&
|
|
1416
|
+
Number.isFinite(Date.parse(value)));
|
|
1417
|
+
}
|
|
1418
|
+
function delegatedBatchBase(data, status, requestId, expectedBatchId) {
|
|
1419
|
+
if (typeof data.batch_id !== "string" ||
|
|
1420
|
+
!DELEGATED_BATCH_ID_PATTERN.test(data.batch_id) ||
|
|
1421
|
+
(expectedBatchId !== undefined && data.batch_id !== expectedBatchId)) {
|
|
1422
|
+
throw delegatedResponseError(status, "response has invalid batch_id", requestId);
|
|
1423
|
+
}
|
|
1424
|
+
if (!Number.isInteger(data.item_count) ||
|
|
1425
|
+
Number(data.item_count) < 1 ||
|
|
1426
|
+
Number(data.item_count) > MAX_DELEGATED_BATCH_ITEMS) {
|
|
1427
|
+
throw delegatedResponseError(status, "response has invalid item_count", requestId);
|
|
1428
|
+
}
|
|
1429
|
+
if (!isUtcTimestamp(data.submitted_at)) {
|
|
1430
|
+
throw delegatedResponseError(status, "response has invalid submitted_at", requestId);
|
|
1431
|
+
}
|
|
1432
|
+
return {
|
|
1433
|
+
batchId: data.batch_id,
|
|
1434
|
+
itemCount: Number(data.item_count),
|
|
1435
|
+
submittedAt: data.submitted_at,
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
function validateDelegatedLocation(value, baseUrl, batchId, status, requestId) {
|
|
1439
|
+
let location;
|
|
1440
|
+
try {
|
|
1441
|
+
const base = new URL(`${baseUrl}/`);
|
|
1442
|
+
const relative = value.startsWith("/v1/delegated-qurl-batches/")
|
|
1443
|
+
? `${base.pathname.replace(/\/$/, "")}${value}`
|
|
1444
|
+
: value;
|
|
1445
|
+
location = new URL(relative, base);
|
|
1446
|
+
}
|
|
1447
|
+
catch {
|
|
1448
|
+
throw delegatedResponseError(status, "response has invalid Location", requestId);
|
|
1449
|
+
}
|
|
1450
|
+
const expected = new URL(`${baseUrl}/v1/delegated-qurl-batches/${batchId}`);
|
|
1451
|
+
if (location.username !== "" ||
|
|
1452
|
+
location.password !== "" ||
|
|
1453
|
+
location.search !== "" ||
|
|
1454
|
+
location.hash !== "" ||
|
|
1455
|
+
location.href !== expected.href) {
|
|
1456
|
+
throw delegatedResponseError(status, "response has invalid Location", requestId);
|
|
1457
|
+
}
|
|
1458
|
+
return location.href;
|
|
1459
|
+
}
|
|
1460
|
+
function validateDelegatedQurlLink(value) {
|
|
1461
|
+
if (typeof value !== "string")
|
|
1462
|
+
return false;
|
|
1463
|
+
try {
|
|
1464
|
+
const link = new URL(value);
|
|
1465
|
+
return (link.protocol === "https:" &&
|
|
1466
|
+
link.username === "" &&
|
|
1467
|
+
link.password === "" &&
|
|
1468
|
+
link.hash.length > 1);
|
|
1469
|
+
}
|
|
1470
|
+
catch {
|
|
1471
|
+
return false;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
function validateDelegatedQurl(value, expectedQurlId, status, requestId) {
|
|
1475
|
+
const data = delegatedResponseObject(value, status, "response data", requestId);
|
|
1476
|
+
if (data.qurl_id !== expectedQurlId) {
|
|
1477
|
+
throw delegatedResponseError(status, "response has invalid qurl_id", requestId);
|
|
1478
|
+
}
|
|
1479
|
+
if (data.status !== "active" &&
|
|
1480
|
+
data.status !== "consumed" &&
|
|
1481
|
+
data.status !== "expired" &&
|
|
1482
|
+
data.status !== "revoked") {
|
|
1483
|
+
throw delegatedResponseError(status, "response has invalid status", requestId);
|
|
1484
|
+
}
|
|
1485
|
+
if (!isUtcTimestamp(data.expires_at) ||
|
|
1486
|
+
typeof data.one_time_use !== "boolean" ||
|
|
1487
|
+
!Number.isInteger(data.max_sessions) ||
|
|
1488
|
+
Number(data.max_sessions) < 0 ||
|
|
1489
|
+
!Number.isInteger(data.session_duration) ||
|
|
1490
|
+
Number(data.session_duration) <= 0 ||
|
|
1491
|
+
(data.label !== undefined && typeof data.label !== "string") ||
|
|
1492
|
+
(data.access_policy !== undefined &&
|
|
1493
|
+
(typeof data.access_policy !== "object" ||
|
|
1494
|
+
data.access_policy === null ||
|
|
1495
|
+
Array.isArray(data.access_policy)))) {
|
|
1496
|
+
throw delegatedResponseError(status, "response has invalid qURL metadata", requestId);
|
|
1497
|
+
}
|
|
1498
|
+
return { ...data, request_id: requestId };
|
|
1499
|
+
}
|
|
1500
|
+
function validateDelegatedBatchResults(value, itemCount, batchStatus, status, requestId) {
|
|
1501
|
+
if (!Array.isArray(value) || value.length !== itemCount) {
|
|
1502
|
+
throw delegatedResponseError(status, "terminal response has invalid results", requestId);
|
|
1503
|
+
}
|
|
1504
|
+
let succeeded = 0;
|
|
1505
|
+
const qurlIds = new Set();
|
|
1506
|
+
const links = new Set();
|
|
1507
|
+
const results = value.map((raw, index) => {
|
|
1508
|
+
const item = delegatedResponseObject(raw, status, `result ${index}`, requestId);
|
|
1509
|
+
if (item.index !== index) {
|
|
1510
|
+
throw delegatedResponseError(status, `result ${index} has invalid index`, requestId);
|
|
1511
|
+
}
|
|
1512
|
+
if (item.status === "succeeded") {
|
|
1513
|
+
if (item.error !== undefined && item.error !== null) {
|
|
1514
|
+
throw delegatedResponseError(status, `result ${index} has conflicting fields`, requestId);
|
|
1515
|
+
}
|
|
1516
|
+
const qurl = delegatedResponseObject(item.qurl, status, `result ${index} qurl`, requestId);
|
|
1517
|
+
if (typeof qurl.qurl_id !== "string" ||
|
|
1518
|
+
!DELEGATED_QURL_ID_PATTERN.test(qurl.qurl_id) ||
|
|
1519
|
+
!validateDelegatedQurlLink(qurl.qurl_link) ||
|
|
1520
|
+
!isUtcTimestamp(qurl.expires_at) ||
|
|
1521
|
+
qurlIds.has(qurl.qurl_id) ||
|
|
1522
|
+
links.has(qurl.qurl_link)) {
|
|
1523
|
+
throw delegatedResponseError(status, `result ${index} has invalid qurl`, requestId);
|
|
1524
|
+
}
|
|
1525
|
+
qurlIds.add(qurl.qurl_id);
|
|
1526
|
+
links.add(qurl.qurl_link);
|
|
1527
|
+
succeeded++;
|
|
1528
|
+
return {
|
|
1529
|
+
index,
|
|
1530
|
+
status: "succeeded",
|
|
1531
|
+
qurl: {
|
|
1532
|
+
qurl_id: qurl.qurl_id,
|
|
1533
|
+
qurl_link: qurl.qurl_link,
|
|
1534
|
+
expires_at: qurl.expires_at,
|
|
1535
|
+
},
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
if (item.status !== "failed" || (item.qurl !== undefined && item.qurl !== null)) {
|
|
1539
|
+
throw delegatedResponseError(status, `result ${index} has invalid status`, requestId);
|
|
1540
|
+
}
|
|
1541
|
+
const error = delegatedResponseObject(item.error, status, `result ${index} error`, requestId);
|
|
1542
|
+
if (typeof error.code !== "string" ||
|
|
1543
|
+
error.code.length === 0 ||
|
|
1544
|
+
typeof error.message !== "string") {
|
|
1545
|
+
throw delegatedResponseError(status, `result ${index} has invalid error`, requestId);
|
|
1546
|
+
}
|
|
1547
|
+
return {
|
|
1548
|
+
index,
|
|
1549
|
+
status: "failed",
|
|
1550
|
+
error: { code: error.code, message: error.message },
|
|
1551
|
+
};
|
|
1552
|
+
});
|
|
1553
|
+
if ((batchStatus === "succeeded" && succeeded !== itemCount) ||
|
|
1554
|
+
(batchStatus === "failed" && succeeded !== 0) ||
|
|
1555
|
+
(batchStatus === "partially_failed" && (succeeded === 0 || succeeded === itemCount))) {
|
|
1556
|
+
throw delegatedResponseError(status, "terminal status does not match results", requestId);
|
|
1557
|
+
}
|
|
1558
|
+
return results;
|
|
1559
|
+
}
|
|
1560
|
+
function classifyDelegatedBatchCreateFailure(error, batchId) {
|
|
1561
|
+
if (!(error instanceof errors_js_1.QURLError)) {
|
|
1562
|
+
throw new errors_js_1.DelegatedBatchOutcomeUnknownError(new errors_js_1.RuntimeError("Unexpected failure after delegated batch dispatch", { cause: error }), batchId);
|
|
1563
|
+
}
|
|
1564
|
+
if ((error.status >= 400 && error.status < 500 && error.status !== 408) || error.status === 501) {
|
|
1565
|
+
throw error;
|
|
1566
|
+
}
|
|
1567
|
+
throw new errors_js_1.DelegatedBatchOutcomeUnknownError(error, batchId);
|
|
1568
|
+
}
|
|
1113
1569
|
// `format: uri` in the OpenAPI spec allows schemes the SDK doesn't
|
|
1114
1570
|
// usefully support (`ftp://`, `file://`, `javascript:`, …). This is a
|
|
1115
1571
|
// cheap client-side sanity check — the server is still the
|
|
@@ -1378,6 +1834,56 @@ function validateApiKeyWriteFields(input, method, requiredFields = {}) {
|
|
|
1378
1834
|
if (requiredFields.scopes || input.scopes !== undefined) {
|
|
1379
1835
|
requireNonEmptyArrayField(input, "scopes", method);
|
|
1380
1836
|
requireStringArrayElements(input.scopes, "scopes", method);
|
|
1837
|
+
const unsupported = input.scopes.find((scope) => !API_KEY_REQUEST_SCOPES.has(scope));
|
|
1838
|
+
if (unsupported !== undefined) {
|
|
1839
|
+
throw clientValidationError(`${method}: scopes contains an unsupported permission ${JSON.stringify(unsupported)}; this SDK accepts ${API_KEY_REQUEST_SCOPE_VALUES.join(", ")}`);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
function validateEnrollmentTokenFields(input, method) {
|
|
1844
|
+
if (input.target !== undefined &&
|
|
1845
|
+
(typeof input.target !== "string" || !CREDENTIAL_TARGETS.has(input.target))) {
|
|
1846
|
+
throw clientValidationError(`${method}: target must be 'agent' or 'connector'`);
|
|
1847
|
+
}
|
|
1848
|
+
let claimCount = 0;
|
|
1849
|
+
if (input.claims !== undefined) {
|
|
1850
|
+
if (!Array.isArray(input.claims)) {
|
|
1851
|
+
throw clientValidationError(`${method}: claims must be an array`);
|
|
1852
|
+
}
|
|
1853
|
+
if (input.claims.length > 1) {
|
|
1854
|
+
throw clientValidationError(`${method}: claims must contain at most one claim`);
|
|
1855
|
+
}
|
|
1856
|
+
claimCount = input.claims.length;
|
|
1857
|
+
for (const [index, claim] of input.claims.entries()) {
|
|
1858
|
+
if (!claim || typeof claim !== "object" || Array.isArray(claim)) {
|
|
1859
|
+
throw clientValidationError(`${method}: claims[${index}] must be an object`);
|
|
1860
|
+
}
|
|
1861
|
+
requireNoUnknownFields(claim, ["type", "id"], `${method}: claims[${index}]`);
|
|
1862
|
+
const record = claim;
|
|
1863
|
+
if (typeof record.type !== "string" || !CREDENTIAL_CLAIM_TYPES.has(record.type)) {
|
|
1864
|
+
throw clientValidationError(`${method}: claims[${index}].type must be 'connector'`);
|
|
1865
|
+
}
|
|
1866
|
+
if (typeof record.id !== "string" || !CONNECTOR_SLUG_PATTERN.test(record.id)) {
|
|
1867
|
+
throw clientValidationError(`${method}: claims[${index}].id is not a valid connector slug; use 3-64 lowercase letters, digits, or hyphens, starting with a letter and ending with a letter or digit`);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
// CreateApiKeyRequest.target permits an agent with zero or one connector claim.
|
|
1872
|
+
if (input.target === "connector" && claimCount !== 1) {
|
|
1873
|
+
throw clientValidationError(`${method}: target 'connector' requires exactly one claim`);
|
|
1874
|
+
}
|
|
1875
|
+
if (input.expires_in !== undefined) {
|
|
1876
|
+
if (typeof input.expires_in !== "string") {
|
|
1877
|
+
throw clientValidationError(`${method}: expires_in must be a duration string`);
|
|
1878
|
+
}
|
|
1879
|
+
const match = CREDENTIAL_DURATION_PATTERN.exec(input.expires_in);
|
|
1880
|
+
if (!match) {
|
|
1881
|
+
throw clientValidationError(`${method}: expires_in must match <integer><s|m|h|d|w>`);
|
|
1882
|
+
}
|
|
1883
|
+
const seconds = BigInt(match[1]) * CREDENTIAL_DURATION_MULTIPLIERS[match[2]];
|
|
1884
|
+
if (seconds <= 0n || seconds > MAX_ENROLLMENT_TOKEN_TTL_SECONDS) {
|
|
1885
|
+
throw clientValidationError(`${method}: expires_in must be greater than zero and at most 24h`);
|
|
1886
|
+
}
|
|
1381
1887
|
}
|
|
1382
1888
|
}
|
|
1383
1889
|
/**
|
|
@@ -1573,15 +2079,17 @@ function parseApiDate(value) {
|
|
|
1573
2079
|
* check; everything else is optional because the two mint endpoints differ
|
|
1574
2080
|
* in which ancillary fields they return.
|
|
1575
2081
|
*/
|
|
1576
|
-
function parsePortal(data, method) {
|
|
2082
|
+
async function parsePortal(data, method, expectedCrid) {
|
|
1577
2083
|
if (typeof data?.resource_id !== "string" || data.resource_id.trim() === "") {
|
|
1578
2084
|
throw unexpectedResponseError(`${method}: response is missing resource_id`);
|
|
1579
2085
|
}
|
|
1580
2086
|
if (typeof data.qurl_link !== "string" || data.qurl_link.trim() === "") {
|
|
1581
2087
|
throw unexpectedResponseError(`${method}: response is missing qurl_link`);
|
|
1582
2088
|
}
|
|
2089
|
+
await verifyResourceIdentity(data, method, expectedCrid);
|
|
1583
2090
|
return {
|
|
1584
|
-
|
|
2091
|
+
crid: data.crid,
|
|
2092
|
+
resourcePublicKey: data.resource_id,
|
|
1585
2093
|
link: data.qurl_link,
|
|
1586
2094
|
site: data.qurl_site,
|
|
1587
2095
|
expiresAt: parseApiDate(data.expires_at),
|
|
@@ -1928,31 +2436,29 @@ class QURLClient {
|
|
|
1928
2436
|
body.alias = normalized.alias;
|
|
1929
2437
|
validateResourceWriteFields(body);
|
|
1930
2438
|
const resource = await this.request("POST", "/v1/resources", body, requestOptions);
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
}
|
|
1934
|
-
return new ProtectedResource(this, resource.resource_id,
|
|
2439
|
+
await verifyResourceIdentity(resource, "protectUrl");
|
|
2440
|
+
return new ProtectedResource(this, resource.crid,
|
|
1935
2441
|
// Some resource types redact target_url in responses; fall back to
|
|
1936
2442
|
// the caller-supplied URL so the handle stays useful.
|
|
1937
2443
|
resource.target_url || targetUrl, resource);
|
|
1938
2444
|
}
|
|
1939
2445
|
/**
|
|
1940
|
-
* Return a portal-minting handle for a stored
|
|
2446
|
+
* Return a portal-minting handle for a stored CRID, without an API
|
|
1941
2447
|
* call.
|
|
1942
2448
|
*
|
|
1943
|
-
* Use when you persisted a LayerV
|
|
1944
|
-
* portals for it (qurl-go: `Client.
|
|
2449
|
+
* Use when you persisted a LayerV CRID and want to mint more
|
|
2450
|
+
* portals for it (qurl-go: `Client.ResourceByCRID`):
|
|
1945
2451
|
*
|
|
1946
2452
|
* ```ts
|
|
1947
|
-
* const resource = client.
|
|
2453
|
+
* const resource = client.resourceByCrid(storedCrid);
|
|
1948
2454
|
* const portal = await resource.createPortal({ validFor: "1h" });
|
|
1949
2455
|
* ```
|
|
1950
2456
|
*
|
|
1951
2457
|
* The handle carries no server metadata; use {@link getResource} when you
|
|
1952
2458
|
* need the full resource details.
|
|
1953
2459
|
*/
|
|
1954
|
-
|
|
1955
|
-
|
|
2460
|
+
resourceByCrid(id) {
|
|
2461
|
+
requireResourceCrid(id, "resourceByCrid");
|
|
1956
2462
|
return new ProtectedResource(this, id);
|
|
1957
2463
|
}
|
|
1958
2464
|
/** Find or create the active qURL Connector resource for an immutable slug. */
|
|
@@ -2000,15 +2506,16 @@ class QURLClient {
|
|
|
2000
2506
|
}
|
|
2001
2507
|
return { resource, foundExisting };
|
|
2002
2508
|
}
|
|
2003
|
-
/** Fetch a qURL Connector resource by its
|
|
2004
|
-
async getConnectorResource(
|
|
2005
|
-
|
|
2006
|
-
|
|
2509
|
+
/** Fetch a qURL Connector resource by its CRID. */
|
|
2510
|
+
async getConnectorResource(crid) {
|
|
2511
|
+
requireResourceCrid(crid, "getConnectorResource");
|
|
2512
|
+
requireConnectorSubtleCrypto("getConnectorResource");
|
|
2513
|
+
const { data, __http_status } = await this.rawRequest("GET", `/v1/resources/${encodeURIComponent(crid)}`, undefined, { retry: false });
|
|
2007
2514
|
if (__http_status !== 200) {
|
|
2008
2515
|
throw unexpectedResponseError(`getConnectorResource: expected HTTP 200, got ${__http_status ?? "unknown"}`);
|
|
2009
2516
|
}
|
|
2010
2517
|
return parseConnectorResource(this, data?.resource, "getConnectorResource", {
|
|
2011
|
-
|
|
2518
|
+
crid,
|
|
2012
2519
|
revokedIsLifecycleError: true,
|
|
2013
2520
|
});
|
|
2014
2521
|
}
|
|
@@ -2046,15 +2553,15 @@ class QURLClient {
|
|
|
2046
2553
|
return parseConnectorResource(this, data[0], "getConnectorResourceBySlug", { slug });
|
|
2047
2554
|
}
|
|
2048
2555
|
/**
|
|
2049
|
-
* Revoke a qURL Connector resource by
|
|
2556
|
+
* Revoke a qURL Connector resource by CRID.
|
|
2050
2557
|
*
|
|
2051
2558
|
* The API does not apply Idempotency-Key replay to DELETE. After an
|
|
2052
2559
|
* outcome-unknown failure, reconcile by ID before issuing a deliberate retry.
|
|
2053
2560
|
*/
|
|
2054
|
-
async deleteConnectorResource(
|
|
2055
|
-
|
|
2561
|
+
async deleteConnectorResource(crid) {
|
|
2562
|
+
requireResourceCrid(crid, "deleteConnectorResource");
|
|
2056
2563
|
try {
|
|
2057
|
-
await this.requestNoContent(`/v1/resources/${encodeURIComponent(
|
|
2564
|
+
await this.requestNoContent(`/v1/resources/${encodeURIComponent(crid)}`);
|
|
2058
2565
|
}
|
|
2059
2566
|
catch (error) {
|
|
2060
2567
|
classifyConnectorMutationFailure("delete", error);
|
|
@@ -2067,7 +2574,7 @@ class QURLClient {
|
|
|
2067
2574
|
* reach one private resource. Recipients open `portal.link` directly and
|
|
2068
2575
|
* need no LayerV credentials. Prefer short lifetimes such as
|
|
2069
2576
|
* `{ validFor: "5m" }`. Accepts a {@link ProtectedResource} handle or a
|
|
2070
|
-
*
|
|
2577
|
+
* CRID string. REST-shaped equivalents: {@link mintLink} /
|
|
2071
2578
|
* {@link createQurlForResource}.
|
|
2072
2579
|
*
|
|
2073
2580
|
* Duration options take a string (`"5m"`, `"24h"`; server-validated) or a
|
|
@@ -2086,22 +2593,22 @@ class QURLClient {
|
|
|
2086
2593
|
if (!ProtectedResource.isBoundTo(resource, this)) {
|
|
2087
2594
|
throw clientValidationError("createPortal: resource is bound to a different client");
|
|
2088
2595
|
}
|
|
2089
|
-
resourceId = resource.
|
|
2596
|
+
resourceId = resource.crid;
|
|
2090
2597
|
}
|
|
2091
2598
|
else {
|
|
2092
|
-
throw clientValidationError(`createPortal: resource must be a ProtectedResource handle or a
|
|
2599
|
+
throw clientValidationError(`createPortal: resource must be a ProtectedResource handle or a CRID string (got ${describeShape(resource)})`);
|
|
2093
2600
|
}
|
|
2094
|
-
|
|
2601
|
+
requireResourceCrid(resourceId, "createPortal");
|
|
2095
2602
|
const body = buildCreatePortalBody(opts, "createPortal");
|
|
2096
2603
|
const data = await this.request("POST", `/v1/resources/${encodeURIComponent(resourceId)}/qurls`, body, requestOptions);
|
|
2097
|
-
return parsePortal(data, "createPortal");
|
|
2604
|
+
return parsePortal(data, "createPortal", resourceId);
|
|
2098
2605
|
}
|
|
2099
2606
|
/**
|
|
2100
2607
|
* Protect `targetUrl` and mint a portal in one API call.
|
|
2101
2608
|
*
|
|
2102
2609
|
* Convenience for one-off scripts (qurl-go: `Client.CreatePortalForURL`).
|
|
2103
2610
|
* The returned resource handle is reusable for minting more portals but
|
|
2104
|
-
* carries
|
|
2611
|
+
* carries the CRID, verification key, and the caller-supplied target URL; use
|
|
2105
2612
|
* {@link protectUrl} when you need the full server-populated resource
|
|
2106
2613
|
* metadata. Portal options match {@link createPortal}; `targetUrl` gets
|
|
2107
2614
|
* the same malformed-URL and embedded-credentials rejection as
|
|
@@ -2116,10 +2623,10 @@ class QURLClient {
|
|
|
2116
2623
|
...buildCreatePortalBody(opts, "createPortalForUrl"),
|
|
2117
2624
|
};
|
|
2118
2625
|
const data = await this.request("POST", "/v1/qurls", body, requestOptions);
|
|
2119
|
-
const portal = parsePortal(data, "createPortalForUrl");
|
|
2626
|
+
const portal = await parsePortal(data, "createPortalForUrl");
|
|
2120
2627
|
return {
|
|
2121
2628
|
portal,
|
|
2122
|
-
resource: new ProtectedResource(this, portal.
|
|
2629
|
+
resource: new ProtectedResource(this, portal.crid, targetUrl),
|
|
2123
2630
|
};
|
|
2124
2631
|
}
|
|
2125
2632
|
/**
|
|
@@ -2153,7 +2660,7 @@ class QURLClient {
|
|
|
2153
2660
|
return {
|
|
2154
2661
|
resourceUrl: resolved.target_url,
|
|
2155
2662
|
openSeconds: resolved.access_grant?.expires_in ?? 0,
|
|
2156
|
-
|
|
2663
|
+
resourcePublicKey: resolved.resource_id,
|
|
2157
2664
|
};
|
|
2158
2665
|
}
|
|
2159
2666
|
// --- Public API ---
|
|
@@ -2313,14 +2820,204 @@ class QURLClient {
|
|
|
2313
2820
|
}
|
|
2314
2821
|
return requestId !== undefined ? { ...result, request_id: requestId } : result;
|
|
2315
2822
|
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Start one durable delegated qURL batch. The SDK sends this mutation once.
|
|
2825
|
+
* If the result may have committed, retry explicitly with the same key and
|
|
2826
|
+
* semantic body.
|
|
2827
|
+
*/
|
|
2828
|
+
async createDelegatedQurlBatch(input, options) {
|
|
2829
|
+
validateCreateDelegatedQurlBatchInput(input);
|
|
2830
|
+
validateDelegatedBatchCreateOptions(options);
|
|
2831
|
+
let dispatched = false;
|
|
2832
|
+
let acceptedBatchId;
|
|
2833
|
+
try {
|
|
2834
|
+
const envelope = await this.rawRequest("POST", "/v1/delegated-qurl-batches", input, {
|
|
2835
|
+
requestOptions: options,
|
|
2836
|
+
retry: false,
|
|
2837
|
+
generateIdempotencyKey: false,
|
|
2838
|
+
captureResponseHeaders: true,
|
|
2839
|
+
onDispatch: () => {
|
|
2840
|
+
dispatched = true;
|
|
2841
|
+
},
|
|
2842
|
+
});
|
|
2843
|
+
const status = envelope.__http_status ?? 0;
|
|
2844
|
+
if (status !== 202) {
|
|
2845
|
+
throw delegatedResponseError(status, "create returned an unexpected success status");
|
|
2846
|
+
}
|
|
2847
|
+
const rawData = envelope.data;
|
|
2848
|
+
if (typeof rawData === "object" && rawData !== null && !Array.isArray(rawData)) {
|
|
2849
|
+
const candidate = rawData.batch_id;
|
|
2850
|
+
if (typeof candidate === "string" && DELEGATED_BATCH_ID_PATTERN.test(candidate)) {
|
|
2851
|
+
acceptedBatchId = candidate;
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
const { data, requestId } = delegatedEnvelope(envelope, status);
|
|
2855
|
+
const base = delegatedBatchBase(data, status, requestId);
|
|
2856
|
+
if (data.status !== "queued" || base.itemCount !== input.grants.length) {
|
|
2857
|
+
throw delegatedResponseError(status, "acceptance data is inconsistent", requestId);
|
|
2858
|
+
}
|
|
2859
|
+
const responseHeaders = delegatedResponseHeaders(envelope, status, requestId);
|
|
2860
|
+
const location = validateDelegatedLocation(requiredDelegatedHeader(envelope, "Location", status, requestId), this.baseUrl, base.batchId, status, requestId);
|
|
2861
|
+
return {
|
|
2862
|
+
http_status: 202,
|
|
2863
|
+
batch_id: base.batchId,
|
|
2864
|
+
status: "queued",
|
|
2865
|
+
item_count: base.itemCount,
|
|
2866
|
+
submitted_at: base.submittedAt,
|
|
2867
|
+
etag: responseHeaders.etag,
|
|
2868
|
+
retry_after: responseHeaders.retryAfter,
|
|
2869
|
+
location,
|
|
2870
|
+
request_id: requestId,
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
catch (error) {
|
|
2874
|
+
if (!dispatched)
|
|
2875
|
+
throw error;
|
|
2876
|
+
classifyDelegatedBatchCreateFailure(error, acceptedBatchId);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
/**
|
|
2880
|
+
* Read one delegated qURL batch state. One call performs one logical read;
|
|
2881
|
+
* callers retain control of the poll count and total deadline.
|
|
2882
|
+
*/
|
|
2883
|
+
async getDelegatedQurlBatch(batchId, options) {
|
|
2884
|
+
if (typeof batchId !== "string" || !DELEGATED_BATCH_ID_PATTERN.test(batchId)) {
|
|
2885
|
+
throw clientValidationError("getDelegatedQurlBatch: batchId must match dqb_ plus 22 URL-safe characters");
|
|
2886
|
+
}
|
|
2887
|
+
validateGetDelegatedQurlBatchOptions(options);
|
|
2888
|
+
const envelope = await this.rawRequest("GET", `/v1/delegated-qurl-batches/${encodeURIComponent(batchId)}`, undefined, {
|
|
2889
|
+
passthroughStatuses: DELEGATED_BATCH_PASSTHROUGH_STATUSES,
|
|
2890
|
+
allowEmptyPassthroughBody: true,
|
|
2891
|
+
ifNoneMatch: options?.etag,
|
|
2892
|
+
retry: false,
|
|
2893
|
+
captureResponseHeaders: true,
|
|
2894
|
+
});
|
|
2895
|
+
const status = envelope.__http_status ?? 0;
|
|
2896
|
+
if (status === 304) {
|
|
2897
|
+
if (envelope.__http_body_empty !== true || options?.etag === undefined) {
|
|
2898
|
+
throw delegatedResponseError(status, "304 response is inconsistent");
|
|
2899
|
+
}
|
|
2900
|
+
const responseHeaders = delegatedResponseHeaders(envelope, status);
|
|
2901
|
+
if (responseHeaders.etag !== undefined && responseHeaders.etag !== options.etag) {
|
|
2902
|
+
throw delegatedResponseError(status, "304 response changed the ETag");
|
|
2903
|
+
}
|
|
2904
|
+
return {
|
|
2905
|
+
http_status: 304,
|
|
2906
|
+
etag: responseHeaders.etag ?? options.etag,
|
|
2907
|
+
...(responseHeaders.retryAfter === undefined
|
|
2908
|
+
? {}
|
|
2909
|
+
: { retry_after: responseHeaders.retryAfter }),
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
if (status !== 200 && status !== 202) {
|
|
2913
|
+
throw delegatedResponseError(status, "read returned an unexpected success status");
|
|
2914
|
+
}
|
|
2915
|
+
const { data, requestId } = delegatedEnvelope(envelope, status);
|
|
2916
|
+
const base = delegatedBatchBase(data, status, requestId, batchId);
|
|
2917
|
+
if (status === 202) {
|
|
2918
|
+
if (data.status !== "queued" && data.status !== "running") {
|
|
2919
|
+
throw delegatedResponseError(status, "pending data has a terminal status", requestId);
|
|
2920
|
+
}
|
|
2921
|
+
const responseHeaders = delegatedResponseHeaders(envelope, status, requestId);
|
|
2922
|
+
return {
|
|
2923
|
+
http_status: 202,
|
|
2924
|
+
batch_id: base.batchId,
|
|
2925
|
+
status: data.status,
|
|
2926
|
+
item_count: base.itemCount,
|
|
2927
|
+
submitted_at: base.submittedAt,
|
|
2928
|
+
etag: responseHeaders.etag,
|
|
2929
|
+
retry_after: responseHeaders.retryAfter,
|
|
2930
|
+
request_id: requestId,
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
if (data.status !== "succeeded" &&
|
|
2934
|
+
data.status !== "partially_failed" &&
|
|
2935
|
+
data.status !== "failed") {
|
|
2936
|
+
throw delegatedResponseError(status, "terminal data has a non-terminal status", requestId);
|
|
2937
|
+
}
|
|
2938
|
+
let results;
|
|
2939
|
+
try {
|
|
2940
|
+
if (!isUtcTimestamp(data.completed_at)) {
|
|
2941
|
+
throw delegatedResponseError(status, "terminal data has invalid completed_at", requestId);
|
|
2942
|
+
}
|
|
2943
|
+
results = validateDelegatedBatchResults(data.results, base.itemCount, data.status, status, requestId);
|
|
2944
|
+
}
|
|
2945
|
+
catch (error) {
|
|
2946
|
+
const partialQurlIds = Array.isArray(data.results)
|
|
2947
|
+
? [
|
|
2948
|
+
...new Set(data.results.flatMap((item) => item &&
|
|
2949
|
+
typeof item === "object" &&
|
|
2950
|
+
item.status === "succeeded" &&
|
|
2951
|
+
typeof item.qurl?.qurl_id === "string" &&
|
|
2952
|
+
DELEGATED_QURL_ID_PATTERN.test(item.qurl.qurl_id)
|
|
2953
|
+
? [item.qurl.qurl_id]
|
|
2954
|
+
: [])),
|
|
2955
|
+
]
|
|
2956
|
+
: [];
|
|
2957
|
+
if (error instanceof errors_js_1.QURLError)
|
|
2958
|
+
Object.defineProperty(error, "partialQurlIds", {
|
|
2959
|
+
value: Object.freeze(partialQurlIds),
|
|
2960
|
+
enumerable: true,
|
|
2961
|
+
});
|
|
2962
|
+
throw error;
|
|
2963
|
+
}
|
|
2964
|
+
// This is the caller's only copy of the bearer links. Do not discard a
|
|
2965
|
+
// valid terminal body because an intermediary removed a cache validator;
|
|
2966
|
+
// qurl-service owns the required no-store response policy.
|
|
2967
|
+
const cacheControl = envelope.__http_headers?.get("Cache-Control") ?? null;
|
|
2968
|
+
if (!hasDelegatedNoStore(cacheControl)) {
|
|
2969
|
+
this.log("delegated batch terminal response is missing private, no-store", {
|
|
2970
|
+
batch_id: base.batchId,
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
2973
|
+
const etag = envelope.__http_headers?.get("ETag") ?? undefined;
|
|
2974
|
+
const terminalEtag = etag !== undefined && etag.length <= MAX_DELEGATED_ETAG && STRONG_ETAG_PATTERN.test(etag)
|
|
2975
|
+
? etag
|
|
2976
|
+
: undefined;
|
|
2977
|
+
return {
|
|
2978
|
+
http_status: 200,
|
|
2979
|
+
batch_id: base.batchId,
|
|
2980
|
+
status: data.status,
|
|
2981
|
+
item_count: base.itemCount,
|
|
2982
|
+
submitted_at: base.submittedAt,
|
|
2983
|
+
completed_at: data.completed_at,
|
|
2984
|
+
results,
|
|
2985
|
+
...(terminalEtag === undefined ? {} : { etag: terminalEtag }),
|
|
2986
|
+
request_id: requestId,
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
/** Read owner-visible metadata for one delegated qURL. */
|
|
2990
|
+
async getDelegatedQurl(qurlId) {
|
|
2991
|
+
if (typeof qurlId !== "string" || !DELEGATED_QURL_ID_PATTERN.test(qurlId)) {
|
|
2992
|
+
throw clientValidationError("getDelegatedQurl: qurlId must match q_ plus 11 lowercase hexadecimal characters");
|
|
2993
|
+
}
|
|
2994
|
+
const envelope = await this.rawRequest("GET", `/v1/delegated-qurls/${encodeURIComponent(qurlId)}`);
|
|
2995
|
+
const status = envelope.__http_status ?? 0;
|
|
2996
|
+
if (status !== 200) {
|
|
2997
|
+
throw delegatedResponseError(status, "read returned an unexpected success status");
|
|
2998
|
+
}
|
|
2999
|
+
const { data, requestId } = delegatedEnvelope(envelope, status);
|
|
3000
|
+
return validateDelegatedQurl(data, qurlId, status, requestId);
|
|
3001
|
+
}
|
|
3002
|
+
/**
|
|
3003
|
+
* Revoke one delegated qURL. DELETE is never retried automatically. A 503
|
|
3004
|
+
* `mutation_outcome_unknown` is safe to retry with the same qURL ID.
|
|
3005
|
+
*/
|
|
3006
|
+
async deleteDelegatedQurl(qurlId) {
|
|
3007
|
+
if (typeof qurlId !== "string" || !DELEGATED_QURL_ID_PATTERN.test(qurlId)) {
|
|
3008
|
+
throw clientValidationError("deleteDelegatedQurl: qurlId must match q_ plus 11 lowercase hexadecimal characters");
|
|
3009
|
+
}
|
|
3010
|
+
await this.requestNoContent(`/v1/delegated-qurls/${encodeURIComponent(qurlId)}`);
|
|
3011
|
+
}
|
|
2316
3012
|
/**
|
|
2317
3013
|
* Get a qURL resource and its access tokens.
|
|
2318
3014
|
*
|
|
2319
|
-
* Accepts
|
|
2320
|
-
* prefix); the API resolves
|
|
3015
|
+
* Accepts a CRID or qURL
|
|
3016
|
+
* display ID (`q_` prefix); the API resolves display IDs to the parent
|
|
3017
|
+
* resource automatically.
|
|
2321
3018
|
*/
|
|
2322
3019
|
async get(id) {
|
|
2323
|
-
|
|
3020
|
+
validatePathId(id, "get", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
|
|
2324
3021
|
const raw = await this.request("GET", `/v1/qurls/${encodeURIComponent(id)}`);
|
|
2325
3022
|
return this.mapQurlsField(raw);
|
|
2326
3023
|
}
|
|
@@ -2362,49 +3059,20 @@ class QURLClient {
|
|
|
2362
3059
|
/**
|
|
2363
3060
|
* Delete (revoke) a qURL resource and all its access tokens.
|
|
2364
3061
|
*
|
|
2365
|
-
*
|
|
2366
|
-
*
|
|
2367
|
-
*
|
|
2368
|
-
*
|
|
3062
|
+
* Requires a CRID returned by the API. To revoke one qURL, use
|
|
3063
|
+
* {@link revokeResourceQurl} instead.
|
|
3064
|
+
*
|
|
3065
|
+
* @throws {ValidationError} If `id` is not a canonical CRID.
|
|
2369
3066
|
*/
|
|
2370
3067
|
async delete(id) {
|
|
2371
|
-
|
|
2372
|
-
// is a raw TypeError instead of the structured ValidationError the
|
|
2373
|
-
// rest of the surface produces.
|
|
2374
|
-
if (typeof id !== "string") {
|
|
2375
|
-
throw clientValidationError(`delete: requires a resource ID (${RESOURCE_ID_PREFIX} prefix + suffix) — got ${id === null ? "null" : typeof id}`);
|
|
2376
|
-
}
|
|
2377
|
-
if (id.trim() !== id) {
|
|
2378
|
-
throw clientValidationError("delete: id must not include leading or trailing whitespace");
|
|
2379
|
-
}
|
|
2380
|
-
// Too-short check runs BEFORE the prefix check so it catches
|
|
2381
|
-
// bare-prefix inputs like `"r_"` (right prefix, no suffix) in
|
|
2382
|
-
// addition to `""` / `"x"` / `"ab"` / `"q_"`. Without this
|
|
2383
|
-
// ordering, an exact `"r_"` would pass the startsWith check and
|
|
2384
|
-
// the SDK would send `DELETE /v1/qurls/r_` to the server —
|
|
2385
|
-
// rejected server-side, but this catches it client-side without
|
|
2386
|
-
// a round-trip.
|
|
2387
|
-
if (id.length <= RESOURCE_ID_PREFIX.length) {
|
|
2388
|
-
throw clientValidationError(`delete: requires a resource ID (${RESOURCE_ID_PREFIX} prefix + suffix) — got ${id.length} character${id.length === 1 ? "" : "s"}`);
|
|
2389
|
-
}
|
|
2390
|
-
if (!id.startsWith(RESOURCE_ID_PREFIX)) {
|
|
2391
|
-
// Wrong-prefix branch: the input is long enough to plausibly be
|
|
2392
|
-
// an ID but has the wrong prefix (e.g. `q_3a7f2c8e91b`).
|
|
2393
|
-
// Echo only the 2-char prefix — never the raw ID
|
|
2394
|
-
// — so observability pipelines don't end up with caller-supplied
|
|
2395
|
-
// identifiers in error logs.
|
|
2396
|
-
const observedPrefix = id.slice(0, RESOURCE_ID_PREFIX.length);
|
|
2397
|
-
throw clientValidationError(`delete: only resource IDs (${RESOURCE_ID_PREFIX} prefix) are accepted — ` +
|
|
2398
|
-
`got an ID starting with "${observedPrefix}". ` +
|
|
2399
|
-
"To revoke a single access token, use the resource-scoped token endpoint.");
|
|
2400
|
-
}
|
|
3068
|
+
requireResourceCrid(id, "delete");
|
|
2401
3069
|
await this.requestNoContent(`/v1/qurls/${encodeURIComponent(id)}`);
|
|
2402
3070
|
}
|
|
2403
3071
|
/**
|
|
2404
3072
|
* Extend a qURL's expiration.
|
|
2405
3073
|
*
|
|
2406
|
-
* Accepts
|
|
2407
|
-
* prefix).
|
|
3074
|
+
* Accepts a CRID or qURL
|
|
3075
|
+
* display ID (`q_` prefix). Delegates to {@link update} with only the
|
|
2408
3076
|
* expiration fields. `ExtendInput` shares its `extend_by` / `expires_at`
|
|
2409
3077
|
* fields with `UpdateInput` but is *narrower in two ways*: (1) exactly
|
|
2410
3078
|
* one of the two must be present (XOR via `?: never`), where `UpdateInput`
|
|
@@ -2416,7 +3084,7 @@ class QURLClient {
|
|
|
2416
3084
|
* enforced separately by the runtime check inside `update()`.
|
|
2417
3085
|
*/
|
|
2418
3086
|
async extend(id, input, options) {
|
|
2419
|
-
|
|
3087
|
+
validatePathId(id, "extend", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
|
|
2420
3088
|
requireObjectInput(input, "extend");
|
|
2421
3089
|
const { extend_by, expires_at } = input;
|
|
2422
3090
|
if (extend_by === undefined && expires_at === undefined) {
|
|
@@ -2430,11 +3098,12 @@ class QURLClient {
|
|
|
2430
3098
|
/**
|
|
2431
3099
|
* Update a qURL — extend expiration, change description, rename tags.
|
|
2432
3100
|
*
|
|
2433
|
-
* Accepts
|
|
2434
|
-
* prefix); the API resolves
|
|
3101
|
+
* Accepts a CRID or qURL
|
|
3102
|
+
* display ID (`q_` prefix); the API resolves display IDs to the parent
|
|
3103
|
+
* resource automatically.
|
|
2435
3104
|
*/
|
|
2436
3105
|
async update(id, input, options) {
|
|
2437
|
-
|
|
3106
|
+
validatePathId(id, "update", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
|
|
2438
3107
|
requireObjectInput(input, "update");
|
|
2439
3108
|
requireNoUnknownFields(input, UPDATE_FIELD_KEYS, "update");
|
|
2440
3109
|
// Normalize null → undefined so untyped-JS callers passing
|
|
@@ -2461,15 +3130,16 @@ class QURLClient {
|
|
|
2461
3130
|
*
|
|
2462
3131
|
* Portal-flow equivalent: {@link createPortal}.
|
|
2463
3132
|
*
|
|
2464
|
-
* Accepts
|
|
2465
|
-
* prefix); the API resolves
|
|
3133
|
+
* Accepts a CRID or qURL
|
|
3134
|
+
* display ID (`q_` prefix); the API resolves display IDs to the parent
|
|
3135
|
+
* resource automatically.
|
|
2466
3136
|
*
|
|
2467
3137
|
* Passing `{}` or an object with all fields `null`/`undefined` is
|
|
2468
3138
|
* equivalent to omitting the second argument: no body is sent, and the
|
|
2469
3139
|
* server applies its 24h default expiration.
|
|
2470
3140
|
*/
|
|
2471
3141
|
async mintLink(id, input, options) {
|
|
2472
|
-
|
|
3142
|
+
validatePathId(id, "mintLink", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
|
|
2473
3143
|
// Normalize null → omitted so untyped-JS callers passing
|
|
2474
3144
|
// `{ expires_in: null, expires_at: "..." }` don't leak null into
|
|
2475
3145
|
// the wire body via JSON.stringify and don't bypass the XOR check
|
|
@@ -2583,12 +3253,12 @@ class QURLClient {
|
|
|
2583
3253
|
}
|
|
2584
3254
|
/** Get one resource plus its bounded qURL preview. */
|
|
2585
3255
|
async getResource(id) {
|
|
2586
|
-
|
|
3256
|
+
requireResourceCrid(id, "getResource");
|
|
2587
3257
|
return this.request("GET", `/v1/resources/${encodeURIComponent(id)}`);
|
|
2588
3258
|
}
|
|
2589
3259
|
/** Update resource metadata. */
|
|
2590
3260
|
async updateResource(id, input, options) {
|
|
2591
|
-
|
|
3261
|
+
requireResourceCrid(id, "updateResource");
|
|
2592
3262
|
requireObjectInput(input, "updateResource");
|
|
2593
3263
|
requireNoUnknownFields(input, UPDATE_RESOURCE_FIELD_KEYS, "updateResource");
|
|
2594
3264
|
const validationInput = normalizePatchFields(input, UPDATE_RESOURCE_FIELD_KEYS, { preserveNullFields: ["alias"] });
|
|
@@ -2602,7 +3272,7 @@ class QURLClient {
|
|
|
2602
3272
|
}
|
|
2603
3273
|
/** Revoke a resource and all of its qURLs. */
|
|
2604
3274
|
async deleteResource(id) {
|
|
2605
|
-
|
|
3275
|
+
requireResourceCrid(id, "deleteResource");
|
|
2606
3276
|
await this.requestNoContent(`/v1/resources/${encodeURIComponent(id)}`);
|
|
2607
3277
|
}
|
|
2608
3278
|
/**
|
|
@@ -2611,7 +3281,7 @@ class QURLClient {
|
|
|
2611
3281
|
* Portal-flow equivalent: {@link createPortal} (same endpoint).
|
|
2612
3282
|
*/
|
|
2613
3283
|
async createQurlForResource(id, input, options) {
|
|
2614
|
-
|
|
3284
|
+
requireResourceCrid(id, "createQurlForResource");
|
|
2615
3285
|
let normalized = input;
|
|
2616
3286
|
if (input !== undefined) {
|
|
2617
3287
|
requireObjectInput(input, "createQurlForResource");
|
|
@@ -2622,16 +3292,83 @@ class QURLClient {
|
|
|
2622
3292
|
validateQurlTokenOptions(normalized);
|
|
2623
3293
|
return this.request("POST", `/v1/resources/${encodeURIComponent(id)}/qurls`, normalized, options);
|
|
2624
3294
|
}
|
|
3295
|
+
/**
|
|
3296
|
+
* Mint a fresh, short-lived share link for an existing CRID.
|
|
3297
|
+
* The returned link is secret and is not retrievable after this response.
|
|
3298
|
+
* If you hold the trusted resource DER SPKI, call `ShareLink.verifyCrid()`
|
|
3299
|
+
* before delivery to verify that the response CRID derives from that key.
|
|
3300
|
+
*/
|
|
3301
|
+
async shareResource(id, options = {}, requestOptions) {
|
|
3302
|
+
requireResourceCrid(id, "shareResource");
|
|
3303
|
+
requireObjectInput(options, "shareResource");
|
|
3304
|
+
requireNoUnknownFields(options, SHARE_RESOURCE_OPTION_KEYS, "shareResource");
|
|
3305
|
+
const body = {};
|
|
3306
|
+
if (options.ttlSeconds !== undefined) {
|
|
3307
|
+
if (typeof options.ttlSeconds !== "number" ||
|
|
3308
|
+
!Number.isSafeInteger(options.ttlSeconds) ||
|
|
3309
|
+
options.ttlSeconds <= 0) {
|
|
3310
|
+
throw clientValidationError("shareResource: ttlSeconds must be a positive safe integer");
|
|
3311
|
+
}
|
|
3312
|
+
body.ttl_seconds = options.ttlSeconds;
|
|
3313
|
+
}
|
|
3314
|
+
const data = await this.request("POST", `/v1/resources/${encodeURIComponent(id)}/share`, body, requestOptions);
|
|
3315
|
+
if (typeof data?.qurl !== "string" || data.qurl.trim() === "") {
|
|
3316
|
+
throw unexpectedResponseError("shareResource: response is missing qurl");
|
|
3317
|
+
}
|
|
3318
|
+
if (data.crid !== id) {
|
|
3319
|
+
throw unexpectedResponseError("shareResource: response CRID does not match the requested CRID");
|
|
3320
|
+
}
|
|
3321
|
+
const wire = data;
|
|
3322
|
+
for (const field of ["qurl_id", "type", "expires_at"]) {
|
|
3323
|
+
const value = wire[field];
|
|
3324
|
+
if (value !== undefined && value !== null && typeof value !== "string") {
|
|
3325
|
+
throw unexpectedResponseError(`shareResource: response has invalid ${field}`);
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
for (const field of ["qurl_id", "type"]) {
|
|
3329
|
+
const value = wire[field];
|
|
3330
|
+
if (typeof value === "string" && (value.trim() === "" || value.trim() !== value)) {
|
|
3331
|
+
throw unexpectedResponseError(`shareResource: response has invalid ${field}`);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
if (wire.expires_in_seconds !== undefined &&
|
|
3335
|
+
wire.expires_in_seconds !== null &&
|
|
3336
|
+
(typeof wire.expires_in_seconds !== "number" ||
|
|
3337
|
+
!Number.isSafeInteger(wire.expires_in_seconds) ||
|
|
3338
|
+
wire.expires_in_seconds < 0)) {
|
|
3339
|
+
throw unexpectedResponseError("shareResource: response has invalid expires_in_seconds");
|
|
3340
|
+
}
|
|
3341
|
+
if (wire.single_use !== undefined &&
|
|
3342
|
+
wire.single_use !== null &&
|
|
3343
|
+
typeof wire.single_use !== "boolean") {
|
|
3344
|
+
throw unexpectedResponseError("shareResource: response has invalid single_use");
|
|
3345
|
+
}
|
|
3346
|
+
const expiresAt = parseApiDate(typeof wire.expires_at === "string" ? wire.expires_at : undefined);
|
|
3347
|
+
if (typeof wire.expires_at === "string" && expiresAt === undefined) {
|
|
3348
|
+
throw unexpectedResponseError("shareResource: response has invalid expires_at");
|
|
3349
|
+
}
|
|
3350
|
+
return new share_js_1.ShareLink({
|
|
3351
|
+
// Repair transport-only edge whitespace on the one-time credential;
|
|
3352
|
+
// ancillary identifiers fail closed instead of being normalized.
|
|
3353
|
+
link: data.qurl.trim(),
|
|
3354
|
+
qurlId: typeof wire.qurl_id === "string" ? wire.qurl_id : undefined,
|
|
3355
|
+
crid: id,
|
|
3356
|
+
type: typeof wire.type === "string" ? wire.type : undefined,
|
|
3357
|
+
expiresAt,
|
|
3358
|
+
expiresInSeconds: typeof wire.expires_in_seconds === "number" ? wire.expires_in_seconds : undefined,
|
|
3359
|
+
singleUse: typeof wire.single_use === "boolean" ? wire.single_use : undefined,
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
2625
3362
|
/** Revoke a specific qURL token on a resource. */
|
|
2626
3363
|
async revokeResourceQurl(id, qurlId) {
|
|
2627
|
-
|
|
2628
|
-
|
|
3364
|
+
requireResourceCrid(id, "revokeResourceQurl");
|
|
3365
|
+
validatePathId(qurlId, "revokeResourceQurl", "qurl id", QURL_ID_PATH_OPTIONS);
|
|
2629
3366
|
await this.requestNoContent(`/v1/resources/${encodeURIComponent(id)}/qurls/${encodeURIComponent(qurlId)}`);
|
|
2630
3367
|
}
|
|
2631
3368
|
/** Update a specific qURL token on a resource. */
|
|
2632
3369
|
async updateResourceQurl(id, qurlId, input, options) {
|
|
2633
|
-
|
|
2634
|
-
|
|
3370
|
+
requireResourceCrid(id, "updateResourceQurl");
|
|
3371
|
+
validatePathId(qurlId, "updateResourceQurl", "qurl id", QURL_ID_PATH_OPTIONS);
|
|
2635
3372
|
requireObjectInput(input, "updateResourceQurl");
|
|
2636
3373
|
requireNoUnknownFields(input, UPDATE_RESOURCE_QURL_FIELD_KEYS, "updateResourceQurl");
|
|
2637
3374
|
const normalized = normalizePatchFields(input, UPDATE_RESOURCE_QURL_FIELD_KEYS);
|
|
@@ -2647,7 +3384,7 @@ class QURLClient {
|
|
|
2647
3384
|
* this page and emits a debug log rather than pretending it fetched all pages.
|
|
2648
3385
|
*/
|
|
2649
3386
|
async listResourceSessions(id) {
|
|
2650
|
-
|
|
3387
|
+
requireResourceCrid(id, "listResourceSessions");
|
|
2651
3388
|
const { data, meta } = await this.rawRequest("GET", `/v1/resources/${encodeURIComponent(id)}/sessions`);
|
|
2652
3389
|
if (meta?.has_more || meta?.next_cursor) {
|
|
2653
3390
|
this.log("listResourceSessions: pagination metadata surfaced on unpaginated endpoint", {
|
|
@@ -2664,7 +3401,7 @@ class QURLClient {
|
|
|
2664
3401
|
}
|
|
2665
3402
|
/** Terminate all active sessions for a resource and return the server count. */
|
|
2666
3403
|
async terminateAllResourceSessions(id) {
|
|
2667
|
-
|
|
3404
|
+
requireResourceCrid(id, "terminateAllResourceSessions");
|
|
2668
3405
|
const path = `/v1/resources/${encodeURIComponent(id)}/sessions`;
|
|
2669
3406
|
const { data, meta, __http_status } = await this.rawRequest("DELETE", path);
|
|
2670
3407
|
if (__http_status === 204) {
|
|
@@ -2679,8 +3416,8 @@ class QURLClient {
|
|
|
2679
3416
|
}
|
|
2680
3417
|
/** Terminate a specific resource session. */
|
|
2681
3418
|
async terminateResourceSession(id, sessionId) {
|
|
2682
|
-
|
|
2683
|
-
|
|
3419
|
+
requireResourceCrid(id, "terminateResourceSession");
|
|
3420
|
+
validatePathId(sessionId, "terminateResourceSession", "session id");
|
|
2684
3421
|
await this.requestNoContent(`/v1/resources/${encodeURIComponent(id)}/sessions/${encodeURIComponent(sessionId)}`);
|
|
2685
3422
|
}
|
|
2686
3423
|
/** List connector installations. */
|
|
@@ -2783,22 +3520,22 @@ class QURLClient {
|
|
|
2783
3520
|
}
|
|
2784
3521
|
/** Get custom domain status. */
|
|
2785
3522
|
async getDomain(domain) {
|
|
2786
|
-
|
|
3523
|
+
validatePathId(domain, "getDomain", "domain");
|
|
2787
3524
|
return this.request("GET", `/v1/domains/${encodeURIComponent(domain)}`);
|
|
2788
3525
|
}
|
|
2789
3526
|
/** Remove a custom domain. */
|
|
2790
3527
|
async deleteDomain(domain) {
|
|
2791
|
-
|
|
3528
|
+
validatePathId(domain, "deleteDomain", "domain");
|
|
2792
3529
|
await this.requestNoContent(`/v1/domains/${encodeURIComponent(domain)}`);
|
|
2793
3530
|
}
|
|
2794
3531
|
/** Trigger DNS verification for a custom domain. */
|
|
2795
3532
|
async verifyDomain(domain, options) {
|
|
2796
|
-
|
|
3533
|
+
validatePathId(domain, "verifyDomain", "domain");
|
|
2797
3534
|
return this.request("POST", `/v1/domains/${encodeURIComponent(domain)}/verify`, undefined, options);
|
|
2798
3535
|
}
|
|
2799
3536
|
/** Regenerate a domain verification token. */
|
|
2800
3537
|
async regenerateDomainToken(domain, options) {
|
|
2801
|
-
|
|
3538
|
+
validatePathId(domain, "regenerateDomainToken", "domain");
|
|
2802
3539
|
return this.request("POST", `/v1/domains/${encodeURIComponent(domain)}/regenerate-token`, undefined, options);
|
|
2803
3540
|
}
|
|
2804
3541
|
/** List webhooks. */
|
|
@@ -2834,12 +3571,12 @@ class QURLClient {
|
|
|
2834
3571
|
}
|
|
2835
3572
|
/** Get a webhook. */
|
|
2836
3573
|
async getWebhook(id) {
|
|
2837
|
-
|
|
3574
|
+
validatePathId(id, "getWebhook");
|
|
2838
3575
|
return this.request("GET", `/v1/webhooks/${encodeURIComponent(id)}`);
|
|
2839
3576
|
}
|
|
2840
3577
|
/** Update a webhook. */
|
|
2841
3578
|
async updateWebhook(id, input, options) {
|
|
2842
|
-
|
|
3579
|
+
validatePathId(id, "updateWebhook");
|
|
2843
3580
|
requireObjectInput(input, "updateWebhook");
|
|
2844
3581
|
requireNoUnknownFields(input, UPDATE_WEBHOOK_FIELD_KEYS, "updateWebhook");
|
|
2845
3582
|
const normalized = normalizePatchFields(input, UPDATE_WEBHOOK_FIELD_KEYS);
|
|
@@ -2849,17 +3586,17 @@ class QURLClient {
|
|
|
2849
3586
|
}
|
|
2850
3587
|
/** Delete a webhook. */
|
|
2851
3588
|
async deleteWebhook(id) {
|
|
2852
|
-
|
|
3589
|
+
validatePathId(id, "deleteWebhook");
|
|
2853
3590
|
await this.requestNoContent(`/v1/webhooks/${encodeURIComponent(id)}`);
|
|
2854
3591
|
}
|
|
2855
3592
|
/** Regenerate a webhook signing secret. */
|
|
2856
3593
|
async regenerateWebhookSecret(id, options) {
|
|
2857
|
-
|
|
3594
|
+
validatePathId(id, "regenerateWebhookSecret");
|
|
2858
3595
|
return this.request("POST", `/v1/webhooks/${encodeURIComponent(id)}/secret`, undefined, options);
|
|
2859
3596
|
}
|
|
2860
3597
|
/** List delivery attempts for a webhook. */
|
|
2861
3598
|
async listWebhookDeliveries(id, input = {}) {
|
|
2862
|
-
|
|
3599
|
+
validatePathId(id, "listWebhookDeliveries");
|
|
2863
3600
|
const { data, meta } = await this.rawRequest("GET", appendQuery(`/v1/webhooks/${encodeURIComponent(id)}/deliveries`, input, WEBHOOK_DELIVERY_LIST_PARAM_KEYS, "listWebhookDeliveries"));
|
|
2864
3601
|
return pageFromMeta({ deliveries: data ?? [] }, meta);
|
|
2865
3602
|
}
|
|
@@ -2870,7 +3607,7 @@ class QURLClient {
|
|
|
2870
3607
|
* iteration begins.
|
|
2871
3608
|
*/
|
|
2872
3609
|
listAllWebhookDeliveries(id, input = {}) {
|
|
2873
|
-
|
|
3610
|
+
validatePathId(id, "listAllWebhookDeliveries");
|
|
2874
3611
|
validateListAllInput(input, WEBHOOK_DELIVERY_LIST_PARAM_KEYS, "listAllWebhookDeliveries");
|
|
2875
3612
|
return this.paginateAll("listAllWebhookDeliveries", input, (pageInput) => this.listWebhookDeliveries(id, pageInput), (page) => page.deliveries);
|
|
2876
3613
|
}
|
|
@@ -2879,10 +3616,34 @@ class QURLClient {
|
|
|
2879
3616
|
requireObjectInput(input, "createApiKey");
|
|
2880
3617
|
requireNoUnknownFields(input, CREATE_API_KEY_FIELD_KEYS, "createApiKey");
|
|
2881
3618
|
const normalizedRecord = normalizePatchFields(input, CREATE_API_KEY_FIELD_KEYS);
|
|
3619
|
+
// `kind` is required by the server and has no default there. Filling it
|
|
3620
|
+
// in keeps the common durable-key call site unchanged.
|
|
3621
|
+
normalizedRecord.kind ??= "api_key";
|
|
3622
|
+
if (normalizedRecord.kind !== "api_key" && normalizedRecord.kind !== "enrollment_token") {
|
|
3623
|
+
throw clientValidationError("createApiKey: kind must be 'api_key' or 'enrollment_token'");
|
|
3624
|
+
}
|
|
3625
|
+
// `scopes` and `target`/`claims` are mutually exclusive by kind, and the
|
|
3626
|
+
// server rejects either mismatch with 400 invalid_input. Fail both
|
|
3627
|
+
// directions locally rather than paying a round trip.
|
|
3628
|
+
const isEnrollmentToken = normalizedRecord.kind === "enrollment_token";
|
|
3629
|
+
if (isEnrollmentToken) {
|
|
3630
|
+
if (normalizedRecord.scopes !== undefined) {
|
|
3631
|
+
throw clientValidationError("createApiKey: scopes is not accepted for kind 'enrollment_token'; " +
|
|
3632
|
+
"the server assigns them from target");
|
|
3633
|
+
}
|
|
3634
|
+
validateEnrollmentTokenFields(normalizedRecord, "createApiKey");
|
|
3635
|
+
}
|
|
3636
|
+
else {
|
|
3637
|
+
for (const field of ["target", "claims", "expires_in"]) {
|
|
3638
|
+
if (normalizedRecord[field] !== undefined) {
|
|
3639
|
+
throw clientValidationError(`createApiKey: ${field} is only accepted for kind 'enrollment_token'`);
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
2882
3643
|
const normalized = normalizedRecord;
|
|
2883
3644
|
validateApiKeyWriteFields(normalizedRecord, "createApiKey", {
|
|
2884
3645
|
name: true,
|
|
2885
|
-
scopes:
|
|
3646
|
+
scopes: !isEnrollmentToken,
|
|
2886
3647
|
});
|
|
2887
3648
|
return this.request("POST", "/v1/api-keys", normalized, options);
|
|
2888
3649
|
}
|
|
@@ -2903,7 +3664,7 @@ class QURLClient {
|
|
|
2903
3664
|
}
|
|
2904
3665
|
/** Update an API key. */
|
|
2905
3666
|
async updateApiKey(keyId, input, options) {
|
|
2906
|
-
|
|
3667
|
+
validatePathId(keyId, "updateApiKey");
|
|
2907
3668
|
requireObjectInput(input, "updateApiKey");
|
|
2908
3669
|
requireNoUnknownFields(input, UPDATE_API_KEY_FIELD_KEYS, "updateApiKey");
|
|
2909
3670
|
const normalized = normalizePatchFields(input, UPDATE_API_KEY_FIELD_KEYS);
|
|
@@ -2913,7 +3674,7 @@ class QURLClient {
|
|
|
2913
3674
|
}
|
|
2914
3675
|
/** Revoke an API key. */
|
|
2915
3676
|
async revokeApiKey(keyId) {
|
|
2916
|
-
|
|
3677
|
+
validatePathId(keyId, "revokeApiKey");
|
|
2917
3678
|
await this.requestNoContent(`/v1/api-keys/${encodeURIComponent(keyId)}`);
|
|
2918
3679
|
}
|
|
2919
3680
|
/** Redeem an access code. */
|
|
@@ -2931,7 +3692,7 @@ class QURLClient {
|
|
|
2931
3692
|
requireNoUnknownFields(input, CREATE_ACCESS_CODE_FIELD_KEYS, "createAccessCode");
|
|
2932
3693
|
const normalizedRecord = normalizePatchFields(input, CREATE_ACCESS_CODE_FIELD_KEYS);
|
|
2933
3694
|
const normalized = normalizedRecord;
|
|
2934
|
-
|
|
3695
|
+
requireResourceCrid(normalizedRecord.resource_id, "createAccessCode", "resource_id");
|
|
2935
3696
|
return this.request("POST", "/v1/access-codes", normalized, options);
|
|
2936
3697
|
}
|
|
2937
3698
|
/**
|
|
@@ -2958,7 +3719,7 @@ class QURLClient {
|
|
|
2958
3719
|
}
|
|
2959
3720
|
/** Revoke an access code. */
|
|
2960
3721
|
async revokeAccessCode(id) {
|
|
2961
|
-
|
|
3722
|
+
validatePathId(id, "revokeAccessCode");
|
|
2962
3723
|
await this.requestNoContent(`/v1/access-codes/${encodeURIComponent(id)}`);
|
|
2963
3724
|
}
|
|
2964
3725
|
// --- Internal HTTP plumbing ---
|
|
@@ -3000,7 +3761,7 @@ class QURLClient {
|
|
|
3000
3761
|
*/
|
|
3001
3762
|
async rawRequest(method, path, body, rawOptions = {}) {
|
|
3002
3763
|
const url = `${this.baseUrl}${path}`;
|
|
3003
|
-
const { passthroughStatuses = NO_PASSTHROUGH_STATUSES, requestOptions, allowEmptySuccessBody = false, retry = true, generateIdempotencyKey = true, } = rawOptions;
|
|
3764
|
+
const { passthroughStatuses = NO_PASSTHROUGH_STATUSES, requestOptions, allowEmptySuccessBody = false, allowEmptyPassthroughBody = false, retry = true, generateIdempotencyKey = true, ifNoneMatch, captureResponseHeaders = false, onDispatch, } = rawOptions;
|
|
3004
3765
|
const idempotencyKey = idempotencyKeyForRequest(method, requestOptions, generateIdempotencyKey);
|
|
3005
3766
|
const headers = {
|
|
3006
3767
|
Authorization: `Bearer ${this.apiKey}`,
|
|
@@ -3013,6 +3774,9 @@ class QURLClient {
|
|
|
3013
3774
|
if (idempotencyKey !== undefined) {
|
|
3014
3775
|
headers["Idempotency-Key"] = idempotencyKey;
|
|
3015
3776
|
}
|
|
3777
|
+
if (ifNoneMatch !== undefined) {
|
|
3778
|
+
headers["If-None-Match"] = ifNoneMatch;
|
|
3779
|
+
}
|
|
3016
3780
|
// Reads may retry transient statuses and transport failures. POST/PATCH
|
|
3017
3781
|
// keep status-code retries limited to rate limits and reuse the same
|
|
3018
3782
|
// Idempotency-Key for fetch-level failures. DELETE is deliberately never
|
|
@@ -3049,6 +3813,7 @@ class QURLClient {
|
|
|
3049
3813
|
// attempt shouldn't poison the next try's deadline. Total
|
|
3050
3814
|
// request bound: roughly `timeout * (maxRetries + 1) +
|
|
3051
3815
|
// sum(retryDelay)`.
|
|
3816
|
+
onDispatch?.();
|
|
3052
3817
|
response = await this.fetchFn(url, {
|
|
3053
3818
|
method,
|
|
3054
3819
|
headers,
|
|
@@ -3224,16 +3989,20 @@ class QURLClient {
|
|
|
3224
3989
|
data: undefined,
|
|
3225
3990
|
__http_status: response.status,
|
|
3226
3991
|
__http_body_empty: responseBody.length === 0,
|
|
3992
|
+
__http_headers: captureResponseHeaders ? response.headers : undefined,
|
|
3227
3993
|
};
|
|
3228
3994
|
}
|
|
3229
3995
|
// Exact no-content helpers must inspect both axes of the success
|
|
3230
3996
|
// contract. Let only those callers observe an empty non-204 response;
|
|
3231
3997
|
// body-returning methods still reject it as non-JSON below.
|
|
3232
|
-
if (response.ok && allowEmptySuccessBody
|
|
3998
|
+
if (((response.ok && allowEmptySuccessBody) ||
|
|
3999
|
+
(isPassthrough && allowEmptyPassthroughBody)) &&
|
|
4000
|
+
responseBody.length === 0) {
|
|
3233
4001
|
return {
|
|
3234
4002
|
data: undefined,
|
|
3235
4003
|
__http_status: response.status,
|
|
3236
4004
|
__http_body_empty: true,
|
|
4005
|
+
__http_headers: captureResponseHeaders ? response.headers : undefined,
|
|
3237
4006
|
};
|
|
3238
4007
|
}
|
|
3239
4008
|
try {
|
|
@@ -3243,6 +4012,7 @@ class QURLClient {
|
|
|
3243
4012
|
__http_status: response.status,
|
|
3244
4013
|
// JSON.parse("") throws, so parsed JSON is necessarily non-empty.
|
|
3245
4014
|
__http_body_empty: false,
|
|
4015
|
+
__http_headers: captureResponseHeaders ? response.headers : undefined,
|
|
3246
4016
|
};
|
|
3247
4017
|
}
|
|
3248
4018
|
catch {
|
|
@@ -3353,14 +4123,13 @@ class QURLClient {
|
|
|
3353
4123
|
const header = response.headers.get("Retry-After");
|
|
3354
4124
|
if (!header)
|
|
3355
4125
|
return undefined;
|
|
3356
|
-
// TODO: parse HTTP-date format per RFC 7231 §7.1.3 — currently only
|
|
3357
|
-
// handles delta-seconds; HTTP-date headers fall through to the
|
|
3358
|
-
// exponential-backoff branch in `retryDelay`. Tracked in #61.
|
|
3359
|
-
//
|
|
3360
|
-
// Digit-only pre-check rather than `parseInt` alone: `parseInt("60abc")`
|
|
3361
|
-
// returns `60` and would silently honor a malformed header. The
|
|
3362
|
-
// strict check makes any deviation observable (debug log) instead.
|
|
3363
4126
|
const trimmed = header.trim();
|
|
4127
|
+
if (/^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(trimmed)) {
|
|
4128
|
+
const seconds = Math.ceil((Date.parse(trimmed) - Date.now()) / 1000);
|
|
4129
|
+
return Number.isSafeInteger(seconds) && seconds > 0 && seconds <= RETRY_AFTER_PARSE_LIMIT_S
|
|
4130
|
+
? seconds
|
|
4131
|
+
: undefined;
|
|
4132
|
+
}
|
|
3364
4133
|
if (!/^\d+$/.test(trimmed)) {
|
|
3365
4134
|
this.log("Retry-After header was non-numeric, using exponential backoff", {
|
|
3366
4135
|
header_value: header.slice(0, 100),
|
|
@@ -3446,7 +4215,7 @@ exports.QURLClient = QURLClient;
|
|
|
3446
4215
|
/**
|
|
3447
4216
|
* A LayerV-protected resource, bound to the client that produced it.
|
|
3448
4217
|
*
|
|
3449
|
-
* Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.
|
|
4218
|
+
* Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.resourceByCrid} —
|
|
3450
4219
|
* not constructed directly. Mint short-lived access links for the resource
|
|
3451
4220
|
* with {@link createPortal} (qurl-go: `qurl.Resource`).
|
|
3452
4221
|
*/
|
|
@@ -3455,19 +4224,19 @@ class ProtectedResource {
|
|
|
3455
4224
|
// CONTRIBUTING.md's dual-build rule bars) so the binding stays on the
|
|
3456
4225
|
// instance and out of Object.keys/JSON.stringify output.
|
|
3457
4226
|
#client;
|
|
3458
|
-
/** The
|
|
3459
|
-
|
|
4227
|
+
/** The public resource CRID. */
|
|
4228
|
+
crid;
|
|
3460
4229
|
/** The private URL protected by this resource, when known. */
|
|
3461
4230
|
targetUrl;
|
|
3462
4231
|
/**
|
|
3463
4232
|
* Full server-populated {@link Resource} metadata when the handle came
|
|
3464
|
-
* from an API response; `undefined` for {@link QURLClient.
|
|
4233
|
+
* from an API response; `undefined` for {@link QURLClient.resourceByCrid}
|
|
3465
4234
|
* handles and for the one-call {@link QURLClient.createPortalForUrl} path.
|
|
3466
4235
|
*/
|
|
3467
4236
|
details;
|
|
3468
4237
|
constructor(client, id, targetUrl, details) {
|
|
3469
4238
|
this.#client = client;
|
|
3470
|
-
this.
|
|
4239
|
+
this.crid = id;
|
|
3471
4240
|
this.targetUrl = targetUrl;
|
|
3472
4241
|
this.details = details;
|
|
3473
4242
|
}
|
|
@@ -3499,8 +4268,8 @@ const CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN = Symbol("validated ConnectorResource
|
|
|
3499
4268
|
*/
|
|
3500
4269
|
class ConnectorResource {
|
|
3501
4270
|
#client;
|
|
3502
|
-
|
|
3503
|
-
/**
|
|
4271
|
+
resourcePublicKey;
|
|
4272
|
+
/** Required public identifier, verified against the returned resource key. */
|
|
3504
4273
|
crid;
|
|
3505
4274
|
connectorRoutingId;
|
|
3506
4275
|
knockResourceId;
|
|
@@ -3512,7 +4281,7 @@ class ConnectorResource {
|
|
|
3512
4281
|
throw clientValidationError("ConnectorResource cannot be constructed directly; load it through QURLClient");
|
|
3513
4282
|
}
|
|
3514
4283
|
this.#client = client;
|
|
3515
|
-
this.
|
|
4284
|
+
this.resourcePublicKey = details.resource_id;
|
|
3516
4285
|
this.crid = details.crid;
|
|
3517
4286
|
this.connectorRoutingId = details.connector_routing_id;
|
|
3518
4287
|
this.knockResourceId = details.knock_resource_id;
|
|
@@ -3521,7 +4290,7 @@ class ConnectorResource {
|
|
|
3521
4290
|
}
|
|
3522
4291
|
/** Mint a short-lived portal for this Connector resource. */
|
|
3523
4292
|
async createPortal(opts = {}, requestOptions) {
|
|
3524
|
-
return this.#client.createPortal(this.
|
|
4293
|
+
return this.#client.createPortal(this.crid, opts, requestOptions);
|
|
3525
4294
|
}
|
|
3526
4295
|
}
|
|
3527
4296
|
exports.ConnectorResource = ConnectorResource;
|