@layervai/qurl 0.4.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 +174 -61
- package/dist/cjs/client.d.ts +46 -20
- package/dist/cjs/client.js +338 -54
- 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/node/deployment.d.ts +1 -1
- package/dist/cjs/node/deployment.js +2 -2
- package/dist/cjs/node/native-udp.d.ts +2 -4
- package/dist/cjs/node/native-udp.js +8 -8
- package/dist/cjs/node/portal-opener.d.ts +54 -23
- package/dist/cjs/node/portal-opener.js +643 -340
- package/dist/cjs/node.d.ts +2 -2
- package/dist/cjs/node.js +8 -1
- package/dist/cjs/types.d.ts +18 -2
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/client.d.ts +46 -20
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +337 -54
- 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/node/deployment.d.ts +1 -1
- package/dist/esm/node/deployment.d.ts.map +1 -1
- package/dist/esm/node/deployment.js +2 -2
- package/dist/esm/node/deployment.js.map +1 -1
- package/dist/esm/node/native-udp.d.ts +2 -4
- package/dist/esm/node/native-udp.d.ts.map +1 -1
- package/dist/esm/node/native-udp.js +5 -5
- package/dist/esm/node/native-udp.js.map +1 -1
- package/dist/esm/node/portal-opener.d.ts +54 -23
- package/dist/esm/node/portal-opener.d.ts.map +1 -1
- package/dist/esm/node/portal-opener.js +636 -340
- package/dist/esm/node/portal-opener.js.map +1 -1
- package/dist/esm/node.d.ts +2 -2
- package/dist/esm/node.d.ts.map +1 -1
- package/dist/esm/node.js +1 -1
- package/dist/esm/node.js.map +1 -1
- package/dist/esm/types.d.ts +18 -2
- 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 +4 -4
package/dist/cjs/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ProtectedResource = exports.QURLClient = exports.SIGNED_FRAGMENT_RE = void 0;
|
|
3
|
+
exports.ConnectorResource = exports.ProtectedResource = exports.QURLClient = exports.SIGNED_FRAGMENT_RE = void 0;
|
|
4
4
|
const errors_js_1 = require("./errors.js");
|
|
5
5
|
const internal_js_1 = require("./internal.js");
|
|
6
6
|
const version_js_1 = require("./version.js");
|
|
@@ -196,6 +196,7 @@ const CREATE_PORTAL_OPTION_KEYS = [
|
|
|
196
196
|
"oneTimeUse",
|
|
197
197
|
"maxSessions",
|
|
198
198
|
"sessionDuration",
|
|
199
|
+
"targetPath",
|
|
199
200
|
];
|
|
200
201
|
assertExhaustive(true);
|
|
201
202
|
const UPDATE_RESOURCE_QURL_FIELD_KEYS = [
|
|
@@ -322,14 +323,14 @@ function validateRequestOptions(options) {
|
|
|
322
323
|
}
|
|
323
324
|
}
|
|
324
325
|
}
|
|
325
|
-
function idempotencyKeyForRequest(method, options) {
|
|
326
|
+
function idempotencyKeyForRequest(method, options, generateWhenMissing = true) {
|
|
326
327
|
// Validate before method gating so future read-only methods that accept
|
|
327
328
|
// RequestOptions fail loudly on malformed options instead of discarding them.
|
|
328
329
|
validateRequestOptions(options);
|
|
329
330
|
if (!IDEMPOTENCY_KEY_METHODS.has(method)) {
|
|
330
331
|
return undefined;
|
|
331
332
|
}
|
|
332
|
-
return options?.idempotencyKey ?? generateUuidV7();
|
|
333
|
+
return options?.idempotencyKey ?? (generateWhenMissing ? generateUuidV7() : undefined);
|
|
333
334
|
}
|
|
334
335
|
function generateUuidV7() {
|
|
335
336
|
const bytes = new Uint8Array(16);
|
|
@@ -723,6 +724,14 @@ const MAX_TARGET_PATH = 2048;
|
|
|
723
724
|
const MAX_TAGS = 10;
|
|
724
725
|
const MAX_TAG_LENGTH = 50;
|
|
725
726
|
const MAX_AUTO_PAGINATION_PAGES = 10_000;
|
|
727
|
+
// Keep these identity contracts aligned with qurl-go and the public API.
|
|
728
|
+
const CONNECTOR_SLUG_PATTERN = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
|
|
729
|
+
// Current service schemas intentionally share this grammar, but keep the
|
|
730
|
+
// mutable alias contract separate from the immutable lookup identity.
|
|
731
|
+
const CONNECTOR_ALIAS_PATTERN = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
|
|
732
|
+
const CONNECTOR_RESOURCE_ID_PATTERN = /^[A-Za-z0-9_-]{122}$/;
|
|
733
|
+
const CONNECTOR_RESOURCE_ID_DER_PREFIX = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE";
|
|
734
|
+
const CONNECTOR_ROUTING_ID_PATTERN = /^c-[a-z2-7]{51}[aq]$/;
|
|
726
735
|
// CreateQurlRequest.target_url pattern is loose (just a URI) but
|
|
727
736
|
// UpdateQurlRequest.tags pattern is specific — enforce it here.
|
|
728
737
|
const TAG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9 _-]*$/;
|
|
@@ -741,6 +750,17 @@ function requireMaxLength(value, field, max) {
|
|
|
741
750
|
throw clientValidationError(`${field}: must be ${max} characters or fewer (got ${value.length})`);
|
|
742
751
|
}
|
|
743
752
|
}
|
|
753
|
+
function requireMaxUtf8Bytes(value, field, max) {
|
|
754
|
+
if (value === undefined)
|
|
755
|
+
return;
|
|
756
|
+
if (typeof value !== "string") {
|
|
757
|
+
throw clientValidationError(`${field}: must be a string (got ${value === null ? "null" : typeof value})`);
|
|
758
|
+
}
|
|
759
|
+
const bytes = TEXT_ENCODER.encode(value).byteLength;
|
|
760
|
+
if (bytes > max) {
|
|
761
|
+
throw clientValidationError(`${field}: must be ${max} UTF-8 bytes or fewer (got ${bytes})`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
744
764
|
function requireNonEmptyIfPresent(value, field) {
|
|
745
765
|
// Reject `""` so uninitialized form state doesn't reach the wire.
|
|
746
766
|
if (value === "") {
|
|
@@ -833,6 +853,163 @@ function requireNonEmptyId(id, method, field = "id") {
|
|
|
833
853
|
throw clientValidationError(`${method}: ${field} must not include leading or trailing whitespace`);
|
|
834
854
|
}
|
|
835
855
|
}
|
|
856
|
+
function requireConnectorSlug(slug, method) {
|
|
857
|
+
if (typeof slug !== "string" || !CONNECTOR_SLUG_PATTERN.test(slug)) {
|
|
858
|
+
throw clientValidationError(`${method}: slug must be 3-64 lowercase alphanumeric or hyphen characters, start with a letter, and end alphanumeric`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function classifyConnectorMutationFailure(operation, error) {
|
|
862
|
+
if (!(error instanceof errors_js_1.QURLError)) {
|
|
863
|
+
throw new errors_js_1.ConnectorResourceOutcomeUnknownError(new errors_js_1.RuntimeError("Unexpected failure after Connector mutation dispatch", { cause: error }));
|
|
864
|
+
}
|
|
865
|
+
// Most authoritative 4xx responses prove that the mutation was rejected.
|
|
866
|
+
// bootstrap_key_consumed is the exception: the service returns it from the
|
|
867
|
+
// consume phase after the resource may have been bound or found.
|
|
868
|
+
// Callers perform argument/runtime preflight outside their mutation try
|
|
869
|
+
// blocks; every other surfaced failure therefore follows dispatch or a
|
|
870
|
+
// nominal success whose resource contract could not be consumed, so callers
|
|
871
|
+
// must reconcile before retrying. Ensure's exact 201 + valid resource but
|
|
872
|
+
// missing found_existing is handled after this classifier: the row proves
|
|
873
|
+
// the selected resource, while only its required metadata is unavailable.
|
|
874
|
+
if (operation === "ensure" && error.status === 409 && error.code === "bootstrap_key_consumed") {
|
|
875
|
+
throw new errors_js_1.ConnectorResourceOutcomeUnknownError(error);
|
|
876
|
+
}
|
|
877
|
+
if (error.status >= 400 && error.status < 500) {
|
|
878
|
+
throw error;
|
|
879
|
+
}
|
|
880
|
+
throw new errors_js_1.ConnectorResourceOutcomeUnknownError(error);
|
|
881
|
+
}
|
|
882
|
+
function decodeCanonicalBase64Url(value) {
|
|
883
|
+
if (!CONNECTOR_RESOURCE_ID_PATTERN.test(value) ||
|
|
884
|
+
!value.startsWith(CONNECTOR_RESOURCE_ID_DER_PREFIX)) {
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/") + "==";
|
|
889
|
+
const binary = atob(padded);
|
|
890
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
891
|
+
const canonical = btoa(String.fromCharCode(...bytes))
|
|
892
|
+
.replace(/\+/g, "-")
|
|
893
|
+
.replace(/\//g, "_")
|
|
894
|
+
.replace(/=+$/, "");
|
|
895
|
+
return canonical === value ? bytes : undefined;
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
return undefined;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
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
|
+
function requireConnectorSubtleCrypto(method) {
|
|
911
|
+
if (!globalThis.crypto?.subtle) {
|
|
912
|
+
throw new errors_js_1.RuntimeError(`${method}: requires the Web Crypto SubtleCrypto API`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
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
|
+
}
|
|
924
|
+
}
|
|
925
|
+
async function importsAsConnectorPublicKey(der) {
|
|
926
|
+
try {
|
|
927
|
+
const keyData = new ArrayBuffer(der.byteLength);
|
|
928
|
+
new Uint8Array(keyData).set(der);
|
|
929
|
+
await globalThis.crypto.subtle.importKey("spki", keyData, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function containsControlCharacter(value) {
|
|
937
|
+
return Array.from(value).some((character) => {
|
|
938
|
+
const codePoint = character.codePointAt(0);
|
|
939
|
+
return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
940
|
+
});
|
|
941
|
+
}
|
|
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`);
|
|
945
|
+
}
|
|
946
|
+
const resource = value;
|
|
947
|
+
if (typeof resource.resource_id !== "string") {
|
|
948
|
+
throw unexpectedResponseError(`${method}: response has missing or invalid resource_id`);
|
|
949
|
+
}
|
|
950
|
+
if (expectation.resourceId !== undefined && resource.resource_id !== expectation.resourceId) {
|
|
951
|
+
throw unexpectedResponseError(`${method}: response resource_id does not match the request`);
|
|
952
|
+
}
|
|
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`);
|
|
958
|
+
}
|
|
959
|
+
if (typeof resource.connector_routing_id !== "string" ||
|
|
960
|
+
!CONNECTOR_ROUTING_ID_PATTERN.test(resource.connector_routing_id)) {
|
|
961
|
+
throw unexpectedResponseError(`${method}: response has missing or invalid connector_routing_id`);
|
|
962
|
+
}
|
|
963
|
+
if (typeof resource.knock_resource_id !== "string" ||
|
|
964
|
+
resource.knock_resource_id.trim() === "" ||
|
|
965
|
+
resource.knock_resource_id.trim() !== resource.knock_resource_id ||
|
|
966
|
+
containsControlCharacter(resource.knock_resource_id)) {
|
|
967
|
+
throw unexpectedResponseError(`${method}: response has missing or invalid knock_resource_id`);
|
|
968
|
+
}
|
|
969
|
+
if (resource.resource_id === resource.knock_resource_id ||
|
|
970
|
+
resource.connector_routing_id === resource.knock_resource_id) {
|
|
971
|
+
throw unexpectedResponseError(`${method}: response cross-wires resource identity, routing, and admission values`);
|
|
972
|
+
}
|
|
973
|
+
if (resource.type !== "tunnel") {
|
|
974
|
+
throw unexpectedResponseError(`${method}: response resource type is not tunnel`);
|
|
975
|
+
}
|
|
976
|
+
if (typeof resource.slug !== "string" || !CONNECTOR_SLUG_PATTERN.test(resource.slug)) {
|
|
977
|
+
throw unexpectedResponseError(`${method}: response has missing or invalid slug`);
|
|
978
|
+
}
|
|
979
|
+
if (expectation.slug !== undefined && resource.slug !== expectation.slug) {
|
|
980
|
+
throw unexpectedResponseError(`${method}: response slug does not match the request`);
|
|
981
|
+
}
|
|
982
|
+
if (resource.alias !== undefined &&
|
|
983
|
+
resource.alias !== null &&
|
|
984
|
+
(typeof resource.alias !== "string" || !CONNECTOR_ALIAS_PATTERN.test(resource.alias))) {
|
|
985
|
+
// The public API intentionally gives mutable aliases and immutable slugs the
|
|
986
|
+
// same canonical wire grammar (domain aliasPattern/slugPattern). Keeping
|
|
987
|
+
// response validation aligned avoids accepting a row the service itself
|
|
988
|
+
// could not create or update.
|
|
989
|
+
throw unexpectedResponseError(`${method}: response has invalid alias`);
|
|
990
|
+
}
|
|
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
|
+
if (resource.status === "revoked") {
|
|
998
|
+
if (expectation.revokedIsLifecycleError !== true) {
|
|
999
|
+
throw unexpectedResponseError(`${method}: active-only operation returned a revoked connector resource`);
|
|
1000
|
+
}
|
|
1001
|
+
throw new errors_js_1.QURLError({
|
|
1002
|
+
status: 0,
|
|
1003
|
+
code: errors_js_1.ERROR_CODE_CONNECTOR_RESOURCE_REVOKED,
|
|
1004
|
+
title: "Connector Resource Revoked",
|
|
1005
|
+
detail: `${method}: qURL Connector resource is revoked`,
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
if (resource.status !== "active") {
|
|
1009
|
+
throw unexpectedResponseError(`${method}: response has invalid resource status`);
|
|
1010
|
+
}
|
|
1011
|
+
return new ConnectorResource(client, resource, CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN);
|
|
1012
|
+
}
|
|
836
1013
|
function requireValidTags(tags) {
|
|
837
1014
|
// null tolerance for untyped-JS callers — treated as "don't touch."
|
|
838
1015
|
// Duplicates pass through to the server (authoritative on dedupe policy).
|
|
@@ -1038,7 +1215,7 @@ function validateQurlTokenOptions(input) {
|
|
|
1038
1215
|
// gate server-authoritative (`invalid_target_path`); a client-side regex
|
|
1039
1216
|
// would create a drift surface. Keep max-before-empty consistent with label;
|
|
1040
1217
|
// empty strings pass the length check and are rejected by the next guard.
|
|
1041
|
-
|
|
1218
|
+
requireMaxUtf8Bytes(input.target_path, "target_path", MAX_TARGET_PATH);
|
|
1042
1219
|
requireNonEmptyIfPresent(input.target_path, "target_path");
|
|
1043
1220
|
requireBooleanIfPresent(input.one_time_use, "one_time_use");
|
|
1044
1221
|
requireMaxSessionsInRange(input.max_sessions);
|
|
@@ -1415,16 +1592,15 @@ function parsePortal(data, method) {
|
|
|
1415
1592
|
};
|
|
1416
1593
|
}
|
|
1417
1594
|
/**
|
|
1418
|
-
* Validate portal options and build the create-portal wire body
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
* createQurlForResource).
|
|
1595
|
+
* Validate portal options and build the create-portal wire body. The service
|
|
1596
|
+
* requires a JSON object even when every option is omitted; `{}` selects the
|
|
1597
|
+
* owner's plan defaults and matches qurl-go's zero-value request struct.
|
|
1422
1598
|
*/
|
|
1423
1599
|
function buildCreatePortalBody(opts, method) {
|
|
1424
1600
|
requireObjectInput(opts, method);
|
|
1425
1601
|
requireNoUnknownFields(opts, CREATE_PORTAL_OPTION_KEYS, method);
|
|
1426
1602
|
// null → omitted, via the SDK-wide normalizePatchFields convention.
|
|
1427
|
-
const { validFor, label, oneTimeUse, maxSessions, sessionDuration } = normalizePatchFields(opts, CREATE_PORTAL_OPTION_KEYS);
|
|
1603
|
+
const { validFor, label, oneTimeUse, maxSessions, sessionDuration, targetPath } = normalizePatchFields(opts, CREATE_PORTAL_OPTION_KEYS);
|
|
1428
1604
|
if (label !== undefined) {
|
|
1429
1605
|
requireMaxLength(label, "label", MAX_LABEL);
|
|
1430
1606
|
// qurl-go's WithLabel rejects blank labels; the REST-shaped mintLink
|
|
@@ -1435,6 +1611,11 @@ function buildCreatePortalBody(opts, method) {
|
|
|
1435
1611
|
}
|
|
1436
1612
|
requireBooleanIfPresent(oneTimeUse, "oneTimeUse");
|
|
1437
1613
|
requireMaxSessionsInRange(maxSessions, "maxSessions");
|
|
1614
|
+
requireMaxUtf8Bytes(targetPath, "targetPath", MAX_TARGET_PATH);
|
|
1615
|
+
requireNonEmptyIfPresent(targetPath, "targetPath");
|
|
1616
|
+
if (targetPath !== undefined && method === "createPortalForUrl") {
|
|
1617
|
+
throw clientValidationError("targetPath: requires an existing resource");
|
|
1618
|
+
}
|
|
1438
1619
|
const body = {};
|
|
1439
1620
|
const expiresIn = serializeApiDuration(validFor, "validFor", MIN_PORTAL_VALID_FOR_SECONDS);
|
|
1440
1621
|
if (expiresIn !== undefined)
|
|
@@ -1449,7 +1630,9 @@ function buildCreatePortalBody(opts, method) {
|
|
|
1449
1630
|
const wireSessionDuration = serializeApiDuration(sessionDuration, "sessionDuration", MIN_SESSION_DURATION_SECONDS);
|
|
1450
1631
|
if (wireSessionDuration !== undefined)
|
|
1451
1632
|
body.session_duration = wireSessionDuration;
|
|
1452
|
-
|
|
1633
|
+
if (targetPath !== undefined)
|
|
1634
|
+
body.target_path = targetPath;
|
|
1635
|
+
return body;
|
|
1453
1636
|
}
|
|
1454
1637
|
/** qURL API client. */
|
|
1455
1638
|
class QURLClient {
|
|
@@ -1772,49 +1955,110 @@ class QURLClient {
|
|
|
1772
1955
|
requireNonEmptyId(id, "resourceById");
|
|
1773
1956
|
return new ProtectedResource(this, id);
|
|
1774
1957
|
}
|
|
1775
|
-
/**
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1958
|
+
/** Find or create the active qURL Connector resource for an immutable slug. */
|
|
1959
|
+
async ensureConnectorResource(slug, requestOptions) {
|
|
1960
|
+
requireConnectorSlug(slug, "ensureConnectorResource");
|
|
1961
|
+
// Response validation imports the producer's P-256 resource key. Preflight
|
|
1962
|
+
// before POST so a missing runtime capability cannot orphan a committed row.
|
|
1963
|
+
requireConnectorSubtleCrypto("ensureConnectorResource");
|
|
1964
|
+
// Keep invalid options in the known pre-dispatch error arm. rawRequest
|
|
1965
|
+
// validates again when building the request, which protects future callers
|
|
1966
|
+
// that do not have this mutation-specific outcome contract.
|
|
1967
|
+
validateRequestOptions(requestOptions);
|
|
1968
|
+
let response;
|
|
1969
|
+
let resource;
|
|
1970
|
+
try {
|
|
1971
|
+
response = await this.rawRequest("POST", "/v1/resources", {
|
|
1972
|
+
type: "tunnel",
|
|
1973
|
+
slug,
|
|
1974
|
+
find_or_create: true,
|
|
1975
|
+
}, { requestOptions, retry: false, generateIdempotencyKey: false });
|
|
1976
|
+
// The find-or-create endpoint returns 201 for both newly-created
|
|
1977
|
+
// and found-existing rows; meta.found_existing distinguishes the arms.
|
|
1978
|
+
if (response.__http_status !== 201) {
|
|
1979
|
+
throw unexpectedResponseError(`ensureConnectorResource: expected HTTP 201, got ${response.__http_status ?? "unknown"}`);
|
|
1980
|
+
}
|
|
1981
|
+
resource = await parseConnectorResource(this, response.data, "ensureConnectorResource", {
|
|
1982
|
+
slug,
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
catch (error) {
|
|
1986
|
+
classifyConnectorMutationFailure("ensure", error);
|
|
1987
|
+
}
|
|
1988
|
+
if (response.meta !== undefined &&
|
|
1989
|
+
response.meta !== null &&
|
|
1990
|
+
(typeof response.meta !== "object" || Array.isArray(response.meta))) {
|
|
1991
|
+
classifyConnectorMutationFailure("ensure", unexpectedResponseError("ensureConnectorResource: response has invalid meta"));
|
|
1992
|
+
}
|
|
1993
|
+
const foundExisting = response.meta
|
|
1994
|
+
?.found_existing;
|
|
1995
|
+
if (foundExisting === undefined || foundExisting === null) {
|
|
1996
|
+
throw unexpectedResponseError("ensureConnectorResource: response is missing meta.found_existing; the validated resource exists, so reconcile by slug before retrying");
|
|
1997
|
+
}
|
|
1998
|
+
if (typeof foundExisting !== "boolean") {
|
|
1999
|
+
classifyConnectorMutationFailure("ensure", unexpectedResponseError("ensureConnectorResource: response has invalid meta.found_existing"));
|
|
2000
|
+
}
|
|
2001
|
+
return { resource, foundExisting };
|
|
2002
|
+
}
|
|
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 });
|
|
2007
|
+
if (__http_status !== 200) {
|
|
2008
|
+
throw unexpectedResponseError(`getConnectorResource: expected HTTP 200, got ${__http_status ?? "unknown"}`);
|
|
2009
|
+
}
|
|
2010
|
+
return parseConnectorResource(this, data?.resource, "getConnectorResource", {
|
|
2011
|
+
resourceId,
|
|
2012
|
+
revokedIsLifecycleError: true,
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
/** Fetch the single active qURL Connector resource for an immutable slug. */
|
|
2016
|
+
async getConnectorResourceBySlug(slug) {
|
|
2017
|
+
requireConnectorSlug(slug, "getConnectorResourceBySlug");
|
|
2018
|
+
// Parsing validates the returned P-256 resource key; avoid dispatching a
|
|
2019
|
+
// read that this runtime cannot safely consume.
|
|
2020
|
+
requireConnectorSubtleCrypto("getConnectorResourceBySlug");
|
|
2021
|
+
// The slug point lookup is intrinsically active-only. It also
|
|
2022
|
+
// rejects combining `slug` with `status`, so this query must stay slug-only.
|
|
2023
|
+
const { data, __http_status } = await this.rawRequest("GET", `/v1/resources?slug=${encodeURIComponent(slug)}`, undefined, { retry: false });
|
|
2024
|
+
if (__http_status !== 200) {
|
|
2025
|
+
throw unexpectedResponseError(`getConnectorResourceBySlug: expected HTTP 200, got ${__http_status ?? "unknown"}`);
|
|
2026
|
+
}
|
|
2027
|
+
if (!Array.isArray(data)) {
|
|
2028
|
+
throw unexpectedResponseError("getConnectorResourceBySlug: response has missing or invalid data");
|
|
2029
|
+
}
|
|
2030
|
+
if (data.length === 0) {
|
|
1795
2031
|
throw new errors_js_1.NotFoundError({
|
|
1796
2032
|
status: 0,
|
|
1797
2033
|
code: errors_js_1.ERROR_CODE_RESOURCE_NOT_FOUND,
|
|
1798
2034
|
title: "Resource Not Found",
|
|
1799
|
-
detail:
|
|
2035
|
+
detail: "getConnectorResourceBySlug: no active resource exists for the requested slug",
|
|
1800
2036
|
});
|
|
1801
2037
|
}
|
|
1802
|
-
if (
|
|
2038
|
+
if (data.length > 1) {
|
|
1803
2039
|
throw new errors_js_1.QURLError({
|
|
1804
2040
|
status: 0,
|
|
1805
2041
|
code: errors_js_1.ERROR_CODE_AMBIGUOUS_RESOURCE,
|
|
1806
2042
|
title: "Ambiguous Resource",
|
|
1807
|
-
detail: `
|
|
2043
|
+
detail: `getConnectorResourceBySlug: expected one active resource, got ${data.length}`,
|
|
1808
2044
|
});
|
|
1809
2045
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2046
|
+
return parseConnectorResource(this, data[0], "getConnectorResourceBySlug", { slug });
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Revoke a qURL Connector resource by immutable public resource ID.
|
|
2050
|
+
*
|
|
2051
|
+
* The API does not apply Idempotency-Key replay to DELETE. After an
|
|
2052
|
+
* outcome-unknown failure, reconcile by ID before issuing a deliberate retry.
|
|
2053
|
+
*/
|
|
2054
|
+
async deleteConnectorResource(resourceId) {
|
|
2055
|
+
await requireConnectorResourceId(resourceId, "deleteConnectorResource");
|
|
2056
|
+
try {
|
|
2057
|
+
await this.requestNoContent(`/v1/resources/${encodeURIComponent(resourceId)}`);
|
|
1813
2058
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
2059
|
+
catch (error) {
|
|
2060
|
+
classifyConnectorMutationFailure("delete", error);
|
|
1816
2061
|
}
|
|
1817
|
-
return new ProtectedResource(this, resource.resource_id, resource.target_url, resource);
|
|
1818
2062
|
}
|
|
1819
2063
|
/**
|
|
1820
2064
|
* Mint a short-lived qURL link (a portal) for a protected resource.
|
|
@@ -1823,14 +2067,15 @@ class QURLClient {
|
|
|
1823
2067
|
* reach one private resource. Recipients open `portal.link` directly and
|
|
1824
2068
|
* need no LayerV credentials. Prefer short lifetimes such as
|
|
1825
2069
|
* `{ validFor: "5m" }`. Accepts a {@link ProtectedResource} handle or a
|
|
1826
|
-
* resource
|
|
2070
|
+
* public resource ID string. REST-shaped equivalents: {@link mintLink} /
|
|
1827
2071
|
* {@link createQurlForResource}.
|
|
1828
2072
|
*
|
|
1829
2073
|
* Duration options take a string (`"5m"`, `"24h"`; server-validated) or a
|
|
1830
2074
|
* number of milliseconds with qurl-go's client-side guardrails: whole
|
|
1831
2075
|
* seconds only, at least one minute for `validFor` and one second for
|
|
1832
2076
|
* `sessionDuration`. `maxSessions: 0` is sent explicitly and means
|
|
1833
|
-
* unlimited; blank labels are rejected.
|
|
2077
|
+
* unlimited; blank labels are rejected. `targetPath` is valid only for an
|
|
2078
|
+
* existing resource. The API owns its full grammar and tunnel-only gate.
|
|
1834
2079
|
*/
|
|
1835
2080
|
async createPortal(resource, opts = {}, requestOptions) {
|
|
1836
2081
|
let resourceId;
|
|
@@ -1860,7 +2105,9 @@ class QURLClient {
|
|
|
1860
2105
|
* {@link protectUrl} when you need the full server-populated resource
|
|
1861
2106
|
* metadata. Portal options match {@link createPortal}; `targetUrl` gets
|
|
1862
2107
|
* the same malformed-URL and embedded-credentials rejection as
|
|
1863
|
-
* {@link protectUrl}.
|
|
2108
|
+
* {@link protectUrl}. `targetPath` is rejected because this call creates a
|
|
2109
|
+
* URL resource instead of addressing an existing resource. REST-shaped
|
|
2110
|
+
* equivalent: {@link create}.
|
|
1864
2111
|
*/
|
|
1865
2112
|
async createPortalForUrl(targetUrl, opts = {}, requestOptions) {
|
|
1866
2113
|
requirePortalTargetUrl(targetUrl);
|
|
@@ -2753,8 +3000,8 @@ class QURLClient {
|
|
|
2753
3000
|
*/
|
|
2754
3001
|
async rawRequest(method, path, body, rawOptions = {}) {
|
|
2755
3002
|
const url = `${this.baseUrl}${path}`;
|
|
2756
|
-
const { passthroughStatuses = NO_PASSTHROUGH_STATUSES, requestOptions, allowEmptySuccessBody = false, } = rawOptions;
|
|
2757
|
-
const idempotencyKey = idempotencyKeyForRequest(method, requestOptions);
|
|
3003
|
+
const { passthroughStatuses = NO_PASSTHROUGH_STATUSES, requestOptions, allowEmptySuccessBody = false, retry = true, generateIdempotencyKey = true, } = rawOptions;
|
|
3004
|
+
const idempotencyKey = idempotencyKeyForRequest(method, requestOptions, generateIdempotencyKey);
|
|
2758
3005
|
const headers = {
|
|
2759
3006
|
Authorization: `Bearer ${this.apiKey}`,
|
|
2760
3007
|
Accept: "application/json",
|
|
@@ -2773,13 +3020,15 @@ class QURLClient {
|
|
|
2773
3020
|
// retry rule and avoids SDK pacing on an application request path. The
|
|
2774
3021
|
// caller receives retryAfter when supplied and must reconcile resource
|
|
2775
3022
|
// state before it chooses to issue another delete.
|
|
2776
|
-
const idempotencyKeyBackedRetry = MUTATING_RETRY_METHODS.has(method);
|
|
2777
|
-
const retryFetchFailure = method === "GET" || idempotencyKeyBackedRetry;
|
|
2778
|
-
const retryable =
|
|
2779
|
-
?
|
|
2780
|
-
:
|
|
2781
|
-
?
|
|
2782
|
-
:
|
|
3023
|
+
const idempotencyKeyBackedRetry = retry && MUTATING_RETRY_METHODS.has(method);
|
|
3024
|
+
const retryFetchFailure = retry && (method === "GET" || idempotencyKeyBackedRetry);
|
|
3025
|
+
const retryable = !retry
|
|
3026
|
+
? NO_RETRYABLE_STATUSES
|
|
3027
|
+
: method === "GET"
|
|
3028
|
+
? RETRYABLE_STATUS
|
|
3029
|
+
: idempotencyKeyBackedRetry
|
|
3030
|
+
? RETRYABLE_STATUS_MUTATING
|
|
3031
|
+
: NO_RETRYABLE_STATUSES;
|
|
2783
3032
|
const serializedBody = body !== undefined ? JSON.stringify(body) : undefined;
|
|
2784
3033
|
let lastError;
|
|
2785
3034
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
@@ -3197,8 +3446,7 @@ exports.QURLClient = QURLClient;
|
|
|
3197
3446
|
/**
|
|
3198
3447
|
* A LayerV-protected resource, bound to the client that produced it.
|
|
3199
3448
|
*
|
|
3200
|
-
* Obtained from {@link QURLClient.protectUrl}
|
|
3201
|
-
* {@link QURLClient.connectorResource}, or {@link QURLClient.resourceById} —
|
|
3449
|
+
* Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.resourceById} —
|
|
3202
3450
|
* not constructed directly. Mint short-lived access links for the resource
|
|
3203
3451
|
* with {@link createPortal} (qurl-go: `qurl.Resource`).
|
|
3204
3452
|
*/
|
|
@@ -3207,7 +3455,7 @@ class ProtectedResource {
|
|
|
3207
3455
|
// CONTRIBUTING.md's dual-build rule bars) so the binding stays on the
|
|
3208
3456
|
// instance and out of Object.keys/JSON.stringify output.
|
|
3209
3457
|
#client;
|
|
3210
|
-
/** The LayerV resource
|
|
3458
|
+
/** The LayerV public resource ID. */
|
|
3211
3459
|
id;
|
|
3212
3460
|
/** The private URL protected by this resource, when known. */
|
|
3213
3461
|
targetUrl;
|
|
@@ -3241,3 +3489,39 @@ class ProtectedResource {
|
|
|
3241
3489
|
}
|
|
3242
3490
|
}
|
|
3243
3491
|
exports.ProtectedResource = ProtectedResource;
|
|
3492
|
+
const CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN = Symbol("validated ConnectorResource");
|
|
3493
|
+
/**
|
|
3494
|
+
* A validated qURL Connector resource bound to the client that loaded it.
|
|
3495
|
+
*
|
|
3496
|
+
* Resource identity, reverse-routing identity, and NHP admission identity are
|
|
3497
|
+
* distinct server-issued values. Callers must consume each field verbatim and
|
|
3498
|
+
* must never derive one from another.
|
|
3499
|
+
*/
|
|
3500
|
+
class ConnectorResource {
|
|
3501
|
+
#client;
|
|
3502
|
+
resourceId;
|
|
3503
|
+
/** Producer-supplied CRID carried verbatim; verify it against a trusted key before trust. */
|
|
3504
|
+
crid;
|
|
3505
|
+
connectorRoutingId;
|
|
3506
|
+
knockResourceId;
|
|
3507
|
+
slug;
|
|
3508
|
+
alias;
|
|
3509
|
+
/** @internal Instances are returned by QURLClient after wire validation. */
|
|
3510
|
+
constructor(client, details, validationToken) {
|
|
3511
|
+
if (validationToken !== CONNECTOR_RESOURCE_CONSTRUCTOR_TOKEN) {
|
|
3512
|
+
throw clientValidationError("ConnectorResource cannot be constructed directly; load it through QURLClient");
|
|
3513
|
+
}
|
|
3514
|
+
this.#client = client;
|
|
3515
|
+
this.resourceId = details.resource_id;
|
|
3516
|
+
this.crid = details.crid;
|
|
3517
|
+
this.connectorRoutingId = details.connector_routing_id;
|
|
3518
|
+
this.knockResourceId = details.knock_resource_id;
|
|
3519
|
+
this.slug = details.slug;
|
|
3520
|
+
this.alias = details.alias ?? undefined;
|
|
3521
|
+
}
|
|
3522
|
+
/** Mint a short-lived portal for this Connector resource. */
|
|
3523
|
+
async createPortal(opts = {}, requestOptions) {
|
|
3524
|
+
return this.#client.createPortal(this.resourceId, opts, requestOptions);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
exports.ConnectorResource = ConnectorResource;
|
package/dist/cjs/errors.d.ts
CHANGED
|
@@ -14,9 +14,13 @@ export declare const ERROR_CODE_UNEXPECTED_RESPONSE = "unexpected_response";
|
|
|
14
14
|
export declare const ERROR_CODE_NETWORK = "network_error";
|
|
15
15
|
export declare const ERROR_CODE_TIMEOUT = "timeout";
|
|
16
16
|
export declare const ERROR_CODE_RUNTIME = "runtime_error";
|
|
17
|
-
/**
|
|
17
|
+
/** A dispatched Connector resource mutation could have committed and must be reconciled. */
|
|
18
|
+
export declare const ERROR_CODE_CONNECTOR_RESOURCE_OUTCOME_UNKNOWN = "connector_resource_outcome_unknown";
|
|
19
|
+
/** A by-ID Connector resource lookup found a revoked lifecycle row. */
|
|
20
|
+
export declare const ERROR_CODE_CONNECTOR_RESOURCE_REVOKED = "connector_resource_revoked";
|
|
21
|
+
/** A Connector slug lookup found no active resource (client-detected, `status: 0`). */
|
|
18
22
|
export declare const ERROR_CODE_RESOURCE_NOT_FOUND = "resource_not_found";
|
|
19
|
-
/**
|
|
23
|
+
/** A Connector slug lookup matched more than one active resource (client-detected, `status: 0`). */
|
|
20
24
|
export declare const ERROR_CODE_AMBIGUOUS_RESOURCE = "ambiguous_resource";
|
|
21
25
|
/** Fallback `.code` when the server returns a non-RFC-7807 response (HTML proxy page, plaintext gateway error, JSON without `error` envelope). */
|
|
22
26
|
export declare const ERROR_CODE_UNKNOWN = "unknown";
|
|
@@ -78,7 +82,10 @@ export declare class NotFoundError extends QURLError {
|
|
|
78
82
|
* returning HTML on a passthrough status, or a batch response missing
|
|
79
83
|
* required fields).
|
|
80
84
|
*
|
|
81
|
-
* `instanceof ValidationError` catches both
|
|
85
|
+
* `instanceof ValidationError` catches both on ordinary client/response
|
|
86
|
+
* paths. A Connector mutation wraps a post-dispatch `unexpected_response`
|
|
87
|
+
* in {@link ConnectorResourceOutcomeUnknownError}; inspect that error's
|
|
88
|
+
* typed `cause` before deciding whether to retry. To distinguish them, check
|
|
82
89
|
* `.code` rather than using `instanceof` alone.
|
|
83
90
|
*
|
|
84
91
|
* **`.status` asymmetry within `code: "unexpected_response"`:**
|
|
@@ -107,6 +114,20 @@ export declare class RateLimitError extends QURLError {
|
|
|
107
114
|
export declare class ServerError extends QURLError {
|
|
108
115
|
constructor(data: QURLErrorData);
|
|
109
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* A Connector resource ensure/delete was dispatched, but the response cannot
|
|
119
|
+
* prove whether it committed. Reconcile by immutable slug or resource ID
|
|
120
|
+
* before choosing whether to retry. The original typed failure is in `cause`.
|
|
121
|
+
* The wrapper uses `status: 0` so generic HTTP-status retry predicates cannot
|
|
122
|
+
* replay the mutation; inspect `cause.status` for the observed HTTP status.
|
|
123
|
+
* A valid HTTP 201 Connector row missing only `meta.found_existing` is a known
|
|
124
|
+
* committed/selected row with incomplete metadata and is not wrapped here.
|
|
125
|
+
*/
|
|
126
|
+
export declare class ConnectorResourceOutcomeUnknownError extends QURLError {
|
|
127
|
+
readonly code: typeof ERROR_CODE_CONNECTOR_RESOURCE_OUTCOME_UNKNOWN;
|
|
128
|
+
readonly cause: QURLError;
|
|
129
|
+
constructor(cause: QURLError);
|
|
130
|
+
}
|
|
110
131
|
/** Transport-level error — DNS failure, connection refused, etc. */
|
|
111
132
|
export declare class NetworkError extends QURLError {
|
|
112
133
|
constructor(message: string, options?: {
|
package/dist/cjs/errors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RuntimeError = exports.TimeoutError = exports.NetworkError = exports.ServerError = exports.RateLimitError = exports.ValidationError = exports.NotFoundError = exports.AuthorizationError = exports.AuthenticationError = exports.QURLError = exports.ERROR_CODE_UNKNOWN = exports.ERROR_CODE_AMBIGUOUS_RESOURCE = exports.ERROR_CODE_RESOURCE_NOT_FOUND = exports.ERROR_CODE_RUNTIME = exports.ERROR_CODE_TIMEOUT = exports.ERROR_CODE_NETWORK = exports.ERROR_CODE_UNEXPECTED_RESPONSE = exports.ERROR_CODE_CLIENT_VALIDATION = void 0;
|
|
3
|
+
exports.RuntimeError = exports.TimeoutError = exports.NetworkError = exports.ConnectorResourceOutcomeUnknownError = exports.ServerError = exports.RateLimitError = exports.ValidationError = exports.NotFoundError = exports.AuthorizationError = exports.AuthenticationError = exports.QURLError = exports.ERROR_CODE_UNKNOWN = exports.ERROR_CODE_AMBIGUOUS_RESOURCE = exports.ERROR_CODE_RESOURCE_NOT_FOUND = exports.ERROR_CODE_CONNECTOR_RESOURCE_REVOKED = exports.ERROR_CODE_CONNECTOR_RESOURCE_OUTCOME_UNKNOWN = exports.ERROR_CODE_RUNTIME = exports.ERROR_CODE_TIMEOUT = exports.ERROR_CODE_NETWORK = exports.ERROR_CODE_UNEXPECTED_RESPONSE = exports.ERROR_CODE_CLIENT_VALIDATION = void 0;
|
|
4
4
|
exports.createError = createError;
|
|
5
5
|
/**
|
|
6
6
|
* Sentinel `.code` values for SDK-internal failure modes (no real HTTP
|
|
@@ -17,9 +17,13 @@ exports.ERROR_CODE_UNEXPECTED_RESPONSE = "unexpected_response";
|
|
|
17
17
|
exports.ERROR_CODE_NETWORK = "network_error";
|
|
18
18
|
exports.ERROR_CODE_TIMEOUT = "timeout";
|
|
19
19
|
exports.ERROR_CODE_RUNTIME = "runtime_error";
|
|
20
|
-
/**
|
|
20
|
+
/** A dispatched Connector resource mutation could have committed and must be reconciled. */
|
|
21
|
+
exports.ERROR_CODE_CONNECTOR_RESOURCE_OUTCOME_UNKNOWN = "connector_resource_outcome_unknown";
|
|
22
|
+
/** A by-ID Connector resource lookup found a revoked lifecycle row. */
|
|
23
|
+
exports.ERROR_CODE_CONNECTOR_RESOURCE_REVOKED = "connector_resource_revoked";
|
|
24
|
+
/** A Connector slug lookup found no active resource (client-detected, `status: 0`). */
|
|
21
25
|
exports.ERROR_CODE_RESOURCE_NOT_FOUND = "resource_not_found";
|
|
22
|
-
/**
|
|
26
|
+
/** A Connector slug lookup matched more than one active resource (client-detected, `status: 0`). */
|
|
23
27
|
exports.ERROR_CODE_AMBIGUOUS_RESOURCE = "ambiguous_resource";
|
|
24
28
|
/** Fallback `.code` when the server returns a non-RFC-7807 response (HTML proxy page, plaintext gateway error, JSON without `error` envelope). */
|
|
25
29
|
exports.ERROR_CODE_UNKNOWN = "unknown";
|
|
@@ -109,7 +113,10 @@ exports.NotFoundError = NotFoundError;
|
|
|
109
113
|
* returning HTML on a passthrough status, or a batch response missing
|
|
110
114
|
* required fields).
|
|
111
115
|
*
|
|
112
|
-
* `instanceof ValidationError` catches both
|
|
116
|
+
* `instanceof ValidationError` catches both on ordinary client/response
|
|
117
|
+
* paths. A Connector mutation wraps a post-dispatch `unexpected_response`
|
|
118
|
+
* in {@link ConnectorResourceOutcomeUnknownError}; inspect that error's
|
|
119
|
+
* typed `cause` before deciding whether to retry. To distinguish them, check
|
|
113
120
|
* `.code` rather than using `instanceof` alone.
|
|
114
121
|
*
|
|
115
122
|
* **`.status` asymmetry within `code: "unexpected_response"`:**
|
|
@@ -161,6 +168,29 @@ function attachErrorCause(error, options) {
|
|
|
161
168
|
configurable: true,
|
|
162
169
|
});
|
|
163
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* A Connector resource ensure/delete was dispatched, but the response cannot
|
|
173
|
+
* prove whether it committed. Reconcile by immutable slug or resource ID
|
|
174
|
+
* before choosing whether to retry. The original typed failure is in `cause`.
|
|
175
|
+
* The wrapper uses `status: 0` so generic HTTP-status retry predicates cannot
|
|
176
|
+
* replay the mutation; inspect `cause.status` for the observed HTTP status.
|
|
177
|
+
* A valid HTTP 201 Connector row missing only `meta.found_existing` is a known
|
|
178
|
+
* committed/selected row with incomplete metadata and is not wrapped here.
|
|
179
|
+
*/
|
|
180
|
+
class ConnectorResourceOutcomeUnknownError extends QURLError {
|
|
181
|
+
constructor(cause) {
|
|
182
|
+
super({
|
|
183
|
+
status: 0,
|
|
184
|
+
code: exports.ERROR_CODE_CONNECTOR_RESOURCE_OUTCOME_UNKNOWN,
|
|
185
|
+
title: "Connector Resource Outcome Unknown",
|
|
186
|
+
detail: `Mutation outcome is unknown; reconcile before retrying. ${cause.detail}`,
|
|
187
|
+
request_id: cause.requestId,
|
|
188
|
+
});
|
|
189
|
+
this.name = "ConnectorResourceOutcomeUnknownError";
|
|
190
|
+
attachErrorCause(this, { cause });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
exports.ConnectorResourceOutcomeUnknownError = ConnectorResourceOutcomeUnknownError;
|
|
164
194
|
/** Transport-level error — DNS failure, connection refused, etc. */
|
|
165
195
|
class NetworkError extends QURLError {
|
|
166
196
|
constructor(message, options) {
|