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.
Files changed (296) hide show
  1. package/dist/.env.example +31 -0
  2. package/dist/locales/en.json +192 -27
  3. package/dist/locales/fi.json +192 -27
  4. package/dist/public/components/Markdown.js +9 -0
  5. package/dist/public/components/MemoryEmbed.js +151 -0
  6. package/dist/public/components/Mermaid.js +4 -1
  7. package/dist/public/components/NotificationBell.js +45 -19
  8. package/dist/public/css/components/markdown.css +15 -0
  9. package/dist/public/css/views/agents-detail.css +17 -0
  10. package/dist/public/css/views/portfolio.css +51 -0
  11. package/dist/public/css/views/profile.css +31 -0
  12. package/dist/public/js/services/agents.js +9 -0
  13. package/dist/public/js/services/organisms.js +37 -1
  14. package/dist/public/llms-template.txt +278 -0
  15. package/dist/public/spa.html +39 -2
  16. package/dist/public/sw.js +50 -34
  17. package/dist/public/views/my-company.js +13 -1
  18. package/dist/public/views/portfolio.js +267 -44
  19. package/dist/public/views/profile/agents/agent-card.js +4 -0
  20. package/dist/public/views/profile/agents/tab-contracts.js +112 -0
  21. package/dist/public/views/profile/landing-page.js +10 -1
  22. package/dist/public/views/profile/memory-tab.js +6 -2
  23. package/dist/public/views/profile/notifications-tab.js +9 -1
  24. package/dist/public/views/profile/organisms/home.js +23 -3
  25. package/dist/public/views/profile/organisms/participants-panel.js +89 -17
  26. package/dist/public/views/profile/organisms/workspace-apps.js +130 -0
  27. package/dist/public/views/profile/organisms/workspace.js +5 -0
  28. package/dist/public/views/profile/organisms-tab.js +1 -1
  29. package/dist/public/views/secretary.js +13 -0
  30. package/dist/scripts/archive-stale-versions.d.ts +33 -0
  31. package/dist/scripts/archive-stale-versions.d.ts.map +1 -0
  32. package/dist/scripts/archive-stale-versions.js +278 -0
  33. package/dist/scripts/archive-stale-versions.js.map +1 -0
  34. package/dist/src/cli/connect/mcp/tools/agent-management.d.ts +4 -1
  35. package/dist/src/cli/connect/mcp/tools/agent-management.d.ts.map +1 -1
  36. package/dist/src/cli/connect/mcp/tools/agent-management.js +1 -1
  37. package/dist/src/cli/connect/mcp/tools/agent-management.js.map +1 -1
  38. package/dist/src/cli/init-wizard.d.ts.map +1 -1
  39. package/dist/src/cli/init-wizard.js +32 -0
  40. package/dist/src/cli/init-wizard.js.map +1 -1
  41. package/dist/src/config.d.ts +24 -0
  42. package/dist/src/config.d.ts.map +1 -1
  43. package/dist/src/config.js +21 -0
  44. package/dist/src/config.js.map +1 -1
  45. package/dist/src/data/aimeat-iam-package.d.ts +10 -2
  46. package/dist/src/data/aimeat-iam-package.d.ts.map +1 -1
  47. package/dist/src/data/aimeat-iam-package.js +39 -10
  48. package/dist/src/data/aimeat-iam-package.js.map +1 -1
  49. package/dist/src/generated/api-types.d.ts +802 -60
  50. package/dist/src/generated/api-types.d.ts.map +1 -1
  51. package/dist/src/generated/prisma-postgres/edge.js +5 -3
  52. package/dist/src/generated/prisma-postgres/index-browser.js +2 -0
  53. package/dist/src/generated/prisma-postgres/index.d.ts +54 -2
  54. package/dist/src/generated/prisma-postgres/index.js +5 -3
  55. package/dist/src/generated/prisma-postgres/package.json +1 -1
  56. package/dist/src/generated/prisma-postgres/schema.prisma +2 -0
  57. package/dist/src/generated/prisma-postgres/wasm.js +5 -3
  58. package/dist/src/mcp/annotations.d.ts.map +1 -1
  59. package/dist/src/mcp/annotations.js +1 -0
  60. package/dist/src/mcp/annotations.js.map +1 -1
  61. package/dist/src/mcp/catalog/definitions.d.ts +3 -0
  62. package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
  63. package/dist/src/mcp/catalog/definitions.js +21 -7
  64. package/dist/src/mcp/catalog/definitions.js.map +1 -1
  65. package/dist/src/mcp/catalog/surfaces.js +1 -1
  66. package/dist/src/mcp/catalog/surfaces.js.map +1 -1
  67. package/dist/src/mcp/core.js +1 -1
  68. package/dist/src/mcp/core.js.map +1 -1
  69. package/dist/src/mcp/extensions.d.ts.map +1 -1
  70. package/dist/src/mcp/extensions.js +21 -0
  71. package/dist/src/mcp/extensions.js.map +1 -1
  72. package/dist/src/mcp/memory-extended.js +1 -1
  73. package/dist/src/mcp/memory-extended.js.map +1 -1
  74. package/dist/src/mcp/organisms.d.ts +1 -0
  75. package/dist/src/mcp/organisms.d.ts.map +1 -1
  76. package/dist/src/mcp/organisms.js +23 -6
  77. package/dist/src/mcp/organisms.js.map +1 -1
  78. package/dist/src/mcp/workspaces.d.ts +7 -0
  79. package/dist/src/mcp/workspaces.d.ts.map +1 -1
  80. package/dist/src/mcp/workspaces.js +23 -5
  81. package/dist/src/mcp/workspaces.js.map +1 -1
  82. package/dist/src/middleware/subdomain.d.ts +9 -0
  83. package/dist/src/middleware/subdomain.d.ts.map +1 -1
  84. package/dist/src/middleware/subdomain.js +24 -3
  85. package/dist/src/middleware/subdomain.js.map +1 -1
  86. package/dist/src/models/schemas.d.ts +2 -0
  87. package/dist/src/models/schemas.d.ts.map +1 -1
  88. package/dist/src/models/schemas.js +2 -2
  89. package/dist/src/models/schemas.js.map +1 -1
  90. package/dist/src/routes/admin-features.js +1 -1
  91. package/dist/src/routes/admin-features.js.map +1 -1
  92. package/dist/src/routes/agent-onboarding.d.ts.map +1 -1
  93. package/dist/src/routes/agent-onboarding.js +44 -7
  94. package/dist/src/routes/agent-onboarding.js.map +1 -1
  95. package/dist/src/routes/agents.d.ts +8 -0
  96. package/dist/src/routes/agents.d.ts.map +1 -1
  97. package/dist/src/routes/agents.js +91 -5
  98. package/dist/src/routes/agents.js.map +1 -1
  99. package/dist/src/routes/app-grants.d.ts +5 -0
  100. package/dist/src/routes/app-grants.d.ts.map +1 -1
  101. package/dist/src/routes/app-grants.js +52 -20
  102. package/dist/src/routes/app-grants.js.map +1 -1
  103. package/dist/src/routes/boards.d.ts.map +1 -1
  104. package/dist/src/routes/boards.js +4 -1
  105. package/dist/src/routes/boards.js.map +1 -1
  106. package/dist/src/routes/bootstrap.d.ts +2 -0
  107. package/dist/src/routes/bootstrap.d.ts.map +1 -1
  108. package/dist/src/routes/bootstrap.js +5 -0
  109. package/dist/src/routes/bootstrap.js.map +1 -1
  110. package/dist/src/routes/capabilities.d.ts.map +1 -1
  111. package/dist/src/routes/capabilities.js +4 -1
  112. package/dist/src/routes/capabilities.js.map +1 -1
  113. package/dist/src/routes/catalogue.d.ts.map +1 -1
  114. package/dist/src/routes/catalogue.js +5 -4
  115. package/dist/src/routes/catalogue.js.map +1 -1
  116. package/dist/src/routes/extensions.d.ts.map +1 -1
  117. package/dist/src/routes/extensions.js +11 -0
  118. package/dist/src/routes/extensions.js.map +1 -1
  119. package/dist/src/routes/ghii.d.ts.map +1 -1
  120. package/dist/src/routes/ghii.js +65 -17
  121. package/dist/src/routes/ghii.js.map +1 -1
  122. package/dist/src/routes/lib-editor.d.ts +26 -0
  123. package/dist/src/routes/lib-editor.d.ts.map +1 -0
  124. package/dist/src/routes/lib-editor.js +215 -0
  125. package/dist/src/routes/lib-editor.js.map +1 -0
  126. package/dist/src/routes/lib-markdown.d.ts +16 -1
  127. package/dist/src/routes/lib-markdown.d.ts.map +1 -1
  128. package/dist/src/routes/lib-markdown.js +281 -2
  129. package/dist/src/routes/lib-markdown.js.map +1 -1
  130. package/dist/src/routes/lib-organism.d.ts +28 -0
  131. package/dist/src/routes/lib-organism.d.ts.map +1 -0
  132. package/dist/src/routes/lib-organism.js +298 -0
  133. package/dist/src/routes/lib-organism.js.map +1 -0
  134. package/dist/src/routes/lib-portfolio-standalone.d.ts +22 -0
  135. package/dist/src/routes/lib-portfolio-standalone.d.ts.map +1 -0
  136. package/dist/src/routes/lib-portfolio-standalone.js +68 -0
  137. package/dist/src/routes/lib-portfolio-standalone.js.map +1 -0
  138. package/dist/src/routes/libs.d.ts +18 -0
  139. package/dist/src/routes/libs.d.ts.map +1 -1
  140. package/dist/src/routes/libs.js +116 -18
  141. package/dist/src/routes/libs.js.map +1 -1
  142. package/dist/src/routes/memory.d.ts +3 -0
  143. package/dist/src/routes/memory.d.ts.map +1 -1
  144. package/dist/src/routes/memory.js +58 -4
  145. package/dist/src/routes/memory.js.map +1 -1
  146. package/dist/src/routes/notifications.d.ts +5 -1
  147. package/dist/src/routes/notifications.d.ts.map +1 -1
  148. package/dist/src/routes/notifications.js +68 -3
  149. package/dist/src/routes/notifications.js.map +1 -1
  150. package/dist/src/routes/oauth-login.d.ts +26 -21
  151. package/dist/src/routes/oauth-login.d.ts.map +1 -1
  152. package/dist/src/routes/oauth-login.js +257 -229
  153. package/dist/src/routes/oauth-login.js.map +1 -1
  154. package/dist/src/routes/openrouter.d.ts +3 -0
  155. package/dist/src/routes/openrouter.d.ts.map +1 -1
  156. package/dist/src/routes/openrouter.js +10 -5
  157. package/dist/src/routes/openrouter.js.map +1 -1
  158. package/dist/src/routes/organisms.d.ts +6 -0
  159. package/dist/src/routes/organisms.d.ts.map +1 -1
  160. package/dist/src/routes/organisms.js +203 -16
  161. package/dist/src/routes/organisms.js.map +1 -1
  162. package/dist/src/routes/owners.d.ts.map +1 -1
  163. package/dist/src/routes/owners.js +12 -8
  164. package/dist/src/routes/owners.js.map +1 -1
  165. package/dist/src/routes/portfolio.d.ts +41 -1
  166. package/dist/src/routes/portfolio.d.ts.map +1 -1
  167. package/dist/src/routes/portfolio.js +94 -30
  168. package/dist/src/routes/portfolio.js.map +1 -1
  169. package/dist/src/routes/push.d.ts +15 -0
  170. package/dist/src/routes/push.d.ts.map +1 -1
  171. package/dist/src/routes/push.js +16 -1
  172. package/dist/src/routes/push.js.map +1 -1
  173. package/dist/src/routes/site.d.ts +2 -0
  174. package/dist/src/routes/site.d.ts.map +1 -1
  175. package/dist/src/routes/site.js +10 -2
  176. package/dist/src/routes/site.js.map +1 -1
  177. package/dist/src/routes/subdomains.d.ts +5 -0
  178. package/dist/src/routes/subdomains.d.ts.map +1 -1
  179. package/dist/src/routes/subdomains.js +56 -0
  180. package/dist/src/routes/subdomains.js.map +1 -1
  181. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  182. package/dist/src/server-bootstrap/routes-loader.js +10 -15
  183. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  184. package/dist/src/server-bootstrap/static-files.d.ts.map +1 -1
  185. package/dist/src/server-bootstrap/static-files.js +9 -4
  186. package/dist/src/server-bootstrap/static-files.js.map +1 -1
  187. package/dist/src/services/access-guard.d.ts.map +1 -1
  188. package/dist/src/services/access-guard.js +7 -0
  189. package/dist/src/services/access-guard.js.map +1 -1
  190. package/dist/src/services/ai-completion.d.ts +2 -0
  191. package/dist/src/services/ai-completion.d.ts.map +1 -1
  192. package/dist/src/services/ai-completion.js +2 -1
  193. package/dist/src/services/ai-completion.js.map +1 -1
  194. package/dist/src/services/config-schema.d.ts.map +1 -1
  195. package/dist/src/services/config-schema.js +2 -0
  196. package/dist/src/services/config-schema.js.map +1 -1
  197. package/dist/src/services/ecosystem-access.d.ts +24 -8
  198. package/dist/src/services/ecosystem-access.d.ts.map +1 -1
  199. package/dist/src/services/ecosystem-access.js +27 -5
  200. package/dist/src/services/ecosystem-access.js.map +1 -1
  201. package/dist/src/services/generator-autopilot.d.ts +2 -0
  202. package/dist/src/services/generator-autopilot.d.ts.map +1 -1
  203. package/dist/src/services/generator-autopilot.js +3 -1
  204. package/dist/src/services/generator-autopilot.js.map +1 -1
  205. package/dist/src/services/iam/app-commands.d.ts +75 -0
  206. package/dist/src/services/iam/app-commands.d.ts.map +1 -0
  207. package/dist/src/services/iam/app-commands.js +55 -0
  208. package/dist/src/services/iam/app-commands.js.map +1 -0
  209. package/dist/src/services/iam/define-app-iam.d.ts +45 -0
  210. package/dist/src/services/iam/define-app-iam.d.ts.map +1 -0
  211. package/dist/src/services/iam/define-app-iam.js +75 -0
  212. package/dist/src/services/iam/define-app-iam.js.map +1 -0
  213. package/dist/src/services/iam/model.d.ts +113 -0
  214. package/dist/src/services/iam/model.d.ts.map +1 -0
  215. package/dist/src/services/iam/model.js +115 -0
  216. package/dist/src/services/iam/model.js.map +1 -0
  217. package/dist/src/services/mailbox-notification.d.ts +1 -0
  218. package/dist/src/services/mailbox-notification.d.ts.map +1 -1
  219. package/dist/src/services/mailbox-notification.js +3 -0
  220. package/dist/src/services/mailbox-notification.js.map +1 -1
  221. package/dist/src/services/notebook-ai.d.ts +2 -0
  222. package/dist/src/services/notebook-ai.d.ts.map +1 -1
  223. package/dist/src/services/notebook-ai.js +1 -1
  224. package/dist/src/services/notebook-ai.js.map +1 -1
  225. package/dist/src/services/notify.d.ts +11 -1
  226. package/dist/src/services/notify.d.ts.map +1 -1
  227. package/dist/src/services/notify.js +39 -2
  228. package/dist/src/services/notify.js.map +1 -1
  229. package/dist/src/services/oidc-providers.d.ts +94 -0
  230. package/dist/src/services/oidc-providers.d.ts.map +1 -0
  231. package/dist/src/services/oidc-providers.js +181 -0
  232. package/dist/src/services/oidc-providers.js.map +1 -0
  233. package/dist/src/services/organism-privacy.d.ts +46 -0
  234. package/dist/src/services/organism-privacy.d.ts.map +1 -0
  235. package/dist/src/services/organism-privacy.js +42 -0
  236. package/dist/src/services/organism-privacy.js.map +1 -0
  237. package/dist/src/services/organism-template.d.ts.map +1 -1
  238. package/dist/src/services/organism-template.js +3 -1
  239. package/dist/src/services/organism-template.js.map +1 -1
  240. package/dist/src/services/prompt-defaults.d.ts +2 -0
  241. package/dist/src/services/prompt-defaults.d.ts.map +1 -1
  242. package/dist/src/services/prompt-defaults.js +5 -0
  243. package/dist/src/services/prompt-defaults.js.map +1 -1
  244. package/dist/src/services/scheduler.d.ts.map +1 -1
  245. package/dist/src/services/scheduler.js +16 -4
  246. package/dist/src/services/scheduler.js.map +1 -1
  247. package/dist/src/services/skill-bundle/generator.d.ts +2 -0
  248. package/dist/src/services/skill-bundle/generator.d.ts.map +1 -1
  249. package/dist/src/services/skill-bundle/generator.js +3 -1
  250. package/dist/src/services/skill-bundle/generator.js.map +1 -1
  251. package/dist/src/services/workspace-access.d.ts +2 -0
  252. package/dist/src/services/workspace-access.d.ts.map +1 -1
  253. package/dist/src/services/workspace-access.js +20 -8
  254. package/dist/src/services/workspace-access.js.map +1 -1
  255. package/dist/src/services/workspace-engagements.d.ts +65 -0
  256. package/dist/src/services/workspace-engagements.d.ts.map +1 -0
  257. package/dist/src/services/workspace-engagements.js +92 -0
  258. package/dist/src/services/workspace-engagements.js.map +1 -0
  259. package/dist/src/services/workspace-meta.d.ts +9 -2
  260. package/dist/src/services/workspace-meta.d.ts.map +1 -1
  261. package/dist/src/services/workspace-meta.js +22 -2
  262. package/dist/src/services/workspace-meta.js.map +1 -1
  263. package/dist/src/storage/interface.d.ts +10 -2
  264. package/dist/src/storage/interface.d.ts.map +1 -1
  265. package/dist/src/storage/interface.js.map +1 -1
  266. package/dist/src/storage/providers/mongodb/index.d.ts +1 -0
  267. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  268. package/dist/src/storage/providers/mongodb/index.js +29 -2
  269. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  270. package/dist/src/storage/providers/sqlite/index.d.ts +1 -0
  271. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  272. package/dist/src/storage/providers/sqlite/index.js +23 -8
  273. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  274. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  275. package/dist/src/storage/providers/sqlite/schema.js +9 -0
  276. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  277. package/dist/src/storage/repositories/identity.repository.d.ts +2 -0
  278. package/dist/src/storage/repositories/identity.repository.d.ts.map +1 -1
  279. package/dist/src/utils/env-config.d.ts.map +1 -1
  280. package/dist/src/utils/env-config.js +23 -0
  281. package/dist/src/utils/env-config.js.map +1 -1
  282. package/dist/src/utils/env-validator.d.ts.map +1 -1
  283. package/dist/src/utils/env-validator.js +8 -0
  284. package/dist/src/utils/env-validator.js.map +1 -1
  285. package/dist/src/utils/node-descriptor.d.ts.map +1 -1
  286. package/dist/src/utils/node-descriptor.js +1 -0
  287. package/dist/src/utils/node-descriptor.js.map +1 -1
  288. package/dist/static/app-catalog.html +8 -2
  289. package/dist/static/icons/badge-96.png +0 -0
  290. package/dist/static/icons/icon-192.png +0 -0
  291. package/dist/static/icons/icon-512.png +0 -0
  292. package/dist/static/manifest.json +19 -17
  293. package/dist/static/sw.js +13 -11
  294. package/package.json +3 -1
  295. package/prisma/schema.postgres.prisma +2 -0
  296. 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 — currently Google.
