@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 +11 -0
- package/README.md +8 -0
- package/node_modules/@fargowealth/website-connector-runtime/README.md +7 -0
- package/node_modules/@fargowealth/website-connector-runtime/package.json +1 -1
- package/node_modules/@fargowealth/website-connector-runtime/src/index.d.mts +21 -1
- package/node_modules/@fargowealth/website-connector-runtime/src/index.mjs +219 -8
- package/package.json +2 -2
- package/src/constants.mjs +1 -1
- package/src/website-browser-profile.mjs +16 -4
- package/src/website-cli.mjs +2 -1
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.
|
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
752
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : undefined;
|
|
542
753
|
} catch {
|
|
543
|
-
return
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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) =>
|
|
357
|
+
return allowedHosts.some((allowed) => allowed === host || allowed.endsWith(`.${host}`));
|
|
346
358
|
}
|
|
347
359
|
|
|
348
360
|
function exactHttpsOrigin(value) {
|
package/src/website-cli.mjs
CHANGED
|
@@ -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 === "
|
|
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: {},
|