@fargowealth/fargo-ai-cli 1.4.1 → 1.5.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/CHANGELOG.md +57 -0
- package/README.md +235 -52
- package/THIRD_PARTY_NOTICES.md +28 -0
- package/package.json +6 -6
- package/src/cli.mjs +58 -7
- package/src/constants.mjs +1 -1
- package/src/loopback-receiver.mjs +2 -1
- package/src/mcp-client.mjs +3 -1
- package/src/mcp-proxy.mjs +15 -1
- package/src/upload-transfer.mjs +422 -0
- package/src/yida-auth.mjs +309 -0
- package/src/yida-bundle-format.mjs +77 -0
- package/src/yida-business.mjs +128 -0
- package/src/yida-cancellation.mjs +121 -0
- package/src/yida-cli.mjs +65 -0
- package/src/yida-confirmation.mjs +86 -0
- package/src/yida-expense-contract.mjs +86 -0
- package/src/yida-expense.mjs +261 -0
- package/src/yida-lookup-plan.mjs +59 -0
- package/src/yida-operation.mjs +64 -0
- package/src/yida-rules.mjs +101 -0
- package/src/yida-runtime.mjs +63 -0
- package/src/yida-schema.mjs +111 -0
- package/src/yida-task.mjs +38 -0
- package/src/yida-tools.mjs +91 -0
- package/src/yida-workflow-contract.mjs +18 -0
- package/node_modules/@fargowealth/website-connector-runtime/README.md +0 -15
- package/node_modules/@fargowealth/website-connector-runtime/package.json +0 -24
- package/node_modules/@fargowealth/website-connector-runtime/src/index.d.mts +0 -136
- package/node_modules/@fargowealth/website-connector-runtime/src/index.mjs +0 -825
- package/src/website-browser-profile.mjs +0 -389
- package/src/website-cli.mjs +0 -365
- package/src/website-connector-definition.d.mts +0 -1
- package/src/website-connector-definition.mjs +0 -1
|
@@ -1,825 +0,0 @@
|
|
|
1
|
-
const CONNECTOR_ID = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
|
|
2
|
-
const OPERATION_ID = /^[a-z][a-z0-9_]{1,62}[a-z0-9]$/;
|
|
3
|
-
const TEMPLATE_ARGUMENT = /\{\{([a-zA-Z][a-zA-Z0-9_]*)\}\}/g;
|
|
4
|
-
const RUNTIME_CAPABILITY = /^[a-z][a-z0-9.-]{1,80}$/;
|
|
5
|
-
const SECRET_SOURCE_ID = /^[a-z][a-z0-9_]{1,62}[a-z0-9]$/;
|
|
6
|
-
const AWS_REGION = /^[a-z]{2}(?:-gov)?-[a-z]+-\d$/;
|
|
7
|
-
const COGNITO_CLIENT_ID = /^[A-Za-z0-9+]{1,128}$/;
|
|
8
|
-
const COGNITO_USER_POOL_ID = /^[a-z]{2}(?:-gov)?-[a-z]+-\d_[A-Za-z0-9]+$/;
|
|
9
|
-
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
10
|
-
const MAX_RESPONSE_BYTES = 1_048_576;
|
|
11
|
-
const DEFAULT_REFRESH_LEEWAY_SECONDS = 300;
|
|
12
|
-
const FORBIDDEN_HEADERS = new Set([
|
|
13
|
-
"connection",
|
|
14
|
-
"content-length",
|
|
15
|
-
"cookie",
|
|
16
|
-
"host",
|
|
17
|
-
"proxy-authorization",
|
|
18
|
-
"transfer-encoding",
|
|
19
|
-
"upgrade"
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
export const WEBSITE_CONNECTOR_SCHEMA_VERSION = "fargo.website-connector/v1";
|
|
23
|
-
export const WEBSITE_CONNECTOR_RUNTIME_VERSION = "1.1.0";
|
|
24
|
-
export const WEBSITE_CONNECTOR_RUNTIME_CAPABILITIES = Object.freeze([
|
|
25
|
-
"auth.browser-storage",
|
|
26
|
-
"auth.refresh.aws-cognito",
|
|
27
|
-
"request.http",
|
|
28
|
-
"request.json-body",
|
|
29
|
-
"request.write",
|
|
30
|
-
"response.auto",
|
|
31
|
-
"response.json",
|
|
32
|
-
"response.text"
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
|
-
export class WebsiteConnectorDefinitionError extends Error {
|
|
36
|
-
constructor(code, path = "") {
|
|
37
|
-
super(path ? `${code}:${path}` : code);
|
|
38
|
-
this.name = "WebsiteConnectorDefinitionError";
|
|
39
|
-
this.code = code;
|
|
40
|
-
this.path = path;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export class WebsiteConnectorRuntimeError extends Error {
|
|
45
|
-
constructor(status, code, details) {
|
|
46
|
-
super(code);
|
|
47
|
-
this.name = "WebsiteConnectorRuntimeError";
|
|
48
|
-
this.status = status;
|
|
49
|
-
this.code = code;
|
|
50
|
-
this.details = details;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function parseWebsiteConnectorDefinition(value) {
|
|
55
|
-
const source = record(value, "website_connector_definition_invalid", "/");
|
|
56
|
-
equal(source.schemaVersion, WEBSITE_CONNECTOR_SCHEMA_VERSION, "website_connector_schema_version_invalid", "/schemaVersion");
|
|
57
|
-
const connectorId = requiredText(source.connectorId, 64, "website_connector_id_invalid", "/connectorId");
|
|
58
|
-
if (!CONNECTOR_ID.test(connectorId)) fail("website_connector_id_invalid", "/connectorId");
|
|
59
|
-
const displayName = requiredText(source.displayName, 120, "website_connector_display_name_invalid", "/displayName");
|
|
60
|
-
const allowedOrigins = uniqueArray(source.allowedOrigins, 1, 12, "/allowedOrigins").map((origin, index) => (
|
|
61
|
-
exactHttpsOrigin(origin, "website_connector_origin_invalid", `/allowedOrigins/${index}`)
|
|
62
|
-
));
|
|
63
|
-
const loginUrl = optionalHttpsUrl(source.loginUrl, allowedOrigins, "/loginUrl");
|
|
64
|
-
const auth = parseAuthRecipe(source.auth, allowedOrigins);
|
|
65
|
-
const runtime = parseRuntimeRequirements(source.runtime);
|
|
66
|
-
if (auth.kind === "browser_storage" && !loginUrl) fail("website_connector_login_url_required", "/loginUrl");
|
|
67
|
-
const operations = uniqueArray(source.operations, 1, 80, "/operations").map((operation, index) => (
|
|
68
|
-
parseOperation(operation, allowedOrigins, index)
|
|
69
|
-
));
|
|
70
|
-
const operationIds = new Set();
|
|
71
|
-
for (const operation of operations) {
|
|
72
|
-
if (operationIds.has(operation.operationId)) fail("website_operation_id_duplicate", `/operations/${operation.operationId}`);
|
|
73
|
-
if (operation.requiresConnection && auth.kind !== "browser_storage") {
|
|
74
|
-
fail("website_operation_connection_auth_required", `/operations/${operation.operationId}/requiresConnection`);
|
|
75
|
-
}
|
|
76
|
-
operationIds.add(operation.operationId);
|
|
77
|
-
}
|
|
78
|
-
return {
|
|
79
|
-
schemaVersion: WEBSITE_CONNECTOR_SCHEMA_VERSION,
|
|
80
|
-
connectorId,
|
|
81
|
-
displayName,
|
|
82
|
-
allowedOrigins,
|
|
83
|
-
...(loginUrl ? { loginUrl } : {}),
|
|
84
|
-
...(runtime ? { runtime } : {}),
|
|
85
|
-
auth,
|
|
86
|
-
operations
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function connectorToolName(connectorId, operationId) {
|
|
91
|
-
const left = String(connectorId || "").replaceAll("-", "_");
|
|
92
|
-
return `${left}_${operationId}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function renderWebsiteOperation(definition, operationId, rawArguments) {
|
|
96
|
-
const parsed = parseWebsiteConnectorDefinition(definition);
|
|
97
|
-
const operation = parsed.operations.find((candidate) => candidate.operationId === operationId);
|
|
98
|
-
if (!operation) fail("website_operation_not_found", "/operationId");
|
|
99
|
-
const args = record(rawArguments ?? {}, "website_operation_arguments_invalid", "/arguments");
|
|
100
|
-
validateArguments(args, operation.inputSchema);
|
|
101
|
-
const path = renderString(operation.request.path, args, "/request/path");
|
|
102
|
-
const url = new URL(path, `${operation.request.origin}/`);
|
|
103
|
-
if (url.origin !== operation.request.origin) fail("website_operation_origin_escape", "/request/path");
|
|
104
|
-
for (const [name, argumentName] of Object.entries(operation.request.query)) {
|
|
105
|
-
const value = args[argumentName];
|
|
106
|
-
if (value !== undefined && value !== null) url.searchParams.set(name, scalarText(value, `/arguments/${argumentName}`));
|
|
107
|
-
}
|
|
108
|
-
const body = operation.request.body === undefined
|
|
109
|
-
? undefined
|
|
110
|
-
: renderBody(operation.request.body, args, "/request/body");
|
|
111
|
-
return {
|
|
112
|
-
definition: parsed,
|
|
113
|
-
operation,
|
|
114
|
-
request: {
|
|
115
|
-
body,
|
|
116
|
-
headers: { ...operation.request.headers },
|
|
117
|
-
method: operation.request.method,
|
|
118
|
-
responseMode: operation.request.responseMode,
|
|
119
|
-
url: url.toString()
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function websiteConnectorRuntimeDescriptor() {
|
|
125
|
-
return {
|
|
126
|
-
capabilities: [...WEBSITE_CONNECTOR_RUNTIME_CAPABILITIES],
|
|
127
|
-
runtimeVersion: WEBSITE_CONNECTOR_RUNTIME_VERSION
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function requiredWebsiteConnectorCapabilities(definition) {
|
|
132
|
-
const parsed = parseWebsiteConnectorDefinition(definition);
|
|
133
|
-
const required = new Set(["request.http"]);
|
|
134
|
-
if (parsed.auth.kind === "browser_storage") required.add("auth.browser-storage");
|
|
135
|
-
if (parsed.auth.kind === "browser_storage" && parsed.auth.refresh?.kind === "aws_cognito") {
|
|
136
|
-
required.add("auth.refresh.aws-cognito");
|
|
137
|
-
}
|
|
138
|
-
for (const operation of parsed.operations) {
|
|
139
|
-
required.add(`response.${operation.request.responseMode}`);
|
|
140
|
-
if (operation.request.body !== undefined) required.add("request.json-body");
|
|
141
|
-
if (operation.request.method !== "GET") required.add("request.write");
|
|
142
|
-
}
|
|
143
|
-
for (const capability of parsed.runtime?.requiredCapabilities ?? []) required.add(capability);
|
|
144
|
-
return [...required].sort();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function assertWebsiteConnectorRuntimeCompatibility(definition, descriptor = websiteConnectorRuntimeDescriptor()) {
|
|
148
|
-
const parsed = parseWebsiteConnectorDefinition(definition);
|
|
149
|
-
const version = String(descriptor?.runtimeVersion || "");
|
|
150
|
-
const capabilities = new Set(Array.isArray(descriptor?.capabilities) ? descriptor.capabilities : []);
|
|
151
|
-
if (!SEMVER.test(version)) throw new WebsiteConnectorRuntimeError(500, "website_runtime_descriptor_invalid");
|
|
152
|
-
if (parsed.runtime?.minimumVersion && compareSemver(version, parsed.runtime.minimumVersion) < 0) {
|
|
153
|
-
throw new WebsiteConnectorRuntimeError(409, "website_runtime_version_incompatible", {
|
|
154
|
-
installedVersion: version,
|
|
155
|
-
minimumVersion: parsed.runtime.minimumVersion
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
const missingCapabilities = requiredWebsiteConnectorCapabilities(parsed).filter((capability) => !capabilities.has(capability));
|
|
159
|
-
if (missingCapabilities.length > 0) {
|
|
160
|
-
throw new WebsiteConnectorRuntimeError(409, "website_runtime_capability_missing", { missingCapabilities });
|
|
161
|
-
}
|
|
162
|
-
return { definition: parsed, descriptor: { capabilities: [...capabilities].sort(), runtimeVersion: version } };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export async function executeWebsiteConnectorOperation(definition, operationId, args, options = {}) {
|
|
166
|
-
assertWebsiteConnectorRuntimeCompatibility(definition, options.runtimeDescriptor);
|
|
167
|
-
let rendered;
|
|
168
|
-
try {
|
|
169
|
-
rendered = renderWebsiteOperation(definition, operationId, args);
|
|
170
|
-
} catch (error) {
|
|
171
|
-
const code = error instanceof WebsiteConnectorDefinitionError ? error.code : "website_operation_invalid";
|
|
172
|
-
throw new WebsiteConnectorRuntimeError(code === "website_operation_not_found" ? 404 : 400, code);
|
|
173
|
-
}
|
|
174
|
-
if (options.readOnlyOnly && !rendered.operation.readOnly) {
|
|
175
|
-
throw new WebsiteConnectorRuntimeError(409, "website_operation_not_read_only");
|
|
176
|
-
}
|
|
177
|
-
if (rendered.operation.requiresConnection && !options.storageState) {
|
|
178
|
-
throw new WebsiteConnectorRuntimeError(409, "browser_connection_required");
|
|
179
|
-
}
|
|
180
|
-
const headers = new Headers(rendered.request.headers);
|
|
181
|
-
if (rendered.operation.requiresConnection) {
|
|
182
|
-
applyBrowserAuthentication(headers, rendered.definition.auth, options.storageState, rendered.request.url);
|
|
183
|
-
}
|
|
184
|
-
let body;
|
|
185
|
-
if (rendered.request.body !== undefined) {
|
|
186
|
-
body = JSON.stringify(rendered.request.body);
|
|
187
|
-
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
188
|
-
}
|
|
189
|
-
let response;
|
|
190
|
-
try {
|
|
191
|
-
response = await (options.fetchImpl ?? fetch)(rendered.request.url, {
|
|
192
|
-
body,
|
|
193
|
-
headers,
|
|
194
|
-
method: rendered.request.method,
|
|
195
|
-
redirect: "manual",
|
|
196
|
-
signal: AbortSignal.timeout(options.timeoutMs ?? 30_000)
|
|
197
|
-
});
|
|
198
|
-
} catch {
|
|
199
|
-
throw new WebsiteConnectorRuntimeError(502, "website_operation_upstream_unavailable");
|
|
200
|
-
}
|
|
201
|
-
if (response.status >= 300 && response.status < 400) {
|
|
202
|
-
throw new WebsiteConnectorRuntimeError(409, "website_operation_redirect_forbidden");
|
|
203
|
-
}
|
|
204
|
-
if (response.status === 401 || response.status === 403) {
|
|
205
|
-
throw new WebsiteConnectorRuntimeError(409, "browser_connection_needs_reauth");
|
|
206
|
-
}
|
|
207
|
-
const raw = await readLimitedBody(response);
|
|
208
|
-
const contentType = response.headers.get("content-type")?.toLowerCase() || "";
|
|
209
|
-
let data = raw;
|
|
210
|
-
const parseJson = rendered.request.responseMode === "json"
|
|
211
|
-
|| (rendered.request.responseMode === "auto" && contentType.includes("json"));
|
|
212
|
-
if (parseJson && raw) {
|
|
213
|
-
try { data = JSON.parse(raw); }
|
|
214
|
-
catch { throw new WebsiteConnectorRuntimeError(502, "website_operation_upstream_json_invalid"); }
|
|
215
|
-
}
|
|
216
|
-
const result = {
|
|
217
|
-
connectorId: rendered.definition.connectorId,
|
|
218
|
-
data,
|
|
219
|
-
fetchedAt: new Date().toISOString(),
|
|
220
|
-
ok: response.ok,
|
|
221
|
-
operationId: rendered.operation.operationId,
|
|
222
|
-
runtime: websiteConnectorRuntimeDescriptor(),
|
|
223
|
-
status: response.status
|
|
224
|
-
};
|
|
225
|
-
return options.storageState ? sanitizeWebsiteConnectorOutput(result, options.storageState) : result;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function browserStorageAuthenticated(state, auth, now = Date.now()) {
|
|
229
|
-
if (auth.kind !== "browser_storage") return true;
|
|
230
|
-
const seconds = now / 1000;
|
|
231
|
-
const hasCookie = !auth.sendCookies || state.cookies.some((cookie) => cookie.expires === undefined || cookie.expires < 0 || cookie.expires > seconds);
|
|
232
|
-
const hasHeaderValues = auth.headerSources.every((source) => storageValue(state, source.origin, source.keySuffix) !== undefined);
|
|
233
|
-
const headersUnexpired = auth.headerSources.every((source) => {
|
|
234
|
-
const value = storageValue(state, source.origin, source.keySuffix);
|
|
235
|
-
return value !== undefined && credentialUnexpired(value, seconds);
|
|
236
|
-
});
|
|
237
|
-
const hasRefreshSecrets = !auth.refresh || auth.secretSources.every((source) => (
|
|
238
|
-
storageValue(state, source.origin, source.keySuffix) !== undefined
|
|
239
|
-
));
|
|
240
|
-
return hasCookie && hasHeaderValues && hasRefreshSecrets && (headersUnexpired || Boolean(auth.refresh));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export async function refreshWebsiteConnectorAuthentication(definition, storageState, options = {}) {
|
|
244
|
-
const parsed = parseWebsiteConnectorDefinition(definition);
|
|
245
|
-
if (parsed.auth.kind !== "browser_storage") {
|
|
246
|
-
return { refreshed: false, storageState };
|
|
247
|
-
}
|
|
248
|
-
const auth = parsed.auth;
|
|
249
|
-
const now = options.now?.() ?? Date.now();
|
|
250
|
-
const leewaySeconds = auth.refresh?.leewaySeconds ?? DEFAULT_REFRESH_LEEWAY_SECONDS;
|
|
251
|
-
const shouldRefresh = auth.refresh
|
|
252
|
-
? !cognitoStoredTokenValid(storageState, auth.refresh.idTokenSource, auth.refresh, "id", now, leewaySeconds)
|
|
253
|
-
|| !cognitoStoredTokenValid(storageState, auth.refresh.accessTokenSource, auth.refresh, "access", now, leewaySeconds)
|
|
254
|
-
: auth.headerSources.some((source) => {
|
|
255
|
-
const value = storageValue(storageState, source.origin, source.keySuffix);
|
|
256
|
-
return value === undefined || !credentialUnexpired(value, now / 1000, leewaySeconds);
|
|
257
|
-
});
|
|
258
|
-
if (!shouldRefresh) return { refreshed: false, storageState };
|
|
259
|
-
if (!auth.refresh) throw new WebsiteConnectorRuntimeError(409, "browser_connection_needs_reauth");
|
|
260
|
-
return refreshAwsCognitoAuthentication(auth, storageState, { ...options, now });
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export function sanitizeWebsiteConnectorOutput(value, storageState) {
|
|
264
|
-
const secrets = [
|
|
265
|
-
...array(storageState?.cookies).map((cookie) => String(cookie?.value || "")),
|
|
266
|
-
...array(storageState?.origins).flatMap((origin) => array(origin?.localStorage).map((item) => String(item?.value || "")))
|
|
267
|
-
].filter((item) => item.length >= 4);
|
|
268
|
-
return sanitizeOutputValue(value, secrets, 0);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function parseRuntimeRequirements(value) {
|
|
272
|
-
if (value === undefined || value === null) return undefined;
|
|
273
|
-
const source = record(value, "website_runtime_requirements_invalid", "/runtime");
|
|
274
|
-
const minimumVersion = requiredText(source.minimumVersion, 40, "website_runtime_version_invalid", "/runtime/minimumVersion");
|
|
275
|
-
if (!SEMVER.test(minimumVersion)) fail("website_runtime_version_invalid", "/runtime/minimumVersion");
|
|
276
|
-
const requiredCapabilities = uniqueArray(source.requiredCapabilities ?? [], 0, 32, "/runtime/requiredCapabilities")
|
|
277
|
-
.map((item, index) => {
|
|
278
|
-
const capability = requiredText(item, 80, "website_runtime_capability_invalid", `/runtime/requiredCapabilities/${index}`);
|
|
279
|
-
if (!RUNTIME_CAPABILITY.test(capability)) fail("website_runtime_capability_invalid", `/runtime/requiredCapabilities/${index}`);
|
|
280
|
-
return capability;
|
|
281
|
-
}).sort();
|
|
282
|
-
return { minimumVersion, requiredCapabilities };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function parseAuthRecipe(value, allowedOrigins) {
|
|
286
|
-
const source = value === undefined ? { kind: "none" } : record(value, "website_auth_recipe_invalid", "/auth");
|
|
287
|
-
if (source.kind === "none") return { kind: "none" };
|
|
288
|
-
equal(source.kind, "browser_storage", "website_auth_recipe_invalid", "/auth/kind");
|
|
289
|
-
const sendCookies = source.sendCookies === true;
|
|
290
|
-
const headerSources = source.headerSources === undefined
|
|
291
|
-
? []
|
|
292
|
-
: uniqueArray(source.headerSources, 0, 12, "/auth/headerSources").map((item, index) => {
|
|
293
|
-
const binding = record(item, "website_auth_header_source_invalid", `/auth/headerSources/${index}`);
|
|
294
|
-
const name = headerName(binding.name, `/auth/headerSources/${index}/name`, true);
|
|
295
|
-
const origin = exactHttpsOrigin(binding.origin, "website_auth_header_source_invalid", `/auth/headerSources/${index}/origin`);
|
|
296
|
-
if (!allowedOrigins.includes(origin)) fail("website_auth_origin_not_allowed", `/auth/headerSources/${index}/origin`);
|
|
297
|
-
const keySuffix = requiredText(binding.keySuffix, 240, "website_auth_header_source_invalid", `/auth/headerSources/${index}/keySuffix`);
|
|
298
|
-
const prefix = optionalHeaderPrefix(binding.prefix, `/auth/headerSources/${index}/prefix`);
|
|
299
|
-
return { keySuffix, name, origin, ...(prefix ? { prefix } : {}) };
|
|
300
|
-
});
|
|
301
|
-
const secretSources = source.secretSources === undefined
|
|
302
|
-
? []
|
|
303
|
-
: uniqueArray(source.secretSources, 0, 12, "/auth/secretSources").map((item, index) => {
|
|
304
|
-
const binding = record(item, "website_auth_secret_source_invalid", `/auth/secretSources/${index}`);
|
|
305
|
-
const id = requiredText(binding.id, 64, "website_auth_secret_source_invalid", `/auth/secretSources/${index}/id`);
|
|
306
|
-
if (!SECRET_SOURCE_ID.test(id)) fail("website_auth_secret_source_invalid", `/auth/secretSources/${index}/id`);
|
|
307
|
-
const origin = exactHttpsOrigin(binding.origin, "website_auth_secret_source_invalid", `/auth/secretSources/${index}/origin`);
|
|
308
|
-
if (!allowedOrigins.includes(origin)) fail("website_auth_origin_not_allowed", `/auth/secretSources/${index}/origin`);
|
|
309
|
-
const keySuffix = requiredText(binding.keySuffix, 240, "website_auth_secret_source_invalid", `/auth/secretSources/${index}/keySuffix`);
|
|
310
|
-
return { id, keySuffix, origin };
|
|
311
|
-
});
|
|
312
|
-
if (new Set(secretSources.map((item) => item.id)).size !== secretSources.length) {
|
|
313
|
-
fail("website_auth_secret_source_duplicate", "/auth/secretSources");
|
|
314
|
-
}
|
|
315
|
-
const refresh = parseRefreshRecipe(source.refresh, { headerSources, secretSources });
|
|
316
|
-
if (!refresh && secretSources.length > 0) fail("website_auth_secret_source_unused", "/auth/secretSources");
|
|
317
|
-
if (refresh && secretSources.some((item) => item.id !== refresh.refreshTokenSource)) {
|
|
318
|
-
fail("website_auth_secret_source_unused", "/auth/secretSources");
|
|
319
|
-
}
|
|
320
|
-
if (!sendCookies && headerSources.length === 0) fail("website_auth_recipe_empty", "/auth");
|
|
321
|
-
return {
|
|
322
|
-
kind: "browser_storage",
|
|
323
|
-
sendCookies,
|
|
324
|
-
headerSources,
|
|
325
|
-
secretSources,
|
|
326
|
-
...(refresh ? { refresh } : {})
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function parseRefreshRecipe(value, sources) {
|
|
331
|
-
if (value === undefined || value === null) return undefined;
|
|
332
|
-
const source = record(value, "website_auth_refresh_invalid", "/auth/refresh");
|
|
333
|
-
equal(source.kind, "aws_cognito", "website_auth_refresh_invalid", "/auth/refresh/kind");
|
|
334
|
-
const clientId = requiredText(source.clientId, 128, "website_auth_refresh_invalid", "/auth/refresh/clientId");
|
|
335
|
-
if (!COGNITO_CLIENT_ID.test(clientId)) fail("website_auth_refresh_invalid", "/auth/refresh/clientId");
|
|
336
|
-
const region = requiredText(source.region, 40, "website_auth_refresh_invalid", "/auth/refresh/region");
|
|
337
|
-
if (!AWS_REGION.test(region)) fail("website_auth_refresh_invalid", "/auth/refresh/region");
|
|
338
|
-
const userPoolId = requiredText(source.userPoolId, 128, "website_auth_refresh_invalid", "/auth/refresh/userPoolId");
|
|
339
|
-
if (!COGNITO_USER_POOL_ID.test(userPoolId) || !userPoolId.startsWith(`${region}_`)) {
|
|
340
|
-
fail("website_auth_refresh_invalid", "/auth/refresh/userPoolId");
|
|
341
|
-
}
|
|
342
|
-
const refreshTokenSource = requiredText(source.refreshTokenSource, 64, "website_auth_refresh_invalid", "/auth/refresh/refreshTokenSource");
|
|
343
|
-
const secret = sources.secretSources.find((candidate) => candidate.id === refreshTokenSource);
|
|
344
|
-
if (!secret) fail("website_auth_refresh_source_unknown", "/auth/refresh/refreshTokenSource");
|
|
345
|
-
const idTokenSource = parseRefreshTokenTarget(source.idTokenSource, sources.headerSources, "/auth/refresh/idTokenSource");
|
|
346
|
-
const accessTokenSource = parseRefreshTokenTarget(source.accessTokenSource, sources.headerSources, "/auth/refresh/accessTokenSource");
|
|
347
|
-
const leewaySeconds = source.leewaySeconds === undefined
|
|
348
|
-
? DEFAULT_REFRESH_LEEWAY_SECONDS
|
|
349
|
-
: boundedInteger(source.leewaySeconds, 30, 3_600, "/auth/refresh/leewaySeconds");
|
|
350
|
-
return {
|
|
351
|
-
kind: "aws_cognito",
|
|
352
|
-
clientId,
|
|
353
|
-
region,
|
|
354
|
-
userPoolId,
|
|
355
|
-
refreshTokenSource,
|
|
356
|
-
idTokenSource,
|
|
357
|
-
accessTokenSource,
|
|
358
|
-
leewaySeconds
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function parseRefreshTokenTarget(value, headerSources, path) {
|
|
363
|
-
const source = record(value, "website_auth_refresh_invalid", path);
|
|
364
|
-
const origin = exactHttpsOrigin(source.origin, "website_auth_refresh_invalid", `${path}/origin`);
|
|
365
|
-
const keySuffix = requiredText(source.keySuffix, 240, "website_auth_refresh_invalid", `${path}/keySuffix`);
|
|
366
|
-
if (!headerSources.some((candidate) => candidate.origin === origin && candidate.keySuffix === keySuffix)) {
|
|
367
|
-
fail("website_auth_refresh_target_unknown", path);
|
|
368
|
-
}
|
|
369
|
-
return { keySuffix, origin };
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function parseOperation(value, allowedOrigins, index) {
|
|
373
|
-
const path = `/operations/${index}`;
|
|
374
|
-
const source = record(value, "website_operation_invalid", path);
|
|
375
|
-
const operationId = requiredText(source.operationId, 64, "website_operation_id_invalid", `${path}/operationId`);
|
|
376
|
-
if (!OPERATION_ID.test(operationId)) fail("website_operation_id_invalid", `${path}/operationId`);
|
|
377
|
-
const description = requiredText(source.description, 500, "website_operation_description_invalid", `${path}/description`);
|
|
378
|
-
const readOnly = source.readOnly === true;
|
|
379
|
-
const requiresConnection = source.requiresConnection === true;
|
|
380
|
-
const riskLevel = optionalEnum(source.riskLevel, ["low", "medium", "high", "critical"], readOnly ? "low" : "medium", `${path}/riskLevel`);
|
|
381
|
-
const inputSchema = parseInputSchema(source.inputSchema, `${path}/inputSchema`);
|
|
382
|
-
const requestSource = record(source.request, "website_operation_request_invalid", `${path}/request`);
|
|
383
|
-
const origin = exactHttpsOrigin(requestSource.origin, "website_operation_origin_invalid", `${path}/request/origin`);
|
|
384
|
-
if (!allowedOrigins.includes(origin)) fail("website_operation_origin_not_allowed", `${path}/request/origin`);
|
|
385
|
-
const method = optionalEnum(requestSource.method, ["GET", "POST", "PUT", "PATCH", "DELETE"], "GET", `${path}/request/method`);
|
|
386
|
-
const responseMode = optionalEnum(requestSource.responseMode, ["auto", "json", "text"], "auto", `${path}/request/responseMode`);
|
|
387
|
-
if (readOnly && method !== "GET") fail("website_operation_read_only_method_invalid", `${path}/request/method`);
|
|
388
|
-
const requestPath = requiredText(requestSource.path, 2_000, "website_operation_path_invalid", `${path}/request/path`);
|
|
389
|
-
if (!requestPath.startsWith("/") || requestPath.startsWith("//") || requestPath.includes("\\")) {
|
|
390
|
-
fail("website_operation_path_invalid", `${path}/request/path`);
|
|
391
|
-
}
|
|
392
|
-
const pathArguments = [...requestPath.matchAll(TEMPLATE_ARGUMENT)].map((match) => match[1]);
|
|
393
|
-
const properties = record(inputSchema.properties ?? {}, "website_operation_input_schema_invalid", `${path}/inputSchema/properties`);
|
|
394
|
-
for (const argument of pathArguments) {
|
|
395
|
-
if (!Object.hasOwn(properties, argument)) fail("website_operation_template_argument_unknown", `${path}/request/path`);
|
|
396
|
-
}
|
|
397
|
-
const query = requestSource.query === undefined
|
|
398
|
-
? {}
|
|
399
|
-
: Object.fromEntries(Object.entries(record(requestSource.query, "website_operation_query_invalid", `${path}/request/query`)).map(([name, argument]) => {
|
|
400
|
-
if (!/^[A-Za-z0-9_.~-]{1,120}$/.test(name)) fail("website_operation_query_name_invalid", `${path}/request/query/${name}`);
|
|
401
|
-
const argumentName = requiredText(argument, 120, "website_operation_query_argument_invalid", `${path}/request/query/${name}`);
|
|
402
|
-
if (!Object.hasOwn(properties, argumentName)) fail("website_operation_query_argument_unknown", `${path}/request/query/${name}`);
|
|
403
|
-
return [name, argumentName];
|
|
404
|
-
}));
|
|
405
|
-
const headers = requestSource.headers === undefined
|
|
406
|
-
? {}
|
|
407
|
-
: Object.fromEntries(Object.entries(record(requestSource.headers, "website_operation_headers_invalid", `${path}/request/headers`)).map(([name, headerValue]) => {
|
|
408
|
-
const normalized = requiredText(headerValue, 500, "website_operation_header_value_invalid", `${path}/request/headers/${name}`);
|
|
409
|
-
if (/[\r\n\0]/.test(normalized)) fail("website_operation_header_value_invalid", `${path}/request/headers/${name}`);
|
|
410
|
-
return [headerName(name, `${path}/request/headers/${name}`, false), normalized];
|
|
411
|
-
}));
|
|
412
|
-
const body = requestSource.body;
|
|
413
|
-
if (body !== undefined && method === "GET") fail("website_operation_get_body_invalid", `${path}/request/body`);
|
|
414
|
-
if (body !== undefined) validateBodyTemplate(body, properties, `${path}/request/body`);
|
|
415
|
-
return {
|
|
416
|
-
operationId,
|
|
417
|
-
description,
|
|
418
|
-
readOnly,
|
|
419
|
-
requiresConnection,
|
|
420
|
-
riskLevel,
|
|
421
|
-
inputSchema,
|
|
422
|
-
request: {
|
|
423
|
-
origin,
|
|
424
|
-
method,
|
|
425
|
-
path: requestPath,
|
|
426
|
-
query,
|
|
427
|
-
headers,
|
|
428
|
-
responseMode,
|
|
429
|
-
...(body === undefined ? {} : { body })
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function optionalHeaderPrefix(value, path) {
|
|
435
|
-
if (value === undefined || value === null || value === "") return "";
|
|
436
|
-
if (typeof value !== "string" || value.length > 80 || /[\r\n\0]/.test(value)) {
|
|
437
|
-
fail("website_auth_header_source_invalid", path);
|
|
438
|
-
}
|
|
439
|
-
return value;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function parseInputSchema(value, path) {
|
|
443
|
-
const source = value === undefined
|
|
444
|
-
? { additionalProperties: false, properties: {}, type: "object" }
|
|
445
|
-
: record(value, "website_operation_input_schema_invalid", path);
|
|
446
|
-
equal(source.type, "object", "website_operation_input_schema_invalid", `${path}/type`);
|
|
447
|
-
if (source.additionalProperties !== false) fail("website_operation_input_schema_open", `${path}/additionalProperties`);
|
|
448
|
-
const properties = record(source.properties ?? {}, "website_operation_input_schema_invalid", `${path}/properties`);
|
|
449
|
-
const normalizedProperties = {};
|
|
450
|
-
for (const [name, candidate] of Object.entries(properties)) {
|
|
451
|
-
if (!/^[A-Za-z][A-Za-z0-9_]{0,119}$/.test(name)) fail("website_operation_input_name_invalid", `${path}/properties/${name}`);
|
|
452
|
-
const property = record(candidate, "website_operation_input_property_invalid", `${path}/properties/${name}`);
|
|
453
|
-
const type = optionalEnum(property.type, ["string", "number", "integer", "boolean"], "string", `${path}/properties/${name}/type`);
|
|
454
|
-
normalizedProperties[name] = {
|
|
455
|
-
type,
|
|
456
|
-
...(typeof property.maxLength === "number" ? { maxLength: boundedInteger(property.maxLength, 1, 20_000, `${path}/properties/${name}/maxLength`) } : {}),
|
|
457
|
-
...(Array.isArray(property.enum) ? { enum: property.enum.map((item) => scalar(item, `${path}/properties/${name}/enum`)) } : {})
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
const required = source.required === undefined ? [] : uniqueArray(source.required, 0, Object.keys(normalizedProperties).length, `${path}/required`).map(String);
|
|
461
|
-
for (const name of required) if (!Object.hasOwn(normalizedProperties, name)) fail("website_operation_required_unknown", `${path}/required`);
|
|
462
|
-
return { additionalProperties: false, properties: normalizedProperties, required, type: "object" };
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function validateArguments(args, schema) {
|
|
466
|
-
for (const required of schema.required) if (!Object.hasOwn(args, required)) fail("website_operation_argument_required", `/arguments/${required}`);
|
|
467
|
-
for (const [name, value] of Object.entries(args)) {
|
|
468
|
-
const property = schema.properties[name];
|
|
469
|
-
if (!property) fail("website_operation_argument_unknown", `/arguments/${name}`);
|
|
470
|
-
if (property.type === "string" && typeof value !== "string") fail("website_operation_argument_type_invalid", `/arguments/${name}`);
|
|
471
|
-
if (property.type === "number" && (typeof value !== "number" || !Number.isFinite(value))) fail("website_operation_argument_type_invalid", `/arguments/${name}`);
|
|
472
|
-
if (property.type === "integer" && !Number.isInteger(value)) fail("website_operation_argument_type_invalid", `/arguments/${name}`);
|
|
473
|
-
if (property.type === "boolean" && typeof value !== "boolean") fail("website_operation_argument_type_invalid", `/arguments/${name}`);
|
|
474
|
-
if (property.maxLength && typeof value === "string" && value.length > property.maxLength) fail("website_operation_argument_too_long", `/arguments/${name}`);
|
|
475
|
-
if (property.enum && !property.enum.some((candidate) => Object.is(candidate, value))) fail("website_operation_argument_enum_invalid", `/arguments/${name}`);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function validateBodyTemplate(value, properties, path) {
|
|
480
|
-
if (typeof value === "string") {
|
|
481
|
-
if (value.startsWith("$") && !Object.hasOwn(properties, value.slice(1))) fail("website_operation_body_argument_unknown", path);
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
if (value === null || typeof value === "number" || typeof value === "boolean") return;
|
|
485
|
-
if (Array.isArray(value)) {
|
|
486
|
-
value.forEach((item, index) => validateBodyTemplate(item, properties, `${path}/${index}`));
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
const source = record(value, "website_operation_body_invalid", path);
|
|
490
|
-
for (const [name, item] of Object.entries(source)) validateBodyTemplate(item, properties, `${path}/${name}`);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function renderBody(value, args, path) {
|
|
494
|
-
if (typeof value === "string") {
|
|
495
|
-
if (value.startsWith("$")) return args[value.slice(1)];
|
|
496
|
-
return renderString(value, args, path);
|
|
497
|
-
}
|
|
498
|
-
if (Array.isArray(value)) return value.map((item, index) => renderBody(item, args, `${path}/${index}`));
|
|
499
|
-
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([name, item]) => [name, renderBody(item, args, `${path}/${name}`)]));
|
|
500
|
-
return value;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function renderString(value, args, path) {
|
|
504
|
-
return String(value).replace(TEMPLATE_ARGUMENT, (_match, name) => encodeURIComponent(scalarText(args[name], `/arguments/${name || path}`)));
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function scalarText(value, path) {
|
|
508
|
-
scalar(value, path);
|
|
509
|
-
return String(value);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function scalar(value, path) {
|
|
513
|
-
if (!["string", "number", "boolean"].includes(typeof value) || (typeof value === "number" && !Number.isFinite(value))) {
|
|
514
|
-
fail("website_operation_argument_scalar_required", path);
|
|
515
|
-
}
|
|
516
|
-
return value;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function headerName(value, path, allowAuthorization) {
|
|
520
|
-
const name = requiredText(value, 100, "website_operation_header_name_invalid", path);
|
|
521
|
-
const normalized = name.toLowerCase();
|
|
522
|
-
if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(name) || FORBIDDEN_HEADERS.has(normalized) || (!allowAuthorization && normalized === "authorization")) {
|
|
523
|
-
fail("website_operation_header_name_invalid", path);
|
|
524
|
-
}
|
|
525
|
-
return name;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function exactHttpsOrigin(value, code, path) {
|
|
529
|
-
const text = requiredText(value, 500, code, path);
|
|
530
|
-
try {
|
|
531
|
-
const url = new URL(text);
|
|
532
|
-
if (url.protocol !== "https:" || url.origin !== text || url.username || url.password || !publicHostname(url.hostname)) fail(code, path);
|
|
533
|
-
return url.origin;
|
|
534
|
-
} catch (error) {
|
|
535
|
-
if (error instanceof WebsiteConnectorDefinitionError) throw error;
|
|
536
|
-
fail(code, path);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function publicHostname(value) {
|
|
541
|
-
const hostname = String(value).toLowerCase().replace(/^\[|\]$/g, "");
|
|
542
|
-
if (!hostname.includes(".") || /^[0-9.]+$/.test(hostname) || hostname.includes(":")) return false;
|
|
543
|
-
return !hostname.endsWith(".localhost")
|
|
544
|
-
&& !hostname.endsWith(".local")
|
|
545
|
-
&& !hostname.endsWith(".internal")
|
|
546
|
-
&& hostname !== "metadata.google.internal";
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function optionalHttpsUrl(value, allowedOrigins, path) {
|
|
550
|
-
if (value === undefined || value === null || value === "") return undefined;
|
|
551
|
-
try {
|
|
552
|
-
const url = new URL(requiredText(value, 2_000, "website_connector_login_url_invalid", path));
|
|
553
|
-
if (url.protocol !== "https:" || !allowedOrigins.includes(url.origin) || url.username || url.password || url.hash) fail("website_connector_login_url_invalid", path);
|
|
554
|
-
return url.toString();
|
|
555
|
-
} catch (error) {
|
|
556
|
-
if (error instanceof WebsiteConnectorDefinitionError) throw error;
|
|
557
|
-
fail("website_connector_login_url_invalid", path);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function record(value, code, path) {
|
|
562
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) fail(code, path);
|
|
563
|
-
return value;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function uniqueArray(value, min, max, path) {
|
|
567
|
-
if (!Array.isArray(value) || value.length < min || value.length > max) fail("website_connector_array_invalid", path);
|
|
568
|
-
const serialized = value.map((item) => typeof item === "string" ? item : JSON.stringify(item));
|
|
569
|
-
if (new Set(serialized).size !== serialized.length) fail("website_connector_array_duplicate", path);
|
|
570
|
-
return value;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
function requiredText(value, max, code, path) {
|
|
574
|
-
const text = typeof value === "string" ? value.trim() : "";
|
|
575
|
-
if (!text || text.length > max) fail(code, path);
|
|
576
|
-
return text;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function optionalText(value, max, code, path) {
|
|
580
|
-
if (value === undefined || value === null || value === "") return undefined;
|
|
581
|
-
return requiredText(value, max, code, path);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function optionalEnum(value, allowed, fallback, path) {
|
|
585
|
-
const candidate = value === undefined ? fallback : value;
|
|
586
|
-
if (!allowed.includes(candidate)) fail("website_connector_enum_invalid", path);
|
|
587
|
-
return candidate;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
function boundedInteger(value, min, max, path) {
|
|
591
|
-
if (!Number.isInteger(value) || value < min || value > max) fail("website_connector_integer_invalid", path);
|
|
592
|
-
return value;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function equal(actual, expected, code, path) {
|
|
596
|
-
if (actual !== expected) fail(code, path);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
function compareSemver(left, right) {
|
|
600
|
-
const a = left.split(".").map(Number);
|
|
601
|
-
const b = right.split(".").map(Number);
|
|
602
|
-
for (let index = 0; index < 3; index += 1) {
|
|
603
|
-
if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
|
|
604
|
-
}
|
|
605
|
-
return 0;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
async function refreshAwsCognitoAuthentication(auth, storageState, options) {
|
|
609
|
-
const refresh = auth.refresh;
|
|
610
|
-
const secretSource = auth.secretSources.find((source) => source.id === refresh.refreshTokenSource);
|
|
611
|
-
const refreshToken = secretSource
|
|
612
|
-
? storageValue(storageState, secretSource.origin, secretSource.keySuffix)
|
|
613
|
-
: undefined;
|
|
614
|
-
if (!refreshToken) throw new WebsiteConnectorRuntimeError(409, "browser_connection_needs_reauth");
|
|
615
|
-
const endpoint = `https://cognito-idp.${refresh.region}.amazonaws.com/`;
|
|
616
|
-
let response;
|
|
617
|
-
try {
|
|
618
|
-
response = await (options.fetchImpl ?? fetch)(endpoint, {
|
|
619
|
-
body: JSON.stringify({ ClientId: refresh.clientId, RefreshToken: refreshToken }),
|
|
620
|
-
headers: {
|
|
621
|
-
"Content-Type": "application/x-amz-json-1.0",
|
|
622
|
-
"X-Amz-Target": "AWSCognitoIdentityProviderService.GetTokensFromRefreshToken"
|
|
623
|
-
},
|
|
624
|
-
method: "POST",
|
|
625
|
-
redirect: "manual",
|
|
626
|
-
signal: AbortSignal.timeout(options.timeoutMs ?? 15_000)
|
|
627
|
-
});
|
|
628
|
-
} catch {
|
|
629
|
-
throw new WebsiteConnectorRuntimeError(502, "website_auth_refresh_unavailable");
|
|
630
|
-
}
|
|
631
|
-
if (response.status >= 300 && response.status < 400) {
|
|
632
|
-
throw new WebsiteConnectorRuntimeError(502, "website_auth_refresh_redirect_forbidden");
|
|
633
|
-
}
|
|
634
|
-
let payload;
|
|
635
|
-
try {
|
|
636
|
-
const raw = await readLimitedBody(response);
|
|
637
|
-
payload = raw ? JSON.parse(raw) : {};
|
|
638
|
-
} catch (error) {
|
|
639
|
-
if (error instanceof WebsiteConnectorRuntimeError && error.code === "website_operation_response_too_large") {
|
|
640
|
-
throw new WebsiteConnectorRuntimeError(502, "website_auth_refresh_response_invalid");
|
|
641
|
-
}
|
|
642
|
-
throw new WebsiteConnectorRuntimeError(502, "website_auth_refresh_response_invalid");
|
|
643
|
-
}
|
|
644
|
-
if (!response.ok) {
|
|
645
|
-
const errorType = String(payload?.__type || payload?.code || "").split("#").at(-1);
|
|
646
|
-
if (["NotAuthorizedException", "UserNotFoundException", "PasswordResetRequiredException"].includes(errorType)) {
|
|
647
|
-
throw new WebsiteConnectorRuntimeError(409, "browser_connection_needs_reauth");
|
|
648
|
-
}
|
|
649
|
-
throw new WebsiteConnectorRuntimeError(502, "website_auth_refresh_unavailable");
|
|
650
|
-
}
|
|
651
|
-
const result = payload?.AuthenticationResult;
|
|
652
|
-
const idToken = typeof result?.IdToken === "string" ? result.IdToken : "";
|
|
653
|
-
const accessToken = typeof result?.AccessToken === "string" ? result.AccessToken : "";
|
|
654
|
-
assertCognitoToken(idToken, refresh, "id", options.now);
|
|
655
|
-
assertCognitoToken(accessToken, refresh, "access", options.now);
|
|
656
|
-
let nextState = replaceStorageValue(storageState, refresh.idTokenSource, idToken);
|
|
657
|
-
nextState = replaceStorageValue(nextState, refresh.accessTokenSource, accessToken);
|
|
658
|
-
if (typeof result?.RefreshToken === "string" && result.RefreshToken) {
|
|
659
|
-
nextState = replaceStorageValue(nextState, secretSource, result.RefreshToken);
|
|
660
|
-
}
|
|
661
|
-
return { refreshed: true, storageState: nextState };
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function assertCognitoToken(value, refresh, tokenUse, now) {
|
|
665
|
-
if (!cognitoTokenValid(value, refresh, tokenUse, now, 30)) {
|
|
666
|
-
throw new WebsiteConnectorRuntimeError(502, "website_auth_refresh_response_invalid");
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function cognitoStoredTokenValid(state, source, refresh, tokenUse, now, leewaySeconds) {
|
|
671
|
-
const value = storageValue(state, source.origin, source.keySuffix);
|
|
672
|
-
return cognitoTokenValid(value, refresh, tokenUse, now, leewaySeconds);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function cognitoTokenValid(value, refresh, tokenUse, now, leewaySeconds) {
|
|
676
|
-
const claims = jwtClaims(value);
|
|
677
|
-
const issuer = `https://cognito-idp.${refresh.region}.amazonaws.com/${refresh.userPoolId}`;
|
|
678
|
-
const clientMatches = tokenUse === "id"
|
|
679
|
-
? claims?.aud === refresh.clientId
|
|
680
|
-
: claims?.client_id === refresh.clientId;
|
|
681
|
-
return Boolean(
|
|
682
|
-
claims
|
|
683
|
-
&& claims.iss === issuer
|
|
684
|
-
&& claims.token_use === tokenUse
|
|
685
|
-
&& clientMatches
|
|
686
|
-
&& typeof claims.exp === "number"
|
|
687
|
-
&& claims.exp > now / 1000 + leewaySeconds
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function replaceStorageValue(state, source, value) {
|
|
692
|
-
let matches = 0;
|
|
693
|
-
const origins = array(state?.origins).map((origin) => {
|
|
694
|
-
if (origin?.origin !== source.origin) return {
|
|
695
|
-
...origin,
|
|
696
|
-
localStorage: array(origin?.localStorage).map((item) => ({ ...item }))
|
|
697
|
-
};
|
|
698
|
-
return {
|
|
699
|
-
...origin,
|
|
700
|
-
localStorage: array(origin?.localStorage).map((item) => {
|
|
701
|
-
if (!String(item?.name || "").endsWith(source.keySuffix)) return { ...item };
|
|
702
|
-
matches += 1;
|
|
703
|
-
return { ...item, value };
|
|
704
|
-
})
|
|
705
|
-
};
|
|
706
|
-
});
|
|
707
|
-
if (matches !== 1) throw new WebsiteConnectorRuntimeError(409, "browser_connection_needs_reauth");
|
|
708
|
-
return {
|
|
709
|
-
cookies: array(state?.cookies).map((cookie) => ({ ...cookie })),
|
|
710
|
-
origins
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function applyBrowserAuthentication(headers, auth, state, requestUrl) {
|
|
715
|
-
if (auth.kind !== "browser_storage") {
|
|
716
|
-
throw new WebsiteConnectorRuntimeError(503, "website_connector_auth_invalid");
|
|
717
|
-
}
|
|
718
|
-
if (auth.sendCookies) {
|
|
719
|
-
const cookie = cookieHeader(state, new URL(requestUrl));
|
|
720
|
-
if (!cookie) throw new WebsiteConnectorRuntimeError(409, "browser_connection_needs_reauth");
|
|
721
|
-
headers.set("Cookie", cookie);
|
|
722
|
-
}
|
|
723
|
-
for (const source of auth.headerSources) {
|
|
724
|
-
const value = storageValue(state, source.origin, source.keySuffix);
|
|
725
|
-
if (value === undefined || !credentialUnexpired(value, Date.now() / 1000)) {
|
|
726
|
-
throw new WebsiteConnectorRuntimeError(409, "browser_connection_needs_reauth");
|
|
727
|
-
}
|
|
728
|
-
headers.set(source.name, `${source.prefix ?? ""}${value}`);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function storageValue(state, origin, keySuffix) {
|
|
733
|
-
const matches = array(state?.origins)
|
|
734
|
-
.filter((candidate) => candidate?.origin === origin)
|
|
735
|
-
.flatMap((candidate) => array(candidate?.localStorage))
|
|
736
|
-
.filter((candidate) => String(candidate?.name || "").endsWith(keySuffix));
|
|
737
|
-
return matches.length === 1 && matches[0]?.value ? String(matches[0].value) : undefined;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function credentialUnexpired(value, nowSeconds, leewaySeconds = 30) {
|
|
741
|
-
const payload = jwtClaims(value);
|
|
742
|
-
return !payload || typeof payload.exp !== "number" || payload.exp > nowSeconds + leewaySeconds;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function jwtClaims(value) {
|
|
746
|
-
const parts = String(value).split(".");
|
|
747
|
-
if (parts.length !== 3) return undefined;
|
|
748
|
-
try {
|
|
749
|
-
const base64 = parts[1].replaceAll("-", "+").replaceAll("_", "/");
|
|
750
|
-
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
751
|
-
const payload = JSON.parse(atob(padded));
|
|
752
|
-
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : undefined;
|
|
753
|
-
} catch {
|
|
754
|
-
return undefined;
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function cookieHeader(state, url) {
|
|
759
|
-
const now = Date.now() / 1000;
|
|
760
|
-
return array(state?.cookies)
|
|
761
|
-
.filter((cookie) => {
|
|
762
|
-
const domain = String(cookie?.domain || "").replace(/^\./, "").toLowerCase();
|
|
763
|
-
const hostname = url.hostname.toLowerCase();
|
|
764
|
-
const domainMatch = hostname === domain || hostname.endsWith(`.${domain}`);
|
|
765
|
-
const pathMatch = url.pathname.startsWith(cookie?.path || "/");
|
|
766
|
-
const unexpired = cookie?.expires === undefined || cookie.expires < 0 || cookie.expires > now;
|
|
767
|
-
return domainMatch && pathMatch && unexpired && (!cookie?.secure || url.protocol === "https:");
|
|
768
|
-
})
|
|
769
|
-
.sort((left, right) => (right.path?.length ?? 0) - (left.path?.length ?? 0))
|
|
770
|
-
.map((cookie) => `${cookie.name}=${cookie.value}`)
|
|
771
|
-
.join("; ");
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
async function readLimitedBody(response) {
|
|
775
|
-
const declared = Number(response.headers.get("content-length") || 0);
|
|
776
|
-
if (declared > MAX_RESPONSE_BYTES) throw new WebsiteConnectorRuntimeError(502, "website_operation_response_too_large");
|
|
777
|
-
if (!response.body) return "";
|
|
778
|
-
const reader = response.body.getReader();
|
|
779
|
-
const chunks = [];
|
|
780
|
-
let size = 0;
|
|
781
|
-
while (true) {
|
|
782
|
-
const { done, value } = await reader.read();
|
|
783
|
-
if (done) break;
|
|
784
|
-
size += value.byteLength;
|
|
785
|
-
if (size > MAX_RESPONSE_BYTES) {
|
|
786
|
-
await reader.cancel();
|
|
787
|
-
throw new WebsiteConnectorRuntimeError(502, "website_operation_response_too_large");
|
|
788
|
-
}
|
|
789
|
-
chunks.push(value);
|
|
790
|
-
}
|
|
791
|
-
const body = new Uint8Array(size);
|
|
792
|
-
let offset = 0;
|
|
793
|
-
for (const chunk of chunks) {
|
|
794
|
-
body.set(chunk, offset);
|
|
795
|
-
offset += chunk.byteLength;
|
|
796
|
-
}
|
|
797
|
-
return new TextDecoder().decode(body);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
function sanitizeOutputValue(value, secrets, depth) {
|
|
801
|
-
if (depth > 12) return "[truncated]";
|
|
802
|
-
if (typeof value === "string") {
|
|
803
|
-
let result = value.slice(0, 100_000);
|
|
804
|
-
for (const secret of secrets) result = result.replaceAll(secret, "[redacted]");
|
|
805
|
-
return result;
|
|
806
|
-
}
|
|
807
|
-
if (typeof value === "number" || typeof value === "boolean" || value === null) return value;
|
|
808
|
-
if (Array.isArray(value)) return value.slice(0, 1_000).map((item) => sanitizeOutputValue(item, secrets, depth + 1));
|
|
809
|
-
if (!value || typeof value !== "object") return undefined;
|
|
810
|
-
return Object.fromEntries(Object.entries(value).flatMap(([key, child]) => (
|
|
811
|
-
sensitiveKey(key) ? [] : [[key, sanitizeOutputValue(child, secrets, depth + 1)]]
|
|
812
|
-
)));
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
function sensitiveKey(key) {
|
|
816
|
-
return /cookie|password|authorization|access.?token|refresh.?token|session.?state|storage.?state|credential/i.test(key);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function array(value) {
|
|
820
|
-
return Array.isArray(value) ? value : [];
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function fail(code, path) {
|
|
824
|
-
throw new WebsiteConnectorDefinitionError(code, path);
|
|
825
|
-
}
|