4
- * A generic, config-gated sign-in path for HUMAN owners that reuses the same OIDC
5
- * relying-party client as FTN verification. On a successful Google login the user is
6
- * mapped to a GHII: a returning Google user (matched by provider subject) or a verified
7
- * email match link straight to their existing account + session. A BRAND-NEW user is NOT
8
- * auto-created — instead they go through a one-time username-choice step: the callback
9
- * stashes the verified Google identity in a short-lived signed cookie and bounces back to
10
- * the SPA, which prompts the user to confirm or change the suggested username (warning
11
- * that it is permanent) before POSTing to finalize, which creates the owner+GHII and
12
- * establishes the session. This avoids silently minting a username from the email
13
- * local-part (which leaks the address) and gives the user their one chance to pick it.
14
- * @structure oauthLoginRouter(config, storage, googleClient): GET /v1/ghii/login/google
15
- * (authorize → redirect to Google) + GET /v1/ghii/login/google/callback (exchange → map
16
- * returning/linked OR stash pending + redirect) + GET /v1/ghii/login/pending (read the
17
- * pending cookie) + POST /v1/ghii/login/google/finalize (create with chosen username +
18
- * session) + GET /v1/ghii/username-available (live validation). Helpers:
19
- * deriveUniqueUsername(), normalizeUsername(), createOwnerForGoogle(), pending-token
20
- * sign/verify + cookie read/set/clear.
21
- * @usage app.use(oauthLoginRouter(config, storage, googleOidcClient)) from routes-loader.
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 Google signup. */
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/google/finalize. */
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 = 'google_signup';
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 Google identity into a short-lived EdDSA token (node key). */
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
- googleSub: data.googleSub,
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.googleSub !== 'string')
90
+ if (payload.purpose !== PENDING_PURPOSE || typeof payload.providerSub !== 'string' || typeof payload.provider !== 'string')
85
91
  return null;
