@agent-native/core 0.10.0 → 0.11.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 (129) hide show
  1. package/dist/a2a/caller-auth.d.ts +12 -0
  2. package/dist/a2a/caller-auth.d.ts.map +1 -0
  3. package/dist/a2a/caller-auth.js +54 -0
  4. package/dist/a2a/caller-auth.js.map +1 -0
  5. package/dist/action.d.ts +17 -0
  6. package/dist/action.d.ts.map +1 -1
  7. package/dist/action.js +22 -0
  8. package/dist/action.js.map +1 -1
  9. package/dist/agent/production-agent.d.ts.map +1 -1
  10. package/dist/agent/production-agent.js +32 -19
  11. package/dist/agent/production-agent.js.map +1 -1
  12. package/dist/client/AgentPanel.d.ts.map +1 -1
  13. package/dist/client/AgentPanel.js +9 -46
  14. package/dist/client/AgentPanel.js.map +1 -1
  15. package/dist/client/AssistantChat.d.ts.map +1 -1
  16. package/dist/client/AssistantChat.js +25 -19
  17. package/dist/client/AssistantChat.js.map +1 -1
  18. package/dist/client/builder-frame.d.ts.map +1 -1
  19. package/dist/client/builder-frame.js +10 -3
  20. package/dist/client/builder-frame.js.map +1 -1
  21. package/dist/client/components/ui/dropdown-menu.d.ts +28 -0
  22. package/dist/client/components/ui/dropdown-menu.d.ts.map +1 -0
  23. package/dist/client/components/ui/dropdown-menu.js +34 -0
  24. package/dist/client/components/ui/dropdown-menu.js.map +1 -0
  25. package/dist/client/composer/TiptapComposer.js +1 -1
  26. package/dist/client/composer/TiptapComposer.js.map +1 -1
  27. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  28. package/dist/client/extensions/EmbeddedExtension.js +2 -0
  29. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  30. package/dist/client/extensions/ExtensionSlot.js +14 -1
  31. package/dist/client/extensions/ExtensionSlot.js.map +1 -1
  32. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  33. package/dist/client/extensions/ExtensionViewer.js +2 -0
  34. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  35. package/dist/client/extensions/ExtensionsListPage.d.ts.map +1 -1
  36. package/dist/client/extensions/ExtensionsListPage.js +2 -2
  37. package/dist/client/extensions/ExtensionsListPage.js.map +1 -1
  38. package/dist/client/extensions/ExtensionsSidebarSection.d.ts.map +1 -1
  39. package/dist/client/extensions/ExtensionsSidebarSection.js +6 -17
  40. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  41. package/dist/client/extensions/iframe-bridge.d.ts.map +1 -1
  42. package/dist/client/extensions/iframe-bridge.js +5 -8
  43. package/dist/client/extensions/iframe-bridge.js.map +1 -1
  44. package/dist/client/org/OrgSwitcher.d.ts +7 -1
  45. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  46. package/dist/client/org/OrgSwitcher.js +8 -3
  47. package/dist/client/org/OrgSwitcher.js.map +1 -1
  48. package/dist/client/org/TeamPage.d.ts.map +1 -1
  49. package/dist/client/org/TeamPage.js +153 -20
  50. package/dist/client/org/TeamPage.js.map +1 -1
  51. package/dist/client/org/hooks.d.ts +29 -1
  52. package/dist/client/org/hooks.d.ts.map +1 -1
  53. package/dist/client/org/hooks.js +39 -2
  54. package/dist/client/org/hooks.js.map +1 -1
  55. package/dist/client/org/index.d.ts +2 -1
  56. package/dist/client/org/index.d.ts.map +1 -1
  57. package/dist/client/org/index.js +1 -1
  58. package/dist/client/org/index.js.map +1 -1
  59. package/dist/client/resources/ResourceTree.d.ts.map +1 -1
  60. package/dist/client/resources/ResourceTree.js +11 -3
  61. package/dist/client/resources/ResourceTree.js.map +1 -1
  62. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  63. package/dist/client/resources/ResourcesPanel.js +21 -5
  64. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  65. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  66. package/dist/client/settings/SettingsPanel.js +39 -3
  67. package/dist/client/settings/SettingsPanel.js.map +1 -1
  68. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  69. package/dist/client/sharing/ShareButton.js +58 -21
  70. package/dist/client/sharing/ShareButton.js.map +1 -1
  71. package/dist/client/use-action.d.ts.map +1 -1
  72. package/dist/client/use-action.js +1 -0
  73. package/dist/client/use-action.js.map +1 -1
  74. package/dist/deploy/build.d.ts.map +1 -1
  75. package/dist/deploy/build.js +20 -49
  76. package/dist/deploy/build.js.map +1 -1
  77. package/dist/index.browser.d.ts +1 -1
  78. package/dist/index.browser.d.ts.map +1 -1
  79. package/dist/index.browser.js +1 -1
  80. package/dist/index.browser.js.map +1 -1
  81. package/dist/index.d.ts +1 -1
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +1 -1
  84. package/dist/index.js.map +1 -1
  85. package/dist/org/accept-pending.d.ts.map +1 -1
  86. package/dist/org/accept-pending.js +5 -3
  87. package/dist/org/accept-pending.js.map +1 -1
  88. package/dist/org/free-email-providers.d.ts +18 -0
  89. package/dist/org/free-email-providers.d.ts.map +1 -0
  90. package/dist/org/free-email-providers.js +124 -0
  91. package/dist/org/free-email-providers.js.map +1 -0
  92. package/dist/org/handlers.d.ts +29 -5
  93. package/dist/org/handlers.d.ts.map +1 -1
  94. package/dist/org/handlers.js +178 -37
  95. package/dist/org/handlers.js.map +1 -1
  96. package/dist/org/index.d.ts +2 -1
  97. package/dist/org/index.d.ts.map +1 -1
  98. package/dist/org/index.js +2 -1
  99. package/dist/org/index.js.map +1 -1
  100. package/dist/org/migrations.d.ts.map +1 -1
  101. package/dist/org/migrations.js +4 -0
  102. package/dist/org/migrations.js.map +1 -1
  103. package/dist/org/plugin.d.ts.map +1 -1
  104. package/dist/org/plugin.js +13 -4
  105. package/dist/org/plugin.js.map +1 -1
  106. package/dist/org/schema.d.ts +19 -0
  107. package/dist/org/schema.d.ts.map +1 -1
  108. package/dist/org/schema.js +1 -0
  109. package/dist/org/schema.js.map +1 -1
  110. package/dist/org/types.d.ts +1 -0
  111. package/dist/org/types.d.ts.map +1 -1
  112. package/dist/org/types.js.map +1 -1
  113. package/dist/resources/metadata.d.ts +1 -0
  114. package/dist/resources/metadata.d.ts.map +1 -1
  115. package/dist/resources/metadata.js +13 -3
  116. package/dist/resources/metadata.js.map +1 -1
  117. package/dist/resources/store.d.ts.map +1 -1
  118. package/dist/resources/store.js +44 -6
  119. package/dist/resources/store.js.map +1 -1
  120. package/dist/server/action-routes.d.ts.map +1 -1
  121. package/dist/server/action-routes.js +1 -0
  122. package/dist/server/action-routes.js.map +1 -1
  123. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  124. package/dist/server/agent-chat-plugin.js +38 -11
  125. package/dist/server/agent-chat-plugin.js.map +1 -1
  126. package/dist/server/google-oauth.d.ts.map +1 -1
  127. package/dist/server/google-oauth.js +10 -3
  128. package/dist/server/google-oauth.js.map +1 -1
  129. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"accept-pending.js","sourceRoot":"","sources":["../../src/org/accept-pending.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDbExec } from \"../db/client.js\";\nimport { putUserSetting } from \"../settings/user-settings.js\";\n\nconst nanoid = (): string =>\n globalThis.crypto?.randomUUID?.().replace(/-/g, \"\") ??\n Math.random().toString(36).slice(2) + Date.now().toString(36);\n\nexport interface AcceptPendingResult {\n accepted: Array<{ invitationId: string; orgId: string }>;\n activeOrgId: string | null;\n}\n\n/**\n * Accept every pending `org_invitations` row for this email:\n * - insert a matching `org_members` row (role 'member') when one doesn't exist\n * - flip the invitation's status to 'accepted'\n * - set the user's `active-org-id` to the most-recently-created invite\n *\n * Called from the Better Auth `user.create.after` hook so that a user who signs\n * up with an email they were just invited to lands in the org immediately,\n * rather than seeing a blank-slate app until they navigate to /team.\n *\n * Safe to call when the org tables don't exist (some templates don't use the\n * org module) — it swallows the \"no such table\" error and returns empty.\n */\nexport async function acceptPendingInvitationsForEmail(\n rawEmail: string,\n): Promise<AcceptPendingResult> {\n const email = rawEmail.trim().toLowerCase();\n if (!email) {\n return { accepted: [], activeOrgId: null };\n }\n\n const db = getDbExec();\n\n let rows: Array<{ id: string; orgId: string }> = [];\n try {\n const res = await db.execute({\n sql: `SELECT id, org_id AS \"orgId\" FROM org_invitations\n WHERE LOWER(email) = ? AND status = 'pending'\n ORDER BY created_at DESC`,\n args: [email],\n });\n rows = res.rows.map((r: any) => ({\n id: String(r.id),\n orgId: String(r.orgId ?? r.org_id),\n }));\n } catch (err) {\n // Template doesn't use the org module / tables not migrated yet.\n return { accepted: [], activeOrgId: null };\n }\n\n if (rows.length === 0) {\n return { accepted: [], activeOrgId: null };\n }\n\n const accepted: AcceptPendingResult[\"accepted\"] = [];\n for (const inv of rows) {\n const existing = await db.execute({\n sql: `SELECT 1 FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,\n args: [inv.orgId, email],\n });\n if (existing.rows.length === 0) {\n await db.execute({\n sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, 'member', ?)`,\n args: [nanoid(), inv.orgId, email, Date.now()],\n });\n }\n await db.execute({\n sql: `UPDATE org_invitations SET status = 'accepted' WHERE id = ?`,\n args: [inv.id],\n });\n accepted.push({ invitationId: inv.id, orgId: inv.orgId });\n }\n\n // Set active-org-id to the most recent invite so the user lands in a\n // populated workspace on first load.\n const activeOrgId = accepted[0]?.orgId ?? null;\n if (activeOrgId) {\n try {\n await putUserSetting(email, \"active-org-id\", { orgId: activeOrgId });\n } catch {\n // user_settings table might not exist in a minimal template — not fatal.\n }\n }\n\n return { accepted, activeOrgId };\n}\n"]}
