@fargowealth/fargo-ai-cli 1.4.0 → 1.4.1

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.1
4
+
5
+ - Capture Connector-declared refresh-only localStorage values without exposing
6
+ them as website request headers or MCP arguments.
7
+ - Add the shared Runtime `auth.refresh.aws-cognito` capability so Browser
8
+ Gateway can refresh expiring ID/access tokens and persist token rotation
9
+ before continuing an operation.
10
+ - Keep rejected refresh credentials fail-closed as `needs_reauth`; transient
11
+ Cognito failures leave the encrypted Connection available for retry.
12
+ - Show Website command help before validating required execution options.
13
+
3
14
  ## 1.4.0
4
15
 
5
16
  - Allow Website Connector operations to select an explicit text response mode
package/README.md CHANGED
@@ -80,6 +80,14 @@ capability using SHA-256 digests; the Profile body then goes directly to
80
80
  protection before replacing the encrypted cloud state. `--no-prompt` is for a
81
81
  Profile that is already authenticated.
82
82
 
83
+ Connectors can declare refresh-only storage selectors separately from request
84
+ headers. For AWS Cognito, Browser Gateway exchanges the encrypted Refresh Token
85
+ for new ID/access tokens before expiry, saves any rotated Refresh Token inside
86
+ the same connection-scoped Durable Object, and only then continues the website
87
+ operation. Refresh Tokens never enter MCP arguments, command output, or normal
88
+ website requests. `sync` is required again only after Cognito rejects or revokes
89
+ the Refresh Token.
90
+
83
91
  Connector operations remain brokered through App MCP `website-bridge`, so the
84
92
  same operations are available to Codex, WorkBuddy, other Agents, and Teammates.
85
93
  V1 releases are owner-private. A Teammate binding delegates the invoking user's
@@ -6,3 +6,10 @@ by both Fargo CLI local debugging and the Fargo Browser Gateway.
6
6
  Connector definitions contain data only. They cannot ship executable code.
7
7
  Callers provide browser storage at execution time; the runtime never persists
8
8
  cookies or localStorage.
9
+
10
+ Runtime `1.1.0` adds declarative AWS Cognito token refresh. `secretSources`
11
+ select refresh-only browser values, while `headerSources` remains the complete
12
+ set of values allowed in business requests. The runtime can exchange an
13
+ expiring session through Cognito's `GetTokensFromRefreshToken` API and returns a
14
+ new in-memory storage state; the hosting BrowserConnectionSession Durable Object
15
+ is responsible for encrypting and persisting that state before use.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fargowealth/website-connector-runtime",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Shared declarative Website Connector runtime for Fargo AI",
5
5
  "type": "module",
6
6
  "exports": {
@@ -15,6 +15,17 @@ export type WebsiteConnectorAuth =
15
15
  kind: "browser_storage";
16
16
  sendCookies: boolean;
17
17
  headerSources: Array<{ keySuffix: string; name: string; origin: string; prefix?: string }>;
18
+ secretSources: Array<{ id: string; keySuffix: string; origin: string }>;
19
+ refresh?: {
20
+ kind: "aws_cognito";
21
+ accessTokenSource: { keySuffix: string; origin: string };
22
+ clientId: string;
23
+ idTokenSource: { keySuffix: string; origin: string };
24
+ leewaySeconds: number;
25
+ refreshTokenSource: string;
26
+ region: string;
27
+ userPoolId: string;
28
+ };
18
29
  };
19
30
 
