@agent-native/core 0.151.2 → 0.152.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/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/analytics/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/analytics/actions/bigquery-query.ts +21 -0
- package/corpus/templates/analytics/app/components/ConnectionTestStatus.tsx +65 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +3 -3
- package/corpus/templates/analytics/app/pages/DataSources.tsx +15 -37
- package/corpus/templates/analytics/app/root.tsx +0 -1
- package/corpus/templates/analytics/changelog/2026-08-11-analytics-custom-blocks-keep-loading-with-legacy-bigquery-action.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-11-analytics-routes-company-knowledge-and-slack-context-questio.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/analytics/public/manifest.json +0 -1
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +5 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +5 -0
- package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/assets/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/assets/app/root.tsx +0 -1
- package/corpus/templates/assets/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/assets/public/manifest.json +0 -1
- package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/brain/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/brain/app/root.tsx +0 -1
- package/corpus/templates/brain/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/brain/public/manifest.json +0 -1
- package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/calendar/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +0 -53
- package/corpus/templates/calendar/app/i18n-data.ts +0 -80
- package/corpus/templates/calendar/app/root.tsx +0 -1
- package/corpus/templates/calendar/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-11-google-calendar-connection-buttons-now-open-the-sign-in-flow.md +6 -0
- package/corpus/templates/calendar/public/manifest.json +0 -1
- package/corpus/templates/calendar/server/plugins/auth.ts +0 -10
- package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/chat/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/chat/app/root.tsx +0 -1
- package/corpus/templates/chat/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/chat/public/manifest.json +0 -1
- package/corpus/templates/clips/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +1 -1
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +6 -0
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +20 -3
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -4
- package/corpus/templates/clips/app/root.tsx +0 -1
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +1 -3
- package/corpus/templates/clips/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-11-clips-desktop-settings-are-now-easier-to-scan-with-a-wider-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-11-google-calendar-connections-from-meetings-now-open-directly.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-11-recording-viewers-can-comment-and-react.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-11-system-audio-recording-now-lives-inside-the-microphone-menu-.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-11-timestamped-comments-stay-on-the-video-for-three-seconds-wh.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +773 -1001
- package/corpus/templates/clips/desktop/src/components/MediaDeviceRow.tsx +15 -27
- package/corpus/templates/clips/desktop/src/styles.css +364 -5
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +1 -1
- package/corpus/templates/clips/public/manifest.json +0 -1
- package/corpus/templates/content/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/content/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/content/AGENTS.md +3 -0
- package/corpus/templates/content/actions/resolve-content-landing.ts +120 -0
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +196 -30
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +9 -0
- package/corpus/templates/content/app/i18n-data.ts +58 -0
- package/corpus/templates/content/app/lib/content-landing.ts +22 -0
- package/corpus/templates/content/app/root.tsx +0 -1
- package/corpus/templates/content/app/routes/_app._index.tsx +36 -26
- package/corpus/templates/content/changelog/2026-08-03-choose-text-or-a-heading-from-one-compact-formatting-control.md +6 -0
- package/corpus/templates/content/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/content/changelog/2026-08-11-content-opens-your-last-page-on-return-and-gives-new-users-a.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.workspace.session-restore.md +7 -1
- package/corpus/templates/content/docs/solutions/evidence/content-text-selector/README.md +26 -0
- package/corpus/templates/content/parity/matrix.md +1 -0
- package/corpus/templates/content/parity/matrix.ts +22 -0
- package/corpus/templates/content/public/manifest.json +0 -1
- package/corpus/templates/content/shared/content-landing.ts +17 -0
- package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/crm/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/crm/app/root.tsx +0 -1
- package/corpus/templates/crm/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/crm/public/manifest.json +0 -1
- package/corpus/templates/design/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/design/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/design/app/i18n/en-US.ts +5 -0
- package/corpus/templates/design/app/root.tsx +0 -1
- package/corpus/templates/design/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/design/changelog/2026-08-11-design-sharing-now-identifies-viewers-who-can-add-review-com.md +6 -0
- package/corpus/templates/design/public/manifest.json +0 -1
- package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/dispatch/AGENTS.md +9 -0
- package/corpus/templates/dispatch/actions/list-dispatch-usage-metrics.ts +5 -19
- package/corpus/templates/dispatch/actions/view-screen.ts +10 -3
- package/corpus/templates/dispatch/app/root.tsx +0 -1
- package/corpus/templates/dispatch/app/routes/settings.tsx +13 -1
- package/corpus/templates/dispatch/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-clicking-the-dispatch-logo-returns-to-the-overview-page.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-no-longer-links-to-itself-from-the-app-switcher.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-settings-now-link-directly-to-admin.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-shows-usage-trends-recent-prompts-and-an-agent-revi.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-human-friendly-app-names.md +6 -0
- package/corpus/templates/dispatch/public/manifest.json +0 -1
- package/corpus/templates/dispatch/server/lib/identity-sso.ts +432 -158
- package/corpus/templates/dispatch/server/plugins/identity-sso.ts +204 -123
- package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +1 -0
- package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/factory/app/root.tsx +0 -1
- package/corpus/templates/factory/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/factory/public/manifest.json +0 -1
- package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/forms/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/forms/app/root.tsx +0 -1
- package/corpus/templates/forms/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/forms/public/manifest.json +0 -1
- package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/macros/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/mail/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/mail/app/root.tsx +0 -1
- package/corpus/templates/mail/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/mail/changelog/2026-08-11-google-sign-in-now-opens-the-gmail-connection-flow-directly.md +6 -0
- package/corpus/templates/mail/public/manifest.json +0 -1
- package/corpus/templates/mail/server/plugins/auth.ts +0 -10
- package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/plan/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/plan/app/i18n/en-US.ts +2 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +6 -0
- package/corpus/templates/plan/app/root.tsx +0 -1
- package/corpus/templates/plan/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/plan/changelog/2026-08-11-plan-sharing-now-identifies-viewers-who-can-add-comments.md +6 -0
- package/corpus/templates/plan/public/manifest.json +0 -1
- package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/slides/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +2 -0
- package/corpus/templates/slides/app/root.tsx +0 -1
- package/corpus/templates/slides/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-share-dialog-uses-commenter-terminology.md +6 -0
- package/corpus/templates/slides/public/manifest.json +0 -1
- package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +1 -1
- package/corpus/templates/tasks/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/tasks/app/root.tsx +0 -1
- package/corpus/templates/tasks/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/corpus/templates/tasks/public/manifest.json +0 -1
- package/dist/agent/production-agent.d.ts +8 -0
- package/dist/agent/production-agent.js +67 -22
- package/dist/agent/run-manager.d.ts +7 -0
- package/dist/agent/run-manager.js +46 -3
- package/dist/agent/tool-approval-migrations.d.ts +40 -0
- package/dist/agent/tool-approval-migrations.js +51 -0
- package/dist/agent/tool-approval-store.js +5 -20
- package/dist/cli/templates-meta.js +2 -2
- package/dist/client/AgentPanel.js +10 -1
- package/dist/client/AssistantChat.d.ts +3 -0
- package/dist/client/AssistantChat.js +151 -141
- package/dist/client/agent-page/AgentTabsPage.js +2 -2
- package/dist/client/chat/attachment-adapters.js +8 -2
- package/dist/client/chat/tool-call-display.d.ts +1 -0
- package/dist/client/chat/tool-call-display.js +3 -1
- package/dist/client/chat-first.js +11 -5
- package/dist/client/guided-questions.d.ts +15 -1
- package/dist/client/guided-questions.js +33 -15
- package/dist/client/hooks/index.d.ts +1 -1
- package/dist/client/hooks/index.js +1 -1
- package/dist/client/org/OrgSwitcher.d.ts +3 -1
- package/dist/client/org/OrgSwitcher.js +22 -16
- package/dist/client/org/WorkspaceNotice.js +3 -1
- package/dist/client/resources/index.d.ts +1 -1
- package/dist/client/resources/index.js +1 -1
- package/dist/client/resources/use-mcp-servers.d.ts +1 -0
- package/dist/client/resources/use-mcp-servers.js +14 -0
- package/dist/client/settings/AccountSettingsCard.js +8 -4
- package/dist/client/settings/SettingsPanel.js +46 -13
- package/dist/client/settings/SettingsTabsPage.js +8 -3
- package/dist/client/sharing/ShareButton.d.ts +7 -1
- package/dist/client/sharing/ShareButton.js +14 -7
- package/dist/client/sharing/ShareDialog.js +5 -1
- package/dist/client/sharing/useShareButtonController.d.ts +4 -0
- package/dist/client/sharing/useShareButtonController.js +15 -0
- package/dist/client/sharing/useShareDialogController.d.ts +7 -0
- package/dist/client/sharing/useShareDialogController.js +14 -0
- package/dist/client/sse-event-processor.js +37 -0
- package/dist/client/use-session.d.ts +2 -0
- package/dist/client/use-session.js +69 -5
- package/dist/deploy/build.js +1 -1
- package/dist/file-upload/pre-upload-attachments.d.ts +3 -4
- package/dist/file-upload/pre-upload-attachments.js +65 -17
- package/dist/ingestion/index.d.ts +1 -0
- package/dist/ingestion/index.js +1 -0
- package/dist/ingestion/office.d.ts +3 -3
- package/dist/ingestion/office.js +24 -2
- package/dist/ingestion/spreadsheet.d.ts +41 -0
- package/dist/ingestion/spreadsheet.js +172 -0
- package/dist/integrations/adapters/slack.js +7 -3
- package/dist/integrations/webhook-handler.js +11 -3
- package/dist/localization/default-messages.d.ts +3 -0
- package/dist/localization/default-messages.js +3 -0
- package/dist/mcp/embed-app.js +20 -9
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/oauth-tokens/migrations.d.ts +8 -0
- package/dist/oauth-tokens/migrations.js +36 -0
- package/dist/observability/routes.d.ts +5 -5
- package/dist/org/workspace-url.d.ts +8 -0
- package/dist/org/workspace-url.js +26 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat/context-tools.js +27 -6
- package/dist/server/agent-chat-plugin.js +16 -8
- package/dist/server/auth.d.ts +0 -11
- package/dist/server/auth.js +37 -20
- package/dist/server/better-auth-migrations.js +23 -0
- package/dist/server/email-template.d.ts +5 -0
- package/dist/server/email-template.js +9 -0
- package/dist/server/embed-route.js +17 -5
- package/dist/server/identity-sso-store.d.ts +40 -66
- package/dist/server/identity-sso-store.js +187 -150
- package/dist/server/identity-sso.d.ts +17 -73
- package/dist/server/identity-sso.js +294 -297
- package/dist/server/onboarding-html.d.ts +0 -12
- package/dist/server/onboarding-html.js +46 -269
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/release-migrations.js +8 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/workspace-app-id.d.ts +2 -0
- package/dist/shared/workspace-app-id.js +10 -0
- package/dist/sharing/actions/share-resource.d.ts +1 -0
- package/dist/sharing/actions/share-resource.js +20 -5
- package/dist/templates/chat/.agents/skills/sharing/SKILL.md +1 -1
- package/dist/templates/chat/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/dist/templates/chat/app/root.tsx +0 -1
- package/dist/templates/chat/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/dist/templates/chat/public/manifest.json +0 -1
- package/dist/templates/default/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/dist/templates/default/app/root.tsx +0 -1
- package/dist/templates/default/public/manifest.json +0 -1
- package/dist/templates/headless/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +22 -11
- package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/docs/content/cross-app-sso.mdx +85 -27
- package/docs/content/template-mail.mdx +0 -6
- package/package.json +4 -3
- package/src/templates/chat/.agents/skills/sharing/SKILL.md +1 -1
- package/src/templates/chat/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/src/templates/chat/app/root.tsx +0 -1
- package/src/templates/chat/changelog/2026-08-11-browser-no-longer-offers-desktop-install.md +6 -0
- package/src/templates/chat/public/manifest.json +0 -1
- package/src/templates/default/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/src/templates/default/app/root.tsx +0 -1
- package/src/templates/default/public/manifest.json +0 -1
- package/src/templates/headless/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +22 -11
- package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +1 -1
- package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +24 -0
- package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +0 -6
- package/corpus/templates/clips/app/components/meetings/google-oauth-identity-notice.tsx +0 -51
- package/corpus/templates/clips/changelog/2026-07-30-google-calendar-warning.md +0 -6
|
@@ -1,88 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Framework-table store for the cross-app SSO
|
|
3
|
-
* CLIENT side. Backs two pieces of the federated login round-trip:
|
|
2
|
+
* Framework-table store for the cross-app SSO client.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* optional same-origin `return` path so the user lands back where they
|
|
9
|
-
* started after federated sign-in.
|
|
10
|
-
* - `identity_sso_jti` — replayed-token guard. The hub-issued identity JWT
|
|
11
|
-
* carries a random `jti`; the first callback that verifies a given `jti`
|
|
12
|
-
* records it here, and any later callback that presents the same `jti`
|
|
13
|
-
* is rejected. Best-effort: a DB blip never widens the trust boundary
|
|
14
|
-
* (signature + exp + scope + single-use state are still enforced), it
|
|
15
|
-
* only relaxes the extra replay gate.
|
|
4
|
+
* The current protocol uses a fresh flow-state table instead of extending the
|
|
5
|
+
* original `identity_sso_state` table. That keeps the migration additive for
|
|
6
|
+
* deployments that already have the merged PR's schema:
|
|
16
7
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
8
|
+
* - `identity_sso_flow_state` binds state to the exact app, client,
|
|
9
|
+
* authority, callback, and PKCE challenge. State is single-use.
|
|
10
|
+
* - `identity_sso_jti` remains a defence-in-depth replay guard for the
|
|
11
|
+
* server-to-server assertion. The assertion is never sent through the
|
|
12
|
+
* browser; the one-time authorization code is the only browser credential.
|
|
21
13
|
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Read + normalise `AGENT_NATIVE_IDENTITY_HUB_URL`. Returns `undefined`
|
|
26
|
-
* (feature OFF) unless it is set to a syntactically valid http(s) URL. A
|
|
27
|
-
* malformed value is treated as OFF rather than throwing, so a typo can
|
|
28
|
-
* never brick an app's login — it just behaves as if SSO were unconfigured.
|
|
14
|
+
* Uses the same portable raw-SQL pattern as the other framework stores. DDL is
|
|
15
|
+
* additive and lazy, and PostgreSQL creation goes through the DDL guard.
|
|
29
16
|
*/
|
|
17
|
+
export declare const SSO_STATE_TTL_MS: number;
|
|
18
|
+
export declare const SSO_LOGIN_MAX = 60;
|
|
19
|
+
export declare const SSO_LOGIN_WINDOW_MS = 60000;
|
|
30
20
|
export declare function getIdentityHubUrl(): string | undefined;
|
|
31
|
-
/**
|
|
32
|
-
* Whether the federated-SSO client is active. When false, NOTHING in the
|
|
33
|
-
* SSO module has any effect: the route 404s, the guard bypass is inert, the
|
|
34
|
-
* login button is not rendered. This is the single switch the
|
|
35
|
-
* env-unset-no-op invariant is asserted against.
|
|
36
|
-
*/
|
|
37
21
|
export declare function isIdentitySsoEnabled(): boolean;
|
|
38
22
|
export declare function isDesktopSsoCanaryUserAgent(userAgent: string | undefined): boolean;
|
|
39
23
|
export declare function isCanonicalAgentNativeAppOrigin(origin: string | undefined): boolean;
|
|
40
24
|
export declare function isCanonicalAgentNativeAppRequest(host: string | undefined, forwardedProtocol: string | undefined): boolean;
|
|
41
25
|
/**
|
|
42
|
-
* The conditional
|
|
43
|
-
*
|
|
44
|
-
* disabled so the login HTML is byte-for-byte identical to today's output
|
|
45
|
-
* with the env unset (asserted by the env-unset-no-op regression test). Pure
|
|
46
|
-
* string builder, no I/O — safe to call during HTML render. Lives in this
|
|
47
|
-
* leaf module so `onboarding-html.ts` can import it without creating an
|
|
48
|
-
* `auth.ts` ↔ `identity-sso.ts` import cycle.
|
|
26
|
+
* The conditional login entry is the only browser UI this feature adds. It
|
|
27
|
+
* stays byte-for-byte absent when the direct web federation env is unset.
|
|
49
28
|
*/
|
|
50
29
|
export declare function identitySsoLoginButtonHtml(): string;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
export
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*/
|
|
67
|
-
export declare function createSsoState(returnPath: string | null): Promise<string>;
|
|
30
|
+
export interface CreateSsoStateInput {
|
|
31
|
+
returnPath: string | null;
|
|
32
|
+
appId: string;
|
|
33
|
+
clientId: string;
|
|
34
|
+
redirectUri: string;
|
|
35
|
+
authority: string;
|
|
36
|
+
codeChallenge: string;
|
|
37
|
+
}
|
|
38
|
+
export interface SsoStateBinding {
|
|
39
|
+
appId: string;
|
|
40
|
+
clientId: string;
|
|
41
|
+
redirectUri: string;
|
|
42
|
+
authority: string;
|
|
43
|
+
codeChallenge: string;
|
|
44
|
+
}
|
|
68
45
|
export interface SsoStateConsumeResult {
|
|
69
46
|
ok: boolean;
|
|
70
47
|
returnPath: string | null;
|
|
71
48
|
}
|
|
49
|
+
/** Mint and persist a bound, crypto-random state value. */
|
|
50
|
+
export declare function createSsoState(input: CreateSsoStateInput): Promise<string>;
|
|
72
51
|
/**
|
|
73
|
-
* Atomically consume
|
|
74
|
-
*
|
|
75
|
-
* and this call is the one that transitioned it to consumed (single-use,
|
|
76
|
-
* enforced via a conditional UPDATE so a double callback can't both pass).
|
|
77
|
-
* Any other condition returns `{ ok: false }`.
|
|
52
|
+
* Atomically consume state only when every security binding still matches.
|
|
53
|
+
* A missing or mismatched binding is indistinguishable from an unknown state.
|
|
78
54
|
*/
|
|
79
|
-
export declare function consumeSsoState(state: string): Promise<SsoStateConsumeResult>;
|
|
55
|
+
export declare function consumeSsoState(state: string, expected: SsoStateBinding): Promise<SsoStateConsumeResult>;
|
|
80
56
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* replay
|
|
85
|
-
* sign-in — signature + exp + scope + single-use CSRF state remain the hard
|
|
86
|
-
* gates; this only adds defence in depth against token replay.
|
|
57
|
+
* Strict replay defense for the server-to-server assertion. A database error
|
|
58
|
+
* fails closed here: code exchange already provides the primary single-use
|
|
59
|
+
* guarantee, and refusing a login is safer than accepting an unverifiable
|
|
60
|
+
* replay boundary.
|
|
87
61
|
*/
|
|
88
62
|
export declare function isJtiReplayed(jti: string | undefined): Promise<boolean>;
|
|
@@ -1,28 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Framework-table store for the cross-app SSO
|
|
3
|
-
* CLIENT side. Backs two pieces of the federated login round-trip:
|
|
2
|
+
* Framework-table store for the cross-app SSO client.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* optional same-origin `return` path so the user lands back where they
|
|
9
|
-
* started after federated sign-in.
|
|
10
|
-
* - `identity_sso_jti` — replayed-token guard. The hub-issued identity JWT
|
|
11
|
-
* carries a random `jti`; the first callback that verifies a given `jti`
|
|
12
|
-
* records it here, and any later callback that presents the same `jti`
|
|
13
|
-
* is rejected. Best-effort: a DB blip never widens the trust boundary
|
|
14
|
-
* (signature + exp + scope + single-use state are still enforced), it
|
|
15
|
-
* only relaxes the extra replay gate.
|
|
4
|
+
* The current protocol uses a fresh flow-state table instead of extending the
|
|
5
|
+
* original `identity_sso_state` table. That keeps the migration additive for
|
|
6
|
+
* deployments that already have the merged PR's schema:
|
|
16
7
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
8
|
+
* - `identity_sso_flow_state` binds state to the exact app, client,
|
|
9
|
+
* authority, callback, and PKCE challenge. State is single-use.
|
|
10
|
+
* - `identity_sso_jti` remains a defence-in-depth replay guard for the
|
|
11
|
+
* server-to-server assertion. The assertion is never sent through the
|
|
12
|
+
* browser; the one-time authorization code is the only browser credential.
|
|
21
13
|
*
|
|
22
|
-
*
|
|
14
|
+
* Uses the same portable raw-SQL pattern as the other framework stores. DDL is
|
|
15
|
+
* additive and lazy, and PostgreSQL creation goes through the DDL guard.
|
|
23
16
|
*/
|
|
24
17
|
import { randomBytes } from "node:crypto";
|
|
25
|
-
import { getDbExec,
|
|
18
|
+
import { getDbExec, intType, isConnectionError, isPostgres, } from "../db/client.js";
|
|
26
19
|
import { ensureTableExists } from "../db/ddl-guard.js";
|
|
27
20
|
let _initPromise;
|
|
28
21
|
const DESKTOP_SSO_CANARY_USER_AGENT = /AgentNativeDesktopSsoCanary\//i;
|
|
@@ -44,39 +37,38 @@ const CANONICAL_IDENTITY_SSO_APP_ORIGINS = new Set([
|
|
|
44
37
|
"https://slides.agent-native.com",
|
|
45
38
|
"https://tasks.agent-native.com",
|
|
46
39
|
]);
|
|
40
|
+
export const SSO_STATE_TTL_MS = 10 * 60_000;
|
|
41
|
+
export const SSO_LOGIN_MAX = 60;
|
|
42
|
+
export const SSO_LOGIN_WINDOW_MS = 60_000;
|
|
43
|
+
const STATE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
44
|
+
const APP_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
45
|
+
const CLIENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
46
|
+
const CODE_CHALLENGE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
47
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
48
|
+
const LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
47
49
|
// ---------------------------------------------------------------------------
|
|
48
|
-
// Feature switch —
|
|
49
|
-
//
|
|
50
|
-
// so BOTH the auth guard and the route handler import the identical
|
|
51
|
-
// validator and can never drift. Pure env read, no I/O — safe on the guard
|
|
52
|
-
// hot path.
|
|
50
|
+
// Feature switch — this module is intentionally dependency-light because the
|
|
51
|
+
// auth guard and the route handler both import the same pure switch.
|
|
53
52
|
// ---------------------------------------------------------------------------
|
|
54
|
-
/**
|
|
55
|
-
* Read + normalise `AGENT_NATIVE_IDENTITY_HUB_URL`. Returns `undefined`
|
|
56
|
-
* (feature OFF) unless it is set to a syntactically valid http(s) URL. A
|
|
57
|
-
* malformed value is treated as OFF rather than throwing, so a typo can
|
|
58
|
-
* never brick an app's login — it just behaves as if SSO were unconfigured.
|
|
59
|
-
*/
|
|
60
53
|
export function getIdentityHubUrl() {
|
|
61
54
|
const raw = process.env.AGENT_NATIVE_IDENTITY_HUB_URL?.trim();
|
|
62
55
|
if (!raw)
|
|
63
56
|
return undefined;
|
|
64
57
|
try {
|
|
65
58
|
const u = new URL(raw);
|
|
66
|
-
if (u.protocol !== "https:" &&
|
|
59
|
+
if (u.protocol !== "https:" &&
|
|
60
|
+
!(u.protocol === "http:" && LOCALHOST_HOSTS.has(u.hostname))) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
if (u.username || u.password || u.search || u.hash)
|
|
67
64
|
return undefined;
|
|
68
65
|
return `${u.protocol}//${u.host}${u.pathname}`.replace(/\/+$/, "");
|
|
69
66
|
}
|
|
70
|
-
catch {
|
|
67
|
+
catch (error) {
|
|
68
|
+
void error;
|
|
71
69
|
return undefined;
|
|
72
70
|
}
|
|
73
71
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Whether the federated-SSO client is active. When false, NOTHING in the
|
|
76
|
-
* SSO module has any effect: the route 404s, the guard bypass is inert, the
|
|
77
|
-
* login button is not rendered. This is the single switch the
|
|
78
|
-
* env-unset-no-op invariant is asserted against.
|
|
79
|
-
*/
|
|
80
72
|
export function isIdentitySsoEnabled() {
|
|
81
73
|
return !!getIdentityHubUrl();
|
|
82
74
|
}
|
|
@@ -96,24 +88,19 @@ export function isCanonicalAgentNativeAppOrigin(origin) {
|
|
|
96
88
|
!parsed.hash &&
|
|
97
89
|
CANONICAL_IDENTITY_SSO_APP_ORIGINS.has(parsed.origin));
|
|
98
90
|
}
|
|
99
|
-
catch {
|
|
91
|
+
catch (error) {
|
|
92
|
+
void error;
|
|
100
93
|
return false;
|
|
101
94
|
}
|
|
102
95
|
}
|
|
103
96
|
export function isCanonicalAgentNativeAppRequest(host, forwardedProtocol) {
|
|
104
|
-
if (!host || forwardedProtocol !== "https")
|
|
97
|
+
if (!host || forwardedProtocol !== "https")
|
|
105
98
|
return false;
|
|
106
|
-
}
|
|
107
99
|
return isCanonicalAgentNativeAppOrigin(`https://${host}`);
|
|
108
100
|
}
|
|
109
101
|
/**
|
|
110
|
-
* The conditional
|
|
111
|
-
*
|
|
112
|
-
* disabled so the login HTML is byte-for-byte identical to today's output
|
|
113
|
-
* with the env unset (asserted by the env-unset-no-op regression test). Pure
|
|
114
|
-
* string builder, no I/O — safe to call during HTML render. Lives in this
|
|
115
|
-
* leaf module so `onboarding-html.ts` can import it without creating an
|
|
116
|
-
* `auth.ts` ↔ `identity-sso.ts` import cycle.
|
|
102
|
+
* The conditional login entry is the only browser UI this feature adds. It
|
|
103
|
+
* stays byte-for-byte absent when the direct web federation env is unset.
|
|
117
104
|
*/
|
|
118
105
|
export function identitySsoLoginButtonHtml() {
|
|
119
106
|
if (!isIdentitySsoEnabled())
|
|
@@ -126,24 +113,16 @@ export function identitySsoLoginButtonHtml() {
|
|
|
126
113
|
`color:inherit;font:inherit;font-weight:600;text-decoration:none;` +
|
|
127
114
|
`cursor:pointer">Sign in with Agent-Native</a>\n`);
|
|
128
115
|
}
|
|
129
|
-
|
|
130
|
-
export const SSO_STATE_TTL_MS = 10 * 60_000;
|
|
131
|
-
/**
|
|
132
|
-
* Rate limit for `identity/login`: at most this many state rows may be
|
|
133
|
-
* created within `SSO_LOGIN_WINDOW_MS`. The endpoint is reachable without a
|
|
134
|
-
* session (it's the entry point), so keep a coarse global cap to stop table
|
|
135
|
-
* flooding without per-IP plumbing.
|
|
136
|
-
*/
|
|
137
|
-
export const SSO_LOGIN_MAX = 60;
|
|
138
|
-
export const SSO_LOGIN_WINDOW_MS = 60_000;
|
|
139
|
-
// Build the CREATE SQL lazily (not at module scope) so intType() runs at
|
|
140
|
-
// RUNTIME, not import time — a module-scope call breaks any consumer whose
|
|
141
|
-
// db/client mock doesn't stub intType (e.g. db-admin specs).
|
|
142
|
-
function buildIdentitySsoStateCreateSql() {
|
|
116
|
+
function buildIdentitySsoFlowStateCreateSql() {
|
|
143
117
|
return `
|
|
144
|
-
CREATE TABLE IF NOT EXISTS
|
|
118
|
+
CREATE TABLE IF NOT EXISTS identity_sso_flow_state (
|
|
145
119
|
state TEXT PRIMARY KEY,
|
|
146
120
|
return_path TEXT,
|
|
121
|
+
app_id TEXT NOT NULL,
|
|
122
|
+
client_id TEXT NOT NULL,
|
|
123
|
+
redirect_uri TEXT NOT NULL,
|
|
124
|
+
authority TEXT NOT NULL,
|
|
125
|
+
code_challenge TEXT NOT NULL,
|
|
147
126
|
created_at ${intType()},
|
|
148
127
|
expires_at ${intType()},
|
|
149
128
|
consumed_at ${intType()}
|
|
@@ -161,94 +140,138 @@ function buildIdentitySsoJtiCreateSql() {
|
|
|
161
140
|
async function ensureTable() {
|
|
162
141
|
if (!_initPromise) {
|
|
163
142
|
_initPromise = (async () => {
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const identitySsoJtiCreateSql = buildIdentitySsoJtiCreateSql();
|
|
167
|
-
// Additive only. Never DROP / ALTER — this DB is shared across every
|
|
168
|
-
// deploy context (preview/branch/prod) for hosted templates.
|
|
143
|
+
const flowStateSql = buildIdentitySsoFlowStateCreateSql();
|
|
144
|
+
const jtiSql = buildIdentitySsoJtiCreateSql();
|
|
169
145
|
if (isPostgres()) {
|
|
170
|
-
|
|
171
|
-
await ensureTableExists("
|
|
172
|
-
await ensureTableExists("identity_sso_jti", identitySsoJtiCreateSql);
|
|
146
|
+
await ensureTableExists("identity_sso_flow_state", flowStateSql);
|
|
147
|
+
await ensureTableExists("identity_sso_jti", jtiSql);
|
|
173
148
|
return;
|
|
174
149
|
}
|
|
175
|
-
|
|
176
|
-
await client.execute(
|
|
177
|
-
await client.execute(
|
|
178
|
-
})().catch((
|
|
179
|
-
// Don't cache a rejection — let the next caller retry a fresh init.
|
|
150
|
+
const client = getDbExec();
|
|
151
|
+
await client.execute(flowStateSql);
|
|
152
|
+
await client.execute(jtiSql);
|
|
153
|
+
})().catch((error) => {
|
|
180
154
|
_initPromise = undefined;
|
|
181
|
-
throw
|
|
155
|
+
throw error;
|
|
182
156
|
});
|
|
183
157
|
}
|
|
184
158
|
return _initPromise;
|
|
185
159
|
}
|
|
186
|
-
function numOrNull(
|
|
187
|
-
if (
|
|
160
|
+
function numOrNull(value) {
|
|
161
|
+
if (value == null)
|
|
188
162
|
return null;
|
|
189
|
-
const
|
|
190
|
-
return Number.isFinite(
|
|
163
|
+
const number = Number(value);
|
|
164
|
+
return Number.isFinite(number) ? number : null;
|
|
191
165
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
166
|
+
function stringOrNull(value) {
|
|
167
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
168
|
+
}
|
|
169
|
+
function affectedRows(result) {
|
|
170
|
+
return Number(result?.rowsAffected ?? result?.rowCount ?? result?.count ?? 0);
|
|
171
|
+
}
|
|
172
|
+
function isSafeStateInput(input) {
|
|
173
|
+
if (!input.appId ||
|
|
174
|
+
!input.clientId ||
|
|
175
|
+
!input.redirectUri ||
|
|
176
|
+
!input.authority ||
|
|
177
|
+
!input.codeChallenge) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
if (!APP_ID_PATTERN.test(input.appId))
|
|
181
|
+
return false;
|
|
182
|
+
if (!CLIENT_ID_PATTERN.test(input.clientId))
|
|
183
|
+
return false;
|
|
184
|
+
if (!CODE_CHALLENGE_PATTERN.test(input.codeChallenge))
|
|
185
|
+
return false;
|
|
186
|
+
if (CONTROL_CHARS.test(input.redirectUri) ||
|
|
187
|
+
CONTROL_CHARS.test(input.authority)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const redirect = new URL(input.redirectUri);
|
|
192
|
+
const authority = new URL(input.authority);
|
|
193
|
+
if (redirect.username ||
|
|
194
|
+
redirect.password ||
|
|
195
|
+
authority.username ||
|
|
196
|
+
authority.password) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
const secureOrLoopback = (url) => url.protocol === "https:" ||
|
|
200
|
+
(url.protocol === "http:" && LOCALHOST_HOSTS.has(url.hostname));
|
|
201
|
+
if (!secureOrLoopback(redirect) || !secureOrLoopback(authority)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
void error;
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
/** Mint and persist a bound, crypto-random state value. */
|
|
212
|
+
export async function createSsoState(input) {
|
|
213
|
+
if (!isSafeStateInput(input))
|
|
214
|
+
throw new Error("INVALID_SSO_STATE");
|
|
202
215
|
await ensureTable();
|
|
203
216
|
const client = getDbExec();
|
|
204
217
|
const now = Date.now();
|
|
205
218
|
try {
|
|
206
219
|
const { rows } = await client.execute({
|
|
207
|
-
sql:
|
|
220
|
+
sql: "SELECT COUNT(*) AS n FROM identity_sso_flow_state WHERE created_at > ?",
|
|
208
221
|
args: [now - SSO_LOGIN_WINDOW_MS],
|
|
209
222
|
});
|
|
210
|
-
const
|
|
211
|
-
if (Number.isFinite(
|
|
223
|
+
const count = Number(rows[0]?.n ?? rows[0]?.["COUNT(*)"] ?? 0);
|
|
224
|
+
if (Number.isFinite(count) && count >= SSO_LOGIN_MAX) {
|
|
212
225
|
throw new Error("RATE_LIMITED");
|
|
213
226
|
}
|
|
214
227
|
}
|
|
215
|
-
catch (
|
|
216
|
-
if (
|
|
217
|
-
throw
|
|
218
|
-
// A read failure
|
|
219
|
-
//
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (error?.message === "RATE_LIMITED")
|
|
230
|
+
throw error;
|
|
231
|
+
// A rate-limit read failure does not widen the auth boundary. The state
|
|
232
|
+
// remains high entropy, bound, short-lived, and single-use.
|
|
220
233
|
}
|
|
221
234
|
const state = randomBytes(32).toString("base64url");
|
|
222
|
-
const expiresAt = now + SSO_STATE_TTL_MS;
|
|
223
235
|
await client.execute({
|
|
224
|
-
sql:
|
|
225
|
-
|
|
236
|
+
sql: "INSERT INTO identity_sso_flow_state " +
|
|
237
|
+
"(state, return_path, app_id, client_id, redirect_uri, authority, code_challenge, created_at, expires_at, consumed_at) " +
|
|
238
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
239
|
+
args: [
|
|
240
|
+
state,
|
|
241
|
+
input.returnPath,
|
|
242
|
+
input.appId,
|
|
243
|
+
input.clientId,
|
|
244
|
+
input.redirectUri,
|
|
245
|
+
input.authority,
|
|
246
|
+
input.codeChallenge,
|
|
247
|
+
now,
|
|
248
|
+
now + SSO_STATE_TTL_MS,
|
|
249
|
+
null,
|
|
250
|
+
],
|
|
226
251
|
});
|
|
227
|
-
// Fire-and-forget: prune fully-expired rows (consumed or abandoned). Without
|
|
228
|
-
// this the table grows unbounded and the rate-limit COUNT(*) above slows down.
|
|
229
252
|
void client
|
|
230
253
|
.execute({
|
|
231
|
-
sql:
|
|
254
|
+
sql: "DELETE FROM identity_sso_flow_state WHERE expires_at < ?",
|
|
232
255
|
args: [now],
|
|
233
256
|
})
|
|
234
257
|
.catch(() => { });
|
|
235
258
|
return state;
|
|
236
259
|
}
|
|
237
260
|
/**
|
|
238
|
-
* Atomically consume
|
|
239
|
-
*
|
|
240
|
-
* and this call is the one that transitioned it to consumed (single-use,
|
|
241
|
-
* enforced via a conditional UPDATE so a double callback can't both pass).
|
|
242
|
-
* Any other condition returns `{ ok: false }`.
|
|
261
|
+
* Atomically consume state only when every security binding still matches.
|
|
262
|
+
* A missing or mismatched binding is indistinguishable from an unknown state.
|
|
243
263
|
*/
|
|
244
|
-
export async function consumeSsoState(state) {
|
|
245
|
-
if (!state)
|
|
264
|
+
export async function consumeSsoState(state, expected) {
|
|
265
|
+
if (!STATE_PATTERN.test(state) ||
|
|
266
|
+
!isSafeStateInput({ ...expected, returnPath: null })) {
|
|
246
267
|
return { ok: false, returnPath: null };
|
|
268
|
+
}
|
|
247
269
|
await ensureTable();
|
|
248
270
|
const client = getDbExec();
|
|
249
271
|
const now = Date.now();
|
|
250
272
|
const { rows } = await client.execute({
|
|
251
|
-
sql:
|
|
273
|
+
sql: "SELECT return_path, app_id, client_id, redirect_uri, authority, code_challenge, expires_at, consumed_at " +
|
|
274
|
+
"FROM identity_sso_flow_state WHERE state = ?",
|
|
252
275
|
args: [state],
|
|
253
276
|
});
|
|
254
277
|
if (rows.length === 0)
|
|
@@ -256,62 +279,76 @@ export async function consumeSsoState(state) {
|
|
|
256
279
|
const row = rows[0];
|
|
257
280
|
const expiresAt = numOrNull(row.expires_at ?? row.expiresAt);
|
|
258
281
|
const consumedAt = numOrNull(row.consumed_at ?? row.consumedAt);
|
|
259
|
-
if (consumedAt != null)
|
|
282
|
+
if (consumedAt != null || (expiresAt != null && expiresAt < now)) {
|
|
260
283
|
return { ok: false, returnPath: null };
|
|
261
|
-
|
|
284
|
+
}
|
|
285
|
+
const appId = stringOrNull(row.app_id ?? row.appId);
|
|
286
|
+
const clientId = stringOrNull(row.client_id ?? row.clientId);
|
|
287
|
+
const redirectUri = stringOrNull(row.redirect_uri ?? row.redirectUri);
|
|
288
|
+
const authority = stringOrNull(row.authority);
|
|
289
|
+
const codeChallenge = stringOrNull(row.code_challenge ?? row.codeChallenge);
|
|
290
|
+
if (appId !== expected.appId ||
|
|
291
|
+
clientId !== expected.clientId ||
|
|
292
|
+
redirectUri !== expected.redirectUri ||
|
|
293
|
+
authority !== expected.authority ||
|
|
294
|
+
codeChallenge !== expected.codeChallenge) {
|
|
262
295
|
return { ok: false, returnPath: null };
|
|
263
296
|
}
|
|
264
|
-
// Single-use: only the caller that flips `consumed_at` from NULL wins.
|
|
265
297
|
const result = await client.execute({
|
|
266
|
-
sql:
|
|
267
|
-
|
|
298
|
+
sql: "UPDATE identity_sso_flow_state SET consumed_at = ? " +
|
|
299
|
+
"WHERE state = ? AND consumed_at IS NULL AND app_id = ? AND client_id = ? " +
|
|
300
|
+
"AND redirect_uri = ? AND authority = ? AND code_challenge = ?",
|
|
301
|
+
args: [
|
|
302
|
+
now,
|
|
303
|
+
state,
|
|
304
|
+
expected.appId,
|
|
305
|
+
expected.clientId,
|
|
306
|
+
expected.redirectUri,
|
|
307
|
+
expected.authority,
|
|
308
|
+
expected.codeChallenge,
|
|
309
|
+
],
|
|
268
310
|
});
|
|
269
|
-
if (result
|
|
270
|
-
// Lost the race to a concurrent callback — treat as already consumed.
|
|
311
|
+
if (affectedRows(result) !== 1)
|
|
271
312
|
return { ok: false, returnPath: null };
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
313
|
+
return {
|
|
314
|
+
ok: true,
|
|
315
|
+
returnPath: stringOrNull(row.return_path ?? row.returnPath),
|
|
316
|
+
};
|
|
275
317
|
}
|
|
276
|
-
// ---------------------------------------------------------------------------
|
|
277
|
-
// Replay (jti) guard
|
|
278
|
-
// ---------------------------------------------------------------------------
|
|
279
318
|
/**
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* replay
|
|
284
|
-
* sign-in — signature + exp + scope + single-use CSRF state remain the hard
|
|
285
|
-
* gates; this only adds defence in depth against token replay.
|
|
319
|
+
* Strict replay defense for the server-to-server assertion. A database error
|
|
320
|
+
* fails closed here: code exchange already provides the primary single-use
|
|
321
|
+
* guarantee, and refusing a login is safer than accepting an unverifiable
|
|
322
|
+
* replay boundary.
|
|
286
323
|
*/
|
|
287
324
|
export async function isJtiReplayed(jti) {
|
|
288
325
|
if (!jti)
|
|
289
|
-
return
|
|
326
|
+
return true;
|
|
290
327
|
try {
|
|
291
328
|
await ensureTable();
|
|
292
329
|
const client = getDbExec();
|
|
330
|
+
const now = Date.now();
|
|
293
331
|
await client.execute({
|
|
294
|
-
sql:
|
|
295
|
-
args: [jti,
|
|
332
|
+
sql: "INSERT INTO identity_sso_jti (jti, seen_at) VALUES (?, ?)",
|
|
333
|
+
args: [jti, now],
|
|
296
334
|
});
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
335
|
+
void client
|
|
336
|
+
.execute({
|
|
337
|
+
sql: "DELETE FROM identity_sso_jti WHERE seen_at < ?",
|
|
338
|
+
args: [now - SSO_STATE_TTL_MS],
|
|
339
|
+
})
|
|
340
|
+
.catch(() => { });
|
|
301
341
|
return false;
|
|
302
342
|
}
|
|
303
|
-
catch (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
msg.includes("constraint")) {
|
|
343
|
+
catch (error) {
|
|
344
|
+
const message = String(error?.message ?? "").toLowerCase();
|
|
345
|
+
if (message.includes("unique") ||
|
|
346
|
+
message.includes("duplicate") ||
|
|
347
|
+
message.includes("constraint")) {
|
|
309
348
|
return true;
|
|
310
349
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return false;
|
|
315
|
-
return false;
|
|
350
|
+
if (isConnectionError(error))
|
|
351
|
+
return true;
|
|
352
|
+
return true;
|
|
316
353
|
}
|
|
317
354
|
}
|