@headless-lms/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/assets/events.d.ts +66 -3
  2. package/dist/assets/events.js +19 -3
  3. package/dist/assets/events.js.map +1 -1
  4. package/dist/assets/service.js +5 -3
  5. package/dist/assets/service.js.map +1 -1
  6. package/dist/automations/actions.d.ts +11 -3
  7. package/dist/automations/actions.js +38 -5
  8. package/dist/automations/actions.js.map +1 -1
  9. package/dist/automations/catalog.d.ts +4 -1
  10. package/dist/automations/catalog.js +289 -188
  11. package/dist/automations/catalog.js.map +1 -1
  12. package/dist/automations/events.d.ts +46 -24
  13. package/dist/automations/events.js +5 -5
  14. package/dist/automations/events.js.map +1 -1
  15. package/dist/automations/index.d.ts +5 -5
  16. package/dist/automations/index.js +2 -2
  17. package/dist/automations/model.d.ts +13 -3
  18. package/dist/automations/model.js +23 -3
  19. package/dist/automations/model.js.map +1 -1
  20. package/dist/automations/ports.d.ts +23 -8
  21. package/dist/automations/service.d.ts +21 -5
  22. package/dist/automations/service.js +267 -47
  23. package/dist/automations/service.js.map +1 -1
  24. package/dist/automations/types.d.ts +2 -2
  25. package/dist/automations/webhook-signing.d.ts +7 -0
  26. package/dist/automations/webhook-signing.js +22 -0
  27. package/dist/automations/webhook-signing.js.map +1 -0
  28. package/dist/content/events.d.ts +401 -25
  29. package/dist/content/events.js +125 -19
  30. package/dist/content/events.js.map +1 -1
  31. package/dist/content/index.d.ts +5 -5
  32. package/dist/content/index.js +2 -2
  33. package/dist/content/model.d.ts +2 -2
  34. package/dist/content/ports.d.ts +42 -11
  35. package/dist/content/service.d.ts +15 -9
  36. package/dist/content/service.js +153 -45
  37. package/dist/content/service.js.map +1 -1
  38. package/dist/content/types.d.ts +2 -2
  39. package/dist/discussion/events.d.ts +60 -4
  40. package/dist/discussion/events.js +30 -4
  41. package/dist/discussion/events.js.map +1 -1
  42. package/dist/discussion/model.d.ts +1 -2
  43. package/dist/discussion/ports.d.ts +1 -1
  44. package/dist/discussion/service.d.ts +1 -1
  45. package/dist/discussion/service.js +11 -10
  46. package/dist/discussion/service.js.map +1 -1
  47. package/dist/entitlements/events.d.ts +96 -12
  48. package/dist/entitlements/events.js +21 -4
  49. package/dist/entitlements/events.js.map +1 -1
  50. package/dist/entitlements/service.js +1 -0
  51. package/dist/entitlements/service.js.map +1 -1
  52. package/dist/identity/events.d.ts +17 -3
  53. package/dist/identity/events.js +12 -2
  54. package/dist/identity/events.js.map +1 -1
  55. package/dist/identity/ports.d.ts +3 -0
  56. package/dist/identity/service.d.ts +3 -0
  57. package/dist/identity/service.js +12 -1
  58. package/dist/identity/service.js.map +1 -1
  59. package/dist/integrations/events.d.ts +27 -3
  60. package/dist/integrations/events.js +15 -3
  61. package/dist/integrations/events.js.map +1 -1
  62. package/dist/integrations/service.js +1 -1
  63. package/dist/integrations/service.js.map +1 -1
  64. package/dist/organizations/events.d.ts +178 -11
  65. package/dist/organizations/events.js +52 -9
  66. package/dist/organizations/events.js.map +1 -1
  67. package/dist/organizations/index.d.ts +3 -3
  68. package/dist/organizations/members.js.map +1 -1
  69. package/dist/organizations/ports.d.ts +6 -10
  70. package/dist/organizations/roles.js.map +1 -1
  71. package/dist/organizations/service.d.ts +9 -4
  72. package/dist/organizations/service.js +131 -72
  73. package/dist/organizations/service.js.map +1 -1
  74. package/dist/organizations/types.d.ts +2 -2
  75. package/dist/progress/events.d.ts +34 -2
  76. package/dist/progress/events.js +15 -2
  77. package/dist/progress/events.js.map +1 -1
  78. package/dist/progress/service.d.ts +2 -2
  79. package/dist/progress/service.js +4 -2
  80. package/dist/progress/service.js.map +1 -1
  81. package/dist/reporting/courses/index.d.ts +4 -0
  82. package/dist/reporting/courses/index.js +2 -0
  83. package/dist/reporting/courses/model.d.ts +24 -0
  84. package/dist/reporting/courses/model.js +1 -0
  85. package/dist/reporting/courses/ports.d.ts +13 -0
  86. package/dist/reporting/courses/ports.js +1 -0
  87. package/dist/reporting/courses/service.d.ts +19 -0
  88. package/dist/reporting/courses/service.js +20 -0
  89. package/dist/reporting/courses/service.js.map +1 -0
  90. package/dist/reporting/learn/model.d.ts +2 -2
  91. package/dist/reporting/learn/ports.d.ts +2 -1
  92. package/dist/reporting/learn/service.d.ts +2 -1
  93. package/dist/reporting/learn/service.js +5 -0
  94. package/dist/reporting/learn/service.js.map +1 -1
  95. package/dist/reporting/students/index.d.ts +2 -2
  96. package/dist/reporting/students/model.d.ts +21 -1
  97. package/dist/reporting/students/ports.d.ts +3 -1
  98. package/dist/reporting/students/service.d.ts +2 -1
  99. package/dist/reporting/students/service.js +3 -0
  100. package/dist/reporting/students/service.js.map +1 -1
  101. package/dist/shared/events.d.ts +5 -3
  102. package/dist/shared/events.js +1 -0
  103. package/dist/shared/events.js.map +1 -1
  104. package/dist/shared/id.d.ts +1 -0
  105. package/dist/shared/id.js +1 -0
  106. package/dist/shared/id.js.map +1 -1
  107. package/dist/shared/mailer.d.ts +2 -0
  108. package/dist/shared/mailer.js.map +1 -1
  109. package/dist/shared/settings.js.map +1 -1
  110. package/dist/types/automations.d.ts +13 -2
  111. package/dist/types/content.d.ts +2 -2
  112. package/dist/types/discussion.d.ts +3 -2
  113. package/dist/types/email-templates.d.ts +3 -1
  114. package/dist/types/events.d.ts +3 -3
  115. package/dist/types/index.d.ts +6 -6
  116. package/dist/types/organizations.d.ts +2 -2
  117. package/dist/types/ports.d.ts +1 -1
  118. package/dist/types/schemas/assets.d.ts +3 -0
  119. package/dist/types/schemas/assets.js +2 -1
  120. package/dist/types/schemas/assets.js.map +1 -1
  121. package/dist/types/schemas/automations.d.ts +71 -4
  122. package/dist/types/schemas/automations.js +35 -4
  123. package/dist/types/schemas/automations.js.map +1 -1
  124. package/dist/types/schemas/content.d.ts +51 -18
  125. package/dist/types/schemas/content.js +40 -13
  126. package/dist/types/schemas/content.js.map +1 -1
  127. package/dist/types/schemas/credentials.js.map +1 -1
  128. package/dist/types/schemas/discussion.d.ts +10 -2
  129. package/dist/types/schemas/discussion.js +12 -5
  130. package/dist/types/schemas/discussion.js.map +1 -1
  131. package/dist/types/schemas/entitlements.d.ts +8 -2
  132. package/dist/types/schemas/entitlements.js +10 -4
  133. package/dist/types/schemas/entitlements.js.map +1 -1
  134. package/dist/types/schemas/identity.d.ts +4 -4
  135. package/dist/types/schemas/identity.js +5 -5
  136. package/dist/types/schemas/identity.js.map +1 -1
  137. package/dist/types/schemas/index.d.ts +6 -6
  138. package/dist/types/schemas/index.js +6 -6
  139. package/dist/types/schemas/integrations.js.map +1 -1
  140. package/dist/types/schemas/organizations.d.ts +19 -5
  141. package/dist/types/schemas/organizations.js +12 -7
  142. package/dist/types/schemas/organizations.js.map +1 -1
  143. package/dist/types/schemas/outbox.d.ts +1 -0
  144. package/dist/types/schemas/outbox.js +1 -0
  145. package/dist/types/schemas/outbox.js.map +1 -1
  146. package/dist/types/schemas/progress.d.ts +1 -0
  147. package/dist/types/schemas/progress.js +1 -0
  148. package/dist/types/schemas/progress.js.map +1 -1
  149. package/dist/types/schemas/settings.js.map +1 -1
  150. package/dist/types/schemas/shared.d.ts +2 -1
  151. package/dist/types/schemas/shared.js +3 -1
  152. package/dist/types/schemas/shared.js.map +1 -1
  153. package/dist/types/shared.d.ts +1 -1
  154. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { AcceptInviteInput, AddOrgUserInput, CreateInviteInput, CreateOrgUserInput, Invite, InviteRole, NewOrganizationInput, OrgUser, Organization, Role } from "../types/schemas/organizations.js";
1
+ import { AcceptInviteInput, AddOrgUserInput, CreateInviteInput, CreateOrgUserInput, Invite, InviteRole, NewOrganizationInput, OrgUser, Organization, Role, UpdateOrgUserInput } from "../types/schemas/organizations.js";
2
2
  import { OutboxAppender, UnitOfWork } from "../shared/ports.js";
3
3
  import { AuthHeaders } from "../identity/ports.js";
4
4
  import "../identity/index.js";