86
92
  return {
87
- googleSub: payload.googleSub,
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 Google→callback redirect). */
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 a Google account, with the welcome bonus.
163
- * Shared by the finalize route (the only path that mints a brand-new Google account).
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 createOwnerForGoogle(storage, config, opts) {
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
- googleSub: sub,
191
- // Google asserts the email; record it as a verified email (level 1).
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, googleClient) {
235
+ export function oauthLoginRouter(config, storage, providers) {
216
236
  const router = Router();
217
- const googleAvailable = () => config.googleOAuthEnabled && !!googleClient?.initialized;
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
- // GET /v1/ghii/login/google — Begin Google sign-in (redirect to Google's consent screen)
225
- router.get('/v1/ghii/login/google', async (req, res) => {
226
- try {
227
- if (!googleAvailable()) {
228
- res.status(503).json(error(config.nodeId, 'FEATURE_DISABLED', 'Google sign-in is not available on this node'));
229
- return;
230
- }
231
- const authRequest = googleClient.createAuthRequest();
232
- const nonceTtl = config.nonceTtlSeconds * 1000;
233
- await storage.createVerificationNonce({
234
- id: randomUUID(),
235
- owner: '', // login flow — no authenticated owner yet
236
- type: 'google_login',
237
- state: authRequest.state,
238
- nonce: authRequest.nonce,
239
- redirectUri: safeRedirectPath(req.query.redirect), // where to send the user after login
240
- createdAt: new Date().toISOString(),
241
- expiresAt: new Date(Date.now() + nonceTtl).toISOString(),
242
- });
243
- res.redirect(authRequest.authorizationUrl);
244
- }
245
- catch (err) {
246
- logger.error('Google sign-in authorize failed', { error: String(err) });
247
- res.redirect(`${config.baseUrl}/?auth_error=GOOGLE_START_FAILED`);
248
- }
249
- });
250
- // GET /v1/ghii/login/google/callback — Google redirects back here with code + state
251
- router.get('/v1/ghii/login/google/callback', async (req, res) => {
252
- const fail = (code) => res.redirect(`${config.baseUrl}/?auth_error=${encodeURIComponent(code)}`);
253
- try {
254
- if (!googleAvailable()) {
255
- res.status(503).json(error(config.nodeId, 'FEATURE_DISABLED', 'Google sign-in is not available on this node'));
256
- return;
257
- }
258
- const code = req.query.code;
259
- const state = req.query.state;
260
- if (!code || !state) {
261
- fail('GOOGLE_MISSING_CODE');
262
- return;
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
- const nonceRecord = await storage.getVerificationNonce(state);
265
- if (!nonceRecord || nonceRecord.type !== 'google_login') {
266
- fail('GOOGLE_INVALID_STATE');
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
- if (new Date(nonceRecord.expiresAt) < new Date()) {
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
- fail('GOOGLE_STATE_EXPIRED');
272
- return;
273
- }
274
- const tokenResult = await googleClient.exchangeCode(code, state, nonceRecord.nonce);
275
- const postLoginPath = safeRedirectPath(nonceRecord.redirectUri);
276
- await storage.deleteVerificationNonce(state);
277
- if (!tokenResult.valid || !tokenResult.claims) {
278
- fail('GOOGLE_EXCHANGE_FAILED');
279
- return;
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
- const claims = tokenResult.claims;
282
- const sub = typeof claims.sub === 'string' ? claims.sub : undefined;
283
- const email = typeof claims.email === 'string' ? claims.email : undefined;
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
- const now = new Date().toISOString();
294
- // ── Map the Google identity to a GHII ──
295
- // 1) Returning Google user — matched by stable provider subject.
296
- let ghiiRecord = await storage.getGHIIByGoogleSub(sub);
297
- // 2) Link to an existing account ONLY when Google's email is verified AND it matches
298
- // a GHII whose email was already locally verified. Requiring the local side to be
299
- // verified too prevents takeover of an account that merely *claimed* (never proved)
300
- // this email at registration.
301
- if (!ghiiRecord && emailVerified && email) {
302
- const byEmail = await storage.getGHIIByEmailHash(emailHashOf(email));
303
- if (byEmail && byEmail.emailVerifiedAt) {
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
- // 3) No match — a brand-new user. Do NOT silently create an account with an
308
- // email-derived username (that leaks the address and locks in a name the user
309
- // never chose). Instead stash the verified Google identity in a short-lived signed
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
- // ── Establish an owner session (httpOnly refresh cookie + access token) ──
329
- const ownerName = ghiiRecord.ownerName;
330
- const ownerRecord = await storage.getOwner(ownerName);
331
- const roles = [];
332
- if (ownerRecord?.roles.includes('owner'))
333
- roles.push('owner');
334
- if (ownerRecord?.roles.includes('operator'))
335
- roles.push('operator');
336
- if (roles.length === 0)
337
- roles.push('owner');
338
- await storage.updateGHII(ghiiRecord.ghii, {
339
- lastLoginAt: now,
340
- loginCount: (ghiiRecord.loginCount ?? 0) + 1,
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 Google signup (suggested username + which
353
- // Google email it is for) so the SPA can render the one-time username-choice step. Reads the
354
- // signed httpOnly cookie set by the callback; returns NO_PENDING_SIGNUP when absent/expired.
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 Google sign-up'));
456
+ res.status(404).json(error(config.nodeId, 'NO_PENDING_SIGNUP', 'No pending sign-up'));
360
457
  return;
361
458
  }
362
- // The googleSub stays server-side; the client only needs what it must show + the redirect.
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