@fargowealth/fargo-ai-cli 1.3.0 → 1.4.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.
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { access, chmod, mkdir, readFile, unlink } from "node:fs/promises";
2
+ import { access, chmod, mkdir, readFile, readdir, unlink } from "node:fs/promises";
3
3
  import { homedir, platform } from "node:os";
4
4
  import { delimiter, join, resolve } from "node:path";
5
5
  import { createInterface } from "node:readline/promises";
@@ -91,6 +91,34 @@ export function defaultLocalWebsiteProfileDirectory(connectorId, options = {}) {
91
91
  return join(options.xdgConfigHome ?? process.env.XDG_CONFIG_HOME ?? join(homeDirectory, ".config"), "fargo", "website-profiles", safeId, profileKey);
92
92
  }
93
93
 
94
+ export async function resolveLocalWebsiteProfileDirectory(connectorId, options = {}) {
95
+ if (options.profileDirectory) return resolve(options.profileDirectory);
96
+ const fallback = defaultLocalWebsiteProfileDirectory(connectorId, options);
97
+ const connectorDirectory = resolve(fallback, "..");
98
+ let entries;
99
+ try {
100
+ entries = await readdir(connectorDirectory, { withFileTypes: true });
101
+ } catch (error) {
102
+ if (error?.code === "ENOENT") return fallback;
103
+ throw error;
104
+ }
105
+ const connectionProfiles = entries
106
+ .filter((entry) => entry.isDirectory() && /^bgc_[A-Za-z0-9_-]{43}$/.test(entry.name))
107
+ .map((entry) => join(connectorDirectory, entry.name));
108
+ if (connectionProfiles.length === 0) return fallback;
109
+ if (connectionProfiles.length === 1) return connectionProfiles[0];
110
+ throw new Error(`Multiple local Profiles exist for ${connectorId}; pass --profile-dir <path>`);
111
+ }
112
+
113
+ export function documentIsReadyAtOrigin(value, expectedOrigin) {
114
+ if (!["interactive", "complete"].includes(value?.readyState)) return false;
115
+ try {
116
+ return new URL(value.url).origin === expectedOrigin;
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+
94
122
  export function parseCaptureSpec(value) {
95
123
  const source = record(value, "Website Profile capture spec is invalid");
96
124
  if (source.profileSchemaVersion !== WEBSITE_BROWSER_PROFILE_SCHEMA_VERSION) throw new Error("Website Profile schema is not supported");
@@ -174,7 +202,7 @@ async function waitForDocument(client, sessionId, expectedOrigin) {
174
202
  returnByValue: true
175
203
  }, sessionId).catch(() => ({}));
176
204
  const value = result?.result?.value;
177
- if (value?.readyState === "complete" && new URL(value.url).origin === expectedOrigin) return;
205
+ if (documentIsReadyAtOrigin(value, expectedOrigin)) return;
178
206
  await delay(100);
179
207
  }
180
208
  throw new Error(`Timed out while reading local storage for ${expectedOrigin}`);
@@ -185,7 +213,10 @@ async function waitForLoginConfirmation(spec, options) {
185
213
  const input = options.stdin ?? process.stdin;
186
214
  const output = options.stderr ?? process.stderr;
187
215
  if (!input.isTTY) throw new Error("Interactive login requires a TTY; use --no-prompt only for an already authenticated local Profile");
188
- output.write(`Fargo opened an isolated local Chrome Profile for ${spec.connectorId}.\nComplete login, then press Enter here to upload only the allowlisted session state.\n`);
216
+ const nextStep = options.purpose === "local-execution"
217
+ ? "Complete login, then press Enter here to run locally; nothing will be uploaded."
218
+ : "Complete login, then press Enter here to upload only the allowlisted session state.";
219
+ output.write(`Fargo opened an isolated local Chrome Profile for ${spec.connectorId}.\n${nextStep}\n`);
189
220
  const prompt = createInterface({ input, output });
190
221
  try {
191
222
  await prompt.question("");
@@ -3,8 +3,13 @@ import { readFile } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
4
  import { callMcpTool } from "./mcp-client.mjs";
5
5
  import { mcpToolPayload } from "./mcp-result.mjs";
6
- import { captureLocalWebsiteProfile } from "./website-browser-profile.mjs";
7
- import { parseWebsiteConnectorDefinition } from "./website-connector-definition.mjs";
6
+ import { captureLocalWebsiteProfile, resolveLocalWebsiteProfileDirectory } from "./website-browser-profile.mjs";
7
+ import {
8
+ assertWebsiteConnectorRuntimeCompatibility,
9
+ executeWebsiteConnectorOperation,
10
+ parseWebsiteConnectorDefinition,
11
+ websiteConnectorRuntimeDescriptor
12
+ } from "./website-connector-definition.mjs";
8
13
 
9
14
  const WEBSITE_BRIDGE_APP_ID = "website-bridge";
10
15
  const WEBSITE_BRIDGE_URL = "https://website-bridge.fargowealth.app";
@@ -17,9 +22,14 @@ export async function runWebsiteCommand(args, stdout, store, options = {}) {
17
22
  }
18
23
  if (action === "validate") {
19
24
  const definition = await readDefinition(args);
25
+ assertWebsiteConnectorRuntimeCompatibility(definition);
20
26
  stdout.write(`${JSON.stringify(validationSummary(definition))}\n`);
21
27
  return;
22
28
  }
29
+ if (action === "runtime") {
30
+ stdout.write(`${JSON.stringify(websiteConnectorRuntimeDescriptor())}\n`);
31
+ return;
32
+ }
23
33
  if (action === "release") {
24
34
  const definition = await readDefinition(args);
25
35
  const expectedRevision = optionalInteger(args, "--expected-revision");
@@ -41,13 +51,50 @@ export async function runWebsiteCommand(args, stdout, store, options = {}) {
41
51
  return;
42
52
  }
43
53
  if (action === "call") {
54
+ const mode = websiteCallMode(args);
44
55
  const write = args.includes("--write");
56
+ if (mode === "local") {
57
+ const definition = await readDefinition(args);
58
+ const connectorId = optionValue(args, "--connector")?.trim();
59
+ if (connectorId && connectorId !== definition.connectorId) {
60
+ throw new Error("--connector must match the Connector definition in --file");
61
+ }
62
+ const operationId = requiredOption(args, "--operation");
63
+ const operation = definition.operations.find((candidate) => candidate.operationId === operationId);
64
+ if (!operation) throw new Error("website_operation_not_found");
65
+ if (!operation.readOnly && (!write || !args.includes("--confirm"))) {
66
+ throw new Error("Local write operations require --write --confirm");
67
+ }
68
+ let storageState;
69
+ if (operation.requiresConnection) {
70
+ const capture = options.captureWebsiteProfile ?? captureLocalWebsiteProfile;
71
+ storageState = await capture(localProfileSpec(definition), {
72
+ chromePath: optionValue(args, "--chrome-path"),
73
+ noPrompt: args.includes("--no-prompt"),
74
+ profileDirectory: await resolveLocalWebsiteProfileDirectory(definition.connectorId, {
75
+ profileDirectory: optionValue(args, "--profile-dir")
76
+ }),
77
+ purpose: "local-execution",
78
+ settleMs: optionalInteger(args, "--settle-ms"),
79
+ stderr: options.stderr,
80
+ stdin: options.stdin
81
+ });
82
+ }
83
+ const execute = options.executeWebsiteOperation ?? executeWebsiteConnectorOperation;
84
+ const result = await execute(definition, operationId, await readArguments(args), {
85
+ fetchImpl: options.fetchImpl,
86
+ readOnlyOnly: !write,
87
+ ...(storageState ? { storageState } : {})
88
+ });
89
+ stdout.write(`${JSON.stringify({ ...result, executionMode: "local" })}\n`);
90
+ return;
91
+ }
45
92
  const result = await callWebsiteBridge(store, write ? "website_connector_execute" : "website_connector_read", {
46
93
  connectorId: requiredOption(args, "--connector"),
47
94
  operationId: requiredOption(args, "--operation"),
48
95
  arguments: await readArguments(args)
49
96
  }, { ...options, confirmation: args.includes("--confirm") });
50
- stdout.write(`${JSON.stringify(result)}\n`);
97
+ stdout.write(`${JSON.stringify({ ...result, executionMode: "remote" })}\n`);
51
98
  return;
52
99
  }
53
100
  if (action === "connection-status") {
@@ -151,10 +198,35 @@ function validationSummary(definition) {
151
198
  displayName: definition.displayName,
152
199
  operationCount: definition.operations.length,
153
200
  requiresConnection: definition.operations.some((operation) => operation.requiresConnection),
201
+ runtime: websiteConnectorRuntimeDescriptor(),
154
202
  sha256: createHash("sha256").update(canonical).digest("hex")
155
203
  };
156
204
  }
157
205
 
206
+ function websiteCallMode(args) {
207
+ if (args.includes("--local")) return "local";
208
+ const mode = optionValue(args, "--mode") ?? "remote";
209
+ if (!["auto", "local", "remote"].includes(mode)) throw new Error("--mode must be auto, local, or remote");
210
+ return mode === "auto" ? (optionValue(args, "--file") ? "local" : "remote") : mode;
211
+ }
212
+
213
+ function localProfileSpec(definition) {
214
+ if (definition.auth.kind !== "browser_storage" || !definition.loginUrl) {
215
+ throw new Error("Website Connector does not define a local browser Profile");
216
+ }
217
+ return {
218
+ allowedOrigins: definition.allowedOrigins,
219
+ auth: {
220
+ headerSources: definition.auth.headerSources.map(({ keySuffix, origin }) => ({ keySuffix, origin })),
221
+ sendCookies: definition.auth.sendCookies
222
+ },
223
+ connection: {},
224
+ connectorId: definition.connectorId,
225
+ loginUrl: definition.loginUrl,
226
+ profileSchemaVersion: "fargo.browser-profile/v1"
227
+ };
228
+ }
229
+
158
230
  function optionalInteger(args, name) {
159
231
  const raw = optionValue(args, name);
160
232
  if (raw === undefined) return undefined;
@@ -264,12 +336,13 @@ function validatedUploadUrl(value, options) {
264
336
  return url.toString();
265
337
  }
266
338
 
267
- export const WEBSITE_USAGE = "Usage: fargo website validate|release|list|operations|call|connect|sync|connection-status|authorize|authorize-complete|binding";
339
+ export const WEBSITE_USAGE = "Usage: fargo website validate|runtime|release|list|operations|call|connect|sync|connection-status|authorize|authorize-complete|binding";
268
340
 
269
341
  export const WEBSITE_HELP = `Usage: fargo website <command> [options]
270
342
 
271
343
  Connector authoring:
272
344
  validate --file <path>
345
+ runtime
273
346
  release --file <path> [--expected-revision <n>] --confirm
274
347
  list
275
348
  operations --connector <id>
@@ -278,7 +351,8 @@ Connection and execution:
278
351
  connect --connector <id> [--chrome-path <path>] [--profile-dir <path>]
279
352
  sync --connector <id> [--no-prompt] [--settle-ms <n>]
280
353
  connection-status --connector <id>
281
- call --connector <id> --operation <id> [--args <json> | --args-file <path>]
354
+ call --connector <id> --operation <id> [--args <json> | --args-file <path>] [--mode auto|remote]
355
+ call --file <path> --operation <id> --mode local [--profile-dir <path>] [--no-prompt]
282
356
  call --connector <id> --operation <id> --write [--confirm]
283
357
 
284
358
  Cloud login fallback:
@@ -1,58 +1 @@
1
- export type WebsiteConnectorInputSchema = {
2
- additionalProperties: false;
3
- properties: Record<string, {
4
- enum?: Array<string | number | boolean>;
5
- maxLength?: number;
6
- type: "string" | "number" | "integer" | "boolean";
7
- }>;
8
- required: string[];
9
- type: "object";
10
- };
11
-
12
- export type WebsiteConnectorAuth =
13
- | { kind: "none" }
14
- | {
15
- kind: "browser_storage";
16
- sendCookies: boolean;
17
- headerSources: Array<{ keySuffix: string; name: string; origin: string; prefix?: string }>;
18
- };
19
-
20
- export type WebsiteConnectorOperation = {
21
- operationId: string;
22
- description: string;
23
- readOnly: boolean;
24
- requiresConnection: boolean;
25
- riskLevel: "low" | "medium" | "high" | "critical";
26
- inputSchema: WebsiteConnectorInputSchema;
27
- request: {
28
- origin: string;
29
- method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
30
- path: string;
31
- query: Record<string, string>;
32
- headers: Record<string, string>;
33
- body?: unknown;
34
- };
35
- };
36
-
37
- export type WebsiteConnectorDefinition = {
38
- schemaVersion: "fargo.website-connector/v1";
39
- connectorId: string;
40
- displayName: string;
41
- allowedOrigins: string[];
42
- loginUrl?: string;
43
- auth: WebsiteConnectorAuth;
44
- operations: WebsiteConnectorOperation[];
45
- };
46
-
47
- export const WEBSITE_CONNECTOR_SCHEMA_VERSION: "fargo.website-connector/v1";
48
- export class WebsiteConnectorDefinitionError extends Error {
49
- code: string;
50
- path: string;
51
- }
52
- export function parseWebsiteConnectorDefinition(value: unknown): WebsiteConnectorDefinition;
53
- export function connectorToolName(connectorId: string, operationId: string): string;
54
- export function renderWebsiteOperation(definition: unknown, operationId: string, rawArguments: unknown): {
55
- definition: WebsiteConnectorDefinition;
56
- operation: WebsiteConnectorOperation;
57
- request: { body?: unknown; headers: Record<string, string>; method: string; url: string };
58
- };
1
+ export * from "@fargowealth/website-connector-runtime";
@@ -1,339 +1 @@
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 FORBIDDEN_HEADERS = new Set([
5
- "connection",
6
- "content-length",
7
- "cookie",
8
- "host",
9
- "proxy-authorization",
10
- "transfer-encoding",
11
- "upgrade"
12
- ]);
13
-
14
- export const WEBSITE_CONNECTOR_SCHEMA_VERSION = "fargo.website-connector/v1";
15
-
16
- export class WebsiteConnectorDefinitionError extends Error {
17
- constructor(code, path = "") {
18
- super(path ? `${code}:${path}` : code);
19
- this.name = "WebsiteConnectorDefinitionError";
20
- this.code = code;
21
- this.path = path;
22
- }
23
- }
24
-
25
- export function parseWebsiteConnectorDefinition(value) {
26
- const source = record(value, "website_connector_definition_invalid", "/");
27
- equal(source.schemaVersion, WEBSITE_CONNECTOR_SCHEMA_VERSION, "website_connector_schema_version_invalid", "/schemaVersion");
28
- const connectorId = requiredText(source.connectorId, 64, "website_connector_id_invalid", "/connectorId");
29
- if (!CONNECTOR_ID.test(connectorId)) fail("website_connector_id_invalid", "/connectorId");
30
- const displayName = requiredText(source.displayName, 120, "website_connector_display_name_invalid", "/displayName");
31
- const allowedOrigins = uniqueArray(source.allowedOrigins, 1, 12, "/allowedOrigins").map((origin, index) => (
32
- exactHttpsOrigin(origin, "website_connector_origin_invalid", `/allowedOrigins/${index}`)
33
- ));
34
- const loginUrl = optionalHttpsUrl(source.loginUrl, allowedOrigins, "/loginUrl");
35
- const auth = parseAuthRecipe(source.auth, allowedOrigins);
36
- if (auth.kind === "browser_storage" && !loginUrl) fail("website_connector_login_url_required", "/loginUrl");
37
- const operations = uniqueArray(source.operations, 1, 80, "/operations").map((operation, index) => (
38
- parseOperation(operation, allowedOrigins, index)
39
- ));
40
- const operationIds = new Set();
41
- for (const operation of operations) {
42
- if (operationIds.has(operation.operationId)) fail("website_operation_id_duplicate", `/operations/${operation.operationId}`);
43
- if (operation.requiresConnection && auth.kind !== "browser_storage") {
44
- fail("website_operation_connection_auth_required", `/operations/${operation.operationId}/requiresConnection`);
45
- }
46
- operationIds.add(operation.operationId);
47
- }
48
- return {
49
- schemaVersion: WEBSITE_CONNECTOR_SCHEMA_VERSION,
50
- connectorId,
51
- displayName,
52
- allowedOrigins,
53
- ...(loginUrl ? { loginUrl } : {}),
54
- auth,
55
- operations
56
- };
57
- }
58
-
59
- export function connectorToolName(connectorId, operationId) {
60
- const left = String(connectorId || "").replaceAll("-", "_");
61
- return `${left}_${operationId}`;
62
- }
63
-
64
- export function renderWebsiteOperation(definition, operationId, rawArguments) {
65
- const parsed = parseWebsiteConnectorDefinition(definition);
66
- const operation = parsed.operations.find((candidate) => candidate.operationId === operationId);
67
- if (!operation) fail("website_operation_not_found", "/operationId");
68
- const args = record(rawArguments ?? {}, "website_operation_arguments_invalid", "/arguments");
69
- validateArguments(args, operation.inputSchema);
70
- const path = renderString(operation.request.path, args, "/request/path");
71
- const url = new URL(path, `${operation.request.origin}/`);
72
- if (url.origin !== operation.request.origin) fail("website_operation_origin_escape", "/request/path");
73
- for (const [name, argumentName] of Object.entries(operation.request.query)) {
74
- const value = args[argumentName];
75
- if (value !== undefined && value !== null) url.searchParams.set(name, scalarText(value, `/arguments/${argumentName}`));
76
- }
77
- const body = operation.request.body === undefined
78
- ? undefined
79
- : renderBody(operation.request.body, args, "/request/body");
80
- return {
81
- definition: parsed,
82
- operation,
83
- request: {
84
- body,
85
- headers: { ...operation.request.headers },
86
- method: operation.request.method,
87
- url: url.toString()
88
- }
89
- };
90
- }
91
-
92
- function parseAuthRecipe(value, allowedOrigins) {
93
- const source = value === undefined ? { kind: "none" } : record(value, "website_auth_recipe_invalid", "/auth");
94
- if (source.kind === "none") return { kind: "none" };
95
- equal(source.kind, "browser_storage", "website_auth_recipe_invalid", "/auth/kind");
96
- const sendCookies = source.sendCookies === true;
97
- const headerSources = source.headerSources === undefined
98
- ? []
99
- : uniqueArray(source.headerSources, 0, 12, "/auth/headerSources").map((item, index) => {
100
- const binding = record(item, "website_auth_header_source_invalid", `/auth/headerSources/${index}`);
101
- const name = headerName(binding.name, `/auth/headerSources/${index}/name`, true);
102
- const origin = exactHttpsOrigin(binding.origin, "website_auth_header_source_invalid", `/auth/headerSources/${index}/origin`);
103
- if (!allowedOrigins.includes(origin)) fail("website_auth_origin_not_allowed", `/auth/headerSources/${index}/origin`);
104
- const keySuffix = requiredText(binding.keySuffix, 240, "website_auth_header_source_invalid", `/auth/headerSources/${index}/keySuffix`);
105
- const prefix = optionalHeaderPrefix(binding.prefix, `/auth/headerSources/${index}/prefix`);
106
- return { keySuffix, name, origin, ...(prefix ? { prefix } : {}) };
107
- });
108
- if (!sendCookies && headerSources.length === 0) fail("website_auth_recipe_empty", "/auth");
109
- return { kind: "browser_storage", sendCookies, headerSources };
110
- }
111
-
112
- function parseOperation(value, allowedOrigins, index) {
113
- const path = `/operations/${index}`;
114
- const source = record(value, "website_operation_invalid", path);
115
- const operationId = requiredText(source.operationId, 64, "website_operation_id_invalid", `${path}/operationId`);
116
- if (!OPERATION_ID.test(operationId)) fail("website_operation_id_invalid", `${path}/operationId`);
117
- const description = requiredText(source.description, 500, "website_operation_description_invalid", `${path}/description`);
118
- const readOnly = source.readOnly === true;
119
- const requiresConnection = source.requiresConnection === true;
120
- const riskLevel = optionalEnum(source.riskLevel, ["low", "medium", "high", "critical"], readOnly ? "low" : "medium", `${path}/riskLevel`);
121
- const inputSchema = parseInputSchema(source.inputSchema, `${path}/inputSchema`);
122
- const requestSource = record(source.request, "website_operation_request_invalid", `${path}/request`);
123
- const origin = exactHttpsOrigin(requestSource.origin, "website_operation_origin_invalid", `${path}/request/origin`);
124
- if (!allowedOrigins.includes(origin)) fail("website_operation_origin_not_allowed", `${path}/request/origin`);
125
- const method = optionalEnum(requestSource.method, ["GET", "POST", "PUT", "PATCH", "DELETE"], "GET", `${path}/request/method`);
126
- if (readOnly && method !== "GET") fail("website_operation_read_only_method_invalid", `${path}/request/method`);
127
- const requestPath = requiredText(requestSource.path, 2_000, "website_operation_path_invalid", `${path}/request/path`);
128
- if (!requestPath.startsWith("/") || requestPath.startsWith("//") || requestPath.includes("\\")) {
129
- fail("website_operation_path_invalid", `${path}/request/path`);
130
- }
131
- const pathArguments = [...requestPath.matchAll(TEMPLATE_ARGUMENT)].map((match) => match[1]);
132
- const properties = record(inputSchema.properties ?? {}, "website_operation_input_schema_invalid", `${path}/inputSchema/properties`);
133
- for (const argument of pathArguments) {
134
- if (!Object.hasOwn(properties, argument)) fail("website_operation_template_argument_unknown", `${path}/request/path`);
135
- }
136
- const query = requestSource.query === undefined
137
- ? {}
138
- : Object.fromEntries(Object.entries(record(requestSource.query, "website_operation_query_invalid", `${path}/request/query`)).map(([name, argument]) => {
139
- if (!/^[A-Za-z0-9_.~-]{1,120}$/.test(name)) fail("website_operation_query_name_invalid", `${path}/request/query/${name}`);
140
- const argumentName = requiredText(argument, 120, "website_operation_query_argument_invalid", `${path}/request/query/${name}`);
141
- if (!Object.hasOwn(properties, argumentName)) fail("website_operation_query_argument_unknown", `${path}/request/query/${name}`);
142
- return [name, argumentName];
143
- }));
144
- const headers = requestSource.headers === undefined
145
- ? {}
146
- : Object.fromEntries(Object.entries(record(requestSource.headers, "website_operation_headers_invalid", `${path}/request/headers`)).map(([name, headerValue]) => {
147
- const normalized = requiredText(headerValue, 500, "website_operation_header_value_invalid", `${path}/request/headers/${name}`);
148
- if (/[\r\n\0]/.test(normalized)) fail("website_operation_header_value_invalid", `${path}/request/headers/${name}`);
149
- return [headerName(name, `${path}/request/headers/${name}`, false), normalized];
150
- }));
151
- const body = requestSource.body;
152
- if (body !== undefined && method === "GET") fail("website_operation_get_body_invalid", `${path}/request/body`);
153
- if (body !== undefined) validateBodyTemplate(body, properties, `${path}/request/body`);
154
- return {
155
- operationId,
156
- description,
157
- readOnly,
158
- requiresConnection,
159
- riskLevel,
160
- inputSchema,
161
- request: {
162
- origin,
163
- method,
164
- path: requestPath,
165
- query,
166
- headers,
167
- ...(body === undefined ? {} : { body })
168
- }
169
- };
170
- }
171
-
172
- function optionalHeaderPrefix(value, path) {
173
- if (value === undefined || value === null || value === "") return "";
174
- if (typeof value !== "string" || value.length > 80 || /[\r\n\0]/.test(value)) {
175
- fail("website_auth_header_source_invalid", path);
176
- }
177
- return value;
178
- }
179
-
180
- function parseInputSchema(value, path) {
181
- const source = value === undefined
182
- ? { additionalProperties: false, properties: {}, type: "object" }
183
- : record(value, "website_operation_input_schema_invalid", path);
184
- equal(source.type, "object", "website_operation_input_schema_invalid", `${path}/type`);
185
- if (source.additionalProperties !== false) fail("website_operation_input_schema_open", `${path}/additionalProperties`);
186
- const properties = record(source.properties ?? {}, "website_operation_input_schema_invalid", `${path}/properties`);
187
- const normalizedProperties = {};
188
- for (const [name, candidate] of Object.entries(properties)) {
189
- if (!/^[A-Za-z][A-Za-z0-9_]{0,119}$/.test(name)) fail("website_operation_input_name_invalid", `${path}/properties/${name}`);
190
- const property = record(candidate, "website_operation_input_property_invalid", `${path}/properties/${name}`);
191
- const type = optionalEnum(property.type, ["string", "number", "integer", "boolean"], "string", `${path}/properties/${name}/type`);
192
- normalizedProperties[name] = {
193
- type,
194
- ...(typeof property.maxLength === "number" ? { maxLength: boundedInteger(property.maxLength, 1, 20_000, `${path}/properties/${name}/maxLength`) } : {}),
195
- ...(Array.isArray(property.enum) ? { enum: property.enum.map((item) => scalar(item, `${path}/properties/${name}/enum`)) } : {})
196
- };
197
- }
198
- const required = source.required === undefined ? [] : uniqueArray(source.required, 0, Object.keys(normalizedProperties).length, `${path}/required`).map(String);
199
- for (const name of required) if (!Object.hasOwn(normalizedProperties, name)) fail("website_operation_required_unknown", `${path}/required`);
200
- return { additionalProperties: false, properties: normalizedProperties, required, type: "object" };
201
- }
202
-
203
- function validateArguments(args, schema) {
204
- for (const required of schema.required) if (!Object.hasOwn(args, required)) fail("website_operation_argument_required", `/arguments/${required}`);
205
- for (const [name, value] of Object.entries(args)) {
206
- const property = schema.properties[name];
207
- if (!property) fail("website_operation_argument_unknown", `/arguments/${name}`);
208
- if (property.type === "string" && typeof value !== "string") fail("website_operation_argument_type_invalid", `/arguments/${name}`);
209
- if (property.type === "number" && (typeof value !== "number" || !Number.isFinite(value))) fail("website_operation_argument_type_invalid", `/arguments/${name}`);
210
- if (property.type === "integer" && !Number.isInteger(value)) fail("website_operation_argument_type_invalid", `/arguments/${name}`);
211
- if (property.type === "boolean" && typeof value !== "boolean") fail("website_operation_argument_type_invalid", `/arguments/${name}`);
212
- if (property.maxLength && typeof value === "string" && value.length > property.maxLength) fail("website_operation_argument_too_long", `/arguments/${name}`);
213
- if (property.enum && !property.enum.some((candidate) => Object.is(candidate, value))) fail("website_operation_argument_enum_invalid", `/arguments/${name}`);
214
- }
215
- }
216
-
217
- function validateBodyTemplate(value, properties, path) {
218
- if (typeof value === "string") {
219
- if (value.startsWith("$") && !Object.hasOwn(properties, value.slice(1))) fail("website_operation_body_argument_unknown", path);
220
- return;
221
- }
222
- if (value === null || typeof value === "number" || typeof value === "boolean") return;
223
- if (Array.isArray(value)) {
224
- value.forEach((item, index) => validateBodyTemplate(item, properties, `${path}/${index}`));
225
- return;
226
- }
227
- const source = record(value, "website_operation_body_invalid", path);
228
- for (const [name, item] of Object.entries(source)) validateBodyTemplate(item, properties, `${path}/${name}`);
229
- }
230
-
231
- function renderBody(value, args, path) {
232
- if (typeof value === "string") {
233
- if (value.startsWith("$")) return args[value.slice(1)];
234
- return renderString(value, args, path);
235
- }
236
- if (Array.isArray(value)) return value.map((item, index) => renderBody(item, args, `${path}/${index}`));
237
- if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([name, item]) => [name, renderBody(item, args, `${path}/${name}`)]));
238
- return value;
239
- }
240
-
241
- function renderString(value, args, path) {
242
- return String(value).replace(TEMPLATE_ARGUMENT, (_match, name) => encodeURIComponent(scalarText(args[name], `/arguments/${name || path}`)));
243
- }
244
-
245
- function scalarText(value, path) {
246
- scalar(value, path);
247
- return String(value);
248
- }
249
-
250
- function scalar(value, path) {
251
- if (!["string", "number", "boolean"].includes(typeof value) || (typeof value === "number" && !Number.isFinite(value))) {
252
- fail("website_operation_argument_scalar_required", path);
253
- }
254
- return value;
255
- }
256
-
257
- function headerName(value, path, allowAuthorization) {
258
- const name = requiredText(value, 100, "website_operation_header_name_invalid", path);
259
- const normalized = name.toLowerCase();
260
- if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(name) || FORBIDDEN_HEADERS.has(normalized) || (!allowAuthorization && normalized === "authorization")) {
261
- fail("website_operation_header_name_invalid", path);
262
- }
263
- return name;
264
- }
265
-
266
- function exactHttpsOrigin(value, code, path) {
267
- const text = requiredText(value, 500, code, path);
268
- try {
269
- const url = new URL(text);
270
- if (url.protocol !== "https:" || url.origin !== text || url.username || url.password || !publicHostname(url.hostname)) fail(code, path);
271
- return url.origin;
272
- } catch (error) {
273
- if (error instanceof WebsiteConnectorDefinitionError) throw error;
274
- fail(code, path);
275
- }
276
- }
277
-
278
- function publicHostname(value) {
279
- const hostname = String(value).toLowerCase().replace(/^\[|\]$/g, "");
280
- if (!hostname.includes(".") || /^[0-9.]+$/.test(hostname) || hostname.includes(":")) return false;
281
- return !hostname.endsWith(".localhost")
282
- && !hostname.endsWith(".local")
283
- && !hostname.endsWith(".internal")
284
- && hostname !== "metadata.google.internal";
285
- }
286
-
287
- function optionalHttpsUrl(value, allowedOrigins, path) {
288
- if (value === undefined || value === null || value === "") return undefined;
289
- try {
290
- const url = new URL(requiredText(value, 2_000, "website_connector_login_url_invalid", path));
291
- if (url.protocol !== "https:" || !allowedOrigins.includes(url.origin) || url.username || url.password || url.hash) fail("website_connector_login_url_invalid", path);
292
- return url.toString();
293
- } catch (error) {
294
- if (error instanceof WebsiteConnectorDefinitionError) throw error;
295
- fail("website_connector_login_url_invalid", path);
296
- }
297
- }
298
-
299
- function record(value, code, path) {
300
- if (!value || typeof value !== "object" || Array.isArray(value)) fail(code, path);
301
- return value;
302
- }
303
-
304
- function uniqueArray(value, min, max, path) {
305
- if (!Array.isArray(value) || value.length < min || value.length > max) fail("website_connector_array_invalid", path);
306
- const serialized = value.map((item) => typeof item === "string" ? item : JSON.stringify(item));
307
- if (new Set(serialized).size !== serialized.length) fail("website_connector_array_duplicate", path);
308
- return value;
309
- }
310
-
311
- function requiredText(value, max, code, path) {
312
- const text = typeof value === "string" ? value.trim() : "";
313
- if (!text || text.length > max) fail(code, path);
314
- return text;
315
- }
316
-
317
- function optionalText(value, max, code, path) {
318
- if (value === undefined || value === null || value === "") return undefined;
319
- return requiredText(value, max, code, path);
320
- }
321
-
322
- function optionalEnum(value, allowed, fallback, path) {
323
- const candidate = value === undefined ? fallback : value;
324
- if (!allowed.includes(candidate)) fail("website_connector_enum_invalid", path);
325
- return candidate;
326
- }
327
-
328
- function boundedInteger(value, min, max, path) {
329
- if (!Number.isInteger(value) || value < min || value > max) fail("website_connector_integer_invalid", path);
330
- return value;
331
- }
332
-
333
- function equal(actual, expected, code, path) {
334
- if (actual !== expected) fail(code, path);
335
- }
336
-
337
- function fail(code, path) {
338
- throw new WebsiteConnectorDefinitionError(code, path);
339
- }
1
+ export * from "@fargowealth/website-connector-runtime";