@agent-native/core 0.127.3 → 0.128.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (457) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +101 -0
  3. package/corpus/core/README.md +5 -26
  4. package/corpus/core/docs/content/actions.mdx +47 -4
  5. package/corpus/core/docs/content/native-chat-ui.mdx +7 -0
  6. package/corpus/core/docs/content/organizations-teams-permissions.mdx +109 -3
  7. package/corpus/core/docs/content/template-design-collaboration-and-full-apps.mdx +15 -0
  8. package/corpus/core/docs/content/toolkit-org-team.mdx +72 -12
  9. package/corpus/core/package.json +2 -1
  10. package/corpus/core/src/a2a/artifact-response.ts +36 -29
  11. package/corpus/core/src/action.ts +83 -2
  12. package/corpus/core/src/chat-threads/store.ts +48 -43
  13. package/corpus/core/src/cli/skills-content/design-visual-edit-skill.ts +46 -3
  14. package/corpus/core/src/client/AgentPanel.tsx +26 -6
  15. package/corpus/core/src/client/AssistantChat.tsx +63 -7
  16. package/corpus/core/src/client/DefaultSpinner.tsx +29 -0
  17. package/corpus/core/src/client/org/TeamPage.tsx +161 -6
  18. package/corpus/core/src/client/org/hooks.ts +67 -0
  19. package/corpus/core/src/client/org/index.ts +9 -0
  20. package/corpus/core/src/client/transcription/use-live-transcription.ts +62 -10
  21. package/corpus/core/src/db/client.ts +42 -3
  22. package/corpus/core/src/db/ensure-additive-columns.ts +60 -33
  23. package/corpus/core/src/deploy/build.ts +31 -2
  24. package/corpus/core/src/mcp-client/routes.ts +10 -1
  25. package/corpus/core/src/org/app-roles-handlers.ts +152 -0
  26. package/corpus/core/src/org/app-roles.ts +355 -0
  27. package/corpus/core/src/org/index.ts +27 -1
  28. package/corpus/core/src/org/migrations.ts +22 -0
  29. package/corpus/core/src/org/plugin.ts +27 -0
  30. package/corpus/core/src/org/schema.ts +15 -0
  31. package/corpus/core/src/scripts/docs/search.ts +17 -0
  32. package/corpus/core/src/scripts/docs/source-search.ts +10 -0
  33. package/corpus/core/src/server/agent-chat/script-entries.ts +3 -0
  34. package/corpus/core/src/server/agent-chat-plugin.ts +53 -52
  35. package/corpus/core/src/server/builder-design-systems.ts +299 -82
  36. package/corpus/core/src/server/core-routes-plugin.ts +6 -1
  37. package/corpus/core/src/server/framework-request-handler.ts +44 -6
  38. package/corpus/core/src/server/index.ts +8 -0
  39. package/corpus/core/src/templates/default/.agents/skills/actions/SKILL.md +32 -0
  40. package/corpus/core/src/templates/default/.agents/skills/adding-a-feature/SKILL.md +7 -0
  41. package/corpus/core/src/templates/default/.agents/skills/security/SKILL.md +31 -0
  42. package/corpus/core/src/templates/headless/.agents/skills/actions/SKILL.md +32 -0
  43. package/corpus/core/src/templates/workspace-core/.agents/skills/actions/SKILL.md +32 -0
  44. package/corpus/core/src/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +7 -0
  45. package/corpus/core/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +40 -0
  46. package/corpus/core/src/templates/workspace-core/.agents/skills/security/SKILL.md +31 -0
  47. package/corpus/core/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +4 -0
  48. package/corpus/templates/analytics/.agents/skills/actions/SKILL.md +32 -0
  49. package/corpus/templates/analytics/.agents/skills/security/SKILL.md +31 -0
  50. package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +4 -0
  51. package/corpus/templates/analytics/_gitignore +2 -0
  52. package/corpus/templates/analytics/actions/data-source-status.ts +21 -12
  53. package/corpus/templates/analytics/app/global.css +7 -7
  54. package/corpus/templates/analytics/app/i18n-data.ts +30 -0
  55. package/corpus/templates/analytics/app/lib/data-source-status.ts +2 -1
  56. package/corpus/templates/analytics/app/pages/DataSources.tsx +54 -10
  57. package/corpus/templates/analytics/changelog/2026-07-28-analytics-pages-load-reliably-with-large-dashboard-workspaces.md +6 -0
  58. package/corpus/templates/analytics/changelog/2026-07-28-dashboard-loading-shimmer-now-flows-smoothly-in-one-directi.md +6 -0
  59. package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +5 -4
  60. package/corpus/templates/analytics/server/plugins/agent-chat.ts +54 -42
  61. package/corpus/templates/analytics/server/plugins/db.ts +1 -18
  62. package/corpus/templates/assets/.agents/skills/actions/SKILL.md +32 -0
  63. package/corpus/templates/assets/.agents/skills/security/SKILL.md +31 -0
  64. package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +4 -0
  65. package/corpus/templates/assets/_gitignore +2 -0
  66. package/corpus/templates/brain/.agents/skills/actions/SKILL.md +32 -0
  67. package/corpus/templates/brain/.agents/skills/adding-a-feature/SKILL.md +7 -0
  68. package/corpus/templates/brain/.agents/skills/security/SKILL.md +31 -0
  69. package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +4 -0
  70. package/corpus/templates/calendar/.agents/skills/actions/SKILL.md +32 -0
  71. package/corpus/templates/calendar/.agents/skills/security/SKILL.md +31 -0
  72. package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +4 -0
  73. package/corpus/templates/calendar/_gitignore +2 -0
  74. package/corpus/templates/chat/.agents/skills/actions/SKILL.md +32 -0
  75. package/corpus/templates/chat/.agents/skills/adding-a-feature/SKILL.md +7 -0
  76. package/corpus/templates/chat/.agents/skills/security/SKILL.md +31 -0
  77. package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +4 -0
  78. package/corpus/templates/clips/.agents/skills/actions/SKILL.md +32 -0
  79. package/corpus/templates/clips/.agents/skills/security/SKILL.md +31 -0
  80. package/corpus/templates/clips/_gitignore +2 -0
  81. package/corpus/templates/clips/actions/request-transcript.ts +77 -11
  82. package/corpus/templates/clips/actions/save-browser-transcript.ts +28 -7
  83. package/corpus/templates/clips/app/routes/record.tsx +11 -3
  84. package/corpus/templates/clips/changelog/2026-07-28-browser-recordings-no-longer-save-a-truncated-transcript-as-.md +6 -0
  85. package/corpus/templates/clips/changelog/2026-07-28-browser-recordings-no-longer-stop-transcribing-partway-throu.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-07-28-desktop-full-screen-clips-now-get-an-instant-local-transcrip.md +6 -0
  87. package/corpus/templates/clips/changelog/2026-07-28-long-transcriptions-no-longer-show-a-false-transcription-sto.md +6 -0
  88. package/corpus/templates/clips/changelog/2026-07-28-restarting-for-an-update-now-installs-the-newest-release-ins.md +6 -0
  89. package/corpus/templates/clips/changelog/2026-07-28-the-meeting-recording-pill-now-anchors-to-the-right-edge-of-.md +6 -0
  90. package/corpus/templates/clips/desktop/src/lib/audio-meter.ts +31 -2
  91. package/corpus/templates/clips/desktop/src/lib/recorder.ts +114 -13
  92. package/corpus/templates/clips/desktop/src/lib/updater.ts +12 -4
  93. package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +40 -15
  94. package/corpus/templates/clips/desktop/src/styles.css +10 -7
  95. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +21 -14
  96. package/corpus/templates/clips/shared/transcript-status.ts +10 -0
  97. package/corpus/templates/content/.agents/skills/actions/SKILL.md +32 -0
  98. package/corpus/templates/content/.agents/skills/security/SKILL.md +31 -0
  99. package/corpus/templates/content/.agents/skills/sharing/SKILL.md +4 -0
  100. package/corpus/templates/content/_gitignore +2 -0
  101. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +4 -0
  102. package/corpus/templates/design/.agents/skills/actions/SKILL.md +32 -0
  103. package/corpus/templates/design/.agents/skills/security/SKILL.md +31 -0
  104. package/corpus/templates/design/.agents/skills/sharing/SKILL.md +4 -0
  105. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +46 -3
  106. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +7 -5
  107. package/corpus/templates/design/_gitignore +2 -0
  108. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
  109. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +7 -4
  110. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +1 -1
  111. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +10 -4
  112. package/corpus/templates/design/app/i18n-data.ts +10 -0
  113. package/corpus/templates/design/app/lib/builder-design-system-upload.ts +280 -0
  114. package/corpus/templates/design/app/lib/design-ui-events.ts +5 -0
  115. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +90 -54
  116. package/corpus/templates/design/app/root.tsx +18 -1
  117. package/corpus/templates/design/changelog/2026-07-28-figma-fig-uploads-now-show-indexing-progress-and-an-open-in-.md +6 -0
  118. package/corpus/templates/design/changelog/2026-07-28-show-or-hide-design-editing-chrome-with-cmd-on-apple-devices.md +6 -0
  119. package/corpus/templates/design/server/handlers/design-system-decode-job-status.ts +49 -0
  120. package/corpus/templates/design/server/handlers/design-system-upload-start.ts +87 -0
  121. package/corpus/templates/design/server/handlers/index-design-system-sources.ts +82 -0
  122. package/corpus/templates/design/server/routes/api/design-system-decode-job-status.get.ts +1 -0
  123. package/corpus/templates/design/server/routes/api/design-system-upload-start.post.ts +1 -0
  124. package/corpus/templates/design/server/routes/api/index-design-system-sources.post.ts +1 -0
  125. package/corpus/templates/dispatch/.agents/skills/actions/SKILL.md +32 -0
  126. package/corpus/templates/dispatch/.agents/skills/security/SKILL.md +31 -0
  127. package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +4 -0
  128. package/corpus/templates/dispatch/_gitignore +2 -0
  129. package/corpus/templates/forms/.agents/skills/actions/SKILL.md +32 -0
  130. package/corpus/templates/forms/.agents/skills/security/SKILL.md +31 -0
  131. package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +4 -0
  132. package/corpus/templates/forms/_gitignore +2 -0
  133. package/corpus/templates/macros/.agents/skills/security/SKILL.md +31 -0
  134. package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +4 -0
  135. package/corpus/templates/macros/_gitignore +2 -0
  136. package/corpus/templates/mail/.agents/skills/actions/SKILL.md +32 -0
  137. package/corpus/templates/mail/.agents/skills/security/SKILL.md +31 -0
  138. package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +4 -0
  139. package/corpus/templates/mail/_gitignore +2 -0
  140. package/corpus/templates/plan/.agents/skills/actions/SKILL.md +32 -0
  141. package/corpus/templates/plan/.agents/skills/adding-a-feature/SKILL.md +7 -0
  142. package/corpus/templates/plan/.agents/skills/security/SKILL.md +31 -0
  143. package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +4 -0
  144. package/corpus/templates/plan/_gitignore +2 -0
  145. package/corpus/templates/slides/.agents/skills/actions/SKILL.md +32 -0
  146. package/corpus/templates/slides/.agents/skills/security/SKILL.md +31 -0
  147. package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +4 -0
  148. package/corpus/templates/slides/_gitignore +2 -0
  149. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +126 -53
  150. package/corpus/templates/slides/app/components/design-system/builder-design-system-upload.ts +271 -0
  151. package/corpus/templates/slides/app/components/design-system/builder-index-response.ts +1 -1
  152. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  153. package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +49 -0
  154. package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +87 -0
  155. package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +79 -0
  156. package/corpus/templates/slides/server/routes/api/design-system-decode-job-status.get.ts +1 -0
  157. package/corpus/templates/slides/server/routes/api/design-system-upload-start.post.ts +1 -0
  158. package/corpus/templates/slides/server/routes/api/index-design-system-sources.post.ts +1 -0
  159. package/corpus/templates/tasks/.agents/skills/actions/SKILL.md +32 -0
  160. package/corpus/templates/tasks/.agents/skills/adding-a-feature/SKILL.md +7 -0
  161. package/corpus/templates/tasks/.agents/skills/security/SKILL.md +31 -0
  162. package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +4 -0
  163. package/corpus/templates/tasks/_gitignore +2 -0
  164. package/dist/a2a/artifact-response.d.ts.map +1 -1
  165. package/dist/a2a/artifact-response.js +30 -29
  166. package/dist/a2a/artifact-response.js.map +1 -1
  167. package/dist/action.d.ts +36 -0
  168. package/dist/action.d.ts.map +1 -1
  169. package/dist/action.js +37 -2
  170. package/dist/action.js.map +1 -1
  171. package/dist/chat-threads/store.d.ts +12 -0
  172. package/dist/chat-threads/store.d.ts.map +1 -1
  173. package/dist/chat-threads/store.js +48 -42
  174. package/dist/chat-threads/store.js.map +1 -1
  175. package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
  176. package/dist/cli/skills-content/design-visual-edit-skill.d.ts.map +1 -1
  177. package/dist/cli/skills-content/design-visual-edit-skill.js +46 -3
  178. package/dist/cli/skills-content/design-visual-edit-skill.js.map +1 -1
  179. package/dist/client/AgentPanel.d.ts +3 -1
  180. package/dist/client/AgentPanel.d.ts.map +1 -1
  181. package/dist/client/AgentPanel.js +11 -9
  182. package/dist/client/AgentPanel.js.map +1 -1
  183. package/dist/client/AssistantChat.d.ts.map +1 -1
  184. package/dist/client/AssistantChat.js +57 -6
  185. package/dist/client/AssistantChat.js.map +1 -1
  186. package/dist/client/DefaultSpinner.d.ts +7 -0
  187. package/dist/client/DefaultSpinner.d.ts.map +1 -1
  188. package/dist/client/DefaultSpinner.js +25 -1
  189. package/dist/client/DefaultSpinner.js.map +1 -1
  190. package/dist/client/org/TeamPage.d.ts +13 -1
  191. package/dist/client/org/TeamPage.d.ts.map +1 -1
  192. package/dist/client/org/TeamPage.js +41 -11
  193. package/dist/client/org/TeamPage.js.map +1 -1
  194. package/dist/client/org/hooks.d.ts +37 -0
  195. package/dist/client/org/hooks.d.ts.map +1 -1
  196. package/dist/client/org/hooks.js +37 -0
  197. package/dist/client/org/hooks.js.map +1 -1
  198. package/dist/client/org/index.d.ts +3 -2
  199. package/dist/client/org/index.d.ts.map +1 -1
  200. package/dist/client/org/index.js +1 -1
  201. package/dist/client/org/index.js.map +1 -1
  202. package/dist/client/transcription/use-live-transcription.d.ts +8 -0
  203. package/dist/client/transcription/use-live-transcription.d.ts.map +1 -1
  204. package/dist/client/transcription/use-live-transcription.js +38 -11
  205. package/dist/client/transcription/use-live-transcription.js.map +1 -1
  206. package/dist/collab/routes.d.ts +1 -1
  207. package/dist/db/client.d.ts +12 -0
  208. package/dist/db/client.d.ts.map +1 -1
  209. package/dist/db/client.js +40 -3
  210. package/dist/db/client.js.map +1 -1
  211. package/dist/db/ensure-additive-columns.d.ts.map +1 -1
  212. package/dist/db/ensure-additive-columns.js +54 -32
  213. package/dist/db/ensure-additive-columns.js.map +1 -1
  214. package/dist/deploy/build.d.ts +1 -0
  215. package/dist/deploy/build.d.ts.map +1 -1
  216. package/dist/deploy/build.js +23 -2
  217. package/dist/deploy/build.js.map +1 -1
  218. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  219. package/dist/mcp-client/routes.d.ts +3 -1
  220. package/dist/mcp-client/routes.d.ts.map +1 -1
  221. package/dist/mcp-client/routes.js +8 -2
  222. package/dist/mcp-client/routes.js.map +1 -1
  223. package/dist/notifications/routes.d.ts +1 -1
  224. package/dist/org/app-roles-handlers.d.ts +27 -0
  225. package/dist/org/app-roles-handlers.d.ts.map +1 -0
  226. package/dist/org/app-roles-handlers.js +125 -0
  227. package/dist/org/app-roles-handlers.js.map +1 -0
  228. package/dist/org/app-roles.d.ts +136 -0
  229. package/dist/org/app-roles.d.ts.map +1 -0
  230. package/dist/org/app-roles.js +216 -0
  231. package/dist/org/app-roles.js.map +1 -0
  232. package/dist/org/index.d.ts +4 -1
  233. package/dist/org/index.d.ts.map +1 -1
  234. package/dist/org/index.js +3 -1
  235. package/dist/org/index.js.map +1 -1
  236. package/dist/org/migrations.d.ts.map +1 -1
  237. package/dist/org/migrations.js +22 -0
  238. package/dist/org/migrations.js.map +1 -1
  239. package/dist/org/plugin.d.ts +2 -0
  240. package/dist/org/plugin.d.ts.map +1 -1
  241. package/dist/org/plugin.js +20 -0
  242. package/dist/org/plugin.js.map +1 -1
  243. package/dist/org/schema.d.ts +143 -0
  244. package/dist/org/schema.d.ts.map +1 -1
  245. package/dist/org/schema.js +14 -0
  246. package/dist/org/schema.js.map +1 -1
  247. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  248. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  249. package/dist/scripts/docs/search.d.ts.map +1 -1
  250. package/dist/scripts/docs/search.js +15 -0
  251. package/dist/scripts/docs/search.js.map +1 -1
  252. package/dist/scripts/docs/source-search.d.ts +7 -0
  253. package/dist/scripts/docs/source-search.d.ts.map +1 -1
  254. package/dist/scripts/docs/source-search.js +9 -0
  255. package/dist/scripts/docs/source-search.js.map +1 -1
  256. package/dist/secrets/routes.d.ts +9 -9
  257. package/dist/server/agent-chat/script-entries.d.ts.map +1 -1
  258. package/dist/server/agent-chat/script-entries.js +3 -0
  259. package/dist/server/agent-chat/script-entries.js.map +1 -1
  260. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  261. package/dist/server/agent-chat-plugin.js +46 -48
  262. package/dist/server/agent-chat-plugin.js.map +1 -1
  263. package/dist/server/builder-design-systems.d.ts +49 -0
  264. package/dist/server/builder-design-systems.d.ts.map +1 -1
  265. package/dist/server/builder-design-systems.js +221 -69
  266. package/dist/server/builder-design-systems.js.map +1 -1
  267. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  268. package/dist/server/core-routes-plugin.js +6 -1
  269. package/dist/server/core-routes-plugin.js.map +1 -1
  270. package/dist/server/framework-request-handler.d.ts.map +1 -1
  271. package/dist/server/framework-request-handler.js +42 -6
  272. package/dist/server/framework-request-handler.js.map +1 -1
  273. package/dist/server/index.d.ts +1 -1
  274. package/dist/server/index.d.ts.map +1 -1
  275. package/dist/server/index.js +1 -1
  276. package/dist/server/index.js.map +1 -1
  277. package/dist/server/realtime-token.d.ts +1 -1
  278. package/dist/templates/chat/.agents/skills/actions/SKILL.md +32 -0
  279. package/dist/templates/chat/.agents/skills/adding-a-feature/SKILL.md +7 -0
  280. package/dist/templates/chat/.agents/skills/security/SKILL.md +31 -0
  281. package/dist/templates/chat/.agents/skills/sharing/SKILL.md +4 -0
  282. package/dist/templates/default/.agents/skills/actions/SKILL.md +32 -0
  283. package/dist/templates/default/.agents/skills/adding-a-feature/SKILL.md +7 -0
  284. package/dist/templates/default/.agents/skills/security/SKILL.md +31 -0
  285. package/dist/templates/headless/.agents/skills/actions/SKILL.md +32 -0
  286. package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +32 -0
  287. package/dist/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +7 -0
  288. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +40 -0
  289. package/dist/templates/workspace-core/.agents/skills/security/SKILL.md +31 -0
  290. package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +4 -0
  291. package/docs/content/actions.mdx +47 -4
  292. package/docs/content/native-chat-ui.mdx +7 -0
  293. package/docs/content/organizations-teams-permissions.mdx +109 -3
  294. package/docs/content/template-design-collaboration-and-full-apps.mdx +15 -0
  295. package/docs/content/toolkit-org-team.mdx +72 -12
  296. package/package.json +2 -1
  297. package/src/a2a/artifact-response.ts +36 -29
  298. package/src/action.ts +83 -2
  299. package/src/chat-threads/store.ts +48 -43
  300. package/src/cli/skills-content/design-visual-edit-skill.ts +46 -3
  301. package/src/client/AgentPanel.tsx +26 -6
  302. package/src/client/AssistantChat.tsx +63 -7
  303. package/src/client/DefaultSpinner.tsx +29 -0
  304. package/src/client/org/TeamPage.tsx +161 -6
  305. package/src/client/org/hooks.ts +67 -0
  306. package/src/client/org/index.ts +9 -0
  307. package/src/client/transcription/use-live-transcription.ts +62 -10
  308. package/src/db/client.ts +42 -3
  309. package/src/db/ensure-additive-columns.ts +60 -33
  310. package/src/deploy/build.ts +31 -2
  311. package/src/mcp-client/routes.ts +10 -1
  312. package/src/org/app-roles-handlers.ts +152 -0
  313. package/src/org/app-roles.ts +355 -0
  314. package/src/org/index.ts +27 -1
  315. package/src/org/migrations.ts +22 -0
  316. package/src/org/plugin.ts +27 -0
  317. package/src/org/schema.ts +15 -0
  318. package/src/scripts/docs/search.ts +17 -0
  319. package/src/scripts/docs/source-search.ts +10 -0
  320. package/src/server/agent-chat/script-entries.ts +3 -0
  321. package/src/server/agent-chat-plugin.ts +53 -52
  322. package/src/server/builder-design-systems.ts +299 -82
  323. package/src/server/core-routes-plugin.ts +6 -1
  324. package/src/server/framework-request-handler.ts +44 -6
  325. package/src/server/index.ts +8 -0
  326. package/src/templates/chat/.agents/skills/actions/SKILL.md +32 -0
  327. package/src/templates/chat/.agents/skills/adding-a-feature/SKILL.md +7 -0
  328. package/src/templates/chat/.agents/skills/security/SKILL.md +31 -0
  329. package/src/templates/chat/.agents/skills/sharing/SKILL.md +4 -0
  330. package/src/templates/default/.agents/skills/actions/SKILL.md +32 -0
  331. package/src/templates/default/.agents/skills/adding-a-feature/SKILL.md +7 -0
  332. package/src/templates/default/.agents/skills/security/SKILL.md +31 -0
  333. package/src/templates/headless/.agents/skills/actions/SKILL.md +32 -0
  334. package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +32 -0
  335. package/src/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +7 -0
  336. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +40 -0
  337. package/src/templates/workspace-core/.agents/skills/security/SKILL.md +31 -0
  338. package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +4 -0
  339. package/corpus/core/src/templates/chat/.agents/skills/actions/SKILL.md +0 -530
  340. package/corpus/core/src/templates/chat/.agents/skills/adding-a-feature/SKILL.md +0 -191
  341. package/corpus/core/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +0 -115
  342. package/corpus/core/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +0 -177
  343. package/corpus/core/src/templates/chat/.agents/skills/capture-learnings/SKILL.md +0 -89
  344. package/corpus/core/src/templates/chat/.agents/skills/create-skill/SKILL.md +0 -221
  345. package/corpus/core/src/templates/chat/.agents/skills/customizing-agent-native/SKILL.md +0 -220
  346. package/corpus/core/src/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +0 -263
  347. package/corpus/core/src/templates/chat/.agents/skills/feature-flags/SKILL.md +0 -169
  348. package/corpus/core/src/templates/chat/.agents/skills/frontend-design/SKILL.md +0 -174
  349. package/corpus/core/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +0 -232
  350. package/corpus/core/src/templates/chat/.agents/skills/security/SKILL.md +0 -282
  351. package/corpus/core/src/templates/chat/.agents/skills/self-modifying-code/SKILL.md +0 -119
  352. package/corpus/core/src/templates/chat/.agents/skills/shadcn-ui/SKILL.md +0 -123
  353. package/corpus/core/src/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +0 -122
  354. package/corpus/core/src/templates/chat/.env.example +0 -5
  355. package/corpus/core/src/templates/chat/.ignore +0 -0
  356. package/corpus/core/src/templates/chat/.oxfmtrc.json +0 -8
  357. package/corpus/core/src/templates/chat/AGENTS.md +0 -77
  358. package/corpus/core/src/templates/chat/CHANGELOG.md +0 -10
  359. package/corpus/core/src/templates/chat/DEVELOPING.md +0 -185
  360. package/corpus/core/src/templates/chat/README.md +0 -32
  361. package/corpus/core/src/templates/chat/_gitignore +0 -46
  362. package/corpus/core/src/templates/chat/actions/hello.ts +0 -13
  363. package/corpus/core/src/templates/chat/actions/navigate.ts +0 -41
  364. package/corpus/core/src/templates/chat/actions/run.ts +0 -2
  365. package/corpus/core/src/templates/chat/actions/view-screen.ts +0 -31
  366. package/corpus/core/src/templates/chat/app/components/layout/Header.tsx +0 -60
  367. package/corpus/core/src/templates/chat/app/components/layout/Layout.tsx +0 -188
  368. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +0 -505
  369. package/corpus/core/src/templates/chat/app/components/ui/button.tsx +0 -1
  370. package/corpus/core/src/templates/chat/app/components/ui/card.tsx +0 -1
  371. package/corpus/core/src/templates/chat/app/components/ui/dropdown-menu.tsx +0 -1
  372. package/corpus/core/src/templates/chat/app/components/ui/input.tsx +0 -1
  373. package/corpus/core/src/templates/chat/app/components/ui/label.tsx +0 -1
  374. package/corpus/core/src/templates/chat/app/components/ui/sheet.tsx +0 -1
  375. package/corpus/core/src/templates/chat/app/components/ui/toolkit-provider.tsx +0 -17
  376. package/corpus/core/src/templates/chat/app/components/ui/tooltip.tsx +0 -1
  377. package/corpus/core/src/templates/chat/app/design-system.ts +0 -3
  378. package/corpus/core/src/templates/chat/app/entry.client.tsx +0 -22
  379. package/corpus/core/src/templates/chat/app/entry.server.tsx +0 -10
  380. package/corpus/core/src/templates/chat/app/global.css +0 -93
  381. package/corpus/core/src/templates/chat/app/hooks/use-navigation-state.ts +0 -93
  382. package/corpus/core/src/templates/chat/app/i18n/ar-SA.ts +0 -68
  383. package/corpus/core/src/templates/chat/app/i18n/de-DE.ts +0 -71
  384. package/corpus/core/src/templates/chat/app/i18n/en-US.ts +0 -70
  385. package/corpus/core/src/templates/chat/app/i18n/es-ES.ts +0 -71
  386. package/corpus/core/src/templates/chat/app/i18n/fr-FR.ts +0 -71
  387. package/corpus/core/src/templates/chat/app/i18n/hi-IN.ts +0 -69
  388. package/corpus/core/src/templates/chat/app/i18n/index.ts +0 -34
  389. package/corpus/core/src/templates/chat/app/i18n/ja-JP.ts +0 -70
  390. package/corpus/core/src/templates/chat/app/i18n/ko-KR.ts +0 -70
  391. package/corpus/core/src/templates/chat/app/i18n/pt-BR.ts +0 -71
  392. package/corpus/core/src/templates/chat/app/i18n/zh-CN.ts +0 -66
  393. package/corpus/core/src/templates/chat/app/i18n/zh-TW.ts +0 -66
  394. package/corpus/core/src/templates/chat/app/i18n-data.ts +0 -556
  395. package/corpus/core/src/templates/chat/app/lib/agent-page.tsx +0 -49
  396. package/corpus/core/src/templates/chat/app/lib/app-config.ts +0 -11
  397. package/corpus/core/src/templates/chat/app/lib/tab-id.ts +0 -1
  398. package/corpus/core/src/templates/chat/app/lib/utils.ts +0 -1
  399. package/corpus/core/src/templates/chat/app/root.tsx +0 -173
  400. package/corpus/core/src/templates/chat/app/routes/_index.tsx +0 -94
  401. package/corpus/core/src/templates/chat/app/routes/agent.tsx +0 -24
  402. package/corpus/core/src/templates/chat/app/routes/chat.$threadId.tsx +0 -1
  403. package/corpus/core/src/templates/chat/app/routes/database.tsx +0 -17
  404. package/corpus/core/src/templates/chat/app/routes/extensions.$id.$slug.tsx +0 -2
  405. package/corpus/core/src/templates/chat/app/routes/extensions.$id.tsx +0 -11
  406. package/corpus/core/src/templates/chat/app/routes/extensions._index.tsx +0 -11
  407. package/corpus/core/src/templates/chat/app/routes/extensions.tsx +0 -5
  408. package/corpus/core/src/templates/chat/app/routes/observability.tsx +0 -19
  409. package/corpus/core/src/templates/chat/app/routes/settings.tsx +0 -89
  410. package/corpus/core/src/templates/chat/app/routes/team.tsx +0 -11
  411. package/corpus/core/src/templates/chat/app/routes.ts +0 -4
  412. package/corpus/core/src/templates/chat/app/vite-env.d.ts +0 -6
  413. package/corpus/core/src/templates/chat/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +0 -6
  414. package/corpus/core/src/templates/chat/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +0 -6
  415. package/corpus/core/src/templates/chat/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +0 -6
  416. package/corpus/core/src/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +0 -6
  417. package/corpus/core/src/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +0 -6
  418. package/corpus/core/src/templates/chat/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +0 -5
  419. package/corpus/core/src/templates/chat/changelog/2026-07-10-chat-now-makes-ai-connection-setup-clear-without-shifting-th.md +0 -6
  420. package/corpus/core/src/templates/chat/changelog/2026-07-13-a-full-agent-page-now-brings-context-files-connections-jobs-.md +0 -6
  421. package/corpus/core/src/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +0 -6
  422. package/corpus/core/src/templates/chat/changelog/2026-07-14-fixed-chat-template-startup-with-older-core-versions.md +0 -6
  423. package/corpus/core/src/templates/chat/changelog/2026-07-15-chat-navigation-focuses-on-chat-and-agent.md +0 -6
  424. package/corpus/core/src/templates/chat/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +0 -6
  425. package/corpus/core/src/templates/chat/changelog/2026-07-22-compact-sidebar-footer.md +0 -6
  426. package/corpus/core/src/templates/chat/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +0 -6
  427. package/corpus/core/src/templates/chat/changelog/2026-07-22-manage-agent-navigation-now-uses-the-connected-nodes-icon.md +0 -6
  428. package/corpus/core/src/templates/chat/changelog/2026-07-22-recent-chats-are-easier-to-scan-and-expand.md +0 -6
  429. package/corpus/core/src/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +0 -6
  430. package/corpus/core/src/templates/chat/changelog/2026-07-24-borderless-secondary-surfaces.md +0 -6
  431. package/corpus/core/src/templates/chat/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +0 -6
  432. package/corpus/core/src/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +0 -6
  433. package/corpus/core/src/templates/chat/components.json +0 -20
  434. package/corpus/core/src/templates/chat/learnings.defaults.md +0 -5
  435. package/corpus/core/src/templates/chat/netlify.toml +0 -11
  436. package/corpus/core/src/templates/chat/package.json +0 -92
  437. package/corpus/core/src/templates/chat/public/agent-native-icon-dark.svg +0 -10
  438. package/corpus/core/src/templates/chat/public/agent-native-icon-light.svg +0 -10
  439. package/corpus/core/src/templates/chat/public/agent-native-logo-dark.svg +0 -21
  440. package/corpus/core/src/templates/chat/public/agent-native-logo-light.svg +0 -21
  441. package/corpus/core/src/templates/chat/public/favicon.svg +0 -1
  442. package/corpus/core/src/templates/chat/public/icon-180.svg +0 -1
  443. package/corpus/core/src/templates/chat/public/icon-192.svg +0 -1
  444. package/corpus/core/src/templates/chat/public/icon-512.svg +0 -1
  445. package/corpus/core/src/templates/chat/public/manifest.json +0 -21
  446. package/corpus/core/src/templates/chat/react-router.config.ts +0 -7
  447. package/corpus/core/src/templates/chat/server/middleware/auth.ts +0 -15
  448. package/corpus/core/src/templates/chat/server/plugins/agent-chat.ts +0 -21
  449. package/corpus/core/src/templates/chat/server/plugins/auth.ts +0 -17
  450. package/corpus/core/src/templates/chat/server/routes/[...page].get.ts +0 -5
  451. package/corpus/core/src/templates/chat/ssr-entry.ts +0 -15
  452. package/corpus/core/src/templates/chat/tsconfig.json +0 -21
  453. package/corpus/core/src/templates/chat/vite.config.ts +0 -19
  454. package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +0 -151
  455. package/corpus/templates/design/server/routes/api/index-design-system-with-builder.post.ts +0 -1
  456. package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +0 -119
  457. package/corpus/templates/slides/server/routes/api/index-design-system-with-builder.post.ts +0 -1
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Per-app member roles — an overlay on org membership, never a second roster.
3
+ *
4
+ * Three role systems now coexist and answer different questions:
5
+ * - org role (`org_members.role`) — what may this person do to the *team*?
6
+ * - app role (this module) — what may this person do inside *one app*?
7
+ * - share role (`sharing/`) — what may this person do to *one row*?
8
+ *
9
+ * They do not imply one another in either direction. In particular an app
10
+ * `admin` is not an org admin: nothing here is ever read by the org handlers.
11
+ */
12
+ import type { ActionRunContext } from "../action.js";
13
+ /**
14
+ * The serializable half of an app-role declaration. Kept free of database
15
+ * imports so the same object can be passed to `defineAppRoles` on the server
16
+ * and to `<TeamPage appRoles={...} />` in the browser bundle.
17
+ */
18
+ export interface AppRolesDescriptor<R extends string = string> {
19
+ /**
20
+ * Stable identifier for the app these roles belong to. Comes from app source,
21
+ * never from a request: the REST layer resolves a descriptor out of the
22
+ * registry below and 404s on anything unregistered, so a client cannot name
23
+ * an app the server did not declare.
24
+ */
25
+ appId: string;
26
+ /**
27
+ * The complete role vocabulary, as an unordered SET. Declaration order
28
+ * carries no meaning — `["ae", "se", "csm"]` are classifications, and ranking
29
+ * them would silently grant the last one everything the first one has.
30
+ * Authorization is always an explicit list of accepted roles.
31
+ */
32
+ roles: readonly R[];
33
+ /**
34
+ * What to show for an org member who has no assignment yet.
35
+ *
36
+ * DISPLAY ONLY — `requireAny` never matches it. If an unassigned member could
37
+ * satisfy a guard, "nobody has assigned this person" and "this person was
38
+ * granted the role" would be the same answer, and later widening the default
39
+ * would silently widen every guard that names it.
40
+ */
41
+ defaultRole?: R;
42
+ /** Optional human labels for the role picker. Falls back to the raw value. */
43
+ roleLabels?: Partial<Record<R, string>>;
44
+ /**
45
+ * Column header for the role in `<TeamPage appRoles={...} />`. Defaults to
46
+ * `appId`. Owned by the app rather than the framework's i18n catalogs — the
47
+ * vocabulary it labels is app-specific, so its translations are too.
48
+ */
49
+ label?: string;
50
+ }
51
+ /**
52
+ * Why a caller does or does not hold an app role. Each outcome is a distinct
53
+ * value on purpose: collapsing "not in this org" into "no role assigned" would
54
+ * report a membership problem as a permissions problem, and the operator would
55
+ * go looking in the wrong settings page.
56
+ *
57
+ * A failed lookup is NOT in this union — it throws. A database error is not an
58
+ * answer, and returning one of these statuses for it would deny loudly in
59
+ * exactly the same shape as a legitimate denial.
60
+ */
61
+ export type AppRoleLookup<R extends string = string> = {
62
+ status: "assigned";
63
+ role: R;
64
+ orgId: string;
65
+ } | {
66
+ status: "unassigned";
67
+ orgId: string;
68
+ } | {
69
+ status: "not-a-member";
70
+ orgId: string;
71
+ } | {
72
+ status: "no-identity";
73
+ } | {
74
+ status: "no-org";
75
+ };
76
+ export interface AppRoleCaller {
77
+ userEmail?: string | null;
78
+ orgId?: string | null;
79
+ }
80
+ export interface AppRoles<R extends string = string> {
81
+ descriptor: AppRolesDescriptor<R>;
82
+ appId: string;
83
+ roles: readonly R[];
84
+ /** Resolve without throwing on denial — for UI and for building richer guards. */
85
+ resolve: (caller?: AppRoleCaller) => Promise<AppRoleLookup<R>>;
86
+ /** Throw unless the caller holds one of `allowed`; returns the matched role. */
87
+ assertAny: (allowed: readonly R[], caller?: AppRoleCaller) => Promise<R>;
88
+ /** Guard for `defineAction({ authorize })`. Accepts an explicit assignment only. */
89
+ requireAny: (...allowed: R[]) => (args: unknown, ctx?: ActionRunContext) => Promise<void>;
90
+ }
91
+ /** Descriptor for a registered app id, or undefined. Used by the REST layer. */
92
+ export declare function getRegisteredAppRoles(appId: string): AppRolesDescriptor<string> | undefined;
93
+ export declare function listRegisteredAppRoles(): AppRolesDescriptor<string>[];
94
+ /**
95
+ * Declare an app's role vocabulary and get the server-side guards for it.
96
+ *
97
+ * ```ts
98
+ * export const coachAccess = defineAppRoles({
99
+ * appId: "coach",
100
+ * roles: ["member", "coach-admin"] as const,
101
+ * defaultRole: "member",
102
+ * });
103
+ * ```
104
+ */
105
+ export declare function defineAppRoles<const R extends string>(descriptor: AppRolesDescriptor<R>): AppRoles<R>;
106
+ /**
107
+ * Single read behind every app-role decision.
108
+ *
109
+ * Membership and assignment are resolved in ONE statement so an assignment left
110
+ * behind by a removed member can never authorize: the row only reaches the
111
+ * result set through `org_members`. Splitting this into two queries would also
112
+ * let a member removed between them still pass.
113
+ */
114
+ export declare function resolveAppRole<R extends string>(descriptor: AppRolesDescriptor<R>, caller?: AppRoleCaller): Promise<AppRoleLookup<R>>;
115
+ export interface AppMemberRoleRow {
116
+ email: string;
117
+ role: string;
118
+ }
119
+ /** Every assignment for current members of one app in one org. */
120
+ export declare function listAppMemberRoles(appId: string, orgId: string): Promise<AppMemberRoleRow[]>;
121
+ /**
122
+ * Assign or clear one member's app role. `role: null` deletes the assignment,
123
+ * which is distinct from assigning the descriptor's `defaultRole` — an unassigned
124
+ * member satisfies no guard, whereas a default is only ever a display value.
125
+ *
126
+ * Callers must have already established that the target is an org member and
127
+ * that the actor may manage the org; this function does not re-check either.
128
+ */
129
+ export declare function setAppMemberRole(opts: {
130
+ appId: string;
131
+ orgId: string;
132
+ email: string;
133
+ role: string | null;
134
+ updatedBy: string;
135
+ }): Promise<void>;
136
+ //# sourceMappingURL=app-roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-roles.d.ts","sourceRoot":"","sources":["../../src/org/app-roles.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Per-app member roles — an overlay on org membership, never a second roster.
3
+ *
4
+ * Three role systems now coexist and answer different questions:
5
+ * - org role (`org_members.role`) — what may this person do to the *team*?
6
+ * - app role (this module) — what may this person do inside *one app*?
7
+ * - share role (`sharing/`) — what may this person do to *one row*?
8
+ *
9
+ * They do not imply one another in either direction. In particular an app
10
+ * `admin` is not an org admin: nothing here is ever read by the org handlers.
11
+ */
12
+ import { getDbExec } from "../db/client.js";
13
+ import { getRequestOrgId, getRequestUserEmail, } from "../server/request-context.js";
14
+ import { ForbiddenError } from "../sharing/access.js";
15
+ const registry = new Map();
16
+ /** Descriptor for a registered app id, or undefined. Used by the REST layer. */
17
+ export function getRegisteredAppRoles(appId) {
18
+ return registry.get(appId);
19
+ }
20
+ export function listRegisteredAppRoles() {
21
+ return [...registry.values()];
22
+ }
23
+ const normalizeEmail = (email) => email.trim().toLowerCase();
24
+ /**
25
+ * `undefined` means "the caller did not say" and falls back to the ambient
26
+ * request context; an explicit `null` means "this run has no user/org" and must
27
+ * NOT. Collapsing the two lets a system or cron caller that deliberately
28
+ * declared no identity inherit whichever request happens to be on the stack,
29
+ * and authorize as that person.
30
+ */
31
+ function callerIdentity(caller) {
32
+ const email = caller?.userEmail !== undefined ? caller.userEmail : getRequestUserEmail();
33
+ const orgId = caller?.orgId !== undefined ? caller.orgId : getRequestOrgId();
34
+ return {
35
+ email: email && email.trim() ? email.trim() : null,
36
+ orgId: orgId && orgId.trim() ? orgId.trim() : null,
37
+ };
38
+ }
39
+ /**
40
+ * Declare an app's role vocabulary and get the server-side guards for it.
41
+ *
42
+ * ```ts
43
+ * export const coachAccess = defineAppRoles({
44
+ * appId: "coach",
45
+ * roles: ["member", "coach-admin"] as const,
46
+ * defaultRole: "member",
47
+ * });
48
+ * ```
49
+ */
50
+ export function defineAppRoles(descriptor) {
51
+ const appId = descriptor.appId.trim();
52
+ if (!appId)
53
+ throw new Error("defineAppRoles: appId is required");
54
+ if (!descriptor.roles.length) {
55
+ throw new Error(`defineAppRoles(${appId}): at least one role is required`);
56
+ }
57
+ if (descriptor.defaultRole &&
58
+ !descriptor.roles.includes(descriptor.defaultRole)) {
59
+ throw new Error(`defineAppRoles(${appId}): defaultRole "${descriptor.defaultRole}" is not in roles`);
60
+ }
61
+ const existing = registry.get(appId);
62
+ if (existing && existing !== descriptor) {
63
+ // Two vocabularies behind one appId means the REST layer and the guards can
64
+ // disagree about which roles exist, and the settings UI would offer roles
65
+ // no guard accepts.
66
+ throw new Error(`defineAppRoles: appId "${appId}" is already declared with a different descriptor`);
67
+ }
68
+ registry.set(appId, descriptor);
69
+ const resolve = (caller) => resolveAppRole(descriptor, caller);
70
+ const assertAny = async (allowed, caller) => {
71
+ const lookup = await resolve(caller);
72
+ if (lookup.status === "assigned" && allowed.includes(lookup.role)) {
73
+ return lookup.role;
74
+ }
75
+ throw new ForbiddenError(denialMessage(appId, allowed, lookup));
76
+ };
77
+ return {
78
+ descriptor,
79
+ appId,
80
+ roles: descriptor.roles,
81
+ resolve,
82
+ assertAny,
83
+ requireAny: (...allowed) => {
84
+ // An empty accepted set can never match. Failing closed at request time
85
+ // would be correct but mute — `authorize: coachAccess.requireAny()` is a
86
+ // dropped argument, and it should fail where the mistake is, not as an
87
+ // unexplained 403 in production.
88
+ if (!allowed.length) {
89
+ throw new Error(`defineAppRoles(${appId}): requireAny() needs at least one accepted role`);
90
+ }
91
+ const unknown = allowed.filter((r) => !descriptor.roles.includes(r));
92
+ if (unknown.length) {
93
+ throw new Error(`defineAppRoles(${appId}): requireAny() names undeclared role(s) ${unknown.join(", ")}`);
94
+ }
95
+ return async (_args, ctx) => {
96
+ await assertAny(allowed, {
97
+ userEmail: ctx?.userEmail,
98
+ orgId: ctx?.orgId,
99
+ });
100
+ };
101
+ },
102
+ };
103
+ }
104
+ function denialMessage(appId, allowed, lookup) {
105
+ const need = `Requires ${appId} role ${allowed.join(" or ")}`;
106
+ switch (lookup.status) {
107
+ case "assigned":
108
+ return `${need} (have ${lookup.role})`;
109
+ case "unassigned":
110
+ return `${need} (no ${appId} role assigned)`;
111
+ case "not-a-member":
112
+ return `${need} (not a member of this organization)`;
113
+ case "no-org":
114
+ return `${need} (no active organization)`;
115
+ case "no-identity":
116
+ return `${need} (no authenticated user)`;
117
+ }
118
+ }
119
+ /**
120
+ * Single read behind every app-role decision.
121
+ *
122
+ * Membership and assignment are resolved in ONE statement so an assignment left
123
+ * behind by a removed member can never authorize: the row only reaches the
124
+ * result set through `org_members`. Splitting this into two queries would also
125
+ * let a member removed between them still pass.
126
+ */
127
+ export async function resolveAppRole(descriptor, caller) {
128
+ const { email, orgId } = callerIdentity(caller);
129
+ if (!email)
130
+ return { status: "no-identity" };
131
+ if (!orgId)
132
+ return { status: "no-org" };
133
+ const { rows } = await getDbExec().execute({
134
+ sql: `SELECT r.role AS "appRole"
135
+ FROM org_members m
136
+ LEFT JOIN app_member_roles r
137
+ ON r.org_id = m.org_id
138
+ AND r.app_id = ?
139
+ AND LOWER(r.email) = LOWER(m.email)
140
+ WHERE m.org_id = ? AND LOWER(m.email) = ?
141
+ LIMIT 1`,
142
+ args: [descriptor.appId, orgId, normalizeEmail(email)],
143
+ });
144
+ const row = rows[0];
145
+ if (!row)
146
+ return { status: "not-a-member", orgId };
147
+ const raw = row.appRole ?? row.approle;
148
+ if (raw === null || raw === undefined)
149
+ return { status: "unassigned", orgId };
150
+ const role = String(raw);
151
+ // A stored value outside the declared vocabulary is a role that was removed
152
+ // from the app's declaration while assignments still referenced it. Treating
153
+ // it as unassigned keeps a retired role from satisfying a guard that no
154
+ // longer knows what it meant.
155
+ if (!descriptor.roles.includes(role))
156
+ return { status: "unassigned", orgId };
157
+ return { status: "assigned", role, orgId };
158
+ }
159
+ /** Every assignment for current members of one app in one org. */
160
+ export async function listAppMemberRoles(appId, orgId) {
161
+ const { rows } = await getDbExec().execute({
162
+ sql: `SELECT r.email, r.role
163
+ FROM app_member_roles r
164
+ INNER JOIN org_members m
165
+ ON m.org_id = r.org_id
166
+ AND LOWER(m.email) = LOWER(r.email)
167
+ WHERE r.org_id = ? AND r.app_id = ?`,
168
+ args: [orgId, appId],
169
+ });
170
+ return rows.map((r) => ({
171
+ email: String(r.email),
172
+ role: String(r.role),
173
+ }));
174
+ }
175
+ const nanoid = () => globalThis.crypto?.randomUUID?.().replace(/-/g, "") ??
176
+ Math.random().toString(36).slice(2) + Date.now().toString(36);
177
+ /**
178
+ * Assign or clear one member's app role. `role: null` deletes the assignment,
179
+ * which is distinct from assigning the descriptor's `defaultRole` — an unassigned
180
+ * member satisfies no guard, whereas a default is only ever a display value.
181
+ *
182
+ * Callers must have already established that the target is an org member and
183
+ * that the actor may manage the org; this function does not re-check either.
184
+ */
185
+ export async function setAppMemberRole(opts) {
186
+ const exec = getDbExec();
187
+ const email = normalizeEmail(opts.email);
188
+ if (opts.role === null) {
189
+ await exec.execute({
190
+ sql: `DELETE FROM app_member_roles
191
+ WHERE org_id = ? AND app_id = ? AND LOWER(email) = ?`,
192
+ args: [opts.orgId, opts.appId, email],
193
+ });
194
+ return;
195
+ }
196
+ const now = Date.now();
197
+ await exec.execute({
198
+ sql: `INSERT INTO app_member_roles
199
+ (id, org_id, app_id, email, role, updated_by, updated_at)
200
+ VALUES (?, ?, ?, ?, ?, ?, ?)
201
+ ON CONFLICT (org_id, app_id, LOWER(email)) DO UPDATE SET
202
+ role = excluded.role,
203
+ updated_by = excluded.updated_by,
204
+ updated_at = excluded.updated_at`,
205
+ args: [
206
+ nanoid(),
207
+ opts.orgId,
208
+ opts.appId,
209
+ email,
210
+ opts.role,
211
+ opts.updatedBy,
212
+ now,
213
+ ],
214
+ });
215
+ }
216
+ //# sourceMappingURL=app-roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-roles.js","sourceRoot":"","sources":["../../src/org/app-roles.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-app member roles — an overlay on org membership, never a second roster.\n *\n * Three role systems now coexist and answer different questions:\n * - org role (`org_members.role`) — what may this person do to the *team*?\n * - app role (this module) — what may this person do inside *one app*?\n * - share role (`sharing/`) — what may this person do to *one row*?\n *\n * They do not imply one another in either direction. In particular an app\n * `admin` is not an org admin: nothing here is ever read by the org handlers.\n */\n\nimport type { ActionRunContext } from \"../action.js\";\nimport { getDbExec } from \"../db/client.js\";\nimport {\n getRequestOrgId,\n getRequestUserEmail,\n} from \"../server/request-context.js\";\nimport { ForbiddenError } from \"../sharing/access.js\";\n\n/**\n * The serializable half of an app-role declaration. Kept free of database\n * imports so the same object can be passed to `defineAppRoles` on the server\n * and to `<TeamPage appRoles={...} />` in the browser bundle.\n */\nexport interface AppRolesDescriptor<R extends string = string> {\n /**\n * Stable identifier for the app these roles belong to. Comes from app source,\n * never from a request: the REST layer resolves a descriptor out of the\n * registry below and 404s on anything unregistered, so a client cannot name\n * an app the server did not declare.\n */\n appId: string;\n /**\n * The complete role vocabulary, as an unordered SET. Declaration order\n * carries no meaning — `[\"ae\", \"se\", \"csm\"]` are classifications, and ranking\n * them would silently grant the last one everything the first one has.\n * Authorization is always an explicit list of accepted roles.\n */\n roles: readonly R[];\n /**\n * What to show for an org member who has no assignment yet.\n *\n * DISPLAY ONLY — `requireAny` never matches it. If an unassigned member could\n * satisfy a guard, \"nobody has assigned this person\" and \"this person was\n * granted the role\" would be the same answer, and later widening the default\n * would silently widen every guard that names it.\n */\n defaultRole?: R;\n /** Optional human labels for the role picker. Falls back to the raw value. */\n roleLabels?: Partial<Record<R, string>>;\n /**\n * Column header for the role in `<TeamPage appRoles={...} />`. Defaults to\n * `appId`. Owned by the app rather than the framework's i18n catalogs — the\n * vocabulary it labels is app-specific, so its translations are too.\n */\n label?: string;\n}\n\n/**\n * Why a caller does or does not hold an app role. Each outcome is a distinct\n * value on purpose: collapsing \"not in this org\" into \"no role assigned\" would\n * report a membership problem as a permissions problem, and the operator would\n * go looking in the wrong settings page.\n *\n * A failed lookup is NOT in this union — it throws. A database error is not an\n * answer, and returning one of these statuses for it would deny loudly in\n * exactly the same shape as a legitimate denial.\n */\nexport type AppRoleLookup<R extends string = string> =\n | { status: \"assigned\"; role: R; orgId: string }\n | { status: \"unassigned\"; orgId: string }\n | { status: \"not-a-member\"; orgId: string }\n | { status: \"no-identity\" }\n | { status: \"no-org\" };\n\nexport interface AppRoleCaller {\n userEmail?: string | null;\n orgId?: string | null;\n}\n\nexport interface AppRoles<R extends string = string> {\n descriptor: AppRolesDescriptor<R>;\n appId: string;\n roles: readonly R[];\n /** Resolve without throwing on denial — for UI and for building richer guards. */\n resolve: (caller?: AppRoleCaller) => Promise<AppRoleLookup<R>>;\n /** Throw unless the caller holds one of `allowed`; returns the matched role. */\n assertAny: (allowed: readonly R[], caller?: AppRoleCaller) => Promise<R>;\n /** Guard for `defineAction({ authorize })`. Accepts an explicit assignment only. */\n requireAny: (\n ...allowed: R[]\n ) => (args: unknown, ctx?: ActionRunContext) => Promise<void>;\n}\n\nconst registry = new Map<string, AppRolesDescriptor<string>>();\n\n/** Descriptor for a registered app id, or undefined. Used by the REST layer. */\nexport function getRegisteredAppRoles(\n appId: string,\n): AppRolesDescriptor<string> | undefined {\n return registry.get(appId);\n}\n\nexport function listRegisteredAppRoles(): AppRolesDescriptor<string>[] {\n return [...registry.values()];\n}\n\nconst normalizeEmail = (email: string): string => email.trim().toLowerCase();\n\n/**\n * `undefined` means \"the caller did not say\" and falls back to the ambient\n * request context; an explicit `null` means \"this run has no user/org\" and must\n * NOT. Collapsing the two lets a system or cron caller that deliberately\n * declared no identity inherit whichever request happens to be on the stack,\n * and authorize as that person.\n */\nfunction callerIdentity(caller?: AppRoleCaller): {\n email: string | null;\n orgId: string | null;\n} {\n const email =\n caller?.userEmail !== undefined ? caller.userEmail : getRequestUserEmail();\n const orgId = caller?.orgId !== undefined ? caller.orgId : getRequestOrgId();\n return {\n email: email && email.trim() ? email.trim() : null,\n orgId: orgId && orgId.trim() ? orgId.trim() : null,\n };\n}\n\n/**\n * Declare an app's role vocabulary and get the server-side guards for it.\n *\n * ```ts\n * export const coachAccess = defineAppRoles({\n * appId: \"coach\",\n * roles: [\"member\", \"coach-admin\"] as const,\n * defaultRole: \"member\",\n * });\n * ```\n */\nexport function defineAppRoles<const R extends string>(\n descriptor: AppRolesDescriptor<R>,\n): AppRoles<R> {\n const appId = descriptor.appId.trim();\n if (!appId) throw new Error(\"defineAppRoles: appId is required\");\n if (!descriptor.roles.length) {\n throw new Error(`defineAppRoles(${appId}): at least one role is required`);\n }\n if (\n descriptor.defaultRole &&\n !descriptor.roles.includes(descriptor.defaultRole)\n ) {\n throw new Error(\n `defineAppRoles(${appId}): defaultRole \"${descriptor.defaultRole}\" is not in roles`,\n );\n }\n\n const existing = registry.get(appId);\n if (existing && existing !== (descriptor as AppRolesDescriptor<string>)) {\n // Two vocabularies behind one appId means the REST layer and the guards can\n // disagree about which roles exist, and the settings UI would offer roles\n // no guard accepts.\n throw new Error(\n `defineAppRoles: appId \"${appId}\" is already declared with a different descriptor`,\n );\n }\n registry.set(appId, descriptor as AppRolesDescriptor<string>);\n\n const resolve = (caller?: AppRoleCaller) =>\n resolveAppRole(descriptor, caller);\n\n const assertAny = async (\n allowed: readonly R[],\n caller?: AppRoleCaller,\n ): Promise<R> => {\n const lookup = await resolve(caller);\n if (lookup.status === \"assigned\" && allowed.includes(lookup.role)) {\n return lookup.role;\n }\n throw new ForbiddenError(denialMessage(appId, allowed, lookup));\n };\n\n return {\n descriptor,\n appId,\n roles: descriptor.roles,\n resolve,\n assertAny,\n requireAny: (...allowed: R[]) => {\n // An empty accepted set can never match. Failing closed at request time\n // would be correct but mute — `authorize: coachAccess.requireAny()` is a\n // dropped argument, and it should fail where the mistake is, not as an\n // unexplained 403 in production.\n if (!allowed.length) {\n throw new Error(\n `defineAppRoles(${appId}): requireAny() needs at least one accepted role`,\n );\n }\n const unknown = allowed.filter((r) => !descriptor.roles.includes(r));\n if (unknown.length) {\n throw new Error(\n `defineAppRoles(${appId}): requireAny() names undeclared role(s) ${unknown.join(\", \")}`,\n );\n }\n return async (_args: unknown, ctx?: ActionRunContext) => {\n await assertAny(allowed, {\n userEmail: ctx?.userEmail,\n orgId: ctx?.orgId,\n });\n };\n },\n };\n}\n\nfunction denialMessage<R extends string>(\n appId: string,\n allowed: readonly R[],\n lookup: AppRoleLookup<R>,\n): string {\n const need = `Requires ${appId} role ${allowed.join(\" or \")}`;\n switch (lookup.status) {\n case \"assigned\":\n return `${need} (have ${lookup.role})`;\n case \"unassigned\":\n return `${need} (no ${appId} role assigned)`;\n case \"not-a-member\":\n return `${need} (not a member of this organization)`;\n case \"no-org\":\n return `${need} (no active organization)`;\n case \"no-identity\":\n return `${need} (no authenticated user)`;\n }\n}\n\n/**\n * Single read behind every app-role decision.\n *\n * Membership and assignment are resolved in ONE statement so an assignment left\n * behind by a removed member can never authorize: the row only reaches the\n * result set through `org_members`. Splitting this into two queries would also\n * let a member removed between them still pass.\n */\nexport async function resolveAppRole<R extends string>(\n descriptor: AppRolesDescriptor<R>,\n caller?: AppRoleCaller,\n): Promise<AppRoleLookup<R>> {\n const { email, orgId } = callerIdentity(caller);\n if (!email) return { status: \"no-identity\" };\n if (!orgId) return { status: \"no-org\" };\n\n const { rows } = await getDbExec().execute({\n sql: `SELECT r.role AS \"appRole\"\n FROM org_members m\n LEFT JOIN app_member_roles r\n ON r.org_id = m.org_id\n AND r.app_id = ?\n AND LOWER(r.email) = LOWER(m.email)\n WHERE m.org_id = ? AND LOWER(m.email) = ?\n LIMIT 1`,\n args: [descriptor.appId, orgId, normalizeEmail(email)],\n });\n\n const row = rows[0] as { appRole?: unknown; approle?: unknown } | undefined;\n if (!row) return { status: \"not-a-member\", orgId };\n\n const raw = row.appRole ?? row.approle;\n if (raw === null || raw === undefined) return { status: \"unassigned\", orgId };\n\n const role = String(raw) as R;\n // A stored value outside the declared vocabulary is a role that was removed\n // from the app's declaration while assignments still referenced it. Treating\n // it as unassigned keeps a retired role from satisfying a guard that no\n // longer knows what it meant.\n if (!descriptor.roles.includes(role)) return { status: \"unassigned\", orgId };\n\n return { status: \"assigned\", role, orgId };\n}\n\nexport interface AppMemberRoleRow {\n email: string;\n role: string;\n}\n\n/** Every assignment for current members of one app in one org. */\nexport async function listAppMemberRoles(\n appId: string,\n orgId: string,\n): Promise<AppMemberRoleRow[]> {\n const { rows } = await getDbExec().execute({\n sql: `SELECT r.email, r.role\n FROM app_member_roles r\n INNER JOIN org_members m\n ON m.org_id = r.org_id\n AND LOWER(m.email) = LOWER(r.email)\n WHERE r.org_id = ? AND r.app_id = ?`,\n args: [orgId, appId],\n });\n return rows.map((r: any) => ({\n email: String(r.email),\n role: String(r.role),\n }));\n}\n\nconst nanoid = (): string =>\n globalThis.crypto?.randomUUID?.().replace(/-/g, \"\") ??\n Math.random().toString(36).slice(2) + Date.now().toString(36);\n\n/**\n * Assign or clear one member's app role. `role: null` deletes the assignment,\n * which is distinct from assigning the descriptor's `defaultRole` — an unassigned\n * member satisfies no guard, whereas a default is only ever a display value.\n *\n * Callers must have already established that the target is an org member and\n * that the actor may manage the org; this function does not re-check either.\n */\nexport async function setAppMemberRole(opts: {\n appId: string;\n orgId: string;\n email: string;\n role: string | null;\n updatedBy: string;\n}): Promise<void> {\n const exec = getDbExec();\n const email = normalizeEmail(opts.email);\n\n if (opts.role === null) {\n await exec.execute({\n sql: `DELETE FROM app_member_roles\n WHERE org_id = ? AND app_id = ? AND LOWER(email) = ?`,\n args: [opts.orgId, opts.appId, email],\n });\n return;\n }\n\n const now = Date.now();\n await exec.execute({\n sql: `INSERT INTO app_member_roles\n (id, org_id, app_id, email, role, updated_by, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (org_id, app_id, LOWER(email)) DO UPDATE SET\n role = excluded.role,\n updated_by = excluded.updated_by,\n updated_at = excluded.updated_at`,\n args: [\n nanoid(),\n opts.orgId,\n opts.appId,\n email,\n opts.role,\n opts.updatedBy,\n now,\n ],\n });\n}\n"]}
@@ -6,9 +6,12 @@ export { acceptPendingInvitationsForEmail } from "./accept-pending.js";
6
6
  export type { AcceptPendingResult } from "./accept-pending.js";
