@layervai/qurl 0.5.0 → 0.7.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.
@@ -1,8 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ProtectedResource = exports.QURLClient = exports.SIGNED_FRAGMENT_RE = void 0;
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;
@@ -176,6 +179,10 @@ const CREATE_QURL_FOR_RESOURCE_FIELD_KEYS = [
176
179
  "access_policy",
177
180
  "target_path",
178
181
  ];
182
+ const SHARE_RESOURCE_OPTION_KEYS = [
183
+ "ttlSeconds",
184
+ ];
185
+ assertExhaustive(true);
179
186
  assertExhaustive(true);
180
187
  /**
181
188
  * Options accepted by the portal-verb `protectUrl` / `createPortal`
@@ -234,11 +241,12 @@ const UPDATE_WEBHOOK_FIELD_KEYS = [
234
241
  ];
235
242
  assertExhaustive(true);
236
243
  const CREATE_API_KEY_FIELD_KEYS = [
244
+ "kind",
237
245
  "name",
238
246
  "scopes",
247
+ "target",
248
+ "claims",
239
249
  "expires_in",
240
- "purpose",
241
- "tunnel_slug",
242
250
  ];
243
251
  assertExhaustive(true);
244
252
  const UPDATE_API_KEY_FIELD_KEYS = [
@@ -323,14 +331,14 @@ function validateRequestOptions(options) {
323
331
  }
324
332
  }
325
333
  }
326
- function idempotencyKeyForRequest(method, options) {
334
+ function idempotencyKeyForRequest(method, options, generateWhenMissing = true) {
327
335
  // Validate before method gating so future read-only methods that accept
328
336
  // RequestOptions fail loudly on malformed options instead of discarding them.
329
337
  validateRequestOptions(options);
330
338
  if (!IDEMPOTENCY_KEY_METHODS.has(method)) {
331
339
  return undefined;
332
340
  }
333
- return options?.idempotencyKey ?? generateUuidV7();
341
+ return options?.idempotencyKey ?? (generateWhenMissing ? generateUuidV7() : undefined);
334
342
  }
335
343
  function generateUuidV7() {
336
344
  const bytes = new Uint8Array(16);
@@ -718,16 +726,58 @@ const MAX_DESCRIPTION = 500;
718
726
  const MAX_CUSTOM_DOMAIN = 253;
719
727
  const MAX_MAX_SESSIONS = 1000;
720
728
  const MAX_API_KEY_NAME = 100;
729
+ const API_KEY_REQUEST_SCOPE_VALUES = [
730
+ "qurl:read",
731
+ "qurl:write",
732
+ "qurl:resolve",
733
+ "qurl:agent",
734
+ ];
735
+ assertExhaustive(true);
736
+ const API_KEY_REQUEST_SCOPES = new Set(API_KEY_REQUEST_SCOPE_VALUES);
737
+ /** Narrow a response scope before reusing it in a create/update request. */
738
+ function isApiKeyRequestScope(scope) {
739
+ return API_KEY_REQUEST_SCOPES.has(scope);
740
+ }
741
+ const CREDENTIAL_TARGET_VALUES = [
742
+ "agent",
743
+ "connector",
744
+ ];
745
+ assertExhaustive(true);
746
+ const CREDENTIAL_TARGETS = new Set(CREDENTIAL_TARGET_VALUES);
747
+ const CREDENTIAL_CLAIM_TYPE_VALUES = [
748
+ "connector",
749
+ ];
750
+ assertExhaustive(true);
751
+ const CREDENTIAL_CLAIM_TYPES = new Set(CREDENTIAL_CLAIM_TYPE_VALUES);
752
+ // Mirrors CreateApiKeyRequest.expires_in in the service OpenAPI. Composite
753
+ // durations are deliberately invalid on this endpoint. The 24h cap permits
754
+ // 1d, but no positive whole-week duration.
755
+ const CREDENTIAL_DURATION_PATTERN = /^([0-9]+)([smhdw])$/;
756
+ const CREDENTIAL_DURATION_MULTIPLIERS = {
757
+ s: 1n,
758
+ m: 60n,
759
+ h: 3600n,
760
+ d: 86400n,
761
+ w: 604800n,
762
+ };
763
+ const MAX_ENROLLMENT_TOKEN_TTL_SECONDS = 86400n;
721
764
  const MAX_ALIAS = 64;
722
765
  const MAX_SLUG = 64;
723
766
  const MAX_TARGET_PATH = 2048;
724
767
  const MAX_TAGS = 10;
725
768
  const MAX_TAG_LENGTH = 50;
726
769
  const MAX_AUTO_PAGINATION_PAGES = 10_000;
770
+ // Keep these identity contracts aligned with qurl-go and the public API.
771
+ const CONNECTOR_SLUG_PATTERN = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
772
+ // Current service schemas intentionally share this grammar, but keep the
773
+ // mutable alias contract separate from the immutable lookup identity.
774
+ const CONNECTOR_ALIAS_PATTERN = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
775
+ const CONNECTOR_RESOURCE_ID_PATTERN = /^[A-Za-z0-9_-]{122}$/;
776
+ const CONNECTOR_RESOURCE_ID_DER_PREFIX = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE";
777
+ const CONNECTOR_ROUTING_ID_PATTERN = /^c-[a-z2-7]{51}[aq]$/;
727
778
  // CreateQurlRequest.target_url pattern is loose (just a URI) but
728
779
  // UpdateQurlRequest.tags pattern is specific — enforce it here.
729
780
  const TAG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9 _-]*$/;
730
- const RESOURCE_ID_PREFIX = "r_";
731
781
  function requireMaxLength(value, field, max) {
732
782
  if (value === undefined)
733
783
  return;
@@ -828,22 +878,279 @@ function pageFromMeta(out, meta) {
828
878
  page_size: meta?.page_size,
829
879
  };
830
880
  }
