@layervai/qurl 3.1.0 → 4.0.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 +259 -46
- package/dist/cjs/client-connectors.d.ts +44 -0
- package/dist/cjs/client-connectors.js +189 -0
- package/dist/cjs/client-delegated.d.ts +30 -0
- package/dist/cjs/client-delegated.js +312 -0
- package/dist/cjs/client-errors.d.ts +44 -0
- package/dist/cjs/client-errors.js +219 -0
- package/dist/cjs/client-http-types.d.ts +37 -0
- package/dist/cjs/client-http-types.js +2 -0
- package/dist/cjs/client-input-fields.d.ts +53 -0
- package/dist/cjs/client-input-fields.js +262 -0
- package/dist/cjs/client-limits.d.ts +36 -0
- package/dist/cjs/client-limits.js +61 -0
- package/dist/cjs/client-management.d.ts +12 -0
- package/dist/cjs/client-management.js +119 -0
- package/dist/cjs/client-paths.d.ts +21 -0
- package/dist/cjs/client-paths.js +143 -0
- package/dist/cjs/client-portals.d.ts +82 -0
- package/dist/cjs/client-portals.js +244 -0
- package/dist/cjs/client-resources.d.ts +29 -0
- package/dist/cjs/client-resources.js +331 -0
- package/dist/cjs/client-validation.d.ts +35 -0
- package/dist/cjs/client-validation.js +326 -0
- package/dist/cjs/client.d.ts +45 -75
- package/dist/cjs/client.js +621 -2448
- package/dist/cjs/errors.d.ts +9 -1
- package/dist/cjs/errors.js +10 -1
- package/dist/cjs/index.d.ts +4 -2
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/response-metadata.d.ts +13 -0
- package/dist/cjs/response-metadata.js +38 -0
- package/dist/cjs/share.js +2 -0
- package/dist/cjs/target-path.d.ts +2 -0
- package/dist/cjs/target-path.js +34 -0
- package/dist/cjs/types.d.ts +83 -7
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/client-connectors.d.ts +44 -0
- package/dist/esm/client-connectors.js +180 -0
- package/dist/esm/client-delegated.d.ts +30 -0
- package/dist/esm/client-delegated.js +295 -0
- package/dist/esm/client-errors.d.ts +44 -0
- package/dist/esm/client-errors.js +206 -0
- package/dist/esm/client-http-types.d.ts +37 -0
- package/dist/esm/client-http-types.js +1 -0
- package/dist/esm/client-input-fields.d.ts +53 -0
- package/dist/esm/client-input-fields.js +258 -0
- package/dist/esm/client-limits.d.ts +36 -0
- package/dist/esm/client-limits.js +58 -0
- package/dist/esm/client-management.d.ts +12 -0
- package/dist/esm/client-management.js +113 -0
- package/dist/esm/client-paths.d.ts +21 -0
- package/dist/esm/client-paths.js +138 -0
- package/dist/esm/client-portals.d.ts +82 -0
- package/dist/esm/client-portals.js +236 -0
- package/dist/esm/client-resources.d.ts +29 -0
- package/dist/esm/client-resources.js +323 -0
- package/dist/esm/client-validation.d.ts +35 -0
- package/dist/esm/client-validation.js +302 -0
- package/dist/esm/client.d.ts +45 -76
- package/dist/esm/client.js +318 -2147
- package/dist/esm/crid.d.ts +0 -1
- package/dist/esm/crid.js +0 -1
- package/dist/esm/errors.d.ts +9 -2
- package/dist/esm/errors.js +9 -1
- package/dist/esm/index.d.ts +4 -3
- package/dist/esm/index.js +2 -2
- package/dist/esm/internal.d.ts +0 -1
- package/dist/esm/internal.js +0 -1
- package/dist/esm/node/agent-runtime.d.ts +0 -1
- package/dist/esm/node/agent-runtime.js +0 -1
- package/dist/esm/node/agent-state.d.ts +0 -1
- package/dist/esm/node/agent-state.js +0 -1
- package/dist/esm/node/agent-transport.d.ts +0 -1
- package/dist/esm/node/agent-transport.js +0 -1
- package/dist/esm/node/deployment.d.ts +0 -1
- package/dist/esm/node/deployment.js +0 -1
- package/dist/esm/node/file-agent-state.d.ts +0 -1
- package/dist/esm/node/file-agent-state.js +0 -1
- package/dist/esm/node/native-udp.d.ts +0 -1
- package/dist/esm/node/native-udp.js +0 -1
- package/dist/esm/node/nhp-wire.d.ts +0 -1
- package/dist/esm/node/nhp-wire.js +0 -1
- package/dist/esm/node/portal-opener.d.ts +0 -1
- package/dist/esm/node/portal-opener.js +0 -1
- package/dist/esm/node/provider.d.ts +0 -1
- package/dist/esm/node/provider.js +0 -1
- package/dist/esm/node/qv2.d.ts +0 -1
- package/dist/esm/node/qv2.js +0 -1
- package/dist/esm/node/relay.d.ts +0 -1
- package/dist/esm/node/relay.js +0 -1
- package/dist/esm/node/sealed-agent-state.d.ts +0 -1
- package/dist/esm/node/sealed-agent-state.js +0 -1
- package/dist/esm/node/strict-json.d.ts +0 -1
- package/dist/esm/node/strict-json.js +0 -1
- package/dist/esm/node.d.ts +0 -1
- package/dist/esm/node.js +0 -1
- package/dist/esm/response-metadata.d.ts +13 -0
- package/dist/esm/response-metadata.js +33 -0
- package/dist/esm/share.d.ts +0 -1
- package/dist/esm/share.js +2 -1
- package/dist/esm/target-path.d.ts +2 -0
- package/dist/esm/target-path.js +30 -0
- package/dist/esm/types.d.ts +83 -8
- package/dist/esm/types.js +0 -1
- package/dist/esm/version.d.ts +1 -2
- package/dist/esm/version.js +1 -2
- package/package.json +9 -7
- package/dist/esm/client.d.ts.map +0 -1
- package/dist/esm/client.js.map +0 -1
- package/dist/esm/crid.d.ts.map +0 -1
- package/dist/esm/crid.js.map +0 -1
- package/dist/esm/errors.d.ts.map +0 -1
- package/dist/esm/errors.js.map +0 -1
- package/dist/esm/index.d.ts.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/internal.d.ts.map +0 -1
- package/dist/esm/internal.js.map +0 -1
- package/dist/esm/node/agent-runtime.d.ts.map +0 -1
- package/dist/esm/node/agent-runtime.js.map +0 -1
- package/dist/esm/node/agent-state.d.ts.map +0 -1
- package/dist/esm/node/agent-state.js.map +0 -1
- package/dist/esm/node/agent-transport.d.ts.map +0 -1
- package/dist/esm/node/agent-transport.js.map +0 -1
- package/dist/esm/node/deployment.d.ts.map +0 -1
- package/dist/esm/node/deployment.js.map +0 -1
- package/dist/esm/node/file-agent-state.d.ts.map +0 -1
- package/dist/esm/node/file-agent-state.js.map +0 -1
- package/dist/esm/node/native-udp.d.ts.map +0 -1
- package/dist/esm/node/native-udp.js.map +0 -1
- package/dist/esm/node/nhp-wire.d.ts.map +0 -1
- package/dist/esm/node/nhp-wire.js.map +0 -1
- package/dist/esm/node/portal-opener.d.ts.map +0 -1
- package/dist/esm/node/portal-opener.js.map +0 -1
- package/dist/esm/node/provider.d.ts.map +0 -1
- package/dist/esm/node/provider.js.map +0 -1
- package/dist/esm/node/qv2.d.ts.map +0 -1
- package/dist/esm/node/qv2.js.map +0 -1
- package/dist/esm/node/relay.d.ts.map +0 -1
- package/dist/esm/node/relay.js.map +0 -1
- package/dist/esm/node/sealed-agent-state.d.ts.map +0 -1
- package/dist/esm/node/sealed-agent-state.js.map +0 -1
- package/dist/esm/node/strict-json.d.ts.map +0 -1
- package/dist/esm/node/strict-json.js.map +0 -1
- package/dist/esm/node.d.ts.map +0 -1
- package/dist/esm/node.js.map +0 -1
- package/dist/esm/share.d.ts.map +0 -1
- package/dist/esm/share.js.map +0 -1
- package/dist/esm/types.d.ts.map +0 -1
- package/dist/esm/types.js.map +0 -1
- package/dist/esm/version.d.ts.map +0 -1
- package/dist/esm/version.js.map +0 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { assertExhaustive } from "./client-input-fields.js";
|
|
2
|
+
import { requireMaxLength, requireNoUnknownFields, requireNonEmptyArrayField, requireNonEmptyStringField, requireStringArrayElements, requireValidHttpUrl, } from "./client-validation.js";
|
|
3
|
+
import { MAX_API_KEY_NAME, MAX_DESCRIPTION, MAX_ENROLLMENT_TOKEN_TTL_SECONDS, } from "./client-limits.js";
|
|
4
|
+
import { clientValidationError } from "./client-errors.js";
|
|
5
|
+
import { CONNECTOR_SLUG_PATTERN } from "./client-connectors.js";
|
|
6
|
+
const API_KEY_REQUEST_SCOPE_VALUES = [
|
|
7
|
+
"qurl:read",
|
|
8
|
+
"qurl:write",
|
|
9
|
+
"qurl:resolve",
|
|
10
|
+
"qurl:agent",
|
|
11
|
+
];
|
|
12
|
+
assertExhaustive(true);
|
|
13
|
+
const API_KEY_REQUEST_SCOPES = new Set(API_KEY_REQUEST_SCOPE_VALUES);
|
|
14
|
+
/** Narrow a response scope before reusing it in a create/update request. */
|
|
15
|
+
export function isApiKeyRequestScope(scope) {
|
|
16
|
+
return API_KEY_REQUEST_SCOPES.has(scope);
|
|
17
|
+
}
|
|
18
|
+
const CREDENTIAL_TARGET_VALUES = [
|
|
19
|
+
"agent",
|
|
20
|
+
"connector",
|
|
21
|
+
];
|
|
22
|
+
assertExhaustive(true);
|
|
23
|
+
const CREDENTIAL_TARGETS = new Set(CREDENTIAL_TARGET_VALUES);
|
|
24
|
+
const CREDENTIAL_CLAIM_TYPE_VALUES = [
|
|
25
|
+
"connector",
|
|
26
|
+
];
|
|
27
|
+
assertExhaustive(true);
|
|
28
|
+
const CREDENTIAL_CLAIM_TYPES = new Set(CREDENTIAL_CLAIM_TYPE_VALUES);
|
|
29
|
+
// Mirrors CreateApiKeyRequest.expires_in in the service OpenAPI. Composite
|
|
30
|
+
// durations are deliberately invalid on this endpoint. The 24h cap permits
|
|
31
|
+
// 1d, but no positive whole-week duration.
|
|
32
|
+
const CREDENTIAL_DURATION_PATTERN = /^([0-9]+)([smhdw])$/;
|
|
33
|
+
const CREDENTIAL_DURATION_MULTIPLIERS = {
|
|
34
|
+
s: 1n,
|
|
35
|
+
m: 60n,
|
|
36
|
+
h: 3600n,
|
|
37
|
+
d: 86400n,
|
|
38
|
+
w: 604800n,
|
|
39
|
+
};
|
|
40
|
+
export function validateWebhookWriteFields(input, method, requiredFields = {}) {
|
|
41
|
+
if (requiredFields.url || input.url !== undefined) {
|
|
42
|
+
requireNonEmptyStringField(input, "url", method);
|
|
43
|
+
requireValidHttpUrl(input.url, "url");
|
|
44
|
+
}
|
|
45
|
+
if (requiredFields.events || input.events !== undefined) {
|
|
46
|
+
requireNonEmptyArrayField(input, "events", method);
|
|
47
|
+
requireStringArrayElements(input.events, "events", method);
|
|
48
|
+
}
|
|
49
|
+
requireMaxLength(input.description, "description", MAX_DESCRIPTION);
|
|
50
|
+
if (input.status !== undefined) {
|
|
51
|
+
requireNonEmptyStringField(input, "status", method);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function validateApiKeyWriteFields(input, method, requiredFields = {}) {
|
|
55
|
+
if (requiredFields.name || input.name !== undefined) {
|
|
56
|
+
requireNonEmptyStringField(input, "name", method);
|
|
57
|
+
requireMaxLength(input.name, "name", MAX_API_KEY_NAME);
|
|
58
|
+
}
|
|
59
|
+
if (requiredFields.scopes || input.scopes !== undefined) {
|
|
60
|
+
requireNonEmptyArrayField(input, "scopes", method);
|
|
61
|
+
requireStringArrayElements(input.scopes, "scopes", method);
|
|
62
|
+
const unsupported = input.scopes.find((scope) => !API_KEY_REQUEST_SCOPES.has(scope));
|
|
63
|
+
if (unsupported !== undefined) {
|
|
64
|
+
throw clientValidationError(`${method}: scopes contains an unsupported permission ${JSON.stringify(unsupported)}; this SDK accepts ${API_KEY_REQUEST_SCOPE_VALUES.join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function validateEnrollmentTokenFields(input, method) {
|
|
69
|
+
if (input.target !== undefined &&
|
|
70
|
+
(typeof input.target !== "string" || !CREDENTIAL_TARGETS.has(input.target))) {
|
|
71
|
+
throw clientValidationError(`${method}: target must be 'agent' or 'connector'`);
|
|
72
|
+
}
|
|
73
|
+
let claimCount = 0;
|
|
74
|
+
if (input.claims !== undefined) {
|
|
75
|
+
if (!Array.isArray(input.claims)) {
|
|
76
|
+
throw clientValidationError(`${method}: claims must be an array`);
|
|
77
|
+
}
|
|
78
|
+
if (input.claims.length > 1) {
|
|
79
|
+
throw clientValidationError(`${method}: claims must contain at most one claim`);
|
|
80
|
+
}
|
|
81
|
+
claimCount = input.claims.length;
|
|
82
|
+
for (const [index, claim] of input.claims.entries()) {
|
|
83
|
+
if (!claim || typeof claim !== "object" || Array.isArray(claim)) {
|
|
84
|
+
throw clientValidationError(`${method}: claims[${index}] must be an object`);
|
|
85
|
+
}
|
|
86
|
+
requireNoUnknownFields(claim, ["type", "id"], `${method}: claims[${index}]`);
|
|
87
|
+
const record = claim;
|
|
88
|
+
if (typeof record.type !== "string" || !CREDENTIAL_CLAIM_TYPES.has(record.type)) {
|
|
89
|
+
throw clientValidationError(`${method}: claims[${index}].type must be 'connector'`);
|
|
90
|
+
}
|
|
91
|
+
if (typeof record.id !== "string" || !CONNECTOR_SLUG_PATTERN.test(record.id)) {
|
|
92
|
+
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`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// CreateApiKeyRequest.target permits an agent with zero or one connector claim.
|
|
97
|
+
if (input.target === "connector" && claimCount !== 1) {
|
|
98
|
+
throw clientValidationError(`${method}: target 'connector' requires exactly one claim`);
|
|
99
|
+
}
|
|
100
|
+
if (input.expires_in !== undefined) {
|
|
101
|
+
if (typeof input.expires_in !== "string") {
|
|
102
|
+
throw clientValidationError(`${method}: expires_in must be a duration string`);
|
|
103
|
+
}
|
|
104
|
+
const match = CREDENTIAL_DURATION_PATTERN.exec(input.expires_in);
|
|
105
|
+
if (!match) {
|
|
106
|
+
throw clientValidationError(`${method}: expires_in must match <integer><s|m|h|d|w>`);
|
|
107
|
+
}
|
|
108
|
+
const seconds = BigInt(match[1]) * CREDENTIAL_DURATION_MULTIPLIERS[match[2]];
|
|
109
|
+
if (seconds <= 0n || seconds > MAX_ENROLLMENT_TOKEN_TTL_SECONDS) {
|
|
110
|
+
throw clientValidationError(`${method}: expires_in must be greater than zero and at most 24h`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface PathIdValidationOptions {
|
|
2
|
+
/** Reject a bare access token in namespaces that accept resource/qURL IDs. */
|
|
3
|
+
rejectBareAccessToken?: boolean;
|
|
4
|
+
/** Safe, non-secret recovery guidance appended to an access-token error. */
|
|
5
|
+
accessTokenRecovery?: string;
|
|
6
|
+
/** Reject qURL display IDs where the operation requires a parent resource. */
|
|
7
|
+
rejectQurlDisplayId?: boolean;
|
|
8
|
+
requireCridUnlessQurlDisplayId?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const RESOURCE_OR_QURL_ID_PATH_OPTIONS: PathIdValidationOptions;
|
|
11
|
+
export declare const QURL_ID_PATH_OPTIONS: PathIdValidationOptions;
|
|
12
|
+
/**
|
|
13
|
+
* Validates that a path-parameter argument is a non-empty string. Some callers
|
|
14
|
+
* pass resource/qURL display IDs, while newer endpoints also pass domains,
|
|
15
|
+
* session IDs, webhook IDs, and API-key IDs, so this intentionally enforces
|
|
16
|
+
* only basic shape plus transport-safety rules and leaves endpoint-specific
|
|
17
|
+
* identifier grammar to the service.
|
|
18
|
+
*/
|
|
19
|
+
export declare function validatePathId(id: string, method: string, field?: string, options?: PathIdValidationOptions): void;
|
|
20
|
+
export declare function requireResourceCrid(crid: string, method: string, field?: string): void;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { parseCrid } from "./crid.js";
|
|
2
|
+
import { clientValidationError } from "./client-errors.js";
|
|
3
|
+
import { MAX_PATH_ID_DECODE_PASSES, MAX_PATH_ID_LENGTH } from "./client-limits.js";
|
|
4
|
+
// Best-effort defence-in-depth against today's access-token grammar. The
|
|
5
|
+
// service remains authoritative because resource IDs and credentials are both
|
|
6
|
+
// opaque and future credential formats may not retain the `at_` prefix. The
|
|
7
|
+
// delimiter requirement cannot collide with current base64url public keys,
|
|
8
|
+
// standard-base64 keys, or base32 CRIDs.
|
|
9
|
+
const PATH_EMBEDDED_ACCESS_TOKEN_RE = /[?&#=:\s]at_/i;
|
|
10
|
+
const PATH_PREFIXED_ACCESS_TOKEN_RE = /\/at_/i;
|
|
11
|
+
const QURL_LINK_URL_RE = /^(?:https?:\/\/)?(?:[a-z0-9-]+\.)*qurl\.link(?::\d{1,5})?(?:\/|[?#])/i;
|
|
12
|
+
// ponytail: today's 22-character token suffix only improves URL-paste diagnostics;
|
|
13
|
+
// other lengths are still rejected as URLs without echoing the input.
|
|
14
|
+
const URL_EMBEDDED_ACCESS_TOKEN_RE = /[?&#=:/]at_[a-z0-9_-]{22}(?:$|[?&#:/])/i;
|
|
15
|
+
// A host without a slash can be a legitimate domain identifier. Require the
|
|
16
|
+
// path boundary so shared validation does not break the domain namespace.
|
|
17
|
+
const PATH_SCHEMELESS_URL_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)+(?::\d{1,5})?\//i;
|
|
18
|
+
const QURL_DISPLAY_ID_PREFIX_RE = /^q_/i;
|
|
19
|
+
function isUrlLikePathId(value) {
|
|
20
|
+
return /^https?:\/\//i.test(value) || PATH_SCHEMELESS_URL_RE.test(value);
|
|
21
|
+
}
|
|
22
|
+
const REVOKE_INDIVIDUAL_QURL_RECOVERY = "revoke individual tokens with revokeResourceQurl(crid, qurlId)";
|
|
23
|
+
const DELETE_RESOURCE_RECOVERY = "pass a CRID returned by the API; to revoke one qURL, call revokeResourceQurl(crid, qurlId)";
|
|
24
|
+
export const RESOURCE_OR_QURL_ID_PATH_OPTIONS = {
|
|
25
|
+
requireCridUnlessQurlDisplayId: true,
|
|
26
|
+
};
|
|
27
|
+
export const QURL_ID_PATH_OPTIONS = {
|
|
28
|
+
rejectBareAccessToken: true,
|
|
29
|
+
accessTokenRecovery: "pass the qURL display ID, not its access token",
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Validates that a path-parameter argument is a non-empty string. Some callers
|
|
33
|
+
* pass resource/qURL display IDs, while newer endpoints also pass domains,
|
|
34
|
+
* session IDs, webhook IDs, and API-key IDs, so this intentionally enforces
|
|
35
|
+
* only basic shape plus transport-safety rules and leaves endpoint-specific
|
|
36
|
+
* identifier grammar to the service.
|
|
37
|
+
*/
|
|
38
|
+
export function validatePathId(id, method, field = "id", options = {}) {
|
|
39
|
+
if (options.requireCridUnlessQurlDisplayId && typeof id === "string" && !id.startsWith("q_")) {
|
|
40
|
+
requireResourceCrid(id, method, field);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// `.trim()` catches whitespace-only and padded IDs before they round-trip as
|
|
44
|
+
// `%20...%20` paths that the server can only reject with a less useful 404.
|
|
45
|
+
// The pre-flight error is more actionable than the 404.
|
|
46
|
+
if (typeof id !== "string") {
|
|
47
|
+
const observedType = id === null ? "null" : typeof id;
|
|
48
|
+
throw clientValidationError(`${method}: ${field} is required (got ${observedType})`);
|
|
49
|
+
}
|
|
50
|
+
if (id.trim() === "") {
|
|
51
|
+
throw clientValidationError(`${method}: ${field} is required`);
|
|
52
|
+
}
|
|
53
|
+
if (id.trim() !== id) {
|
|
54
|
+
throw clientValidationError(`${method}: ${field} must not include leading or trailing whitespace`);
|
|
55
|
+
}
|
|
56
|
+
if (id.length > MAX_PATH_ID_LENGTH) {
|
|
57
|
+
throw clientValidationError(`${method}: ${field} must be ${MAX_PATH_ID_LENGTH} characters or fewer`);
|
|
58
|
+
}
|
|
59
|
+
const decodedIds = decodedPathIdForms(id);
|
|
60
|
+
// URL resolvers and intermediaries can normalize raw or pre-encoded dot
|
|
61
|
+
// segments before routing. Reject every decoded form we inspected so an
|
|
62
|
+
// item operation cannot be retargeted to a collection endpoint. Embedded
|
|
63
|
+
// `a/../b` remains an opaque single segment because the later URL builder
|
|
64
|
+
// percent-encodes its slash.
|
|
65
|
+
if (decodedIds.some((value) => value === "." || value === "..")) {
|
|
66
|
+
throw clientValidationError(`${method}: ${field} is an invalid URL path segment`);
|
|
67
|
+
}
|
|
68
|
+
// `/` is deliberately outside the general delimiter class so a URL path
|
|
69
|
+
// such as `/at_a_glance` is not misread as a credential. For non-URL input,
|
|
70
|
+
// `/at_` remains a strong pasted-path signal. Fragment, query, header, and
|
|
71
|
+
// pasted `Bearer at_...` / `token:at_...` forms are also strong signals.
|
|
72
|
+
// Bare `at_...` values are rejected only for resource/qURL identifiers:
|
|
73
|
+
// those current formats have fixed non-colliding prefixes, while domain,
|
|
74
|
+
// webhook, session, and API-key IDs are deliberately opaque/open contracts.
|
|
75
|
+
// Full URLs and delimiter/path-prefixed credentials remain rejected in
|
|
76
|
+
// every namespace.
|
|
77
|
+
const containsUrl = decodedIds.some(isUrlLikePathId);
|
|
78
|
+
const containsAccessToken = decodedIds.some((value) => {
|
|
79
|
+
const valueIsUrl = isUrlLikePathId(value);
|
|
80
|
+
return ((QURL_LINK_URL_RE.test(value) &&
|
|
81
|
+
(PATH_EMBEDDED_ACCESS_TOKEN_RE.test(value) || PATH_PREFIXED_ACCESS_TOKEN_RE.test(value))) ||
|
|
82
|
+
(valueIsUrl
|
|
83
|
+
? URL_EMBEDDED_ACCESS_TOKEN_RE.test(value)
|
|
84
|
+
: PATH_EMBEDDED_ACCESS_TOKEN_RE.test(value) || PATH_PREFIXED_ACCESS_TOKEN_RE.test(value)) ||
|
|
85
|
+
(options.rejectBareAccessToken === true && value.slice(0, 3).toLowerCase() === "at_"));
|
|
86
|
+
});
|
|
87
|
+
if (containsAccessToken) {
|
|
88
|
+
const recovery = options.accessTokenRecovery ?? "pass the identifier returned by the API";
|
|
89
|
+
throw clientValidationError(`${method}: ${field} must not contain an access token; ${recovery}`);
|
|
90
|
+
}
|
|
91
|
+
// A full target/qURL is a common argument mix-up, but it is distinct from
|
|
92
|
+
// passing an access-token credential. Diagnose it accurately without
|
|
93
|
+
// echoing the URL, which may itself contain sensitive query parameters.
|
|
94
|
+
if (containsUrl) {
|
|
95
|
+
throw clientValidationError(`${method}: ${field} must be an identifier, not a URL`);
|
|
96
|
+
}
|
|
97
|
+
if (options.rejectQurlDisplayId === true &&
|
|
98
|
+
decodedIds.some((value) => QURL_DISPLAY_ID_PREFIX_RE.test(value))) {
|
|
99
|
+
throw clientValidationError(`${method}: ${field} must not be a qURL display ID; ${REVOKE_INDIVIDUAL_QURL_RECOVERY}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function decodedPathIdForms(id) {
|
|
103
|
+
// Probe repeatedly encoded input too. Callers sometimes pre-encode a copied
|
|
104
|
+
// link before passing it through a URL builder that encodes it again. Three
|
|
105
|
+
// passes cover the common accidental cases without allowing hostile input
|
|
106
|
+
// to create unbounded work. More deeply encoded or malformed-percent input
|
|
107
|
+
// deliberately fails open to the authoritative service after safe segment
|
|
108
|
+
// encoding; this heuristic is not an identifier parser.
|
|
109
|
+
const decodedIds = [id];
|
|
110
|
+
let decodedId = id;
|
|
111
|
+
for (let pass = 0; pass < MAX_PATH_ID_DECODE_PASSES; pass += 1) {
|
|
112
|
+
try {
|
|
113
|
+
const next = decodeURIComponent(decodedId);
|
|
114
|
+
if (next === decodedId)
|
|
115
|
+
break;
|
|
116
|
+
decodedIds.push(next);
|
|
117
|
+
decodedId = next;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Malformed percent escapes remain opaque and are left to the service.
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return decodedIds;
|
|
125
|
+
}
|
|
126
|
+
export function requireResourceCrid(crid, method, field = "id") {
|
|
127
|
+
if (parseCrid(crid))
|
|
128
|
+
return;
|
|
129
|
+
// Keep actionable paste errors without inspecting valid CRIDs twice.
|
|
130
|
+
validatePathId(crid, method, field, {
|
|
131
|
+
rejectBareAccessToken: true,
|
|
132
|
+
rejectQurlDisplayId: true,
|
|
133
|
+
accessTokenRecovery: method === "delete" || method === "deleteResource"
|
|
134
|
+
? DELETE_RESOURCE_RECOVERY
|
|
135
|
+
: "pass a CRID returned by the API",
|
|
136
|
+
});
|
|
137
|
+
throw clientValidationError(`${method}: requires a valid CRID`);
|
|
138
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { CreatePortalOptions, Portal, RequestOptions, Resource } from "./types.js";
|
|
2
|
+
import type { QURLClient } from "./client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Shape of qurl-go's offline signed-fragment links: three or more
|
|
5
|
+
* dot-separated base64url parts (<version>.<claims>.<secret>.<sig>).
|
|
6
|
+
* Detected only to give those links a precise error in extractAccessToken.
|
|
7
|
+
*
|
|
8
|
+
* @internal Exported for in-package tests; not re-exported from index.ts.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SIGNED_FRAGMENT_RE: RegExp;
|
|
11
|
+
/**
|
|
12
|
+
* Extract the access token from a qURL link, or pass a bare token through.
|
|
13
|
+
*
|
|
14
|
+
* Platform-issued links carry the access token in the URL fragment
|
|
15
|
+
* (`https://qurl.link/#at_...`). Error messages deliberately never echo the
|
|
16
|
+
* input — qURL links are credentials and must stay out of logs.
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractAccessToken(qurlLink: unknown): string;
|
|
19
|
+
/**
|
|
20
|
+
* Parse an RFC 3339 timestamp from the API into a `Date`, or `undefined`
|
|
21
|
+
* when absent or unparseable. The portal surface exposes `Date` (matching
|
|
22
|
+
* qurl-go's `time.Time` and qurl-python's `datetime`); the REST-shaped
|
|
23
|
+
* layer keeps raw wire strings.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseApiDate(value: string | undefined): Date | undefined;
|
|
26
|
+
/** Wire shape shared by the two portal-minting endpoints. */
|
|
27
|
+
export type PortalWireResponse = {
|
|
28
|
+
resource_id?: string;
|
|
29
|
+
crid?: string;
|
|
30
|
+
qurl_link?: string;
|
|
31
|
+
qurl_site?: string;
|
|
32
|
+
expires_at?: string;
|
|
33
|
+
qurl_id?: string;
|
|
34
|
+
label?: string;
|
|
35
|
+
target_path?: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Parse a {@link Portal} from a create-portal API response. `resource_id`
|
|
39
|
+
* and `qurl_link` are required, mirroring qurl-go's fail-closed response
|
|
40
|
+
* check; everything else is optional because the two mint endpoints differ
|
|
41
|
+
* in which ancillary fields they return.
|
|
42
|
+
*/
|
|
43
|
+
export declare function parsePortal(data: PortalWireResponse | undefined, method: string, expectedCrid?: string, expectedTargetPath?: string): Promise<Portal>;
|
|
44
|
+
/**
|
|
45
|
+
* Validate portal options and build the create-portal wire body. The service
|
|
46
|
+
* requires a JSON object even when every option is omitted; `{}` selects the
|
|
47
|
+
* owner's plan defaults and matches qurl-go's zero-value request struct.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildCreatePortalBody(opts: CreatePortalOptions, method: string): Record<string, unknown>;
|
|
50
|
+
/**
|
|
51
|
+
* A LayerV-protected resource, bound to the client that produced it.
|
|
52
|
+
*
|
|
53
|
+
* Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.resourceByCrid} —
|
|
54
|
+
* not constructed directly. Mint short-lived access links for the resource
|
|
55
|
+
* with {@link createPortal} (qurl-go: `qurl.Resource`).
|
|
56
|
+
*/
|
|
57
|
+
export declare class ProtectedResource {
|
|
58
|
+
#private;
|
|
59
|
+
/** The public resource CRID. */
|
|
60
|
+
readonly crid: string;
|
|
61
|
+
/** The private URL protected by this resource, when known. */
|
|
62
|
+
readonly targetUrl?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Full server-populated {@link Resource} metadata when the handle came
|
|
65
|
+
* from an API response; `undefined` for {@link QURLClient.resourceByCrid}
|
|
66
|
+
* handles and for the one-call {@link QURLClient.createPortalForUrl} path.
|
|
67
|
+
*/
|
|
68
|
+
readonly details?: Resource;
|
|
69
|
+
constructor(client: QURLClient, id: string, targetUrl?: string, details?: Resource);
|
|
70
|
+
/**
|
|
71
|
+
* Whether `resource` was produced by `client`. A static because the
|
|
72
|
+
* binding lives in a private field that only this class can read; used
|
|
73
|
+
* by {@link QURLClient.createPortal} to refuse handles bound to a
|
|
74
|
+
* different client.
|
|
75
|
+
*/
|
|
76
|
+
static isBoundTo(resource: ProtectedResource, client: QURLClient): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Mint a short-lived qURL link (a portal) for this resource. See
|
|
79
|
+
* {@link QURLClient.createPortal} for the option reference.
|
|
80
|
+
*/
|
|
81
|
+
createPortal(opts?: CreatePortalOptions, requestOptions?: RequestOptions): Promise<Portal>;
|
|
82
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { copyResponseMetadata, getResponseMetadata } from "./response-metadata.js";
|
|
2
|
+
import { clientValidationError, unexpectedResponseError } from "./client-errors.js";
|
|
3
|
+
import { verifyResourceIdentity } from "./client-connectors.js";
|
|
4
|
+
import { normalizePatchFields, requireBooleanIfPresent, requireMaxLength, requireMaxSessionsInRange, requireNoUnknownFields, requireObjectInput, requireResourceNameIfPresent, } from "./client-validation.js";
|
|
5
|
+
import { CREATE_PORTAL_OPTION_KEYS } from "./client-input-fields.js";
|
|
6
|
+
import { MAX_LABEL } from "./client-limits.js";
|
|
7
|
+
import { isCanonicalTargetPath, requireCanonicalTargetPath } from "./target-path.js";
|
|
8
|
+
// ---- Portal-verb helpers (mirrors qurl-go) -------------------------------
|
|
9
|
+
// Client-side guardrails for the portal flow (protectUrl → createPortal →
|
|
10
|
+
// enterPortal), kept in lockstep with qurl-go's option validation and
|
|
11
|
+
// qurl-python's port of it.
|
|
12
|
+
/**
|
|
13
|
+
* Client-side floor for portal lifetimes, mirroring qurl-go's `ValidFor`.
|
|
14
|
+
* The LayerV API remains the source of truth for account limits; there is
|
|
15
|
+
* intentionally no SDK-side maximum because an account may allow
|
|
16
|
+
* longer-lived portals.
|
|
17
|
+
*/
|
|
18
|
+
const MIN_PORTAL_VALID_FOR_SECONDS = 60;
|
|
19
|
+
/**
|
|
20
|
+
* Client-side floor for session durations, mirroring qurl-go's
|
|
21
|
+
* `WithSessionDuration` (at least one second; zero is rejected rather than
|
|
22
|
+
* treated as the server default).
|
|
23
|
+
*/
|
|
24
|
+
const MIN_SESSION_DURATION_SECONDS = 1;
|
|
25
|
+
/**
|
|
26
|
+
* Shape of qurl-go's offline signed-fragment links: three or more
|
|
27
|
+
* dot-separated base64url parts (<version>.<claims>.<secret>.<sig>).
|
|
28
|
+
* Detected only to give those links a precise error in extractAccessToken.
|
|
29
|
+
*
|
|
30
|
+
* @internal Exported for in-package tests; not re-exported from index.ts.
|
|
31
|
+
*/
|
|
32
|
+
export const SIGNED_FRAGMENT_RE = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+){2,}$/;
|
|
33
|
+
// Access-token grammar accepted by enterPortal (base64url charset).
|
|
34
|
+
const ACCESS_TOKEN_RE = /^[A-Za-z0-9_-]+$/;
|
|
35
|
+
/**
|
|
36
|
+
* Serialize a duration option to the API's h/m/s duration grammar.
|
|
37
|
+
*
|
|
38
|
+
* Strings pass through unvalidated — the server is authoritative, same as
|
|
39
|
+
* the REST-shaped `expires_in` fields. Numbers are milliseconds and get
|
|
40
|
+
* qurl-go's client-side guardrails: whole seconds only and at least
|
|
41
|
+
* `minimumSeconds` (checked in that order so e.g. 30.5s reports "at least
|
|
42
|
+
* 60s" in every SDK). Serialized with hours as the largest unit so all API
|
|
43
|
+
* duration fields use the same grammar across SDKs.
|
|
44
|
+
*/
|
|
45
|
+
function serializeApiDuration(value, field, minimumSeconds) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (typeof value === "string") {
|
|
49
|
+
if (value.trim() === "") {
|
|
50
|
+
throw clientValidationError(`${field}: must be a non-empty duration string`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === "number") {
|
|
55
|
+
// NaN fails no comparison, so an explicit finite check keeps the
|
|
56
|
+
// error actionable instead of a misleading "whole seconds" report.
|
|
57
|
+
if (!Number.isFinite(value)) {
|
|
58
|
+
throw clientValidationError(`${field}: must be a finite number of milliseconds`);
|
|
59
|
+
}
|
|
60
|
+
if (value < minimumSeconds * 1000) {
|
|
61
|
+
throw clientValidationError(`${field}: must be at least ${minimumSeconds}s`);
|
|
62
|
+
}
|
|
63
|
+
if (value % 1000 !== 0) {
|
|
64
|
+
throw clientValidationError(`${field}: must be whole seconds`);
|
|
65
|
+
}
|
|
66
|
+
const seconds = value / 1000;
|
|
67
|
+
if (seconds % 3600 === 0)
|
|
68
|
+
return `${seconds / 3600}h`;
|
|
69
|
+
if (seconds % 60 === 0)
|
|
70
|
+
return `${seconds / 60}m`;
|
|
71
|
+
return `${seconds}s`;
|
|
72
|
+
}
|
|
73
|
+
throw clientValidationError(`${field}: must be a duration string (e.g. "5m") or a number of milliseconds`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Extract the access token from a qURL link, or pass a bare token through.
|
|
77
|
+
*
|
|
78
|
+
* Platform-issued links carry the access token in the URL fragment
|
|
79
|
+
* (`https://qurl.link/#at_...`). Error messages deliberately never echo the
|
|
80
|
+
* input — qURL links are credentials and must stay out of logs.
|
|
81
|
+
*/
|
|
82
|
+
export function extractAccessToken(qurlLink) {
|
|
83
|
+
if (typeof qurlLink !== "string" || qurlLink.trim() === "") {
|
|
84
|
+
throw clientValidationError("qurlLink: must be a non-empty string");
|
|
85
|
+
}
|
|
86
|
+
const hashIndex = qurlLink.indexOf("#");
|
|
87
|
+
const token = hashIndex >= 0 ? qurlLink.slice(hashIndex + 1) : qurlLink;
|
|
88
|
+
if (SIGNED_FRAGMENT_RE.test(token)) {
|
|
89
|
+
throw clientValidationError("qurlLink: this is a signed qURL link (offline fragment format), which cannot be " +
|
|
90
|
+
"opened through the API resolver — enterPortal only accepts platform links " +
|
|
91
|
+
"(https://qurl.link/#at_...) or bare access tokens");
|
|
92
|
+
}
|
|
93
|
+
if (token === "" || !ACCESS_TOKEN_RE.test(token)) {
|
|
94
|
+
throw clientValidationError("qurlLink: no access token found — pass a platform qURL link " +
|
|
95
|
+
"(https://qurl.link/#at_...) or a bare access token");
|
|
96
|
+
}
|
|
97
|
+
return token;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Parse an RFC 3339 timestamp from the API into a `Date`, or `undefined`
|
|
101
|
+
* when absent or unparseable. The portal surface exposes `Date` (matching
|
|
102
|
+
* qurl-go's `time.Time` and qurl-python's `datetime`); the REST-shaped
|
|
103
|
+
* layer keeps raw wire strings.
|
|
104
|
+
*/
|
|
105
|
+
export function parseApiDate(value) {
|
|
106
|
+
if (typeof value !== "string" || value === "")
|
|
107
|
+
return undefined;
|
|
108
|
+
const parsed = new Date(value);
|
|
109
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Parse a {@link Portal} from a create-portal API response. `resource_id`
|
|
113
|
+
* and `qurl_link` are required, mirroring qurl-go's fail-closed response
|
|
114
|
+
* check; everything else is optional because the two mint endpoints differ
|
|
115
|
+
* in which ancillary fields they return.
|
|
116
|
+
*/
|
|
117
|
+
export async function parsePortal(data, method, expectedCrid, expectedTargetPath) {
|
|
118
|
+
const errorOptions = { requestId: getResponseMetadata(data)?.requestId };
|
|
119
|
+
if (typeof data?.resource_id !== "string" || data.resource_id.trim() === "") {
|
|
120
|
+
throw unexpectedResponseError(`${method}: response is missing resource_id`, errorOptions);
|
|
121
|
+
}
|
|
122
|
+
if (typeof data.qurl_link !== "string" || data.qurl_link.trim() === "") {
|
|
123
|
+
throw unexpectedResponseError(`${method}: response is missing qurl_link`, errorOptions);
|
|
124
|
+
}
|
|
125
|
+
await verifyResourceIdentity(data, method, expectedCrid);
|
|
126
|
+
if (data.target_path !== expectedTargetPath ||
|
|
127
|
+
(data.target_path !== undefined && !isCanonicalTargetPath(data.target_path))) {
|
|
128
|
+
throw unexpectedResponseError(`${method}: mint response target path differs from request`, errorOptions);
|
|
129
|
+
}
|
|
130
|
+
return copyResponseMetadata(data, {
|
|
131
|
+
crid: data.crid,
|
|
132
|
+
resourcePublicKey: data.resource_id,
|
|
133
|
+
link: data.qurl_link,
|
|
134
|
+
site: data.qurl_site,
|
|
135
|
+
expiresAt: parseApiDate(data.expires_at),
|
|
136
|
+
// Same empty-string → absent normalization the REST-shaped layer's
|
|
137
|
+
// consumers apply to qurl_id.
|
|
138
|
+
qurlId: data.qurl_id || undefined,
|
|
139
|
+
label: data.label,
|
|
140
|
+
targetPath: data.target_path,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Validate portal options and build the create-portal wire body. The service
|
|
145
|
+
* requires a JSON object even when every option is omitted; `{}` selects the
|
|
146
|
+
* owner's plan defaults and matches qurl-go's zero-value request struct.
|
|
147
|
+
*/
|
|
148
|
+
export function buildCreatePortalBody(opts, method) {
|
|
149
|
+
requireObjectInput(opts, method);
|
|
150
|
+
requireNoUnknownFields(opts, CREATE_PORTAL_OPTION_KEYS, method);
|
|
151
|
+
// null → omitted, via the SDK-wide normalizePatchFields convention.
|
|
152
|
+
const { validFor, label, oneTimeUse, maxSessions, sessionDuration, targetPath, name } = normalizePatchFields(opts, CREATE_PORTAL_OPTION_KEYS);
|
|
153
|
+
if (label !== undefined) {
|
|
154
|
+
requireMaxLength(label, "label", MAX_LABEL);
|
|
155
|
+
// qurl-go's WithLabel rejects blank labels; the REST-shaped mintLink
|
|
156
|
+
// keeps its permissive whitespace behavior, so the check lives here.
|
|
157
|
+
if (label.trim() === "") {
|
|
158
|
+
throw clientValidationError("label: must not be empty");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
requireBooleanIfPresent(oneTimeUse, "oneTimeUse");
|
|
162
|
+
requireMaxSessionsInRange(maxSessions, "maxSessions");
|
|
163
|
+
requireCanonicalTargetPath(targetPath, "targetPath");
|
|
164
|
+
if (targetPath !== undefined && method === "createPortalForUrl") {
|
|
165
|
+
throw clientValidationError("targetPath: requires an existing resource");
|
|
166
|
+
}
|
|
167
|
+
if (name !== undefined && method !== "createPortalForUrl") {
|
|
168
|
+
throw clientValidationError("name: only applies when creating a resource (use createPortalForUrl or protectUrl); this resource exists");
|
|
169
|
+
}
|
|
170
|
+
requireResourceNameIfPresent(name);
|
|
171
|
+
const body = {};
|
|
172
|
+
const expiresIn = serializeApiDuration(validFor, "validFor", MIN_PORTAL_VALID_FOR_SECONDS);
|
|
173
|
+
if (expiresIn !== undefined)
|
|
174
|
+
body.expires_in = expiresIn;
|
|
175
|
+
if (label !== undefined)
|
|
176
|
+
body.label = label;
|
|
177
|
+
if (oneTimeUse !== undefined)
|
|
178
|
+
body.one_time_use = oneTimeUse;
|
|
179
|
+
// Explicit 0 means unlimited and must survive body construction.
|
|
180
|
+
if (maxSessions !== undefined)
|
|
181
|
+
body.max_sessions = maxSessions;
|
|
182
|
+
const wireSessionDuration = serializeApiDuration(sessionDuration, "sessionDuration", MIN_SESSION_DURATION_SECONDS);
|
|
183
|
+
if (wireSessionDuration !== undefined)
|
|
184
|
+
body.session_duration = wireSessionDuration;
|
|
185
|
+
if (targetPath !== undefined)
|
|
186
|
+
body.target_path = targetPath;
|
|
187
|
+
if (name !== undefined)
|
|
188
|
+
body.name = name;
|
|
189
|
+
return body;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* A LayerV-protected resource, bound to the client that produced it.
|
|
193
|
+
*
|
|
194
|
+
* Obtained from {@link QURLClient.protectUrl} or {@link QURLClient.resourceByCrid} —
|
|
195
|
+
* not constructed directly. Mint short-lived access links for the resource
|
|
196
|
+
* with {@link createPortal} (qurl-go: `qurl.Resource`).
|
|
197
|
+
*/
|
|
198
|
+
export class ProtectedResource {
|
|
199
|
+
// A true private field (not a module-scope WeakMap registry, which
|
|
200
|
+
// CONTRIBUTING.md's dual-build rule bars) so the binding stays on the
|
|
201
|
+
// instance and out of Object.keys/JSON.stringify output.
|
|
202
|
+
#client;
|
|
203
|
+
/** The public resource CRID. */
|
|
204
|
+
crid;
|
|
205
|
+
/** The private URL protected by this resource, when known. */
|
|
206
|
+
targetUrl;
|
|
207
|
+
/**
|
|
208
|
+
* Full server-populated {@link Resource} metadata when the handle came
|
|
209
|
+
* from an API response; `undefined` for {@link QURLClient.resourceByCrid}
|
|
210
|
+
* handles and for the one-call {@link QURLClient.createPortalForUrl} path.
|
|
211
|
+
*/
|
|
212
|
+
details;
|
|
213
|
+
constructor(client, id, targetUrl, details) {
|
|
214
|
+
this.#client = client;
|
|
215
|
+
this.crid = id;
|
|
216
|
+
this.targetUrl = targetUrl;
|
|
217
|
+
this.details = details;
|
|
218
|
+
copyResponseMetadata(details, this);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Whether `resource` was produced by `client`. A static because the
|
|
222
|
+
* binding lives in a private field that only this class can read; used
|
|
223
|
+
* by {@link QURLClient.createPortal} to refuse handles bound to a
|
|
224
|
+
* different client.
|
|
225
|
+
*/
|
|
226
|
+
static isBoundTo(resource, client) {
|
|
227
|
+
return resource.#client === client;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Mint a short-lived qURL link (a portal) for this resource. See
|
|
231
|
+
* {@link QURLClient.createPortal} for the option reference.
|
|
232
|
+
*/
|
|
233
|
+
async createPortal(opts = {}, requestOptions) {
|
|
234
|
+
return this.#client.createPortal(this, opts, requestOptions);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ConsumerAccessPolicy, CreateInput, CreateQurlForResourceInput, UpdateResourceQurlInput } from "./types.js";
|
|
2
|
+
export declare function validateCreateInput(input: CreateInput): void;
|
|
3
|
+
export declare function validateQurlTokenOptions(input: CreateQurlForResourceInput | undefined): void;
|
|
4
|
+
export declare function validateResourceQurlUpdateInput(input: UpdateResourceQurlInput): void;
|
|
5
|
+
export declare function validateResourceWriteFields(input: Record<string, unknown>, options?: {
|
|
6
|
+
allowAliasClear?: boolean;
|
|
7
|
+
allowCustomDomainClear?: boolean;
|
|
8
|
+
requireUrlTarget?: boolean;
|
|
9
|
+
validateFindOrCreate?: boolean;
|
|
10
|
+
validatePreserveHost?: boolean;
|
|
11
|
+
}): void;
|
|
12
|
+
/**
|
|
13
|
+
* Per-entry shape guard for {@link BatchItemResult}. Verifies every
|
|
14
|
+
* non-optional field on the branch of the discriminated union selected
|
|
15
|
+
* by the `success` discriminant. Protects consumers who narrow on
|
|
16
|
+
* `success` and then access `resource_id` / `qurl_link` / `error` as
|
|
17
|
+
* guaranteed-present — if the API ever omits one, we trip the guard
|
|
18
|
+
* rather than returning `undefined` where the type says `string`.
|
|
19
|
+
*
|
|
20
|
+
* Returns `null` when the entry is valid, or a human-readable reason
|
|
21
|
+
* string describing the *specific* field that's missing or wrong. The
|
|
22
|
+
* reason never echoes the entry's values — only field names and shape
|
|
23
|
+
* descriptors — so the caller can safely surface it in error messages
|
|
24
|
+
* that may end up in observability pipelines. Contrast with the outer
|
|
25
|
+
* guard's intentional silence about response-body content for the
|
|
26
|
+
* same info-leak reason.
|
|
27
|
+
*/
|
|
28
|
+
export declare function batchItemResultValidationReason(entry: unknown): string | null;
|
|
29
|
+
export declare function sameConsumerPolicy(actual: ConsumerAccessPolicy | undefined, expected: ConsumerAccessPolicy, foundExisting: boolean): boolean;
|