7
7
  export { autoJoinDomainMatchingOrgs } from "./auto-join-domain.js";
8
8
  export type { AutoJoinDomainResult } from "./auto-join-domain.js";
9
+ export { defineAppRoles, getRegisteredAppRoles, listRegisteredAppRoles, listAppMemberRoles, resolveAppRole, setAppMemberRole, } from "./app-roles.js";
10
+ export type { AppRoles, AppRolesDescriptor, AppRoleCaller, AppRoleLookup, AppMemberRoleRow, } from "./app-roles.js";
9
11
  export { ORG_MIGRATIONS } from "./migrations.js";
10
12
  export { createOrgPlugin, defaultOrgPlugin } from "./plugin.js";
11
- export { organizations, orgMembers, orgInvitations } from "./schema.js";
13
+ export { organizations, orgMembers, orgInvitations, appMemberRoles, } from "./schema.js";
12
14
  export { getMyOrgHandler, createOrgHandler, updateOrgHandler, switchOrgHandler, listMembersHandler, removeMemberHandler, changeMemberRoleHandler, listInvitationsHandler, createInvitationHandler, acceptInvitationHandler, setA2ASecretHandler, syncA2ASecretHandler, receiveA2ASecretHandler, } from "./handlers.js";
15
+ export { listAppRolesHandler, setAppRoleHandler, } from "./app-roles-handlers.js";
13
16
  export { isFreeEmailProvider } from "./free-email-providers.js";