881
+ // Best-effort defence-in-depth against today's access-token grammar. The
882
+ // service remains authoritative because resource IDs and credentials are both
883
+ // opaque and future credential formats may not retain the `at_` prefix. The
884
+ // delimiter requirement cannot collide with current base64url public keys,
885
+ // standard-base64 keys, or base32 CRIDs.
886
+ const PATH_EMBEDDED_ACCESS_TOKEN_RE = /[?&#=:\s]at_/i;
887
+ const PATH_PREFIXED_ACCESS_TOKEN_RE = /\/at_/i;
888
+ const QURL_LINK_URL_RE = /^(?:https?:\/\/)?(?:[a-z0-9-]+\.)*qurl\.link(?::\d{1,5})?(?:\/|[?#])/i;
889
+ // ponytail: today's 22-character token suffix only improves URL-paste diagnostics;
890
+ // other lengths are still rejected as URLs without echoing the input.
891
+ const URL_EMBEDDED_ACCESS_TOKEN_RE = /[?&#=:/]at_[a-z0-9_-]{22}(?:$|[?&#:/])/i;
892
+ // A host without a slash can be a legitimate domain identifier. Require the
893
+ // path boundary so shared validation does not break the domain namespace.
894
+ const PATH_SCHEMELESS_URL_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)+(?::\d{1,5})?\//i;
895
+ const QURL_DISPLAY_ID_PREFIX_RE = /^q_/i;
896
+ const MAX_PATH_ID_DECODE_PASSES = 3;
897
+ const MAX_PATH_ID_LENGTH = 4096;
898
+ function isUrlLikePathId(value) {
899
+ return /^https?:\/\//i.test(value) || PATH_SCHEMELESS_URL_RE.test(value);
900
+ }
901
+ const REVOKE_INDIVIDUAL_QURL_RECOVERY = "revoke individual tokens with revokeResourceQurl(crid, qurlId)";
902
+ const DELETE_RESOURCE_RECOVERY = "pass a CRID returned by the API; to revoke one qURL, call revokeResourceQurl(crid, qurlId)";
903
+ const RESOURCE_OR_QURL_ID_PATH_OPTIONS = {
904
+ requireCridUnlessQurlDisplayId: true,
905
+ };
906
+ const QURL_ID_PATH_OPTIONS = {
907
+ rejectBareAccessToken: true,
908
+ accessTokenRecovery: "pass the qURL display ID, not its access token",
909
+ };
831
910
  /**
832
911
  * Validates that a path-parameter argument is a non-empty string. Some callers
833
912
  * pass resource/qURL display IDs, while newer endpoints also pass domains,
834
913
  * session IDs, webhook IDs, and API-key IDs, so this intentionally enforces
835
- * only basic shape and leaves endpoint-specific grammar to the service.
914
+ * only basic shape plus transport-safety rules and leaves endpoint-specific
915
+ * identifier grammar to the service.
836
916
  */
837
- function requireNonEmptyId(id, method, field = "id") {
917
+ function validatePathId(id, method, field = "id", options = {}) {
918
+ if (options.requireCridUnlessQurlDisplayId && typeof id === "string" && !id.startsWith("q_")) {
919
+ requireResourceCrid(id, method, field);
920
+ return;
921
+ }
838
922
  // `.trim()` catches whitespace-only and padded IDs before they round-trip as
839
923
  // `%20...%20` paths that the server can only reject with a less useful 404.
840
924
  // The pre-flight error is more actionable than the 404.
841
- if (typeof id !== "string" || id.trim() === "") {
925
+ if (typeof id !== "string") {
926
+ const observedType = id === null ? "null" : typeof id;
927
+ throw clientValidationError(`${method}: ${field} is required (got ${observedType})`);
928
+ }
929
+ if (id.trim() === "") {
842
930
  throw clientValidationError(`${method}: ${field} is required`);
843
931
  }
844
932
  if (id.trim() !== id) {
845
933
  throw clientValidationError(`${method}: ${field} must not include leading or trailing whitespace`);
846
934
  }
935
+ if (id.length > MAX_PATH_ID_LENGTH) {
936
+ throw clientValidationError(`${method}: ${field} must be ${MAX_PATH_ID_LENGTH} characters or fewer`);
937
+ }
938
+ const decodedIds = decodedPathIdForms(id);
939
+ // URL resolvers and intermediaries can normalize raw or pre-encoded dot
940
+ // segments before routing. Reject every decoded form we inspected so an
941
+ // item operation cannot be retargeted to a collection endpoint. Embedded
942
+ // `a/../b` remains an opaque single segment because the later URL builder
943
+ // percent-encodes its slash.
944
+ if (decodedIds.some((value) => value === "." || value === "..")) {
945
+ throw clientValidationError(`${method}: ${field} is an invalid URL path segment`);
946
+ }
947
+ // `/` is deliberately outside the general delimiter class so a URL path
948
+ // such as `/at_a_glance` is not misread as a credential. For non-URL input,
949
+ // `/at_` remains a strong pasted-path signal. Fragment, query, header, and
950
+ // pasted `Bearer at_...` / `token:at_...` forms are also strong signals.
951
+ // Bare `at_...` values are rejected only for resource/qURL identifiers:
952
+ // those current formats have fixed non-colliding prefixes, while domain,
953
+ // webhook, session, and API-key IDs are deliberately opaque/open contracts.
954
+ // Full URLs and delimiter/path-prefixed credentials remain rejected in
955
+ // every namespace.
956
+ const containsUrl = decodedIds.some(isUrlLikePathId);
957
+ const containsAccessToken = decodedIds.some((value) => {
958
+ const valueIsUrl = isUrlLikePathId(value);
959
+ return ((QURL_LINK_URL_RE.test(value) &&
960
+ (PATH_EMBEDDED_ACCESS_TOKEN_RE.test(value) || PATH_PREFIXED_ACCESS_TOKEN_RE.test(value))) ||
961
+ (valueIsUrl
962
+ ? URL_EMBEDDED_ACCESS_TOKEN_RE.test(value)
963
+ : PATH_EMBEDDED_ACCESS_TOKEN_RE.test(value) || PATH_PREFIXED_ACCESS_TOKEN_RE.test(value)) ||
964
+ (options.rejectBareAccessToken === true && value.slice(0, 3).toLowerCase() === "at_"));
965
+ });
966
+ if (containsAccessToken) {
967
+ const recovery = options.accessTokenRecovery ?? "pass the identifier returned by the API";
968
+ throw clientValidationError(`${method}: ${field} must not contain an access token; ${recovery}`);
969
+ }
970
+ // A full target/qURL is a common argument mix-up, but it is distinct from
971
+ // passing an access-token credential. Diagnose it accurately without
972
+ // echoing the URL, which may itself contain sensitive query parameters.
973
+ if (containsUrl) {
974
+ throw clientValidationError(`${method}: ${field} must be an identifier, not a URL`);
975
+ }
976
+ if (options.rejectQurlDisplayId === true &&
977
+ decodedIds.some((value) => QURL_DISPLAY_ID_PREFIX_RE.test(value))) {
978
+ throw clientValidationError(`${method}: ${field} must not be a qURL display ID; ${REVOKE_INDIVIDUAL_QURL_RECOVERY}`);
979
+ }
980
+ }
981
+ function decodedPathIdForms(id) {
982
+ // Probe repeatedly encoded input too. Callers sometimes pre-encode a copied
983
+ // link before passing it through a URL builder that encodes it again. Three
984
+ // passes cover the common accidental cases without allowing hostile input
985
+ // to create unbounded work. More deeply encoded or malformed-percent input
986
+ // deliberately fails open to the authoritative service after safe segment
987
+ // encoding; this heuristic is not an identifier parser.
988
+ const decodedIds = [id];
989
+ let decodedId = id;
990
+ for (let pass = 0; pass < MAX_PATH_ID_DECODE_PASSES; pass += 1) {
991
+ try {
992
+ const next = decodeURIComponent(decodedId);
993
+ if (next === decodedId)
994
+ break;
995
+ decodedIds.push(next);
996
+ decodedId = next;
997
+ }
998
+ catch {
999
+ // Malformed percent escapes remain opaque and are left to the service.
1000
+ break;
1001
+ }
1002
+ }
1003
+ return decodedIds;
1004
+ }
1005
+ function requireConnectorSlug(slug, method) {
1006
+ if (typeof slug !== "string" || !CONNECTOR_SLUG_PATTERN.test(slug)) {
1007
+ throw clientValidationError(`${method}: slug must be 3-64 lowercase alphanumeric or hyphen characters, start with a letter, and end alphanumeric`);
1008
+ }
1009
+ }
1010
+ function classifyConnectorMutationFailure(operation, error) {
1011
+ if (!(error instanceof errors_js_1.QURLError)) {
1012
+ throw new errors_js_1.ConnectorResourceOutcomeUnknownError(new errors_js_1.RuntimeError("Unexpected failure after Connector mutation dispatch", { cause: error }));
1013
+ }
1014
+ // Most authoritative 4xx responses prove that the mutation was rejected.
1015
+ // bootstrap_key_consumed is the exception: the service returns it from the
1016
+ // consume phase after the resource may have been bound or found.
1017
+ // Callers perform argument/runtime preflight outside their mutation try
1018
+ // blocks; every other surfaced failure therefore follows dispatch or a
1019
+ // nominal success whose resource contract could not be consumed, so callers
1020
+ // must reconcile before retrying. Ensure's exact 201 + valid resource but
1021
+ // missing found_existing is handled after this classifier: the row proves
1022
+ // the selected resource, while only its required metadata is unavailable.
1023
+ if (operation === "ensure" && error.status === 409 && error.code === "bootstrap_key_consumed") {
1024
+ throw new errors_js_1.ConnectorResourceOutcomeUnknownError(error);
1025
+ }
1026
+ if (error.status >= 400 && error.status < 500) {
1027
+ throw error;
1028
+ }
1029
+ throw new errors_js_1.ConnectorResourceOutcomeUnknownError(error);
1030
+ }
1031
+ function decodeCanonicalBase64Url(value) {
1032
+ if (!CONNECTOR_RESOURCE_ID_PATTERN.test(value) ||
1033
+ !value.startsWith(CONNECTOR_RESOURCE_ID_DER_PREFIX)) {
1034
+ return undefined;
1035
+ }
1036
+ try {
1037
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/") + "==";
1038
+ const binary = atob(padded);
1039
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
1040
+ const canonical = btoa(String.fromCharCode(...bytes))
1041
+ .replace(/\+/g, "-")
1042
+ .replace(/\//g, "_")
1043
+ .replace(/=+$/, "");
1044
+ return canonical === value ? bytes : undefined;
1045
+ }
1046
+ catch {
1047
+ return undefined;
1048
+ }
1049
+ }
1050
+ function requireConnectorSubtleCrypto(method) {
1051
+ if (!globalThis.crypto?.subtle) {
1052
+ throw new errors_js_1.RuntimeError(`${method}: requires the Web Crypto SubtleCrypto API`);
1053
+ }
1054
+ }
1055
+ function requireResourceCrid(crid, method, field = "id") {
1056
+ if ((0, crid_js_1.parseCrid)(crid))
1057
+ return;
1058
+ // Keep actionable paste errors without inspecting valid CRIDs twice.
1059
+ validatePathId(crid, method, field, {
1060
+ rejectBareAccessToken: true,
1061
+ rejectQurlDisplayId: true,
1062
+ accessTokenRecovery: method === "delete" || method === "deleteResource"
1063
+ ? DELETE_RESOURCE_RECOVERY
1064
+ : "pass a CRID returned by the API",
1065
+ });
1066
+ throw clientValidationError(`${method}: requires a valid CRID`);
1067
+ }
1068
+ async function importsAsConnectorPublicKey(der) {
1069
+ try {
1070
+ const keyData = new ArrayBuffer(der.byteLength);
1071
+ new Uint8Array(keyData).set(der);
1072
+ await globalThis.crypto.subtle.importKey("spki", keyData, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
1073
+ return true;
1074
+ }
1075
+ catch {
1076
+ return false;
1077
+ }
1078
+ }
1079
+ function containsControlCharacter(value) {
1080
+ return Array.from(value).some((character) => {
1081
+ const codePoint = character.codePointAt(0);
1082
+ return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
1083
+ });
1084
+ }
1085
+ async function verifyResourceIdentity(resource, method, expectedCrid) {
1086
+ if (!resource || typeof resource.resource_id !== "string") {
1087
+ throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
1088
+ }
1089
+ const resourceKey = decodeCanonicalBase64Url(resource.resource_id);
1090
+ requireConnectorSubtleCrypto(method);
1091
+ if (!resourceKey || !(await importsAsConnectorPublicKey(resourceKey))) {
1092
+ throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
1093
+ }
1094
+ if (expectedCrid !== undefined && resource.crid !== expectedCrid) {
1095
+ throw unexpectedResponseError(`${method}: response crid does not match the request`);
1096
+ }
1097
+ if (!(await (0, crid_js_1.cridKeyMatches)(resource.crid, resourceKey))) {
1098
+ throw unexpectedResponseError(`${method}: response has missing, invalid, or public-key-mismatched crid`);
1099
+ }
1100
+ }
1101
+ async function parseConnectorResource(client, value, method, expectation = {}) {
1102
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1103
+ throw unexpectedResponseError(`${method}: response is missing connector resource data`);
1104
+ }
1105
+ const resource = value;
1106
+ await verifyResourceIdentity(resource, method, expectation.crid);
1107
+ if (typeof resource.connector_routing_id !== "string" ||
1108
+ !CONNECTOR_ROUTING_ID_PATTERN.test(resource.connector_routing_id)) {
1109
+ throw unexpectedResponseError(`${method}: response has missing or invalid connector_routing_id`);
1110
+ }
1111
+ if (typeof resource.knock_resource_id !== "string" ||
1112
+ resource.knock_resource_id.trim() === "" ||
1113
+ resource.knock_resource_id.trim() !== resource.knock_resource_id ||
1114
+ containsControlCharacter(resource.knock_resource_id)) {
1115
+ throw unexpectedResponseError(`${method}: response has missing or invalid knock_resource_id`);
1116
+ }
1117
+ if (resource.resource_id === resource.knock_resource_id ||
1118
+ resource.connector_routing_id === resource.knock_resource_id) {
1119
+ throw unexpectedResponseError(`${method}: response cross-wires resource identity, routing, and admission values`);
1120
+ }
1121
+ if (resource.type !== "tunnel") {
1122
+ throw unexpectedResponseError(`${method}: response resource type is not tunnel`);
1123
+ }
1124
+ if (typeof resource.slug !== "string" || !CONNECTOR_SLUG_PATTERN.test(resource.slug)) {
1125
+ throw unexpectedResponseError(`${method}: response has missing or invalid slug`);
1126
+ }
1127
+ if (expectation.slug !== undefined && resource.slug !== expectation.slug) {
1128
+ throw unexpectedResponseError(`${method}: response slug does not match the request`);
1129
+ }
1130
+ if (resource.alias !== undefined &&
1131
+ resource.alias !== null &&
1132
+ (typeof resource.alias !== "string" || !CONNECTOR_ALIAS_PATTERN.test(resource.alias))) {
1133
+ // The public API intentionally gives mutable aliases and immutable slugs the
1134
+ // same canonical wire grammar (domain aliasPattern/slugPattern). Keeping
1135
+ // response validation aligned avoids accepting a row the service itself
1136
+ // could not create or update.
1137
+ throw unexpectedResponseError(`${method}: response has invalid alias`);
1138
+ }
1139
+ if (resource.status === "revoked") {
1140
+ if (expectation.revokedIsLifecycleError !== true) {
1141
+ throw unexpectedResponseError(`${method}: active-only operation returned a revoked connector resource`);
1142
+ }
1143
+ throw new errors_js_1.QURLError({
1144
+ status: 0,
1145
+ code: errors_js_1.ERROR_CODE_CONNECTOR_RESOURCE_REVOKED,
1146
+ title: "Connector Resource Revoked",
1147
+ detail: `${method}: qURL Connector resource is revoked`,
1148
+ });
1149
+ }
1150
+ if (resource.status !== "active") {
1151
+ throw unexpectedResponseError(`${method}: response has invalid resource status`);
1152
+ }
1153
+ return new ConnectorResource(client, resource, CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN);
847
1154
  }
848
1155
  function requireValidTags(tags) {
849
1156
  // null tolerance for untyped-JS callers — treated as "don't touch."
@@ -1213,6 +1520,56 @@ function validateApiKeyWriteFields(input, method, requiredFields = {}) {
1213
1520
  if (requiredFields.scopes || input.scopes !== undefined) {
1214
1521
  requireNonEmptyArrayField(input, "scopes", method);
1215
1522
  requireStringArrayElements(input.scopes, "scopes", method);
1523
+ const unsupported = input.scopes.find((scope) => !API_KEY_REQUEST_SCOPES.has(scope));
1524
+ if (unsupported !== undefined) {
1525
+ throw clientValidationError(`${method}: scopes contains an unsupported permission ${JSON.stringify(unsupported)}; this SDK accepts ${API_KEY_REQUEST_SCOPE_VALUES.join(", ")}`);
1526
+ }
1527
+ }
1528
+ }
1529
+ function validateEnrollmentTokenFields(input, method) {
1530
+ if (input.target !== undefined &&
1531
+ (typeof input.target !== "string" || !CREDENTIAL_TARGETS.has(input.target))) {
1532
+ throw clientValidationError(`${method}: target must be 'agent' or 'connector'`);
1533
+ }
1534
+ let claimCount = 0;
1535
+ if (input.claims !== undefined) {
1536
+ if (!Array.isArray(input.claims)) {
1537
+ throw clientValidationError(`${method}: claims must be an array`);
1538
+ }
1539
+ if (input.claims.length > 1) {
1540
+ throw clientValidationError(`${method}: claims must contain at most one claim`);
1541
+ }
1542
+ claimCount = input.claims.length;
1543
+ for (const [index, claim] of input.claims.entries()) {
1544
+ if (!claim || typeof claim !== "object" || Array.isArray(claim)) {
1545
+ throw clientValidationError(`${method}: claims[${index}] must be an object`);
1546
+ }
1547
+ requireNoUnknownFields(claim, ["type", "id"], `${method}: claims[${index}]`);
1548
+ const record = claim;
1549
+ if (typeof record.type !== "string" || !CREDENTIAL_CLAIM_TYPES.has(record.type)) {
1550
+ throw clientValidationError(`${method}: claims[${index}].type must be 'connector'`);
1551
+ }
1552
+ if (typeof record.id !== "string" || !CONNECTOR_SLUG_PATTERN.test(record.id)) {
1553
+ 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`);
1554
+ }
1555
+ }
1556
+ }
1557
+ // CreateApiKeyRequest.target permits an agent with zero or one connector claim.
1558
+ if (input.target === "connector" && claimCount !== 1) {
1559
+ throw clientValidationError(`${method}: target 'connector' requires exactly one claim`);
1560
+ }
1561
+ if (input.expires_in !== undefined) {
1562
+ if (typeof input.expires_in !== "string") {
1563
+ throw clientValidationError(`${method}: expires_in must be a duration string`);
1564
+ }
1565
+ const match = CREDENTIAL_DURATION_PATTERN.exec(input.expires_in);
1566
+ if (!match) {
1567
+ throw clientValidationError(`${method}: expires_in must match <integer><s|m|h|d|w>`);
1568
+ }
1569
+ const seconds = BigInt(match[1]) * CREDENTIAL_DURATION_MULTIPLIERS[match[2]];
1570
+ if (seconds <= 0n || seconds > MAX_ENROLLMENT_TOKEN_TTL_SECONDS) {
1571
+ throw clientValidationError(`${method}: expires_in must be greater than zero and at most 24h`);
1572
+ }
1216
1573
  }
1217
1574
  }
1218
1575
  /**
@@ -1408,15 +1765,17 @@ function parseApiDate(value) {
1408
1765
  * check; everything else is optional because the two mint endpoints differ
1409
1766
  * in which ancillary fields they return.
1410
1767
  */
1411
- function parsePortal(data, method) {
1768
+ async function parsePortal(data, method, expectedCrid) {
1412
1769
  if (typeof data?.resource_id !== "string" || data.resource_id.trim() === "") {
1413
1770
  throw unexpectedResponseError(`${method}: response is missing resource_id`);
1414
1771
  }
1415
1772
  if (typeof data.qurl_link !== "string" || data.qurl_link.trim() === "") {
1416
1773
  throw unexpectedResponseError(`${method}: response is missing qurl_link`);
1417
1774
  }
1775
+ await verifyResourceIdentity(data, method, expectedCrid);
1418
1776
  return {
1419
- resourceId: data.resource_id,
1777
+ crid: data.crid,
1778
+ resourcePublicKey: data.resource_id,
1420
1779
  link: data.qurl_link,
1421
1780
  site: data.qurl_site,
1422
1781
  expiresAt: parseApiDate(data.expires_at),
@@ -1427,10 +1786,9 @@ function parsePortal(data, method) {
1427
1786
  };
1428
1787
  }
1429
1788
  /**
1430
- * Validate portal options and build the create-portal wire body, or
1431
- * `undefined` when every option is omitted so no request body is sent and
1432
- * the server applies its default lifetime (matching mintLink /
1433
- * createQurlForResource).
1789
+ * Validate portal options and build the create-portal wire body. The service
1790
+ * requires a JSON object even when every option is omitted; `{}` selects the
1791
+ * owner's plan defaults and matches qurl-go's zero-value request struct.
1434
1792
  */
1435
1793
  function buildCreatePortalBody(opts, method) {
1436
1794
  requireObjectInput(opts, method);
@@ -1468,7 +1826,7 @@ function buildCreatePortalBody(opts, method) {
1468
1826
  body.session_duration = wireSessionDuration;
1469
1827
  if (targetPath !== undefined)
1470
1828
  body.target_path = targetPath;
1471
- return Object.keys(body).length > 0 ? body : undefined;
1829
+ return body;
1472
1830
  }
1473
1831
  /** qURL API client. */
1474
1832
  class QURLClient {
@@ -1764,76 +2122,136 @@ class QURLClient {
1764
2122
  body.alias = normalized.alias;
1765
2123
  validateResourceWriteFields(body);
1766
2124
  const resource = await this.request("POST", "/v1/resources", body, requestOptions);
1767
- if (typeof resource?.resource_id !== "string" || resource.resource_id.trim() === "") {
1768
- throw unexpectedResponseError("protectUrl: response is missing resource_id");
1769
- }
1770
- return new ProtectedResource(this, resource.resource_id,
2125
+ await verifyResourceIdentity(resource, "protectUrl");
2126
+ return new ProtectedResource(this, resource.crid,
1771
2127
  // Some resource types redact target_url in responses; fall back to
1772
2128
  // the caller-supplied URL so the handle stays useful.
1773
2129
  resource.target_url || targetUrl, resource);
1774
2130
  }
1775
2131
  /**
1776
- * Return a portal-minting handle for a stored resource id, without an API
2132
+ * Return a portal-minting handle for a stored CRID, without an API
1777
2133
  * call.
1778
2134
  *
1779
- * Use when you persisted a LayerV resource id and want to mint more
1780
- * portals for it (qurl-go: `Client.ResourceByID`):
2135
+ * Use when you persisted a LayerV CRID and want to mint more
2136
+ * portals for it (qurl-go: `Client.ResourceByCRID`):
1781
2137
  *
1782
2138
  * ```ts
1783
- * const resource = client.resourceById("r_demo1234567");
2139
+ * const resource = client.resourceByCrid(storedCrid);
1784
2140
  * const portal = await resource.createPortal({ validFor: "1h" });
1785
2141
  * ```
1786
2142
  *
1787
2143
  * The handle carries no server metadata; use {@link getResource} when you
1788
2144
  * need the full resource details.
1789
2145
  */
1790
- resourceById(id) {
1791
- requireNonEmptyId(id, "resourceById");
2146
+ resourceByCrid(id) {
2147
+ requireResourceCrid(id, "resourceByCrid");
1792
2148
  return new ProtectedResource(this, id);
1793
2149
  }
1794
- /**
1795
- * Return the resource qURL Connector created for `connectorId`.
1796
- *
1797
- * Use when qURL Connector already protects the service — do not call
1798
- * {@link protectUrl} again for the same service. The connector id is the
1799
- * resource slug LayerV stores for that connector; the LayerV API performs
1800
- * the slug lookup, and the SDK confirms the returned alias matches
1801
- * `connectorId` before binding the handle (qurl-go:
1802
- * `Client.ConnectorResource`).
1803
- *
1804
- * Throws {@link NotFoundError} (`status: 0`, `code: "resource_not_found"`)
1805
- * when no resource exists for the connector id, {@link QURLError}
1806
- * (`code: "ambiguous_resource"`) when the lookup returns more than one
1807
- * resource, and {@link ValidationError} (`code: "unexpected_response"`)
1808
- * when the returned resource's alias is missing or does not match.
1809
- */
1810
- async connectorResource(connectorId) {
1811
- requireNonEmptyId(connectorId, "connectorResource", "connector id");
1812
- const { resources } = await this.listResources({ slug: connectorId });
1813
- if (resources.length === 0) {
2150
+ /** Find or create the active qURL Connector resource for an immutable slug. */
2151
+ async ensureConnectorResource(slug, requestOptions) {
2152
+ requireConnectorSlug(slug, "ensureConnectorResource");
2153
+ // Response validation imports the producer's P-256 resource key. Preflight
2154
+ // before POST so a missing runtime capability cannot orphan a committed row.
2155
+ requireConnectorSubtleCrypto("ensureConnectorResource");
2156
+ // Keep invalid options in the known pre-dispatch error arm. rawRequest
2157
+ // validates again when building the request, which protects future callers
2158
+ // that do not have this mutation-specific outcome contract.
2159
+ validateRequestOptions(requestOptions);
2160
+ let response;
2161
+ let resource;
2162
+ try {
2163
+ response = await this.rawRequest("POST", "/v1/resources", {
2164
+ type: "tunnel",
2165
+ slug,
2166
+ find_or_create: true,
2167
+ }, { requestOptions, retry: false, generateIdempotencyKey: false });
2168
+ // The find-or-create endpoint returns 201 for both newly-created
2169
+ // and found-existing rows; meta.found_existing distinguishes the arms.
2170
+ if (response.__http_status !== 201) {
2171
+ throw unexpectedResponseError(`ensureConnectorResource: expected HTTP 201, got ${response.__http_status ?? "unknown"}`);
2172
+ }
2173
+ resource = await parseConnectorResource(this, response.data, "ensureConnectorResource", {
2174
+ slug,
2175
+ });
2176
+ }
2177
+ catch (error) {
2178
+ classifyConnectorMutationFailure("ensure", error);
2179
+ }
2180
+ if (response.meta !== undefined &&
2181
+ response.meta !== null &&
2182
+ (typeof response.meta !== "object" || Array.isArray(response.meta))) {
2183
+ classifyConnectorMutationFailure("ensure", unexpectedResponseError("ensureConnectorResource: response has invalid meta"));
2184
+ }
2185
+ const foundExisting = response.meta
2186
+ ?.found_existing;
2187
+ if (foundExisting === undefined || foundExisting === null) {
2188
+ throw unexpectedResponseError("ensureConnectorResource: response is missing meta.found_existing; the validated resource exists, so reconcile by slug before retrying");
2189
+ }
2190
+ if (typeof foundExisting !== "boolean") {
2191
+ classifyConnectorMutationFailure("ensure", unexpectedResponseError("ensureConnectorResource: response has invalid meta.found_existing"));
2192
+ }
2193
+ return { resource, foundExisting };
2194
+ }
2195
+ /** Fetch a qURL Connector resource by its CRID. */
2196
+ async getConnectorResource(crid) {
2197
+ requireResourceCrid(crid, "getConnectorResource");
2198
+ requireConnectorSubtleCrypto("getConnectorResource");
2199
+ const { data, __http_status } = await this.rawRequest("GET", `/v1/resources/${encodeURIComponent(crid)}`, undefined, { retry: false });
2200
+ if (__http_status !== 200) {
2201
+ throw unexpectedResponseError(`getConnectorResource: expected HTTP 200, got ${__http_status ?? "unknown"}`);
2202
+ }
2203
+ return parseConnectorResource(this, data?.resource, "getConnectorResource", {
2204
+ crid,
2205
+ revokedIsLifecycleError: true,
2206
+ });
2207
+ }
2208
+ /** Fetch the single active qURL Connector resource for an immutable slug. */
2209
+ async getConnectorResourceBySlug(slug) {
2210
+ requireConnectorSlug(slug, "getConnectorResourceBySlug");
2211
+ // Parsing validates the returned P-256 resource key; avoid dispatching a
2212
+ // read that this runtime cannot safely consume.
2213
+ requireConnectorSubtleCrypto("getConnectorResourceBySlug");
2214
+ // The slug point lookup is intrinsically active-only. It also
2215
+ // rejects combining `slug` with `status`, so this query must stay slug-only.
2216
+ const { data, __http_status } = await this.rawRequest("GET", `/v1/resources?slug=${encodeURIComponent(slug)}`, undefined, { retry: false });
2217
+ if (__http_status !== 200) {
2218
+ throw unexpectedResponseError(`getConnectorResourceBySlug: expected HTTP 200, got ${__http_status ?? "unknown"}`);
2219
+ }
2220
+ if (!Array.isArray(data)) {
2221
+ throw unexpectedResponseError("getConnectorResourceBySlug: response has missing or invalid data");
2222
+ }
2223
+ if (data.length === 0) {
1814
2224
  throw new errors_js_1.NotFoundError({
1815
2225
  status: 0,
1816
2226
  code: errors_js_1.ERROR_CODE_RESOURCE_NOT_FOUND,
1817
2227
  title: "Resource Not Found",
1818
- detail: `connectorResource: no resource found for connector "${connectorId}"`,
2228
+ detail: "getConnectorResourceBySlug: no active resource exists for the requested slug",
1819
2229
  });
1820
2230
  }
1821
- if (resources.length > 1) {
2231
+ if (data.length > 1) {
1822
2232
  throw new errors_js_1.QURLError({
1823
2233
  status: 0,
1824
2234
  code: errors_js_1.ERROR_CODE_AMBIGUOUS_RESOURCE,
1825
2235
  title: "Ambiguous Resource",
1826
- detail: `connectorResource: connector "${connectorId}" returned ${resources.length} resources`,
2236
+ detail: `getConnectorResourceBySlug: expected one active resource, got ${data.length}`,
1827
2237
  });
1828
2238
  }
1829
- const resource = resources[0];
1830
- if (resource.alias !== connectorId) {
1831
- throw unexpectedResponseError(`connectorResource: connector "${connectorId}" returned a resource with a missing or different alias`);
2239
+ return parseConnectorResource(this, data[0], "getConnectorResourceBySlug", { slug });
2240
+ }
2241
+ /**
2242
+ * Revoke a qURL Connector resource by CRID.
2243
+ *
2244
+ * The API does not apply Idempotency-Key replay to DELETE. After an
2245
+ * outcome-unknown failure, reconcile by ID before issuing a deliberate retry.
2246
+ */
2247
+ async deleteConnectorResource(crid) {
2248
+ requireResourceCrid(crid, "deleteConnectorResource");
2249
+ try {
2250
+ await this.requestNoContent(`/v1/resources/${encodeURIComponent(crid)}`);
1832
2251
  }
1833
- if (typeof resource.resource_id !== "string" || resource.resource_id.trim() === "") {
1834
- throw unexpectedResponseError("connectorResource: response is missing resource_id");
2252
+ catch (error) {
2253
+ classifyConnectorMutationFailure("delete", error);
1835
2254
  }
1836
- return new ProtectedResource(this, resource.resource_id, resource.target_url, resource);
1837
2255
  }
1838
2256
  /**
1839
2257
  * Mint a short-lived qURL link (a portal) for a protected resource.
@@ -1842,7 +2260,7 @@ class QURLClient {
1842
2260
  * reach one private resource. Recipients open `portal.link` directly and
1843
2261
  * need no LayerV credentials. Prefer short lifetimes such as
1844
2262
  * `{ validFor: "5m" }`. Accepts a {@link ProtectedResource} handle or a
1845
- * resource id (`r_` prefix). REST-shaped equivalents: {@link mintLink} /
2263
+ * CRID string. REST-shaped equivalents: {@link mintLink} /
1846
2264
  * {@link createQurlForResource}.
1847
2265
  *
1848
2266
  * Duration options take a string (`"5m"`, `"24h"`; server-validated) or a
@@ -1861,22 +2279,22 @@ class QURLClient {
1861
2279
  if (!ProtectedResource.isBoundTo(resource, this)) {
1862
2280
  throw clientValidationError("createPortal: resource is bound to a different client");
1863
2281
  }
1864
- resourceId = resource.id;
2282
+ resourceId = resource.crid;
1865
2283
  }
1866
2284
  else {
1867
- throw clientValidationError(`createPortal: resource must be a ProtectedResource handle or a resource id string (got ${describeShape(resource)})`);
2285
+ throw clientValidationError(`createPortal: resource must be a ProtectedResource handle or a CRID string (got ${describeShape(resource)})`);
1868
2286
  }
1869
- requireNonEmptyId(resourceId, "createPortal", "resource id");
2287
+ requireResourceCrid(resourceId, "createPortal");
1870
2288
  const body = buildCreatePortalBody(opts, "createPortal");
1871
2289
  const data = await this.request("POST", `/v1/resources/${encodeURIComponent(resourceId)}/qurls`, body, requestOptions);
1872
- return parsePortal(data, "createPortal");
2290
+ return parsePortal(data, "createPortal", resourceId);
1873
2291
  }
1874
2292
  /**
1875
2293
  * Protect `targetUrl` and mint a portal in one API call.
1876
2294
  *
1877
2295
  * Convenience for one-off scripts (qurl-go: `Client.CreatePortalForURL`).
1878
2296
  * The returned resource handle is reusable for minting more portals but
1879
- * carries only the resource id and the caller-supplied target URL; use
2297
+ * carries the CRID, verification key, and the caller-supplied target URL; use
1880
2298
  * {@link protectUrl} when you need the full server-populated resource
1881
2299
  * metadata. Portal options match {@link createPortal}; `targetUrl` gets
1882
2300
  * the same malformed-URL and embedded-credentials rejection as
@@ -1891,10 +2309,10 @@ class QURLClient {
1891
2309
  ...buildCreatePortalBody(opts, "createPortalForUrl"),
1892
2310
  };
1893
2311
  const data = await this.request("POST", "/v1/qurls", body, requestOptions);
1894
- const portal = parsePortal(data, "createPortalForUrl");
2312
+ const portal = await parsePortal(data, "createPortalForUrl");
1895
2313
  return {
1896
2314
  portal,
1897
- resource: new ProtectedResource(this, portal.resourceId, targetUrl),
2315
+ resource: new ProtectedResource(this, portal.crid, targetUrl),
1898
2316
  };
1899
2317
  }
1900
2318
  /**
@@ -1928,7 +2346,7 @@ class QURLClient {
1928
2346
  return {
1929
2347
  resourceUrl: resolved.target_url,
1930
2348
  openSeconds: resolved.access_grant?.expires_in ?? 0,
1931
- resourceId: resolved.resource_id,
2349
+ resourcePublicKey: resolved.resource_id,
1932
2350
  };
1933
2351
  }
1934
2352
  // --- Public API ---
@@ -2091,11 +2509,12 @@ class QURLClient {
2091
2509
  /**
2092
2510
  * Get a qURL resource and its access tokens.
2093
2511
  *
2094
- * Accepts either a resource ID (`r_` prefix) or a qURL display ID (`q_`
2095
- * prefix); the API resolves `q_` IDs to the parent resource automatically.
2512
+ * Accepts a CRID or qURL
2513
+ * display ID (`q_` prefix); the API resolves display IDs to the parent
2514
+ * resource automatically.
2096
2515
  */
2097
2516
  async get(id) {
2098
- requireNonEmptyId(id, "get");
2517
+ validatePathId(id, "get", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
2099
2518
  const raw = await this.request("GET", `/v1/qurls/${encodeURIComponent(id)}`);
2100
2519
  return this.mapQurlsField(raw);
2101
2520
  }
@@ -2137,49 +2556,20 @@ class QURLClient {
2137
2556
  /**
2138
2557
  * Delete (revoke) a qURL resource and all its access tokens.
2139
2558
  *
2140
- * Only accepts a resource ID (`r_` prefix), not a qURL display ID (`q_`
2141
- * prefix). Per the OpenAPI spec: *"Requires a resource ID (r_ prefix).
2142
- * To revoke a single token, use DELETE /v1/resources/:id/qurls/:qurl_id"*.
2143
- * A client-side prefix check catches the mistake before the API round-trip.
2559
+ * Requires a CRID returned by the API. To revoke one qURL, use
2560
+ * {@link revokeResourceQurl} instead.
2561
+ *
2562
+ * @throws {ValidationError} If `id` is not a canonical CRID.
2144
2563
  */
2145
2564
  async delete(id) {
2146
- // Type guard for untyped-JS callers — without it, `(undefined).length`
2147
- // is a raw TypeError instead of the structured ValidationError the
2148
- // rest of the surface produces.
2149
- if (typeof id !== "string") {
2150
- throw clientValidationError(`delete: requires a resource ID (${RESOURCE_ID_PREFIX} prefix + suffix) — got ${id === null ? "null" : typeof id}`);
2151
- }
2152
- if (id.trim() !== id) {
2153
- throw clientValidationError("delete: id must not include leading or trailing whitespace");
2154
- }
2155
- // Too-short check runs BEFORE the prefix check so it catches
2156
- // bare-prefix inputs like `"r_"` (right prefix, no suffix) in
2157
- // addition to `""` / `"x"` / `"ab"` / `"q_"`. Without this
2158
- // ordering, an exact `"r_"` would pass the startsWith check and
2159
- // the SDK would send `DELETE /v1/qurls/r_` to the server —
2160
- // rejected server-side, but this catches it client-side without
2161
- // a round-trip.
2162
- if (id.length <= RESOURCE_ID_PREFIX.length) {
2163
- throw clientValidationError(`delete: requires a resource ID (${RESOURCE_ID_PREFIX} prefix + suffix) — got ${id.length} character${id.length === 1 ? "" : "s"}`);
2164
- }
2165
- if (!id.startsWith(RESOURCE_ID_PREFIX)) {
2166
- // Wrong-prefix branch: the input is long enough to plausibly be
2167
- // an ID but has the wrong prefix (e.g. `q_3a7f2c8e91b`).
2168
- // Echo only the 2-char prefix — never the raw ID
2169
- // — so observability pipelines don't end up with caller-supplied
2170
- // identifiers in error logs.
2171
- const observedPrefix = id.slice(0, RESOURCE_ID_PREFIX.length);
2172
- throw clientValidationError(`delete: only resource IDs (${RESOURCE_ID_PREFIX} prefix) are accepted — ` +
2173
- `got an ID starting with "${observedPrefix}". ` +
2174
- "To revoke a single access token, use the resource-scoped token endpoint.");
2175
- }
2565
+ requireResourceCrid(id, "delete");
2176
2566
  await this.requestNoContent(`/v1/qurls/${encodeURIComponent(id)}`);
2177
2567
  }
2178
2568
  /**
2179
2569
  * Extend a qURL's expiration.
2180
2570
  *
2181
- * Accepts either a resource ID (`r_` prefix) or a qURL display ID (`q_`
2182
- * prefix). Convenience method — delegates to {@link update} with only the
2571
+ * Accepts a CRID or qURL
2572
+ * display ID (`q_` prefix). Delegates to {@link update} with only the
2183
2573
  * expiration fields. `ExtendInput` shares its `extend_by` / `expires_at`
2184
2574
  * fields with `UpdateInput` but is *narrower in two ways*: (1) exactly
2185
2575
  * one of the two must be present (XOR via `?: never`), where `UpdateInput`
@@ -2191,7 +2581,7 @@ class QURLClient {
2191
2581
  * enforced separately by the runtime check inside `update()`.
2192
2582
  */
2193
2583
  async extend(id, input, options) {
2194
- requireNonEmptyId(id, "extend");
2584
+ validatePathId(id, "extend", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
2195
2585
  requireObjectInput(input, "extend");
2196
2586
  const { extend_by, expires_at } = input;
2197
2587
  if (extend_by === undefined && expires_at === undefined) {
@@ -2205,11 +2595,12 @@ class QURLClient {
2205
2595
  /**
2206
2596
  * Update a qURL — extend expiration, change description, rename tags.
2207
2597
  *
2208
- * Accepts either a resource ID (`r_` prefix) or a qURL display ID (`q_`
2209
- * prefix); the API resolves `q_` IDs to the parent resource automatically.
2598
+ * Accepts a CRID or qURL
2599
+ * display ID (`q_` prefix); the API resolves display IDs to the parent
2600
+ * resource automatically.
2210
2601
  */
2211
2602
  async update(id, input, options) {
2212
- requireNonEmptyId(id, "update");
2603
+ validatePathId(id, "update", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
2213
2604
  requireObjectInput(input, "update");
2214
2605
  requireNoUnknownFields(input, UPDATE_FIELD_KEYS, "update");
2215
2606
  // Normalize null → undefined so untyped-JS callers passing
@@ -2236,15 +2627,16 @@ class QURLClient {
2236
2627
  *
2237
2628
  * Portal-flow equivalent: {@link createPortal}.
2238
2629
  *
2239
- * Accepts either a resource ID (`r_` prefix) or a qURL display ID (`q_`
2240
- * prefix); the API resolves `q_` IDs to the parent resource automatically.
2630
+ * Accepts a CRID or qURL
2631
+ * display ID (`q_` prefix); the API resolves display IDs to the parent
2632
+ * resource automatically.
2241
2633
  *
2242
2634
  * Passing `{}` or an object with all fields `null`/`undefined` is
2243
2635
  * equivalent to omitting the second argument: no body is sent, and the
2244
2636
  * server applies its 24h default expiration.
2245
2637
  */
2246
2638
  async mintLink(id, input, options) {
2247
- requireNonEmptyId(id, "mintLink");
2639
+ validatePathId(id, "mintLink", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
2248
2640
  // Normalize null → omitted so untyped-JS callers passing
2249
2641
  // `{ expires_in: null, expires_at: "..." }` don't leak null into
2250
2642
  // the wire body via JSON.stringify and don't bypass the XOR check
@@ -2358,12 +2750,12 @@ class QURLClient {
2358
2750
  }
2359
2751
  /** Get one resource plus its bounded qURL preview. */
2360
2752
  async getResource(id) {
2361
- requireNonEmptyId(id, "getResource");
2753
+ requireResourceCrid(id, "getResource");
2362
2754
  return this.request("GET", `/v1/resources/${encodeURIComponent(id)}`);
2363
2755
  }
2364
2756
  /** Update resource metadata. */
2365
2757
  async updateResource(id, input, options) {
2366
- requireNonEmptyId(id, "updateResource");
2758
+ requireResourceCrid(id, "updateResource");
2367
2759
  requireObjectInput(input, "updateResource");
2368
2760
  requireNoUnknownFields(input, UPDATE_RESOURCE_FIELD_KEYS, "updateResource");
2369
2761
  const validationInput = normalizePatchFields(input, UPDATE_RESOURCE_FIELD_KEYS, { preserveNullFields: ["alias"] });
@@ -2377,7 +2769,7 @@ class QURLClient {
2377
2769
  }
2378
2770
  /** Revoke a resource and all of its qURLs. */
2379
2771
  async deleteResource(id) {
2380
- requireNonEmptyId(id, "deleteResource");
2772
+ requireResourceCrid(id, "deleteResource");
2381
2773
  await this.requestNoContent(`/v1/resources/${encodeURIComponent(id)}`);
2382
2774
  }
2383
2775
  /**
@@ -2386,7 +2778,7 @@ class QURLClient {
2386
2778
  * Portal-flow equivalent: {@link createPortal} (same endpoint).
2387
2779
  */
2388
2780
  async createQurlForResource(id, input, options) {
2389
- requireNonEmptyId(id, "createQurlForResource");
2781
+ requireResourceCrid(id, "createQurlForResource");
2390
2782
  let normalized = input;
2391
2783
  if (input !== undefined) {
2392
2784
  requireObjectInput(input, "createQurlForResource");
@@ -2397,16 +2789,83 @@ class QURLClient {
2397
2789
  validateQurlTokenOptions(normalized);
2398
2790
  return this.request("POST", `/v1/resources/${encodeURIComponent(id)}/qurls`, normalized, options);
2399
2791
  }
2792
+ /**
2793
+ * Mint a fresh, short-lived share link for an existing CRID.
2794
+ * The returned link is secret and is not retrievable after this response.
2795
+ * If you hold the trusted resource DER SPKI, call `ShareLink.verifyCrid()`
2796
+ * before delivery to verify that the response CRID derives from that key.
2797
+ */
2798
+ async shareResource(id, options = {}, requestOptions) {
2799
+ requireResourceCrid(id, "shareResource");
2800
+ requireObjectInput(options, "shareResource");
2801
+ requireNoUnknownFields(options, SHARE_RESOURCE_OPTION_KEYS, "shareResource");
2802
+ const body = {};
2803
+ if (options.ttlSeconds !== undefined) {
2804
+ if (typeof options.ttlSeconds !== "number" ||
2805
+ !Number.isSafeInteger(options.ttlSeconds) ||
2806
+ options.ttlSeconds <= 0) {
2807
+ throw clientValidationError("shareResource: ttlSeconds must be a positive safe integer");
2808
+ }
2809
+ body.ttl_seconds = options.ttlSeconds;
2810
+ }
2811
+ const data = await this.request("POST", `/v1/resources/${encodeURIComponent(id)}/share`, body, requestOptions);
2812
+ if (typeof data?.qurl !== "string" || data.qurl.trim() === "") {
2813
+ throw unexpectedResponseError("shareResource: response is missing qurl");
2814
+ }
2815
+ if (data.crid !== id) {
2816
+ throw unexpectedResponseError("shareResource: response CRID does not match the requested CRID");
2817
+ }
2818
+ const wire = data;
2819
+ for (const field of ["qurl_id", "type", "expires_at"]) {
2820
+ const value = wire[field];
2821
+ if (value !== undefined && value !== null && typeof value !== "string") {
2822
+ throw unexpectedResponseError(`shareResource: response has invalid ${field}`);
2823
+ }
2824
+ }
2825
+ for (const field of ["qurl_id", "type"]) {
2826
+ const value = wire[field];
2827
+ if (typeof value === "string" && (value.trim() === "" || value.trim() !== value)) {
2828
+ throw unexpectedResponseError(`shareResource: response has invalid ${field}`);
2829
+ }
2830
+ }
2831
+ if (wire.expires_in_seconds !== undefined &&
2832
+ wire.expires_in_seconds !== null &&
2833
+ (typeof wire.expires_in_seconds !== "number" ||
2834
+ !Number.isSafeInteger(wire.expires_in_seconds) ||
2835
+ wire.expires_in_seconds < 0)) {
2836
+ throw unexpectedResponseError("shareResource: response has invalid expires_in_seconds");
2837
+ }
2838
+ if (wire.single_use !== undefined &&
2839
+ wire.single_use !== null &&
2840
+ typeof wire.single_use !== "boolean") {
2841
+ throw unexpectedResponseError("shareResource: response has invalid single_use");
2842
+ }
2843
+ const expiresAt = parseApiDate(typeof wire.expires_at === "string" ? wire.expires_at : undefined);
2844
+ if (typeof wire.expires_at === "string" && expiresAt === undefined) {
2845
+ throw unexpectedResponseError("shareResource: response has invalid expires_at");
2846
+ }
2847
+ return new share_js_1.ShareLink({
2848
+ // Repair transport-only edge whitespace on the one-time credential;
2849
+ // ancillary identifiers fail closed instead of being normalized.
2850
+ link: data.qurl.trim(),
2851
+ qurlId: typeof wire.qurl_id === "string" ? wire.qurl_id : undefined,
2852
+ crid: id,
2853
+ type: typeof wire.type === "string" ? wire.type : undefined,
2854
+ expiresAt,
2855
+ expiresInSeconds: typeof wire.expires_in_seconds === "number" ? wire.expires_in_seconds : undefined,
2856
+ singleUse: typeof wire.single_use === "boolean" ? wire.single_use : undefined,
2857
+ });
2858
+ }
2400
2859
  /** Revoke a specific qURL token on a resource. */
2401
2860
  async revokeResourceQurl(id, qurlId) {
2402
- requireNonEmptyId(id, "revokeResourceQurl");
2403
- requireNonEmptyId(qurlId, "revokeResourceQurl");
2861
+ requireResourceCrid(id, "revokeResourceQurl");
2862
+ validatePathId(qurlId, "revokeResourceQurl", "qurl id", QURL_ID_PATH_OPTIONS);
2404
2863
  await this.requestNoContent(`/v1/resources/${encodeURIComponent(id)}/qurls/${encodeURIComponent(qurlId)}`);
2405
2864
  }
2406
2865
  /** Update a specific qURL token on a resource. */
2407
2866
  async updateResourceQurl(id, qurlId, input, options) {
2408
- requireNonEmptyId(id, "updateResourceQurl");
2409
- requireNonEmptyId(qurlId, "updateResourceQurl");
2867
+ requireResourceCrid(id, "updateResourceQurl");
2868
+ validatePathId(qurlId, "updateResourceQurl", "qurl id", QURL_ID_PATH_OPTIONS);
2410
2869
  requireObjectInput(input, "updateResourceQurl");
2411
2870
  requireNoUnknownFields(input, UPDATE_RESOURCE_QURL_FIELD_KEYS, "updateResourceQurl");
2412
2871
  const normalized = normalizePatchFields(input, UPDATE_RESOURCE_QURL_FIELD_KEYS);
@@ -2422,7 +2881,7 @@ class QURLClient {
2422
2881
  * this page and emits a debug log rather than pretending it fetched all pages.
2423
2882
  */
2424
2883
  async listResourceSessions(id) {
2425
- requireNonEmptyId(id, "listResourceSessions");
2884
+ requireResourceCrid(id, "listResourceSessions");
2426
2885
  const { data, meta } = await this.rawRequest("GET", `/v1/resources/${encodeURIComponent(id)}/sessions`);
2427
2886
  if (meta?.has_more || meta?.next_cursor) {
2428
2887
  this.log("listResourceSessions: pagination metadata surfaced on unpaginated endpoint", {
@@ -2439,7 +2898,7 @@ class QURLClient {
2439
2898
  }
2440
2899
  /** Terminate all active sessions for a resource and return the server count. */
2441
2900
  async terminateAllResourceSessions(id) {
2442
- requireNonEmptyId(id, "terminateAllResourceSessions");
2901
+ requireResourceCrid(id, "terminateAllResourceSessions");
2443
2902
  const path = `/v1/resources/${encodeURIComponent(id)}/sessions`;
2444
2903
  const { data, meta, __http_status } = await this.rawRequest("DELETE", path);
2445
2904
  if (__http_status === 204) {
@@ -2454,8 +2913,8 @@ class QURLClient {
2454
2913
  }
2455
2914
  /** Terminate a specific resource session. */
2456
2915
  async terminateResourceSession(id, sessionId) {
2457
- requireNonEmptyId(id, "terminateResourceSession");
2458
- requireNonEmptyId(sessionId, "terminateResourceSession");
2916
+ requireResourceCrid(id, "terminateResourceSession");
2917
+ validatePathId(sessionId, "terminateResourceSession", "session id");
2459
2918
  await this.requestNoContent(`/v1/resources/${encodeURIComponent(id)}/sessions/${encodeURIComponent(sessionId)}`);
2460
2919
  }
2461
2920
  /** List connector installations. */
@@ -2558,22 +3017,22 @@ class QURLClient {
2558
3017
  }
2559
3018
  /** Get custom domain status. */
2560
3019
  async getDomain(domain) {
2561
- requireNonEmptyId(domain, "getDomain", "domain");
3020
+ validatePathId(domain, "getDomain", "domain");
2562
3021
  return this.request("GET", `/v1/domains/${encodeURIComponent(domain)}`);
2563
3022
  }
2564
3023
  /** Remove a custom domain. */
2565
3024
  async deleteDomain(domain) {
2566
- requireNonEmptyId(domain, "deleteDomain", "domain");
3025
+ validatePathId(domain, "deleteDomain", "domain");
2567
3026
  await this.requestNoContent(`/v1/domains/${encodeURIComponent(domain)}`);
2568
3027
  }
2569
3028
  /** Trigger DNS verification for a custom domain. */
2570
3029
  async verifyDomain(domain, options) {
2571
- requireNonEmptyId(domain, "verifyDomain", "domain");
3030
+ validatePathId(domain, "verifyDomain", "domain");
2572
3031
  return this.request("POST", `/v1/domains/${encodeURIComponent(domain)}/verify`, undefined, options);
2573
3032
  }
2574
3033
  /** Regenerate a domain verification token. */
2575
3034
  async regenerateDomainToken(domain, options) {
2576
- requireNonEmptyId(domain, "regenerateDomainToken", "domain");
3035
+ validatePathId(domain, "regenerateDomainToken", "domain");
2577
3036
  return this.request("POST", `/v1/domains/${encodeURIComponent(domain)}/regenerate-token`, undefined, options);
2578
3037
  }
2579
3038
  /** List webhooks. */
@@ -2609,12 +3068,12 @@ class QURLClient {
2609
3068
  }
2610
3069
  /** Get a webhook. */
2611
3070
  async getWebhook(id) {
2612
- requireNonEmptyId(id, "getWebhook");
3071
+ validatePathId(id, "getWebhook");
2613
3072
  return this.request("GET", `/v1/webhooks/${encodeURIComponent(id)}`);
2614
3073
  }
2615
3074
  /** Update a webhook. */
2616
3075
  async updateWebhook(id, input, options) {
2617
- requireNonEmptyId(id, "updateWebhook");
3076
+ validatePathId(id, "updateWebhook");
2618
3077
  requireObjectInput(input, "updateWebhook");
2619
3078
  requireNoUnknownFields(input, UPDATE_WEBHOOK_FIELD_KEYS, "updateWebhook");
2620
3079
  const normalized = normalizePatchFields(input, UPDATE_WEBHOOK_FIELD_KEYS);
@@ -2624,17 +3083,17 @@ class QURLClient {
2624
3083
  }
2625
3084
  /** Delete a webhook. */
2626
3085
  async deleteWebhook(id) {
2627
- requireNonEmptyId(id, "deleteWebhook");
3086
+ validatePathId(id, "deleteWebhook");
2628
3087
  await this.requestNoContent(`/v1/webhooks/${encodeURIComponent(id)}`);
2629
3088
  }
2630
3089
  /** Regenerate a webhook signing secret. */
2631
3090
  async regenerateWebhookSecret(id, options) {
2632
- requireNonEmptyId(id, "regenerateWebhookSecret");
3091
+ validatePathId(id, "regenerateWebhookSecret");
2633
3092
  return this.request("POST", `/v1/webhooks/${encodeURIComponent(id)}/secret`, undefined, options);
2634
3093
  }
2635
3094
  /** List delivery attempts for a webhook. */
2636
3095
  async listWebhookDeliveries(id, input = {}) {
2637
- requireNonEmptyId(id, "listWebhookDeliveries");
3096
+ validatePathId(id, "listWebhookDeliveries");
2638
3097
  const { data, meta } = await this.rawRequest("GET", appendQuery(`/v1/webhooks/${encodeURIComponent(id)}/deliveries`, input, WEBHOOK_DELIVERY_LIST_PARAM_KEYS, "listWebhookDeliveries"));
2639
3098
  return pageFromMeta({ deliveries: data ?? [] }, meta);
2640
3099
  }
@@ -2645,7 +3104,7 @@ class QURLClient {
2645
3104
  * iteration begins.
2646
3105
  */
2647
3106
  listAllWebhookDeliveries(id, input = {}) {
2648
- requireNonEmptyId(id, "listAllWebhookDeliveries");
3107
+ validatePathId(id, "listAllWebhookDeliveries");
2649
3108
  validateListAllInput(input, WEBHOOK_DELIVERY_LIST_PARAM_KEYS, "listAllWebhookDeliveries");
2650
3109
  return this.paginateAll("listAllWebhookDeliveries", input, (pageInput) => this.listWebhookDeliveries(id, pageInput), (page) => page.deliveries);
2651
3110
  }
@@ -2654,10 +3113,34 @@ class QURLClient {
2654
3113
  requireObjectInput(input, "createApiKey");
2655
3114
  requireNoUnknownFields(input, CREATE_API_KEY_FIELD_KEYS, "createApiKey");
2656
3115
  const normalizedRecord = normalizePatchFields(input, CREATE_API_KEY_FIELD_KEYS);
3116
+ // `kind` is required by the server and has no default there. Filling it
3117
+ // in keeps the common durable-key call site unchanged.
3118
+ normalizedRecord.kind ??= "api_key";
3119
+ if (normalizedRecord.kind !== "api_key" && normalizedRecord.kind !== "enrollment_token") {
3120
+ throw clientValidationError("createApiKey: kind must be 'api_key' or 'enrollment_token'");
3121
+ }
3122
+ // `scopes` and `target`/`claims` are mutually exclusive by kind, and the
3123
+ // server rejects either mismatch with 400 invalid_input. Fail both
3124
+ // directions locally rather than paying a round trip.
3125
+ const isEnrollmentToken = normalizedRecord.kind === "enrollment_token";
3126
+ if (isEnrollmentToken) {
3127
+ if (normalizedRecord.scopes !== undefined) {
3128
+ throw clientValidationError("createApiKey: scopes is not accepted for kind 'enrollment_token'; " +
3129
+ "the server assigns them from target");
3130
+ }
3131
+ validateEnrollmentTokenFields(normalizedRecord, "createApiKey");
3132
+ }
3133
+ else {
3134
+ for (const field of ["target", "claims", "expires_in"]) {
3135
+ if (normalizedRecord[field] !== undefined) {
3136
+ throw clientValidationError(`createApiKey: ${field} is only accepted for kind 'enrollment_token'`);
3137
+ }
3138
+ }
3139
+ }
2657
3140
  const normalized = normalizedRecord;
2658
3141
  validateApiKeyWriteFields(normalizedRecord, "createApiKey", {
2659
3142
  name: true,
2660
- scopes: true,
3143
+ scopes: !isEnrollmentToken,
2661
3144
  });
2662
3145
  return this.request("POST", "/v1/api-keys", normalized, options);
2663
3146
  }
@@ -2678,7 +3161,7 @@ class QURLClient {
2678
3161
  }
2679
3162
  /** Update an API key. */
2680
3163
  async updateApiKey(keyId, input, options) {
2681
- requireNonEmptyId(keyId, "updateApiKey");
3164
+ validatePathId(keyId, "updateApiKey");
2682
3165
  requireObjectInput(input, "updateApiKey");
2683
3166
  requireNoUnknownFields(input, UPDATE_API_KEY_FIELD_KEYS, "updateApiKey");
2684
3167
  const normalized = normalizePatchFields(input, UPDATE_API_KEY_FIELD_KEYS);
@@ -2688,7 +3171,7 @@ class QURLClient {
2688
3171
  }
2689
3172
  /** Revoke an API key. */
2690
3173
  async revokeApiKey(keyId) {
2691
- requireNonEmptyId(keyId, "revokeApiKey");
3174
+ validatePathId(keyId, "revokeApiKey");
2692
3175
  await this.requestNoContent(`/v1/api-keys/${encodeURIComponent(keyId)}`);
2693
3176
  }
2694
3177
  /** Redeem an access code. */
@@ -2706,7 +3189,7 @@ class QURLClient {
2706
3189
  requireNoUnknownFields(input, CREATE_ACCESS_CODE_FIELD_KEYS, "createAccessCode");
2707
3190
  const normalizedRecord = normalizePatchFields(input, CREATE_ACCESS_CODE_FIELD_KEYS);
2708
3191
  const normalized = normalizedRecord;
2709
- requireNonEmptyStringField(normalizedRecord, "resource_id", "createAccessCode");
3192
+ requireResourceCrid(normalizedRecord.resource_id, "createAccessCode", "resource_id");
2710
3193
  return this.request("POST", "/v1/access-codes", normalized, options);
2711
3194
  }
2712
3195
  /**
@@ -2733,7 +3216,7 @@ class QURLClient {
2733
3216
  }
2734
3217
  /** Revoke an access code. */
2735
3218
  async revokeAccessCode(id) {
2736
- requireNonEmptyId(id, "revokeAccessCode");
3219
+ validatePathId(id, "revokeAccessCode");
2737
3220
  await this.requestNoContent(`/v1/access-codes/${encodeURIComponent(id)}`);
2738
3221
  }
2739
3222
  // --- Internal HTTP plumbing ---
@@ -2775,8 +3258,8 @@ class QURLClient {
2775
3258
  */
2776
3259
  async rawRequest(method, path, body, rawOptions = {}) {
2777
3260
  const url = `${this.baseUrl}${path}`;
2778
- const { passthroughStatuses = NO_PASSTHROUGH_STATUSES, requestOptions, allowEmptySuccessBody = false, } = rawOptions;
2779
- const idempotencyKey = idempotencyKeyForRequest(method, requestOptions);
3261
+ const { passthroughStatuses = NO_PASSTHROUGH_STATUSES, requestOptions, allowEmptySuccessBody = false, retry = true, generateIdempotencyKey = true, } = rawOptions;
3262
+ const idempotencyKey = idempotencyKeyForRequest(method, requestOptions, generateIdempotencyKey);
2780
3263
  const headers = {
2781
3264
  Authorization: `Bearer ${this.apiKey}`,
2782
3265
  Accept: "application/json",
@@ -2795,13 +3278,15 @@ class QURLClient {
2795
3278
  // retry rule and avoids SDK pacing on an application request path. The
2796
3279
  // caller receives retryAfter when supplied and must reconcile resource
2797
3280
  // state before it chooses to issue another delete.
2798
- const idempotencyKeyBackedRetry = MUTATING_RETRY_METHODS.has(method);
2799
- const retryFetchFailure = method === "GET" || idempotencyKeyBackedRetry;
2800
- const retryable = method === "GET"
2801
- ? RETRYABLE_STATUS
2802
- : idempotencyKeyBackedRetry
2803
- ? RETRYABLE_STATUS_MUTATING
2804
- : NO_RETRYABLE_STATUSES;
3281
+ const idempotencyKeyBackedRetry = retry && MUTATING_RETRY_METHODS.has(method);
3282
+ const retryFetchFailure = retry && (method === "GET" || idempotencyKeyBackedRetry);
3283
+ const retryable = !retry
3284
+ ? NO_RETRYABLE_STATUSES
3285
+ : method === "GET"
3286
+ ? RETRYABLE_STATUS
3287
+ : idempotencyKeyBackedRetry
3288
+ ? RETRYABLE_STATUS_MUTATING
3289
+ : NO_RETRYABLE_STATUSES;
2805
3290
  const serializedBody = body !== undefined ? JSON.stringify(body) : undefined;
2806
3291
  let lastError;
2807
3292
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
@@ -3219,8 +3704,7 @@ exports.QURLClient = QURLClient;
3219
3704
  /**
3220
3705
  * A LayerV-protected resource, bound to the client that produced it.
3221
3706
  *
3222
- * Obtained from {@link QURLClient.protectUrl},
3223
- * {@link QURLClient.connectorResource}, or {@link QURLClient.resourceById} —
3707
+ * Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.resourceByCrid} —
3224
3708
  * not constructed directly. Mint short-lived access links for the resource
3225
3709
  * with {@link createPortal} (qurl-go: `qurl.Resource`).
3226
3710
  */
@@ -3229,19 +3713,19 @@ class ProtectedResource {
3229
3713
  // CONTRIBUTING.md's dual-build rule bars) so the binding stays on the
3230
3714
  // instance and out of Object.keys/JSON.stringify output.
3231
3715
  #client;
3232
- /** The LayerV resource id (`r_` prefix). */
3233
- id;
3716
+ /** The public resource CRID. */
3717
+ crid;
3234
3718
  /** The private URL protected by this resource, when known. */
3235
3719
  targetUrl;
3236
3720
  /**
3237
3721
  * Full server-populated {@link Resource} metadata when the handle came
3238
- * from an API response; `undefined` for {@link QURLClient.resourceById}
3722
+ * from an API response; `undefined` for {@link QURLClient.resourceByCrid}
3239
3723
  * handles and for the one-call {@link QURLClient.createPortalForUrl} path.
3240
3724
  */
3241
3725
  details;
3242
3726
  constructor(client, id, targetUrl, details) {
3243
3727
  this.#client = client;
3244
- this.id = id;
3728
+ this.crid = id;
3245
3729
  this.targetUrl = targetUrl;
3246
3730
  this.details = details;
3247
3731
  }
@@ -3263,3 +3747,39 @@ class ProtectedResource {
3263
3747
  }
3264
3748
  }
3265
3749
  exports.ProtectedResource = ProtectedResource;
3750
+ const CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN = Symbol("validated ConnectorResource");
3751
+ /**
3752
+ * A validated qURL Connector resource bound to the client that loaded it.
3753
+ *
3754
+ * Resource identity, reverse-routing identity, and NHP admission identity are
3755
+ * distinct server-issued values. Callers must consume each field verbatim and
3756
+ * must never derive one from another.
3757
+ */
3758
+ class ConnectorResource {
3759
+ #client;
3760
+ resourcePublicKey;
3761
+ /** Required public identifier, verified against the returned resource key. */
3762
+ crid;
3763
+ connectorRoutingId;
3764
+ knockResourceId;
3765
+ slug;
3766
+ alias;
3767
+ /** @internal Instances are returned by QURLClient after wire validation. */
3768
+ constructor(client, details, validationToken) {
3769
+ if (validationToken !== CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN) {
3770
+ throw clientValidationError("ConnectorResource cannot be constructed directly; load it through QURLClient");
3771
+ }
3772
+ this.#client = client;
3773
+ this.resourcePublicKey = details.resource_id;
3774
+ this.crid = details.crid;
3775
+ this.connectorRoutingId = details.connector_routing_id;
3776
+ this.knockResourceId = details.knock_resource_id;
3777
+ this.slug = details.slug;
3778
+ this.alias = details.alias ?? undefined;
3779
+ }
3780
+ /** Mint a short-lived portal for this Connector resource. */
3781
+ async createPortal(opts = {}, requestOptions) {
3782
+ return this.#client.createPortal(this.crid, opts, requestOptions);
3783
+ }
3784
+ }
3785
+ exports.ConnectorResource = ConnectorResource;