@varshylinc/team-management 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 (254) hide show
  1. package/dist/client/actions.d.ts +20 -0
  2. package/dist/client/actions.d.ts.map +1 -0
  3. package/dist/client/actions.js +23 -0
  4. package/dist/client/actions.js.map +1 -0
  5. package/dist/client/api.d.ts +74 -0
  6. package/dist/client/api.d.ts.map +1 -0
  7. package/dist/client/api.js +245 -0
  8. package/dist/client/api.js.map +1 -0
  9. package/dist/client/components/AddMemberForm.d.ts +12 -0
  10. package/dist/client/components/AddMemberForm.d.ts.map +1 -0
  11. package/dist/client/components/AddMemberForm.js +37 -0
  12. package/dist/client/components/AddMemberForm.js.map +1 -0
  13. package/dist/client/components/AuditEventRow.d.ts +7 -0
  14. package/dist/client/components/AuditEventRow.d.ts.map +1 -0
  15. package/dist/client/components/AuditEventRow.js +20 -0
  16. package/dist/client/components/AuditEventRow.js.map +1 -0
  17. package/dist/client/components/CascadePreview.d.ts +11 -0
  18. package/dist/client/components/CascadePreview.d.ts.map +1 -0
  19. package/dist/client/components/CascadePreview.js +7 -0
  20. package/dist/client/components/CascadePreview.js.map +1 -0
  21. package/dist/client/components/DangerZoneCard.d.ts +10 -0
  22. package/dist/client/components/DangerZoneCard.d.ts.map +1 -0
  23. package/dist/client/components/DangerZoneCard.js +28 -0
  24. package/dist/client/components/DangerZoneCard.js.map +1 -0
  25. package/dist/client/components/InvitationCodeDisplay.d.ts +7 -0
  26. package/dist/client/components/InvitationCodeDisplay.d.ts.map +1 -0
  27. package/dist/client/components/InvitationCodeDisplay.js +26 -0
  28. package/dist/client/components/InvitationCodeDisplay.js.map +1 -0
  29. package/dist/client/components/InviteForm.d.ts +7 -0
  30. package/dist/client/components/InviteForm.d.ts.map +1 -0
  31. package/dist/client/components/InviteForm.js +35 -0
  32. package/dist/client/components/InviteForm.js.map +1 -0
  33. package/dist/client/components/MemberRow.d.ts +10 -0
  34. package/dist/client/components/MemberRow.d.ts.map +1 -0
  35. package/dist/client/components/MemberRow.js +17 -0
  36. package/dist/client/components/MemberRow.js.map +1 -0
  37. package/dist/client/components/OrgPeopleRoster.d.ts +11 -0
  38. package/dist/client/components/OrgPeopleRoster.d.ts.map +1 -0
  39. package/dist/client/components/OrgPeopleRoster.js +13 -0
  40. package/dist/client/components/OrgPeopleRoster.js.map +1 -0
  41. package/dist/client/components/PendingTransferBanner.d.ts +10 -0
  42. package/dist/client/components/PendingTransferBanner.d.ts.map +1 -0
  43. package/dist/client/components/PendingTransferBanner.js +48 -0
  44. package/dist/client/components/PendingTransferBanner.js.map +1 -0
  45. package/dist/client/components/PlaceholderCard.d.ts +7 -0
  46. package/dist/client/components/PlaceholderCard.d.ts.map +1 -0
  47. package/dist/client/components/PlaceholderCard.js +15 -0
  48. package/dist/client/components/PlaceholderCard.js.map +1 -0
  49. package/dist/client/components/RoleBadge.d.ts +5 -0
  50. package/dist/client/components/RoleBadge.d.ts.map +1 -0
  51. package/dist/client/components/RoleBadge.js +17 -0
  52. package/dist/client/components/RoleBadge.js.map +1 -0
  53. package/dist/client/components/RoleSelect.d.ts +10 -0
  54. package/dist/client/components/RoleSelect.d.ts.map +1 -0
  55. package/dist/client/components/RoleSelect.js +12 -0
  56. package/dist/client/components/RoleSelect.js.map +1 -0
  57. package/dist/client/components/SeatUsagePanel.d.ts +6 -0
  58. package/dist/client/components/SeatUsagePanel.d.ts.map +1 -0
  59. package/dist/client/components/SeatUsagePanel.js +13 -0
  60. package/dist/client/components/SeatUsagePanel.js.map +1 -0
  61. package/dist/client/hooks/useCurrentMembership.d.ts +8 -0
  62. package/dist/client/hooks/useCurrentMembership.d.ts.map +1 -0
  63. package/dist/client/hooks/useCurrentMembership.js +20 -0
  64. package/dist/client/hooks/useCurrentMembership.js.map +1 -0
  65. package/dist/client/hooks/useMembers.d.ts +10 -0
  66. package/dist/client/hooks/useMembers.d.ts.map +1 -0
  67. package/dist/client/hooks/useMembers.js +20 -0
  68. package/dist/client/hooks/useMembers.js.map +1 -0
  69. package/dist/client/hooks/useOrgMembers.d.ts +20 -0
  70. package/dist/client/hooks/useOrgMembers.d.ts.map +1 -0
  71. package/dist/client/hooks/useOrgMembers.js +63 -0
  72. package/dist/client/hooks/useOrgMembers.js.map +1 -0
  73. package/dist/client/hooks/usePendingInvitations.d.ts +8 -0
  74. package/dist/client/hooks/usePendingInvitations.d.ts.map +1 -0
  75. package/dist/client/hooks/usePendingInvitations.js +20 -0
  76. package/dist/client/hooks/usePendingInvitations.js.map +1 -0
  77. package/dist/client/hooks/usePendingTransfer.d.ts +8 -0
  78. package/dist/client/hooks/usePendingTransfer.d.ts.map +1 -0
  79. package/dist/client/hooks/usePendingTransfer.js +23 -0
  80. package/dist/client/hooks/usePendingTransfer.js.map +1 -0
  81. package/dist/client/index.d.ts +31 -0
  82. package/dist/client/index.d.ts.map +1 -0
  83. package/{src/client/index.ts → dist/client/index.js} +6 -54
  84. package/dist/client/index.js.map +1 -0
  85. package/dist/client/pages/AuditLogPage.d.ts +6 -0
  86. package/dist/client/pages/AuditLogPage.d.ts.map +1 -0
  87. package/dist/client/pages/AuditLogPage.js +51 -0
  88. package/dist/client/pages/AuditLogPage.js.map +1 -0
  89. package/dist/client/pages/EmailChangePage.d.ts +8 -0
  90. package/dist/client/pages/EmailChangePage.d.ts.map +1 -0
  91. package/dist/client/pages/EmailChangePage.js +52 -0
  92. package/dist/client/pages/EmailChangePage.js.map +1 -0
  93. package/dist/client/pages/InvitationAcceptPage.d.ts +11 -0
  94. package/dist/client/pages/InvitationAcceptPage.d.ts.map +1 -0
  95. package/dist/client/pages/InvitationAcceptPage.js +42 -0
  96. package/dist/client/pages/InvitationAcceptPage.js.map +1 -0
  97. package/dist/client/pages/InvitationCodePage.d.ts +6 -0
  98. package/dist/client/pages/InvitationCodePage.d.ts.map +1 -0
  99. package/dist/client/pages/InvitationCodePage.js +28 -0
  100. package/dist/client/pages/InvitationCodePage.js.map +1 -0
  101. package/dist/client/pages/MembersPage.d.ts +6 -0
  102. package/dist/client/pages/MembersPage.d.ts.map +1 -0
  103. package/dist/client/pages/MembersPage.js +67 -0
  104. package/dist/client/pages/MembersPage.js.map +1 -0
  105. package/dist/client/pages/OrgPeoplePage.d.ts +14 -0
  106. package/dist/client/pages/OrgPeoplePage.d.ts.map +1 -0
  107. package/dist/client/pages/OrgPeoplePage.js +40 -0
  108. package/dist/client/pages/OrgPeoplePage.js.map +1 -0
  109. package/dist/client/pages/OrgSettingsPage.d.ts +6 -0
  110. package/dist/client/pages/OrgSettingsPage.d.ts.map +1 -0
  111. package/dist/client/pages/OrgSettingsPage.js +78 -0
  112. package/dist/client/pages/OrgSettingsPage.js.map +1 -0
  113. package/dist/client/pages/OwnershipTransferPage.d.ts +6 -0
  114. package/dist/client/pages/OwnershipTransferPage.d.ts.map +1 -0
  115. package/dist/client/pages/OwnershipTransferPage.js +68 -0
  116. package/dist/client/pages/OwnershipTransferPage.js.map +1 -0
  117. package/dist/client/pages/PasswordResetPage.d.ts +6 -0
  118. package/dist/client/pages/PasswordResetPage.d.ts.map +1 -0
  119. package/dist/client/pages/PasswordResetPage.js +34 -0
  120. package/dist/client/pages/PasswordResetPage.js.map +1 -0
  121. package/dist/client/pages/PasswordResetRequestPage.d.ts +2 -0
  122. package/dist/client/pages/PasswordResetRequestPage.d.ts.map +1 -0
  123. package/dist/client/pages/PasswordResetRequestPage.js +27 -0
  124. package/dist/client/pages/PasswordResetRequestPage.js.map +1 -0
  125. package/dist/client/pages/PlaceholderPage.d.ts +7 -0
  126. package/dist/client/pages/PlaceholderPage.d.ts.map +1 -0
  127. package/dist/client/pages/PlaceholderPage.js +16 -0
  128. package/dist/client/pages/PlaceholderPage.js.map +1 -0
  129. package/dist/client/pages/SuperAdminDashboard.d.ts +2 -0
  130. package/dist/client/pages/SuperAdminDashboard.d.ts.map +1 -0
  131. package/dist/client/pages/SuperAdminDashboard.js +123 -0
  132. package/dist/client/pages/SuperAdminDashboard.js.map +1 -0
  133. package/dist/client/theme.d.ts +12 -0
  134. package/dist/client/theme.d.ts.map +1 -0
  135. package/dist/client/theme.js +16 -0
  136. package/dist/client/theme.js.map +1 -0
  137. package/dist/client/types.d.ts +78 -0
  138. package/dist/client/types.d.ts.map +1 -0
  139. package/dist/client/types.js +2 -0
  140. package/dist/client/types.js.map +1 -0
  141. package/dist/index.d.ts +3 -1
  142. package/dist/index.d.ts.map +1 -1
  143. package/dist/index.js +2 -1
  144. package/dist/index.js.map +1 -1
  145. package/dist/server/index.d.ts +2 -0
  146. package/dist/server/index.d.ts.map +1 -1
  147. package/dist/server/index.js +1 -0
  148. package/dist/server/index.js.map +1 -1
  149. package/dist/server/org-admin.d.ts +45 -0
  150. package/dist/server/org-admin.d.ts.map +1 -0
  151. package/dist/server/org-admin.js +63 -0
  152. package/dist/server/org-admin.js.map +1 -0
  153. package/dist/server/routes/orgs.routes.d.ts.map +1 -1
  154. package/dist/server/routes/orgs.routes.js +81 -12
  155. package/dist/server/routes/orgs.routes.js.map +1 -1
  156. package/dist/server/types.d.ts +2 -0
  157. package/dist/server/types.d.ts.map +1 -1
  158. package/dist/server/types.js.map +1 -1
  159. package/package.json +18 -11
  160. package/.eslintrc.cjs +0 -18
  161. package/CHANGELOG.md +0 -159
  162. package/src/client/api.ts +0 -314
  163. package/src/client/components/AuditEventRow.tsx +0 -59
  164. package/src/client/components/CascadePreview.tsx +0 -36
  165. package/src/client/components/DangerZoneCard.tsx +0 -103
  166. package/src/client/components/InvitationCodeDisplay.tsx +0 -48
  167. package/src/client/components/InviteForm.tsx +0 -77
  168. package/src/client/components/MemberRow.tsx +0 -69
  169. package/src/client/components/PendingTransferBanner.tsx +0 -98
  170. package/src/client/components/PlaceholderCard.tsx +0 -26
  171. package/src/client/components/RoleBadge.tsx +0 -26
  172. package/src/client/components/RoleSelect.tsx +0 -35
  173. package/src/client/hooks/.gitkeep +0 -0
  174. package/src/client/hooks/useCurrentMembership.ts +0 -24
  175. package/src/client/hooks/useMembers.ts +0 -24
  176. package/src/client/hooks/usePendingInvitations.ts +0 -24
  177. package/src/client/hooks/usePendingTransfer.ts +0 -27
  178. package/src/client/pages/AuditLogPage.tsx +0 -164
  179. package/src/client/pages/EmailChangePage.tsx +0 -144
  180. package/src/client/pages/InvitationAcceptPage.tsx +0 -163
  181. package/src/client/pages/InvitationCodePage.tsx +0 -108
  182. package/src/client/pages/MembersPage.tsx +0 -290
  183. package/src/client/pages/OrgSettingsPage.tsx +0 -185
  184. package/src/client/pages/OwnershipTransferPage.tsx +0 -163
  185. package/src/client/pages/PasswordResetPage.tsx +0 -104
  186. package/src/client/pages/PasswordResetRequestPage.tsx +0 -71
  187. package/src/client/pages/PlaceholderPage.tsx +0 -20
  188. package/src/client/pages/SuperAdminDashboard.tsx +0 -401
  189. package/src/client/types.ts +0 -78
  190. package/src/index.ts +0 -24
  191. package/src/server/crypto.ts +0 -47
  192. package/src/server/index.ts +0 -167
  193. package/src/server/middleware/require-membership.ts +0 -48
  194. package/src/server/middleware/require-role.ts +0 -19
  195. package/src/server/middleware/require-super-admin.ts +0 -32
  196. package/src/server/migrations/0001_create_tm_schema_migrations.sql +0 -13
  197. package/src/server/migrations/0002_create_tm_organizations.sql +0 -14
  198. package/src/server/migrations/0003_create_tm_memberships.sql +0 -24
  199. package/src/server/migrations/0004_create_tm_invitations.sql +0 -22
  200. package/src/server/migrations/0005_create_tm_audit_events.sql +0 -17
  201. package/src/server/migrations/0006_create_tm_email_change_requests.sql +0 -13
  202. package/src/server/migrations/0007_create_tm_ownership_transfers.sql +0 -22
  203. package/src/server/migrations/0008_create_tm_super_admins.sql +0 -8
  204. package/src/server/migrations/0009_create_tm_password_reset_requests.sql +0 -9
  205. package/src/server/migrations/0010_create_tm_shared_access.sql +0 -8
  206. package/src/server/migrations/0011_seed_super_admin.sql +0 -15
  207. package/src/server/migrations/0012_create_tm_user_locks.sql +0 -7
  208. package/src/server/routes/admin.routes.ts +0 -208
  209. package/src/server/routes/audit.routes.ts +0 -93
  210. package/src/server/routes/health.routes.ts +0 -46
  211. package/src/server/routes/invitations.routes.ts +0 -252
  212. package/src/server/routes/me.routes.ts +0 -143
  213. package/src/server/routes/orgs.routes.ts +0 -428
  214. package/src/server/routes/transfer.routes.ts +0 -110
  215. package/src/server/services/.gitkeep +0 -0
  216. package/src/server/services/audit.service.ts +0 -49
  217. package/src/server/services/email-change.service.ts +0 -178
  218. package/src/server/services/invitations.service.ts +0 -316
  219. package/src/server/services/memberships.service.ts +0 -129
  220. package/src/server/services/organizations.service.ts +0 -110
  221. package/src/server/services/ownership.service.ts +0 -170
  222. package/src/server/services/password-reset.service.ts +0 -94
  223. package/src/server/services/super-admin.service.ts +0 -321
  224. package/src/server/sql/.gitkeep +0 -0
  225. package/src/server/types.ts +0 -145
  226. package/src/shared/types.ts +0 -24
  227. package/tests/integration/audit-fires.test.ts +0 -288
  228. package/tests/integration/cascade-preview.test.ts +0 -157
  229. package/tests/integration/email-change.test.ts +0 -190
  230. package/tests/integration/feature-flags.test.ts +0 -213
  231. package/tests/integration/invitations-code.test.ts +0 -218
  232. package/tests/integration/invitations-expiry.test.ts +0 -216
  233. package/tests/integration/invitations-resend.test.ts +0 -241
  234. package/tests/integration/invitations-revoke.test.ts +0 -226
  235. package/tests/integration/invitations-switch-org.test.ts +0 -156
  236. package/tests/integration/invitations-token.test.ts +0 -221
  237. package/tests/integration/migrations.test.ts +0 -119
  238. package/tests/integration/only-owner-protections.test.ts +0 -130
  239. package/tests/integration/org-lifecycle.test.ts +0 -169
  240. package/tests/integration/ownership-transfer-cancel.test.ts +0 -171
  241. package/tests/integration/ownership-transfer-expire.test.ts +0 -171
  242. package/tests/integration/ownership-transfer-happy.test.ts +0 -184
  243. package/tests/integration/ownership-transfer-locks.test.ts +0 -146
  244. package/tests/integration/password-reset.test.ts +0 -200
  245. package/tests/integration/super-admin-actions.test.ts +0 -180
  246. package/tests/integration/super-admin-restrictions.test.ts +0 -209
  247. package/tests/setup/global-setup.ts +0 -20
  248. package/tests/unit/adapter-shape.test.ts +0 -330
  249. package/tests/unit/role-permissions.test.ts +0 -236
  250. package/tests/unit/validation.test.ts +0 -304
  251. package/tsconfig.client.json +0 -13
  252. package/tsconfig.json +0 -12
  253. package/tsconfig.tsbuildinfo +0 -1
  254. package/vitest.config.ts +0 -13