14
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/org/index.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,OAAO,EACP,UAAU,EACV,UAAU,EACV,oBAAoB,EACpB,OAAO,EACP,SAAS,EACT,oBAAoB,GACrB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,aAAa,EACb,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AACvE,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AACnE,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAElE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIhE,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIxE,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/org/index.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,OAAO,EACP,UAAU,EACV,UAAU,EACV,oBAAoB,EACpB,OAAO,EACP,SAAS,EACT,oBAAoB,GACrB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,aAAa,EACb,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AACvE,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AACnE,YAAY,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAElE,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,sBAAsB,EACtB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,aAAa,EACb,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIhE,OAAO,EACL,aAAa,EACb,UAAU,EACV,cAAc,EACd,cAAc,GACf,MAAM,aAAa,CAAC;AAIrB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC"}
package/dist/org/index.js CHANGED
@@ -4,13 +4,15 @@ export { getOrgContext, getOrgDomain, getOrgA2ASecret, getA2ASecretByDomain, res
4
4
  export { implicitServiceOrgRole, parseServiceIdentityEmail, } from "./service-identity.js";
5
5
  export { acceptPendingInvitationsForEmail } from "./accept-pending.js";
6
6
  export { autoJoinDomainMatchingOrgs } from "./auto-join-domain.js";
7
+ export { defineAppRoles, getRegisteredAppRoles, listRegisteredAppRoles, listAppMemberRoles, resolveAppRole, setAppMemberRole, } from "./app-roles.js";
7
8
  export { ORG_MIGRATIONS } from "./migrations.js";
8
9
  export { createOrgPlugin, defaultOrgPlugin } from "./plugin.js";
9
10
  // Drizzle schema (re-exported so templates can write typed queries against
10
11
  // org tables without redefining the schema themselves).
11
- export { organizations, orgMembers, orgInvitations } from "./schema.js";
12
+ export { organizations, orgMembers, orgInvitations, appMemberRoles, } from "./schema.js";
12
13
  // Individual handlers — exported so templates can compose a custom org plugin
13
14
  // while still using the framework-provided handlers.
14
15
  export { getMyOrgHandler, createOrgHandler, updateOrgHandler, switchOrgHandler, listMembersHandler, removeMemberHandler, changeMemberRoleHandler, listInvitationsHandler, createInvitationHandler, acceptInvitationHandler, setA2ASecretHandler, syncA2ASecretHandler, receiveA2ASecretHandler, } from "./handlers.js";
16
+ export { listAppRolesHandler, setAppRoleHandler, } from "./app-roles-handlers.js";
15
17
  export { isFreeEmailProvider } from "./free-email-providers.js";
16
18
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/org/index.ts"],"names":[],"mappings":"AAAA,iCAAiC;AAYjC,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,aAAa,EACb,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AAGvE,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAGnE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEhE,2EAA2E;AAC3E,wDAAwD;AACxD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAExE,8EAA8E;AAC9E,qDAAqD;AACrD,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC","sourcesContent":["// Public API for the org module.\n\nexport type {\n OrgRole,\n OrgContext,\n OrgSummary,\n OrgInvitationSummary,\n OrgInfo,\n OrgMember,\n OrgPendingInvitation,\n} from \"./types.js\";\n\nexport {\n canInviteOrgMembers,\n canManageOrg,\n canManageOrgDomain,\n orgRoleAtLeast,\n orgRoleRank,\n} from \"./permissions.js\";\n\nexport {\n getOrgContext,\n getOrgDomain,\n getOrgA2ASecret,\n getA2ASecretByDomain,\n resolveOrgByDomain,\n resolveOrgIdForEmail,\n createOrganization,\n} from \"./context.js\";\n\nexport {\n implicitServiceOrgRole,\n parseServiceIdentityEmail,\n} from \"./service-identity.js\";\n\nexport { acceptPendingInvitationsForEmail } from \"./accept-pending.js\";\nexport type { AcceptPendingResult } from \"./accept-pending.js\";\n\nexport { autoJoinDomainMatchingOrgs } from \"./auto-join-domain.js\";\nexport type { AutoJoinDomainResult } from \"./auto-join-domain.js\";\n\nexport { ORG_MIGRATIONS } from \"./migrations.js\";\n\nexport { createOrgPlugin, defaultOrgPlugin } from \"./plugin.js\";\n\n// Drizzle schema (re-exported so templates can write typed queries against\n// org tables without redefining the schema themselves).\nexport { organizations, orgMembers, orgInvitations } from \"./schema.js\";\n\n// Individual handlers — exported so templates can compose a custom org plugin\n// while still using the framework-provided handlers.\nexport {\n getMyOrgHandler,\n createOrgHandler,\n updateOrgHandler,\n switchOrgHandler,\n listMembersHandler,\n removeMemberHandler,\n changeMemberRoleHandler,\n listInvitationsHandler,\n createInvitationHandler,\n acceptInvitationHandler,\n setA2ASecretHandler,\n syncA2ASecretHandler,\n receiveA2ASecretHandler,\n} from \"./handlers.js\";\n\nexport { isFreeEmailProvider } from \"./free-email-providers.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/org/index.ts"],"names":[],"mappings":"AAAA,iCAAiC;AAYjC,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,aAAa,EACb,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AAGvE,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAGnE,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,sBAAsB,EACtB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AASxB,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEhE,2EAA2E;AAC3E,wDAAwD;AACxD,OAAO,EACL,aAAa,EACb,UAAU,EACV,cAAc,EACd,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,8EAA8E;AAC9E,qDAAqD;AACrD,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC","sourcesContent":["// Public API for the org module.\n\nexport type {\n OrgRole,\n OrgContext,\n OrgSummary,\n OrgInvitationSummary,\n OrgInfo,\n OrgMember,\n OrgPendingInvitation,\n} from \"./types.js\";\n\nexport {\n canInviteOrgMembers,\n canManageOrg,\n canManageOrgDomain,\n orgRoleAtLeast,\n orgRoleRank,\n} from \"./permissions.js\";\n\nexport {\n getOrgContext,\n getOrgDomain,\n getOrgA2ASecret,\n getA2ASecretByDomain,\n resolveOrgByDomain,\n resolveOrgIdForEmail,\n createOrganization,\n} from \"./context.js\";\n\nexport {\n implicitServiceOrgRole,\n parseServiceIdentityEmail,\n} from \"./service-identity.js\";\n\nexport { acceptPendingInvitationsForEmail } from \"./accept-pending.js\";\nexport type { AcceptPendingResult } from \"./accept-pending.js\";\n\nexport { autoJoinDomainMatchingOrgs } from \"./auto-join-domain.js\";\nexport type { AutoJoinDomainResult } from \"./auto-join-domain.js\";\n\nexport {\n defineAppRoles,\n getRegisteredAppRoles,\n listRegisteredAppRoles,\n listAppMemberRoles,\n resolveAppRole,\n setAppMemberRole,\n} from \"./app-roles.js\";\nexport type {\n AppRoles,\n AppRolesDescriptor,\n AppRoleCaller,\n AppRoleLookup,\n AppMemberRoleRow,\n} from \"./app-roles.js\";\n\nexport { ORG_MIGRATIONS } from \"./migrations.js\";\n\nexport { createOrgPlugin, defaultOrgPlugin } from \"./plugin.js\";\n\n// Drizzle schema (re-exported so templates can write typed queries against\n// org tables without redefining the schema themselves).\nexport {\n organizations,\n orgMembers,\n orgInvitations,\n appMemberRoles,\n} from \"./schema.js\";\n\n// Individual handlers — exported so templates can compose a custom org plugin\n// while still using the framework-provided handlers.\nexport {\n getMyOrgHandler,\n createOrgHandler,\n updateOrgHandler,\n switchOrgHandler,\n listMembersHandler,\n removeMemberHandler,\n changeMemberRoleHandler,\n listInvitationsHandler,\n createInvitationHandler,\n acceptInvitationHandler,\n setA2ASecretHandler,\n syncA2ASecretHandler,\n receiveA2ASecretHandler,\n} from \"./handlers.js\";\n\nexport {\n listAppRolesHandler,\n setAppRoleHandler,\n} from \"./app-roles-handlers.js\";\n\nexport { isFreeEmailProvider } from \"./free-email-providers.js\";\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/org/migrations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;IAmG1B,CAAC"}
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/org/migrations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;IAyH1B,CAAC"}
@@ -102,5 +102,27 @@ export const ORG_MIGRATIONS = [
102
102
  name: "org-members-unique-lower-email-idx",
103
103
  sql: `CREATE UNIQUE INDEX IF NOT EXISTS org_members_org_lower_email_uidx ON org_members (org_id, LOWER(email))`,
104
104
  },
105
+ {
106
+ version: 1011,
107
+ sql: `CREATE TABLE IF NOT EXISTS app_member_roles (
108
+ id TEXT PRIMARY KEY,
109
+ org_id TEXT NOT NULL,
110
+ app_id TEXT NOT NULL,
111
+ email TEXT NOT NULL,
112
+ role TEXT NOT NULL,
113
+ updated_by TEXT NOT NULL,
114
+ updated_at INTEGER NOT NULL
115
+ )`,
116
+ },
117
+ {
118
+ // Every guarded action resolves (org_id, app_id, LOWER(email)) on the
119
+ // request path, and the assignment write upserts on the same key. Unique
120
+ // rather than plain so a concurrent double-assign is rejected by the
121
+ // database instead of leaving two rows whose winner depends on read order.
122
+ version: 1012,
123
+ name: "app-member-roles-unique-org-app-lower-email-idx",
124
+ sql: `CREATE UNIQUE INDEX IF NOT EXISTS app_member_roles_org_app_lower_email_uidx
125
+ ON app_member_roles (org_id, app_id, LOWER(email))`,
126
+ },
105
127
  ];