20
31
  export type WebsiteConnectorOperation = {
@@ -72,7 +83,7 @@ export type WebsiteConnectorRuntimeDescriptor = {
72
83
  };
73
84
 
74
85
  export const WEBSITE_CONNECTOR_SCHEMA_VERSION: "fargo.website-connector/v1";
75
- export const WEBSITE_CONNECTOR_RUNTIME_VERSION: "1.0.0";
86
+ export const WEBSITE_CONNECTOR_RUNTIME_VERSION: "1.1.0";
76
87
  export const WEBSITE_CONNECTOR_RUNTIME_CAPABILITIES: readonly string[];
77
88
  export class WebsiteConnectorDefinitionError extends Error {
78
89
  code: string;
@@ -108,6 +119,15 @@ export function executeWebsiteConnectorOperation(
108
119
  timeoutMs?: number;
109
120
  }
110
121
  ): Promise<unknown>;
122
+ export function refreshWebsiteConnectorAuthentication(
123
+ definition: unknown,
124
+ storageState: WebsiteConnectorBrowserStorageState,
125
+ options?: {
126
+ fetchImpl?: typeof fetch;
127
+ now?: () => number;
128
+ timeoutMs?: number;
129
+ }
130
+ ): Promise<{ refreshed: boolean; storageState: WebsiteConnectorBrowserStorageState }>;
111
131
  export function browserStorageAuthenticated(
112
132
  state: WebsiteConnectorBrowserStorageState,
113
133
  auth: WebsiteConnectorAuth,
@@ -2,8 +2,13 @@ const CONNECTOR_ID = /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/;
2
2
  const OPERATION_ID = /^[a-z][a-z0-9_]{1,62}[a-z0-9]$/;
3
3
  const TEMPLATE_ARGUMENT = /\{\{([a-zA-Z][a-zA-Z0-9_]*)\}\}/g;
4
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]+$/;
5
9
  const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
6
10
  const MAX_RESPONSE_BYTES = 1_048_576;
11
+ const DEFAULT_REFRESH_LEEWAY_SECONDS = 300;
7
12
  const FORBIDDEN_HEADERS = new Set([
8
13
  "connection",
9
14
  "content-length",
@@ -15,9 +20,10 @@ const FORBIDDEN_HEADERS = new Set([
15
20
  ]);
16
21
 
17
22
  export const WEBSITE_CONNECTOR_SCHEMA_VERSION = "fargo.website-connector/v1";
18
- export const WEBSITE_CONNECTOR_RUNTIME_VERSION = "1.0.0";
23
+ export const WEBSITE_CONNECTOR_RUNTIME_VERSION = "1.1.0";
19
24
  export const WEBSITE_CONNECTOR_RUNTIME_CAPABILITIES = Object.freeze([
20
25
  "auth.browser-storage",
26
+ "auth.refresh.aws-cognito",
21
27
  "request.http",
22
28
  "request.json-body",
23
29
  "request.write",
@@ -126,6 +132,9 @@ export function requiredWebsiteConnectorCapabilities(definition) {
126
132
  const parsed = parseWebsiteConnectorDefinition(definition);
127
133
  const required = new Set(["request.http"]);
128
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
+ }
129
138
  for (const operation of parsed.operations) {
130
139
  required.add(`response.${operation.request.responseMode}`);
131
140
  if (operation.request.body !== undefined) required.add("request.json-body");
@@ -220,11 +229,35 @@ export function browserStorageAuthenticated(state, auth, now = Date.now()) {
220
229
  if (auth.kind !== "browser_storage") return true;
221
230
  const seconds = now / 1000;
222
231
  const hasCookie = !auth.sendCookies || state.cookies.some((cookie) => cookie.expires === undefined || cookie.expires < 0 || cookie.expires > seconds);
223
- const hasHeaders = auth.headerSources.every((source) => {
232
+ const hasHeaderValues = auth.headerSources.every((source) => storageValue(state, source.origin, source.keySuffix) !== undefined);
233
+ const headersUnexpired = auth.headerSources.every((source) => {
224
234
  const value = storageValue(state, source.origin, source.keySuffix);
225
235
  return value !== undefined && credentialUnexpired(value, seconds);
226
236
  });
227
- return hasCookie && hasHeaders;
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 });
228
261
  }
229
262
 
230
263
  export function sanitizeWebsiteConnectorOutput(value, storageState) {
@@ -265,8 +298,75 @@ function parseAuthRecipe(value, allowedOrigins) {
265
298
  const prefix = optionalHeaderPrefix(binding.prefix, `/auth/headerSources/${index}/prefix`);
266
299
  return { keySuffix, name, origin, ...(prefix ? { prefix } : {}) };
267
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
+ }
268
320
  if (!sendCookies && headerSources.length === 0) fail("website_auth_recipe_empty", "/auth");
269
- return { kind: "browser_storage", sendCookies, headerSources };
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 };
270
370
  }
271
371
 
272
372
  function parseOperation(value, allowedOrigins, index) {
@@ -505,6 +605,112 @@ function compareSemver(left, right) {
505
605
  return 0;
506
606
  }
507
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
+
508
714
  function applyBrowserAuthentication(headers, auth, state, requestUrl) {
509
715
  if (auth.kind !== "browser_storage") {
510
716
  throw new WebsiteConnectorRuntimeError(503, "website_connector_auth_invalid");
@@ -531,16 +737,21 @@ function storageValue(state, origin, keySuffix) {
531
737
  return matches.length === 1 && matches[0]?.value ? String(matches[0].value) : undefined;
532
738
  }
533
739
 
534
- function credentialUnexpired(value, nowSeconds) {
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) {
535
746
  const parts = String(value).split(".");
536
- if (parts.length !== 3) return true;
747
+ if (parts.length !== 3) return undefined;
537
748
  try {
538
749
  const base64 = parts[1].replaceAll("-", "+").replaceAll("_", "/");
539
750
  const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
540
751
  const payload = JSON.parse(atob(padded));
541
- return typeof payload.exp !== "number" || payload.exp > nowSeconds + 30;
752
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : undefined;
542
753
  } catch {
543
- return true;
754
+ return undefined;
544
755
  }
545
756
  }
546
757
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fargowealth/fargo-ai-cli",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Agent-neutral Fargo AI CLI, MCP client, and LEGO project tooling",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  "node": ">=20.19.0"
18
18
  },
19
19
  "dependencies": {
20
- "@fargowealth/website-connector-runtime": "^1.0.0",
20
+ "@fargowealth/website-connector-runtime": "^1.1.0",
21
21
  "fflate": "^0.8.2",
22
22
  "ignore": "^7.0.6"
23
23
  },
package/src/constants.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const CLI_VERSION = "1.4.0";
1
+ export const CLI_VERSION = "1.4.1";
2
2
  export const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
3
3
  export const DEFAULT_AUTH_URL = "https://auth.fargowealth.app";
4
4
  export const DEFAULT_API_URL = "https://mcp.fargowealth.app";
@@ -52,7 +52,7 @@ export function buildWebsiteBrowserProfile(spec, state, capturedAt = new Date())
52
52
  ? array(state.cookies).filter((cookie) => cookieAllowed(cookie.domain, allowedHosts)).map(normalizeCookie)
53
53
  : [];
54
54
  const suffixesByOrigin = new Map();
55
- for (const source of captureSpec.auth.headerSources) {
55
+ for (const source of [...captureSpec.auth.headerSources, ...captureSpec.auth.secretSources]) {
56
56
  const suffixes = suffixesByOrigin.get(source.origin) ?? new Set();
57
57
  suffixes.add(source.keySuffix);
58
58
  suffixesByOrigin.set(source.origin, suffixes);
@@ -145,11 +145,21 @@ export function parseCaptureSpec(value) {
145
145
  }
146
146
  return { keySuffix, origin };
147
147
  });
148
+ const secretSources = array(auth.secretSources).map((item) => {
149
+ const binding = record(item, "Website Profile secret selector is invalid");
150
+ const origin = exactHttpsOrigin(binding.origin);
151
+ const keySuffix = String(binding.keySuffix || "");
152
+ if (!allowedOrigins.includes(origin) || !keySuffix || keySuffix.length > 240) {
153
+ throw new Error("Website Profile secret selector is invalid");
154
+ }
155
+ return { keySuffix, origin };
156
+ });
148
157
  if (headerSources.length > 12) throw new Error("Website Profile storage selector is invalid");
158
+ if (secretSources.length > 12) throw new Error("Website Profile secret selector is invalid");
149
159
  if (auth.sendCookies !== true && headerSources.length === 0) throw new Error("Website Profile authentication recipe is empty");
150
160
  return {
151
161
  allowedOrigins,
152
- auth: { headerSources, sendCookies: auth.sendCookies === true },
162
+ auth: { headerSources, secretSources, sendCookies: auth.sendCookies === true },
153
163
  connectorId,
154
164
  ...(localProfileKey ? { localProfileKey } : {}),
155
165
  loginUrl: loginUrl.toString(),
@@ -160,7 +170,9 @@ export function parseCaptureSpec(value) {
160
170
  async function originStorage(client, sessionId, spec) {
161
171
  await client.call("Page.enable", {}, sessionId);
162
172
  await client.call("Runtime.enable", {}, sessionId);
163
- const origins = [...new Set(spec.auth.headerSources.map((source) => source.origin))];
173
+ const origins = [...new Set(
174
+ [...spec.auth.headerSources, ...spec.auth.secretSources].map((source) => source.origin)
175
+ )];
164
176
  const result = [];
165
177
  for (const origin of origins) {
166
178
  await client.call("Page.navigate", { url: `${origin}/` }, sessionId);
@@ -342,7 +354,7 @@ function normalizeCookie(cookie) {
342
354
 
343
355
  function cookieAllowed(domain, allowedHosts) {
344
356
  const host = String(domain || "").toLowerCase().replace(/^\./, "");
345
- return allowedHosts.some((allowed) => host === allowed || host.endsWith(`.${allowed}`));
357
+ return allowedHosts.some((allowed) => allowed === host || allowed.endsWith(`.${host}`));
346
358
  }
347
359
 
348
360
  function exactHttpsOrigin(value) {
@@ -16,7 +16,7 @@ const WEBSITE_BRIDGE_URL = "https://website-bridge.fargowealth.app";
16
16
 
17
17
  export async function runWebsiteCommand(args, stdout, store, options = {}) {
18
18
  const action = args[1];
19
- if (action === "--help" || action === "-h" || action === "help") {
19
+ if (action === "help" || args.includes("--help") || args.includes("-h")) {
20
20
  stdout.write(WEBSITE_HELP);
21
21
  return;
22
22
  }
@@ -218,6 +218,7 @@ function localProfileSpec(definition) {
218
218
  allowedOrigins: definition.allowedOrigins,
219
219
  auth: {
220
220
  headerSources: definition.auth.headerSources.map(({ keySuffix, origin }) => ({ keySuffix, origin })),
221
+ secretSources: definition.auth.secretSources.map(({ keySuffix, origin }) => ({ keySuffix, origin })),
221
222
  sendCookies: definition.auth.sendCookies
222
223
  },
223
224
  connection: {},