1
+ {"version":3,"file":"accept-pending.js","sourceRoot":"","sources":["../../src/org/accept-pending.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDbExec } from \"../db/client.js\";\nimport { putUserSetting } from \"../settings/user-settings.js\";\n\nconst nanoid = (): string =>\n globalThis.crypto?.randomUUID?.().replace(/-/g, \"\") ??\n Math.random().toString(36).slice(2) + Date.now().toString(36);\n\nexport interface AcceptPendingResult {\n accepted: Array<{ invitationId: string; orgId: string }>;\n activeOrgId: string | null;\n}\n\n/**\n * Accept every pending `org_invitations` row for this email:\n * - insert a matching `org_members` row (role 'member') when one doesn't exist\n * - flip the invitation's status to 'accepted'\n * - set the user's `active-org-id` to the most-recently-created invite\n *\n * Called from the Better Auth `user.create.after` hook so that a user who signs\n * up with an email they were just invited to lands in the org immediately,\n * rather than seeing a blank-slate app until they navigate to /team.\n *\n * Safe to call when the org tables don't exist (some templates don't use the\n * org module) — it swallows the \"no such table\" error and returns empty.\n */\nexport async function acceptPendingInvitationsForEmail(\n rawEmail: string,\n): Promise<AcceptPendingResult> {\n const email = rawEmail.trim().toLowerCase();\n if (!email) {\n return { accepted: [], activeOrgId: null };\n }\n\n const db = getDbExec();\n\n let rows: Array<{ id: string; orgId: string; role: string | null }> = [];\n try {\n const res = await db.execute({\n sql: `SELECT id, org_id AS \"orgId\", role FROM org_invitations\n WHERE LOWER(email) = ? AND status = 'pending'\n ORDER BY created_at DESC`,\n args: [email],\n });\n rows = res.rows.map((r: any) => ({\n id: String(r.id),\n orgId: String(r.orgId ?? r.org_id),\n role: r.role == null ? null : String(r.role),\n }));\n } catch (err) {\n // Template doesn't use the org module / tables not migrated yet.\n return { accepted: [], activeOrgId: null };\n }\n\n if (rows.length === 0) {\n return { accepted: [], activeOrgId: null };\n }\n\n const accepted: AcceptPendingResult[\"accepted\"] = [];\n for (const inv of rows) {\n const existing = await db.execute({\n sql: `SELECT 1 FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,\n args: [inv.orgId, email],\n });\n if (existing.rows.length === 0) {\n const role = inv.role === \"admin\" ? \"admin\" : \"member\";\n await db.execute({\n sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)`,\n args: [nanoid(), inv.orgId, email, role, Date.now()],\n });\n }\n await db.execute({\n sql: `UPDATE org_invitations SET status = 'accepted' WHERE id = ?`,\n args: [inv.id],\n });\n accepted.push({ invitationId: inv.id, orgId: inv.orgId });\n }\n\n // Set active-org-id to the most recent invite so the user lands in a\n // populated workspace on first load.\n const activeOrgId = accepted[0]?.orgId ?? null;\n if (activeOrgId) {\n try {\n await putUserSetting(email, \"active-org-id\", { orgId: activeOrgId });\n } catch {\n // user_settings table might not exist in a minimal template — not fatal.\n }\n }\n\n return { accepted, activeOrgId };\n}\n"]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Free / public mailbox providers that should NOT be allowed as an
3
+ * organization's auto-join domain.
4
+ *
5
+ * Why: the auto-join feature lets anyone who signs up with an email at the
6
+ * org's `allowed_domain` join the org without an invitation. That is safe
7
+ * for company-owned domains (`acme.com`) — the company controls who gets
8
+ * an `@acme.com` address. It is catastrophic for shared mailbox providers
9
+ * (`gmail.com`, `outlook.com`, etc.) — anyone in the world can create a
10
+ * matching address and would be auto-added to the org.
11
+ *
12
+ * The list intentionally errs on the side of well-known providers; if a
13
+ * future provider isn't here we'll learn from a bug report rather than
14
+ * pretend we have an exhaustive registry.
15
+ */
16
+ export declare const FREE_EMAIL_PROVIDER_DOMAINS: ReadonlySet<string>;
17
+ export declare function isFreeEmailProvider(domain: string): boolean;
18
+ //# sourceMappingURL=free-email-providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"free-email-providers.d.ts","sourceRoot":"","sources":["../../src/org/free-email-providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,2BAA2B,EAAE,WAAW,CAAC,MAAM,CAwG1D,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE3D"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Free / public mailbox providers that should NOT be allowed as an
3
+ * organization's auto-join domain.
4
+ *
5
+ * Why: the auto-join feature lets anyone who signs up with an email at the
6
+ * org's `allowed_domain` join the org without an invitation. That is safe
7
+ * for company-owned domains (`acme.com`) — the company controls who gets
8
+ * an `@acme.com` address. It is catastrophic for shared mailbox providers
9
+ * (`gmail.com`, `outlook.com`, etc.) — anyone in the world can create a
10
+ * matching address and would be auto-added to the org.
11
+ *
12
+ * The list intentionally errs on the side of well-known providers; if a
13
+ * future provider isn't here we'll learn from a bug report rather than
14
+ * pretend we have an exhaustive registry.
15
+ */
16
+ export const FREE_EMAIL_PROVIDER_DOMAINS = new Set([
17
+ // Google
18
+ "gmail.com",
19
+ "googlemail.com",
20
+ // Microsoft
21
+ "outlook.com",
22
+ "hotmail.com",
23
+ "live.com",
24
+ "msn.com",
25
+ "outlook.co.uk",
26
+ "hotmail.co.uk",
27
+ "live.co.uk",
28
+ "outlook.de",
29
+ "hotmail.de",
30
+ "live.de",
31
+ "outlook.fr",
32
+ "hotmail.fr",
33
+ "live.fr",
34
+ // Yahoo
35
+ "yahoo.com",
36
+ "yahoo.co.uk",
37
+ "yahoo.co.jp",
38
+ "yahoo.fr",
39
+ "yahoo.de",
40
+ "yahoo.it",
41
+ "yahoo.es",
42
+ "yahoo.ca",
43
+ "yahoo.com.au",
44
+ "yahoo.com.br",
45
+ "ymail.com",
46
+ "rocketmail.com",
47
+ // Apple
48
+ "icloud.com",
49
+ "me.com",
50
+ "mac.com",
51
+ // AOL / Verizon
52
+ "aol.com",
53
+ "aim.com",
54
+ // Privacy / disposable / forwarding
55
+ "proton.me",
56
+ "protonmail.com",
57
+ "pm.me",
58
+ "tutanota.com",
59
+ "tutanota.de",
60
+ "tuta.io",
61
+ "fastmail.com",
62
+ "fastmail.fm",
63
+ "duck.com",
64
+ "hey.com",
65
+ // Russian / Chinese majors
66
+ "yandex.com",
67
+ "yandex.ru",
68
+ "mail.ru",
69
+ "list.ru",
70
+ "bk.ru",
71
+ "inbox.ru",
72
+ "qq.com",
73
+ "163.com",
74
+ "126.com",
75
+ "sina.com",
76
+ "sina.cn",
77
+ "sohu.com",
78
+ // ISP / legacy / misc
79
+ "gmx.com",
80
+ "gmx.de",
81
+ "gmx.net",
82
+ "web.de",
83
+ "t-online.de",
84
+ "freenet.de",
85
+ "zoho.com",
86
+ "zohomail.com",
87
+ "rediffmail.com",
88
+ "mail.com",
89
+ "att.net",
90
+ "comcast.net",
91
+ "verizon.net",
92
+ "sbcglobal.net",
93
+ "bellsouth.net",
94
+ "cox.net",
95
+ "earthlink.net",
96
+ "btinternet.com",
97
+ "btopenworld.com",
98
+ "talktalk.net",
99
+ "sky.com",
100
+ "ntlworld.com",
101
+ "virginmedia.com",
102
+ "free.fr",
103
+ "orange.fr",
104
+ "wanadoo.fr",
105
+ "laposte.net",
106
+ "libero.it",
107
+ "tiscali.it",
108
+ "uol.com.br",
109
+ "bol.com.br",
110
+ "terra.com.br",
111
+ // Disposable
112
+ "mailinator.com",
113
+ "guerrillamail.com",
114
+ "10minutemail.com",
115
+ "trashmail.com",
116
+ "yopmail.com",
117
+ "tempmail.com",
118
+ "throwawaymail.com",
119
+ "sharklasers.com",
120
+ ]);
121
+ export function isFreeEmailProvider(domain) {
122
+ return FREE_EMAIL_PROVIDER_DOMAINS.has(domain.trim().toLowerCase());
123
+ }
124
+ //# sourceMappingURL=free-email-providers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"free-email-providers.js","sourceRoot":"","sources":["../../src/org/free-email-providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAwB,IAAI,GAAG,CAAC;IACtE,SAAS;IACT,WAAW;IACX,gBAAgB;IAChB,YAAY;IACZ,aAAa;IACb,aAAa;IACb,UAAU;IACV,SAAS;IACT,eAAe;IACf,eAAe;IACf,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,SAAS;IACT,YAAY;IACZ,YAAY;IACZ,SAAS;IACT,QAAQ;IACR,WAAW;IACX,aAAa;IACb,aAAa;IACb,UAAU;IACV,UAAU;IACV,UAAU;IACV,UAAU;IACV,UAAU;IACV,cAAc;IACd,cAAc;IACd,WAAW;IACX,gBAAgB;IAChB,QAAQ;IACR,YAAY;IACZ,QAAQ;IACR,SAAS;IACT,gBAAgB;IAChB,SAAS;IACT,SAAS;IACT,oCAAoC;IACpC,WAAW;IACX,gBAAgB;IAChB,OAAO;IACP,cAAc;IACd,aAAa;IACb,SAAS;IACT,cAAc;IACd,aAAa;IACb,UAAU;IACV,SAAS;IACT,2BAA2B;IAC3B,YAAY;IACZ,WAAW;IACX,SAAS;IACT,SAAS;IACT,OAAO;IACP,UAAU;IACV,QAAQ;IACR,SAAS;IACT,SAAS;IACT,UAAU;IACV,SAAS;IACT,UAAU;IACV,sBAAsB;IACtB,SAAS;IACT,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,aAAa;IACb,YAAY;IACZ,UAAU;IACV,cAAc;IACd,gBAAgB;IAChB,UAAU;IACV,SAAS;IACT,aAAa;IACb,aAAa;IACb,eAAe;IACf,eAAe;IACf,SAAS;IACT,eAAe;IACf,gBAAgB;IAChB,iBAAiB;IACjB,cAAc;IACd,SAAS;IACT,cAAc;IACd,iBAAiB;IACjB,SAAS;IACT,WAAW;IACX,YAAY;IACZ,aAAa;IACb,WAAW;IACX,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,cAAc;IACd,aAAa;IACb,gBAAgB;IAChB,mBAAmB;IACnB,kBAAkB;IAClB,eAAe;IACf,aAAa;IACb,cAAc;IACd,mBAAmB;IACnB,iBAAiB;CAClB,CAAC,CAAC;AAEH,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,OAAO,2BAA2B,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AACtE,CAAC","sourcesContent":["/**\n * Free / public mailbox providers that should NOT be allowed as an\n * organization's auto-join domain.\n *\n * Why: the auto-join feature lets anyone who signs up with an email at the\n * org's `allowed_domain` join the org without an invitation. That is safe\n * for company-owned domains (`acme.com`) — the company controls who gets\n * an `@acme.com` address. It is catastrophic for shared mailbox providers\n * (`gmail.com`, `outlook.com`, etc.) — anyone in the world can create a\n * matching address and would be auto-added to the org.\n *\n * The list intentionally errs on the side of well-known providers; if a\n * future provider isn't here we'll learn from a bug report rather than\n * pretend we have an exhaustive registry.\n */\nexport const FREE_EMAIL_PROVIDER_DOMAINS: ReadonlySet<string> = new Set([\n // Google\n \"gmail.com\",\n \"googlemail.com\",\n // Microsoft\n \"outlook.com\",\n \"hotmail.com\",\n \"live.com\",\n \"msn.com\",\n \"outlook.co.uk\",\n \"hotmail.co.uk\",\n \"live.co.uk\",\n \"outlook.de\",\n \"hotmail.de\",\n \"live.de\",\n \"outlook.fr\",\n \"hotmail.fr\",\n \"live.fr\",\n // Yahoo\n \"yahoo.com\",\n \"yahoo.co.uk\",\n \"yahoo.co.jp\",\n \"yahoo.fr\",\n \"yahoo.de\",\n \"yahoo.it\",\n \"yahoo.es\",\n \"yahoo.ca\",\n \"yahoo.com.au\",\n \"yahoo.com.br\",\n \"ymail.com\",\n \"rocketmail.com\",\n // Apple\n \"icloud.com\",\n \"me.com\",\n \"mac.com\",\n // AOL / Verizon\n \"aol.com\",\n \"aim.com\",\n // Privacy / disposable / forwarding\n \"proton.me\",\n \"protonmail.com\",\n \"pm.me\",\n \"tutanota.com\",\n \"tutanota.de\",\n \"tuta.io\",\n \"fastmail.com\",\n \"fastmail.fm\",\n \"duck.com\",\n \"hey.com\",\n // Russian / Chinese majors\n \"yandex.com\",\n \"yandex.ru\",\n \"mail.ru\",\n \"list.ru\",\n \"bk.ru\",\n \"inbox.ru\",\n \"qq.com\",\n \"163.com\",\n \"126.com\",\n \"sina.com\",\n \"sina.cn\",\n \"sohu.com\",\n // ISP / legacy / misc\n \"gmx.com\",\n \"gmx.de\",\n \"gmx.net\",\n \"web.de\",\n \"t-online.de\",\n \"freenet.de\",\n \"zoho.com\",\n \"zohomail.com\",\n \"rediffmail.com\",\n \"mail.com\",\n \"att.net\",\n \"comcast.net\",\n \"verizon.net\",\n \"sbcglobal.net\",\n \"bellsouth.net\",\n \"cox.net\",\n \"earthlink.net\",\n \"btinternet.com\",\n \"btopenworld.com\",\n \"talktalk.net\",\n \"sky.com\",\n \"ntlworld.com\",\n \"virginmedia.com\",\n \"free.fr\",\n \"orange.fr\",\n \"wanadoo.fr\",\n \"laposte.net\",\n \"libero.it\",\n \"tiscali.it\",\n \"uol.com.br\",\n \"bol.com.br\",\n \"terra.com.br\",\n // Disposable\n \"mailinator.com\",\n \"guerrillamail.com\",\n \"10minutemail.com\",\n \"trashmail.com\",\n \"yopmail.com\",\n \"tempmail.com\",\n \"throwawaymail.com\",\n \"sharklasers.com\",\n]);\n\nexport function isFreeEmailProvider(domain: string): boolean {\n return FREE_EMAIL_PROVIDER_DOMAINS.has(domain.trim().toLowerCase());\n}\n"]}
@@ -37,13 +37,23 @@ export declare const listMembersHandler: import("h3").EventHandlerWithFetch<impo
37
37
  joinedAt: number;
38
38
  }[];
