aimeat 1.35.0 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.env.example +31 -0
- package/dist/locales/en.json +192 -27
- package/dist/locales/fi.json +192 -27
- package/dist/public/components/Markdown.js +9 -0
- package/dist/public/components/MemoryEmbed.js +151 -0
- package/dist/public/components/Mermaid.js +4 -1
- package/dist/public/components/NotificationBell.js +45 -19
- package/dist/public/css/components/markdown.css +15 -0
- package/dist/public/css/views/agents-detail.css +17 -0
- package/dist/public/css/views/portfolio.css +51 -0
- package/dist/public/css/views/profile.css +31 -0
- package/dist/public/js/services/agents.js +9 -0
- package/dist/public/js/services/organisms.js +37 -1
- package/dist/public/llms-template.txt +278 -0
- package/dist/public/spa.html +39 -2
- package/dist/public/sw.js +50 -34
- package/dist/public/views/my-company.js +13 -1
- package/dist/public/views/portfolio.js +267 -44
- package/dist/public/views/profile/agents/agent-card.js +4 -0
- package/dist/public/views/profile/agents/tab-contracts.js +112 -0
- package/dist/public/views/profile/landing-page.js +10 -1
- package/dist/public/views/profile/memory-tab.js +6 -2
- package/dist/public/views/profile/notifications-tab.js +9 -1
- package/dist/public/views/profile/organisms/home.js +23 -3
- package/dist/public/views/profile/organisms/participants-panel.js +89 -17
- package/dist/public/views/profile/organisms/workspace-apps.js +130 -0
- package/dist/public/views/profile/organisms/workspace.js +5 -0
- package/dist/public/views/profile/organisms-tab.js +1 -1
- package/dist/public/views/secretary.js +13 -0
- package/dist/scripts/archive-stale-versions.d.ts +33 -0
- package/dist/scripts/archive-stale-versions.d.ts.map +1 -0
- package/dist/scripts/archive-stale-versions.js +278 -0
- package/dist/scripts/archive-stale-versions.js.map +1 -0
- package/dist/src/cli/connect/mcp/tools/agent-management.d.ts +4 -1
- package/dist/src/cli/connect/mcp/tools/agent-management.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-management.js +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-management.js.map +1 -1
- package/dist/src/cli/init-wizard.d.ts.map +1 -1
- package/dist/src/cli/init-wizard.js +32 -0
- package/dist/src/cli/init-wizard.js.map +1 -1
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +21 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/data/aimeat-iam-package.d.ts +10 -2
- package/dist/src/data/aimeat-iam-package.d.ts.map +1 -1
- package/dist/src/data/aimeat-iam-package.js +39 -10
- package/dist/src/data/aimeat-iam-package.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +802 -60
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +5 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +2 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +54 -2
- package/dist/src/generated/prisma-postgres/index.js +5 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +2 -0
- package/dist/src/generated/prisma-postgres/wasm.js +5 -3
- package/dist/src/mcp/annotations.d.ts.map +1 -1
- package/dist/src/mcp/annotations.js +1 -0
- package/dist/src/mcp/annotations.js.map +1 -1
- package/dist/src/mcp/catalog/definitions.d.ts +3 -0
- package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
- package/dist/src/mcp/catalog/definitions.js +21 -7
- package/dist/src/mcp/catalog/definitions.js.map +1 -1
- package/dist/src/mcp/catalog/surfaces.js +1 -1
- package/dist/src/mcp/catalog/surfaces.js.map +1 -1
- package/dist/src/mcp/core.js +1 -1
- package/dist/src/mcp/core.js.map +1 -1
- package/dist/src/mcp/extensions.d.ts.map +1 -1
- package/dist/src/mcp/extensions.js +21 -0
- package/dist/src/mcp/extensions.js.map +1 -1
- package/dist/src/mcp/memory-extended.js +1 -1
- package/dist/src/mcp/memory-extended.js.map +1 -1
- package/dist/src/mcp/organisms.d.ts +1 -0
- package/dist/src/mcp/organisms.d.ts.map +1 -1
- package/dist/src/mcp/organisms.js +23 -6
- package/dist/src/mcp/organisms.js.map +1 -1
- package/dist/src/mcp/workspaces.d.ts +7 -0
- package/dist/src/mcp/workspaces.d.ts.map +1 -1
- package/dist/src/mcp/workspaces.js +23 -5
- package/dist/src/mcp/workspaces.js.map +1 -1
- package/dist/src/middleware/subdomain.d.ts +9 -0
- package/dist/src/middleware/subdomain.d.ts.map +1 -1
- package/dist/src/middleware/subdomain.js +24 -3
- package/dist/src/middleware/subdomain.js.map +1 -1
- package/dist/src/models/schemas.d.ts +2 -0
- package/dist/src/models/schemas.d.ts.map +1 -1
- package/dist/src/models/schemas.js +2 -2
- package/dist/src/models/schemas.js.map +1 -1
- package/dist/src/routes/admin-features.js +1 -1
- package/dist/src/routes/admin-features.js.map +1 -1
- package/dist/src/routes/agent-onboarding.d.ts.map +1 -1
- package/dist/src/routes/agent-onboarding.js +44 -7
- package/dist/src/routes/agent-onboarding.js.map +1 -1
- package/dist/src/routes/agents.d.ts +8 -0
- package/dist/src/routes/agents.d.ts.map +1 -1
- package/dist/src/routes/agents.js +91 -5
- package/dist/src/routes/agents.js.map +1 -1
- package/dist/src/routes/app-grants.d.ts +5 -0
- package/dist/src/routes/app-grants.d.ts.map +1 -1
- package/dist/src/routes/app-grants.js +52 -20
- package/dist/src/routes/app-grants.js.map +1 -1
- package/dist/src/routes/boards.d.ts.map +1 -1
- package/dist/src/routes/boards.js +4 -1
- package/dist/src/routes/boards.js.map +1 -1
- package/dist/src/routes/bootstrap.d.ts +2 -0
- package/dist/src/routes/bootstrap.d.ts.map +1 -1
- package/dist/src/routes/bootstrap.js +5 -0
- package/dist/src/routes/bootstrap.js.map +1 -1
- package/dist/src/routes/capabilities.d.ts.map +1 -1
- package/dist/src/routes/capabilities.js +4 -1
- package/dist/src/routes/capabilities.js.map +1 -1
- package/dist/src/routes/catalogue.d.ts.map +1 -1
- package/dist/src/routes/catalogue.js +5 -4
- package/dist/src/routes/catalogue.js.map +1 -1
- package/dist/src/routes/extensions.d.ts.map +1 -1
- package/dist/src/routes/extensions.js +11 -0
- package/dist/src/routes/extensions.js.map +1 -1
- package/dist/src/routes/ghii.d.ts.map +1 -1
- package/dist/src/routes/ghii.js +65 -17
- package/dist/src/routes/ghii.js.map +1 -1
- package/dist/src/routes/lib-editor.d.ts +26 -0
- package/dist/src/routes/lib-editor.d.ts.map +1 -0
- package/dist/src/routes/lib-editor.js +215 -0
- package/dist/src/routes/lib-editor.js.map +1 -0
- package/dist/src/routes/lib-markdown.d.ts +16 -1
- package/dist/src/routes/lib-markdown.d.ts.map +1 -1
- package/dist/src/routes/lib-markdown.js +281 -2
- package/dist/src/routes/lib-markdown.js.map +1 -1
- package/dist/src/routes/lib-organism.d.ts +28 -0
- package/dist/src/routes/lib-organism.d.ts.map +1 -0
- package/dist/src/routes/lib-organism.js +298 -0
- package/dist/src/routes/lib-organism.js.map +1 -0
- package/dist/src/routes/lib-portfolio-standalone.d.ts +22 -0
- package/dist/src/routes/lib-portfolio-standalone.d.ts.map +1 -0
- package/dist/src/routes/lib-portfolio-standalone.js +68 -0
- package/dist/src/routes/lib-portfolio-standalone.js.map +1 -0
- package/dist/src/routes/libs.d.ts +18 -0
- package/dist/src/routes/libs.d.ts.map +1 -1
- package/dist/src/routes/libs.js +116 -18
- package/dist/src/routes/libs.js.map +1 -1
- package/dist/src/routes/memory.d.ts +3 -0
- package/dist/src/routes/memory.d.ts.map +1 -1
- package/dist/src/routes/memory.js +58 -4
- package/dist/src/routes/memory.js.map +1 -1
- package/dist/src/routes/notifications.d.ts +5 -1
- package/dist/src/routes/notifications.d.ts.map +1 -1
- package/dist/src/routes/notifications.js +68 -3
- package/dist/src/routes/notifications.js.map +1 -1
- package/dist/src/routes/oauth-login.d.ts +26 -21
- package/dist/src/routes/oauth-login.d.ts.map +1 -1
- package/dist/src/routes/oauth-login.js +257 -229
- package/dist/src/routes/oauth-login.js.map +1 -1
- package/dist/src/routes/openrouter.d.ts +3 -0
- package/dist/src/routes/openrouter.d.ts.map +1 -1
- package/dist/src/routes/openrouter.js +10 -5
- package/dist/src/routes/openrouter.js.map +1 -1
- package/dist/src/routes/organisms.d.ts +6 -0
- package/dist/src/routes/organisms.d.ts.map +1 -1
- package/dist/src/routes/organisms.js +203 -16
- package/dist/src/routes/organisms.js.map +1 -1
- package/dist/src/routes/owners.d.ts.map +1 -1
- package/dist/src/routes/owners.js +12 -8
- package/dist/src/routes/owners.js.map +1 -1
- package/dist/src/routes/portfolio.d.ts +41 -1
- package/dist/src/routes/portfolio.d.ts.map +1 -1
- package/dist/src/routes/portfolio.js +94 -30
- package/dist/src/routes/portfolio.js.map +1 -1
- package/dist/src/routes/push.d.ts +15 -0
- package/dist/src/routes/push.d.ts.map +1 -1
- package/dist/src/routes/push.js +16 -1
- package/dist/src/routes/push.js.map +1 -1
- package/dist/src/routes/site.d.ts +2 -0
- package/dist/src/routes/site.d.ts.map +1 -1
- package/dist/src/routes/site.js +10 -2
- package/dist/src/routes/site.js.map +1 -1
- package/dist/src/routes/subdomains.d.ts +5 -0
- package/dist/src/routes/subdomains.d.ts.map +1 -1
- package/dist/src/routes/subdomains.js +56 -0
- package/dist/src/routes/subdomains.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +10 -15
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/static-files.d.ts.map +1 -1
- package/dist/src/server-bootstrap/static-files.js +9 -4
- package/dist/src/server-bootstrap/static-files.js.map +1 -1
- package/dist/src/services/access-guard.d.ts.map +1 -1
- package/dist/src/services/access-guard.js +7 -0
- package/dist/src/services/access-guard.js.map +1 -1
- package/dist/src/services/ai-completion.d.ts +2 -0
- package/dist/src/services/ai-completion.d.ts.map +1 -1
- package/dist/src/services/ai-completion.js +2 -1
- package/dist/src/services/ai-completion.js.map +1 -1
- package/dist/src/services/config-schema.d.ts.map +1 -1
- package/dist/src/services/config-schema.js +2 -0
- package/dist/src/services/config-schema.js.map +1 -1
- package/dist/src/services/ecosystem-access.d.ts +24 -8
- package/dist/src/services/ecosystem-access.d.ts.map +1 -1
- package/dist/src/services/ecosystem-access.js +27 -5
- package/dist/src/services/ecosystem-access.js.map +1 -1
- package/dist/src/services/generator-autopilot.d.ts +2 -0
- package/dist/src/services/generator-autopilot.d.ts.map +1 -1
- package/dist/src/services/generator-autopilot.js +3 -1
- package/dist/src/services/generator-autopilot.js.map +1 -1
- package/dist/src/services/iam/app-commands.d.ts +75 -0
- package/dist/src/services/iam/app-commands.d.ts.map +1 -0
- package/dist/src/services/iam/app-commands.js +55 -0
- package/dist/src/services/iam/app-commands.js.map +1 -0
- package/dist/src/services/iam/define-app-iam.d.ts +45 -0
- package/dist/src/services/iam/define-app-iam.d.ts.map +1 -0
- package/dist/src/services/iam/define-app-iam.js +75 -0
- package/dist/src/services/iam/define-app-iam.js.map +1 -0
- package/dist/src/services/iam/model.d.ts +113 -0
- package/dist/src/services/iam/model.d.ts.map +1 -0
- package/dist/src/services/iam/model.js +115 -0
- package/dist/src/services/iam/model.js.map +1 -0
- package/dist/src/services/mailbox-notification.d.ts +1 -0
- package/dist/src/services/mailbox-notification.d.ts.map +1 -1
- package/dist/src/services/mailbox-notification.js +3 -0
- package/dist/src/services/mailbox-notification.js.map +1 -1
- package/dist/src/services/notebook-ai.d.ts +2 -0
- package/dist/src/services/notebook-ai.d.ts.map +1 -1
- package/dist/src/services/notebook-ai.js +1 -1
- package/dist/src/services/notebook-ai.js.map +1 -1
- package/dist/src/services/notify.d.ts +11 -1
- package/dist/src/services/notify.d.ts.map +1 -1
- package/dist/src/services/notify.js +39 -2
- package/dist/src/services/notify.js.map +1 -1
- package/dist/src/services/oidc-providers.d.ts +94 -0
- package/dist/src/services/oidc-providers.d.ts.map +1 -0
- package/dist/src/services/oidc-providers.js +181 -0
- package/dist/src/services/oidc-providers.js.map +1 -0
- package/dist/src/services/organism-privacy.d.ts +46 -0
- package/dist/src/services/organism-privacy.d.ts.map +1 -0
- package/dist/src/services/organism-privacy.js +42 -0
- package/dist/src/services/organism-privacy.js.map +1 -0
- package/dist/src/services/organism-template.d.ts.map +1 -1
- package/dist/src/services/organism-template.js +3 -1
- package/dist/src/services/organism-template.js.map +1 -1
- package/dist/src/services/prompt-defaults.d.ts +2 -0
- package/dist/src/services/prompt-defaults.d.ts.map +1 -1
- package/dist/src/services/prompt-defaults.js +5 -0
- package/dist/src/services/prompt-defaults.js.map +1 -1
- package/dist/src/services/scheduler.d.ts.map +1 -1
- package/dist/src/services/scheduler.js +16 -4
- package/dist/src/services/scheduler.js.map +1 -1
- package/dist/src/services/skill-bundle/generator.d.ts +2 -0
- package/dist/src/services/skill-bundle/generator.d.ts.map +1 -1
- package/dist/src/services/skill-bundle/generator.js +3 -1
- package/dist/src/services/skill-bundle/generator.js.map +1 -1
- package/dist/src/services/workspace-access.d.ts +2 -0
- package/dist/src/services/workspace-access.d.ts.map +1 -1
- package/dist/src/services/workspace-access.js +20 -8
- package/dist/src/services/workspace-access.js.map +1 -1
- package/dist/src/services/workspace-engagements.d.ts +65 -0
- package/dist/src/services/workspace-engagements.d.ts.map +1 -0
- package/dist/src/services/workspace-engagements.js +92 -0
- package/dist/src/services/workspace-engagements.js.map +1 -0
- package/dist/src/services/workspace-meta.d.ts +9 -2
- package/dist/src/services/workspace-meta.d.ts.map +1 -1
- package/dist/src/services/workspace-meta.js +22 -2
- package/dist/src/services/workspace-meta.js.map +1 -1
- package/dist/src/storage/interface.d.ts +10 -2
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/interface.js.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +1 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +29 -2
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +1 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +23 -8
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +9 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/identity.repository.d.ts +2 -0
- package/dist/src/storage/repositories/identity.repository.d.ts.map +1 -1
- package/dist/src/utils/env-config.d.ts.map +1 -1
- package/dist/src/utils/env-config.js +23 -0
- package/dist/src/utils/env-config.js.map +1 -1
- package/dist/src/utils/env-validator.d.ts.map +1 -1
- package/dist/src/utils/env-validator.js +8 -0
- package/dist/src/utils/env-validator.js.map +1 -1
- package/dist/src/utils/node-descriptor.d.ts.map +1 -1
- package/dist/src/utils/node-descriptor.js +1 -0
- package/dist/src/utils/node-descriptor.js.map +1 -1
- package/dist/static/app-catalog.html +8 -2
- package/dist/static/icons/badge-96.png +0 -0
- package/dist/static/icons/icon-192.png +0 -0
- package/dist/static/icons/icon-512.png +0 -0
- package/dist/static/manifest.json +19 -17
- package/dist/static/sw.js +13 -11
- package/package.json +3 -1
- package/prisma/schema.postgres.prisma +2 -0
- package/prisma/schema.prisma +2 -0
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file oauth-login.ts
|
|
3
|
-
* @description Social login (sign-in) via external OIDC providers —
|
|
4
|
-
* A generic, config-gated sign-in path for HUMAN owners that reuses the
|
|
5
|
-
* relying-party client
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* pending
|
|
18
|
-
* session)
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
3
|
+
* @description Social login (sign-in) via external OIDC providers — Google, Casdoor, and Microsoft
|
|
4
|
+
* Entra ID. A single generic, config-gated sign-in path for HUMAN owners that reuses the shared
|
|
5
|
+
* OIDC relying-party client (src/services/oidc-client.ts) via the provider registry
|
|
6
|
+
* (src/services/oidc-providers.ts). For every configured provider the SAME three routes are
|
|
7
|
+
* registered — authorize → callback → finalize — differing only by the provider's URL segment,
|
|
8
|
+
* nonce type, claim mapping, and optional tenant gate. On a successful login the user is mapped to
|
|
9
|
+
* a GHII: a returning user (matched by the provider's stable subject) or a verified-email match
|
|
10
|
+
* links straight to their existing account + session. A BRAND-NEW user is NOT auto-created —
|
|
11
|
+
* instead they go through a one-time username-choice step: the callback stashes the verified
|
|
12
|
+
* identity in a short-lived signed cookie and bounces back to the SPA, which prompts the user to
|
|
13
|
+
* confirm or change the suggested username (permanent) before POSTing to finalize, which creates
|
|
14
|
+
* the owner+GHII and establishes the session.
|
|
15
|
+
* @structure oauthLoginRouter(config, storage, providers[]) registers, per provider `<id>`:
|
|
16
|
+
* GET /v1/ghii/login/<id> (authorize) + GET /v1/ghii/login/<id>/callback (exchange → map
|
|
17
|
+
* returning/linked OR stash pending + redirect) + POST /v1/ghii/login/<id>/finalize (create with
|
|
18
|
+
* chosen username + session); plus shared-once: GET /v1/ghii/login/pending (read the pending
|
|
19
|
+
* cookie), GET /v1/ghii/username-available (live validation), GET /v1/auth/providers (discovery).
|
|
20
|
+
* Account linking is generic via storage.getGHIIByExternalId + GHIIRecord.externalIdentities
|
|
21
|
+
* (google also mirrors GHIIRecord.googleSub for the fast/indexed path).
|
|
22
|
+
* @usage const providers = buildOidcProviders(config); app.use(oauthLoginRouter(config, storage, providers));
|
|
22
23
|
* @version-history
|
|
23
24
|
* v1.0.0 — 2026-06-20 — Initial implementation: Google sign-in (link-by-verified-email or create).
|
|
24
25
|
* v1.1.0 — 2026-06-25 — Brand-new users choose their username once (pending-signup cookie +
|
|
25
26
|
* /login/pending + /login/google/finalize + /username-available); no more silent email-derived name.
|
|
27
|
+
* v2.0.0 — 2026-07-01 — Generalised to a multi-provider registry (Google + Casdoor + Entra ID):
|
|
28
|
+
* provider-parameterised routes, generic externalIdentities linking, per-provider claim mapping +
|
|
29
|
+
* Entra tenant gating, and a GET /v1/auth/providers discovery endpoint. Google routes/behaviour
|
|
30
|
+
* are unchanged (its id is 'google', so it emits the exact same paths + error codes).
|
|
26
31
|
*/
|
|
27
32
|
import { Router } from 'express';
|
|
28
33
|
import { createHash, randomUUID } from 'node:crypto';
|
|
@@ -35,14 +40,14 @@ import { establishOwnerSession } from '../services/owner-session.js';
|
|
|
35
40
|
import { rateLimit } from '../middleware/rate-limit.js';
|
|
36
41
|
import { validateOwnerName } from '../utils/gaii.js';
|
|
37
42
|
import { logger } from '../utils/logger.js';
|
|
38
|
-
/** Name of the short-lived, signed cookie that carries a not-yet-finalized
|
|
43
|
+
/** Name of the short-lived, signed cookie that carries a not-yet-finalized OIDC signup. */
|
|
39
44
|
const PENDING_COOKIE = 'aimeat_pending_signup';
|
|
40
|
-
/** Cookie path — covers both /v1/ghii/login/pending and /v1/ghii/login
|
|
45
|
+
/** Cookie path — covers both /v1/ghii/login/pending and /v1/ghii/login/<id>/finalize. */
|
|
41
46
|
const PENDING_COOKIE_PATH = '/v1/ghii';
|
|
42
47
|
/** Pending-signup token lifetime — long enough to pick a name, short enough to limit exposure. */
|
|
43
48
|
const PENDING_TTL_SECONDS = 30 * 60;
|
|
44
49
|
/** Discriminator claim so a pending-signup token can never be replayed as an auth token. */
|
|
45
|
-
const PENDING_PURPOSE = '
|
|
50
|
+
const PENDING_PURPOSE = 'oauth_signup';
|
|
46
51
|
/** SHA-256 hex of a normalized email — matches the hashing used elsewhere for GHII.emailHash. */
|
|
47
52
|
function emailHashOf(email) {
|
|
48
53
|
return createHash('sha256').update(email.toLowerCase().trim()).digest('hex');
|
|
@@ -59,12 +64,13 @@ function normalizeUsername(raw, nodeId) {
|
|
|
59
64
|
}
|
|
60
65
|
return { username };
|
|
61
66
|
}
|
|
62
|
-
/** Sign the verified
|
|
67
|
+
/** Sign the verified external identity into a short-lived EdDSA token (node key). */
|
|
63
68
|
async function signPendingToken(data) {
|
|
64
69
|
const { privateKey } = getNodeCryptoKeys();
|
|
65
70
|
return new SignJWT({
|
|
66
71
|
purpose: PENDING_PURPOSE,
|
|
67
|
-
|
|
72
|
+
provider: data.provider,
|
|
73
|
+
providerSub: data.providerSub,
|
|
68
74
|
email: data.email,
|
|
69
75
|
emailVerified: data.emailVerified,
|
|
70
76
|
displayName: data.displayName,
|
|
@@ -81,10 +87,11 @@ async function verifyPendingToken(token) {
|
|
|
81
87
|
try {
|
|
82
88
|
const { publicKey } = getNodeCryptoKeys();
|
|
83
89
|
const { payload } = await jwtVerify(token, publicKey, { algorithms: ['EdDSA'] });
|
|
84
|
-
if (payload.purpose !== PENDING_PURPOSE || typeof payload.
|
|
90
|
+
if (payload.purpose !== PENDING_PURPOSE || typeof payload.providerSub !== 'string' || typeof payload.provider !== 'string')
|
|
85
91
|
return null;
|
|
86
92
|
return {
|
|
87
|
-
|
|
93
|
+
provider: payload.provider,
|
|
94
|
+
providerSub: payload.providerSub,
|
|
88
95
|
email: typeof payload.email === 'string' ? payload.email : null,
|
|
89
96
|
emailVerified: payload.emailVerified === true,
|
|
90
97
|
displayName: typeof payload.displayName === 'string' ? payload.displayName : 'AIMEAT User',
|
|
@@ -115,7 +122,7 @@ function readPendingCookie(req) {
|
|
|
115
122
|
}
|
|
116
123
|
return null;
|
|
117
124
|
}
|
|
118
|
-
/** Set the httpOnly, host-only pending-signup cookie (Lax so it survives the
|
|
125
|
+
/** Set the httpOnly, host-only pending-signup cookie (Lax so it survives the IdP→callback redirect). */
|
|
119
126
|
function setPendingCookie(req, res, token) {
|
|
120
127
|
res.cookie(PENDING_COOKIE, token, {
|
|
121
128
|
httpOnly: true,
|
|
@@ -134,6 +141,17 @@ function clearPendingCookie(req, res) {
|
|
|
134
141
|
path: PENDING_COOKIE_PATH,
|
|
135
142
|
});
|
|
136
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* The partial GHII update that links a provider identity onto a record: writes the generic
|
|
146
|
+
* externalIdentities map entry and, for google, keeps the indexed googleSub mirror in sync.
|
|
147
|
+
*/
|
|
148
|
+
function externalIdUpdate(existing, providerId, sub) {
|
|
149
|
+
const externalIdentities = { ...(existing?.externalIdentities ?? {}), [providerId]: sub };
|
|
150
|
+
const update = { externalIdentities };
|
|
151
|
+
if (providerId === 'google')
|
|
152
|
+
update.googleSub = sub;
|
|
153
|
+
return update;
|
|
154
|
+
}
|
|
137
155
|
/**
|
|
138
156
|
* Derive a valid, unique owner name from an email address (or display name fallback).
|
|
139
157
|
* Owner names are `^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$` (3-64 chars). We sanitize the email
|
|
@@ -159,12 +177,12 @@ async function deriveUniqueUsername(storage, email, fallback) {
|
|
|
159
177
|
return candidate;
|
|
160
178
|
}
|
|
161
179
|
/**
|
|
162
|
-
* Create a fresh owner + GHII linked to
|
|
163
|
-
* Shared by the finalize
|
|
180
|
+
* Create a fresh owner + GHII linked to an external OIDC identity, with the welcome bonus.
|
|
181
|
+
* Shared by the finalize routes (the only path that mints a brand-new social account).
|
|
164
182
|
* Caller must have already validated the username is free + valid.
|
|
165
183
|
*/
|
|
166
|
-
async function
|
|
167
|
-
const { username, displayName, sub, email, emailVerified } = opts;
|
|
184
|
+
async function createOwnerForProvider(storage, config, opts) {
|
|
185
|
+
const { providerId, username, displayName, sub, email, emailVerified } = opts;
|
|
168
186
|
const now = new Date().toISOString();
|
|
169
187
|
const keyPair = await generateKeyPair();
|
|
170
188
|
// First real owner becomes operator (same self-heal logic as registration).
|
|
@@ -187,8 +205,10 @@ async function createOwnerForGoogle(storage, config, opts) {
|
|
|
187
205
|
nodeId: config.nodeId,
|
|
188
206
|
ghii,
|
|
189
207
|
displayName,
|
|
190
|
-
|
|
191
|
-
//
|
|
208
|
+
externalIdentities: { [providerId]: sub },
|
|
209
|
+
// Keep the indexed googleSub mirror for the google provider (fast returning-user lookup).
|
|
210
|
+
googleSub: providerId === 'google' ? sub : undefined,
|
|
211
|
+
// The IdP asserts the email; record it as a verified email (level 1) when trusted.
|
|
192
212
|
emailHash: emailVerified && email ? emailHashOf(email) : undefined,
|
|
193
213
|
emailVerifiedAt: emailVerified && email ? now : undefined,
|
|
194
214
|
notificationEmail: emailVerified && email ? email.toLowerCase().trim() : undefined,
|
|
@@ -212,155 +232,233 @@ async function createOwnerForGoogle(storage, config, opts) {
|
|
|
212
232
|
}
|
|
213
233
|
return ghiiRecord;
|
|
214
234
|
}
|
|
215
|
-
export function oauthLoginRouter(config, storage,
|
|
235
|
+
export function oauthLoginRouter(config, storage, providers) {
|
|
216
236
|
const router = Router();
|
|
217
|
-
const
|
|
237
|
+
const providerAvailable = (p) => p.enabled && !!p.client?.initialized;
|
|
218
238
|
// Sanitize a post-login redirect target: only same-site absolute paths are allowed.
|
|
219
239
|
function safeRedirectPath(raw) {
|
|
220
240
|
if (typeof raw === 'string' && raw.startsWith('/') && !raw.startsWith('//'))
|
|
221
241
|
return raw;
|
|
222
242
|
return '/';
|
|
223
243
|
}
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
244
|
+
// Shared: establish an owner session (httpOnly refresh cookie + access token) for a resolved GHII.
|
|
245
|
+
async function establishForGhii(req, res, ghiiRecord) {
|
|
246
|
+
const now = new Date().toISOString();
|
|
247
|
+
const ownerName = ghiiRecord.ownerName;
|
|
248
|
+
const ownerRecord = await storage.getOwner(ownerName);
|
|
249
|
+
const roles = [];
|
|
250
|
+
if (ownerRecord?.roles.includes('owner'))
|
|
251
|
+
roles.push('owner');
|
|
252
|
+
if (ownerRecord?.roles.includes('operator'))
|
|
253
|
+
roles.push('operator');
|
|
254
|
+
if (roles.length === 0)
|
|
255
|
+
roles.push('owner');
|
|
256
|
+
await storage.updateGHII(ghiiRecord.ghii, {
|
|
257
|
+
lastLoginAt: now,
|
|
258
|
+
loginCount: (ghiiRecord.loginCount ?? 0) + 1,
|
|
259
|
+
});
|
|
260
|
+
await establishOwnerSession(storage, config, req, res, { owner: ownerName, roles });
|
|
261
|
+
emitChange('ghii');
|
|
262
|
+
}
|
|
263
|
+
// Register the authorize/callback/finalize trio for one provider.
|
|
264
|
+
function registerProvider(p) {
|
|
265
|
+
const PREFIX = p.id.toUpperCase();
|
|
266
|
+
// GET /v1/ghii/login/<id> — Begin sign-in (redirect to the IdP's consent screen)
|
|
267
|
+
router.get(`/v1/ghii/login/${p.id}`, async (req, res) => {
|
|
268
|
+
try {
|
|
269
|
+
if (!providerAvailable(p)) {
|
|
270
|
+
res.status(503).json(error(config.nodeId, 'FEATURE_DISABLED', `${p.label} is not available on this node`));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const authRequest = p.client.createAuthRequest();
|
|
274
|
+
const nonceTtl = config.nonceTtlSeconds * 1000;
|
|
275
|
+
await storage.createVerificationNonce({
|
|
276
|
+
id: randomUUID(),
|
|
277
|
+
owner: '', // login flow — no authenticated owner yet
|
|
278
|
+
type: p.nonceType,
|
|
279
|
+
state: authRequest.state,
|
|
280
|
+
nonce: authRequest.nonce,
|
|
281
|
+
redirectUri: safeRedirectPath(req.query.redirect), // where to send the user after login
|
|
282
|
+
createdAt: new Date().toISOString(),
|
|
283
|
+
expiresAt: new Date(Date.now() + nonceTtl).toISOString(),
|
|
284
|
+
});
|
|
285
|
+
res.redirect(authRequest.authorizationUrl);
|
|
263
286
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return;
|
|
287
|
+
catch (err) {
|
|
288
|
+
logger.error(`${p.id} sign-in authorize failed`, { error: String(err) });
|
|
289
|
+
res.redirect(`${config.baseUrl}/?auth_error=${PREFIX}_START_FAILED`);
|
|
268
290
|
}
|
|
269
|
-
|
|
291
|
+
});
|
|
292
|
+
// GET /v1/ghii/login/<id>/callback — IdP redirects back here with code + state
|
|
293
|
+
router.get(`/v1/ghii/login/${p.id}/callback`, async (req, res) => {
|
|
294
|
+
const fail = (code) => res.redirect(`${config.baseUrl}/?auth_error=${encodeURIComponent(code)}`);
|
|
295
|
+
try {
|
|
296
|
+
if (!providerAvailable(p)) {
|
|
297
|
+
res.status(503).json(error(config.nodeId, 'FEATURE_DISABLED', `${p.label} is not available on this node`));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const code = req.query.code;
|
|
301
|
+
const state = req.query.state;
|
|
302
|
+
if (!code || !state) {
|
|
303
|
+
fail(`${PREFIX}_MISSING_CODE`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const nonceRecord = await storage.getVerificationNonce(state);
|
|
307
|
+
if (!nonceRecord || nonceRecord.type !== p.nonceType) {
|
|
308
|
+
fail(`${PREFIX}_INVALID_STATE`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (new Date(nonceRecord.expiresAt) < new Date()) {
|
|
312
|
+
await storage.deleteVerificationNonce(state);
|
|
313
|
+
fail(`${PREFIX}_STATE_EXPIRED`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const tokenResult = await p.client.exchangeCode(code, state, nonceRecord.nonce);
|
|
317
|
+
const postLoginPath = safeRedirectPath(nonceRecord.redirectUri);
|
|
270
318
|
await storage.deleteVerificationNonce(state);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
319
|
+
if (!tokenResult.valid || !tokenResult.claims) {
|
|
320
|
+
fail(`${PREFIX}_EXCHANGE_FAILED`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// Optional pre-mapping gate (e.g. Entra single-tenant enforcement via the `tid` claim).
|
|
324
|
+
if (p.validateClaims) {
|
|
325
|
+
const verr = p.validateClaims(tokenResult.claims);
|
|
326
|
+
if (verr) {
|
|
327
|
+
fail(verr);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const mapped = p.mapClaims(tokenResult.claims);
|
|
332
|
+
if (!mapped) {
|
|
333
|
+
fail(`${PREFIX}_NO_SUBJECT`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const { sub, email, emailVerified, displayName } = mapped;
|
|
337
|
+
// ── Map the external identity to a GHII ──
|
|
338
|
+
// 1) Returning user — matched by this provider's stable subject.
|
|
339
|
+
let ghiiRecord = await storage.getGHIIByExternalId(p.id, sub);
|
|
340
|
+
// 2) Link to an existing account ONLY when the IdP's email is verified AND it matches a GHII
|
|
341
|
+
// whose email was already locally verified. Requiring the local side to be verified too
|
|
342
|
+
// prevents takeover of an account that merely *claimed* (never proved) this email.
|
|
343
|
+
if (!ghiiRecord && emailVerified && email) {
|
|
344
|
+
const byEmail = await storage.getGHIIByEmailHash(emailHashOf(email));
|
|
345
|
+
if (byEmail && byEmail.emailVerifiedAt) {
|
|
346
|
+
ghiiRecord = await storage.updateGHII(byEmail.ghii, externalIdUpdate(byEmail, p.id, sub));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// 3) No match — a brand-new user. Do NOT silently create an account with an email-derived
|
|
350
|
+
// username. Stash the verified identity in a short-lived signed cookie and bounce back to
|
|
351
|
+
// the SPA for a one-time username choice; POST .../finalize then creates account + session.
|
|
352
|
+
if (!ghiiRecord) {
|
|
353
|
+
const suggested = await deriveUniqueUsername(storage, email ?? undefined, displayName);
|
|
354
|
+
const token = await signPendingToken({
|
|
355
|
+
provider: p.id,
|
|
356
|
+
providerSub: sub,
|
|
357
|
+
email: email ?? null,
|
|
358
|
+
emailVerified,
|
|
359
|
+
displayName,
|
|
360
|
+
suggested,
|
|
361
|
+
redirect: postLoginPath,
|
|
362
|
+
});
|
|
363
|
+
setPendingCookie(req, res, token);
|
|
364
|
+
const sep = postLoginPath.includes('?') ? '&' : '?';
|
|
365
|
+
res.redirect(`${config.baseUrl}${postLoginPath}${sep}aimeat_signup=1`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
await establishForGhii(req, res, ghiiRecord);
|
|
369
|
+
// Redirect back to the SPA — it boots logged-in from the refresh cookie.
|
|
370
|
+
res.redirect(`${config.baseUrl}${postLoginPath}`);
|
|
280
371
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const emailVerified = claims.email_verified === true || claims.email_verified === 'true';
|
|
285
|
-
const displayName = (typeof claims.name === 'string' && claims.name)
|
|
286
|
-
|| (typeof claims.given_name === 'string' && claims.given_name)
|
|
287
|
-
|| email?.split('@')[0]
|
|
288
|
-
|| 'AIMEAT User';
|
|
289
|
-
if (!sub) {
|
|
290
|
-
fail('GOOGLE_NO_SUBJECT');
|
|
291
|
-
return;
|
|
372
|
+
catch (err) {
|
|
373
|
+
logger.error(`${p.id} sign-in callback failed`, { error: String(err) });
|
|
374
|
+
fail(`${PREFIX}_CALLBACK_FAILED`);
|
|
292
375
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
ghiiRecord = await storage.updateGHII(byEmail.ghii, { googleSub: sub });
|
|
376
|
+
});
|
|
377
|
+
// POST /v1/ghii/login/<id>/finalize — Complete a brand-new signup with the chosen username.
|
|
378
|
+
// Reads the signed pending cookie (must be for THIS provider), validates the name, creates the
|
|
379
|
+
// owner + GHII, and establishes the session. Idempotent on double-submit.
|
|
380
|
+
router.post(`/v1/ghii/login/${p.id}/finalize`, rateLimit({ max: config.registrationRateLimitMax, windowMs: config.registrationRateLimitWindowMs }), async (req, res) => {
|
|
381
|
+
try {
|
|
382
|
+
const raw = readPendingCookie(req);
|
|
383
|
+
const pending = raw ? await verifyPendingToken(raw) : null;
|
|
384
|
+
if (!pending || pending.provider !== p.id) {
|
|
385
|
+
res.status(400).json(error(config.nodeId, 'NO_PENDING_SIGNUP', 'No pending sign-up — start sign-in again'));
|
|
386
|
+
return;
|
|
305
387
|
}
|
|
388
|
+
let ghiiRecord = await storage.getGHIIByExternalId(p.id, pending.providerSub);
|
|
389
|
+
if (!ghiiRecord) {
|
|
390
|
+
// Username: the user's choice, falling back to the suggested name if they left it blank.
|
|
391
|
+
const chosenRaw = (req.body && typeof req.body.username === 'string' && req.body.username.trim())
|
|
392
|
+
? req.body.username
|
|
393
|
+
: pending.suggested;
|
|
394
|
+
const { username, remoteNode } = normalizeUsername(chosenRaw, config.nodeId);
|
|
395
|
+
if (remoteNode) {
|
|
396
|
+
res.status(400).json(error(config.nodeId, 'INVALID_INPUT', `Cannot register here with a remote identity (node ${remoteNode})`));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const nameError = validateOwnerName(username);
|
|
400
|
+
if (nameError) {
|
|
401
|
+
res.status(400).json(error(config.nodeId, 'INVALID_INPUT', nameError));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (await storage.getOwner(username)) {
|
|
405
|
+
res.status(409).json(error(config.nodeId, 'NAME_TAKEN', `Username "${username}" is already registered`));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
// Display name: the user's choice from the signup modal, falling back to the
|
|
409
|
+
// provider-supplied name when left blank. Unlike the username, it is editable later.
|
|
410
|
+
const chosenDisplayName = (req.body && typeof req.body.displayName === 'string' && req.body.displayName.trim())
|
|
411
|
+
? req.body.displayName.trim()
|
|
412
|
+
: pending.displayName;
|
|
413
|
+
ghiiRecord = await createOwnerForProvider(storage, config, {
|
|
414
|
+
providerId: p.id,
|
|
415
|
+
username,
|
|
416
|
+
displayName: chosenDisplayName,
|
|
417
|
+
sub: pending.providerSub,
|
|
418
|
+
email: pending.email,
|
|
419
|
+
emailVerified: pending.emailVerified,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
await establishForGhii(req, res, ghiiRecord);
|
|
423
|
+
clearPendingCookie(req, res);
|
|
424
|
+
res.json(success(config.nodeId, {
|
|
425
|
+
ghii: ghiiRecord.ghii,
|
|
426
|
+
owner: ghiiRecord.ownerName,
|
|
427
|
+
displayName: ghiiRecord.displayName,
|
|
428
|
+
redirect: safeRedirectPath(pending.redirect),
|
|
429
|
+
}));
|
|
306
430
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// cookie and bounce back to the SPA, which prompts for a one-time username choice;
|
|
311
|
-
// POST /v1/ghii/login/google/finalize then creates the account + session.
|
|
312
|
-
if (!ghiiRecord) {
|
|
313
|
-
const suggested = await deriveUniqueUsername(storage, email, String(displayName));
|
|
314
|
-
const token = await signPendingToken({
|
|
315
|
-
googleSub: sub,
|
|
316
|
-
email: email ?? null,
|
|
317
|
-
emailVerified,
|
|
318
|
-
displayName: String(displayName),
|
|
319
|
-
suggested,
|
|
320
|
-
redirect: postLoginPath,
|
|
321
|
-
});
|
|
322
|
-
setPendingCookie(req, res, token);
|
|
323
|
-
// Return them to where they started, flagged so the auth lib opens the choose-username step.
|
|
324
|
-
const sep = postLoginPath.includes('?') ? '&' : '?';
|
|
325
|
-
res.redirect(`${config.baseUrl}${postLoginPath}${sep}aimeat_signup=1`);
|
|
326
|
-
return;
|
|
431
|
+
catch (err) {
|
|
432
|
+
logger.error(`${p.id} sign-in finalize failed`, { error: String(err) });
|
|
433
|
+
res.status(500).json(error(config.nodeId, 'FINALIZE_FAILED', 'Could not complete sign-up'));
|
|
327
434
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
});
|
|
342
|
-
await establishOwnerSession(storage, config, req, res, { owner: ownerName, roles });
|
|
343
|
-
emitChange('ghii');
|
|
344
|
-
// Redirect back to the SPA — it boots logged-in from the refresh cookie.
|
|
345
|
-
res.redirect(`${config.baseUrl}${postLoginPath}`);
|
|
346
|
-
}
|
|
347
|
-
catch (err) {
|
|
348
|
-
logger.error('Google sign-in callback failed', { error: String(err) });
|
|
349
|
-
fail('GOOGLE_CALLBACK_FAILED');
|
|
350
|
-
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
for (const p of providers)
|
|
438
|
+
registerProvider(p);
|
|
439
|
+
// GET /v1/auth/providers — Discovery: which social-login providers this node offers. Lets any SPA
|
|
440
|
+
// render the right sign-in buttons instead of hard-coding provider availability.
|
|
441
|
+
router.get('/v1/auth/providers', (_req, res) => {
|
|
442
|
+
const enabled = providers.filter(p => p.enabled).map(p => ({
|
|
443
|
+
id: p.id,
|
|
444
|
+
label: p.label,
|
|
445
|
+
loginUrl: `/v1/ghii/login/${p.id}`,
|
|
446
|
+
}));
|
|
447
|
+
res.json(success(config.nodeId, { providers: enabled }));
|
|
351
448
|
});
|
|
352
|
-
// GET /v1/ghii/login/pending — Read the pending
|
|
353
|
-
//
|
|
354
|
-
// signed httpOnly cookie set by the callback;
|
|
449
|
+
// GET /v1/ghii/login/pending — Read the pending signup (suggested username, which email, and which
|
|
450
|
+
// provider) so the SPA can render the one-time username-choice step + finalize to the right route.
|
|
451
|
+
// Reads the signed httpOnly cookie set by the callback; 404s when absent/expired.
|
|
355
452
|
router.get('/v1/ghii/login/pending', async (req, res) => {
|
|
356
453
|
const raw = readPendingCookie(req);
|
|
357
454
|
const pending = raw ? await verifyPendingToken(raw) : null;
|
|
358
455
|
if (!pending) {
|
|
359
|
-
res.status(404).json(error(config.nodeId, 'NO_PENDING_SIGNUP', 'No pending
|
|
456
|
+
res.status(404).json(error(config.nodeId, 'NO_PENDING_SIGNUP', 'No pending sign-up'));
|
|
360
457
|
return;
|
|
361
458
|
}
|
|
362
|
-
// The
|
|
459
|
+
// The provider subject stays server-side; the client only needs what it must show + the redirect.
|
|
363
460
|
res.json(success(config.nodeId, {
|
|
461
|
+
provider: pending.provider,
|
|
364
462
|
suggested: pending.suggested,
|
|
365
463
|
email: pending.email,
|
|
366
464
|
displayName: pending.displayName,
|
|
@@ -383,76 +481,6 @@ export function oauthLoginRouter(config, storage, googleClient) {
|
|
|
383
481
|
const taken = !!(await storage.getOwner(username));
|
|
384
482
|
res.json(success(config.nodeId, { name: username, valid: true, available: !taken, reason: taken ? 'Username is already taken' : null }));
|
|
385
483
|
});
|
|
386
|
-
// POST /v1/ghii/login/google/finalize — Complete a brand-new Google sign-up with the username
|
|
387
|
-
// the user chose (or confirmed). Reads the signed pending cookie, validates the name, creates the
|
|
388
|
-
// owner + GHII, and establishes the owner session. Idempotent: if the Google identity already
|
|
389
|
-
// resolved to a GHII (double-submit / a parallel finalize), just establish that session.
|
|
390
|
-
router.post('/v1/ghii/login/google/finalize', rateLimit({ max: config.registrationRateLimitMax, windowMs: config.registrationRateLimitWindowMs }), async (req, res) => {
|
|
391
|
-
try {
|
|
392
|
-
const raw = readPendingCookie(req);
|
|
393
|
-
const pending = raw ? await verifyPendingToken(raw) : null;
|
|
394
|
-
if (!pending) {
|
|
395
|
-
res.status(400).json(error(config.nodeId, 'NO_PENDING_SIGNUP', 'No pending Google sign-up — start sign-in again'));
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
let ghiiRecord = await storage.getGHIIByGoogleSub(pending.googleSub);
|
|
399
|
-
if (!ghiiRecord) {
|
|
400
|
-
// Username: the user's choice, falling back to the suggested name if they left it blank.
|
|
401
|
-
const chosenRaw = (req.body && typeof req.body.username === 'string' && req.body.username.trim())
|
|
402
|
-
? req.body.username
|
|
403
|
-
: pending.suggested;
|
|
404
|
-
const { username, remoteNode } = normalizeUsername(chosenRaw, config.nodeId);
|
|
405
|
-
if (remoteNode) {
|
|
406
|
-
res.status(400).json(error(config.nodeId, 'INVALID_INPUT', `Cannot register here with a remote identity (node ${remoteNode})`));
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
const nameError = validateOwnerName(username);
|
|
410
|
-
if (nameError) {
|
|
411
|
-
res.status(400).json(error(config.nodeId, 'INVALID_INPUT', nameError));
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
if (await storage.getOwner(username)) {
|
|
415
|
-
res.status(409).json(error(config.nodeId, 'NAME_TAKEN', `Username "${username}" is already registered`));
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
ghiiRecord = await createOwnerForGoogle(storage, config, {
|
|
419
|
-
username,
|
|
420
|
-
displayName: pending.displayName,
|
|
421
|
-
sub: pending.googleSub,
|
|
422
|
-
email: pending.email,
|
|
423
|
-
emailVerified: pending.emailVerified,
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
// ── Establish an owner session (httpOnly refresh cookie + access token) ──
|
|
427
|
-
const now = new Date().toISOString();
|
|
428
|
-
const ownerName = ghiiRecord.ownerName;
|
|
429
|
-
const ownerRecord = await storage.getOwner(ownerName);
|
|
430
|
-
const roles = [];
|
|
431
|
-
if (ownerRecord?.roles.includes('owner'))
|
|
432
|
-
roles.push('owner');
|
|
433
|
-
if (ownerRecord?.roles.includes('operator'))
|
|
434
|
-
roles.push('operator');
|
|
435
|
-
if (roles.length === 0)
|
|
436
|
-
roles.push('owner');
|
|
437
|
-
await storage.updateGHII(ghiiRecord.ghii, {
|
|
438
|
-
lastLoginAt: now,
|
|
439
|
-
loginCount: (ghiiRecord.loginCount ?? 0) + 1,
|
|
440
|
-
});
|
|
441
|
-
await establishOwnerSession(storage, config, req, res, { owner: ownerName, roles });
|
|
442
|
-
clearPendingCookie(req, res);
|
|
443
|
-
emitChange('ghii');
|
|
444
|
-
res.json(success(config.nodeId, {
|
|
445
|
-
ghii: ghiiRecord.ghii,
|
|
446
|
-
owner: ownerName,
|
|
447
|
-
displayName: ghiiRecord.displayName,
|
|
448
|
-
redirect: safeRedirectPath(pending.redirect),
|
|
449
|
-
}));
|
|
450
|
-
}
|
|
451
|
-
catch (err) {
|
|
452
|
-
logger.error('Google sign-in finalize failed', { error: String(err) });
|
|
453
|
-
res.status(500).json(error(config.nodeId, 'FINALIZE_FAILED', 'Could not complete sign-up'));
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
484
|
return router;
|
|
457
485
|
}
|
|
458
486
|
//# sourceMappingURL=oauth-login.js.map
|