@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.
- package/README.md +121 -15
- package/dist/cjs/client.d.ts +44 -31
- package/dist/cjs/client.js +412 -154
- package/dist/cjs/crid.d.ts +2 -0
- package/dist/cjs/crid.js +53 -0
- package/dist/cjs/errors.d.ts +10 -2
- package/dist/cjs/errors.js +11 -3
- package/dist/cjs/index.d.ts +5 -3
- package/dist/cjs/index.js +9 -1
- package/dist/cjs/share.d.ts +56 -0
- package/dist/cjs/share.js +113 -0
- package/dist/cjs/types.d.ts +76 -13
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/client.d.ts +44 -31
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +411 -154
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/crid.d.ts +3 -0
- package/dist/esm/crid.d.ts.map +1 -0
- package/dist/esm/crid.js +50 -0
- package/dist/esm/crid.js.map +1 -0
- package/dist/esm/errors.d.ts +10 -2
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +10 -2
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +5 -3
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +3 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/share.d.ts +57 -0
- package/dist/esm/share.d.ts.map +1 -0
- package/dist/esm/share.js +109 -0
- package/dist/esm/share.js.map +1 -0
- package/dist/esm/types.d.ts +76 -13
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +2 -2
package/dist/esm/client.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { parseCrid, cridKeyMatches } from "./crid.js";
|
|
1
2
|
import { ConnectorResourceOutcomeUnknownError, createError, ERROR_CODE_AMBIGUOUS_RESOURCE, ERROR_CODE_CLIENT_VALIDATION, ERROR_CODE_RESOURCE_NOT_FOUND, ERROR_CODE_CONNECTOR_RESOURCE_REVOKED, ERROR_CODE_UNEXPECTED_RESPONSE, ERROR_CODE_UNKNOWN, NetworkError, NotFoundError, QURLError, RuntimeError, TimeoutError, ValidationError, } from "./errors.js";
|
|
2
3
|
import { stripTrailingSlashes } from "./internal.js";
|
|
4
|
+
import { ShareLink } from "./share.js";
|
|
3
5
|
import { VERSION } from "./version.js";
|
|
4
6
|
const DEFAULT_BASE_URL = "https://api.layerv.ai";
|
|
5
7
|
const DEFAULT_MAX_RETRIES = 3;
|
|
@@ -173,6 +175,10 @@ const CREATE_QURL_FOR_RESOURCE_FIELD_KEYS = [
|
|
|
173
175
|
"access_policy",
|
|
174
176
|
"target_path",
|
|
175
177
|
];
|
|
178
|
+
const SHARE_RESOURCE_OPTION_KEYS = [
|
|
179
|
+
"ttlSeconds",
|
|
180
|
+
];
|
|
181
|
+
assertExhaustive(true);
|
|
176
182
|
assertExhaustive(true);
|
|
177
183
|
/**
|
|
178
184
|
* Options accepted by the portal-verb `protectUrl` / `createPortal`
|
|
@@ -231,11 +237,12 @@ const UPDATE_WEBHOOK_FIELD_KEYS = [
|
|
|
231
237
|
];
|
|
232
238
|
assertExhaustive(true);
|
|
233
239
|
const CREATE_API_KEY_FIELD_KEYS = [
|
|
240
|
+
"kind",
|
|
234
241
|
"name",
|
|
235
242
|
"scopes",
|
|
243
|
+
"target",
|
|
244
|
+
"claims",
|
|
236
245
|
"expires_in",
|
|
237
|
-
"purpose",
|
|
238
|
-
"tunnel_slug",
|
|
239
246
|
];
|
|
240
247
|
assertExhaustive(true);
|
|
241
248
|
const UPDATE_API_KEY_FIELD_KEYS = [
|
|
@@ -715,6 +722,41 @@ const MAX_DESCRIPTION = 500;
|
|
|
715
722
|
const MAX_CUSTOM_DOMAIN = 253;
|
|
716
723
|
const MAX_MAX_SESSIONS = 1000;
|
|
717
724
|
const MAX_API_KEY_NAME = 100;
|
|
725
|
+
const API_KEY_REQUEST_SCOPE_VALUES = [
|
|
726
|
+
"qurl:read",
|
|
727
|
+
"qurl:write",
|
|
728
|
+
"qurl:resolve",
|
|
729
|
+
"qurl:agent",
|
|
730
|
+
];
|
|
731
|
+
assertExhaustive(true);
|
|
732
|
+
const API_KEY_REQUEST_SCOPES = new Set(API_KEY_REQUEST_SCOPE_VALUES);
|
|
733
|
+
/** Narrow a response scope before reusing it in a create/update request. */
|
|
734
|
+
export function isApiKeyRequestScope(scope) {
|
|
735
|
+
return API_KEY_REQUEST_SCOPES.has(scope);
|
|
736
|
+
}
|
|
737
|
+
const CREDENTIAL_TARGET_VALUES = [
|
|
738
|
+
"agent",
|
|
739
|
+
"connector",
|
|
740
|
+
];
|
|
741
|
+
assertExhaustive(true);
|
|
742
|
+
const CREDENTIAL_TARGETS = new Set(CREDENTIAL_TARGET_VALUES);
|
|
743
|
+
const CREDENTIAL_CLAIM_TYPE_VALUES = [
|
|
744
|
+
"connector",
|
|
745
|
+
];
|
|
746
|
+
assertExhaustive(true);
|
|
747
|
+
const CREDENTIAL_CLAIM_TYPES = new Set(CREDENTIAL_CLAIM_TYPE_VALUES);
|
|
748
|
+
// Mirrors CreateApiKeyRequest.expires_in in the service OpenAPI. Composite
|
|
749
|
+
// durations are deliberately invalid on this endpoint. The 24h cap permits
|
|
750
|
+
// 1d, but no positive whole-week duration.
|
|
751
|
+
const CREDENTIAL_DURATION_PATTERN = /^([0-9]+)([smhdw])$/;
|
|
752
|
+
const CREDENTIAL_DURATION_MULTIPLIERS = {
|
|
753
|
+
s: 1n,
|
|
754
|
+
m: 60n,
|
|
755
|
+
h: 3600n,
|
|
756
|
+
d: 86400n,
|
|
757
|
+
w: 604800n,
|
|
758
|
+
};
|
|
759
|
+
const MAX_ENROLLMENT_TOKEN_TTL_SECONDS = 86400n;
|
|
718
760
|
const MAX_ALIAS = 64;
|
|
719
761
|
const MAX_SLUG = 64;
|
|
720
762
|
const MAX_TARGET_PATH = 2048;
|
|
@@ -732,7 +774,6 @@ const CONNECTOR_ROUTING_ID_PATTERN = /^c-[a-z2-7]{51}[aq]$/;
|
|
|
732
774
|
// CreateQurlRequest.target_url pattern is loose (just a URI) but
|
|
733
775
|
// UpdateQurlRequest.tags pattern is specific — enforce it here.
|
|
734
776
|
const TAG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9 _-]*$/;
|
|
735
|
-
const RESOURCE_ID_PREFIX = "r_";
|
|
736
777
|
function requireMaxLength(value, field, max) {
|
|
737
778
|
if (value === undefined)
|
|
738
779
|
return;
|
|
@@ -833,22 +874,129 @@ function pageFromMeta(out, meta) {
|
|
|
833
874
|
page_size: meta?.page_size,
|
|
834
875
|
};
|
|
835
876
|
}
|
|
877
|
+
// Best-effort defence-in-depth against today's access-token grammar. The
|
|
878
|
+
// service remains authoritative because resource IDs and credentials are both
|
|
879
|
+
// opaque and future credential formats may not retain the `at_` prefix. The
|
|
880
|
+
// delimiter requirement cannot collide with current base64url public keys,
|
|
881
|
+
// standard-base64 keys, or base32 CRIDs.
|
|
882
|
+
const PATH_EMBEDDED_ACCESS_TOKEN_RE = /[?&#=:\s]at_/i;
|
|
883
|
+
const PATH_PREFIXED_ACCESS_TOKEN_RE = /\/at_/i;
|
|
884
|
+
const QURL_LINK_URL_RE = /^(?:https?:\/\/)?(?:[a-z0-9-]+\.)*qurl\.link(?::\d{1,5})?(?:\/|[?#])/i;
|
|
885
|
+
// ponytail: today's 22-character token suffix only improves URL-paste diagnostics;
|
|
886
|
+
// other lengths are still rejected as URLs without echoing the input.
|
|
887
|
+
const URL_EMBEDDED_ACCESS_TOKEN_RE = /[?&#=:/]at_[a-z0-9_-]{22}(?:$|[?&#:/])/i;
|
|
888
|
+
// A host without a slash can be a legitimate domain identifier. Require the
|
|
889
|
+
// path boundary so shared validation does not break the domain namespace.
|
|
890
|
+
const PATH_SCHEMELESS_URL_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)+(?::\d{1,5})?\//i;
|
|
891
|
+
const QURL_DISPLAY_ID_PREFIX_RE = /^q_/i;
|
|
892
|
+
const MAX_PATH_ID_DECODE_PASSES = 3;
|
|
893
|
+
const MAX_PATH_ID_LENGTH = 4096;
|
|
894
|
+
function isUrlLikePathId(value) {
|
|
895
|
+
return /^https?:\/\//i.test(value) || PATH_SCHEMELESS_URL_RE.test(value);
|
|
896
|
+
}
|
|
897
|
+
const REVOKE_INDIVIDUAL_QURL_RECOVERY = "revoke individual tokens with revokeResourceQurl(crid, qurlId)";
|
|
898
|
+
const DELETE_RESOURCE_RECOVERY = "pass a CRID returned by the API; to revoke one qURL, call revokeResourceQurl(crid, qurlId)";
|
|
899
|
+
const RESOURCE_OR_QURL_ID_PATH_OPTIONS = {
|
|
900
|
+
requireCridUnlessQurlDisplayId: true,
|
|
901
|
+
};
|
|
902
|
+
const QURL_ID_PATH_OPTIONS = {
|
|
903
|
+
rejectBareAccessToken: true,
|
|
904
|
+
accessTokenRecovery: "pass the qURL display ID, not its access token",
|
|
905
|
+
};
|
|
836
906
|
/**
|
|
837
907
|
* Validates that a path-parameter argument is a non-empty string. Some callers
|
|
838
908
|
* pass resource/qURL display IDs, while newer endpoints also pass domains,
|
|
839
909
|
* session IDs, webhook IDs, and API-key IDs, so this intentionally enforces
|
|
840
|
-
* only basic shape and leaves endpoint-specific
|
|
910
|
+
* only basic shape plus transport-safety rules and leaves endpoint-specific
|
|
911
|
+
* identifier grammar to the service.
|
|
841
912
|
*/
|
|
842
|
-
function
|
|
913
|
+
function validatePathId(id, method, field = "id", options = {}) {
|
|
914
|
+
if (options.requireCridUnlessQurlDisplayId && typeof id === "string" && !id.startsWith("q_")) {
|
|
915
|
+
requireResourceCrid(id, method, field);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
843
918
|
// `.trim()` catches whitespace-only and padded IDs before they round-trip as
|
|
844
919
|
// `%20...%20` paths that the server can only reject with a less useful 404.
|
|
845
920
|
// The pre-flight error is more actionable than the 404.
|
|
846
|
-
if (typeof id !== "string"
|
|
921
|
+
if (typeof id !== "string") {
|
|
922
|
+
const observedType = id === null ? "null" : typeof id;
|
|
923
|
+
throw clientValidationError(`${method}: ${field} is required (got ${observedType})`);
|
|
924
|
+
}
|
|
925
|
+
if (id.trim() === "") {
|
|
847
926
|
throw clientValidationError(`${method}: ${field} is required`);
|
|
848
927
|
}
|
|
849
928
|
if (id.trim() !== id) {
|
|
850
929
|
throw clientValidationError(`${method}: ${field} must not include leading or trailing whitespace`);
|
|
851
930
|
}
|
|
931
|
+
if (id.length > MAX_PATH_ID_LENGTH) {
|
|
932
|
+
throw clientValidationError(`${method}: ${field} must be ${MAX_PATH_ID_LENGTH} characters or fewer`);
|
|
933
|
+
}
|
|
934
|
+
const decodedIds = decodedPathIdForms(id);
|
|
935
|
+
// URL resolvers and intermediaries can normalize raw or pre-encoded dot
|
|
936
|
+
// segments before routing. Reject every decoded form we inspected so an
|
|
937
|
+
// item operation cannot be retargeted to a collection endpoint. Embedded
|
|
938
|
+
// `a/../b` remains an opaque single segment because the later URL builder
|
|
939
|
+
// percent-encodes its slash.
|
|
940
|
+
if (decodedIds.some((value) => value === "." || value === "..")) {
|
|
941
|
+
throw clientValidationError(`${method}: ${field} is an invalid URL path segment`);
|
|
942
|
+
}
|
|
943
|
+
// `/` is deliberately outside the general delimiter class so a URL path
|
|
944
|
+
// such as `/at_a_glance` is not misread as a credential. For non-URL input,
|
|
945
|
+
// `/at_` remains a strong pasted-path signal. Fragment, query, header, and
|
|
946
|
+
// pasted `Bearer at_...` / `token:at_...` forms are also strong signals.
|
|
947
|
+
// Bare `at_...` values are rejected only for resource/qURL identifiers:
|
|
948
|
+
// those current formats have fixed non-colliding prefixes, while domain,
|
|
949
|
+
// webhook, session, and API-key IDs are deliberately opaque/open contracts.
|
|
950
|
+
// Full URLs and delimiter/path-prefixed credentials remain rejected in
|
|
951
|
+
// every namespace.
|
|
952
|
+
const containsUrl = decodedIds.some(isUrlLikePathId);
|
|
953
|
+
const containsAccessToken = decodedIds.some((value) => {
|
|
954
|
+
const valueIsUrl = isUrlLikePathId(value);
|
|
955
|
+
return ((QURL_LINK_URL_RE.test(value) &&
|
|
956
|
+
(PATH_EMBEDDED_ACCESS_TOKEN_RE.test(value) || PATH_PREFIXED_ACCESS_TOKEN_RE.test(value))) ||
|
|
957
|
+
(valueIsUrl
|
|
958
|
+
? URL_EMBEDDED_ACCESS_TOKEN_RE.test(value)
|
|
959
|
+
: PATH_EMBEDDED_ACCESS_TOKEN_RE.test(value) || PATH_PREFIXED_ACCESS_TOKEN_RE.test(value)) ||
|
|
960
|
+
(options.rejectBareAccessToken === true && value.slice(0, 3).toLowerCase() === "at_"));
|
|
961
|
+
});
|
|
962
|
+
if (containsAccessToken) {
|
|
963
|
+
const recovery = options.accessTokenRecovery ?? "pass the identifier returned by the API";
|
|
964
|
+
throw clientValidationError(`${method}: ${field} must not contain an access token; ${recovery}`);
|
|
965
|
+
}
|
|
966
|
+
// A full target/qURL is a common argument mix-up, but it is distinct from
|
|
967
|
+
// passing an access-token credential. Diagnose it accurately without
|
|
968
|
+
// echoing the URL, which may itself contain sensitive query parameters.
|
|
969
|
+
if (containsUrl) {
|
|
970
|
+
throw clientValidationError(`${method}: ${field} must be an identifier, not a URL`);
|
|
971
|
+
}
|
|
972
|
+
if (options.rejectQurlDisplayId === true &&
|
|
973
|
+
decodedIds.some((value) => QURL_DISPLAY_ID_PREFIX_RE.test(value))) {
|
|
974
|
+
throw clientValidationError(`${method}: ${field} must not be a qURL display ID; ${REVOKE_INDIVIDUAL_QURL_RECOVERY}`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function decodedPathIdForms(id) {
|
|
978
|
+
// Probe repeatedly encoded input too. Callers sometimes pre-encode a copied
|
|
979
|
+
// link before passing it through a URL builder that encodes it again. Three
|
|
980
|
+
// passes cover the common accidental cases without allowing hostile input
|
|
981
|
+
// to create unbounded work. More deeply encoded or malformed-percent input
|
|
982
|
+
// deliberately fails open to the authoritative service after safe segment
|
|
983
|
+
// encoding; this heuristic is not an identifier parser.
|
|
984
|
+
const decodedIds = [id];
|
|
985
|
+
let decodedId = id;
|
|
986
|
+
for (let pass = 0; pass < MAX_PATH_ID_DECODE_PASSES; pass += 1) {
|
|
987
|
+
try {
|
|
988
|
+
const next = decodeURIComponent(decodedId);
|
|
989
|
+
if (next === decodedId)
|
|
990
|
+
break;
|
|
991
|
+
decodedIds.push(next);
|
|
992
|
+
decodedId = next;
|
|
993
|
+
}
|
|
994
|
+
catch {
|
|
995
|
+
// Malformed percent escapes remain opaque and are left to the service.
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return decodedIds;
|
|
852
1000
|
}
|
|
853
1001
|
function requireConnectorSlug(slug, method) {
|
|
854
1002
|
if (typeof slug !== "string" || !CONNECTOR_SLUG_PATTERN.test(slug)) {
|
|
@@ -895,29 +1043,23 @@ function decodeCanonicalBase64Url(value) {
|
|
|
895
1043
|
return undefined;
|
|
896
1044
|
}
|
|
897
1045
|
}
|
|
898
|
-
async function isValidConnectorResourceId(value) {
|
|
899
|
-
const der = decodeCanonicalBase64Url(value);
|
|
900
|
-
if (!der)
|
|
901
|
-
return false;
|
|
902
|
-
if (!globalThis.crypto?.subtle) {
|
|
903
|
-
throw new RuntimeError("qURL Connector resource validation requires the Web Crypto SubtleCrypto API");
|
|
904
|
-
}
|
|
905
|
-
return importsAsConnectorPublicKey(der);
|
|
906
|
-
}
|
|
907
1046
|
function requireConnectorSubtleCrypto(method) {
|
|
908
1047
|
if (!globalThis.crypto?.subtle) {
|
|
909
1048
|
throw new RuntimeError(`${method}: requires the Web Crypto SubtleCrypto API`);
|
|
910
1049
|
}
|
|
911
1050
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1051
|
+
function requireResourceCrid(crid, method, field = "id") {
|
|
1052
|
+
if (parseCrid(crid))
|
|
1053
|
+
return;
|
|
1054
|
+
// Keep actionable paste errors without inspecting valid CRIDs twice.
|
|
1055
|
+
validatePathId(crid, method, field, {
|
|
1056
|
+
rejectBareAccessToken: true,
|
|
1057
|
+
rejectQurlDisplayId: true,
|
|
1058
|
+
accessTokenRecovery: method === "delete" || method === "deleteResource"
|
|
1059
|
+
? DELETE_RESOURCE_RECOVERY
|
|
1060
|
+
: "pass a CRID returned by the API",
|
|
1061
|
+
});
|
|
1062
|
+
throw clientValidationError(`${method}: requires a valid CRID`);
|
|
921
1063
|
}
|
|
922
1064
|
async function importsAsConnectorPublicKey(der) {
|
|
923
1065
|
try {
|
|
@@ -936,23 +1078,28 @@ function containsControlCharacter(value) {
|
|
|
936
1078
|
return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
937
1079
|
});
|
|
938
1080
|
}
|
|
939
|
-
async function
|
|
940
|
-
if (typeof
|
|
941
|
-
throw unexpectedResponseError(`${method}: response
|
|
1081
|
+
async function verifyResourceIdentity(resource, method, expectedCrid) {
|
|
1082
|
+
if (!resource || typeof resource.resource_id !== "string") {
|
|
1083
|
+
throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
|
|
942
1084
|
}
|
|
943
|
-
const
|
|
944
|
-
|
|
1085
|
+
const resourceKey = decodeCanonicalBase64Url(resource.resource_id);
|
|
1086
|
+
requireConnectorSubtleCrypto(method);
|
|
1087
|
+
if (!resourceKey || !(await importsAsConnectorPublicKey(resourceKey))) {
|
|
945
1088
|
throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
|
|
946
1089
|
}
|
|
947
|
-
if (
|
|
948
|
-
throw unexpectedResponseError(`${method}: response
|
|
1090
|
+
if (expectedCrid !== undefined && resource.crid !== expectedCrid) {
|
|
1091
|
+
throw unexpectedResponseError(`${method}: response crid does not match the request`);
|
|
949
1092
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1093
|
+
if (!(await cridKeyMatches(resource.crid, resourceKey))) {
|
|
1094
|
+
throw unexpectedResponseError(`${method}: response has missing, invalid, or public-key-mismatched crid`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
async function parseConnectorResource(client, value, method, expectation = {}) {
|
|
1098
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1099
|
+
throw unexpectedResponseError(`${method}: response is missing connector resource data`);
|
|
955
1100
|
}
|
|
1101
|
+
const resource = value;
|
|
1102
|
+
await verifyResourceIdentity(resource, method, expectation.crid);
|
|
956
1103
|
if (typeof resource.connector_routing_id !== "string" ||
|
|
957
1104
|
!CONNECTOR_ROUTING_ID_PATTERN.test(resource.connector_routing_id)) {
|
|
958
1105
|
throw unexpectedResponseError(`${method}: response has missing or invalid connector_routing_id`);
|
|
@@ -985,12 +1132,6 @@ async function parseConnectorResource(client, value, method, expectation = {}) {
|
|
|
985
1132
|
// could not create or update.
|
|
986
1133
|
throw unexpectedResponseError(`${method}: response has invalid alias`);
|
|
987
1134
|
}
|
|
988
|
-
if (resource.crid !== undefined && typeof resource.crid !== "string") {
|
|
989
|
-
throw unexpectedResponseError(`${method}: response has invalid crid`);
|
|
990
|
-
}
|
|
991
|
-
// Match qurl-go: ConnectorResource carries an optional producer CRID
|
|
992
|
-
// verbatim. Consumers that possess a trusted delivered key can verify the
|
|
993
|
-
// binding separately; this management-plane row is not itself a trust root.
|
|
994
1135
|
if (resource.status === "revoked") {
|
|
995
1136
|
if (expectation.revokedIsLifecycleError !== true) {
|
|
996
1137
|
throw unexpectedResponseError(`${method}: active-only operation returned a revoked connector resource`);
|
|
@@ -1375,6 +1516,56 @@ function validateApiKeyWriteFields(input, method, requiredFields = {}) {
|
|
|
1375
1516
|
if (requiredFields.scopes || input.scopes !== undefined) {
|
|
1376
1517
|
requireNonEmptyArrayField(input, "scopes", method);
|
|
1377
1518
|
requireStringArrayElements(input.scopes, "scopes", method);
|
|
1519
|
+
const unsupported = input.scopes.find((scope) => !API_KEY_REQUEST_SCOPES.has(scope));
|
|
1520
|
+
if (unsupported !== undefined) {
|
|
1521
|
+
throw clientValidationError(`${method}: scopes contains an unsupported permission ${JSON.stringify(unsupported)}; this SDK accepts ${API_KEY_REQUEST_SCOPE_VALUES.join(", ")}`);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
function validateEnrollmentTokenFields(input, method) {
|
|
1526
|
+
if (input.target !== undefined &&
|
|
1527
|
+
(typeof input.target !== "string" || !CREDENTIAL_TARGETS.has(input.target))) {
|
|
1528
|
+
throw clientValidationError(`${method}: target must be 'agent' or 'connector'`);
|
|
1529
|
+
}
|
|
1530
|
+
let claimCount = 0;
|
|
1531
|
+
if (input.claims !== undefined) {
|
|
1532
|
+
if (!Array.isArray(input.claims)) {
|
|
1533
|
+
throw clientValidationError(`${method}: claims must be an array`);
|
|
1534
|
+
}
|
|
1535
|
+
if (input.claims.length > 1) {
|
|
1536
|
+
throw clientValidationError(`${method}: claims must contain at most one claim`);
|
|
1537
|
+
}
|
|
1538
|
+
claimCount = input.claims.length;
|
|
1539
|
+
for (const [index, claim] of input.claims.entries()) {
|
|
1540
|
+
if (!claim || typeof claim !== "object" || Array.isArray(claim)) {
|
|
1541
|
+
throw clientValidationError(`${method}: claims[${index}] must be an object`);
|
|
1542
|
+
}
|
|
1543
|
+
requireNoUnknownFields(claim, ["type", "id"], `${method}: claims[${index}]`);
|
|
1544
|
+
const record = claim;
|
|
1545
|
+
if (typeof record.type !== "string" || !CREDENTIAL_CLAIM_TYPES.has(record.type)) {
|
|
1546
|
+
throw clientValidationError(`${method}: claims[${index}].type must be 'connector'`);
|
|
1547
|
+
}
|
|
1548
|
+
if (typeof record.id !== "string" || !CONNECTOR_SLUG_PATTERN.test(record.id)) {
|
|
1549
|
+
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`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
// CreateApiKeyRequest.target permits an agent with zero or one connector claim.
|
|
1554
|
+
if (input.target === "connector" && claimCount !== 1) {
|
|
1555
|
+
throw clientValidationError(`${method}: target 'connector' requires exactly one claim`);
|
|
1556
|
+
}
|
|
1557
|
+
if (input.expires_in !== undefined) {
|
|
1558
|
+
if (typeof input.expires_in !== "string") {
|
|
1559
|
+
throw clientValidationError(`${method}: expires_in must be a duration string`);
|
|
1560
|
+
}
|
|
1561
|
+
const match = CREDENTIAL_DURATION_PATTERN.exec(input.expires_in);
|
|
1562
|
+
if (!match) {
|
|
1563
|
+
throw clientValidationError(`${method}: expires_in must match <integer><s|m|h|d|w>`);
|
|
1564
|
+
}
|
|
1565
|
+
const seconds = BigInt(match[1]) * CREDENTIAL_DURATION_MULTIPLIERS[match[2]];
|
|
1566
|
+
if (seconds <= 0n || seconds > MAX_ENROLLMENT_TOKEN_TTL_SECONDS) {
|
|
1567
|
+
throw clientValidationError(`${method}: expires_in must be greater than zero and at most 24h`);
|
|
1568
|
+
}
|
|
1378
1569
|
}
|
|
1379
1570
|
}
|
|
1380
1571
|
/**
|
|
@@ -1570,15 +1761,17 @@ function parseApiDate(value) {
|
|
|
1570
1761
|
* check; everything else is optional because the two mint endpoints differ
|
|
1571
1762
|
* in which ancillary fields they return.
|
|
1572
1763
|
*/
|
|
1573
|
-
function parsePortal(data, method) {
|
|
1764
|
+
async function parsePortal(data, method, expectedCrid) {
|
|
1574
1765
|
if (typeof data?.resource_id !== "string" || data.resource_id.trim() === "") {
|
|
1575
1766
|
throw unexpectedResponseError(`${method}: response is missing resource_id`);
|
|
1576
1767
|
}
|
|
1577
1768
|
if (typeof data.qurl_link !== "string" || data.qurl_link.trim() === "") {
|
|
1578
1769
|
throw unexpectedResponseError(`${method}: response is missing qurl_link`);
|
|
1579
1770
|
}
|
|
1771
|
+
await verifyResourceIdentity(data, method, expectedCrid);
|
|
1580
1772
|
return {
|
|
1581
|
-
|
|
1773
|
+
crid: data.crid,
|
|
1774
|
+
resourcePublicKey: data.resource_id,
|
|
1582
1775
|
link: data.qurl_link,
|
|
1583
1776
|
site: data.qurl_site,
|
|
1584
1777
|
expiresAt: parseApiDate(data.expires_at),
|
|
@@ -1925,31 +2118,29 @@ export class QURLClient {
|
|
|
1925
2118
|
body.alias = normalized.alias;
|
|
1926
2119
|
validateResourceWriteFields(body);
|
|
1927
2120
|
const resource = await this.request("POST", "/v1/resources", body, requestOptions);
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
}
|
|
1931
|
-
return new ProtectedResource(this, resource.resource_id,
|
|
2121
|
+
await verifyResourceIdentity(resource, "protectUrl");
|
|
2122
|
+
return new ProtectedResource(this, resource.crid,
|
|
1932
2123
|
// Some resource types redact target_url in responses; fall back to
|
|
1933
2124
|
// the caller-supplied URL so the handle stays useful.
|
|
1934
2125
|
resource.target_url || targetUrl, resource);
|
|
1935
2126
|
}
|
|
1936
2127
|
/**
|
|
1937
|
-
* Return a portal-minting handle for a stored
|
|
2128
|
+
* Return a portal-minting handle for a stored CRID, without an API
|
|
1938
2129
|
* call.
|
|
1939
2130
|
*
|
|
1940
|
-
* Use when you persisted a LayerV
|
|
1941
|
-
* portals for it (qurl-go: `Client.
|
|
2131
|
+
* Use when you persisted a LayerV CRID and want to mint more
|
|
2132
|
+
* portals for it (qurl-go: `Client.ResourceByCRID`):
|
|
1942
2133
|
*
|
|
1943
2134
|
* ```ts
|
|
1944
|
-
* const resource = client.
|
|
2135
|
+
* const resource = client.resourceByCrid(storedCrid);
|
|
1945
2136
|
* const portal = await resource.createPortal({ validFor: "1h" });
|
|
1946
2137
|
* ```
|
|
1947
2138
|
*
|
|
1948
2139
|
* The handle carries no server metadata; use {@link getResource} when you
|
|
1949
2140
|
* need the full resource details.
|
|
1950
2141
|
*/
|
|
1951
|
-
|
|
1952
|
-
|
|
2142
|
+
resourceByCrid(id) {
|
|
2143
|
+
requireResourceCrid(id, "resourceByCrid");
|
|
1953
2144
|
return new ProtectedResource(this, id);
|
|
1954
2145
|
}
|
|
1955
2146
|
/** Find or create the active qURL Connector resource for an immutable slug. */
|
|
@@ -1997,15 +2188,16 @@ export class QURLClient {
|
|
|
1997
2188
|
}
|
|
1998
2189
|
return { resource, foundExisting };
|
|
1999
2190
|
}
|
|
2000
|
-
/** Fetch a qURL Connector resource by its
|
|
2001
|
-
async getConnectorResource(
|
|
2002
|
-
|
|
2003
|
-
|
|
2191
|
+
/** Fetch a qURL Connector resource by its CRID. */
|
|
2192
|
+
async getConnectorResource(crid) {
|
|
2193
|
+
requireResourceCrid(crid, "getConnectorResource");
|
|
2194
|
+
requireConnectorSubtleCrypto("getConnectorResource");
|
|
2195
|
+
const { data, __http_status } = await this.rawRequest("GET", `/v1/resources/${encodeURIComponent(crid)}`, undefined, { retry: false });
|
|
2004
2196
|
if (__http_status !== 200) {
|
|
2005
2197
|
throw unexpectedResponseError(`getConnectorResource: expected HTTP 200, got ${__http_status ?? "unknown"}`);
|
|
2006
2198
|
}
|
|
2007
2199
|
return parseConnectorResource(this, data?.resource, "getConnectorResource", {
|
|
2008
|
-
|
|
2200
|
+
crid,
|
|
2009
2201
|
revokedIsLifecycleError: true,
|
|
2010
2202
|
});
|
|
2011
2203
|
}
|
|
@@ -2043,15 +2235,15 @@ export class QURLClient {
|
|
|
2043
2235
|
return parseConnectorResource(this, data[0], "getConnectorResourceBySlug", { slug });
|
|
2044
2236
|
}
|
|
2045
2237
|
/**
|
|
2046
|
-
* Revoke a qURL Connector resource by
|
|
2238
|
+
* Revoke a qURL Connector resource by CRID.
|
|
2047
2239
|
*
|
|
2048
2240
|
* The API does not apply Idempotency-Key replay to DELETE. After an
|
|
2049
2241
|
* outcome-unknown failure, reconcile by ID before issuing a deliberate retry.
|
|
2050
2242
|
*/
|
|
2051
|
-
async deleteConnectorResource(
|
|
2052
|
-
|
|
2243
|
+
async deleteConnectorResource(crid) {
|
|
2244
|
+
requireResourceCrid(crid, "deleteConnectorResource");
|
|
2053
2245
|
try {
|
|
2054
|
-
await this.requestNoContent(`/v1/resources/${encodeURIComponent(
|
|
2246
|
+
await this.requestNoContent(`/v1/resources/${encodeURIComponent(crid)}`);
|
|
2055
2247
|
}
|
|
2056
2248
|
catch (error) {
|
|
2057
2249
|
classifyConnectorMutationFailure("delete", error);
|
|
@@ -2064,7 +2256,7 @@ export class QURLClient {
|
|
|
2064
2256
|
* reach one private resource. Recipients open `portal.link` directly and
|
|
2065
2257
|
* need no LayerV credentials. Prefer short lifetimes such as
|
|
2066
2258
|
* `{ validFor: "5m" }`. Accepts a {@link ProtectedResource} handle or a
|
|
2067
|
-
*
|
|
2259
|
+
* CRID string. REST-shaped equivalents: {@link mintLink} /
|
|
2068
2260
|
* {@link createQurlForResource}.
|
|
2069
2261
|
*
|
|
2070
2262
|
* Duration options take a string (`"5m"`, `"24h"`; server-validated) or a
|
|
@@ -2083,22 +2275,22 @@ export class QURLClient {
|
|
|
2083
2275
|
if (!ProtectedResource.isBoundTo(resource, this)) {
|
|
2084
2276
|
throw clientValidationError("createPortal: resource is bound to a different client");
|
|
2085
2277
|
}
|
|
2086
|
-
resourceId = resource.
|
|
2278
|
+
resourceId = resource.crid;
|
|
2087
2279
|
}
|
|
2088
2280
|
else {
|
|
2089
|
-
throw clientValidationError(`createPortal: resource must be a ProtectedResource handle or a
|
|
2281
|
+
throw clientValidationError(`createPortal: resource must be a ProtectedResource handle or a CRID string (got ${describeShape(resource)})`);
|
|
2090
2282
|
}
|
|
2091
|
-
|
|
2283
|
+
requireResourceCrid(resourceId, "createPortal");
|
|
2092
2284
|
const body = buildCreatePortalBody(opts, "createPortal");
|
|
2093
2285
|
const data = await this.request("POST", `/v1/resources/${encodeURIComponent(resourceId)}/qurls`, body, requestOptions);
|
|
2094
|
-
return parsePortal(data, "createPortal");
|
|
2286
|
+
return parsePortal(data, "createPortal", resourceId);
|
|
2095
2287
|
}
|
|
2096
2288
|
/**
|
|
2097
2289
|
* Protect `targetUrl` and mint a portal in one API call.
|
|
2098
2290
|
*
|
|
2099
2291
|
* Convenience for one-off scripts (qurl-go: `Client.CreatePortalForURL`).
|
|
2100
2292
|
* The returned resource handle is reusable for minting more portals but
|
|
2101
|
-
* carries
|
|
2293
|
+
* carries the CRID, verification key, and the caller-supplied target URL; use
|
|
2102
2294
|
* {@link protectUrl} when you need the full server-populated resource
|
|
2103
2295
|
* metadata. Portal options match {@link createPortal}; `targetUrl` gets
|
|
2104
2296
|
* the same malformed-URL and embedded-credentials rejection as
|
|
@@ -2113,10 +2305,10 @@ export class QURLClient {
|
|
|
2113
2305
|
...buildCreatePortalBody(opts, "createPortalForUrl"),
|
|
2114
2306
|
};
|
|
2115
2307
|
const data = await this.request("POST", "/v1/qurls", body, requestOptions);
|
|
2116
|
-
const portal = parsePortal(data, "createPortalForUrl");
|
|
2308
|
+
const portal = await parsePortal(data, "createPortalForUrl");
|
|
2117
2309
|
return {
|
|
2118
2310
|
portal,
|
|
2119
|
-
resource: new ProtectedResource(this, portal.
|
|
2311
|
+
resource: new ProtectedResource(this, portal.crid, targetUrl),
|
|
2120
2312
|
};
|
|
2121
2313
|
}
|
|
2122
2314
|
/**
|
|
@@ -2150,7 +2342,7 @@ export class QURLClient {
|
|
|
2150
2342
|
return {
|
|
2151
2343
|
resourceUrl: resolved.target_url,
|
|
2152
2344
|
openSeconds: resolved.access_grant?.expires_in ?? 0,
|
|
2153
|
-
|
|
2345
|
+
resourcePublicKey: resolved.resource_id,
|
|
2154
2346
|
};
|
|
2155
2347
|
}
|
|
2156
2348
|
// --- Public API ---
|
|
@@ -2313,11 +2505,12 @@ export class QURLClient {
|
|
|
2313
2505
|
/**
|
|
2314
2506
|
* Get a qURL resource and its access tokens.
|
|
2315
2507
|
*
|
|
2316
|
-
* Accepts
|
|
2317
|
-
* prefix); the API resolves
|
|
2508
|
+
* Accepts a CRID or qURL
|
|
2509
|
+
* display ID (`q_` prefix); the API resolves display IDs to the parent
|
|
2510
|
+
* resource automatically.
|
|
2318
2511
|
*/
|
|
2319
2512
|
async get(id) {
|
|
2320
|
-
|
|
2513
|
+
validatePathId(id, "get", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
|
|
2321
2514
|
const raw = await this.request("GET", `/v1/qurls/${encodeURIComponent(id)}`);
|
|
2322
2515
|
return this.mapQurlsField(raw);
|
|
2323
2516
|
}
|
|
@@ -2359,49 +2552,20 @@ export class QURLClient {
|
|
|
2359
2552
|
/**
|
|
2360
2553
|
* Delete (revoke) a qURL resource and all its access tokens.
|
|
2361
2554
|
*
|
|
2362
|
-
*
|
|
2363
|
-
*
|
|
2364
|
-
*
|
|
2365
|
-
*
|
|
2555
|
+
* Requires a CRID returned by the API. To revoke one qURL, use
|
|
2556
|
+
* {@link revokeResourceQurl} instead.
|
|
2557
|
+
*
|
|
2558
|
+
* @throws {ValidationError} If `id` is not a canonical CRID.
|
|
2366
2559
|
*/
|
|
2367
2560
|
async delete(id) {
|
|
2368
|
-
|
|
2369
|
-
// is a raw TypeError instead of the structured ValidationError the
|
|
2370
|
-
// rest of the surface produces.
|
|
2371
|
-
if (typeof id !== "string") {
|
|
2372
|
-
throw clientValidationError(`delete: requires a resource ID (${RESOURCE_ID_PREFIX} prefix + suffix) — got ${id === null ? "null" : typeof id}`);
|
|
2373
|
-
}
|
|
2374
|
-
if (id.trim() !== id) {
|
|
2375
|
-
throw clientValidationError("delete: id must not include leading or trailing whitespace");
|
|
2376
|
-
}
|
|
2377
|
-
// Too-short check runs BEFORE the prefix check so it catches
|
|
2378
|
-
// bare-prefix inputs like `"r_"` (right prefix, no suffix) in
|
|
2379
|
-
// addition to `""` / `"x"` / `"ab"` / `"q_"`. Without this
|
|
2380
|
-
// ordering, an exact `"r_"` would pass the startsWith check and
|
|
2381
|
-
// the SDK would send `DELETE /v1/qurls/r_` to the server —
|
|
2382
|
-
// rejected server-side, but this catches it client-side without
|
|
2383
|
-
// a round-trip.
|
|
2384
|
-
if (id.length <= RESOURCE_ID_PREFIX.length) {
|
|
2385
|
-
throw clientValidationError(`delete: requires a resource ID (${RESOURCE_ID_PREFIX} prefix + suffix) — got ${id.length} character${id.length === 1 ? "" : "s"}`);
|
|
2386
|
-
}
|
|
2387
|
-
if (!id.startsWith(RESOURCE_ID_PREFIX)) {
|
|
2388
|
-
// Wrong-prefix branch: the input is long enough to plausibly be
|
|
2389
|
-
// an ID but has the wrong prefix (e.g. `q_3a7f2c8e91b`).
|
|
2390
|
-
// Echo only the 2-char prefix — never the raw ID
|
|
2391
|
-
// — so observability pipelines don't end up with caller-supplied
|
|
2392
|
-
// identifiers in error logs.
|
|
2393
|
-
const observedPrefix = id.slice(0, RESOURCE_ID_PREFIX.length);
|
|
2394
|
-
throw clientValidationError(`delete: only resource IDs (${RESOURCE_ID_PREFIX} prefix) are accepted — ` +
|
|
2395
|
-
`got an ID starting with "${observedPrefix}". ` +
|
|
2396
|
-
"To revoke a single access token, use the resource-scoped token endpoint.");
|
|
2397
|
-
}
|
|
2561
|
+
requireResourceCrid(id, "delete");
|
|
2398
2562
|
await this.requestNoContent(`/v1/qurls/${encodeURIComponent(id)}`);
|
|
2399
2563
|
}
|
|
2400
2564
|
/**
|
|
2401
2565
|
* Extend a qURL's expiration.
|
|
2402
2566
|
*
|
|
2403
|
-
* Accepts
|
|
2404
|
-
* prefix).
|
|
2567
|
+
* Accepts a CRID or qURL
|
|
2568
|
+
* display ID (`q_` prefix). Delegates to {@link update} with only the
|
|
2405
2569
|
* expiration fields. `ExtendInput` shares its `extend_by` / `expires_at`
|
|
2406
2570
|
* fields with `UpdateInput` but is *narrower in two ways*: (1) exactly
|
|
2407
2571
|
* one of the two must be present (XOR via `?: never`), where `UpdateInput`
|
|
@@ -2413,7 +2577,7 @@ export class QURLClient {
|
|
|
2413
2577
|
* enforced separately by the runtime check inside `update()`.
|
|
2414
2578
|
*/
|
|
2415
2579
|
async extend(id, input, options) {
|
|
2416
|
-
|
|
2580
|
+
validatePathId(id, "extend", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
|
|
2417
2581
|
requireObjectInput(input, "extend");
|
|
2418
2582
|
const { extend_by, expires_at } = input;
|
|
2419
2583
|
if (extend_by === undefined && expires_at === undefined) {
|
|
@@ -2427,11 +2591,12 @@ export class QURLClient {
|
|
|
2427
2591
|
/**
|
|
2428
2592
|
* Update a qURL — extend expiration, change description, rename tags.
|
|
2429
2593
|
*
|
|
2430
|
-
* Accepts
|
|
2431
|
-
* prefix); the API resolves
|
|
2594
|
+
* Accepts a CRID or qURL
|
|
2595
|
+
* display ID (`q_` prefix); the API resolves display IDs to the parent
|
|
2596
|
+
* resource automatically.
|
|
2432
2597
|
*/
|
|
2433
2598
|
async update(id, input, options) {
|
|
2434
|
-
|
|
2599
|
+
validatePathId(id, "update", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
|
|
2435
2600
|
requireObjectInput(input, "update");
|
|
2436
2601
|
requireNoUnknownFields(input, UPDATE_FIELD_KEYS, "update");
|
|
2437
2602
|
// Normalize null → undefined so untyped-JS callers passing
|
|
@@ -2458,15 +2623,16 @@ export class QURLClient {
|
|
|
2458
2623
|
*
|
|
2459
2624
|
* Portal-flow equivalent: {@link createPortal}.
|
|
2460
2625
|
*
|
|
2461
|
-
* Accepts
|
|
2462
|
-
* prefix); the API resolves
|
|
2626
|
+
* Accepts a CRID or qURL
|
|
2627
|
+
* display ID (`q_` prefix); the API resolves display IDs to the parent
|
|
2628
|
+
* resource automatically.
|
|
2463
2629
|
*
|
|
2464
2630
|
* Passing `{}` or an object with all fields `null`/`undefined` is
|
|
2465
2631
|
* equivalent to omitting the second argument: no body is sent, and the
|
|
2466
2632
|
* server applies its 24h default expiration.
|
|
2467
2633
|
*/
|
|
2468
2634
|
async mintLink(id, input, options) {
|
|
2469
|
-
|
|
2635
|
+
validatePathId(id, "mintLink", "id", RESOURCE_OR_QURL_ID_PATH_OPTIONS);
|
|
2470
2636
|
// Normalize null → omitted so untyped-JS callers passing
|
|
2471
2637
|
// `{ expires_in: null, expires_at: "..." }` don't leak null into
|
|
2472
2638
|
// the wire body via JSON.stringify and don't bypass the XOR check
|
|
@@ -2580,12 +2746,12 @@ export class QURLClient {
|
|
|
2580
2746
|
}
|
|
2581
2747
|
/** Get one resource plus its bounded qURL preview. */
|
|
2582
2748
|
async getResource(id) {
|
|
2583
|
-
|
|
2749
|
+
requireResourceCrid(id, "getResource");
|
|
2584
2750
|
return this.request("GET", `/v1/resources/${encodeURIComponent(id)}`);
|
|
2585
2751
|
}
|
|
2586
2752
|
/** Update resource metadata. */
|
|
2587
2753
|
async updateResource(id, input, options) {
|
|
2588
|
-
|
|
2754
|
+
requireResourceCrid(id, "updateResource");
|
|
2589
2755
|
requireObjectInput(input, "updateResource");
|
|
2590
2756
|
requireNoUnknownFields(input, UPDATE_RESOURCE_FIELD_KEYS, "updateResource");
|
|
2591
2757
|
const validationInput = normalizePatchFields(input, UPDATE_RESOURCE_FIELD_KEYS, { preserveNullFields: ["alias"] });
|
|
@@ -2599,7 +2765,7 @@ export class QURLClient {
|
|
|
2599
2765
|
}
|
|
2600
2766
|
/** Revoke a resource and all of its qURLs. */
|
|
2601
2767
|
async deleteResource(id) {
|
|
2602
|
-
|
|
2768
|
+
requireResourceCrid(id, "deleteResource");
|
|
2603
2769
|
await this.requestNoContent(`/v1/resources/${encodeURIComponent(id)}`);
|
|
2604
2770
|
}
|
|
2605
2771
|
/**
|
|
@@ -2608,7 +2774,7 @@ export class QURLClient {
|
|
|
2608
2774
|
* Portal-flow equivalent: {@link createPortal} (same endpoint).
|
|
2609
2775
|
*/
|
|
2610
2776
|
async createQurlForResource(id, input, options) {
|
|
2611
|
-
|
|
2777
|
+
requireResourceCrid(id, "createQurlForResource");
|
|
2612
2778
|
let normalized = input;
|
|
2613
2779
|
if (input !== undefined) {
|
|
2614
2780
|
requireObjectInput(input, "createQurlForResource");
|
|
@@ -2619,16 +2785,83 @@ export class QURLClient {
|
|
|
2619
2785
|
validateQurlTokenOptions(normalized);
|
|
2620
2786
|
return this.request("POST", `/v1/resources/${encodeURIComponent(id)}/qurls`, normalized, options);
|
|
2621
2787
|
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Mint a fresh, short-lived share link for an existing CRID.
|
|
2790
|
+
* The returned link is secret and is not retrievable after this response.
|
|
2791
|
+
* If you hold the trusted resource DER SPKI, call `ShareLink.verifyCrid()`
|
|
2792
|
+
* before delivery to verify that the response CRID derives from that key.
|
|
2793
|
+
*/
|
|
2794
|
+
async shareResource(id, options = {}, requestOptions) {
|
|
2795
|
+
requireResourceCrid(id, "shareResource");
|
|
2796
|
+
requireObjectInput(options, "shareResource");
|
|
2797
|
+
requireNoUnknownFields(options, SHARE_RESOURCE_OPTION_KEYS, "shareResource");
|
|
2798
|
+
const body = {};
|
|
2799
|
+
if (options.ttlSeconds !== undefined) {
|
|
2800
|
+
if (typeof options.ttlSeconds !== "number" ||
|
|
2801
|
+
!Number.isSafeInteger(options.ttlSeconds) ||
|
|
2802
|
+
options.ttlSeconds <= 0) {
|
|
2803
|
+
throw clientValidationError("shareResource: ttlSeconds must be a positive safe integer");
|
|
2804
|
+
}
|
|
2805
|
+
body.ttl_seconds = options.ttlSeconds;
|
|
2806
|
+
}
|
|
2807
|
+
const data = await this.request("POST", `/v1/resources/${encodeURIComponent(id)}/share`, body, requestOptions);
|
|
2808
|
+
if (typeof data?.qurl !== "string" || data.qurl.trim() === "") {
|
|
2809
|
+
throw unexpectedResponseError("shareResource: response is missing qurl");
|
|
2810
|
+
}
|
|
2811
|
+
if (data.crid !== id) {
|
|
2812
|
+
throw unexpectedResponseError("shareResource: response CRID does not match the requested CRID");
|
|
2813
|
+
}
|
|
2814
|
+
const wire = data;
|
|
2815
|
+
for (const field of ["qurl_id", "type", "expires_at"]) {
|
|
2816
|
+
const value = wire[field];
|
|
2817
|
+
if (value !== undefined && value !== null && typeof value !== "string") {
|
|
2818
|
+
throw unexpectedResponseError(`shareResource: response has invalid ${field}`);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
for (const field of ["qurl_id", "type"]) {
|
|
2822
|
+
const value = wire[field];
|
|
2823
|
+
if (typeof value === "string" && (value.trim() === "" || value.trim() !== value)) {
|
|
2824
|
+
throw unexpectedResponseError(`shareResource: response has invalid ${field}`);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
if (wire.expires_in_seconds !== undefined &&
|
|
2828
|
+
wire.expires_in_seconds !== null &&
|
|
2829
|
+
(typeof wire.expires_in_seconds !== "number" ||
|
|
2830
|
+
!Number.isSafeInteger(wire.expires_in_seconds) ||
|
|
2831
|
+
wire.expires_in_seconds < 0)) {
|
|
2832
|
+
throw unexpectedResponseError("shareResource: response has invalid expires_in_seconds");
|
|
2833
|
+
}
|
|
2834
|
+
if (wire.single_use !== undefined &&
|
|
2835
|
+
wire.single_use !== null &&
|
|
2836
|
+
typeof wire.single_use !== "boolean") {
|
|
2837
|
+
throw unexpectedResponseError("shareResource: response has invalid single_use");
|
|
2838
|
+
}
|
|
2839
|
+
const expiresAt = parseApiDate(typeof wire.expires_at === "string" ? wire.expires_at : undefined);
|
|
2840
|
+
if (typeof wire.expires_at === "string" && expiresAt === undefined) {
|
|
2841
|
+
throw unexpectedResponseError("shareResource: response has invalid expires_at");
|
|
2842
|
+
}
|
|
2843
|
+
return new ShareLink({
|
|
2844
|
+
// Repair transport-only edge whitespace on the one-time credential;
|
|
2845
|
+
// ancillary identifiers fail closed instead of being normalized.
|
|
2846
|
+
link: data.qurl.trim(),
|
|
2847
|
+
qurlId: typeof wire.qurl_id === "string" ? wire.qurl_id : undefined,
|
|
2848
|
+
crid: id,
|
|
2849
|
+
type: typeof wire.type === "string" ? wire.type : undefined,
|
|
2850
|
+
expiresAt,
|
|
2851
|
+
expiresInSeconds: typeof wire.expires_in_seconds === "number" ? wire.expires_in_seconds : undefined,
|
|
2852
|
+
singleUse: typeof wire.single_use === "boolean" ? wire.single_use : undefined,
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2622
2855
|
/** Revoke a specific qURL token on a resource. */
|
|
2623
2856
|
async revokeResourceQurl(id, qurlId) {
|
|
2624
|
-
|
|
2625
|
-
|
|
2857
|
+
requireResourceCrid(id, "revokeResourceQurl");
|
|
2858
|
+
validatePathId(qurlId, "revokeResourceQurl", "qurl id", QURL_ID_PATH_OPTIONS);
|
|
2626
2859
|
await this.requestNoContent(`/v1/resources/${encodeURIComponent(id)}/qurls/${encodeURIComponent(qurlId)}`);
|
|
2627
2860
|
}
|
|
2628
2861
|
/** Update a specific qURL token on a resource. */
|
|
2629
2862
|
async updateResourceQurl(id, qurlId, input, options) {
|
|
2630
|
-
|
|
2631
|
-
|
|
2863
|
+
requireResourceCrid(id, "updateResourceQurl");
|
|
2864
|
+
validatePathId(qurlId, "updateResourceQurl", "qurl id", QURL_ID_PATH_OPTIONS);
|
|
2632
2865
|
requireObjectInput(input, "updateResourceQurl");
|
|
2633
2866
|
requireNoUnknownFields(input, UPDATE_RESOURCE_QURL_FIELD_KEYS, "updateResourceQurl");
|
|
2634
2867
|
const normalized = normalizePatchFields(input, UPDATE_RESOURCE_QURL_FIELD_KEYS);
|
|
@@ -2644,7 +2877,7 @@ export class QURLClient {
|
|
|
2644
2877
|
* this page and emits a debug log rather than pretending it fetched all pages.
|
|
2645
2878
|
*/
|
|
2646
2879
|
async listResourceSessions(id) {
|
|
2647
|
-
|
|
2880
|
+
requireResourceCrid(id, "listResourceSessions");
|
|
2648
2881
|
const { data, meta } = await this.rawRequest("GET", `/v1/resources/${encodeURIComponent(id)}/sessions`);
|
|
2649
2882
|
if (meta?.has_more || meta?.next_cursor) {
|
|
2650
2883
|
this.log("listResourceSessions: pagination metadata surfaced on unpaginated endpoint", {
|
|
@@ -2661,7 +2894,7 @@ export class QURLClient {
|
|
|
2661
2894
|
}
|
|
2662
2895
|
/** Terminate all active sessions for a resource and return the server count. */
|
|
2663
2896
|
async terminateAllResourceSessions(id) {
|
|
2664
|
-
|
|
2897
|
+
requireResourceCrid(id, "terminateAllResourceSessions");
|
|
2665
2898
|
const path = `/v1/resources/${encodeURIComponent(id)}/sessions`;
|
|
2666
2899
|
const { data, meta, __http_status } = await this.rawRequest("DELETE", path);
|
|
2667
2900
|
if (__http_status === 204) {
|
|
@@ -2676,8 +2909,8 @@ export class QURLClient {
|
|
|
2676
2909
|
}
|
|
2677
2910
|
/** Terminate a specific resource session. */
|
|
2678
2911
|
async terminateResourceSession(id, sessionId) {
|
|
2679
|
-
|
|
2680
|
-
|
|
2912
|
+
requireResourceCrid(id, "terminateResourceSession");
|
|
2913
|
+
validatePathId(sessionId, "terminateResourceSession", "session id");
|
|
2681
2914
|
await this.requestNoContent(`/v1/resources/${encodeURIComponent(id)}/sessions/${encodeURIComponent(sessionId)}`);
|
|
2682
2915
|
}
|
|
2683
2916
|
/** List connector installations. */
|
|
@@ -2780,22 +3013,22 @@ export class QURLClient {
|
|
|
2780
3013
|
}
|
|
2781
3014
|
/** Get custom domain status. */
|
|
2782
3015
|
async getDomain(domain) {
|
|
2783
|
-
|
|
3016
|
+
validatePathId(domain, "getDomain", "domain");
|
|
2784
3017
|
return this.request("GET", `/v1/domains/${encodeURIComponent(domain)}`);
|
|
2785
3018
|
}
|
|
2786
3019
|
/** Remove a custom domain. */
|
|
2787
3020
|
async deleteDomain(domain) {
|
|
2788
|
-
|
|
3021
|
+
validatePathId(domain, "deleteDomain", "domain");
|
|
2789
3022
|
await this.requestNoContent(`/v1/domains/${encodeURIComponent(domain)}`);
|
|
2790
3023
|
}
|
|
2791
3024
|
/** Trigger DNS verification for a custom domain. */
|
|
2792
3025
|
async verifyDomain(domain, options) {
|
|
2793
|
-
|
|
3026
|
+
validatePathId(domain, "verifyDomain", "domain");
|
|
2794
3027
|
return this.request("POST", `/v1/domains/${encodeURIComponent(domain)}/verify`, undefined, options);
|
|
2795
3028
|
}
|
|
2796
3029
|
/** Regenerate a domain verification token. */
|
|
2797
3030
|
async regenerateDomainToken(domain, options) {
|
|
2798
|
-
|
|
3031
|
+
validatePathId(domain, "regenerateDomainToken", "domain");
|
|
2799
3032
|
return this.request("POST", `/v1/domains/${encodeURIComponent(domain)}/regenerate-token`, undefined, options);
|
|
2800
3033
|
}
|
|
2801
3034
|
/** List webhooks. */
|
|
@@ -2831,12 +3064,12 @@ export class QURLClient {
|
|
|
2831
3064
|
}
|
|
2832
3065
|
/** Get a webhook. */
|
|
2833
3066
|
async getWebhook(id) {
|
|
2834
|
-
|
|
3067
|
+
validatePathId(id, "getWebhook");
|
|
2835
3068
|
return this.request("GET", `/v1/webhooks/${encodeURIComponent(id)}`);
|
|
2836
3069
|
}
|
|
2837
3070
|
/** Update a webhook. */
|
|
2838
3071
|
async updateWebhook(id, input, options) {
|
|
2839
|
-
|
|
3072
|
+
validatePathId(id, "updateWebhook");
|
|
2840
3073
|
requireObjectInput(input, "updateWebhook");
|
|
2841
3074
|
requireNoUnknownFields(input, UPDATE_WEBHOOK_FIELD_KEYS, "updateWebhook");
|
|
2842
3075
|
const normalized = normalizePatchFields(input, UPDATE_WEBHOOK_FIELD_KEYS);
|
|
@@ -2846,17 +3079,17 @@ export class QURLClient {
|
|
|
2846
3079
|
}
|
|
2847
3080
|
/** Delete a webhook. */
|
|
2848
3081
|
async deleteWebhook(id) {
|
|
2849
|
-
|
|
3082
|
+
validatePathId(id, "deleteWebhook");
|
|
2850
3083
|
await this.requestNoContent(`/v1/webhooks/${encodeURIComponent(id)}`);
|
|
2851
3084
|
}
|
|
2852
3085
|
/** Regenerate a webhook signing secret. */
|
|
2853
3086
|
async regenerateWebhookSecret(id, options) {
|
|
2854
|
-
|
|
3087
|
+
validatePathId(id, "regenerateWebhookSecret");
|
|
2855
3088
|
return this.request("POST", `/v1/webhooks/${encodeURIComponent(id)}/secret`, undefined, options);
|
|
2856
3089
|
}
|
|
2857
3090
|
/** List delivery attempts for a webhook. */
|
|
2858
3091
|
async listWebhookDeliveries(id, input = {}) {
|
|
2859
|
-
|
|
3092
|
+
validatePathId(id, "listWebhookDeliveries");
|
|
2860
3093
|
const { data, meta } = await this.rawRequest("GET", appendQuery(`/v1/webhooks/${encodeURIComponent(id)}/deliveries`, input, WEBHOOK_DELIVERY_LIST_PARAM_KEYS, "listWebhookDeliveries"));
|
|
2861
3094
|
return pageFromMeta({ deliveries: data ?? [] }, meta);
|
|
2862
3095
|
}
|
|
@@ -2867,7 +3100,7 @@ export class QURLClient {
|
|
|
2867
3100
|
* iteration begins.
|
|
2868
3101
|
*/
|
|
2869
3102
|
listAllWebhookDeliveries(id, input = {}) {
|
|
2870
|
-
|
|
3103
|
+
validatePathId(id, "listAllWebhookDeliveries");
|
|
2871
3104
|
validateListAllInput(input, WEBHOOK_DELIVERY_LIST_PARAM_KEYS, "listAllWebhookDeliveries");
|
|
2872
3105
|
return this.paginateAll("listAllWebhookDeliveries", input, (pageInput) => this.listWebhookDeliveries(id, pageInput), (page) => page.deliveries);
|
|
2873
3106
|
}
|
|
@@ -2876,10 +3109,34 @@ export class QURLClient {
|
|
|
2876
3109
|
requireObjectInput(input, "createApiKey");
|
|
2877
3110
|
requireNoUnknownFields(input, CREATE_API_KEY_FIELD_KEYS, "createApiKey");
|
|
2878
3111
|
const normalizedRecord = normalizePatchFields(input, CREATE_API_KEY_FIELD_KEYS);
|
|
3112
|
+
// `kind` is required by the server and has no default there. Filling it
|
|
3113
|
+
// in keeps the common durable-key call site unchanged.
|
|
3114
|
+
normalizedRecord.kind ??= "api_key";
|
|
3115
|
+
if (normalizedRecord.kind !== "api_key" && normalizedRecord.kind !== "enrollment_token") {
|
|
3116
|
+
throw clientValidationError("createApiKey: kind must be 'api_key' or 'enrollment_token'");
|
|
3117
|
+
}
|
|
3118
|
+
// `scopes` and `target`/`claims` are mutually exclusive by kind, and the
|
|
3119
|
+
// server rejects either mismatch with 400 invalid_input. Fail both
|
|
3120
|
+
// directions locally rather than paying a round trip.
|
|
3121
|
+
const isEnrollmentToken = normalizedRecord.kind === "enrollment_token";
|
|
3122
|
+
if (isEnrollmentToken) {
|
|
3123
|
+
if (normalizedRecord.scopes !== undefined) {
|
|
3124
|
+
throw clientValidationError("createApiKey: scopes is not accepted for kind 'enrollment_token'; " +
|
|
3125
|
+
"the server assigns them from target");
|
|
3126
|
+
}
|
|
3127
|
+
validateEnrollmentTokenFields(normalizedRecord, "createApiKey");
|
|
3128
|
+
}
|
|
3129
|
+
else {
|
|
3130
|
+
for (const field of ["target", "claims", "expires_in"]) {
|
|
3131
|
+
if (normalizedRecord[field] !== undefined) {
|
|
3132
|
+
throw clientValidationError(`createApiKey: ${field} is only accepted for kind 'enrollment_token'`);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
2879
3136
|
const normalized = normalizedRecord;
|
|
2880
3137
|
validateApiKeyWriteFields(normalizedRecord, "createApiKey", {
|
|
2881
3138
|
name: true,
|
|
2882
|
-
scopes:
|
|
3139
|
+
scopes: !isEnrollmentToken,
|
|
2883
3140
|
});
|
|
2884
3141
|
return this.request("POST", "/v1/api-keys", normalized, options);
|
|
2885
3142
|
}
|
|
@@ -2900,7 +3157,7 @@ export class QURLClient {
|
|
|
2900
3157
|
}
|
|
2901
3158
|
/** Update an API key. */
|
|
2902
3159
|
async updateApiKey(keyId, input, options) {
|
|
2903
|
-
|
|
3160
|
+
validatePathId(keyId, "updateApiKey");
|
|
2904
3161
|
requireObjectInput(input, "updateApiKey");
|
|
2905
3162
|
requireNoUnknownFields(input, UPDATE_API_KEY_FIELD_KEYS, "updateApiKey");
|
|
2906
3163
|
const normalized = normalizePatchFields(input, UPDATE_API_KEY_FIELD_KEYS);
|
|
@@ -2910,7 +3167,7 @@ export class QURLClient {
|
|
|
2910
3167
|
}
|
|
2911
3168
|
/** Revoke an API key. */
|
|
2912
3169
|
async revokeApiKey(keyId) {
|
|
2913
|
-
|
|
3170
|
+
validatePathId(keyId, "revokeApiKey");
|
|
2914
3171
|
await this.requestNoContent(`/v1/api-keys/${encodeURIComponent(keyId)}`);
|
|
2915
3172
|
}
|
|
2916
3173
|
/** Redeem an access code. */
|
|
@@ -2928,7 +3185,7 @@ export class QURLClient {
|
|
|
2928
3185
|
requireNoUnknownFields(input, CREATE_ACCESS_CODE_FIELD_KEYS, "createAccessCode");
|
|
2929
3186
|
const normalizedRecord = normalizePatchFields(input, CREATE_ACCESS_CODE_FIELD_KEYS);
|
|
2930
3187
|
const normalized = normalizedRecord;
|
|
2931
|
-
|
|
3188
|
+
requireResourceCrid(normalizedRecord.resource_id, "createAccessCode", "resource_id");
|
|
2932
3189
|
return this.request("POST", "/v1/access-codes", normalized, options);
|
|
2933
3190
|
}
|
|
2934
3191
|
/**
|
|
@@ -2955,7 +3212,7 @@ export class QURLClient {
|
|
|
2955
3212
|
}
|
|
2956
3213
|
/** Revoke an access code. */
|
|
2957
3214
|
async revokeAccessCode(id) {
|
|
2958
|
-
|
|
3215
|
+
validatePathId(id, "revokeAccessCode");
|
|
2959
3216
|
await this.requestNoContent(`/v1/access-codes/${encodeURIComponent(id)}`);
|
|
2960
3217
|
}
|
|
2961
3218
|
// --- Internal HTTP plumbing ---
|
|
@@ -3442,7 +3699,7 @@ export class QURLClient {
|
|
|
3442
3699
|
/**
|
|
3443
3700
|
* A LayerV-protected resource, bound to the client that produced it.
|
|
3444
3701
|
*
|
|
3445
|
-
* Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.
|
|
3702
|
+
* Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.resourceByCrid} —
|
|
3446
3703
|
* not constructed directly. Mint short-lived access links for the resource
|
|
3447
3704
|
* with {@link createPortal} (qurl-go: `qurl.Resource`).
|
|
3448
3705
|
*/
|
|
@@ -3451,19 +3708,19 @@ export class ProtectedResource {
|
|
|
3451
3708
|
// CONTRIBUTING.md's dual-build rule bars) so the binding stays on the
|
|
3452
3709
|
// instance and out of Object.keys/JSON.stringify output.
|
|
3453
3710
|
#client;
|
|
3454
|
-
/** The
|
|
3455
|
-
|
|
3711
|
+
/** The public resource CRID. */
|
|
3712
|
+
crid;
|
|
3456
3713
|
/** The private URL protected by this resource, when known. */
|
|
3457
3714
|
targetUrl;
|
|
3458
3715
|
/**
|
|
3459
3716
|
* Full server-populated {@link Resource} metadata when the handle came
|
|
3460
|
-
* from an API response; `undefined` for {@link QURLClient.
|
|
3717
|
+
* from an API response; `undefined` for {@link QURLClient.resourceByCrid}
|
|
3461
3718
|
* handles and for the one-call {@link QURLClient.createPortalForUrl} path.
|
|
3462
3719
|
*/
|
|
3463
3720
|
details;
|
|
3464
3721
|
constructor(client, id, targetUrl, details) {
|
|
3465
3722
|
this.#client = client;
|
|
3466
|
-
this.
|
|
3723
|
+
this.crid = id;
|
|
3467
3724
|
this.targetUrl = targetUrl;
|
|
3468
3725
|
this.details = details;
|
|
3469
3726
|
}
|
|
@@ -3494,8 +3751,8 @@ const CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN = Symbol("validated ConnectorResource
|
|
|
3494
3751
|
*/
|
|
3495
3752
|
export class ConnectorResource {
|
|
3496
3753
|
#client;
|
|
3497
|
-
|
|
3498
|
-
/**
|
|
3754
|
+
resourcePublicKey;
|
|
3755
|
+
/** Required public identifier, verified against the returned resource key. */
|
|
3499
3756
|
crid;
|
|
3500
3757
|
connectorRoutingId;
|
|
3501
3758
|
knockResourceId;
|
|
@@ -3507,7 +3764,7 @@ export class ConnectorResource {
|
|
|
3507
3764
|
throw clientValidationError("ConnectorResource cannot be constructed directly; load it through QURLClient");
|
|
3508
3765
|
}
|
|
3509
3766
|
this.#client = client;
|
|
3510
|
-
this.
|
|
3767
|
+
this.resourcePublicKey = details.resource_id;
|
|
3511
3768
|
this.crid = details.crid;
|
|
3512
3769
|
this.connectorRoutingId = details.connector_routing_id;
|
|
3513
3770
|
this.knockResourceId = details.knock_resource_id;
|
|
@@ -3516,7 +3773,7 @@ export class ConnectorResource {
|
|
|
3516
3773
|
}
|
|
3517
3774
|
/** Mint a short-lived portal for this Connector resource. */
|
|
3518
3775
|
async createPortal(opts = {}, requestOptions) {
|
|
3519
|
-
return this.#client.createPortal(this.
|
|
3776
|
+
return this.#client.createPortal(this.crid, opts, requestOptions);
|
|
3520
3777
|
}
|
|
3521
3778
|
}
|
|
3522
3779
|
//# sourceMappingURL=client.js.map
|