package/package.json CHANGED
@@ -1,22 +1,29 @@
1
1
  {
2
2
  "name": "@varshylinc/team-management",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Team management shared module for Varshyl products.",
5
5
  "private": false,
6
+ "type": "module",
7
+ "files": [
8
+ "dist"
9
+ ],
6
10
  "main": "./dist/index.js",
7
11
  "types": "./dist/index.d.ts",
8
12
  "exports": {
9
13
  ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
14
+ "types": "./dist/index.d.ts",
15
+ "require": "./dist/index.js",
16
+ "import": "./dist/index.js"
12
17
  },
13
18
  "./server": {
14
- "import": "./dist/server/index.js",
15
- "types": "./dist/server/index.d.ts"
19
+ "types": "./dist/server/index.d.ts",
20
+ "require": "./dist/server/index.js",
21
+ "import": "./dist/server/index.js"
16
22
  },
17
23
  "./client": {
18
- "import": "./dist/client/index.js",
19
- "types": "./dist/client/index.d.ts"
24
+ "types": "./dist/client/index.d.ts",
25
+ "require": "./dist/client/index.js",
26
+ "import": "./dist/client/index.js"
20
27
  }
21
28
  },
22
29
  "dependencies": {
@@ -40,16 +47,16 @@
40
47
  },