@@ -12,7 +12,8 @@ interface OrganizationService {
12
12
  updateOrganization(id: string, input: UpdateOrganizationInput): Promise<Organization>;
13
13
  deleteOrganization(id: string): Promise<Organization>;
14
14
  addOrgUser(input: AddOrgUserInput): Promise<OrgUser>;
15
- removeOrgUser(orgId: string, id: string): Promise<OrgUser>;
15
+ removeOrgUser(orgId: string, userId: string): Promise<OrgUser>;
16
+ updateOrgUser(orgId: string, userId: string, patch: UpdateOrgUserInput): Promise<OrgUser>;
16
17
  getByExternalId(externalId: string): Promise<Organization | null>;
17
18
  getOrgUser(orgId: string, userId: string): Promise<OrgUser | null>;
18
19
  getOrgUsersForUser(userId: string): Promise<OrgUser[]>;
@@ -59,12 +60,13 @@ interface OrganizationsRepository {
59
60
  findById(id: string): Promise<Organization | null>;
60
61
  findByExternalId(externalId: string): Promise<Organization | null>;
61
62
  findBySlug(slug: string): Promise<Organization | null>;
62
- upsertOrgUser(orgId: string, input: AddOrgUserInput): Promise<OrgUser>;
63
63
  /** Inserts a pending invite, or re-issues the org's existing pending one
64
64
  * for this email (fresh token/expiry/role) — atomic upsert. */
65
65
  upsertPendingInvite(orgId: string, input: NewInviteRow): Promise<Invite>;
66
66
  setInviteStatus(orgId: string, id: string, status: string): Promise<Invite | null>;
67
67
  findInviteByTokenHash(tokenHash: string): Promise<Invite | null>;
68
+ findPendingInvite(orgId: string, email: string): Promise<Invite | null>;
69
+ setInviteToken(orgId: string, id: string, tokenHash: string, expiresAt: Date): Promise<Invite | null>;
68
70
  /** The person's row in one org. `(org_id, user_id)` is unique. */
69
71
  findOrgUser(orgId: string, userId: string): Promise<OrgUser | null>;
70
72
  /** Every org this person belongs to, oldest first. */
@@ -73,13 +75,7 @@ interface OrganizationsRepository {
73
75
  /** Links a user to the org under a role. Throws ConflictError when they
74
76
  * already hold a row there. */
75
77
  createOrgUser(input: CreateOrgUserInput): Promise<OrgUser>;
76
- /** createOrgUser, but idempotent on `(org_id, user_id)`: an existing row is
77
- * promoted to the requested status rather than refused. `created` says
78
- * whether a row appeared, so `student.created` fires exactly once. */
79
- ensureOrgUser(input: CreateOrgUserInput): Promise<{
80
- orgUser: OrgUser;
81
- created: boolean;
82
- }>;
78
+ updateOrgUser(orgId: string, id: string, patch: UpdateOrgUserInput): Promise<OrgUser | null>;
83
79
  /** Every org this person is a student in — the scope of `student.linked`. */
84
80
  findStudentOrgUsers(userId: string): Promise<OrgUser[]>;
85
81
  /** Kills the org's pending invite for an address, if any. */
@@ -1 +1 @@
1
- {"version":3,"file":"roles.js","names":[],"sources":["../../src/organizations/roles.ts"],"sourcesContent":["// organizations context — roles and the authorization matrix.\n// Defined in code (no DB enum). The DB stores role as text; the domain narrows\n// it to Role and answers authorization questions here. The Role type itself is\n// owned by @headless-lms/core/types; RANK/MATRIX below are Record<Role, …>, so the\n// compiler forces this file to cover every role the published type declares.\nimport type { Role } from '../types/index.js';\n\nexport type { Role };\nexport const ROLES = [\n 'owner',\n 'admin',\n 'instructor',\n 'student',\n] as const satisfies readonly Role[];\n\n/**\n * The roles that operate the back office. `student` belongs to the org but\n * never appears on a staff surface, so the member-management types are typed\n * with this narrower union — the exclusion is a compiler guarantee, not a\n * convention a query has to remember.\n */\nexport type StaffRole = Exclude<Role, 'student'>;\nexport const STAFF_ROLES = ['owner', 'admin', 'instructor'] as const satisfies readonly StaffRole[];\n\nexport function isRole(value: string): value is Role {\n return (ROLES as readonly string[]).includes(value);\n}\n\nexport function isStaffRole(value: string): value is StaffRole {\n return (STAFF_ROLES as readonly string[]).includes(value);\n}\n\nexport function parseRole(value: string): Role {\n if (!isRole(value)) {\n throw new Error(`unknown role: ${value}`);\n }\n return value;\n}\n\nexport type Permission =\n | 'manage_billing'\n | 'manage_org_settings'\n | 'manage_users'\n | 'create_course'\n | 'edit_assigned_course'\n | 'view_student_progress'\n | 'consume_content';\n\n// Unconditional (true), course-scoped (\"assigned\"), or enrollment-scoped\n// (\"enrolled\" — access owned by entitlements). Absent ⇒ denied.\n//\n// \"assigned\" is a granted-but-narrowed answer: the role holds the permission,\n// but only over some subset of courses. Nothing narrows it today — there is no\n// course-assignment store — so callers must decide what a narrowed grant means\n// for them. `authorize` treats it as denied; `tools.ts` treats it as granted.\nexport type Capability = true | 'assigned' | 'enrolled';\n\nconst MATRIX: Record<Role, Partial<Record<Permission, Capability>>> = {\n owner: {\n manage_billing: true,\n manage_org_settings: true,\n manage_users: true,\n create_course: true,\n edit_assigned_course: true,\n view_student_progress: true,\n },\n admin: {\n manage_org_settings: true,\n manage_users: true,\n create_course: true,\n edit_assigned_course: true,\n view_student_progress: true,\n },\n instructor: {\n edit_assigned_course: 'assigned',\n view_student_progress: 'assigned',\n },\n // A learner belongs to the org to consume content they hold an\n // entitlement for. No management capability of any kind.\n student: {\n consume_content: 'enrolled',\n },\n};\n\nexport function capability(role: Role, permission: Permission): Capability | false {\n return MATRIX[role][permission] ?? false;\n}\n\n// Better Auth's role model is broader than ours: it keeps a built-in `member`\n// role and allows comma-joined multi-role strings. Normalize any incoming role\n// string to a single domain Role before persisting: `member` -> `instructor`, a\n// multi-role string -> its highest-privilege known role, unknown -> `instructor`.\nconst RANK: Record<Role, number> = { owner: 3, admin: 2, instructor: 1, student: 0 };\n\nexport function normalizeRole(raw: string): Role {\n let best: Role = 'instructor';\n let bestRank = -1;\n for (const token of raw.split(',').map((t) => t.trim())) {\n const r: Role | null = isRole(token) ? token : token === 'member' ? 'instructor' : null;\n if (r && RANK[r] > bestRank) {\n best = r;\n bestRank = RANK[r];\n }\n }\n return best;\n}\n\n// Invites carry either a staff Role or this marker: the invited account is\n// a portal student, not an org member. Owned by the organizations domain — the\n// invite provider adapter reuses it to branch links, emails, and grants.\nexport const STUDENT_ROLE = 'student';\n"],"mappings":";AAQA,MAAa,QAAQ;CACnB;CACA;CACA;CACA;AACF;AASA,MAAa,cAAc;CAAC;CAAS;CAAS;AAAY;AAE1D,SAAgB,OAAO,OAA8B;CACnD,OAAQ,MAA4B,SAAS,KAAK;AACpD;AAEA,SAAgB,YAAY,OAAmC;CAC7D,OAAQ,YAAkC,SAAS,KAAK;AAC1D;AAEA,SAAgB,UAAU,OAAqB;CAC7C,IAAI,CAAC,OAAO,KAAK,GACf,MAAM,IAAI,MAAM,iBAAiB,OAAO;CAE1C,OAAO;AACT;AAoBA,MAAM,SAAgE;CACpE,OAAO;EACL,gBAAgB;EAChB,qBAAqB;EACrB,cAAc;EACd,eAAe;EACf,sBAAsB;EACtB,uBAAuB;CACzB;CACA,OAAO;EACL,qBAAqB;EACrB,cAAc;EACd,eAAe;EACf,sBAAsB;EACtB,uBAAuB;CACzB;CACA,YAAY;EACV,sBAAsB;EACtB,uBAAuB;CACzB;CAGA,SAAS,EACP,iBAAiB,WACnB;AACF;AAEA,SAAgB,WAAW,MAAY,YAA4C;CACjF,OAAO,OAAO,KAAK,CAAC,eAAe;AACrC;AAMA,MAAM,OAA6B;CAAE,OAAO;CAAG,OAAO;CAAG,YAAY;CAAG,SAAS;AAAE;AAEnF,SAAgB,cAAc,KAAmB;CAC/C,IAAI,OAAa;CACjB,IAAI,WAAW;CACf,KAAK,MAAM,SAAS,IAAI,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,GAAG;EACvD,MAAM,IAAiB,OAAO,KAAK,IAAI,QAAQ,UAAU,WAAW,eAAe;EACnF,IAAI,KAAK,KAAK,KAAK,UAAU;GAC3B,OAAO;GACP,WAAW,KAAK;EAClB;CACF;CACA,OAAO;AACT;AAKA,MAAa,eAAe"}
1
+ {"version":3,"file":"roles.js","names":[],"sources":["../../src/organizations/roles.ts"],"sourcesContent":["// organizations context — roles and the authorization matrix.\n// Defined in code (no DB enum). The DB stores role as text; the domain narrows\n// it to Role and answers authorization questions here. The Role type itself is\n// owned by @headless-lms/core/types; RANK/MATRIX below are Record<Role, …>, so the\n// compiler forces this file to cover every role the published type declares.\nimport type { Role } from '../types/index.js';\n\nexport type { Role };\nexport const ROLES = ['owner', 'admin', 'instructor', 'student'] as const satisfies readonly Role[];\n\n/**\n * The roles that operate the back office. `student` belongs to the org but\n * never appears on a staff surface, so the member-management types are typed\n * with this narrower union — the exclusion is a compiler guarantee, not a\n * convention a query has to remember.\n */\nexport type StaffRole = Exclude<Role, 'student'>;\nexport const STAFF_ROLES = ['owner', 'admin', 'instructor'] as const satisfies readonly StaffRole[];\n\nexport function isRole(value: string): value is Role {\n return (ROLES as readonly string[]).includes(value);\n}\n\nexport function isStaffRole(value: string): value is StaffRole {\n return (STAFF_ROLES as readonly string[]).includes(value);\n}\n\nexport function parseRole(value: string): Role {\n if (!isRole(value)) {\n throw new Error(`unknown role: ${value}`);\n }\n return value;\n}\n\nexport type Permission =\n | 'manage_billing'\n | 'manage_org_settings'\n | 'manage_users'\n | 'create_course'\n | 'edit_assigned_course'\n | 'view_student_progress'\n | 'consume_content';\n\n// Unconditional (true), course-scoped (\"assigned\"), or enrollment-scoped\n// (\"enrolled\" — access owned by entitlements). Absent ⇒ denied.\n//\n// \"assigned\" is a granted-but-narrowed answer: the role holds the permission,\n// but only over some subset of courses. Nothing narrows it today — there is no\n// course-assignment store — so callers must decide what a narrowed grant means\n// for them. `authorize` treats it as denied; `tools.ts` treats it as granted.\nexport type Capability = true | 'assigned' | 'enrolled';\n\nconst MATRIX: Record<Role, Partial<Record<Permission, Capability>>> = {\n owner: {\n manage_billing: true,\n manage_org_settings: true,\n manage_users: true,\n create_course: true,\n edit_assigned_course: true,\n view_student_progress: true,\n },\n admin: {\n manage_org_settings: true,\n manage_users: true,\n create_course: true,\n edit_assigned_course: true,\n view_student_progress: true,\n },\n instructor: {\n edit_assigned_course: 'assigned',\n view_student_progress: 'assigned',\n },\n // A learner belongs to the org to consume content they hold an\n // entitlement for. No management capability of any kind.\n student: {\n consume_content: 'enrolled',\n },\n};\n\nexport function capability(role: Role, permission: Permission): Capability | false {\n return MATRIX[role][permission] ?? false;\n}\n\n// Better Auth's role model is broader than ours: it keeps a built-in `member`\n// role and allows comma-joined multi-role strings. Normalize any incoming role\n// string to a single domain Role before persisting: `member` -> `instructor`, a\n// multi-role string -> its highest-privilege known role, unknown -> `instructor`.\nconst RANK: Record<Role, number> = { owner: 3, admin: 2, instructor: 1, student: 0 };\n\nexport function normalizeRole(raw: string): Role {\n let best: Role = 'instructor';\n let bestRank = -1;\n for (const token of raw.split(',').map((t) => t.trim())) {\n const r: Role | null = isRole(token) ? token : token === 'member' ? 'instructor' : null;\n if (r && RANK[r] > bestRank) {\n best = r;\n bestRank = RANK[r];\n }\n }\n return best;\n}\n\n// Invites carry either a staff Role or this marker: the invited account is\n// a portal student, not an org member. Owned by the organizations domain — the\n// invite provider adapter reuses it to branch links, emails, and grants.\nexport const STUDENT_ROLE = 'student';\n"],"mappings":";AAQA,MAAa,QAAQ;CAAC;CAAS;CAAS;CAAc;AAAS;AAS/D,MAAa,cAAc;CAAC;CAAS;CAAS;AAAY;AAE1D,SAAgB,OAAO,OAA8B;CACnD,OAAQ,MAA4B,SAAS,KAAK;AACpD;AAEA,SAAgB,YAAY,OAAmC;CAC7D,OAAQ,YAAkC,SAAS,KAAK;AAC1D;AAEA,SAAgB,UAAU,OAAqB;CAC7C,IAAI,CAAC,OAAO,KAAK,GACf,MAAM,IAAI,MAAM,iBAAiB,OAAO;CAE1C,OAAO;AACT;AAoBA,MAAM,SAAgE;CACpE,OAAO;EACL,gBAAgB;EAChB,qBAAqB;EACrB,cAAc;EACd,eAAe;EACf,sBAAsB;EACtB,uBAAuB;CACzB;CACA,OAAO;EACL,qBAAqB;EACrB,cAAc;EACd,eAAe;EACf,sBAAsB;EACtB,uBAAuB;CACzB;CACA,YAAY;EACV,sBAAsB;EACtB,uBAAuB;CACzB;CAGA,SAAS,EACP,iBAAiB,WACnB;AACF;AAEA,SAAgB,WAAW,MAAY,YAA4C;CACjF,OAAO,OAAO,KAAK,CAAC,eAAe;AACrC;AAMA,MAAM,OAA6B;CAAE,OAAO;CAAG,OAAO;CAAG,YAAY;CAAG,SAAS;AAAE;AAEnF,SAAgB,cAAc,KAAmB;CAC/C,IAAI,OAAa;CACjB,IAAI,WAAW;CACf,KAAK,MAAM,SAAS,IAAI,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,GAAG;EACvD,MAAM,IAAiB,OAAO,KAAK,IAAI,QAAQ,UAAU,WAAW,eAAe;EACnF,IAAI,KAAK,KAAK,KAAK,UAAU;GAC3B,OAAO;GACP,WAAW,KAAK;EAClB;CACF;CACA,OAAO;AACT;AAKA,MAAa,eAAe"}
@@ -1,5 +1,5 @@
1
1
  import { Logger } from "../types/ports.js";
2
- import { AcceptInviteInput, AddOrgUserInput, CreateInviteInput, Invite, OrgUser, Organization, Role } from "../types/schemas/organizations.js";
2
+ import { AcceptInviteInput, AddOrgUserInput, CreateInviteInput, Invite, OrgUser, Organization, Role, UpdateOrgUserInput } from "../types/schemas/organizations.js";
3
3
  import "../shared/ports.js";
4
4
  import { Mailer } from "../shared/mailer.js";
5
5
  import { IdentityService } from "../identity/ports.js";
@@ -8,7 +8,7 @@ import "./model.js";
8
8
  import "./roles.js";
9
9
  import { Member, MembersQuery, Page } from "./members.js";
10
10
  import { CreateOrganizationInput, ResendStudentInviteInput, UpdateOrganizationInput, UpdateStudentInput } from "./types.js";
11
- import { MemberWriteContext, MembersRepository, OrganizationService, OrganizationsRepository, OrganizationsUnitOfWork } from "./ports.js";
11
+ import { MemberWriteContext, MembersRepository, OrgAdmin, OrganizationService, OrganizationsRepository, OrganizationsUnitOfWork } from "./ports.js";
12
12
  //#region src/organizations/service.d.ts
13
13
  interface InviteUrls {
14
14
  studentPortalUrl: string;
@@ -19,6 +19,7 @@ type OrganizationServiceParams = {
19
19
  membersRepo: MembersRepository;
20
20
  people: IdentityService;
21
21
  uow: OrganizationsUnitOfWork;
22
+ orgAdmin: () => OrgAdmin;
22
23
  logger?: Logger;
23
24
  mailer?: Pick<Mailer, 'send'>;
24
25
  inviteUrls?: InviteUrls;
@@ -28,6 +29,7 @@ declare class OrganizationServiceImpl implements OrganizationService {
28
29
  private readonly membersRepo;
29
30
  private readonly people;
30
31
  private readonly uow;
32
+ private readonly orgAdmin;
31
33
  private readonly logger;
32
34
  private readonly mailer?;
33
35
  private readonly inviteUrls?;
@@ -36,12 +38,15 @@ declare class OrganizationServiceImpl implements OrganizationService {
36
38
  updateOrganization(id: string, input: UpdateOrganizationInput): Promise<Organization>;
37
39
  deleteOrganization(id: string): Promise<Organization>;
38
40
  addOrgUser({ orgId, userId, role }: AddOrgUserInput): Promise<OrgUser>;
39
- removeOrgUser(orgId: string, id: string): Promise<OrgUser>;
41
+ removeOrgUser(orgId: string, userId: string): Promise<OrgUser>;
42
+ updateOrgUser(orgId: string, userId: string, patch: UpdateOrgUserInput): Promise<OrgUser>;
40
43
  createInvite(input: CreateInviteInput): Promise<Invite>;
41
44
  updateStudent(input: UpdateStudentInput): Promise<OrgUser>;
42
45
  resendStudentInvite(input: ResendStudentInviteInput): Promise<void>;
43
46
  peekInvite(token: string): Promise<Invite | null>;
44
47
  acceptInvite(input: AcceptInviteInput): Promise<OrgUser>;
48
+ private acceptStudentInvite;
49
+ private acceptStaffInvite;
45
50
  getOrgUser(orgId: string, userId: string): Promise<OrgUser | null>;
46
51
  deleteOrgUser(orgId: string, id: string): Promise<OrgUser>;
47
52
  getOrgUsersForUser(userId: string): Promise<OrgUser[]>;
@@ -51,7 +56,7 @@ declare class OrganizationServiceImpl implements OrganizationService {
51
56
  getBySlug(slug: string): Promise<Organization | null>;
52
57
  listMembers(orgId: string, query: MembersQuery): Promise<Page<Member>>;
53
58
  updateMemberRole(ctx: MemberWriteContext, id: string, role: Role): Promise<Member | null>;
54
- removeMember(_ctx: MemberWriteContext, _id: string): Promise<boolean>;
59
+ removeMember(ctx: MemberWriteContext, id: string): Promise<boolean>;
55
60
  }
56
61
  //#endregion
57
62
  export { InviteUrls, OrganizationServiceImpl, OrganizationServiceParams };
@@ -1,5 +1,5 @@
1
- import { genId } from "../shared/id.js";
2
1
  import { noopLogger } from "../shared/logger.js";
2
+ import { genId } from "../shared/id.js";
3
3
  import { NotFoundError } from "../shared/errors.js";
4
4
  import { STUDENT_ROLE } from "./roles.js";
5
5
  import { InviteError, OrganizationRuleError } from "./members.js";
@@ -25,6 +25,7 @@ var OrganizationServiceImpl = class {
25
25
  membersRepo;
26
26
  people;
27
27
  uow;
28
+ orgAdmin;
28
29
  logger;
29
30
  mailer;
30
31
  inviteUrls;
@@ -33,6 +34,7 @@ var OrganizationServiceImpl = class {
33
34
  this.membersRepo = params.membersRepo;
34
35
  this.people = params.people;
35
36
  this.uow = params.uow;
37
+ this.orgAdmin = params.orgAdmin;
36
38
  this.logger = params.logger ?? noopLogger;
37
39
  this.mailer = params.mailer;
38
40
  this.inviteUrls = params.inviteUrls;
@@ -108,57 +110,85 @@ var OrganizationServiceImpl = class {
108
110
  });
109
111
  return orgUser;
110
112
  }
111
- async removeOrgUser(orgId, id) {
113
+ async removeOrgUser(orgId, userId) {
112
114
  const deleted = await this.uow.run(async ({ organizations, outbox }) => {
113
- const deletedUser = await organizations.deleteOrgUser(orgId, id);
114
- if (!deletedUser) throw new NotFoundError("OrgUser", id);
115
+ const orgUser = await organizations.findOrgUser(orgId, userId);
116
+ if (!orgUser) throw new NotFoundError("OrgUser", userId);
117
+ const deletedUser = await organizations.deleteOrgUser(orgId, orgUser.id);
118
+ if (!deletedUser) throw new NotFoundError("OrgUser", orgUser.id);
115
119
  await outbox.append([organizationEvents.orgUserDeleted.make({
116
120
  orgId,
117
121
  data: deletedUser
118
122
  })]);
119
123
  return deletedUser;
120
124
  });
121
- this.logger.info("organization deleted", { orgId: deleted.id });
125
+ this.logger.info("org user removed", {
126
+ orgId,
127
+ userId,
128
+ orgUserId: deleted.id
129
+ });
122
130
  return deleted;
123
131
  }
132
+ async updateOrgUser(orgId, userId, patch) {
133
+ const orgUser = await this.repo.findOrgUser(orgId, userId);
134
+ if (!orgUser) throw new NotFoundError("OrgUser", userId);
135
+ const updated = await this.repo.updateOrgUser(orgId, orgUser.id, patch);
136
+ if (!updated) throw new NotFoundError("OrgUser", orgUser.id);
137
+ this.logger.info("org user updated", {
138
+ orgId,
139
+ userId,
140
+ orgUserId: updated.id,
141
+ patch
142
+ });
143
+ return updated;
144
+ }
124
145
  async createInvite(input) {
125
146
  const { orgId, email, role, inviterUserId, firstName, lastName } = input;
126
147
  const sendEmail = input.sendEmail ?? true;
127
148
  const { token, tokenHash } = generateInviteToken();
128
149
  const expiresAt = new Date(Date.now() + INVITE_TTL_MS);
129
- const person = role === "student" ? await this.people.createUser({
150
+ const person = role === "student" ? await this.people.getUserByEmail(email) ?? await this.people.createUser({
130
151
  email,
131
152
  ...firstName !== void 0 && { firstName },
132
153
  ...lastName !== void 0 && { lastName }
133
154
  }) : null;
134
155
  const invite = await this.uow.run(async ({ organizations, outbox }) => {
135
- const row = await organizations.upsertPendingInvite(orgId, {
136
- email,
137
- role,
138
- invitedBy: inviterUserId,
139
- tokenHash,
140
- expiresAt
141
- });
142
- const events = [organizationEvents.inviteCreated.make({
143
- orgId,
144
- data: row
145
- })];
156
+ const events = [];
146
157
  if (person) {
147
- const { orgUser, created } = await organizations.ensureOrgUser({
158
+ const orgUser = await organizations.createOrgUser({
148
159
  orgId,
149
160
  userId: person.id,
150
161
  role: STUDENT_ROLE,
151
162
  status: "invited"
152
163
  });
153
- if (created) events.push(organizationEvents.studentCreated.make({
164
+ events.push(organizationEvents.studentCreated.make({
154
165
  orgId,
155
166
  data: orgUser
156
167
  }));
157
168
  }
169
+ const row = await organizations.upsertPendingInvite(orgId, {
170
+ email,
171
+ role,
172
+ invitedBy: inviterUserId,
173
+ tokenHash,
174
+ expiresAt
175
+ });
176
+ events.push(organizationEvents.inviteCreated.make({
177
+ orgId,
178
+ data: row
179
+ }));
158
180
  await outbox.append(events);
159
181
  return row;
160
182
  });
161
- if (sendEmail) await this.sendInviteEmail(invite, token);
183
+ if (sendEmail) try {
184
+ await this.sendInviteEmail(invite, token);
185
+ } catch (err) {
186
+ this.logger.error("failed to send invite email", {
187
+ email: invite.email,
188
+ role: invite.role,
189
+ err: err instanceof Error ? err : new Error(String(err))
190
+ });
191
+ }
162
192
  this.logger.info("invite created", {
163
193
  orgId,
164
194
  inviteId: invite.id,
@@ -195,16 +225,18 @@ var OrganizationServiceImpl = class {
195
225
  if (orgUser.status === "active") throw new OrganizationRuleError("That student has already accepted their invitation");
196
226
  const person = await this.people.getUserById(orgUser.userId);
197
227
  if (!person) throw new NotFoundError("User", orgUser.userId);
198
- await this.createInvite({
199
- orgId,
200
- email: person.email,
201
- role: STUDENT_ROLE,
202
- inviterUserId,
203
- sendEmail: true
204
- });
228
+ const pending = await this.repo.findPendingInvite(orgId, person.email);
229
+ if (!pending) throw new NotFoundError("Invite", person.email);
230
+ const { token, tokenHash } = generateInviteToken();
231
+ const expiresAt = new Date(Date.now() + INVITE_TTL_MS);
232
+ const invite = await this.repo.setInviteToken(orgId, pending.id, tokenHash, expiresAt);
233
+ if (!invite) throw new NotFoundError("Invite", pending.id);
234
+ await this.sendInviteEmail(invite, token);
205
235
  this.logger.info("student invite resent", {
206
236
  orgId,
207
- orgUserId
237
+ orgUserId,
238
+ inviteId: invite.id,
239
+ inviterUserId
208
240
  });
209
241
  }
210
242
  async peekInvite(token) {
@@ -226,39 +258,48 @@ var OrganizationServiceImpl = class {
226
258
  });
227
259
  throw new InviteError("emails must match");
228
260
  }
229
- if (!await this.repo.findById(invite.orgId)) throw new InviteError("Org not found");
230
- const orgUser = await this.uow.run(async ({ organizations, outbox }) => {
231
- const { orgUser: row, created } = await organizations.ensureOrgUser({
232
- orgId: invite.orgId,
233
- userId: input.userId,
234
- role: invite.role,
235
- status: "active"
236
- });
261
+ const org = await this.repo.findById(invite.orgId);
262
+ if (!org) throw new InviteError("Org not found");
263
+ const orgUser = invite.role === "student" ? await this.acceptStudentInvite(invite, input.userId) : await this.acceptStaffInvite(invite, org, input.userId);
264
+ this.logger.info("invite accepted", {
265
+ orgId: invite.orgId,
266
+ userId: input.userId
267
+ });
268
+ return orgUser;
269
+ }
270
+ async acceptStudentInvite(invite, userId) {
271
+ return this.uow.run(async ({ organizations, outbox }) => {
272
+ const existing = await organizations.findOrgUser(invite.orgId, userId);
273
+ if (!existing) throw new NotFoundError("OrgUser", userId);
274
+ const activated = await organizations.updateOrgUser(invite.orgId, existing.id, { status: "active" });
275
+ if (!activated) throw new NotFoundError("OrgUser", existing.id);
237
276
  const acceptedInvite = await organizations.setInviteStatus(invite.orgId, invite.id, "accepted");
238
277
  if (!acceptedInvite) throw new NotFoundError("Invite", invite.id);
239
- const events = [organizationEvents.inviteAccepted.make({
278
+ await outbox.append([organizationEvents.inviteAccepted.make({
240
279
  orgId: invite.orgId,
241
280
  data: acceptedInvite
242
- })];
243
- if (row.role === "student") events.push(created ? organizationEvents.studentCreated.make({
281
+ }), organizationEvents.studentLinked.make({
244
282
  orgId: invite.orgId,
245
- data: row
246
- }) : organizationEvents.studentLinked.make({
247
- orgId: invite.orgId,
248
- data: row
249
- }));
250
- else if (created) events.push(organizationEvents.orgUserLinked.make({
251
- orgId: invite.orgId,
252
- data: row
253
- }));
254
- await outbox.append(events);
255
- return row;
283
+ data: activated
284
+ })]);
285
+ return activated;
256
286
  });
257
- this.logger.info("invite accepted", {
258
- orgId: invite.orgId,
259
- userId: input.userId
287
+ }
288
+ async acceptStaffInvite(invite, org, userId) {
289
+ const person = await this.people.getUserById(userId);
290
+ if (!person?.externalId) throw new NotFoundError("User", userId);
291
+ await this.orgAdmin().grantMembership(org.externalId ?? org.id, person.externalId, invite.role);
292
+ return this.uow.run(async ({ organizations, outbox }) => {
293
+ const orgUser = await organizations.findOrgUser(invite.orgId, userId);
294
+ if (!orgUser) throw new NotFoundError("OrgUser", userId);
295
+ const acceptedInvite = await organizations.setInviteStatus(invite.orgId, invite.id, "accepted");
296
+ if (!acceptedInvite) throw new NotFoundError("Invite", invite.id);
297
+ await outbox.append([organizationEvents.inviteAccepted.make({
298
+ orgId: invite.orgId,
299
+ data: acceptedInvite
300
+ })]);
301
+ return orgUser;
260
302
  });
261
- return orgUser;
262
303
  }
263
304
  async getOrgUser(orgId, userId) {
264
305
  return this.repo.findOrgUser(orgId, userId);
@@ -297,23 +338,15 @@ var OrganizationServiceImpl = class {
297
338
  token,
298
339
  email
299
340
  })).toString()}`;
300
- try {
301
- if (role === "student") await this.mailer.send(email, "studentInvite", {
302
- inviteUrl,
303
- studentName: email
304
- });
305
- else await this.mailer.send(email, "memberInvite", {
306
- inviteUrl,
307
- inviterName: "Your team",
308
- role
309
- });
310
- } catch (err) {
311
- this.logger.error("failed to send invite email", {
312
- email,
313
- role,
314
- err: err instanceof Error ? err : new Error(String(err))
315
- });
316
- }
341
+ if (role === "student") await this.mailer.send(email, "studentInvite", {
342
+ inviteUrl,
343
+ studentName: email
344
+ });
345
+ else await this.mailer.send(email, "memberInvite", {
346
+ inviteUrl,
347
+ inviterName: "Your team",
348
+ role
349
+ });
317
350
  }
318
351
  async getByExternalId(externalId) {
319
352
  return this.repo.findByExternalId(externalId);
@@ -341,6 +374,7 @@ var OrganizationServiceImpl = class {
341
374
  });
342
375
  throw new OrganizationRuleError("Only active members can have their role changed");
343
376
  }
377
+ await this.orgAdmin().updateRole(ctx, member.userExternalId, role);
344
378
  const updated = await this.membersRepo.findById(ctx.orgId, id);
345
379
  this.logger.info("member role updated", {
346
380
  orgId: ctx.orgId,
@@ -349,7 +383,32 @@ var OrganizationServiceImpl = class {
349
383
  });
350
384
  return updated ? toMember(updated) : null;
351
385
  }
352
- async removeMember(_ctx, _id) {
386
+ async removeMember(ctx, id) {
387
+ const member = await this.membersRepo.findById(ctx.orgId, id);
388
+ if (!member) return false;
389
+ if (member.role === "owner") {
390
+ this.logger.warn("member removal rejected: owner cannot be removed", {
391
+ orgId: ctx.orgId,
392
+ memberId: id
393
+ });
394
+ throw new OrganizationRuleError("The owner cannot be removed");
395
+ }
396
+ if (member.kind === "member" && member.userExternalId) await this.orgAdmin().removeMember(ctx, member.userExternalId);
397
+ else if (member.kind === "invite" && member.inviteId) {
398
+ const inviteId = member.inviteId;
399
+ await this.uow.run(async ({ organizations, outbox }) => {
400
+ const canceled = await organizations.setInviteStatus(ctx.orgId, inviteId, "canceled");
401
+ if (!canceled) throw new NotFoundError("Invite", inviteId);
402
+ await outbox.append([organizationEvents.inviteCanceled.make({
403
+ orgId: ctx.orgId,
404
+ data: canceled
405
+ })]);
406
+ });
407
+ }
408
+ this.logger.info("member removed", {
409
+ orgId: ctx.orgId,
410
+ memberId: id
411
+ });
353
412
  return true;
354
413
  }
355
414
  };
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","names":[],"sources":["../../src/organizations/service.ts"],"sourcesContent":["// organizations context — service implementation (inbound port).\nimport type {\n MemberRecord,\n MembersRepository,\n MemberWriteContext,\n OrganizationService,\n OrganizationsRepository,\n OrganizationsUnitOfWork,\n} from './ports.js';\nimport type { Invite, Organization, OrgUser } from './model.js';\nimport { type Role, STUDENT_ROLE } from './roles.js';\nimport {\n InviteError,\n type Member,\n type MembersQuery,\n OrganizationRuleError,\n type Page,\n} from './members.js';\nimport type {\n AcceptInviteInput,\n AddOrgUserInput,\n CreateInviteInput,\n CreateOrganizationInput,\n ResendStudentInviteInput,\n UpdateOrganizationInput,\n UpdateStudentInput,\n} from './types.js';\nimport { organizationEvents, type OrganizationEvent } from './events.js';\nimport type { Logger, NewDomainEvent } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Mailer } from '../shared/mailer.js';\nimport { generateInviteToken, hashInviteToken } from '../shared/invite-token.js';\nimport { NotFoundError } from '../shared/errors.js';\nimport type { IdentityService } from '../identity/index.js';\nimport { genId } from '../shared/id.js';\n\nconst INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n\ntype NewOrganizationEvent = NewDomainEvent<OrganizationEvent>;\n\nexport interface InviteUrls {\n studentPortalUrl: string;\n adminAppUrl: string;\n}\n\nfunction toMember(r: MemberRecord): Member {\n return {\n id: r.id,\n firstName: r.firstName,\n lastName: r.lastName,\n email: r.email,\n image: r.image,\n role: r.role,\n status: r.status,\n joinedAt: r.joinedAt,\n invitedAt: r.invitedAt,\n };\n}\n\nexport type OrganizationServiceParams = {\n repo: OrganizationsRepository;\n membersRepo: MembersRepository;\n people: IdentityService;\n uow: OrganizationsUnitOfWork;\n logger?: Logger;\n mailer?: Pick<Mailer, 'send'>;\n inviteUrls?: InviteUrls;\n};\n\nexport class OrganizationServiceImpl implements OrganizationService {\n private readonly repo: OrganizationsRepository;\n private readonly membersRepo: MembersRepository;\n private readonly people: IdentityService;\n private readonly uow: OrganizationsUnitOfWork;\n private readonly logger: Logger;\n private readonly mailer?: Pick<Mailer, 'send'>;\n private readonly inviteUrls?: InviteUrls;\n\n constructor(params: OrganizationServiceParams) {\n this.repo = params.repo;\n this.membersRepo = params.membersRepo;\n this.people = params.people;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n this.mailer = params.mailer;\n this.inviteUrls = params.inviteUrls;\n }\n\n async createOrganization(input: CreateOrganizationInput): Promise<Organization> {\n const created = await this.uow.run(async ({ organizations, outbox }) => {\n const id = genId('organization');\n\n const created = await organizations.create({\n id,\n // The auth provider adopts this id for its own org row, so the org is\n // its own external id — a lookup by either id lands on the same row.\n externalId: input.externalId ?? id,\n name: input.name ?? `My Org`,\n slug: input.slug ?? '',\n ownerId: input.ownerId,\n });\n await outbox.append([\n organizationEvents.organizationCreated.make({\n orgId: created.id,\n data: created,\n }),\n ]);\n return created;\n });\n this.logger.info('organization created', { orgId: created.id, slug: created.slug });\n return created;\n }\n\n async updateOrganization(id: string, input: UpdateOrganizationInput): Promise<Organization> {\n const updatedOrg = await this.uow.run(async ({ organizations, outbox }) => {\n const updated = await organizations.update(id, {\n name: input.name,\n slug: input.slug,\n });\n if (!updated) {\n throw new NotFoundError('Organization', id);\n }\n await outbox.append([\n organizationEvents.organizationUpdated.make({ orgId: id, data: updated }),\n ]);\n return updated;\n });\n\n this.logger.info('organization updated', { updatedOrg });\n return updatedOrg!;\n }\n\n async deleteOrganization(id: string): Promise<Organization> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const deleted = await organizations.delete(id);\n\n await outbox.append([\n organizationEvents.organizationDeleted.make({\n orgId: deleted.id,\n data: deleted,\n }),\n ]);\n return deleted;\n });\n this.logger.info('organization deleted', { orgId: deleted.id });\n return deleted;\n }\n\n async addOrgUser({ orgId, userId, role }: AddOrgUserInput): Promise<OrgUser> {\n const orgUser = await this.uow.run(async ({ organizations, outbox }) => {\n const created = await organizations.createOrgUser({ orgId, role, userId, status: 'active' });\n await outbox.append([\n organizationEvents.orgUserLinked.make({ orgId, data: created }),\n ]);\n return created;\n });\n this.logger.info('user added to organization', { orgId, userId, role });\n return orgUser;\n }\n\n async removeOrgUser(orgId: string, id: string): Promise<OrgUser> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const deletedUser = await organizations.deleteOrgUser(orgId, id);\n if (!deletedUser) {\n throw new NotFoundError('OrgUser', id);\n }\n await outbox.append([\n organizationEvents.orgUserDeleted.make({\n orgId,\n data: deletedUser,\n }),\n ]);\n return deletedUser;\n });\n this.logger.info('organization deleted', { orgId: deleted.id });\n return deleted;\n }\n\n async createInvite(input: CreateInviteInput): Promise<Invite> {\n const { orgId, email, role, inviterUserId, firstName, lastName } = input;\n const sendEmail = input.sendEmail ?? true;\n const { token, tokenHash } = generateInviteToken();\n const expiresAt = new Date(Date.now() + INVITE_TTL_MS);\n\n // A student belongs to the org from the moment an admin adds them, not from\n // the moment they click a link — so the person exists before the invite\n // does. Staff are excluded: their org_users row is mirrored from the auth\n // provider's member record, which does not exist until they join.\n const person =\n role === STUDENT_ROLE\n ? await this.people.createUser({\n email,\n ...(firstName !== undefined && { firstName }),\n ...(lastName !== undefined && { lastName }),\n })\n : null;\n\n const invite = await this.uow.run(async ({ organizations, outbox }) => {\n const row = await organizations.upsertPendingInvite(orgId, {\n email,\n role,\n invitedBy: inviterUserId,\n tokenHash,\n expiresAt,\n });\n const events: NewOrganizationEvent[] = [\n organizationEvents.inviteCreated.make({ orgId, data: row }),\n ];\n if (person) {\n // Idempotent: re-inviting an address rotates the token without\n // producing a second org user or a second student.created.\n const { orgUser, created } = await organizations.ensureOrgUser({\n orgId,\n userId: person.id,\n role: STUDENT_ROLE,\n status: 'invited',\n });\n if (created) {\n events.push(\n organizationEvents.studentCreated.make({ orgId, data: orgUser }),\n );\n }\n }\n await outbox.append(events);\n return row;\n });\n\n if (sendEmail) {\n // TODO make this workflow durable\n await this.sendInviteEmail(invite, token);\n }\n\n this.logger.info('invite created', { orgId, inviteId: invite.id, role, sendEmail });\n return invite;\n }\n\n async updateStudent(input: UpdateStudentInput): Promise<OrgUser> {\n const { orgId, orgUserId, firstName, lastName, email } = input;\n const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);\n if (!orgUser || orgUser.role !== STUDENT_ROLE) {\n throw new NotFoundError('Student', orgUserId);\n }\n const person = await this.people.getUserById(orgUser.userId);\n if (!person) {\n throw new NotFoundError('User', orgUser.userId);\n }\n\n const previousEmail = person.email;\n await this.people.updateUser(person.id, {\n ...(firstName !== undefined && { firstName }),\n ...(lastName !== undefined && { lastName }),\n ...(email !== undefined && { email }),\n });\n\n // The pending invite carries its own copy of the address and a token minted\n // against it. Once the address is corrected that invite points at the wrong\n // person, so it is cancelled rather than left live — the admin re-sends,\n // which mints a fresh one for the new address. A student who has already\n // accepted has no pending invite to worry about.\n const emailChanged = email !== undefined && email.toLowerCase() !== previousEmail.toLowerCase();\n if (emailChanged && orgUser.status !== 'active') {\n await this.repo.cancelPendingInvite(orgId, previousEmail);\n }\n\n this.logger.info('student updated', { orgId, orgUserId, emailChanged });\n return orgUser;\n }\n\n async resendStudentInvite(input: ResendStudentInviteInput): Promise<void> {\n const { orgId, orgUserId, inviterUserId } = input;\n const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);\n if (!orgUser || orgUser.role !== STUDENT_ROLE) {\n throw new NotFoundError('Student', orgUserId);\n }\n if (orgUser.status === 'active') {\n throw new OrganizationRuleError('That student has already accepted their invitation');\n }\n const person = await this.people.getUserById(orgUser.userId);\n if (!person) {\n throw new NotFoundError('User', orgUser.userId);\n }\n // Straight back through createInvite: it rotates the token on the pending\n // row and finds the existing org user, so a resend is an invite that\n // happens to be the second one.\n await this.createInvite({\n orgId,\n email: person.email,\n role: STUDENT_ROLE,\n inviterUserId,\n sendEmail: true,\n });\n this.logger.info('student invite resent', { orgId, orgUserId });\n }\n\n async peekInvite(token: string): Promise<Invite | null> {\n const invite = await this.repo.findInviteByTokenHash(hashInviteToken(token));\n if (!invite || invite.status !== 'pending') {\n return null;\n }\n if (invite.expiresAt && invite.expiresAt < new Date()) {\n return null;\n }\n return invite;\n }\n\n async acceptInvite(input: AcceptInviteInput): Promise<OrgUser> {\n const invite = await this.peekInvite(input.token);\n if (!invite) {\n this.logger.warn('invite accept refused: token invalid or expired');\n throw new InviteError('invalid invite');\n }\n if (invite.email.toLowerCase() !== input.email.toLowerCase()) {\n this.logger.warn('invite accept refused: email mismatch', {\n orgId: invite.orgId,\n inviteId: invite.id,\n });\n throw new InviteError('emails must match');\n }\n const org = await this.repo.findById(invite.orgId);\n if (!org) {\n throw new InviteError('Org not found');\n }\n\n const orgUser = await this.uow.run(async ({ organizations, outbox }) => {\n // A student provisioned at invite time already has a row — acceptance\n // activates it. Staff have none yet, so this inserts one.\n const { orgUser: row, created } = await organizations.ensureOrgUser({\n orgId: invite.orgId,\n userId: input.userId,\n role: invite.role,\n status: 'active',\n });\n const acceptedInvite = await organizations.setInviteStatus(invite.orgId, invite.id, 'accepted');\n if (!acceptedInvite) {\n throw new NotFoundError('Invite', invite.id);\n }\n const events: NewOrganizationEvent[] = [\n organizationEvents.inviteAccepted.make({\n orgId: invite.orgId,\n data: acceptedInvite,\n }),\n ];\n if (row.role === STUDENT_ROLE) {\n events.push(\n created\n ? organizationEvents.studentCreated.make({\n orgId: invite.orgId,\n data: row,\n })\n : organizationEvents.studentLinked.make({\n orgId: invite.orgId,\n data: row,\n }),\n );\n } else if (created) {\n events.push(\n organizationEvents.orgUserLinked.make({\n orgId: invite.orgId,\n data: row,\n }),\n );\n }\n await outbox.append(events);\n return row;\n });\n this.logger.info('invite accepted', { orgId: invite.orgId, userId: input.userId });\n return orgUser;\n }\n\n // By identity USER id — `(org_id, user_id)` is unique. Every caller (scope\n // resolution, the MCP principal, the learn routes) asks \"what is this person\n // in this org\", never \"what is this org_users row\".\n async getOrgUser(orgId: string, userId: string): Promise<OrgUser | null> {\n return this.repo.findOrgUser(orgId, userId);\n }\n\n async deleteOrgUser(orgId: string, id: string): Promise<OrgUser> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const orgUser = await organizations.findOrgUserById(orgId, id);\n if (!orgUser) {\n throw new NotFoundError('OrgUser', id);\n }\n const deletedUser = await organizations.deleteOrgUser(orgId, id);\n if (!deletedUser) {\n throw new NotFoundError('OrgUser', id);\n }\n const events: NewOrganizationEvent[] = [];\n if (deletedUser.role === STUDENT_ROLE) {\n events.push(\n organizationEvents.studentDeleted.make({ orgId, data: orgUser }),\n );\n }\n\n await outbox.append(events);\n return deletedUser;\n });\n this.logger.info('org user deleted', { orgId, orgUserId: id });\n return deleted;\n }\n\n async getOrgUsersForUser(userId: string): Promise<OrgUser[]> {\n return this.repo.findOrgUsersByUser(userId);\n }\n\n async getById(id: string): Promise<Organization | null> {\n return this.repo.findById(id);\n }\n\n private async sendInviteEmail(invite: Invite, token: string): Promise<void> {\n if (!this.mailer || !this.inviteUrls) {\n throw new Error('invite delivery is not configured (mailer / invite urls missing)');\n }\n const { email, role } = invite;\n const isStudent = role === STUDENT_ROLE;\n const base = isStudent\n ? `${this.inviteUrls.studentPortalUrl}/welcome`\n : `${this.inviteUrls.adminAppUrl}/invite`;\n // The portal resolves the token to the invited email, so the link carries\n // only the token. The admin app still reads it from the query.\n const query = isStudent\n ? new URLSearchParams({ token })\n : new URLSearchParams({ token, email });\n const inviteUrl = `${base}?${query.toString()}`;\n try {\n if (role === STUDENT_ROLE) {\n await this.mailer.send(email, 'studentInvite', { inviteUrl, studentName: email });\n } else {\n await this.mailer.send(email, 'memberInvite', {\n inviteUrl,\n inviterName: 'Your team',\n role,\n });\n }\n } catch (err) {\n // A failed email must not abort invite creation: the token is already\n // minted and recorded, so the admin can fix transport and resend.\n this.logger.error('failed to send invite email', {\n email,\n role,\n err: err instanceof Error ? err : new Error(String(err)),\n });\n }\n }\n\n async getByExternalId(externalId: string): Promise<Organization | null> {\n return this.repo.findByExternalId(externalId);\n }\n\n async getBySlug(slug: string): Promise<Organization | null> {\n return this.repo.findBySlug(slug);\n }\n\n listMembers(orgId: string, query: MembersQuery): Promise<Page<Member>> {\n return this.membersRepo.list(orgId, query);\n }\n\n async updateMemberRole(ctx: MemberWriteContext, id: string, role: Role): Promise<Member | null> {\n const member = await this.membersRepo.findById(ctx.orgId, id);\n if (!member) {\n return null;\n }\n if (member.role === 'owner') {\n this.logger.warn('role change rejected: owner role immutable', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('The owner role cannot be reassigned');\n }\n if (member.kind !== 'member' || !member.userExternalId) {\n this.logger.warn('role change rejected: not an active member', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('Only active members can have their role changed');\n }\n\n const updated = await this.membersRepo.findById(ctx.orgId, id);\n this.logger.info('member role updated', { orgId: ctx.orgId, memberId: id, role });\n return updated ? toMember(updated) : null;\n }\n\n async removeMember(_ctx: MemberWriteContext, _id: string): Promise<boolean> {\n return true;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"service.js","names":[],"sources":["../../src/organizations/service.ts"],"sourcesContent":["// organizations context — service implementation (inbound port).\nimport type {\n MemberRecord,\n MembersRepository,\n MemberWriteContext,\n OrgAdmin,\n OrganizationService,\n OrganizationsRepository,\n OrganizationsUnitOfWork,\n} from './ports.js';\nimport type { Invite, Organization, OrgUser } from './model.js';\nimport { type Role, STUDENT_ROLE } from './roles.js';\nimport {\n InviteError,\n type Member,\n type MembersQuery,\n OrganizationRuleError,\n type Page,\n} from './members.js';\nimport type {\n AcceptInviteInput,\n AddOrgUserInput,\n CreateInviteInput,\n CreateOrganizationInput,\n ResendStudentInviteInput,\n UpdateOrganizationInput,\n UpdateOrgUserInput,\n UpdateStudentInput,\n} from './types.js';\nimport { organizationEvents, type OrganizationEvent } from './events.js';\nimport type { Logger, NewDomainEvent } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Mailer } from '../shared/mailer.js';\nimport { generateInviteToken, hashInviteToken } from '../shared/invite-token.js';\nimport { NotFoundError } from '../shared/errors.js';\nimport type { IdentityService } from '../identity/index.js';\nimport { genId } from '../shared/id.js';\n\nconst INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n\ntype NewOrganizationEvent = NewDomainEvent<OrganizationEvent>;\n\nexport interface InviteUrls {\n studentPortalUrl: string;\n adminAppUrl: string;\n}\n\nfunction toMember(r: MemberRecord): Member {\n return {\n id: r.id,\n firstName: r.firstName,\n lastName: r.lastName,\n email: r.email,\n image: r.image,\n role: r.role,\n status: r.status,\n joinedAt: r.joinedAt,\n invitedAt: r.invitedAt,\n };\n}\n\nexport type OrganizationServiceParams = {\n repo: OrganizationsRepository;\n membersRepo: MembersRepository;\n people: IdentityService;\n uow: OrganizationsUnitOfWork;\n orgAdmin: () => OrgAdmin;\n logger?: Logger;\n mailer?: Pick<Mailer, 'send'>;\n inviteUrls?: InviteUrls;\n};\n\nexport class OrganizationServiceImpl implements OrganizationService {\n private readonly repo: OrganizationsRepository;\n private readonly membersRepo: MembersRepository;\n private readonly people: IdentityService;\n private readonly uow: OrganizationsUnitOfWork;\n private readonly orgAdmin: () => OrgAdmin;\n private readonly logger: Logger;\n private readonly mailer?: Pick<Mailer, 'send'>;\n private readonly inviteUrls?: InviteUrls;\n\n constructor(params: OrganizationServiceParams) {\n this.repo = params.repo;\n this.membersRepo = params.membersRepo;\n this.people = params.people;\n this.uow = params.uow;\n this.orgAdmin = params.orgAdmin;\n this.logger = params.logger ?? noopLogger;\n this.mailer = params.mailer;\n this.inviteUrls = params.inviteUrls;\n }\n\n async createOrganization(input: CreateOrganizationInput): Promise<Organization> {\n const created = await this.uow.run(async ({ organizations, outbox }) => {\n const id = genId('organization');\n\n const created = await organizations.create({\n id,\n // The auth provider adopts this id for its own org row, so the org is\n // its own external id — a lookup by either id lands on the same row.\n externalId: input.externalId ?? id,\n name: input.name ?? `My Org`,\n slug: input.slug ?? '',\n ownerId: input.ownerId,\n });\n await outbox.append([\n organizationEvents.organizationCreated.make({\n orgId: created.id,\n data: created,\n }),\n ]);\n return created;\n });\n this.logger.info('organization created', { orgId: created.id, slug: created.slug });\n return created;\n }\n\n async updateOrganization(id: string, input: UpdateOrganizationInput): Promise<Organization> {\n const updatedOrg = await this.uow.run(async ({ organizations, outbox }) => {\n const updated = await organizations.update(id, {\n name: input.name,\n slug: input.slug,\n });\n if (!updated) {\n throw new NotFoundError('Organization', id);\n }\n await outbox.append([\n organizationEvents.organizationUpdated.make({ orgId: id, data: updated }),\n ]);\n return updated;\n });\n\n this.logger.info('organization updated', { updatedOrg });\n return updatedOrg!;\n }\n\n async deleteOrganization(id: string): Promise<Organization> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const deleted = await organizations.delete(id);\n\n await outbox.append([\n organizationEvents.organizationDeleted.make({\n orgId: deleted.id,\n data: deleted,\n }),\n ]);\n return deleted;\n });\n this.logger.info('organization deleted', { orgId: deleted.id });\n return deleted;\n }\n\n async addOrgUser({ orgId, userId, role }: AddOrgUserInput): Promise<OrgUser> {\n const orgUser = await this.uow.run(async ({ organizations, outbox }) => {\n const created = await organizations.createOrgUser({ orgId, role, userId, status: 'active' });\n await outbox.append([organizationEvents.orgUserLinked.make({ orgId, data: created })]);\n return created;\n });\n this.logger.info('user added to organization', { orgId, userId, role });\n return orgUser;\n }\n\n async removeOrgUser(orgId: string, userId: string): Promise<OrgUser> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const orgUser = await organizations.findOrgUser(orgId, userId);\n if (!orgUser) {\n throw new NotFoundError('OrgUser', userId);\n }\n const deletedUser = await organizations.deleteOrgUser(orgId, orgUser.id);\n if (!deletedUser) {\n throw new NotFoundError('OrgUser', orgUser.id);\n }\n await outbox.append([\n organizationEvents.orgUserDeleted.make({\n orgId,\n data: deletedUser,\n }),\n ]);\n return deletedUser;\n });\n this.logger.info('org user removed', { orgId, userId, orgUserId: deleted.id });\n return deleted;\n }\n\n async updateOrgUser(orgId: string, userId: string, patch: UpdateOrgUserInput): Promise<OrgUser> {\n const orgUser = await this.repo.findOrgUser(orgId, userId);\n if (!orgUser) {\n throw new NotFoundError('OrgUser', userId);\n }\n const updated = await this.repo.updateOrgUser(orgId, orgUser.id, patch);\n if (!updated) {\n throw new NotFoundError('OrgUser', orgUser.id);\n }\n this.logger.info('org user updated', { orgId, userId, orgUserId: updated.id, patch });\n return updated;\n }\n\n async createInvite(input: CreateInviteInput): Promise<Invite> {\n const { orgId, email, role, inviterUserId, firstName, lastName } = input;\n const sendEmail = input.sendEmail ?? true;\n const { token, tokenHash } = generateInviteToken();\n const expiresAt = new Date(Date.now() + INVITE_TTL_MS);\n\n // A student belongs to the org from the moment an admin adds them, not from\n // the moment they click a link — so the person exists before the invite\n // does. Staff are excluded: their org_users row is mirrored from the auth\n // provider's member record, which does not exist until they join.\n const person =\n role === STUDENT_ROLE\n ? ((await this.people.getUserByEmail(email)) ??\n (await this.people.createUser({\n email,\n ...(firstName !== undefined && { firstName }),\n ...(lastName !== undefined && { lastName }),\n })))\n : null;\n\n const invite = await this.uow.run(async ({ organizations, outbox }) => {\n const events: NewOrganizationEvent[] = [];\n if (person) {\n const orgUser = await organizations.createOrgUser({\n orgId,\n userId: person.id,\n role: STUDENT_ROLE,\n status: 'invited',\n });\n events.push(organizationEvents.studentCreated.make({ orgId, data: orgUser }));\n }\n const row = await organizations.upsertPendingInvite(orgId, {\n email,\n role,\n invitedBy: inviterUserId,\n tokenHash,\n expiresAt,\n });\n events.push(organizationEvents.inviteCreated.make({ orgId, data: row }));\n await outbox.append(events);\n return row;\n });\n\n if (sendEmail) {\n // TODO make this workflow durable\n try {\n await this.sendInviteEmail(invite, token);\n } catch (err) {\n // A failed email must not abort invite creation: the token is already\n // minted and recorded, so the admin can fix transport and resend.\n this.logger.error('failed to send invite email', {\n email: invite.email,\n role: invite.role,\n err: err instanceof Error ? err : new Error(String(err)),\n });\n }\n }\n\n this.logger.info('invite created', { orgId, inviteId: invite.id, role, sendEmail });\n return invite;\n }\n\n async updateStudent(input: UpdateStudentInput): Promise<OrgUser> {\n const { orgId, orgUserId, firstName, lastName, email } = input;\n const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);\n if (!orgUser || orgUser.role !== STUDENT_ROLE) {\n throw new NotFoundError('Student', orgUserId);\n }\n const person = await this.people.getUserById(orgUser.userId);\n if (!person) {\n throw new NotFoundError('User', orgUser.userId);\n }\n\n const previousEmail = person.email;\n await this.people.updateUser(person.id, {\n ...(firstName !== undefined && { firstName }),\n ...(lastName !== undefined && { lastName }),\n ...(email !== undefined && { email }),\n });\n\n // The pending invite carries its own copy of the address and a token minted\n // against it. Once the address is corrected that invite points at the wrong\n // person, so it is cancelled rather than left live — the admin re-sends,\n // which mints a fresh one for the new address. A student who has already\n // accepted has no pending invite to worry about.\n const emailChanged = email !== undefined && email.toLowerCase() !== previousEmail.toLowerCase();\n if (emailChanged && orgUser.status !== 'active') {\n await this.repo.cancelPendingInvite(orgId, previousEmail);\n }\n\n this.logger.info('student updated', { orgId, orgUserId, emailChanged });\n return orgUser;\n }\n\n async resendStudentInvite(input: ResendStudentInviteInput): Promise<void> {\n const { orgId, orgUserId, inviterUserId } = input;\n const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);\n if (!orgUser || orgUser.role !== STUDENT_ROLE) {\n throw new NotFoundError('Student', orgUserId);\n }\n if (orgUser.status === 'active') {\n throw new OrganizationRuleError('That student has already accepted their invitation');\n }\n const person = await this.people.getUserById(orgUser.userId);\n if (!person) {\n throw new NotFoundError('User', orgUser.userId);\n }\n const pending = await this.repo.findPendingInvite(orgId, person.email);\n if (!pending) {\n throw new NotFoundError('Invite', person.email);\n }\n const { token, tokenHash } = generateInviteToken();\n const expiresAt = new Date(Date.now() + INVITE_TTL_MS);\n const invite = await this.repo.setInviteToken(orgId, pending.id, tokenHash, expiresAt);\n if (!invite) {\n throw new NotFoundError('Invite', pending.id);\n }\n await this.sendInviteEmail(invite, token);\n this.logger.info('student invite resent', {\n orgId,\n orgUserId,\n inviteId: invite.id,\n inviterUserId,\n });\n }\n\n async peekInvite(token: string): Promise<Invite | null> {\n const invite = await this.repo.findInviteByTokenHash(hashInviteToken(token));\n if (!invite || invite.status !== 'pending') {\n return null;\n }\n if (invite.expiresAt && invite.expiresAt < new Date()) {\n return null;\n }\n return invite;\n }\n\n async acceptInvite(input: AcceptInviteInput): Promise<OrgUser> {\n const invite = await this.peekInvite(input.token);\n if (!invite) {\n this.logger.warn('invite accept refused: token invalid or expired');\n throw new InviteError('invalid invite');\n }\n if (invite.email.toLowerCase() !== input.email.toLowerCase()) {\n this.logger.warn('invite accept refused: email mismatch', {\n orgId: invite.orgId,\n inviteId: invite.id,\n });\n throw new InviteError('emails must match');\n }\n const org = await this.repo.findById(invite.orgId);\n if (!org) {\n throw new InviteError('Org not found');\n }\n\n const orgUser =\n invite.role === STUDENT_ROLE\n ? await this.acceptStudentInvite(invite, input.userId)\n : await this.acceptStaffInvite(invite, org, input.userId);\n this.logger.info('invite accepted', { orgId: invite.orgId, userId: input.userId });\n return orgUser;\n }\n\n private async acceptStudentInvite(invite: Invite, userId: string): Promise<OrgUser> {\n return this.uow.run(async ({ organizations, outbox }) => {\n const existing = await organizations.findOrgUser(invite.orgId, userId);\n if (!existing) {\n throw new NotFoundError('OrgUser', userId);\n }\n const activated = await organizations.updateOrgUser(invite.orgId, existing.id, {\n status: 'active',\n });\n if (!activated) {\n throw new NotFoundError('OrgUser', existing.id);\n }\n const acceptedInvite = await organizations.setInviteStatus(\n invite.orgId,\n invite.id,\n 'accepted',\n );\n if (!acceptedInvite) {\n throw new NotFoundError('Invite', invite.id);\n }\n await outbox.append([\n organizationEvents.inviteAccepted.make({ orgId: invite.orgId, data: acceptedInvite }),\n organizationEvents.studentLinked.make({ orgId: invite.orgId, data: activated }),\n ]);\n return activated;\n });\n }\n\n private async acceptStaffInvite(\n invite: Invite,\n org: Organization,\n userId: string,\n ): Promise<OrgUser> {\n const person = await this.people.getUserById(userId);\n if (!person?.externalId) {\n throw new NotFoundError('User', userId);\n }\n await this.orgAdmin().grantMembership(org.externalId ?? org.id, person.externalId, invite.role);\n return this.uow.run(async ({ organizations, outbox }) => {\n const orgUser = await organizations.findOrgUser(invite.orgId, userId);\n if (!orgUser) {\n throw new NotFoundError('OrgUser', userId);\n }\n const acceptedInvite = await organizations.setInviteStatus(\n invite.orgId,\n invite.id,\n 'accepted',\n );\n if (!acceptedInvite) {\n throw new NotFoundError('Invite', invite.id);\n }\n await outbox.append([\n organizationEvents.inviteAccepted.make({ orgId: invite.orgId, data: acceptedInvite }),\n ]);\n return orgUser;\n });\n }\n\n // By identity USER id — `(org_id, user_id)` is unique. Every caller (scope\n // resolution, the MCP principal, the learn routes) asks \"what is this person\n // in this org\", never \"what is this org_users row\".\n async getOrgUser(orgId: string, userId: string): Promise<OrgUser | null> {\n return this.repo.findOrgUser(orgId, userId);\n }\n\n async deleteOrgUser(orgId: string, id: string): Promise<OrgUser> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const orgUser = await organizations.findOrgUserById(orgId, id);\n if (!orgUser) {\n throw new NotFoundError('OrgUser', id);\n }\n const deletedUser = await organizations.deleteOrgUser(orgId, id);\n if (!deletedUser) {\n throw new NotFoundError('OrgUser', id);\n }\n const events: NewOrganizationEvent[] = [];\n if (deletedUser.role === STUDENT_ROLE) {\n events.push(organizationEvents.studentDeleted.make({ orgId, data: orgUser }));\n }\n\n await outbox.append(events);\n return deletedUser;\n });\n this.logger.info('org user deleted', { orgId, orgUserId: id });\n return deleted;\n }\n\n async getOrgUsersForUser(userId: string): Promise<OrgUser[]> {\n return this.repo.findOrgUsersByUser(userId);\n }\n\n async getById(id: string): Promise<Organization | null> {\n return this.repo.findById(id);\n }\n\n private async sendInviteEmail(invite: Invite, token: string): Promise<void> {\n if (!this.mailer || !this.inviteUrls) {\n throw new Error('invite delivery is not configured (mailer / invite urls missing)');\n }\n const { email, role } = invite;\n const isStudent = role === STUDENT_ROLE;\n const base = isStudent\n ? `${this.inviteUrls.studentPortalUrl}/welcome`\n : `${this.inviteUrls.adminAppUrl}/invite`;\n // The portal resolves the token to the invited email, so the link carries\n // only the token. The admin app still reads it from the query.\n const query = isStudent\n ? new URLSearchParams({ token })\n : new URLSearchParams({ token, email });\n const inviteUrl = `${base}?${query.toString()}`;\n if (role === STUDENT_ROLE) {\n await this.mailer.send(email, 'studentInvite', { inviteUrl, studentName: email });\n } else {\n await this.mailer.send(email, 'memberInvite', {\n inviteUrl,\n inviterName: 'Your team',\n role,\n });\n }\n }\n\n async getByExternalId(externalId: string): Promise<Organization | null> {\n return this.repo.findByExternalId(externalId);\n }\n\n async getBySlug(slug: string): Promise<Organization | null> {\n return this.repo.findBySlug(slug);\n }\n\n listMembers(orgId: string, query: MembersQuery): Promise<Page<Member>> {\n return this.membersRepo.list(orgId, query);\n }\n\n async updateMemberRole(ctx: MemberWriteContext, id: string, role: Role): Promise<Member | null> {\n const member = await this.membersRepo.findById(ctx.orgId, id);\n if (!member) {\n return null;\n }\n if (member.role === 'owner') {\n this.logger.warn('role change rejected: owner role immutable', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('The owner role cannot be reassigned');\n }\n if (member.kind !== 'member' || !member.userExternalId) {\n this.logger.warn('role change rejected: not an active member', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('Only active members can have their role changed');\n }\n await this.orgAdmin().updateRole(ctx, member.userExternalId, role);\n const updated = await this.membersRepo.findById(ctx.orgId, id);\n this.logger.info('member role updated', { orgId: ctx.orgId, memberId: id, role });\n return updated ? toMember(updated) : null;\n }\n\n async removeMember(ctx: MemberWriteContext, id: string): Promise<boolean> {\n const member = await this.membersRepo.findById(ctx.orgId, id);\n if (!member) {\n return false;\n }\n if (member.role === 'owner') {\n this.logger.warn('member removal rejected: owner cannot be removed', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('The owner cannot be removed');\n }\n if (member.kind === 'member' && member.userExternalId) {\n await this.orgAdmin().removeMember(ctx, member.userExternalId);\n } else if (member.kind === 'invite' && member.inviteId) {\n const inviteId = member.inviteId;\n await this.uow.run(async ({ organizations, outbox }) => {\n const canceled = await organizations.setInviteStatus(ctx.orgId, inviteId, 'canceled');\n if (!canceled) {\n throw new NotFoundError('Invite', inviteId);\n }\n await outbox.append([\n organizationEvents.inviteCanceled.make({ orgId: ctx.orgId, data: canceled }),\n ]);\n });\n }\n this.logger.info('member removed', { orgId: ctx.orgId, memberId: id });\n return true;\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { ProvisionUserInput } from "../types/schemas/identity.js";
2
- import { AcceptInviteInput, AddOrgUserInput, CreateInviteInput, CreateOrgUserInput, InviteRole, NewOrganizationInput, OrgUserStatus, Organization } from "../types/schemas/organizations.js";
2
+ import { AcceptInviteInput, AddOrgUserInput, CreateInviteInput, CreateOrgUserInput, InviteRole, NewOrganizationInput, OrgUserStatus, Organization, UpdateOrgUserInput } from "../types/schemas/organizations.js";
3
3
  import { InviteId, OrgUserId, OrganizationId } from "../types/organizations.js";
4
4
  import "../types/index.js";
5
5
  //#region src/organizations/types.d.ts
@@ -35,5 +35,5 @@ interface ResendStudentInviteInput {
35
35
  inviterUserId: string;
36
36
  }
37
37
  //#endregion
38
- export { type AcceptInviteInput, type AddOrgUserInput, type CreateInviteInput, type CreateOrgUserInput, CreateOrganizationInput, type InviteId, type InviteRole, LinkOrgUserInput, type NewOrganizationInput, type OrgUserId, type OrgUserStatus, type OrganizationId, type ProvisionUserInput, ResendStudentInviteInput, UnlinkOrgUserInput, UpdateOrganizationInput, UpdatePersonInput, UpdateStudentInput };
38
+ export { type AcceptInviteInput, type AddOrgUserInput, type CreateInviteInput, type CreateOrgUserInput, CreateOrganizationInput, type InviteId, type InviteRole, LinkOrgUserInput, type NewOrganizationInput, type OrgUserId, type OrgUserStatus, type OrganizationId, type ProvisionUserInput, ResendStudentInviteInput, UnlinkOrgUserInput, type UpdateOrgUserInput, UpdateOrganizationInput, UpdatePersonInput, UpdateStudentInput };
39
39
  //# sourceMappingURL=types.d.ts.map