39
39
  }>>;
40
- /** POST /_agent-native/org/invitations — invite a user by email */
41
- export declare const createInvitationHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
40
+ interface SingleInviteResult {
42
41
  id: string;
43
- email: any;
44
- status: string;
42
+ email: string;
43
+ role: "member" | "admin";
44
+ status: "pending";
45
45
  emailSent: boolean;
46
- emailError: string;
46
+ emailError?: string;
47
+ }
48
+ interface SingleInviteFailure {
49
+ email: string;
50
+ error: string;
51
+ }
52
+ /** POST /_agent-native/org/invitations — invite one or many users by email */
53
+ export declare const createInvitationHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<SingleInviteResult | {
54
+ succeeded: SingleInviteResult[];
55
+ failed: SingleInviteFailure[];
56
+ total: number;
47
57
  }>>;
48
58
  /** GET /_agent-native/org/invitations — list pending invitations for the org */
49
59
  export declare const listInvitationsHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
@@ -53,6 +63,7 @@ export declare const listInvitationsHandler: import("h3").EventHandlerWithFetch<
53
63
  invitedBy: string;
54
64
  createdAt: number;
55
65
  status: string;
66
+ role: string;
56
67
  }[];
57
68
  }>>;
58
69
  /** POST /_agent-native/org/invitations/:id/accept — accept an invitation */