106
128
  //# sourceMappingURL=migrations.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/org/migrations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B;QACE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE;;;;;MAKH;KACH;IACD;QACE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE;;;;;;;MAOH;KACH;IACD;QACE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE;;;;;;;MAOH;KACH;IACD;QACE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE,wEAAwE;KAC9E;IACD;QACE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE,oEAAoE;KAC1E;IACD;QACE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE,gEAAgE;KACtE;IACD;QACE,kEAAkE;QAClE,mEAAmE;QACnE,oEAAoE;QACpE,8CAA8C;QAC9C,OAAO,EAAE,IAAI;QACb,GAAG,EAAE,sFAAsF;KAC5F;IACD;QACE,gEAAgE;QAChE,yEAAyE;QACzE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE,4GAA4G;KAClH;IACD;QACE,wEAAwE;QACxE,sEAAsE;QACtE,sEAAsE;QACtE,kEAAkE;QAClE,mEAAmE;QACnE,sEAAsE;QACtE,8DAA8D;QAC9D,sEAAsE;QACtE,mEAAmE;QACnE,mEAAmE;QACnE,oEAAoE;QACpE,sEAAsE;QACtE,yDAAyD;QACzD,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,gCAAgC;QACtC,GAAG,EAAE;;;;;;;;;QASD;KACL;IACD;QACE,iEAAiE;QACjE,qEAAqE;QACrE,mEAAmE;QACnE,gEAAgE;QAChE,iEAAiE;QACjE,mEAAmE;QACnE,4DAA4D;QAC5D,+DAA+D;QAC/D,6CAA6C;QAC7C,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,oCAAoC;QAC1C,GAAG,EAAE,0GAA0G;KAChH;CACF,CAAC","sourcesContent":["/**\n * Migration definitions for the org module. Versions are namespaced into a high\n * range (1000+) so they don't collide with template-owned migrations sharing\n * the same `_migrations` table.\n */\nexport const ORG_MIGRATIONS = [\n {\n version: 1001,\n sql: `CREATE TABLE IF NOT EXISTS organizations (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n created_by TEXT NOT NULL,\n created_at INTEGER NOT NULL\n )`,\n },\n {\n version: 1002,\n sql: `CREATE TABLE IF NOT EXISTS org_members (\n id TEXT PRIMARY KEY,\n org_id TEXT NOT NULL,\n email TEXT NOT NULL,\n role TEXT NOT NULL,\n joined_at INTEGER NOT NULL,\n UNIQUE(org_id, email)\n )`,\n },\n {\n version: 1003,\n sql: `CREATE TABLE IF NOT EXISTS org_invitations (\n id TEXT PRIMARY KEY,\n org_id TEXT NOT NULL,\n email TEXT NOT NULL,\n invited_by TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n status TEXT NOT NULL\n )`,\n },\n {\n version: 1004,\n sql: `ALTER TABLE organizations ADD COLUMN IF NOT EXISTS allowed_domain TEXT`,\n },\n {\n version: 1005,\n sql: `ALTER TABLE organizations ADD COLUMN IF NOT EXISTS a2a_secret TEXT`,\n },\n {\n version: 1006,\n sql: `ALTER TABLE org_invitations ADD COLUMN IF NOT EXISTS role TEXT`,\n },\n {\n // Every authenticated request calls `getOrgContext` which queries\n // `WHERE LOWER(m.email) = ?`. Without a supporting index this is a\n // full table scan on every request. A LOWER(email) expression index\n // lets the planner use an index seek instead.\n version: 1007,\n sql: `CREATE INDEX IF NOT EXISTS org_members_lower_email_idx ON org_members (LOWER(email))`,\n },\n {\n // Domain join and org resolution query `LOWER(allowed_domain)`.\n // Keep that opt-in lookup indexed before it appears on any request path.\n version: 1008,\n sql: `CREATE INDEX IF NOT EXISTS organizations_lower_allowed_domain_idx ON organizations (LOWER(allowed_domain))`,\n },\n {\n // De-dup pass ahead of the unique index below. `org_members` has always\n // had a `UNIQUE(org_id, email)` constraint (see v1002), but that's an\n // exact-string match — callers that insert a session's raw-case email\n // (e.g. handlers.ts's acceptInvitationHandler) can still create a\n // second row for the same person under a different case, and every\n // membership *read* in this module already matches case-insensitively\n // via `LOWER(email)`. This keeps exactly one row per (org_id,\n // LOWER(email)) — the row with the oldest `joined_at` (ties broken by\n // `id` for determinism) — by deleting any row for which a strictly\n // \"older\" row exists in the same group. Portable across SQLite and\n // Postgres (plain correlated EXISTS subquery, no window functions).\n // Must run before the unique index below or that CREATE would fail on\n // any database that already has case-variant duplicates.\n version: 1009,\n name: \"org-members-dedupe-lower-email\",\n sql: `DELETE FROM org_members\n WHERE EXISTS (\n SELECT 1 FROM org_members older\n WHERE older.org_id = org_members.org_id\n AND LOWER(older.email) = LOWER(org_members.email)\n AND (\n older.joined_at < org_members.joined_at\n OR (older.joined_at = org_members.joined_at AND older.id < org_members.id)\n )\n )`,\n },\n {\n // Closes the TOCTOU window in `acceptPendingInvitationsForEmail`\n // (org/accept-pending.ts): a SELECT-then-INSERT check with no unique\n // constraint standing behind the case-insensitive comparison every\n // reader uses. Without this, two concurrent acceptances (e.g. a\n // retried signup hook) can both pass the SELECT and both INSERT,\n // producing duplicate membership rows. This expression index makes\n // (org_id, LOWER(email)) unique at the database level so an\n // `ON CONFLICT` insert (or a raw duplicate insert) is rejected\n // instead of silently creating a second row.\n version: 1010,\n name: \"org-members-unique-lower-email-idx\",\n sql: `CREATE UNIQUE INDEX IF NOT EXISTS org_members_org_lower_email_uidx ON org_members (org_id, LOWER(email))`,\n },\n];\n"]}
