@layervai/qurl 0.5.0 → 0.6.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 +48 -5
- package/dist/cjs/client.d.ts +41 -18
- package/dist/cjs/client.js +312 -50
- package/dist/cjs/errors.d.ts +24 -3
- package/dist/cjs/errors.js +34 -4
- package/dist/cjs/index.d.ts +3 -2
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/types.d.ts +8 -0
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/client.d.ts +41 -18
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +311 -50
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/errors.d.ts +24 -3
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +32 -3
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types.d.ts +8 -0
- 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 +1 -1
package/dist/esm/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createError, ERROR_CODE_AMBIGUOUS_RESOURCE, ERROR_CODE_CLIENT_VALIDATION, ERROR_CODE_RESOURCE_NOT_FOUND, ERROR_CODE_UNEXPECTED_RESPONSE, ERROR_CODE_UNKNOWN, NetworkError, NotFoundError, QURLError, RuntimeError, TimeoutError, ValidationError, } from "./errors.js";
|
|
1
|
+
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
2
|
import { stripTrailingSlashes } from "./internal.js";
|
|
3
3
|
import { VERSION } from "./version.js";
|
|
4
4
|
const DEFAULT_BASE_URL = "https://api.layerv.ai";
|
|
@@ -320,14 +320,14 @@ function validateRequestOptions(options) {
|
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
|
-
function idempotencyKeyForRequest(method, options) {
|
|
323
|
+
function idempotencyKeyForRequest(method, options, generateWhenMissing = true) {
|
|
324
324
|
// Validate before method gating so future read-only methods that accept
|
|
325
325
|
// RequestOptions fail loudly on malformed options instead of discarding them.
|
|
326
326
|
validateRequestOptions(options);
|
|
327
327
|
if (!IDEMPOTENCY_KEY_METHODS.has(method)) {
|
|
328
328
|
return undefined;
|
|
329
329
|
}
|
|
330
|
-
return options?.idempotencyKey ?? generateUuidV7();
|
|
330
|
+
return options?.idempotencyKey ?? (generateWhenMissing ? generateUuidV7() : undefined);
|
|
331
331
|
}
|
|
332
332
|
function generateUuidV7() {
|
|
333
333
|
const bytes = new Uint8Array(16);
|
|
@@ -721,6 +721,14 @@ const MAX_TARGET_PATH = 2048;
|
|
|
721
721
|
const MAX_TAGS = 10;
|
|
722
722
|
const MAX_TAG_LENGTH = 50;
|
|
723
723
|
const MAX_AUTO_PAGINATION_PAGES = 10_000;
|
|
724
|
+
// Keep these identity contracts aligned with qurl-go and the public API.
|
|
725
|
+
const CONNECTOR_SLUG_PATTERN = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
|
|
726
|
+
// Current service schemas intentionally share this grammar, but keep the
|
|
727
|
+
// mutable alias contract separate from the immutable lookup identity.
|
|
728
|
+
const CONNECTOR_ALIAS_PATTERN = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
|
|
729
|
+
const CONNECTOR_RESOURCE_ID_PATTERN = /^[A-Za-z0-9_-]{122}$/;
|
|
730
|
+
const CONNECTOR_RESOURCE_ID_DER_PREFIX = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE";
|
|
731
|
+
const CONNECTOR_ROUTING_ID_PATTERN = /^c-[a-z2-7]{51}[aq]$/;
|
|
724
732
|
// CreateQurlRequest.target_url pattern is loose (just a URI) but
|
|
725
733
|
// UpdateQurlRequest.tags pattern is specific — enforce it here.
|
|
726
734
|
const TAG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9 _-]*$/;
|
|
@@ -842,6 +850,163 @@ function requireNonEmptyId(id, method, field = "id") {
|
|
|
842
850
|
throw clientValidationError(`${method}: ${field} must not include leading or trailing whitespace`);
|
|
843
851
|
}
|
|
844
852
|
}
|
|
853
|
+
function requireConnectorSlug(slug, method) {
|
|
854
|
+
if (typeof slug !== "string" || !CONNECTOR_SLUG_PATTERN.test(slug)) {
|
|
855
|
+
throw clientValidationError(`${method}: slug must be 3-64 lowercase alphanumeric or hyphen characters, start with a letter, and end alphanumeric`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function classifyConnectorMutationFailure(operation, error) {
|
|
859
|
+
if (!(error instanceof QURLError)) {
|
|
860
|
+
throw new ConnectorResourceOutcomeUnknownError(new RuntimeError("Unexpected failure after Connector mutation dispatch", { cause: error }));
|
|
861
|
+
}
|
|
862
|
+
// Most authoritative 4xx responses prove that the mutation was rejected.
|
|
863
|
+
// bootstrap_key_consumed is the exception: the service returns it from the
|
|
864
|
+
// consume phase after the resource may have been bound or found.
|
|
865
|
+
// Callers perform argument/runtime preflight outside their mutation try
|
|
866
|
+
// blocks; every other surfaced failure therefore follows dispatch or a
|
|
867
|
+
// nominal success whose resource contract could not be consumed, so callers
|
|
868
|
+
// must reconcile before retrying. Ensure's exact 201 + valid resource but
|
|
869
|
+
// missing found_existing is handled after this classifier: the row proves
|
|
870
|
+
// the selected resource, while only its required metadata is unavailable.
|
|
871
|
+
if (operation === "ensure" && error.status === 409 && error.code === "bootstrap_key_consumed") {
|
|
872
|
+
throw new ConnectorResourceOutcomeUnknownError(error);
|
|
873
|
+
}
|
|
874
|
+
if (error.status >= 400 && error.status < 500) {
|
|
875
|
+
throw error;
|
|
876
|
+
}
|
|
877
|
+
throw new ConnectorResourceOutcomeUnknownError(error);
|
|
878
|
+
}
|
|
879
|
+
function decodeCanonicalBase64Url(value) {
|
|
880
|
+
if (!CONNECTOR_RESOURCE_ID_PATTERN.test(value) ||
|
|
881
|
+
!value.startsWith(CONNECTOR_RESOURCE_ID_DER_PREFIX)) {
|
|
882
|
+
return undefined;
|
|
883
|
+
}
|
|
884
|
+
try {
|
|
885
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/") + "==";
|
|
886
|
+
const binary = atob(padded);
|
|
887
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
888
|
+
const canonical = btoa(String.fromCharCode(...bytes))
|
|
889
|
+
.replace(/\+/g, "-")
|
|
890
|
+
.replace(/\//g, "_")
|
|
891
|
+
.replace(/=+$/, "");
|
|
892
|
+
return canonical === value ? bytes : undefined;
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
return undefined;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
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
|
+
function requireConnectorSubtleCrypto(method) {
|
|
908
|
+
if (!globalThis.crypto?.subtle) {
|
|
909
|
+
throw new RuntimeError(`${method}: requires the Web Crypto SubtleCrypto API`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
async function requireConnectorResourceId(resourceId, method) {
|
|
913
|
+
const der = decodeCanonicalBase64Url(resourceId);
|
|
914
|
+
if (!der) {
|
|
915
|
+
throw clientValidationError(`${method}: resource id must be a canonical unpadded base64url P-256 DER SPKI public key`);
|
|
916
|
+
}
|
|
917
|
+
requireConnectorSubtleCrypto(method);
|
|
918
|
+
if (!(await importsAsConnectorPublicKey(der))) {
|
|
919
|
+
throw clientValidationError(`${method}: resource id must be a canonical unpadded base64url P-256 DER SPKI public key`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
async function importsAsConnectorPublicKey(der) {
|
|
923
|
+
try {
|
|
924
|
+
const keyData = new ArrayBuffer(der.byteLength);
|
|
925
|
+
new Uint8Array(keyData).set(der);
|
|
926
|
+
await globalThis.crypto.subtle.importKey("spki", keyData, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
catch {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
function containsControlCharacter(value) {
|
|
934
|
+
return Array.from(value).some((character) => {
|
|
935
|
+
const codePoint = character.codePointAt(0);
|
|
936
|
+
return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
async function parseConnectorResource(client, value, method, expectation = {}) {
|
|
940
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
941
|
+
throw unexpectedResponseError(`${method}: response is missing connector resource data`);
|
|
942
|
+
}
|
|
943
|
+
const resource = value;
|
|
944
|
+
if (typeof resource.resource_id !== "string") {
|
|
945
|
+
throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
|
|
946
|
+
}
|
|
947
|
+
if (expectation.resourceId !== undefined && resource.resource_id !== expectation.resourceId) {
|
|
948
|
+
throw unexpectedResponseError(`${method}: response resource_id does not match the request`);
|
|
949
|
+
}
|
|
950
|
+
// A by-ID request already validated its requested resource key and matched
|
|
951
|
+
// this response byte-for-byte, so do not repeat the async key import.
|
|
952
|
+
if (expectation.resourceId === undefined &&
|
|
953
|
+
!(await isValidConnectorResourceId(resource.resource_id))) {
|
|
954
|
+
throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
|
|
955
|
+
}
|
|
956
|
+
if (typeof resource.connector_routing_id !== "string" ||
|
|
957
|
+
!CONNECTOR_ROUTING_ID_PATTERN.test(resource.connector_routing_id)) {
|
|
958
|
+
throw unexpectedResponseError(`${method}: response has missing or invalid connector_routing_id`);
|
|
959
|
+
}
|
|
960
|
+
if (typeof resource.knock_resource_id !== "string" ||
|
|
961
|
+
resource.knock_resource_id.trim() === "" ||
|
|
962
|
+
resource.knock_resource_id.trim() !== resource.knock_resource_id ||
|
|
963
|
+
containsControlCharacter(resource.knock_resource_id)) {
|
|
964
|
+
throw unexpectedResponseError(`${method}: response has missing or invalid knock_resource_id`);
|
|
965
|
+
}
|
|
966
|
+
if (resource.resource_id === resource.knock_resource_id ||
|
|
967
|
+
resource.connector_routing_id === resource.knock_resource_id) {
|
|
968
|
+
throw unexpectedResponseError(`${method}: response cross-wires resource identity, routing, and admission values`);
|
|
969
|
+
}
|
|
970
|
+
if (resource.type !== "tunnel") {
|
|
971
|
+
throw unexpectedResponseError(`${method}: response resource type is not tunnel`);
|
|
972
|
+
}
|
|
973
|
+
if (typeof resource.slug !== "string" || !CONNECTOR_SLUG_PATTERN.test(resource.slug)) {
|
|
974
|
+
throw unexpectedResponseError(`${method}: response has missing or invalid slug`);
|
|
975
|
+
}
|
|
976
|
+
if (expectation.slug !== undefined && resource.slug !== expectation.slug) {
|
|
977
|
+
throw unexpectedResponseError(`${method}: response slug does not match the request`);
|
|
978
|
+
}
|
|
979
|
+
if (resource.alias !== undefined &&
|
|
980
|
+
resource.alias !== null &&
|
|
981
|
+
(typeof resource.alias !== "string" || !CONNECTOR_ALIAS_PATTERN.test(resource.alias))) {
|
|
982
|
+
// The public API intentionally gives mutable aliases and immutable slugs the
|
|
983
|
+
// same canonical wire grammar (domain aliasPattern/slugPattern). Keeping
|
|
984
|
+
// response validation aligned avoids accepting a row the service itself
|
|
985
|
+
// could not create or update.
|
|
986
|
+
throw unexpectedResponseError(`${method}: response has invalid alias`);
|
|
987
|
+
}
|
|
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
|
+
if (resource.status === "revoked") {
|
|
995
|
+
if (expectation.revokedIsLifecycleError !== true) {
|
|
996
|
+
throw unexpectedResponseError(`${method}: active-only operation returned a revoked connector resource`);
|
|
997
|
+
}
|
|
998
|
+
throw new QURLError({
|
|
999
|
+
status: 0,
|
|
1000
|
+
code: ERROR_CODE_CONNECTOR_RESOURCE_REVOKED,
|
|
1001
|
+
title: "Connector Resource Revoked",
|
|
1002
|
+
detail: `${method}: qURL Connector resource is revoked`,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
if (resource.status !== "active") {
|
|
1006
|
+
throw unexpectedResponseError(`${method}: response has invalid resource status`);
|
|
1007
|
+
}
|
|
1008
|
+
return new ConnectorResource(client, resource, CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN);
|
|
1009
|
+
}
|
|
845
1010
|
function requireValidTags(tags) {
|
|
846
1011
|
// null tolerance for untyped-JS callers — treated as "don't touch."
|
|
847
1012
|
// Duplicates pass through to the server (authoritative on dedupe policy).
|
|
@@ -1424,10 +1589,9 @@ function parsePortal(data, method) {
|
|
|
1424
1589
|
};
|
|
1425
1590
|
}
|
|
1426
1591
|
/**
|
|
1427
|
-
* Validate portal options and build the create-portal wire body
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1430
|
-
* createQurlForResource).
|
|
1592
|
+
* Validate portal options and build the create-portal wire body. The service
|
|
1593
|
+
* requires a JSON object even when every option is omitted; `{}` selects the
|
|
1594
|
+
* owner's plan defaults and matches qurl-go's zero-value request struct.
|
|
1431
1595
|
*/
|
|
1432
1596
|
function buildCreatePortalBody(opts, method) {
|
|
1433
1597
|
requireObjectInput(opts, method);
|
|
@@ -1465,7 +1629,7 @@ function buildCreatePortalBody(opts, method) {
|
|
|
1465
1629
|
body.session_duration = wireSessionDuration;
|
|
1466
1630
|
if (targetPath !== undefined)
|
|
1467
1631
|
body.target_path = targetPath;
|
|
1468
|
-
return
|
|
1632
|
+
return body;
|
|
1469
1633
|
}
|
|
1470
1634
|
/** qURL API client. */
|
|
1471
1635
|
export class QURLClient {
|
|
@@ -1788,49 +1952,110 @@ export class QURLClient {
|
|
|
1788
1952
|
requireNonEmptyId(id, "resourceById");
|
|
1789
1953
|
return new ProtectedResource(this, id);
|
|
1790
1954
|
}
|
|
1791
|
-
/**
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1955
|
+
/** Find or create the active qURL Connector resource for an immutable slug. */
|
|
1956
|
+
async ensureConnectorResource(slug, requestOptions) {
|
|
1957
|
+
requireConnectorSlug(slug, "ensureConnectorResource");
|
|
1958
|
+
// Response validation imports the producer's P-256 resource key. Preflight
|
|
1959
|
+
// before POST so a missing runtime capability cannot orphan a committed row.
|
|
1960
|
+
requireConnectorSubtleCrypto("ensureConnectorResource");
|
|
1961
|
+
// Keep invalid options in the known pre-dispatch error arm. rawRequest
|
|
1962
|
+
// validates again when building the request, which protects future callers
|
|
1963
|
+
// that do not have this mutation-specific outcome contract.
|
|
1964
|
+
validateRequestOptions(requestOptions);
|
|
1965
|
+
let response;
|
|
1966
|
+
let resource;
|
|
1967
|
+
try {
|
|
1968
|
+
response = await this.rawRequest("POST", "/v1/resources", {
|
|
1969
|
+
type: "tunnel",
|
|
1970
|
+
slug,
|
|
1971
|
+
find_or_create: true,
|
|
1972
|
+
}, { requestOptions, retry: false, generateIdempotencyKey: false });
|
|
1973
|
+
// The find-or-create endpoint returns 201 for both newly-created
|
|
1974
|
+
// and found-existing rows; meta.found_existing distinguishes the arms.
|
|
1975
|
+
if (response.__http_status !== 201) {
|
|
1976
|
+
throw unexpectedResponseError(`ensureConnectorResource: expected HTTP 201, got ${response.__http_status ?? "unknown"}`);
|
|
1977
|
+
}
|
|
1978
|
+
resource = await parseConnectorResource(this, response.data, "ensureConnectorResource", {
|
|
1979
|
+
slug,
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
catch (error) {
|
|
1983
|
+
classifyConnectorMutationFailure("ensure", error);
|
|
1984
|
+
}
|
|
1985
|
+
if (response.meta !== undefined &&
|
|
1986
|
+
response.meta !== null &&
|
|
1987
|
+
(typeof response.meta !== "object" || Array.isArray(response.meta))) {
|
|
1988
|
+
classifyConnectorMutationFailure("ensure", unexpectedResponseError("ensureConnectorResource: response has invalid meta"));
|
|
1989
|
+
}
|
|
1990
|
+
const foundExisting = response.meta
|
|
1991
|
+
?.found_existing;
|
|
1992
|
+
if (foundExisting === undefined || foundExisting === null) {
|
|
1993
|
+
throw unexpectedResponseError("ensureConnectorResource: response is missing meta.found_existing; the validated resource exists, so reconcile by slug before retrying");
|
|
1994
|
+
}
|
|
1995
|
+
if (typeof foundExisting !== "boolean") {
|
|
1996
|
+
classifyConnectorMutationFailure("ensure", unexpectedResponseError("ensureConnectorResource: response has invalid meta.found_existing"));
|
|
1997
|
+
}
|
|
1998
|
+
return { resource, foundExisting };
|
|
1999
|
+
}
|
|
2000
|
+
/** Fetch a qURL Connector resource by its immutable public resource ID. */
|
|
2001
|
+
async getConnectorResource(resourceId) {
|
|
2002
|
+
await requireConnectorResourceId(resourceId, "getConnectorResource");
|
|
2003
|
+
const { data, __http_status } = await this.rawRequest("GET", `/v1/resources/${encodeURIComponent(resourceId)}`, undefined, { retry: false });
|
|
2004
|
+
if (__http_status !== 200) {
|
|
2005
|
+
throw unexpectedResponseError(`getConnectorResource: expected HTTP 200, got ${__http_status ?? "unknown"}`);
|
|
2006
|
+
}
|
|
2007
|
+
return parseConnectorResource(this, data?.resource, "getConnectorResource", {
|
|
2008
|
+
resourceId,
|
|
2009
|
+
revokedIsLifecycleError: true,
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
/** Fetch the single active qURL Connector resource for an immutable slug. */
|
|
2013
|
+
async getConnectorResourceBySlug(slug) {
|
|
2014
|
+
requireConnectorSlug(slug, "getConnectorResourceBySlug");
|
|
2015
|
+
// Parsing validates the returned P-256 resource key; avoid dispatching a
|
|
2016
|
+
// read that this runtime cannot safely consume.
|
|
2017
|
+
requireConnectorSubtleCrypto("getConnectorResourceBySlug");
|
|
2018
|
+
// The slug point lookup is intrinsically active-only. It also
|
|
2019
|
+
// rejects combining `slug` with `status`, so this query must stay slug-only.
|
|
2020
|
+
const { data, __http_status } = await this.rawRequest("GET", `/v1/resources?slug=${encodeURIComponent(slug)}`, undefined, { retry: false });
|
|
2021
|
+
if (__http_status !== 200) {
|
|
2022
|
+
throw unexpectedResponseError(`getConnectorResourceBySlug: expected HTTP 200, got ${__http_status ?? "unknown"}`);
|
|
2023
|
+
}
|
|
2024
|
+
if (!Array.isArray(data)) {
|
|
2025
|
+
throw unexpectedResponseError("getConnectorResourceBySlug: response has missing or invalid data");
|
|
2026
|
+
}
|
|
2027
|
+
if (data.length === 0) {
|
|
1811
2028
|
throw new NotFoundError({
|
|
1812
2029
|
status: 0,
|
|
1813
2030
|
code: ERROR_CODE_RESOURCE_NOT_FOUND,
|
|
1814
2031
|
title: "Resource Not Found",
|
|
1815
|
-
detail:
|
|
2032
|
+
detail: "getConnectorResourceBySlug: no active resource exists for the requested slug",
|
|
1816
2033
|
});
|
|
1817
2034
|
}
|
|
1818
|
-
if (
|
|
2035
|
+
if (data.length > 1) {
|
|
1819
2036
|
throw new QURLError({
|
|
1820
2037
|
status: 0,
|
|
1821
2038
|
code: ERROR_CODE_AMBIGUOUS_RESOURCE,
|
|
1822
2039
|
title: "Ambiguous Resource",
|
|
1823
|
-
detail: `
|
|
2040
|
+
detail: `getConnectorResourceBySlug: expected one active resource, got ${data.length}`,
|
|
1824
2041
|
});
|
|
1825
2042
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2043
|
+
return parseConnectorResource(this, data[0], "getConnectorResourceBySlug", { slug });
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Revoke a qURL Connector resource by immutable public resource ID.
|
|
2047
|
+
*
|
|
2048
|
+
* The API does not apply Idempotency-Key replay to DELETE. After an
|
|
2049
|
+
* outcome-unknown failure, reconcile by ID before issuing a deliberate retry.
|
|
2050
|
+
*/
|
|
2051
|
+
async deleteConnectorResource(resourceId) {
|
|
2052
|
+
await requireConnectorResourceId(resourceId, "deleteConnectorResource");
|
|
2053
|
+
try {
|
|
2054
|
+
await this.requestNoContent(`/v1/resources/${encodeURIComponent(resourceId)}`);
|
|
1829
2055
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
2056
|
+
catch (error) {
|
|
2057
|
+
classifyConnectorMutationFailure("delete", error);
|
|
1832
2058
|
}
|
|
1833
|
-
return new ProtectedResource(this, resource.resource_id, resource.target_url, resource);
|
|
1834
2059
|
}
|
|
1835
2060
|
/**
|
|
1836
2061
|
* Mint a short-lived qURL link (a portal) for a protected resource.
|
|
@@ -1839,7 +2064,7 @@ export class QURLClient {
|
|
|
1839
2064
|
* reach one private resource. Recipients open `portal.link` directly and
|
|
1840
2065
|
* need no LayerV credentials. Prefer short lifetimes such as
|
|
1841
2066
|
* `{ validFor: "5m" }`. Accepts a {@link ProtectedResource} handle or a
|
|
1842
|
-
* resource
|
|
2067
|
+
* public resource ID string. REST-shaped equivalents: {@link mintLink} /
|
|
1843
2068
|
* {@link createQurlForResource}.
|
|
1844
2069
|
*
|
|
1845
2070
|
* Duration options take a string (`"5m"`, `"24h"`; server-validated) or a
|
|
@@ -2772,8 +2997,8 @@ export class QURLClient {
|
|
|
2772
2997
|
*/
|
|
2773
2998
|
async rawRequest(method, path, body, rawOptions = {}) {
|
|
2774
2999
|
const url = `${this.baseUrl}${path}`;
|
|
2775
|
-
const { passthroughStatuses = NO_PASSTHROUGH_STATUSES, requestOptions, allowEmptySuccessBody = false, } = rawOptions;
|
|
2776
|
-
const idempotencyKey = idempotencyKeyForRequest(method, requestOptions);
|
|
3000
|
+
const { passthroughStatuses = NO_PASSTHROUGH_STATUSES, requestOptions, allowEmptySuccessBody = false, retry = true, generateIdempotencyKey = true, } = rawOptions;
|
|
3001
|
+
const idempotencyKey = idempotencyKeyForRequest(method, requestOptions, generateIdempotencyKey);
|
|
2777
3002
|
const headers = {
|
|
2778
3003
|
Authorization: `Bearer ${this.apiKey}`,
|
|
2779
3004
|
Accept: "application/json",
|
|
@@ -2792,13 +3017,15 @@ export class QURLClient {
|
|
|
2792
3017
|
// retry rule and avoids SDK pacing on an application request path. The
|
|
2793
3018
|
// caller receives retryAfter when supplied and must reconcile resource
|
|
2794
3019
|
// state before it chooses to issue another delete.
|
|
2795
|
-
const idempotencyKeyBackedRetry = MUTATING_RETRY_METHODS.has(method);
|
|
2796
|
-
const retryFetchFailure = method === "GET" || idempotencyKeyBackedRetry;
|
|
2797
|
-
const retryable =
|
|
2798
|
-
?
|
|
2799
|
-
:
|
|
2800
|
-
?
|
|
2801
|
-
:
|
|
3020
|
+
const idempotencyKeyBackedRetry = retry && MUTATING_RETRY_METHODS.has(method);
|
|
3021
|
+
const retryFetchFailure = retry && (method === "GET" || idempotencyKeyBackedRetry);
|
|
3022
|
+
const retryable = !retry
|
|
3023
|
+
? NO_RETRYABLE_STATUSES
|
|
3024
|
+
: method === "GET"
|
|
3025
|
+
? RETRYABLE_STATUS
|
|
3026
|
+
: idempotencyKeyBackedRetry
|
|
3027
|
+
? RETRYABLE_STATUS_MUTATING
|
|
3028
|
+
: NO_RETRYABLE_STATUSES;
|
|
2802
3029
|
const serializedBody = body !== undefined ? JSON.stringify(body) : undefined;
|
|
2803
3030
|
let lastError;
|
|
2804
3031
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
@@ -3215,8 +3442,7 @@ export class QURLClient {
|
|
|
3215
3442
|
/**
|
|
3216
3443
|
* A LayerV-protected resource, bound to the client that produced it.
|
|
3217
3444
|
*
|
|
3218
|
-
* Obtained from {@link QURLClient.protectUrl}
|
|
3219
|
-
* {@link QURLClient.connectorResource}, or {@link QURLClient.resourceById} —
|
|
3445
|
+
* Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.resourceById} —
|
|
3220
3446
|
* not constructed directly. Mint short-lived access links for the resource
|
|
3221
3447
|
* with {@link createPortal} (qurl-go: `qurl.Resource`).
|
|
3222
3448
|
*/
|
|
@@ -3225,7 +3451,7 @@ export class ProtectedResource {
|
|
|
3225
3451
|
// CONTRIBUTING.md's dual-build rule bars) so the binding stays on the
|
|
3226
3452
|
// instance and out of Object.keys/JSON.stringify output.
|
|
3227
3453
|
#client;
|
|
3228
|
-
/** The LayerV resource
|
|
3454
|
+
/** The LayerV public resource ID. */
|
|
3229
3455
|
id;
|
|
3230
3456
|
/** The private URL protected by this resource, when known. */
|
|
3231
3457
|
targetUrl;
|
|
@@ -3258,4 +3484,39 @@ export class ProtectedResource {
|
|
|
3258
3484
|
return this.#client.createPortal(this, opts, requestOptions);
|
|
3259
3485
|
}
|
|
3260
3486
|
}
|
|
3487
|
+
const CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN = Symbol("validated ConnectorResource");
|
|
3488
|
+
/**
|
|
3489
|
+
* A validated qURL Connector resource bound to the client that loaded it.
|
|
3490
|
+
*
|
|
3491
|
+
* Resource identity, reverse-routing identity, and NHP admission identity are
|
|
3492
|
+
* distinct server-issued values. Callers must consume each field verbatim and
|
|
3493
|
+
* must never derive one from another.
|
|
3494
|
+
*/
|
|
3495
|
+
export class ConnectorResource {
|
|
3496
|
+
#client;
|
|
3497
|
+
resourceId;
|
|
3498
|
+
/** Producer-supplied CRID carried verbatim; verify it against a trusted key before trust. */
|
|
3499
|
+
crid;
|
|
3500
|
+
connectorRoutingId;
|
|
3501
|
+
knockResourceId;
|
|
3502
|
+
slug;
|
|
3503
|
+
alias;
|
|
3504
|
+
/** @internal Instances are returned by QURLClient after wire validation. */
|
|
3505
|
+
constructor(client, details, validationToken) {
|
|
3506
|
+
if (validationToken !== CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN) {
|
|
3507
|
+
throw clientValidationError("ConnectorResource cannot be constructed directly; load it through QURLClient");
|
|
3508
|
+
}
|
|
3509
|
+
this.#client = client;
|
|
3510
|
+
this.resourceId = details.resource_id;
|
|
3511
|
+
this.crid = details.crid;
|
|
3512
|
+
this.connectorRoutingId = details.connector_routing_id;
|
|
3513
|
+
this.knockResourceId = details.knock_resource_id;
|
|
3514
|
+
this.slug = details.slug;
|
|
3515
|
+
this.alias = details.alias ?? undefined;
|
|
3516
|
+
}
|
|
3517
|
+
/** Mint a short-lived portal for this Connector resource. */
|
|
3518
|
+
async createPortal(opts = {}, requestOptions) {
|
|
3519
|
+
return this.#client.createPortal(this.resourceId, opts, requestOptions);
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3261
3522
|
//# sourceMappingURL=client.js.map
|