@@ -65,6 +76,18 @@ export declare const acceptInvitationHandler: import("h3").EventHandlerWithFetch
65
76
  export declare const removeMemberHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
66
77
  success: boolean;
67
78
  }>>;
79
+ /**
80
+ * PUT /_agent-native/org/members/:email/role — change a member's role
81
+ * (owner/admin only). Body: { role: "admin" | "member" }.
82
+ *
83
+ * Only owners can promote/demote admins. (Admins can manage members but
84
+ * not other admins — otherwise an admin could escalate themselves to
85
+ * owner-equivalent control by promoting a confederate.)
86
+ */
87
+ export declare const changeMemberRoleHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
88
+ email: string;
89
+ role: string;
90
+ }>>;
68
91
  /** PATCH /_agent-native/org — rename the current organization (owner/admin only) */
69
92
  export declare const updateOrgHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
70
93
  orgId: string;
@@ -141,4 +164,5 @@ export declare const receiveA2ASecretHandler: import("h3").EventHandlerWithFetch
141
164
  ok: boolean;
142
165
  orgId: string;
143
166
  }>>;
167
+ export {};
144
168
  //# sourceMappingURL=handlers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/org/handlers.ts"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AA2B1C,2FAA2F;AAC3F,eAAO,MAAM,eAAe;;;;;;;cAaA,OAAO;;;;;;;;;;eAIC,MAAM;iBAAW,MAAM;;;;GA0EzD,CAAC;AAEH,0DAA0D;AAC1D,eAAO,MAAM,gBAAgB;;;;GAkC3B,CAAC;AAEH,wDAAwD;AACxD,eAAO,MAAM,kBAAkB;;;cAWH,OAAO;;;GAIjC,CAAC;AAEH,mEAAmE;AACnE,eAAO,MAAM,uBAAuB;;;;;;GA+EnC,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,sBAAsB;;;;;;;;GAqBlC,CAAC;AAEF,4EAA4E;AAC5E,eAAO,MAAM,uBAAuB;;;UAmD8B,OAAO;GAkBxE,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,mBAAmB;;GA2D/B,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,gBAAgB;;;GA4B3B,CAAC;AAEH,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB;;;UAkCC,OAAO;GAEnC,CAAC;AAEH,mGAAmG;AACnG,eAAO,MAAM,mBAAmB;;;UAqDR,OAAO;GAG9B,CAAC;AAEF,+FAA+F;AAC/F,eAAO,MAAM,gBAAgB;;GA2C3B,CAAC;AAEH,oGAAoG;AACpG,eAAO,MAAM,mBAAmB;;;GA0C/B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,oBAAoB;;;;;YA2DvB,MAAM;cACJ,MAAM;aACP,MAAM;YACP,OAAO;iBACF,MAAM;gBACP,MAAM;;GAyDnB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB;;;GAgGnC,CAAC"}
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/org/handlers.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AA2B1C,2FAA2F;AAC3F,eAAO,MAAM,eAAe;;;;;;;cAaA,OAAO;;;;;;;;;;eAIC,MAAM;iBAAW,MAAM;;;;GA0EzD,CAAC;AAEH,0DAA0D;AAC1D,eAAO,MAAM,gBAAgB;;;;GAkC3B,CAAC;AAEH,wDAAwD;AACxD,eAAO,MAAM,kBAAkB;;;cAWH,OAAO;;;GAIjC,CAAC;AAMH,UAAU,kBAAkB;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,MAAM,EAAE,SAAS,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,mBAAmB;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAsED,8EAA8E;AAC9E,eAAO,MAAM,uBAAuB;;;;GAuEnC,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,sBAAsB;;;;;;;;;GAyBlC,CAAC;AAEF,4EAA4E;AAC5E,eAAO,MAAM,uBAAuB;;;UAoD8B,OAAO;GAkBxE,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,mBAAmB;;GA2D/B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB;;;GAqEnC,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,gBAAgB;;;GA4B3B,CAAC;AAEH,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB;;;UAkCC,OAAO;GAEnC,CAAC;AAEH,mGAAmG;AACnG,eAAO,MAAM,mBAAmB;;;UAqDR,OAAO;GAG9B,CAAC;AAEF,+FAA+F;AAC/F,eAAO,MAAM,gBAAgB;;GAsE3B,CAAC;AAEH,oGAAoG;AACpG,eAAO,MAAM,mBAAmB;;;GA0C/B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,oBAAoB;;;;;YA2DvB,MAAM;cACJ,MAAM;aACP,MAAM;YACP,OAAO;iBACF,MAAM;gBACP,MAAM;;GAyDnB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB;;;GAgGnC,CAAC"}
@@ -14,13 +14,15 @@ function extractInvitationId(event) {
14
14
  path.match(/\/org\/invitations\/([^\/]+)\/accept\/?$/);
15
15
  return match?.[1] ? decodeURIComponent(match[1]) : undefined;
16
16
  }