1
+ {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/org/migrations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B;QACE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE;;;;;MAKH;KACH;IACD;QACE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE;;;;;;;MAOH;KACH;IACD;QACE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE;;;;;;;MAOH;KACH;IACD;QACE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE,wEAAwE;KAC9E;IACD;QACE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE,oEAAoE;KAC1E;IACD;QACE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE,gEAAgE;KACtE;IACD;QACE,kEAAkE;QAClE,mEAAmE;QACnE,oEAAoE;QACpE,8CAA8C;QAC9C,OAAO,EAAE,IAAI;QACb,GAAG,EAAE,sFAAsF;KAC5F;IACD;QACE,gEAAgE;QAChE,yEAAyE;QACzE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE,4GAA4G;KAClH;IACD;QACE,wEAAwE;QACxE,sEAAsE;QACtE,sEAAsE;QACtE,kEAAkE;QAClE,mEAAmE;QACnE,sEAAsE;QACtE,8DAA8D;QAC9D,sEAAsE;QACtE,mEAAmE;QACnE,mEAAmE;QACnE,oEAAoE;QACpE,sEAAsE;QACtE,yDAAyD;QACzD,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,gCAAgC;QACtC,GAAG,EAAE;;;;;;;;;QASD;KACL;IACD;QACE,iEAAiE;QACjE,qEAAqE;QACrE,mEAAmE;QACnE,gEAAgE;QAChE,iEAAiE;QACjE,mEAAmE;QACnE,4DAA4D;QAC5D,+DAA+D;QAC/D,6CAA6C;QAC7C,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,oCAAoC;QAC1C,GAAG,EAAE,0GAA0G;KAChH;IACD;QACE,OAAO,EAAE,IAAI;QACb,GAAG,EAAE;;;;;;;;MAQH;KACH;IACD;QACE,sEAAsE;QACtE,yEAAyE;QACzE,qEAAqE;QACrE,2EAA2E;QAC3E,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,iDAAiD;QACvD,GAAG,EAAE;6DACoD;KAC1D;CACF,CAAC","sourcesContent":["/**\n * Migration definitions for the org module. Versions are namespaced into a high\n * range (1000+) so they don't collide with template-owned migrations sharing\n * the same `_migrations` table.\n */\nexport const ORG_MIGRATIONS = [\n {\n version: 1001,\n sql: `CREATE TABLE IF NOT EXISTS organizations (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n created_by TEXT NOT NULL,\n created_at INTEGER NOT NULL\n )`,\n },\n {\n version: 1002,\n sql: `CREATE TABLE IF NOT EXISTS org_members (\n id TEXT PRIMARY KEY,\n org_id TEXT NOT NULL,\n email TEXT NOT NULL,\n role TEXT NOT NULL,\n joined_at INTEGER NOT NULL,\n UNIQUE(org_id, email)\n )`,\n },\n {\n version: 1003,\n sql: `CREATE TABLE IF NOT EXISTS org_invitations (\n id TEXT PRIMARY KEY,\n org_id TEXT NOT NULL,\n email TEXT NOT NULL,\n invited_by TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n status TEXT NOT NULL\n )`,\n },\n {\n version: 1004,\n sql: `ALTER TABLE organizations ADD COLUMN IF NOT EXISTS allowed_domain TEXT`,\n },\n {\n version: 1005,\n sql: `ALTER TABLE organizations ADD COLUMN IF NOT EXISTS a2a_secret TEXT`,\n },\n {\n version: 1006,\n sql: `ALTER TABLE org_invitations ADD COLUMN IF NOT EXISTS role TEXT`,\n },\n {\n // Every authenticated request calls `getOrgContext` which queries\n // `WHERE LOWER(m.email) = ?`. Without a supporting index this is a\n // full table scan on every request. A LOWER(email) expression index\n // lets the planner use an index seek instead.\n version: 1007,\n sql: `CREATE INDEX IF NOT EXISTS org_members_lower_email_idx ON org_members (LOWER(email))`,\n },\n {\n // Domain join and org resolution query `LOWER(allowed_domain)`.\n // Keep that opt-in lookup indexed before it appears on any request path.\n version: 1008,\n sql: `CREATE INDEX IF NOT EXISTS organizations_lower_allowed_domain_idx ON organizations (LOWER(allowed_domain))`,\n },\n {\n // De-dup pass ahead of the unique index below. `org_members` has always\n // had a `UNIQUE(org_id, email)` constraint (see v1002), but that's an\n // exact-string match — callers that insert a session's raw-case email\n // (e.g. handlers.ts's acceptInvitationHandler) can still create a\n // second row for the same person under a different case, and every\n // membership *read* in this module already matches case-insensitively\n // via `LOWER(email)`. This keeps exactly one row per (org_id,\n // LOWER(email)) — the row with the oldest `joined_at` (ties broken by\n // `id` for determinism) — by deleting any row for which a strictly\n // \"older\" row exists in the same group. Portable across SQLite and\n // Postgres (plain correlated EXISTS subquery, no window functions).\n // Must run before the unique index below or that CREATE would fail on\n // any database that already has case-variant duplicates.\n version: 1009,\n name: \"org-members-dedupe-lower-email\",\n sql: `DELETE FROM org_members\n WHERE EXISTS (\n SELECT 1 FROM org_members older\n WHERE older.org_id = org_members.org_id\n AND LOWER(older.email) = LOWER(org_members.email)\n AND (\n older.joined_at < org_members.joined_at\n OR (older.joined_at = org_members.joined_at AND older.id < org_members.id)\n )\n )`,\n },\n {\n // Closes the TOCTOU window in `acceptPendingInvitationsForEmail`\n // (org/accept-pending.ts): a SELECT-then-INSERT check with no unique\n // constraint standing behind the case-insensitive comparison every\n // reader uses. Without this, two concurrent acceptances (e.g. a\n // retried signup hook) can both pass the SELECT and both INSERT,\n // producing duplicate membership rows. This expression index makes\n // (org_id, LOWER(email)) unique at the database level so an\n // `ON CONFLICT` insert (or a raw duplicate insert) is rejected\n // instead of silently creating a second row.\n version: 1010,\n name: \"org-members-unique-lower-email-idx\",\n sql: `CREATE UNIQUE INDEX IF NOT EXISTS org_members_org_lower_email_uidx ON org_members (org_id, LOWER(email))`,\n },\n {\n version: 1011,\n sql: `CREATE TABLE IF NOT EXISTS app_member_roles (\n id TEXT PRIMARY KEY,\n org_id TEXT NOT NULL,\n app_id TEXT NOT NULL,\n email TEXT NOT NULL,\n role TEXT NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n )`,\n },\n {\n // Every guarded action resolves (org_id, app_id, LOWER(email)) on the\n // request path, and the assignment write upserts on the same key. Unique\n // rather than plain so a concurrent double-assign is rejected by the\n // database instead of leaving two rows whose winner depends on read order.\n version: 1012,\n name: \"app-member-roles-unique-org-app-lower-email-idx\",\n sql: `CREATE UNIQUE INDEX IF NOT EXISTS app_member_roles_org_app_lower_email_uidx\n ON app_member_roles (org_id, app_id, LOWER(email))`,\n },\n];\n"]}
@@ -11,6 +11,8 @@ type NitroPluginDef = (nitroApp: any) => void | Promise<void>;
11
11
  * PUT /_agent-native/org/switch — switch active org
12
12
  * GET /_agent-native/org/members — list members of active org
13
13
  * DELETE /_agent-native/org/members/:email — remove member (owner/admin only)
14
+ * GET /_agent-native/org/app-roles?appId=X — app role vocabulary + assignments
15
+ * PUT /_agent-native/org/app-roles/:email — assign/clear app role (owner/admin)
14
16
  * GET /_agent-native/org/invitations — list pending invites
15
17
  * POST /_agent-native/org/invitations — invite by email
16
18
  * POST /_agent-native/org/invitations/:id/accept — accept an invitation
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/org/plugin.ts"],"names":[],"mappings":"AAoCA,KAAK,cAAc,GAAG,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAI9D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,IAAI,cAAc,CA8LhD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,cAAkC,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/org/plugin.ts"],"names":[],"mappings":"AAwCA,KAAK,cAAc,GAAG,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAI9D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,eAAe,IAAI,cAAc,CAmNhD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,cAAkC,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import { defineEventHandler, setResponseStatus, getMethod, getRequestURL, } from "h3";
2
2
  import { runMigrations } from "../db/migrations.js";
3
3
  import { awaitBootstrap, getH3App, FRAMEWORK_PREFIX, markDefaultPluginProvided, } from "../server/framework-request-handler.js";
4
+ import { listAppRolesHandler, setAppRoleHandler, } from "./app-roles-handlers.js";
4
5
  import { getMyOrgHandler, createOrgHandler, updateOrgHandler, deleteOrgHandler, switchOrgHandler, listMembersHandler, removeMemberHandler, changeMemberRoleHandler, listInvitationsHandler, createInvitationHandler, acceptInvitationHandler, joinByDomainHandler, setDomainHandler, revealA2ASecretHandler, setA2ASecretHandler, syncA2ASecretHandler, receiveA2ASecretHandler, } from "./handlers.js";
5
6
  import { ORG_MIGRATIONS } from "./migrations.js";
6
7
  const ORG_PREFIX = `${FRAMEWORK_PREFIX}/org`;
@@ -16,6 +17,8 @@ const ORG_PREFIX = `${FRAMEWORK_PREFIX}/org`;
16
17
  * PUT /_agent-native/org/switch — switch active org
17
18
  * GET /_agent-native/org/members — list members of active org
18
19
  * DELETE /_agent-native/org/members/:email — remove member (owner/admin only)
20
+ * GET /_agent-native/org/app-roles?appId=X — app role vocabulary + assignments
21
+ * PUT /_agent-native/org/app-roles/:email — assign/clear app role (owner/admin)
19
22
  * GET /_agent-native/org/invitations — list pending invites
20
23
  * POST /_agent-native/org/invitations — invite by email
21
24
  * POST /_agent-native/org/invitations/:id/accept — accept an invitation
@@ -41,6 +44,23 @@ export function createOrgPlugin() {
41
44
  }
42
45
  return getMyOrgHandler(event);
43
46
  }));
47
+ // /app-roles and /app-roles/:email — per-app role overlay on the roster.
48
+ app.use(`${ORG_PREFIX}/app-roles`, defineEventHandler(async (event) => {
49
+ const tail = getRequestURL(event).pathname || "/";
50
+ const method = getMethod(event);
51
+ if (tail === "" || tail === "/") {
52
+ if (method !== "GET") {
53
+ setResponseStatus(event, 405);
54
+ return { error: "Method not allowed" };
55
+ }
56
+ return listAppRolesHandler(event);
57
+ }
58
+ if (method !== "PUT") {
59
+ setResponseStatus(event, 405);
60
+ return { error: "Method not allowed" };
61
+ }
62
+ return setAppRoleHandler(event);
63
+ }));
44
64
  // /members, /members/:email, /members/:email/role — dispatch by path-
45
65
  // tail + method in a single handler so H3's prefix-based `app.use`
46
66
  // doesn't route a DELETE for /members/alice@example.com to the