@layervai/qurl 0.6.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
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 = [
@@ -718,6 +726,41 @@ 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;
@@ -735,7 +778,6 @@ const CONNECTOR_ROUTING_ID_PATTERN = /^c-[a-z2-7]{51}[aq]$/;
735
778
  // CreateQurlRequest.target_url pattern is loose (just a URI) but
736
779
  // UpdateQurlRequest.tags pattern is specific — enforce it here.
737
780
  const TAG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9 _-]*$/;
738
- const RESOURCE_ID_PREFIX = "r_";
739
781
  function requireMaxLength(value, field, max) {
740
782
  if (value === undefined)
741
783
  return;
@@ -836,22 +878,129 @@ function pageFromMeta(out, meta) {
836
878
  page_size: meta?.page_size,
837
879
  };
838
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
+ };
839
910
  /**
840
911
  * Validates that a path-parameter argument is a non-empty string. Some callers
841
912
  * pass resource/qURL display IDs, while newer endpoints also pass domains,
842
913
  * session IDs, webhook IDs, and API-key IDs, so this intentionally enforces
843
- * 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.
844
916
  */
845
- 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
+ }
846
922
  // `.trim()` catches whitespace-only and padded IDs before they round-trip as
847
923
  // `%20...%20` paths that the server can only reject with a less useful 404.
848
924
  // The pre-flight error is more actionable than the 404.
849
- 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() === "") {
850
930
  throw clientValidationError(`${method}: ${field} is required`);
851
931
  }
852
932
  if (id.trim() !== id) {
853
933
  throw clientValidationError(`${method}: ${field} must not include leading or trailing whitespace`);
854
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;
855
1004
  }
856
1005
  function requireConnectorSlug(slug, method) {
857
1006
  if (typeof slug !== "string" || !CONNECTOR_SLUG_PATTERN.test(slug)) {
@@ -898,29 +1047,23 @@ function decodeCanonicalBase64Url(value) {
898
1047
  return undefined;
899
1048
  }
900
1049
  }
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
1050
  function requireConnectorSubtleCrypto(method) {
911
1051
  if (!globalThis.crypto?.subtle) {
912
1052
  throw new errors_js_1.RuntimeError(`${method}: requires the Web Crypto SubtleCrypto API`);
913
1053
  }
914
1054
  }
915
- async function requireConnectorResourceId(resourceId, method) {
916
- const der = decodeCanonicalBase64Url(resourceId);
917
- if (!der) {
918
- throw clientValidationError(`${method}: resource id must be a canonical unpadded base64url P-256 DER SPKI public key`);
919
- }
920
- requireConnectorSubtleCrypto(method);
921
- if (!(await importsAsConnectorPublicKey(der))) {
922
- throw clientValidationError(`${method}: resource id must be a canonical unpadded base64url P-256 DER SPKI public key`);
923
- }
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`);
924
1067
  }
925
1068
  async function importsAsConnectorPublicKey(der) {
926
1069
  try {
@@ -939,23 +1082,28 @@ function containsControlCharacter(value) {
939
1082
  return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
940
1083
  });
941
1084
  }
942
- async function parseConnectorResource(client, value, method, expectation = {}) {
943
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
944
- throw unexpectedResponseError(`${method}: response is missing connector resource data`);
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`);
945
1088
  }