17
- /** Extract the :email from member-delete paths. Same prefix-stripping caveat. */
17
+ /** Extract the :email from member-delete and member-role paths. Same prefix-stripping caveat. */
18
18
  function extractMemberEmail(event) {
19
19
  const fromRouter = getRouterParam(event, "email");
20
20
  if (fromRouter)
21
21
  return fromRouter;
22
22
  const path = getRequestURL(event).pathname;
23
- const match = path.match(/^\/([^\/]+)\/?$/) ?? path.match(/\/org\/members\/([^\/]+)\/?$/);
23
+ const match = path.match(/^\/([^\/]+)\/role\/?$/) ??
24
+ path.match(/^\/([^\/]+)\/?$/) ??
25
+ path.match(/\/org\/members\/([^\/]+)(?:\/role)?\/?$/);
24
26
  return match?.[1] ? decodeURIComponent(match[1]) : undefined;
25
27
  }
26
28
  const nanoid = () => globalThis.crypto?.randomUUID?.().replace(/-/g, "") ??
@@ -33,6 +35,7 @@ import { sendEmail, isEmailConfigured } from "../server/email.js";
33
35
  import { renderInviteEmail } from "../server/email-templates.js";
34
36
  import { getAppProductionUrl } from "../server/app-url.js";
35
37
  import { getOrgContext } from "./context.js";