41
48
  "peerDependencies": {
42
49
  "express": "^4.18.3",
43
- "pg": "^8.11.5"
50
+ "pg": "^8.11.5",
51
+ "react": "^18.0.0"
44
52
  },
45
- "type": "module",
46
53
  "publishConfig": {
47
54
  "access": "public",
48
55
  "registry": "https://registry.npmjs.org/"
49
56
  },
50
57
  "scripts": {
51
- "build": "tsc -p tsconfig.json && mkdir -p dist/server/migrations && cp src/server/migrations/*.sql dist/server/migrations/",
52
- "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json",
58
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.client.build.json && mkdir -p dist/server/migrations && cp src/server/migrations/*.sql dist/server/migrations/",
59
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
53
60
  "test": "vitest run",
54
61
  "lint": "eslint src --ext .ts,.tsx --max-warnings 0"
55
62
  }
package/.eslintrc.cjs DELETED
@@ -1,18 +0,0 @@
1
- module.exports = {
2
- root: true,
3
- parser: '@typescript-eslint/parser',
4
- parserOptions: {
5
- project: ['./tsconfig.json', './tsconfig.client.json'],
6
- tsconfigRootDir: __dirname,
7
- },
8
- plugins: ['@typescript-eslint'],
9
- extends: [
10
- 'eslint:recommended',
11
- 'plugin:@typescript-eslint/recommended',
12
- ],
13
- rules: {
14
- '@typescript-eslint/no-explicit-any': 'warn',
15
- '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
16
- },
17
- env: { node: true, browser: true, es2022: true },
18
- };
package/CHANGELOG.md DELETED
@@ -1,159 +0,0 @@
1
- # Changelog — @varshylinc/team-management
2
-
3
- ## 0.1.0 — 2026-05-08
4
-
5
- First production-grade release. v0.0.1 was a stub with no business logic; v0.1.0 is the first version products can actually consume — a complete team management module with org model, RBAC, invitations, audit log, ownership transfer, email change, password reset, and super-admin back office.
6
-
7
- ### Added
8
-
9
- **Org management**
10
- - `POST /orgs` — create organization (name, slug)
11
- - `GET /orgs/:id` — fetch org details
12
- - `PATCH /orgs/:id` — update org name and slug (admin+)
13
- - `DELETE /orgs/:id` — soft-delete with 30-day grace period; must type org name to confirm (owner only)
14
- - Org slug unique partial index (active orgs only)
15
- - Soft-delete columns: `deleted_at`, `delete_scheduled_for`, `deleted_by_user_id`
16
-
17
- **Member management**
18
- - `GET /orgs/:id/members` — list active members with roles
19
- - `GET /orgs/:id/members/former` — list removed members with removal metadata
20
- - `DELETE /orgs/:id/members/:userId` — soft-remove member; records `removed_by_user_id` and `removal_reason`
21
- - `PATCH /orgs/:id/members/:userId` — change member role (role-gated per matrix)
22
- - `GET /orgs/:id/members/:userId/cascade-preview` — preview what will be affected by removal
23
-
24
- **Invitations** (flag: `enableInvites`)
25
- - `POST /orgs/:id/invitations` — invite by email; sends magic link + 6-digit code in same email; 7-day expiry
26
- - `GET /orgs/:id/invitations` — list pending invitations (admin+)
27
- - `DELETE /orgs/:id/invitations/:invId` — revoke pending invitation (admin+)
28
- - `POST /orgs/:id/invitations/:invId/resend` — regenerate token + code, invalidate old (admin+)
29
- - `GET /orgs/:id/invitations/:invId/code` — fetch decrypted 6-digit code for phone fallback (admin+)
30
- - `GET /invitations/accept/:token` — public magic-link landing; shows org, role, inviter
31
- - `POST /invitations/accept/code` — public code-entry; `{email, code}`
32
- - Sub-A enforcement: member accepting while already in an org gets warning; owner is blocked entirely
33
-
34
- **Audit log** (flag: `enableAuditLog`)
35
- - `GET /orgs/:id/audit` — paginated audit events (admin+ only)
36
- - 13 event types: `org.created`, `org.settings.updated`, `org.deleted`, `member.invited`, `member.invite_accepted`, `member.invite_revoked`, `member.removed`, `member.role_changed`, `ownership.transfer_initiated`, `ownership.transfer_accepted`, `ownership.transfer_cancelled`, `email.change_requested`, `email.change_completed`
37
- - Each event stores: `actor_user_id`, `actor_type` (`user`/`super_admin`), `action`, `target_type`, `target_id`, `before` JSONB, `after` JSONB, `ip`, `user_agent`, `reason`, `created_at`
38
- - Super-admin events appear as "Varshyl Support" (real identity never exposed to org)
39
-
40
- **Ownership transfer** (flag: `enableOwnershipTransfer`)
41
- - `POST /orgs/:id/transfer` — initiate transfer to an admin (owner only; target must already be admin)
42
- - `GET /orgs/:id/transfer` — get pending transfer details
43
- - `POST /orgs/:id/transfer/:transferId/accept` — accept transfer (atomic role swap)
44
- - `DELETE /orgs/:id/transfer/:transferId` — cancel transfer (either party)
45
- - Transfer locks: no second transfer, no org delete, no party removal during pending transfer
46
- - 7-day expiration; auto-cancelled on expiry
47
-
48
- **Email change** (flag: `enableEmailChange`)
49
- - `POST /me/email-change` — request email change; sends verification to new + notice to old
50
- - `GET /me/email-change/verify` — verify new email via token
51
- - `POST /me/email-change/cancel` — cancel via old-email cancellation link; triggers mandatory password reset
52
- - Rate limit: 3 requests per user per 24 hours
53
- - Old-email notice contains cancel link with mandatory-reset semantics if clicked
54
-
55
- **Password reset** (flag: `enablePasswordReset`)
56
- - `POST /me/password-reset/request` — self-service; always returns 200 (no user enumeration)
57
- - `POST /me/password-reset/confirm` — confirm with token + new password
58
- - Rate limit: 3 requests per email per hour
59
- - 1-hour token expiry
60
-
61
- **Super-admin back office** (flag: `enableSuperAdmin`, default OFF)
62
- - `GET /admin/orgs` — list all orgs including deleted
63
- - `GET /admin/orgs/:id` — org details + members
64
- - `POST /admin/orgs/:id/restore` — restore soft-deleted org during grace period
65
- - `POST /admin/orgs/:id/appoint-owner` — appoint new owner (requires `reason`)
66
- - `POST /admin/orgs/:id/hard-delete` — legal-compliance hard delete (requires `legal_basis`)
67
- - `POST /admin/orgs/:id/members/add` — add member to any org (requires `reason`)
68
- - `POST /admin/orgs/:id/members/remove` — remove member from any org (requires `reason`)
69
- - `POST /admin/users/:id/lock` — lock user account (requires `reason`)
70
- - `POST /admin/users/:id/unlock` — unlock account
71
- - `POST /admin/users/:id/password-reset` — trigger reset link; super-admin never sees password
72
- - All super-admin actions require `reason` text; write audit event with `actor_type='super_admin'`
73
- - Cannot impersonate users; cannot modify product data; cannot set passwords directly
74
-
75
- **Self-service**
76
- - `GET /me/membership` — current user's org membership info
77
-
78
- **Health**
79
- - `GET /health` — module health with flag states
80
-
81
- ### Adapter contract changes
82
-
83
- 11 new required methods added to `ServerModuleAdapter` (all hosts must implement):
84
-
85
- | Method | Purpose |
86
- |--------|---------|
87
- | `getUserById(userId)` | Look up user by ID |
88
- | `getUsersByIds(userIds[])` | Batch user lookup |
89
- | `findUserByEmail(email)` | Look up user by email |
90
- | `createUserFromInvite({email, orgId, role})` | Create user on invite accept |
91
- | `setUserPassword(userId, hash)` | Store new password hash |
92
- | `hashPassword(plaintext)` | Hash a password |
93
- | `verifyPassword(plaintext, hash)` | Verify password against hash |
94
- | `invalidateAllUserSessions(userId)` | Invalidate all sessions for user |
95
- | `sendInviteEmail({to, orgName, inviterName, role, magicLinkUrl, code})` | Send invitation email |
96
- | `sendOwnershipTransferEmail({to, orgName, fromName, transferUrl})` | Send transfer notification |
97
- | `sendEmailChangeVerification({to, verifyUrl})` | Send verification to new email |
98
- | `sendEmailChangeOldNotice({to, newEmail, cancelUrl})` | Send notice to old email |
99
- | `sendEmailChangedFinalNotice({to, oldEmail, newEmail})` | Send final confirmation |
100
- | `sendPasswordResetEmail({to, resetUrl})` | Send reset link |
101
- | `sendOrgDeletionNotice({to, orgName, scheduledFor})` | Notify members on org delete |
102
- | `emitNotification({userId, type, payload})` | In-app notification hook |
103
-
104
- ### Migrations included
105
-
106
- | File | Description |
107
- |------|-------------|
108
- | `0001_create_tm_schema_migrations.sql` | Migration ledger table |
109
- | `0002_create_tm_organizations.sql` | Organizations table |
110
- | `0003_create_tm_memberships.sql` | Memberships with soft-delete columns |
111
- | `0004_create_tm_invitations.sql` | Invitations with encrypted code storage |
112
- | `0005_create_tm_audit_events.sql` | Audit events log |
113
- | `0006_create_tm_email_change_requests.sql` | Email change requests |
114
- | `0007_create_tm_ownership_transfers.sql` | Ownership transfer requests |
115
- | `0008_create_tm_super_admins.sql` | Super-admin registry |
116
- | `0009_create_tm_password_reset_requests.sql` | Password reset tokens |
117
- | `0010_create_tm_shared_access.sql` | Shared access scaffold (empty, v0.2.0) |
118
- | `0011_seed_super_admin.sql` | Super-admin seed (idempotent, flag-gated) |
119
-
120
- All migrations are forward-only, idempotent (`IF NOT EXISTS`), and tracked in `tm_schema_migrations`.
121
-
122
- ### Feature flags introduced
123
-
124
- | Flag | Default | Description |
125
- |------|---------|-------------|
126
- | `enableInvites` | `true` | Invitation flows (magic link + code) |
127
- | `enableAuditLog` | `true` | Audit log table and routes |
128
- | `enableOwnershipTransfer` | `true` | Ownership transfer flow |
129
- | `enableEmailChange` | `true` | Self-service email change |
130
- | `enablePasswordReset` | `true` | Self-service password reset |
131
- | `enableSuperAdmin` | `false` | Super-admin back office (Varshyl internal) |
132
- | `enableSharedAccess` | `false` | Shared access scaffold (reserved v0.2.0) |
133
- | `enableHardDelete` | `false` | Super-admin direct hard-delete |
134
-
135
- Routes return `501 Not Implemented` when their flag is off.
136
-
137
- ### Locked design decisions (set in v0.1.0; future major bumps may revise)
138
-
139
- **Decision 1 — Org model:** Every user belongs to exactly one org at a time. Solo accounts = org of one. `tm_shared_access` scaffolded empty (v0.2.0 placeholder). Multi-contractor vendor case handled by existing vendor-invite system in ConstructIInv (out of scope).
140
-
141
- **Decision 2 — Roles:** Four roles: `owner` (unique per org, billing, delete, transfer), `admin` (invite/remove/settings, no audit-visible super-admin identity), `member` (day-to-day, no team powers), `viewer` (read-only). Permission matrix enforced at middleware level.
142
-
143
- **Decision 3 — Audit log:** Stored forever. Visible to admin+ only. 13 event types. Super-admin entries visible as "Varshyl Support" — real identity never exposed.
144
-
145
- **Decision 4 — Invitations:** Magic link + 6-digit code in same email. AES-256-GCM encryption for codes. 7-day expiry. Sub-A: member accepting into new org gets explicit warning + atomic switch; owner is blocked until they transfer or delete current org.
146
-
147
- **Decision 5 — Deletion:** Member removal is soft (`removed_at`). Org deletion is soft with 30-day grace; confirm by typing org name. Background sweep hard-deletes after grace period.
148
-
149
- **Decision 6 — Email change:** Verification to new address + immediate notice to old. Cancel from old address triggers mandatory password reset. Rate limit: 3 per user per 24h.
150
-
151
- **Decision 7 — Ownership transfer:** Target must already be admin. Confirm by typing org name (initiator) and email (recipient). Atomic role swap on accept. Locks: no second transfer, no org delete, no party removal.
152
-
153
- **Decision 8 — Super-admin:** Flag-gated (default OFF). Seeded from `TM_SUPER_ADMIN_EMAIL` env. Cannot impersonate, cannot modify product data, cannot set passwords. All actions require reason text.
154
-
155
- ---
156
-
157
- ## v0.0.1 — 2026-04-28
158
-
159
- Initial stub. Module scaffolding only — no business logic.
package/src/client/api.ts DELETED
@@ -1,314 +0,0 @@
1
- import type {
2
- PublicOrg,
3
- PublicMember,
4
- PendingInvitation,
5
- CurrentMembership,
6
- OwnershipTransfer,
7
- AuditEvent,
8
- SuperAdminOrgSummary,
9
- OrgRole,
10
- ApiError,
11
- } from './types.js';
12
-
13
- export let TM_API_BASE = '/api/team';
14
-
15
- export function setTmApiBase(base: string): void {
16
- TM_API_BASE = base;
17
- }
18
-
19
- async function fetchTm<T>(
20
- path: string,
21
- options: RequestInit = {}
22
- ): Promise<T> {
23
- const res = await fetch(`${TM_API_BASE}${path}`, {
24
- ...options,
25
- headers: {
26
- 'Content-Type': 'application/json',
27
- ...(options.headers ?? {}),
28
- },
29
- });
30
-
31
- if (res.status === 204) {
32
- return undefined as unknown as T;
33
- }
34
-
35
- const data = await res.json().catch(() => ({ error: res.statusText }));
36
-
37
- if (!res.ok) {
38
- const err = data as ApiError;
39
- const msg = err.details ? `${err.error}: ${err.details.join(', ')}` : err.error;
40
- throw new Error(msg ?? `HTTP ${res.status}`);
41
- }
42
-
43
- return data as T;
44
- }
45
-
46
- // ─── Orgs ────────────────────────────────────────────────────────────────────
47
-
48
- export async function getOrg(orgId: number): Promise<PublicOrg> {
49
- return fetchTm<PublicOrg>(`/orgs/${orgId}`);
50
- }
51
-
52
- export async function updateOrg(
53
- orgId: number,
54
- data: { name?: string; slug?: string }
55
- ): Promise<PublicOrg> {
56
- return fetchTm<PublicOrg>(`/orgs/${orgId}`, {
57
- method: 'PATCH',
58
- body: JSON.stringify(data),
59
- });
60
- }
61
-
62
- export async function deleteOrg(orgId: number, confirmName: string): Promise<void> {
63
- return fetchTm<void>(`/orgs/${orgId}`, {
64
- method: 'DELETE',
65
- body: JSON.stringify({ confirmName }),
66
- });
67
- }
68
-
69
- export async function listMembers(
70
- orgId: number,
71
- opts?: { includeFormer?: boolean }
72
- ): Promise<PublicMember[]> {
73
- const qs = opts?.includeFormer ? '?includeFormer=true' : '';
74
- return fetchTm<PublicMember[]>(`/orgs/${orgId}/members${qs}`);
75
- }
76
-
77
- export async function removeMember(
78
- orgId: number,
79
- userId: number,
80
- reason?: string
81
- ): Promise<void> {
82
- return fetchTm<void>(`/orgs/${orgId}/members/${userId}`, {
83
- method: 'DELETE',
84
- body: JSON.stringify({ reason }),
85
- });
86
- }
87
-
88
- export async function changeMemberRole(
89
- orgId: number,
90
- userId: number,
91
- newRole: OrgRole
92
- ): Promise<PublicMember> {
93
- return fetchTm<PublicMember>(`/orgs/${orgId}/members/${userId}/role`, {
94
- method: 'PATCH',
95
- body: JSON.stringify({ role: newRole }),
96
- });
97
- }
98
-
99
- // ─── Invitations ─────────────────────────────────────────────────────────────
100
-
101
- export async function listInvitations(orgId: number): Promise<PendingInvitation[]> {
102
- return fetchTm<PendingInvitation[]>(`/orgs/${orgId}/invitations`);
103
- }
104
-
105
- export async function createInvitation(
106
- orgId: number,
107
- data: { email: string; role: OrgRole }
108
- ): Promise<PendingInvitation> {
109
- return fetchTm<PendingInvitation>(`/orgs/${orgId}/invitations`, {
110
- method: 'POST',
111
- body: JSON.stringify(data),
112
- });
113
- }
114
-
115
- export async function revokeInvitation(orgId: number, invitationId: number): Promise<void> {
116
- return fetchTm<void>(`/orgs/${orgId}/invitations/${invitationId}`, {
117
- method: 'DELETE',
118
- });
119
- }
120
-
121
- export async function resendInvitation(orgId: number, invitationId: number): Promise<void> {
122
- return fetchTm<void>(`/orgs/${orgId}/invitations/${invitationId}/resend`, {
123
- method: 'POST',
124
- });
125
- }
126
-
127
- export async function getInvitationCode(
128
- orgId: number,
129
- invitationId: number
130
- ): Promise<{ code: string }> {
131
- return fetchTm<{ code: string }>(`/orgs/${orgId}/invitations/${invitationId}/code`);
132
- }
133
-
134
- export async function acceptInvitationByToken(
135
- token: string
136
- ): Promise<{ orgId: number; role: OrgRole }> {
137
- return fetchTm<{ orgId: number; role: OrgRole }>(`/invitations/accept`, {
138
- method: 'POST',
139
- body: JSON.stringify({ token }),
140
- });
141
- }
142
-
143
- export async function acceptInvitationByCode(
144
- email: string,
145
- code: string
146
- ): Promise<{ orgId: number; role: OrgRole }> {
147
- return fetchTm<{ orgId: number; role: OrgRole }>(`/invitations/accept-code`, {
148
- method: 'POST',
149
- body: JSON.stringify({ email, code }),
150
- });
151
- }
152
-
153
- // ─── Me / self-service ───────────────────────────────────────────────────────
154
-
155
- export async function getMyMembership(): Promise<CurrentMembership> {
156
- return fetchTm<CurrentMembership>(`/me/membership`);
157
- }
158
-
159
- export async function requestEmailChange(newEmail: string): Promise<void> {
160
- return fetchTm<void>(`/me/email-change`, {
161
- method: 'POST',
162
- body: JSON.stringify({ newEmail }),
163
- });
164
- }
165
-
166
- export async function verifyEmailChange(token: string): Promise<void> {
167
- return fetchTm<void>(`/me/email-change/verify`, {
168
- method: 'POST',
169
- body: JSON.stringify({ token }),
170
- });
171
- }
172
-
173
- export async function cancelEmailChange(token: string): Promise<void> {
174
- return fetchTm<void>(`/me/email-change/cancel`, {
175
- method: 'POST',
176
- body: JSON.stringify({ token }),
177
- });
178
- }
179
-
180
- export async function requestPasswordReset(email: string): Promise<void> {
181
- return fetchTm<void>(`/password-reset/request`, {
182
- method: 'POST',
183
- body: JSON.stringify({ email }),
184
- });
185
- }
186
-
187
- export async function resetPassword(token: string, newPassword: string): Promise<void> {
188
- return fetchTm<void>(`/password-reset/confirm`, {
189
- method: 'POST',
190
- body: JSON.stringify({ token, newPassword }),
191
- });
192
- }
193
-
194
- // ─── Ownership transfer ───────────────────────────────────────────────────────
195
-
196
- export async function getPendingTransfer(orgId: number): Promise<OwnershipTransfer | null> {
197
- return fetchTm<OwnershipTransfer | null>(`/orgs/${orgId}/transfer`);
198
- }
199
-
200
- export async function initiateTransfer(
201
- orgId: number,
202
- toUserId: number
203
- ): Promise<OwnershipTransfer> {
204
- return fetchTm<OwnershipTransfer>(`/orgs/${orgId}/transfer`, {
205
- method: 'POST',
206
- body: JSON.stringify({ toUserId }),
207
- });
208
- }
209
-
210
- export async function acceptTransfer(orgId: number): Promise<void> {
211
- return fetchTm<void>(`/orgs/${orgId}/transfer/accept`, {
212
- method: 'POST',
213
- });
214
- }
215
-
216
- export async function cancelTransfer(orgId: number): Promise<void> {
217
- return fetchTm<void>(`/orgs/${orgId}/transfer/cancel`, {
218
- method: 'POST',
219
- });
220
- }
221
-
222
- // ─── Audit log ───────────────────────────────────────────────────────────────
223
-
224
- export async function getAuditLog(
225
- orgId: number,
226
- opts?: { page?: number; limit?: number; action?: string }
227
- ): Promise<{ events: AuditEvent[]; total: number; page: number }> {
228
- const params = new URLSearchParams();
229
- if (opts?.page !== undefined) params.set('page', String(opts.page));
230
- if (opts?.limit !== undefined) params.set('limit', String(opts.limit));
231
- if (opts?.action) params.set('action', opts.action);
232
- const qs = params.toString() ? `?${params.toString()}` : '';
233
- return fetchTm<{ events: AuditEvent[]; total: number; page: number }>(
234
- `/orgs/${orgId}/audit-log${qs}`
235
- );
236
- }
237
-
238
- // ─── Super-admin ─────────────────────────────────────────────────────────────
239
-
240
- export async function adminListOrgs(): Promise<SuperAdminOrgSummary[]> {
241
- return fetchTm<SuperAdminOrgSummary[]>(`/admin/orgs`);
242
- }
243
-
244
- export async function adminGetOrg(
245
- orgId: number
246
- ): Promise<SuperAdminOrgSummary & { members: PublicMember[] }> {
247
- return fetchTm<SuperAdminOrgSummary & { members: PublicMember[] }>(`/admin/orgs/${orgId}`);
248
- }
249
-
250
- export async function adminRestoreOrg(orgId: number): Promise<void> {
251
- return fetchTm<void>(`/admin/orgs/${orgId}/restore`, { method: 'POST' });
252
- }
253
-
254
- export async function adminAppointOwner(
255
- orgId: number,
256
- targetUserId: number,
257
- reason: string
258
- ): Promise<void> {
259
- return fetchTm<void>(`/admin/orgs/${orgId}/appoint-owner`, {
260
- method: 'POST',
261
- body: JSON.stringify({ targetUserId, reason }),
262
- });
263
- }
264
-
265
- export async function adminHardDeleteOrg(orgId: number, legalBasis: string): Promise<void> {
266
- return fetchTm<void>(`/admin/orgs/${orgId}/hard-delete`, {
267
- method: 'DELETE',
268
- body: JSON.stringify({ legalBasis }),
269
- });
270
- }
271
-
272
- export async function adminAddMember(
273
- orgId: number,
274
- userId: number,
275
- role: OrgRole,
276
- reason: string
277
- ): Promise<void> {
278
- return fetchTm<void>(`/admin/orgs/${orgId}/members`, {
279
- method: 'POST',
280
- body: JSON.stringify({ userId, role, reason }),
281
- });
282
- }
283
-
284
- export async function adminRemoveMember(
285
- orgId: number,
286
- userId: number,
287
- reason: string
288
- ): Promise<void> {
289
- return fetchTm<void>(`/admin/orgs/${orgId}/members/${userId}`, {
290
- method: 'DELETE',
291
- body: JSON.stringify({ reason }),
292
- });
293
- }
294
-
295
- export async function adminLockUser(userId: number, reason: string): Promise<void> {
296
- return fetchTm<void>(`/admin/users/${userId}/lock`, {
297
- method: 'POST',
298
- body: JSON.stringify({ reason }),
299
- });
300
- }
301
-
302
- export async function adminUnlockUser(userId: number, reason: string): Promise<void> {
303
- return fetchTm<void>(`/admin/users/${userId}/unlock`, {
304
- method: 'POST',
305
- body: JSON.stringify({ reason }),
306
- });
307
- }
308
-
309
- export async function adminResetPassword(userId: number, reason: string): Promise<void> {
310
- return fetchTm<void>(`/admin/users/${userId}/reset-password`, {
311
- method: 'POST',
312
- body: JSON.stringify({ reason }),
313
- });
314
- }
@@ -1,59 +0,0 @@
1
- import React from 'react';
2
- import type { AuditEvent } from '../types.js';
3
-
4
- interface AuditEventRowProps {
5
- event: AuditEvent;
6
- }
7
-
8
- function humanizeAction(action: string): string {
9
- return action
10
- .replace(/_/g, ' ')
11
- .replace(/\b\w/g, (c) => c.toUpperCase());
12
- }
13
-
14
- function formatTimestamp(iso: string): string {
15
- return new Date(iso).toLocaleString(undefined, {
16
- year: 'numeric',
17
- month: 'short',
18
- day: 'numeric',
19
- hour: '2-digit',
20
- minute: '2-digit',
21
- });
22
- }
23
-
24
- export function AuditEventRow({ event }: AuditEventRowProps) {
25
- const isAdminAction = event.actor_type === 'super_admin';
26
-
27
- return (
28
- <tr className="border-b border-slate-100 last:border-0 hover:bg-slate-50 transition-colors">
29
- <td className="py-3 px-4">
30
- <div className="flex items-center gap-2">
31
- <span className={`text-sm font-medium ${isAdminAction ? 'text-purple-700' : 'text-slate-900'}`}>
32
- {event.actor_display_name}
33
- </span>
34
- {isAdminAction && (
35
- <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700">
36
- Support
37
- </span>
38
- )}
39
- </div>
40
- </td>
41
- <td className="py-3 px-4">
42
- <span className="text-sm text-slate-700">{humanizeAction(event.action)}</span>
43
- </td>
44
- <td className="py-3 px-4">
45
- {event.target_type && event.target_id ? (
46
- <span className="text-xs text-slate-500">
47
- {event.target_type} #{event.target_id}
48
- </span>
49
- ) : (
50
- <span className="text-slate-300">—</span>
51
- )}
52
- </td>
53
- <td className="py-3 px-4 text-xs text-slate-500 whitespace-nowrap">
54
- {formatTimestamp(event.created_at)}
55
- </td>
56
- </tr>
57
- );
58
- }
59
-
@@ -1,36 +0,0 @@
1
- import React from 'react';
2
-
3
- interface CascadeItem {
4
- label: string;
5
- count: number;
6
- description: string;
7
- }
8
-
9
- interface CascadePreviewProps {
10
- items: CascadeItem[];
11
- }
12
-
13
- export function CascadePreview({ items }: CascadePreviewProps) {
14
- if (items.length === 0) return null;
15
-
16
- return (
17
- <div className="rounded-lg border border-amber-200 bg-amber-50 p-4">
18
- <p className="text-sm font-semibold text-amber-800 mb-3">
19
- The following will be affected:
20
- </p>
21
- <ul className="space-y-2">
22
- {items.map((item, i) => (
23
- <li key={i} className="flex items-start gap-3">
24
- <span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-amber-200 text-xs font-bold text-amber-800">
25
- {item.count}
26
- </span>
27
- <div>
28
- <span className="text-sm font-medium text-amber-900">{item.label}</span>
29
- <p className="text-xs text-amber-700">{item.description}</p>
30
- </div>
31
- </li>
32
- ))}
33
- </ul>
34
- </div>
35
- );
36
- }