946
- const resource = value;
947
- if (typeof resource.resource_id !== "string") {
1089
+ const resourceKey = decodeCanonicalBase64Url(resource.resource_id);
1090
+ requireConnectorSubtleCrypto(method);
1091
+ if (!resourceKey || !(await importsAsConnectorPublicKey(resourceKey))) {
948
1092
  throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
949
1093
  }
950
- if (expectation.resourceId !== undefined && resource.resource_id !== expectation.resourceId) {
951
- throw unexpectedResponseError(`${method}: response resource_id does not match the request`);
1094
+ if (expectedCrid !== undefined && resource.crid !== expectedCrid) {
1095
+ throw unexpectedResponseError(`${method}: response crid does not match the request`);
952
1096
  }
953
- // A by-ID request already validated its requested resource key and matched
954
- // this response byte-for-byte, so do not repeat the async key import.
955
- if (expectation.resourceId === undefined &&
956
- !(await isValidConnectorResourceId(resource.resource_id))) {
957
- throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
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`);
958
1104
  }
1105
+ const resource = value;
1106
+ await verifyResourceIdentity(resource, method, expectation.crid);
959
1107
  if (typeof resource.connector_routing_id !== "string" ||
960
1108
  !CONNECTOR_ROUTING_ID_PATTERN.test(resource.connector_routing_id)) {
961
1109
  throw unexpectedResponseError(`${method}: response has missing or invalid connector_routing_id`);
@@ -988,12 +1136,6 @@ async function parseConnectorResource(client, value, method, expectation = {}) {
988
1136
  // could not create or update.
989
1137
  throw unexpectedResponseError(`${method}: response has invalid alias`);
990
1138
  }
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
1139
  if (resource.status === "revoked") {
998
1140
  if (expectation.revokedIsLifecycleError !== true) {
999
1141
  throw unexpectedResponseError(`${method}: active-only operation returned a revoked connector resource`);
@@ -1378,6 +1520,56 @@ function validateApiKeyWriteFields(input, method, requiredFields = {}) {
1378
1520
  if (requiredFields.scopes || input.scopes !== undefined) {
1379
1521
  requireNonEmptyArrayField(input, "scopes", method);
1380
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
+ }
1381
1573
  }
1382
1574
  }
1383
1575
  /**
@@ -1573,15 +1765,17 @@ function parseApiDate(value) {
1573
1765
  * check; everything else is optional because the two mint endpoints differ
1574
1766
  * in which ancillary fields they return.
1575
1767
  */
1576
- function parsePortal(data, method) {
1768
+ async function parsePortal(data, method, expectedCrid) {
1577
1769
  if (typeof data?.resource_id !== "string" || data.resource_id.trim() === "") {
1578
1770
  throw unexpectedResponseError(`${method}: response is missing resource_id`);
1579
1771
  }
1580
1772
  if (typeof data.qurl_link !== "string" || data.qurl_link.trim() === "") {
1581
1773
  throw unexpectedResponseError(`${method}: response is missing qurl_link`);
1582
1774
  }
1775
+ await verifyResourceIdentity(data, method, expectedCrid);
1583
1776
  return {
1584
- resourceId: data.resource_id,
1777
+ crid: data.crid,
1778
+ resourcePublicKey: data.resource_id,
1585
1779
  link: data.qurl_link,
1586
1780
  site: data.qurl_site,
1587
1781
  expiresAt: parseApiDate(data.expires_at),
@@ -1928,31 +2122,29 @@ class QURLClient {
1928
2122
  body.alias = normalized.alias;
1929
2123
  validateResourceWriteFields(body);
1930
2124
  const resource = await this.request("POST", "/v1/resources", body, requestOptions);
1931
- if (typeof resource?.resource_id !== "string" || resource.resource_id.trim() === "") {
1932
- throw unexpectedResponseError("protectUrl: response is missing resource_id");
1933
- }
1934
- return new ProtectedResource(this, resource.resource_id,
2125
+ await verifyResourceIdentity(resource, "protectUrl");
2126
+ return new ProtectedResource(this, resource.crid,
1935
2127
  // Some resource types redact target_url in responses; fall back to
1936
2128
  // the caller-supplied URL so the handle stays useful.
1937
2129
  resource.target_url || targetUrl, resource);
1938
2130
  }
1939
2131
  /**
1940
- * 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
1941
2133
  * call.
1942
2134
  *
1943
- * Use when you persisted a LayerV resource id and want to mint more
1944
- * 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`):
1945
2137
  *
1946
2138
  * ```ts
1947
- * const resource = client.resourceById("r_demo1234567");
2139
+ * const resource = client.resourceByCrid(storedCrid);
1948
2140
  * const portal = await resource.createPortal({ validFor: "1h" });
1949
2141
  * ```
1950
2142
  *
1951
2143
  * The handle carries no server metadata; use {@link getResource} when you
1952
2144
  * need the full resource details.
1953
2145
  */
1954
- resourceById(id) {
1955
- requireNonEmptyId(id, "resourceById");
2146
+ resourceByCrid(id) {
2147
+ requireResourceCrid(id, "resourceByCrid");
1956
2148
  return new ProtectedResource(this, id);
1957
2149
  }
1958
2150
  /** Find or create the active qURL Connector resource for an immutable slug. */
@@ -2000,15 +2192,16 @@ class QURLClient {
2000
2192
  }
2001
2193
  return { resource, foundExisting };
2002
2194
  }
2003
- /** Fetch a qURL Connector resource by its immutable public resource ID. */
2004
- async getConnectorResource(resourceId) {
2005
- await requireConnectorResourceId(resourceId, "getConnectorResource");
2006
- const { data, __http_status } = await this.rawRequest("GET", `/v1/resources/${encodeURIComponent(resourceId)}`, undefined, { retry: false });
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 });
2007
2200
  if (__http_status !== 200) {
2008
2201
  throw unexpectedResponseError(`getConnectorResource: expected HTTP 200, got ${__http_status ?? "unknown"}`);
2009
2202
  }
2010
2203
  return parseConnectorResource(this, data?.resource, "getConnectorResource", {
2011
- resourceId,
2204
+ crid,
2012
2205
  revokedIsLifecycleError: true,
2013
2206
  });
2014
2207
  }