38
+ import { isFreeEmailProvider } from "./free-email-providers.js";
36
39
  function getInviteAppUrl(event) {
37
40
  return getAppProductionUrl(event);
38
41
  }
@@ -189,58 +192,45 @@ export const listMembersHandler = defineEventHandler(async (event) => {
189
192
  }));
190
193
  return { members };
191
194
  });
192
- /** POST /_agent-native/org/invitations — invite a user by email */
193
- export const createInvitationHandler = defineEventHandler(async (event) => {
194
- const ctx = await getOrgContext(event);
195
- if (!ctx.orgId) {
196
- throw createError({
197
- statusCode: 400,
198
- message: "You must belong to an organization to invite members",
199
- });
195
+ function normalizeInviteRole(input) {
196
+ return input === "admin" ? "admin" : "member";
197
+ }
198
+ async function inviteOne(ctx, rawEmail, role, event) {
199
+ const email = rawEmail.trim().toLowerCase();
200
+ if (!email) {
201
+ throw createError({ statusCode: 400, message: "Email is required" });
200
202
  }
201
- if (ctx.role !== "owner" && ctx.role !== "admin") {
203
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
202
204
  throw createError({
203
- statusCode: 403,
204
- message: "Only owners and admins can invite members",
205
+ statusCode: 400,
206
+ message: `Invalid email: ${rawEmail}`,
205
207
  });
206
208
  }
207
- const body = await readBody(event);
208
- const email = body?.email?.trim()?.toLowerCase();
209
- if (!email) {
210
- throw createError({ statusCode: 400, message: "Email is required" });
211
- }
212
209
  const e = await exec();
213
- // Existing rows in org_members / org_invitations may have any case
214
- // (writes haven't been normalized historically). Compare with LOWER
215
- // on both sides so an "alice@..." invite check correctly recognizes
216
- // an existing "Alice@..." membership. `email` is already lowercased
217
- // at parse time above, but call .toLowerCase() explicitly here so
218
- // the contract at the SQL boundary matches every other handler in
219
- // this file.
220
210
  const existingMember = await e.execute({
221
211
  sql: `SELECT 1 FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,
222
- args: [ctx.orgId, email.toLowerCase()],
212
+ args: [ctx.orgId, email],
223
213
  });
224
214
  if (existingMember.rows.length > 0) {
225
215
  throw createError({
226
216
  statusCode: 409,
227
- message: "User is already a member of this organization",
217
+ message: `${email} is already a member`,
228
218
  });
229
219
  }
230
220
  const existingInvite = await e.execute({
231
221
  sql: `SELECT 1 FROM org_invitations WHERE org_id = ? AND LOWER(email) = ? AND status = 'pending' LIMIT 1`,
232
- args: [ctx.orgId, email.toLowerCase()],
222
+ args: [ctx.orgId, email],
233
223
  });
234
224
  if (existingInvite.rows.length > 0) {
235
225
  throw createError({
236
226
  statusCode: 409,
237
- message: "An invitation is already pending for this email",
227
+ message: `An invitation is already pending for ${email}`,
238
228
  });
239
229
  }
240
230
  const id = nanoid();
241
231
  await e.execute({
242
- sql: `INSERT INTO org_invitations (id, org_id, email, invited_by, created_at, status) VALUES (?, ?, ?, ?, ?, 'pending')`,
243
- args: [id, ctx.orgId, email, ctx.email, Date.now()],
232
+ sql: `INSERT INTO org_invitations (id, org_id, email, invited_by, created_at, status, role) VALUES (?, ?, ?, ?, ?, 'pending', ?)`,
233
+ args: [id, ctx.orgId, email, ctx.email, Date.now(), role],
244
234
  });
245
235
  let emailSent = false;
246
236
  let emailError;
@@ -260,7 +250,63 @@ export const createInvitationHandler = defineEventHandler(async (event) => {
260
250
  console.error("[org/invitations] failed to send invite email", err);
261
251
  }
262
252
  }
263
- return { id, email, status: "pending", emailSent, emailError };
253
+ return { id, email, role, status: "pending", emailSent, emailError };
254
+ }
255
+ /** POST /_agent-native/org/invitations — invite one or many users by email */
256
+ export const createInvitationHandler = defineEventHandler(async (event) => {
257
+ const ctx = await getOrgContext(event);
258
+ if (!ctx.orgId) {
259
+ throw createError({
260
+ statusCode: 400,
261
+ message: "You must belong to an organization to invite members",
262
+ });
263
+ }
264
+ if (ctx.role !== "owner" && ctx.role !== "admin") {
265
+ throw createError({
266
+ statusCode: 403,
267
+ message: "Only owners and admins can invite members",
268
+ });
269
+ }
270
+ const body = await readBody(event);
271
+ // Bulk shape: { invites: [{ email, role }, ...] } — preferred for any
272
+ // multi-recipient flow (paste-many, CSV upload). Single shape:
273
+ // { email, role } — kept for backwards compatibility.
274
+ const invitesInput = Array.isArray(body?.invites)
275
+ ? body.invites.map((inv) => ({
276
+ email: String(inv?.email ?? ""),
277
+ role: inv?.role,
278
+ }))
279
+ : null;
280
+ if (invitesInput) {
281
+ const succeeded = [];
282
+ const failed = [];
283
+ const seen = new Set();
284
+ for (const inv of invitesInput) {
285
+ const lower = inv.email.trim().toLowerCase();
286
+ if (!lower)
287
+ continue;
288
+ if (seen.has(lower))
289
+ continue;
290
+ seen.add(lower);
291
+ try {
292
+ const result = await inviteOne({ orgId: ctx.orgId, orgName: ctx.orgName, email: ctx.email }, inv.email, normalizeInviteRole(inv.role), event);
293
+ succeeded.push(result);
294
+ }
295
+ catch (err) {
296
+ const message = err instanceof Error ? err.message : String(err);
297
+ failed.push({ email: lower, error: message });
298
+ }
299
+ }
300
+ return {
301
+ succeeded,
302
+ failed,
303
+ total: succeeded.length + failed.length,
304
+ };
305
+ }
306
+ // Single-invite shape.
307
+ const role = normalizeInviteRole(body?.role);
308
+ const result = await inviteOne({ orgId: ctx.orgId, orgName: ctx.orgName, email: ctx.email }, body?.email ?? "", role, event);
309
+ return result;
264
310
  });
265
311
  /** GET /_agent-native/org/invitations — list pending invitations for the org */
266
312
  export const listInvitationsHandler = defineEventHandler(async (event) => {
@@ -269,7 +315,7 @@ export const listInvitationsHandler = defineEventHandler(async (event) => {
269
315
  return { invitations: [] };
270
316
  const e = await exec();
271
317
  const { rows } = await e.execute({
272
- sql: `SELECT id, email, invited_by AS "invitedBy", created_at AS "createdAt", status
318
+ sql: `SELECT id, email, invited_by AS "invitedBy", created_at AS "createdAt", status, role
273
319
  FROM org_invitations
274
320
  WHERE org_id = ? AND status = 'pending'`,
275
321
  args: [ctx.orgId],
@@ -280,6 +326,9 @@ export const listInvitationsHandler = defineEventHandler(async (event) => {
280
326
  invitedBy: String(r.invitedBy ?? r.invited_by),
281
327
  createdAt: Number(r.createdAt ?? r.created_at),
282
328
  status: String(r.status),
329
+ role: String(r.role ?? "member") === "admin"
330
+ ? "admin"
331
+ : "member",
283
332
  }));
284
333
  return { invitations };
285
334
  });
@@ -298,7 +347,7 @@ export const acceptInvitationHandler = defineEventHandler(async (event) => {
298
347
  const invRes = await e.execute({
299
348
  // Case-insensitive on email — see comment on the analogous
300
349
  // pending-invitations query in getMyOrgHandler.
301
- sql: `SELECT id, org_id AS "orgId" FROM org_invitations
350
+ sql: `SELECT id, org_id AS "orgId", role FROM org_invitations
302
351
  WHERE id = ? AND LOWER(email) = ? AND status = 'pending' LIMIT 1`,
303
352
  args: [invitationId, email.toLowerCase()],
304
353
  });
@@ -310,6 +359,7 @@ export const acceptInvitationHandler = defineEventHandler(async (event) => {
310
359
  }
311
360
  const inv = invRes.rows[0];
312
361
  const invOrgId = String(inv.orgId ?? inv.org_id);
362
+ const inviteRole = inv.role === "admin" ? "admin" : "member";
313
363
  const existingMembership = await e.execute({
314
364
  sql: `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,
315
365
  args: [invOrgId, email.toLowerCase()],
@@ -332,15 +382,15 @@ export const acceptInvitationHandler = defineEventHandler(async (event) => {
332
382
  };
333
383
  }
334
384
  await e.execute({
335
- sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, 'member', ?)`,
336
- args: [nanoid(), invOrgId, email, Date.now()],
385
+ sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)`,
386
+ args: [nanoid(), invOrgId, email, inviteRole, Date.now()],
337
387
  });
338
388
  await e.execute({
339
389
  sql: `UPDATE org_invitations SET status = 'accepted' WHERE id = ?`,
340
390
  args: [invitationId],
341
391
  });
342
392
  await putUserSetting(email, "active-org-id", { orgId: invOrgId });
343
- return { orgId: invOrgId, orgName, role: "member" };
393
+ return { orgId: invOrgId, orgName, role: inviteRole };
344
394
  });
345
395
  /** DELETE /_agent-native/org/members/:email — remove a member (owner/admin only) */
346
396
  export const removeMemberHandler = defineEventHandler(async (event) => {
@@ -397,6 +447,73 @@ export const removeMemberHandler = defineEventHandler(async (event) => {
397
447
  });
398
448
  return { success: true };
399
449
  });
450
+ /**
451
+ * PUT /_agent-native/org/members/:email/role — change a member's role
452
+ * (owner/admin only). Body: { role: "admin" | "member" }.
453
+ *
454
+ * Only owners can promote/demote admins. (Admins can manage members but
455
+ * not other admins — otherwise an admin could escalate themselves to
456
+ * owner-equivalent control by promoting a confederate.)
457
+ */
458
+ export const changeMemberRoleHandler = defineEventHandler(async (event) => {
459
+ const ctx = await getOrgContext(event);
460
+ if (!ctx.orgId) {
461
+ throw createError({ statusCode: 400, message: "No organization found" });
462
+ }
463
+ if (ctx.role !== "owner" && ctx.role !== "admin") {
464
+ throw createError({
465
+ statusCode: 403,
466
+ message: "Only owners and admins can change member roles",
467
+ });
468
+ }
469
+ const memberEmail = extractMemberEmail(event);
470
+ if (!memberEmail) {
471
+ throw createError({ statusCode: 400, message: "Email is required" });
472
+ }
473
+ const memberEmailLower = memberEmail.toLowerCase();
474
+ const body = await readBody(event);
475
+ const role = body?.role === "admin" ? "admin" : "member";
476
+ const e = await exec();
477
+ // Look up the target member's current role to enforce sensible rules
478
+ // about what changes are allowed.
479
+ const current = await e.execute({
480
+ sql: `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,
481
+ args: [ctx.orgId, memberEmailLower],
482
+ });
483
+ if (current.rows.length === 0) {
484
+ throw createError({ statusCode: 404, message: "Member not found" });
485
+ }
486
+ const currentRole = String(current.rows[0].role);
487
+ if (currentRole === "owner") {
488
+ throw createError({
489
+ statusCode: 400,
490
+ message: "Cannot change the organization owner's role",
491
+ });
492
+ }
493
+ // Admins are scoped to managing members. If they could promote
494
+ // members to admin, they could grant near-owner powers without owner
495
+ // approval. Restrict admin/admin role transitions to the owner.
496
+ if (ctx.role === "admin" && (currentRole === "admin" || role === "admin")) {
497
+ throw createError({
498
+ statusCode: 403,
499
+ message: "Only the organization owner can manage admins",
500
+ });
501
+ }
502
+ // Self-demotion guard: prevent the only admin from removing their own
503
+ // ability to manage things, and prevent the owner-self edge case
504
+ // (already filtered above by the currentRole check).
505
+ if (memberEmailLower === ctx.email.toLowerCase() && ctx.role === "admin") {
506
+ throw createError({
507
+ statusCode: 400,
508
+ message: "Use the owner account to change your own admin role",
509
+ });
510
+ }
511
+ await e.execute({
512
+ sql: `UPDATE org_members SET role = ? WHERE org_id = ? AND LOWER(email) = ?`,
513
+ args: [role, ctx.orgId, memberEmailLower],
514
+ });
515
+ return { email: memberEmailLower, role };
516
+ });
400
517
  /** PATCH /_agent-native/org — rename the current organization (owner/admin only) */
401
518
  export const updateOrgHandler = defineEventHandler(async (event) => {
402
519
  const ctx = await getOrgContext(event);
@@ -523,6 +640,30 @@ export const setDomainHandler = defineEventHandler(async (event) => {
523
640
  message: "Invalid domain format",
524
641
  });
525
642
  }
643
+ if (raw) {
644
+ // Auto-join is "anyone with this domain joins automatically". That is
645
+ // safe for company domains (the company controls who gets an address)
646
+ // and catastrophic for shared mailbox providers — anyone in the world
647
+ // could create a matching mailbox and silently join the org.
648
+ if (isFreeEmailProvider(raw)) {
649
+ throw createError({
650
+ statusCode: 400,
651
+ message: "Free email providers (gmail.com, outlook.com, etc.) cannot be used as an auto-join domain. Use your company's own domain.",
652
+ });
653
+ }
654
+ // Restrict to the admin's own email domain. Without this, an admin
655
+ // could set `allowed_domain` to a domain they don't control, and
656
+ // anyone signing up under that domain would join the org. Even with
657
+ // the free-provider blocklist above, that would still let an admin
658
+ // hijack a competitor's domain.
659
+ const ownDomain = ctx.email.split("@")[1]?.toLowerCase() ?? "";
660
+ if (raw !== ownDomain) {
661
+ throw createError({
662
+ statusCode: 400,
663
+ message: `You can only auto-join your own email domain (${ownDomain}).`,
664
+ });
665
+ }
666
+ }
526
667
  const e = await exec();
527
668
  if (raw) {
528
669
  const existing = await e.execute({