@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,49 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Identity-authority
|
|
2
|
+
* Identity-authority primitives for "Sign in with Agent-Native".
|
|
3
3
|
*
|
|
4
|
-
* Dispatch is the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* JWT (the existing A2A signer) and 302s back to the client's `redirect_uri`
|
|
9
|
-
* with the token + the caller's untouched `state`.
|
|
4
|
+
* Dispatch is the identity authority. The browser receives only a short-lived
|
|
5
|
+
* one-time authorization code. The client redeems it server-to-server with a
|
|
6
|
+
* PKCE verifier; only that server-to-server response may contain the signed
|
|
7
|
+
* identity assertion used to create the app-local session.
|
|
10
8
|
*
|
|
11
|
-
* This module
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - `buildRedirectLocation` — where the token + state land on the redirect.
|
|
16
|
-
*
|
|
17
|
-
* The HTTP wiring (session resolution, login bounce, signing) lives in
|
|
18
|
-
* `server/plugins/identity-sso.ts` so this file stays trivially testable with
|
|
19
|
-
* no Nitro / crypto / DB dependencies.
|
|
9
|
+
* This module also owns the exact app registry and the additive code store.
|
|
10
|
+
* Canonical apps are compiled into the registry. Custom workspace apps must be
|
|
11
|
+
* explicitly registered with `IDENTITY_SSO_APP_REGISTRY_JSON`; host suffixes
|
|
12
|
+
* and wildcard domains are never accepted.
|
|
20
13
|
*/
|
|
21
14
|
|
|
22
|
-
import {
|
|
15
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
23
16
|
|
|
24
|
-
|
|
25
|
-
// file stays plain ASCII (mirrors packages/core/src/server/open-route.ts).
|
|
26
|
-
const CONTROL_CHARS = new RegExp("[\\u0000-\\u001f\\u007f]");
|
|
17
|
+
import { getDbExec, intType } from "@agent-native/core/db";
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* exported alongside it for tests/assertions only.
|
|
39
|
-
*/
|
|
19
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
20
|
+
const APP_ID = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
21
|
+
const CLIENT_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
22
|
+
const STATE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
23
|
+
const CODE = /^[A-Za-z0-9_-]{43}$/;
|
|
24
|
+
const CODE_CHALLENGE = /^[A-Za-z0-9_-]{43}$/;
|
|
25
|
+
const CODE_VERIFIER = /^[A-Za-z0-9._~-]{43,128}$/;
|
|
26
|
+
const LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
27
|
+
|
|
28
|
+
export const IDENTITY_SCOPE = "identity";
|
|
40
29
|
export const IDENTITY_TOKEN_TTL_SECONDS = 120;
|
|
41
30
|
export const IDENTITY_TOKEN_TTL = "2m";
|
|
31
|
+
export const IDENTITY_AUTHORIZATION_CODE_TTL_MS =
|
|
32
|
+
IDENTITY_TOKEN_TTL_SECONDS * 1_000;
|
|
33
|
+
export const IDENTITY_SSO_CALLBACK_PATH = "/_agent-native/identity/callback";
|
|
34
|
+
export const IDENTITY_SSO_TOKEN_PATH = "/_agent-native/identity/token";
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
export function isValidSsoState(value: unknown): value is string {
|
|
37
|
+
return typeof value === "string" && STATE_PATTERN.test(value);
|
|
38
|
+
}
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
export function normalizeIdentityAuthority(raw: unknown): string | null {
|
|
41
|
+
if (typeof raw !== "string" || !raw.trim() || CONTROL_CHARS.test(raw)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(raw.trim());
|
|
46
|
+
if (
|
|
47
|
+
(url.protocol !== "https:" &&
|
|
48
|
+
!(url.protocol === "http:" && LOCALHOST_HOSTS.has(url.hostname))) ||
|
|
49
|
+
url.username ||
|
|
50
|
+
url.password ||
|
|
51
|
+
url.search ||
|
|
52
|
+
url.hash
|
|
53
|
+
) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return `${url.protocol}//${url.host}${url.pathname}`.replace(/\/+$/, "");
|
|
57
|
+
} catch (error) {
|
|
58
|
+
void error;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Exact first-party app origins. Never replace this with a suffix check. */
|
|
47
64
|
export const CANONICAL_IDENTITY_SSO_APP_ORIGINS = {
|
|
48
65
|
analytics: "https://analytics.agent-native.com",
|
|
49
66
|
assets: "https://assets.agent-native.com",
|
|
@@ -66,143 +83,226 @@ export const CANONICAL_IDENTITY_SSO_APP_ORIGINS = {
|
|
|
66
83
|
export const DEFAULT_ALLOWED_ORIGINS: readonly string[] = Object.values(
|
|
67
84
|
CANONICAL_IDENTITY_SSO_APP_ORIGINS,
|
|
68
85
|
);
|
|
69
|
-
const IDENTITY_SSO_CALLBACK_PATH = "/_agent-native/identity/callback";
|
|
70
86
|
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
export interface IdentitySsoAppRegistration {
|
|
88
|
+
appId: string;
|
|
89
|
+
clientId: string;
|
|
90
|
+
origin: string;
|
|
91
|
+
callbackPath: typeof IDENTITY_SSO_CALLBACK_PATH;
|
|
92
|
+
}
|
|
73
93
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
* the environment here is correct and outside the credentials-guard paths.
|
|
83
|
-
*/
|
|
84
|
-
export function getConfiguredHostSuffixes(
|
|
85
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
86
|
-
): string[] {
|
|
87
|
-
const raw = env.IDENTITY_SSO_ALLOWED_HOST_SUFFIXES;
|
|
88
|
-
if (!raw || typeof raw !== "string") return [];
|
|
89
|
-
return (
|
|
90
|
-
raw
|
|
91
|
-
.split(",")
|
|
92
|
-
.map((s) => s.trim().toLowerCase())
|
|
93
|
-
.filter(Boolean)
|
|
94
|
-
.map((s) => (s.startsWith(".") ? s : `.${s}`))
|
|
95
|
-
// Guard against a misconfigured "" / "." that would match everything.
|
|
96
|
-
.filter((s) => s.length > 1 && s !== ".")
|
|
94
|
+
function canonicalRegistrations(): IdentitySsoAppRegistration[] {
|
|
95
|
+
return Object.entries(CANONICAL_IDENTITY_SSO_APP_ORIGINS).map(
|
|
96
|
+
([appId, origin]) => ({
|
|
97
|
+
appId,
|
|
98
|
+
clientId: appId,
|
|
99
|
+
origin,
|
|
100
|
+
callbackPath: IDENTITY_SSO_CALLBACK_PATH,
|
|
101
|
+
}),
|
|
97
102
|
);
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
105
|
+
function exactOrigin(raw: unknown): string | null {
|
|
106
|
+
if (typeof raw !== "string" || !raw || CONTROL_CHARS.test(raw)) return null;
|
|
107
|
+
try {
|
|
108
|
+
const url = new URL(raw);
|
|
109
|
+
if (
|
|
110
|
+
url.protocol !== "https:" &&
|
|
111
|
+
!(url.protocol === "http:" && LOCALHOST_HOSTS.has(url.hostname))
|
|
112
|
+
) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
url.username ||
|
|
117
|
+
url.password ||
|
|
118
|
+
url.pathname !== "/" ||
|
|
119
|
+
url.search ||
|
|
120
|
+
url.hash
|
|
121
|
+
) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return url.origin;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
void error;
|
|
127
|
+
return null;
|
|
121
128
|
}
|
|
122
|
-
|
|
123
|
-
if (CONTROL_CHARS.test(rawRedirectUri)) return false;
|
|
129
|
+
}
|
|
124
130
|
|
|
125
|
-
|
|
131
|
+
function parseCustomRegistrations(
|
|
132
|
+
env: NodeJS.ProcessEnv,
|
|
133
|
+
): IdentitySsoAppRegistration[] {
|
|
134
|
+
const raw = env.IDENTITY_SSO_APP_REGISTRY_JSON?.trim();
|
|
135
|
+
if (!raw) return [];
|
|
136
|
+
let value: unknown;
|
|
126
137
|
try {
|
|
127
|
-
|
|
128
|
-
} catch {
|
|
129
|
-
|
|
138
|
+
value = JSON.parse(raw);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
void error;
|
|
141
|
+
return [];
|
|
130
142
|
}
|
|
143
|
+
if (!Array.isArray(value)) return [];
|
|
131
144
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
const registrations: IdentitySsoAppRegistration[] = [];
|
|
146
|
+
const seen = new Set<string>();
|
|
147
|
+
for (const entry of value) {
|
|
148
|
+
if (!entry || typeof entry !== "object") continue;
|
|
149
|
+
const candidate = entry as Record<string, unknown>;
|
|
150
|
+
const appId = candidate.appId;
|
|
151
|
+
const clientId = candidate.clientId;
|
|
152
|
+
const origin = exactOrigin(candidate.origin);
|
|
153
|
+
const callbackPath = candidate.callbackPath;
|
|
154
|
+
const capabilities = candidate.capabilities;
|
|
155
|
+
if (
|
|
156
|
+
typeof appId !== "string" ||
|
|
157
|
+
!APP_ID.test(appId) ||
|
|
158
|
+
typeof clientId !== "string" ||
|
|
159
|
+
!CLIENT_ID.test(clientId) ||
|
|
160
|
+
!origin ||
|
|
161
|
+
callbackPath !== IDENTITY_SSO_CALLBACK_PATH ||
|
|
162
|
+
!Array.isArray(capabilities) ||
|
|
163
|
+
!capabilities.includes("identity-sso") ||
|
|
164
|
+
seen.has(appId) ||
|
|
165
|
+
Object.prototype.hasOwnProperty.call(
|
|
166
|
+
CANONICAL_IDENTITY_SSO_APP_ORIGINS,
|
|
167
|
+
appId,
|
|
168
|
+
)
|
|
169
|
+
) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
seen.add(appId);
|
|
173
|
+
registrations.push({
|
|
174
|
+
appId,
|
|
175
|
+
clientId,
|
|
176
|
+
origin,
|
|
177
|
+
callbackPath: IDENTITY_SSO_CALLBACK_PATH,
|
|
178
|
+
});
|
|
145
179
|
}
|
|
180
|
+
return registrations;
|
|
181
|
+
}
|
|
146
182
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Return the exact configured registry. Invalid custom entries are ignored;
|
|
185
|
+
* they never broaden the canonical registry. A deployment can therefore roll
|
|
186
|
+
* back custom registration by removing the env without changing code or data.
|
|
187
|
+
*/
|
|
188
|
+
export function getIdentitySsoAppRegistry(
|
|
189
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
190
|
+
): IdentitySsoAppRegistration[] {
|
|
191
|
+
return [...canonicalRegistrations(), ...parseCustomRegistrations(env)];
|
|
192
|
+
}
|
|
150
193
|
|
|
151
|
-
|
|
194
|
+
function parseAbsoluteUrl(raw: string): URL | null {
|
|
195
|
+
if (!raw || CONTROL_CHARS.test(raw)) return null;
|
|
196
|
+
try {
|
|
197
|
+
const url = new URL(raw);
|
|
198
|
+
if (url.username || url.password) return null;
|
|
199
|
+
return url;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
void error;
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
152
205
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
206
|
+
/**
|
|
207
|
+
* General redirect-origin validation. The authorize route additionally calls
|
|
208
|
+
* `resolveIdentitySsoApp`, which enforces the exact registered callback.
|
|
209
|
+
* Localhost remains available for development, but only with exact callback
|
|
210
|
+
* path and client binding in the app resolver below.
|
|
211
|
+
*/
|
|
212
|
+
export function isAllowedRedirectUri(rawRedirectUri: unknown): boolean {
|
|
213
|
+
if (typeof rawRedirectUri !== "string") return false;
|
|
214
|
+
const url = parseAbsoluteUrl(rawRedirectUri);
|
|
215
|
+
if (!url) return false;
|
|
216
|
+
if (url.protocol === "http:" && LOCALHOST_HOSTS.has(url.hostname)) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
return (
|
|
220
|
+
url.protocol === "https:" && DEFAULT_ALLOWED_ORIGINS.includes(url.origin)
|
|
221
|
+
);
|
|
159
222
|
}
|
|
160
223
|
|
|
161
|
-
|
|
224
|
+
function exactCallbackMatches(
|
|
225
|
+
registration: IdentitySsoAppRegistration,
|
|
226
|
+
rawRedirectUri: string,
|
|
227
|
+
): boolean {
|
|
228
|
+
const url = parseAbsoluteUrl(rawRedirectUri);
|
|
229
|
+
if (!url) return false;
|
|
230
|
+
return (
|
|
231
|
+
url.origin === registration.origin &&
|
|
232
|
+
url.pathname === registration.callbackPath &&
|
|
233
|
+
!url.search &&
|
|
234
|
+
!url.hash
|
|
235
|
+
);
|
|
236
|
+
}
|
|
162
237
|
|
|
163
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Resolve an app only when app id, client id, exact origin, and callback path
|
|
240
|
+
* all agree. Loopback is the narrowly-scoped development exception and still
|
|
241
|
+
* requires a valid app/client id pair plus the fixed callback path.
|
|
242
|
+
*/
|
|
243
|
+
export function resolveIdentitySsoApp(
|
|
164
244
|
appId: unknown,
|
|
245
|
+
clientId: unknown,
|
|
165
246
|
rawRedirectUri: unknown,
|
|
166
|
-
|
|
167
|
-
):
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
247
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
248
|
+
): IdentitySsoAppRegistration | null {
|
|
249
|
+
if (
|
|
250
|
+
typeof appId !== "string" ||
|
|
251
|
+
!APP_ID.test(appId) ||
|
|
252
|
+
typeof clientId !== "string" ||
|
|
253
|
+
!CLIENT_ID.test(clientId) ||
|
|
254
|
+
typeof rawRedirectUri !== "string"
|
|
255
|
+
) {
|
|
256
|
+
return null;
|
|
174
257
|
}
|
|
258
|
+
const redirectUri = rawRedirectUri as string;
|
|
259
|
+
const parsedRedirect = parseAbsoluteUrl(redirectUri);
|
|
260
|
+
if (!parsedRedirect) return null;
|
|
261
|
+
const registered = getIdentitySsoAppRegistry(env).find(
|
|
262
|
+
(candidate) =>
|
|
263
|
+
candidate.appId === appId &&
|
|
264
|
+
candidate.clientId === clientId &&
|
|
265
|
+
exactCallbackMatches(candidate, redirectUri),
|
|
266
|
+
);
|
|
267
|
+
if (registered) return registered;
|
|
175
268
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
269
|
+
const url = parseAbsoluteUrl(redirectUri);
|
|
270
|
+
if (
|
|
271
|
+
url &&
|
|
272
|
+
LOCALHOST_HOSTS.has(url.hostname) &&
|
|
273
|
+
clientId === appId &&
|
|
274
|
+
url.pathname === IDENTITY_SSO_CALLBACK_PATH &&
|
|
275
|
+
!url.search &&
|
|
276
|
+
!url.hash
|
|
277
|
+
) {
|
|
278
|
+
return {
|
|
279
|
+
appId,
|
|
280
|
+
clientId,
|
|
281
|
+
origin: url.origin,
|
|
282
|
+
callbackPath: IDENTITY_SSO_CALLBACK_PATH,
|
|
283
|
+
};
|
|
182
284
|
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
183
287
|
|
|
184
|
-
|
|
288
|
+
export function isAllowedIdentityRedirect(
|
|
289
|
+
appId: unknown,
|
|
290
|
+
rawRedirectUri: unknown,
|
|
291
|
+
options: { clientId?: unknown; env?: NodeJS.ProcessEnv } = {},
|
|
292
|
+
): boolean {
|
|
293
|
+
const clientId = options.clientId ?? appId;
|
|
294
|
+
return !!resolveIdentitySsoApp(appId, clientId, rawRedirectUri, options.env);
|
|
185
295
|
}
|
|
186
296
|
|
|
187
297
|
export interface IdentityClaims {
|
|
188
|
-
/** Stable user identifier — the Dispatch account email. */
|
|
189
298
|
sub: string;
|
|
190
299
|
email: string;
|
|
191
|
-
/** Display name when the auth provider supplied one. */
|
|
192
300
|
name?: string;
|
|
193
|
-
/** Resolved org domain when the user has an active org; omitted otherwise. */
|
|
194
301
|
org_domain?: string;
|
|
195
|
-
/** Marks this token as an identity (login) token. */
|
|
196
302
|
scope: typeof IDENTITY_SCOPE;
|
|
197
|
-
/** Random per-token id for replay/debugging. */
|
|
198
303
|
jti: string;
|
|
199
304
|
}
|
|
200
305
|
|
|
201
|
-
/**
|
|
202
|
-
* Build the EXACT claim set for the identity token. No secrets/passwords
|
|
203
|
-
* ever go in here. `sub` and `email` are the same value (the account email)
|
|
204
|
-
* so a verifier can key on either. `org_domain` is included only when known.
|
|
205
|
-
*/
|
|
206
306
|
export function buildIdentityClaims(input: {
|
|
207
307
|
email: string;
|
|
208
308
|
name?: string | null;
|
|
@@ -212,37 +312,211 @@ export function buildIdentityClaims(input: {
|
|
|
212
312
|
sub: input.email,
|
|
213
313
|
email: input.email,
|
|
214
314
|
scope: IDENTITY_SCOPE,
|
|
215
|
-
jti:
|
|
315
|
+
jti: randomBytes(16).toString("base64url"),
|
|
216
316
|
};
|
|
217
|
-
if (input.name
|
|
218
|
-
if (input.orgDomain
|
|
219
|
-
claims.org_domain = input.orgDomain.trim();
|
|
220
|
-
}
|
|
317
|
+
if (input.name?.trim()) claims.name = input.name.trim();
|
|
318
|
+
if (input.orgDomain?.trim()) claims.org_domain = input.orgDomain.trim();
|
|
221
319
|
return claims;
|
|
222
320
|
}
|
|
223
321
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
322
|
+
function sha256Base64Url(value: string): string {
|
|
323
|
+
return createHash("sha256").update(value).digest("base64url");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function createCodeChallenge(verifier: string): string | null {
|
|
327
|
+
if (!CODE_VERIFIER.test(verifier)) return null;
|
|
328
|
+
return sha256Base64Url(verifier);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function safeEqual(left: string, right: string): boolean {
|
|
332
|
+
const a = Buffer.from(left);
|
|
333
|
+
const b = Buffer.from(right);
|
|
334
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
335
|
+
}
|
|
336
|
+
|
|
237
337
|
export function buildRedirectLocation(
|
|
238
338
|
rawRedirectUri: string,
|
|
239
|
-
|
|
339
|
+
code: string,
|
|
240
340
|
state: string | null | undefined,
|
|
241
341
|
): string {
|
|
242
342
|
const url = new URL(rawRedirectUri);
|
|
243
|
-
url.searchParams.set("
|
|
343
|
+
url.searchParams.set("code", code);
|
|
244
344
|
if (typeof state === "string" && state.length > 0) {
|
|
245
345
|
url.searchParams.set("state", state);
|
|
246
346
|
}
|
|
247
347
|
return url.toString();
|
|
248
348
|
}
|
|
349
|
+
|
|
350
|
+
export interface CreateIdentityAuthorizationCodeInput {
|
|
351
|
+
state: string;
|
|
352
|
+
appId: string;
|
|
353
|
+
clientId: string;
|
|
354
|
+
redirectUri: string;
|
|
355
|
+
authority: string;
|
|
356
|
+
codeChallenge: string;
|
|
357
|
+
email: string;
|
|
358
|
+
name?: string | null;
|
|
359
|
+
orgDomain?: string | null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export interface ConsumedIdentityAuthorizationCode {
|
|
363
|
+
email: string;
|
|
364
|
+
name?: string;
|
|
365
|
+
orgDomain?: string;
|
|
366
|
+
jti: string;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let codeTableInitPromise: Promise<void> | undefined;
|
|
370
|
+
|
|
371
|
+
function buildCodeTableSql(): string {
|
|
372
|
+
return `
|
|
373
|
+
CREATE TABLE IF NOT EXISTS identity_sso_authorization_code (
|
|
374
|
+
code_hash TEXT PRIMARY KEY,
|
|
375
|
+
state TEXT NOT NULL,
|
|
376
|
+
app_id TEXT NOT NULL,
|
|
377
|
+
client_id TEXT NOT NULL,
|
|
378
|
+
redirect_uri TEXT NOT NULL,
|
|
379
|
+
authority TEXT NOT NULL,
|
|
380
|
+
code_challenge TEXT NOT NULL,
|
|
381
|
+
email TEXT NOT NULL,
|
|
382
|
+
name TEXT,
|
|
383
|
+
org_domain TEXT,
|
|
384
|
+
jti TEXT NOT NULL,
|
|
385
|
+
created_at ${intType()} NOT NULL,
|
|
386
|
+
expires_at ${intType()} NOT NULL,
|
|
387
|
+
consumed_at ${intType()}
|
|
388
|
+
)
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function ensureCodeTable(): Promise<void> {
|
|
393
|
+
if (!codeTableInitPromise) {
|
|
394
|
+
codeTableInitPromise = getDbExec()
|
|
395
|
+
.execute(buildCodeTableSql())
|
|
396
|
+
.then(() => undefined)
|
|
397
|
+
.catch((error) => {
|
|
398
|
+
codeTableInitPromise = undefined;
|
|
399
|
+
throw error;
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return codeTableInitPromise;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function affectedRows(result: any): number {
|
|
406
|
+
return Number(result?.rowsAffected ?? result?.rowCount ?? result?.count ?? 0);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function identityCodeHash(code: string): string {
|
|
410
|
+
return sha256Base64Url(code);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function createIdentityAuthorizationCode(
|
|
414
|
+
input: CreateIdentityAuthorizationCodeInput,
|
|
415
|
+
): Promise<string> {
|
|
416
|
+
if (
|
|
417
|
+
!STATE_PATTERN.test(input.state) ||
|
|
418
|
+
!APP_ID.test(input.appId) ||
|
|
419
|
+
!CLIENT_ID.test(input.clientId) ||
|
|
420
|
+
!CODE_CHALLENGE.test(input.codeChallenge) ||
|
|
421
|
+
!input.email ||
|
|
422
|
+
!resolveIdentitySsoApp(input.appId, input.clientId, input.redirectUri)
|
|
423
|
+
) {
|
|
424
|
+
throw new Error("INVALID_IDENTITY_AUTHORIZATION_CODE");
|
|
425
|
+
}
|
|
426
|
+
await ensureCodeTable();
|
|
427
|
+
const code = randomBytes(32).toString("base64url");
|
|
428
|
+
const now = Date.now();
|
|
429
|
+
const claims = buildIdentityClaims(input);
|
|
430
|
+
await getDbExec().execute({
|
|
431
|
+
sql:
|
|
432
|
+
"INSERT INTO identity_sso_authorization_code " +
|
|
433
|
+
"(code_hash, state, app_id, client_id, redirect_uri, authority, code_challenge, email, name, org_domain, jti, created_at, expires_at, consumed_at) " +
|
|
434
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
435
|
+
args: [
|
|
436
|
+
identityCodeHash(code),
|
|
437
|
+
input.state,
|
|
438
|
+
input.appId,
|
|
439
|
+
input.clientId,
|
|
440
|
+
input.redirectUri,
|
|
441
|
+
input.authority,
|
|
442
|
+
input.codeChallenge,
|
|
443
|
+
claims.email,
|
|
444
|
+
claims.name ?? null,
|
|
445
|
+
claims.org_domain ?? null,
|
|
446
|
+
claims.jti,
|
|
447
|
+
now,
|
|
448
|
+
now + IDENTITY_AUTHORIZATION_CODE_TTL_MS,
|
|
449
|
+
null,
|
|
450
|
+
],
|
|
451
|
+
});
|
|
452
|
+
void getDbExec()
|
|
453
|
+
.execute({
|
|
454
|
+
sql: "DELETE FROM identity_sso_authorization_code WHERE expires_at < ?",
|
|
455
|
+
args: [now],
|
|
456
|
+
})
|
|
457
|
+
.catch(() => {});
|
|
458
|
+
return code;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export async function consumeIdentityAuthorizationCode(input: {
|
|
462
|
+
code: string;
|
|
463
|
+
state: string;
|
|
464
|
+
appId: string;
|
|
465
|
+
clientId: string;
|
|
466
|
+
redirectUri: string;
|
|
467
|
+
authority: string;
|
|
468
|
+
codeVerifier: string;
|
|
469
|
+
}): Promise<ConsumedIdentityAuthorizationCode | null> {
|
|
470
|
+
if (
|
|
471
|
+
!CODE.test(input.code) ||
|
|
472
|
+
!isValidSsoState(input.state) ||
|
|
473
|
+
!APP_ID.test(input.appId) ||
|
|
474
|
+
!CLIENT_ID.test(input.clientId) ||
|
|
475
|
+
!CODE_VERIFIER.test(input.codeVerifier)
|
|
476
|
+
) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
const challenge = createCodeChallenge(input.codeVerifier);
|
|
480
|
+
if (!challenge) return null;
|
|
481
|
+
await ensureCodeTable();
|
|
482
|
+
const codeHash = identityCodeHash(input.code);
|
|
483
|
+
const { rows } = await getDbExec().execute({
|
|
484
|
+
sql:
|
|
485
|
+
"SELECT state, app_id, client_id, redirect_uri, authority, code_challenge, email, name, org_domain, jti, expires_at, consumed_at " +
|
|
486
|
+
"FROM identity_sso_authorization_code WHERE code_hash = ?",
|
|
487
|
+
args: [codeHash],
|
|
488
|
+
});
|
|
489
|
+
if (rows.length !== 1) return null;
|
|
490
|
+
const row: any = rows[0];
|
|
491
|
+
const expiresAt = Number(row.expires_at ?? row.expiresAt);
|
|
492
|
+
if (
|
|
493
|
+
row.consumed_at != null ||
|
|
494
|
+
!Number.isFinite(expiresAt) ||
|
|
495
|
+
expiresAt < Date.now() ||
|
|
496
|
+
row.state !== input.state ||
|
|
497
|
+
row.app_id !== input.appId ||
|
|
498
|
+
row.client_id !== input.clientId ||
|
|
499
|
+
row.redirect_uri !== input.redirectUri ||
|
|
500
|
+
row.authority !== input.authority ||
|
|
501
|
+
!safeEqual(String(row.code_challenge ?? ""), challenge)
|
|
502
|
+
) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
const result = await getDbExec().execute({
|
|
506
|
+
sql:
|
|
507
|
+
"UPDATE identity_sso_authorization_code SET consumed_at = ? " +
|
|
508
|
+
"WHERE code_hash = ? AND consumed_at IS NULL",
|
|
509
|
+
args: [Date.now(), codeHash],
|
|
510
|
+
});
|
|
511
|
+
if (affectedRows(result) !== 1) return null;
|
|
512
|
+
if (typeof row.email !== "string" || !row.email.includes("@")) return null;
|
|
513
|
+
if (typeof row.jti !== "string" || !row.jti) return null;
|
|
514
|
+
return {
|
|
515
|
+
email: row.email,
|
|
516
|
+
...(typeof row.name === "string" && row.name ? { name: row.name } : {}),
|
|
517
|
+
...(typeof row.org_domain === "string" && row.org_domain
|
|
518
|
+
? { orgDomain: row.org_domain }
|
|
519
|
+
: {}),
|
|
520
|
+
jti: row.jti,
|
|
521
|
+
};
|
|
522
|
+
}
|