@@ -2046,15 +2239,15 @@ class QURLClient {
2046
2239
  return parseConnectorResource(this, data[0], "getConnectorResourceBySlug", { slug });
2047
2240
  }
2048
2241
  /**
2049
- * Revoke a qURL Connector resource by immutable public resource ID.
2242
+ * Revoke a qURL Connector resource by CRID.
2050
2243
  *
2051
2244
  * The API does not apply Idempotency-Key replay to DELETE. After an
2052
2245
  * outcome-unknown failure, reconcile by ID before issuing a deliberate retry.
2053
2246
  */
2054
- async deleteConnectorResource(resourceId) {
2055
- await requireConnectorResourceId(resourceId, "deleteConnectorResource");
2247
+ async deleteConnectorResource(crid) {
2248
+ requireResourceCrid(crid, "deleteConnectorResource");
2056
2249
  try {
2057
- await this.requestNoContent(`/v1/resources/${encodeURIComponent(resourceId)}`);
2250
+ await this.requestNoContent(`/v1/resources/${encodeURIComponent(crid)}`);
2058
2251
  }
2059
2252
  catch (error) {
2060
2253
  classifyConnectorMutationFailure("delete", error);
@@ -2067,7 +2260,7 @@ class QURLClient {
2067
2260
  * reach one private resource. Recipients open `portal.link` directly and
2068
2261
  * need no LayerV credentials. Prefer short lifetimes such as
2069
2262
  * `{ validFor: "5m" }`. Accepts a {@link ProtectedResource} handle or a
2070
- * public resource ID string. REST-shaped equivalents: {@link mintLink} /
2263
+ * CRID string. REST-shaped equivalents: {@link mintLink} /
2071
2264
  * {@link createQurlForResource}.
2072
2265
  *
2073
2266
  * Duration options take a string (`"5m"`, `"24h"`; server-validated) or a
@@ -2086,22 +2279,22 @@ class QURLClient {
2086
2279
  if (!ProtectedResource.isBoundTo(resource, this)) {
2087
2280
  throw clientValidationError("createPortal: resource is bound to a different client");
2088
2281
  }
2089
- resourceId = resource.id;
2282
+ resourceId = resource.crid;
2090
2283
  }
2091
2284
  else {
2092
- 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)})`);
2093
2286
  }
2094
- requireNonEmptyId(resourceId, "createPortal", "resource id");
2287
+ requireResourceCrid(resourceId, "createPortal");
2095
2288
  const body = buildCreatePortalBody(opts, "createPortal");
2096
2289
  const data = await this.request("POST", `/v1/resources/${encodeURIComponent(resourceId)}/qurls`, body, requestOptions);
2097
- return parsePortal(data, "createPortal");
2290
+ return parsePortal(data, "createPortal", resourceId);
2098
2291
  }
2099
2292
  /**
2100
2293
  * Protect `targetUrl` and mint a portal in one API call.
2101
2294
  *
2102
2295
  * Convenience for one-off scripts (qurl-go: `Client.CreatePortalForURL`).
2103
2296
  * The returned resource handle is reusable for minting more portals but
2104
- * 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
2105
2298
  * {@link protectUrl} when you need the full server-populated resource
2106
2299
  * metadata. Portal options match {@link createPortal}; `targetUrl` gets
2107
2300
  * the same malformed-URL and embedded-credentials rejection as
@@ -2116,10 +2309,10 @@ class QURLClient {
2116
2309
  ...buildCreatePortalBody(opts, "createPortalForUrl"),
2117
2310
  };
2118
2311
  const data = await this.request("POST", "/v1/qurls", body, requestOptions);
2119
- const portal = parsePortal(data, "createPortalForUrl");
2312
+ const portal = await parsePortal(data, "createPortalForUrl");
2120
2313
  return {
2121
2314
  portal,
2122
- resource: new ProtectedResource(this, portal.resourceId, targetUrl),
2315
+ resource: new ProtectedResource(this, portal.crid, targetUrl),
2123
2316
  };
2124
2317
  }
2125
2318
  /**
@@ -2153,7 +2346,7 @@ class QURLClient {
2153
2346
  return {
2154
2347
  resourceUrl: resolved.target_url,
2155
2348
  openSeconds: resolved.access_grant?.expires_in ?? 0,
2156
- resourceId: resolved.resource_id,
2349
+ resourcePublicKey: resolved.resource_id,
2157
2350
  };
2158
2351
  }
2159
2352
  // --- Public API ---
@@ -2316,11 +2509,12 @@ class QURLClient {
2316
2509
  /**
2317
2510
  * Get a qURL resource and its access tokens.
2318
2511
  *
2319
- * Accepts either a resource ID (`r_` prefix) or a qURL display ID (`q_`
2320
- * 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.
2321
2515
  */
2322
2516
  async get(id) {
2323
- requireNonEmptyId(id, "get");
2517
+ validatePathId(id, "get", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
2324
2518
  const raw = await this.request("GET", `/v1/qurls/${encodeURIComponent(id)}`);
2325
2519
  return this.mapQurlsField(raw);
2326
2520
  }
@@ -2362,49 +2556,20 @@ class QURLClient {
2362
2556
  /**
2363
2557
  * Delete (revoke) a qURL resource and all its access tokens.
2364
2558
  *
2365
- * Only accepts a resource ID (`r_` prefix), not a qURL display ID (`q_`
2366
- * prefix). Per the OpenAPI spec: *"Requires a resource ID (r_ prefix).
2367
- * To revoke a single token, use DELETE /v1/resources/:id/qurls/:qurl_id"*.
2368
- * 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.
2369
2563
  */
2370
2564
  async delete(id) {
2371
- // Type guard for untyped-JS callers — without it, `(undefined).length`
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
- }
2565
+ requireResourceCrid(id, "delete");
2401
2566
  await this.requestNoContent(`/v1/qurls/${encodeURIComponent(id)}`);
2402
2567
  }
2403
2568
  /**
2404
2569
  * Extend a qURL's expiration.
2405
2570
  *
2406
- * Accepts either a resource ID (`r_` prefix) or a qURL display ID (`q_`
2407
- * 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
2408
2573
  * expiration fields. `ExtendInput` shares its `extend_by` / `expires_at`
2409
2574
  * fields with `UpdateInput` but is *narrower in two ways*: (1) exactly
2410
2575
  * one of the two must be present (XOR via `?: never`), where `UpdateInput`
@@ -2416,7 +2581,7 @@ class QURLClient {
2416
2581
  * enforced separately by the runtime check inside `update()`.
2417
2582
  */
2418
2583
  async extend(id, input, options) {
2419
- requireNonEmptyId(id, "extend");
2584
+ validatePathId(id, "extend", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
2420
2585
  requireObjectInput(input, "extend");
2421
2586
  const { extend_by, expires_at } = input;
2422
2587
  if (extend_by === undefined && expires_at === undefined) {
@@ -2430,11 +2595,12 @@ class QURLClient {
2430
2595
  /**
2431
2596
  * Update a qURL — extend expiration, change description, rename tags.
2432
2597
  *
2433
- * Accepts either a resource ID (`r_` prefix) or a qURL display ID (`q_`
2434
- * 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.
2435
2601
  */
2436
2602
  async update(id, input, options) {
2437
- requireNonEmptyId(id, "update");
2603
+ validatePathId(id, "update", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
2438
2604
  requireObjectInput(input, "update");
2439
2605
  requireNoUnknownFields(input, UPDATE_FIELD_KEYS, "update");
2440
2606
  // Normalize null → undefined so untyped-JS callers passing
@@ -2461,15 +2627,16 @@ class QURLClient {
2461
2627
  *
2462
2628
  * Portal-flow equivalent: {@link createPortal}.
2463
2629
  *
2464
- * Accepts either a resource ID (`r_` prefix) or a qURL display ID (`q_`
2465
- * 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.
2466
2633
  *
2467
2634
  * Passing `{}` or an object with all fields `null`/`undefined` is
2468
2635
  * equivalent to omitting the second argument: no body is sent, and the
2469
2636
  * server applies its 24h default expiration.
2470
2637
  */
2471
2638
  async mintLink(id, input, options) {
2472
- requireNonEmptyId(id, "mintLink");
2639
+ validatePathId(id, "mintLink", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
2473
2640
  // Normalize null → omitted so untyped-JS callers passing
2474
2641
  // `{ expires_in: null, expires_at: "..." }` don't leak null into
2475
2642
  // the wire body via JSON.stringify and don't bypass the XOR check
@@ -2583,12 +2750,12 @@ class QURLClient {
2583
2750
  }
2584
2751
  /** Get one resource plus its bounded qURL preview. */
2585
2752
  async getResource(id) {
2586
- requireNonEmptyId(id, "getResource");
2753
+ requireResourceCrid(id, "getResource");
2587
2754
  return this.request("GET", `/v1/resources/${encodeURIComponent(id)}`);
2588
2755
  }
2589
2756
  /** Update resource metadata. */
2590
2757
  async updateResource(id, input, options) {
2591
- requireNonEmptyId(id, "updateResource");
2758
+ requireResourceCrid(id, "updateResource");
2592
2759
  requireObjectInput(input, "updateResource");
2593
2760
  requireNoUnknownFields(input, UPDATE_RESOURCE_FIELD_KEYS, "updateResource");
2594
2761
  const validationInput = normalizePatchFields(input, UPDATE_RESOURCE_FIELD_KEYS, { preserveNullFields: ["alias"] });
@@ -2602,7 +2769,7 @@ class QURLClient {
2602
2769
  }
2603
2770
  /** Revoke a resource and all of its qURLs. */
2604
2771
  async deleteResource(id) {
2605
- requireNonEmptyId(id, "deleteResource");
2772
+ requireResourceCrid(id, "deleteResource");
2606
2773
  await this.requestNoContent(`/v1/resources/${encodeURIComponent(id)}`);
2607
2774
  }
2608
2775
  /**
@@ -2611,7 +2778,7 @@ class QURLClient {
2611
2778
  * Portal-flow equivalent: {@link createPortal} (same endpoint).
2612
2779
  */
2613
2780
  async createQurlForResource(id, input, options) {
2614
- requireNonEmptyId(id, "createQurlForResource");
2781
+ requireResourceCrid(id, "createQurlForResource");
2615
2782
  let normalized = input;
2616
2783
  if (input !== undefined) {
2617
2784
  requireObjectInput(input, "createQurlForResource");
@@ -2622,16 +2789,83 @@ class QURLClient {
2622
2789
  validateQurlTokenOptions(normalized);
2623
2790
  return this.request("POST", `/v1/resources/${encodeURIComponent(id)}/qurls`, normalized, options);
2624
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
+ }
2625
2859
  /** Revoke a specific qURL token on a resource. */
2626
2860
  async revokeResourceQurl(id, qurlId) {
2627
- requireNonEmptyId(id, "revokeResourceQurl");
2628
- requireNonEmptyId(qurlId, "revokeResourceQurl");
2861
+ requireResourceCrid(id, "revokeResourceQurl");
2862
+ validatePathId(qurlId, "revokeResourceQurl", "qurl id", QURL_ID_PATH_OPTIONS);
2629
2863
  await this.requestNoContent(`/v1/resources/${encodeURIComponent(id)}/qurls/${encodeURIComponent(qurlId)}`);
2630
2864
  }
2631
2865
  /** Update a specific qURL token on a resource. */
2632
2866
  async updateResourceQurl(id, qurlId, input, options) {
2633
- requireNonEmptyId(id, "updateResourceQurl");
2634
- requireNonEmptyId(qurlId, "updateResourceQurl");
2867
+ requireResourceCrid(id, "updateResourceQurl");
2868
+ validatePathId(qurlId, "updateResourceQurl", "qurl id", QURL_ID_PATH_OPTIONS);
2635
2869
  requireObjectInput(input, "updateResourceQurl");
2636
2870
  requireNoUnknownFields(input, UPDATE_RESOURCE_QURL_FIELD_KEYS, "updateResourceQurl");
2637
2871
  const normalized = normalizePatchFields(input, UPDATE_RESOURCE_QURL_FIELD_KEYS);
@@ -2647,7 +2881,7 @@ class QURLClient {
2647
2881
  * this page and emits a debug log rather than pretending it fetched all pages.
2648
2882
  */
2649
2883
  async listResourceSessions(id) {
2650
- requireNonEmptyId(id, "listResourceSessions");
2884
+ requireResourceCrid(id, "listResourceSessions");
2651
2885
  const { data, meta } = await this.rawRequest("GET", `/v1/resources/${encodeURIComponent(id)}/sessions`);
2652
2886
  if (meta?.has_more || meta?.next_cursor) {
2653
2887
  this.log("listResourceSessions: pagination metadata surfaced on unpaginated endpoint", {
@@ -2664,7 +2898,7 @@ class QURLClient {
2664
2898
  }
2665
2899
  /** Terminate all active sessions for a resource and return the server count. */
2666
2900
  async terminateAllResourceSessions(id) {
2667
- requireNonEmptyId(id, "terminateAllResourceSessions");
2901
+ requireResourceCrid(id, "terminateAllResourceSessions");
2668
2902
  const path = `/v1/resources/${encodeURIComponent(id)}/sessions`;
2669
2903
  const { data, meta, __http_status } = await this.rawRequest("DELETE", path);
2670
2904
  if (__http_status === 204) {
@@ -2679,8 +2913,8 @@ class QURLClient {
2679
2913
  }
2680
2914
  /** Terminate a specific resource session. */
2681
2915
  async terminateResourceSession(id, sessionId) {
2682
- requireNonEmptyId(id, "terminateResourceSession");
2683
- requireNonEmptyId(sessionId, "terminateResourceSession");
2916
+ requireResourceCrid(id, "terminateResourceSession");
2917
+ validatePathId(sessionId, "terminateResourceSession", "session id");
2684
2918
  await this.requestNoContent(`/v1/resources/${encodeURIComponent(id)}/sessions/${encodeURIComponent(sessionId)}`);
2685
2919
  }
2686
2920
  /** List connector installations. */
@@ -2783,22 +3017,22 @@ class QURLClient {
2783
3017
  }
2784
3018
  /** Get custom domain status. */
2785
3019
  async getDomain(domain) {
2786
- requireNonEmptyId(domain, "getDomain", "domain");
3020
+ validatePathId(domain, "getDomain", "domain");
2787
3021
  return this.request("GET", `/v1/domains/${encodeURIComponent(domain)}`);
2788
3022
  }
2789
3023
  /** Remove a custom domain. */
2790
3024
  async deleteDomain(domain) {
2791
- requireNonEmptyId(domain, "deleteDomain", "domain");
3025
+ validatePathId(domain, "deleteDomain", "domain");
2792
3026
  await this.requestNoContent(`/v1/domains/${encodeURIComponent(domain)}`);
2793
3027
  }
2794
3028
  /** Trigger DNS verification for a custom domain. */
2795
3029
  async verifyDomain(domain, options) {
2796
- requireNonEmptyId(domain, "verifyDomain", "domain");
3030
+ validatePathId(domain, "verifyDomain", "domain");
2797
3031
  return this.request("POST", `/v1/domains/${encodeURIComponent(domain)}/verify`, undefined, options);
2798
3032
  }
2799
3033
  /** Regenerate a domain verification token. */
2800
3034
  async regenerateDomainToken(domain, options) {
2801
- requireNonEmptyId(domain, "regenerateDomainToken", "domain");
3035
+ validatePathId(domain, "regenerateDomainToken", "domain");
2802
3036
  return this.request("POST", `/v1/domains/${encodeURIComponent(domain)}/regenerate-token`, undefined, options);
2803
3037
  }
2804
3038
  /** List webhooks. */
@@ -2834,12 +3068,12 @@ class QURLClient {
2834
3068
  }
2835
3069
  /** Get a webhook. */
2836
3070
  async getWebhook(id) {
2837
- requireNonEmptyId(id, "getWebhook");
3071
+ validatePathId(id, "getWebhook");
2838
3072
  return this.request("GET", `/v1/webhooks/${encodeURIComponent(id)}`);
2839
3073
  }
2840
3074
  /** Update a webhook. */
2841
3075
  async updateWebhook(id, input, options) {
2842
- requireNonEmptyId(id, "updateWebhook");
3076
+ validatePathId(id, "updateWebhook");
2843
3077
  requireObjectInput(input, "updateWebhook");
2844
3078
  requireNoUnknownFields(input, UPDATE_WEBHOOK_FIELD_KEYS, "updateWebhook");
2845
3079
  const normalized = normalizePatchFields(input, UPDATE_WEBHOOK_FIELD_KEYS);
@@ -2849,17 +3083,17 @@ class QURLClient {
2849
3083
  }
2850
3084
  /** Delete a webhook. */
2851
3085
  async deleteWebhook(id) {
2852
- requireNonEmptyId(id, "deleteWebhook");
3086
+ validatePathId(id, "deleteWebhook");
2853
3087
  await this.requestNoContent(`/v1/webhooks/${encodeURIComponent(id)}`);
2854
3088
  }
2855
3089
  /** Regenerate a webhook signing secret. */
2856
3090
  async regenerateWebhookSecret(id, options) {
2857
- requireNonEmptyId(id, "regenerateWebhookSecret");
3091
+ validatePathId(id, "regenerateWebhookSecret");
2858
3092
  return this.request("POST", `/v1/webhooks/${encodeURIComponent(id)}/secret`, undefined, options);
2859
3093
  }
2860
3094
  /** List delivery attempts for a webhook. */
2861
3095
  async listWebhookDeliveries(id, input = {}) {
2862
- requireNonEmptyId(id, "listWebhookDeliveries");
3096
+ validatePathId(id, "listWebhookDeliveries");
2863
3097
  const { data, meta } = await this.rawRequest("GET", appendQuery(`/v1/webhooks/${encodeURIComponent(id)}/deliveries`, input, WEBHOOK_DELIVERY_LIST_PARAM_KEYS, "listWebhookDeliveries"));
2864
3098
  return pageFromMeta({ deliveries: data ?? [] }, meta);
2865
3099
  }
@@ -2870,7 +3104,7 @@ class QURLClient {
2870
3104
  * iteration begins.
2871
3105
  */
2872
3106
  listAllWebhookDeliveries(id, input = {}) {
2873
- requireNonEmptyId(id, "listAllWebhookDeliveries");
3107
+ validatePathId(id, "listAllWebhookDeliveries");
2874
3108
  validateListAllInput(input, WEBHOOK_DELIVERY_LIST_PARAM_KEYS, "listAllWebhookDeliveries");
2875
3109
  return this.paginateAll("listAllWebhookDeliveries", input, (pageInput) => this.listWebhookDeliveries(id, pageInput), (page) => page.deliveries);
2876
3110
  }
@@ -2879,10 +3113,34 @@ class QURLClient {
2879
3113
  requireObjectInput(input, "createApiKey");
2880
3114
  requireNoUnknownFields(input, CREATE_API_KEY_FIELD_KEYS, "createApiKey");
2881
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
+ }
2882
3140
  const normalized = normalizedRecord;
2883
3141
  validateApiKeyWriteFields(normalizedRecord, "createApiKey", {
2884
3142
  name: true,
2885
- scopes: true,
3143
+ scopes: !isEnrollmentToken,
2886
3144
  });
2887
3145
  return this.request("POST", "/v1/api-keys", normalized, options);
2888
3146
  }
@@ -2903,7 +3161,7 @@ class QURLClient {
2903
3161
  }
2904
3162
  /** Update an API key. */
2905
3163
  async updateApiKey(keyId, input, options) {
2906
- requireNonEmptyId(keyId, "updateApiKey");
3164
+ validatePathId(keyId, "updateApiKey");
2907
3165
  requireObjectInput(input, "updateApiKey");
2908
3166
  requireNoUnknownFields(input, UPDATE_API_KEY_FIELD_KEYS, "updateApiKey");
2909
3167
  const normalized = normalizePatchFields(input, UPDATE_API_KEY_FIELD_KEYS);
@@ -2913,7 +3171,7 @@ class QURLClient {
2913
3171
  }
2914
3172
  /** Revoke an API key. */
2915
3173
  async revokeApiKey(keyId) {
2916
- requireNonEmptyId(keyId, "revokeApiKey");
3174
+ validatePathId(keyId, "revokeApiKey");
2917
3175
  await this.requestNoContent(`/v1/api-keys/${encodeURIComponent(keyId)}`);
2918
3176
  }
2919
3177
  /** Redeem an access code. */
@@ -2931,7 +3189,7 @@ class QURLClient {
2931
3189
  requireNoUnknownFields(input, CREATE_ACCESS_CODE_FIELD_KEYS, "createAccessCode");
2932
3190
  const normalizedRecord = normalizePatchFields(input, CREATE_ACCESS_CODE_FIELD_KEYS);
2933
3191
  const normalized = normalizedRecord;
2934
- requireNonEmptyStringField(normalizedRecord, "resource_id", "createAccessCode");
3192
+ requireResourceCrid(normalizedRecord.resource_id, "createAccessCode", "resource_id");
2935
3193
  return this.request("POST", "/v1/access-codes", normalized, options);
2936
3194
  }
2937
3195
  /**
@@ -2958,7 +3216,7 @@ class QURLClient {
2958
3216
  }
2959
3217
  /** Revoke an access code. */
2960
3218
  async revokeAccessCode(id) {
2961
- requireNonEmptyId(id, "revokeAccessCode");
3219
+ validatePathId(id, "revokeAccessCode");
2962
3220
  await this.requestNoContent(`/v1/access-codes/${encodeURIComponent(id)}`);
2963
3221
  }
2964
3222
  // --- Internal HTTP plumbing ---
@@ -3446,7 +3704,7 @@ exports.QURLClient = QURLClient;
3446
3704
  /**
3447
3705
  * A LayerV-protected resource, bound to the client that produced it.
3448
3706
  *
3449
- * Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.resourceById} —
3707
+ * Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.resourceByCrid} —
3450
3708
  * not constructed directly. Mint short-lived access links for the resource
3451
3709
  * with {@link createPortal} (qurl-go: `qurl.Resource`).
3452
3710
  */
@@ -3455,19 +3713,19 @@ class ProtectedResource {
3455
3713
  // CONTRIBUTING.md's dual-build rule bars) so the binding stays on the
3456
3714
  // instance and out of Object.keys/JSON.stringify output.
3457
3715
  #client;
3458
- /** The LayerV public resource ID. */
3459
- id;
3716
+ /** The public resource CRID. */
3717
+ crid;
3460
3718
  /** The private URL protected by this resource, when known. */
3461
3719
  targetUrl;
3462
3720
  /**
3463
3721
  * Full server-populated {@link Resource} metadata when the handle came
3464
- * from an API response; `undefined` for {@link QURLClient.resourceById}
3722
+ * from an API response; `undefined` for {@link QURLClient.resourceByCrid}
3465
3723
  * handles and for the one-call {@link QURLClient.createPortalForUrl} path.
3466
3724
  */
3467
3725
  details;
3468
3726
  constructor(client, id, targetUrl, details) {
3469
3727
  this.#client = client;
3470
- this.id = id;
3728
+ this.crid = id;
3471
3729
  this.targetUrl = targetUrl;
3472
3730
  this.details = details;
3473
3731
  }
@@ -3499,8 +3757,8 @@ const CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN = Symbol("validated ConnectorResource
3499
3757
  */
3500
3758
  class ConnectorResource {
3501
3759
  #client;
3502
- resourceId;
3503
- /** Producer-supplied CRID carried verbatim; verify it against a trusted key before trust. */
3760
+ resourcePublicKey;
3761
+ /** Required public identifier, verified against the returned resource key. */
3504
3762
  crid;
3505
3763
  connectorRoutingId;
3506
3764
  knockResourceId;
@@ -3512,7 +3770,7 @@ class ConnectorResource {
3512
3770
  throw clientValidationError("ConnectorResource cannot be constructed directly; load it through QURLClient");
3513
3771
  }
3514
3772
  this.#client = client;
3515
- this.resourceId = details.resource_id;
3773
+ this.resourcePublicKey = details.resource_id;
3516
3774
  this.crid = details.crid;
3517
3775
  this.connectorRoutingId = details.connector_routing_id;
3518
3776
  this.knockResourceId = details.knock_resource_id;
@@ -3521,7 +3779,7 @@ class ConnectorResource {
3521
3779
  }
3522
3780
  /** Mint a short-lived portal for this Connector resource. */
3523
3781
  async createPortal(opts = {}, requestOptions) {
3524
- return this.#client.createPortal(this.resourceId, opts, requestOptions);
3782
+ return this.#client.createPortal(this.crid, opts, requestOptions);
3525
3783
  }
3526
3784
  }
3527
3785
  exports.ConnectorResource = ConnectorResource;