@selvajs/server 1.1.0 → 1.3.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 (233) hide show
  1. package/dist/access/guards.d.ts +213 -0
  2. package/dist/access/guards.d.ts.map +1 -0
  3. package/dist/access/guards.js +417 -0
  4. package/dist/access/guards.js.map +1 -0
  5. package/dist/access/index.d.ts +2 -0
  6. package/dist/access/index.d.ts.map +1 -1
  7. package/dist/access/index.js +6 -0
  8. package/dist/access/index.js.map +1 -1
  9. package/dist/access/permissions-scope.d.ts +15 -0
  10. package/dist/access/permissions-scope.d.ts.map +1 -0
  11. package/dist/access/permissions-scope.js +23 -0
  12. package/dist/access/permissions-scope.js.map +1 -0
  13. package/dist/api/bodies.d.ts +104 -0
  14. package/dist/api/bodies.d.ts.map +1 -0
  15. package/dist/api/bodies.js +87 -0
  16. package/dist/api/bodies.js.map +1 -0
  17. package/dist/api/deps.d.ts +135 -0
  18. package/dist/api/deps.d.ts.map +1 -0
  19. package/dist/api/deps.js +55 -0
  20. package/dist/api/deps.js.map +1 -0
  21. package/dist/api/errors.d.ts +35 -0
  22. package/dist/api/errors.d.ts.map +1 -0
  23. package/dist/api/errors.js +63 -0
  24. package/dist/api/errors.js.map +1 -0
  25. package/dist/api/index.d.ts +12 -0
  26. package/dist/api/index.d.ts.map +1 -0
  27. package/dist/api/index.js +15 -0
  28. package/dist/api/index.js.map +1 -0
  29. package/dist/api/map-core-error.d.ts +19 -0
  30. package/dist/api/map-core-error.d.ts.map +1 -0
  31. package/dist/api/map-core-error.js +49 -0
  32. package/dist/api/map-core-error.js.map +1 -0
  33. package/dist/api/pagination.d.ts +19 -0
  34. package/dist/api/pagination.d.ts.map +1 -0
  35. package/dist/api/pagination.js +40 -0
  36. package/dist/api/pagination.js.map +1 -0
  37. package/dist/api/request.d.ts +93 -0
  38. package/dist/api/request.d.ts.map +1 -0
  39. package/dist/api/request.js +150 -0
  40. package/dist/api/request.js.map +1 -0
  41. package/dist/api/respond.d.ts +29 -0
  42. package/dist/api/respond.d.ts.map +1 -0
  43. package/dist/api/respond.js +59 -0
  44. package/dist/api/respond.js.map +1 -0
  45. package/dist/api/responses-schema.d.ts +158 -0
  46. package/dist/api/responses-schema.d.ts.map +1 -0
  47. package/dist/api/responses-schema.js +104 -0
  48. package/dist/api/responses-schema.js.map +1 -0
  49. package/dist/api/responses.d.ts +19 -0
  50. package/dist/api/responses.d.ts.map +1 -0
  51. package/dist/api/responses.js +21 -0
  52. package/dist/api/responses.js.map +1 -0
  53. package/dist/api/shaped.d.ts +23 -0
  54. package/dist/api/shaped.d.ts.map +1 -0
  55. package/dist/api/shaped.js +29 -0
  56. package/dist/api/shaped.js.map +1 -0
  57. package/dist/api/types.d.ts +41 -0
  58. package/dist/api/types.d.ts.map +1 -0
  59. package/dist/api/types.js +12 -0
  60. package/dist/api/types.js.map +1 -0
  61. package/dist/compute/errors.d.ts +18 -0
  62. package/dist/compute/errors.d.ts.map +1 -0
  63. package/dist/compute/errors.js +21 -0
  64. package/dist/compute/errors.js.map +1 -0
  65. package/dist/compute/evict-changed-servers.d.ts +22 -0
  66. package/dist/compute/evict-changed-servers.d.ts.map +1 -0
  67. package/dist/compute/evict-changed-servers.js +31 -0
  68. package/dist/compute/evict-changed-servers.js.map +1 -0
  69. package/dist/compute/idempotency-http.d.ts +47 -0
  70. package/dist/compute/idempotency-http.d.ts.map +1 -0
  71. package/dist/compute/idempotency-http.js +51 -0
  72. package/dist/compute/idempotency-http.js.map +1 -0
  73. package/dist/compute/index.d.ts +5 -0
  74. package/dist/compute/index.d.ts.map +1 -1
  75. package/dist/compute/index.js +5 -0
  76. package/dist/compute/index.js.map +1 -1
  77. package/dist/compute/resolve-server.d.ts +25 -0
  78. package/dist/compute/resolve-server.d.ts.map +1 -0
  79. package/dist/compute/resolve-server.js +35 -0
  80. package/dist/compute/resolve-server.js.map +1 -0
  81. package/dist/compute/server-config-write.d.ts +52 -0
  82. package/dist/compute/server-config-write.d.ts.map +1 -0
  83. package/dist/compute/server-config-write.js +100 -0
  84. package/dist/compute/server-config-write.js.map +1 -0
  85. package/dist/definitions/index.d.ts +1 -0
  86. package/dist/definitions/index.d.ts.map +1 -1
  87. package/dist/definitions/index.js +1 -0
  88. package/dist/definitions/index.js.map +1 -1
  89. package/dist/definitions/visibility.d.ts +74 -0
  90. package/dist/definitions/visibility.d.ts.map +1 -0
  91. package/dist/definitions/visibility.js +117 -0
  92. package/dist/definitions/visibility.js.map +1 -0
  93. package/dist/handlers/definitionVersions.d.ts +30 -0
  94. package/dist/handlers/definitionVersions.d.ts.map +1 -0
  95. package/dist/handlers/definitionVersions.js +93 -0
  96. package/dist/handlers/definitionVersions.js.map +1 -0
  97. package/dist/handlers/definitions.d.ts +35 -0
  98. package/dist/handlers/definitions.d.ts.map +1 -0
  99. package/dist/handlers/definitions.js +207 -0
  100. package/dist/handlers/definitions.js.map +1 -0
  101. package/dist/handlers/index.d.ts +27 -0
  102. package/dist/handlers/index.d.ts.map +1 -0
  103. package/dist/handlers/index.js +29 -0
  104. package/dist/handlers/index.js.map +1 -0
  105. package/dist/handlers/invite-lookup.d.ts +11 -0
  106. package/dist/handlers/invite-lookup.d.ts.map +1 -0
  107. package/dist/handlers/invite-lookup.js +20 -0
  108. package/dist/handlers/invite-lookup.js.map +1 -0
  109. package/dist/handlers/invites.d.ts +31 -0
  110. package/dist/handlers/invites.d.ts.map +1 -0
  111. package/dist/handlers/invites.js +148 -0
  112. package/dist/handlers/invites.js.map +1 -0
  113. package/dist/handlers/me.d.ts +8 -0
  114. package/dist/handlers/me.d.ts.map +1 -0
  115. package/dist/handlers/me.js +21 -0
  116. package/dist/handlers/me.js.map +1 -0
  117. package/dist/handlers/me.starred.d.ts +11 -0
  118. package/dist/handlers/me.starred.d.ts.map +1 -0
  119. package/dist/handlers/me.starred.js +21 -0
  120. package/dist/handlers/me.starred.js.map +1 -0
  121. package/dist/handlers/orgAssets.d.ts +13 -0
  122. package/dist/handlers/orgAssets.d.ts.map +1 -0
  123. package/dist/handlers/orgAssets.js +54 -0
  124. package/dist/handlers/orgAssets.js.map +1 -0
  125. package/dist/handlers/orgCompute.d.ts +16 -0
  126. package/dist/handlers/orgCompute.d.ts.map +1 -0
  127. package/dist/handlers/orgCompute.js +96 -0
  128. package/dist/handlers/orgCompute.js.map +1 -0
  129. package/dist/handlers/orgMembers.d.ts +29 -0
  130. package/dist/handlers/orgMembers.d.ts.map +1 -0
  131. package/dist/handlers/orgMembers.js +200 -0
  132. package/dist/handlers/orgMembers.js.map +1 -0
  133. package/dist/handlers/orgs.d.ts +16 -0
  134. package/dist/handlers/orgs.d.ts.map +1 -0
  135. package/dist/handlers/orgs.js +26 -0
  136. package/dist/handlers/orgs.js.map +1 -0
  137. package/dist/handlers/projectMembers.d.ts +15 -0
  138. package/dist/handlers/projectMembers.d.ts.map +1 -0
  139. package/dist/handlers/projectMembers.js +95 -0
  140. package/dist/handlers/projectMembers.js.map +1 -0
  141. package/dist/handlers/projects.d.ts +21 -0
  142. package/dist/handlers/projects.d.ts.map +1 -0
  143. package/dist/handlers/projects.js +164 -0
  144. package/dist/handlers/projects.js.map +1 -0
  145. package/dist/handlers/reclaim.d.ts +10 -0
  146. package/dist/handlers/reclaim.d.ts.map +1 -0
  147. package/dist/handlers/reclaim.js +38 -0
  148. package/dist/handlers/reclaim.js.map +1 -0
  149. package/dist/handlers/services.d.ts +22 -0
  150. package/dist/handlers/services.d.ts.map +1 -0
  151. package/dist/handlers/services.js +37 -0
  152. package/dist/handlers/services.js.map +1 -0
  153. package/dist/handlers/shareLinks.d.ts +23 -0
  154. package/dist/handlers/shareLinks.d.ts.map +1 -0
  155. package/dist/handlers/shareLinks.js +85 -0
  156. package/dist/handlers/shareLinks.js.map +1 -0
  157. package/dist/http/index.d.ts +1 -0
  158. package/dist/http/index.d.ts.map +1 -1
  159. package/dist/http/index.js +1 -0
  160. package/dist/http/index.js.map +1 -1
  161. package/dist/http/login-rate-limit.d.ts +72 -0
  162. package/dist/http/login-rate-limit.d.ts.map +1 -0
  163. package/dist/http/login-rate-limit.js +84 -0
  164. package/dist/http/login-rate-limit.js.map +1 -0
  165. package/dist/notifications/deliver-invite.d.ts +27 -0
  166. package/dist/notifications/deliver-invite.d.ts.map +1 -0
  167. package/dist/notifications/deliver-invite.js +32 -0
  168. package/dist/notifications/deliver-invite.js.map +1 -0
  169. package/dist/notifications/index.d.ts +4 -0
  170. package/dist/notifications/index.d.ts.map +1 -0
  171. package/dist/notifications/index.js +4 -0
  172. package/dist/notifications/index.js.map +1 -0
  173. package/dist/notifications/smtp-config.d.ts +37 -0
  174. package/dist/notifications/smtp-config.d.ts.map +1 -0
  175. package/dist/notifications/smtp-config.js +41 -0
  176. package/dist/notifications/smtp-config.js.map +1 -0
  177. package/dist/notifications/smtp-provider.d.ts +18 -0
  178. package/dist/notifications/smtp-provider.d.ts.map +1 -0
  179. package/dist/notifications/smtp-provider.js +71 -0
  180. package/dist/notifications/smtp-provider.js.map +1 -0
  181. package/dist/ops/deploymentConfig.d.ts +73 -0
  182. package/dist/ops/deploymentConfig.d.ts.map +1 -0
  183. package/dist/ops/deploymentConfig.js +154 -0
  184. package/dist/ops/deploymentConfig.js.map +1 -0
  185. package/dist/ops/index.d.ts +1 -0
  186. package/dist/ops/index.d.ts.map +1 -1
  187. package/dist/ops/index.js +3 -2
  188. package/dist/ops/index.js.map +1 -1
  189. package/dist/organizations/index.d.ts +3 -0
  190. package/dist/organizations/index.d.ts.map +1 -0
  191. package/dist/organizations/index.js +5 -0
  192. package/dist/organizations/index.js.map +1 -0
  193. package/dist/organizations/list-all-members.d.ts +10 -0
  194. package/dist/organizations/list-all-members.d.ts.map +1 -0
  195. package/dist/organizations/list-all-members.js +36 -0
  196. package/dist/organizations/list-all-members.js.map +1 -0
  197. package/dist/organizations/org-asset-service.d.ts +39 -0
  198. package/dist/organizations/org-asset-service.d.ts.map +1 -0
  199. package/dist/organizations/org-asset-service.js +71 -0
  200. package/dist/organizations/org-asset-service.js.map +1 -0
  201. package/dist/projects/create-project.d.ts +30 -0
  202. package/dist/projects/create-project.d.ts.map +1 -0
  203. package/dist/projects/create-project.js +56 -0
  204. package/dist/projects/create-project.js.map +1 -0
  205. package/dist/projects/index.d.ts +2 -0
  206. package/dist/projects/index.d.ts.map +1 -0
  207. package/dist/projects/index.js +3 -0
  208. package/dist/projects/index.js.map +1 -0
  209. package/dist/providers/auth-lookup.d.ts +14 -0
  210. package/dist/providers/auth-lookup.d.ts.map +1 -0
  211. package/dist/providers/auth-lookup.js +34 -0
  212. package/dist/providers/auth-lookup.js.map +1 -0
  213. package/dist/providers/index.d.ts +1 -0
  214. package/dist/providers/index.d.ts.map +1 -1
  215. package/dist/providers/index.js +1 -0
  216. package/dist/providers/index.js.map +1 -1
  217. package/dist/testing/harness.d.ts +168 -0
  218. package/dist/testing/harness.d.ts.map +1 -0
  219. package/dist/testing/harness.js +314 -0
  220. package/dist/testing/harness.js.map +1 -0
  221. package/dist/testing/index.d.ts +18 -0
  222. package/dist/testing/index.d.ts.map +1 -0
  223. package/dist/testing/index.js +15 -0
  224. package/dist/testing/index.js.map +1 -0
  225. package/dist/testing/scenarios.d.ts +67 -0
  226. package/dist/testing/scenarios.d.ts.map +1 -0
  227. package/dist/testing/scenarios.js +104 -0
  228. package/dist/testing/scenarios.js.map +1 -0
  229. package/dist/testing/seed-adapter.d.ts +31 -0
  230. package/dist/testing/seed-adapter.d.ts.map +1 -0
  231. package/dist/testing/seed-adapter.js +19 -0
  232. package/dist/testing/seed-adapter.js.map +1 -0
  233. package/package.json +42 -3
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Org member role, permissions, and removal.
3
+ *
4
+ * Two gates run on every request: `requireManageOrgMembers` (owner+admin) and
5
+ * `requireActingOrg` (the URL `orgId` must equal `ctx.actingOrgId`, so
6
+ * cross-org member mutation is blocked even for an actor with permissions in
7
+ * another org).
8
+ *
9
+ * The sole-owner invariant applies to demotion and removal alike — an org with
10
+ * no owner is unadministrable, and removing an owner ends their role exactly as
11
+ * demoting them does.
12
+ */
13
+ import { apiError, ApiErrorCode, noContent, parseBody, requireParams, UpdateOrgMemberBodySchema } from '../api/index.js';
14
+ import { MEMBER_ASSIGNABLE_PERMISSIONS, ALL_ORG_PERMISSIONS, actorFrom, canChangeOrgRole } from '@selvajs/platform';
15
+ import { requireManageOrgMembers, requireActingOrg } from '../access/index.js';
16
+ const ROSTER_PAGE_LIMIT = 200;
17
+ // Runaway guard against an adapter returning a non-advancing cursor. Matches
18
+ // `listAllOrgMembers`; 100 pages is 20k members, past any real org here.
19
+ const MAX_ROSTER_PAGES = 100;
20
+ /**
21
+ * Whether anyone other than `exceptUserId` still owns the org.
22
+ *
23
+ * Both branches below need this and both used to scan the roster themselves —
24
+ * the sole-owner invariant is one rule, so it reads from one place.
25
+ *
26
+ * Pages rather than reading one 200-row window: a second owner sitting past the
27
+ * first page read as "no other owner", so the invariant refused a demotion or
28
+ * removal that was in fact safe. That failed closed, but it made the org
29
+ * unadministrable exactly when it had grown enough to need administering.
30
+ * Returns on the first owner found, so the common case is still one round-trip.
31
+ */
32
+ async function hasAnotherOwner(req, ctx, orgId, exceptUserId) {
33
+ let cursor;
34
+ for (let page = 0; page < MAX_ROSTER_PAGES; page++) {
35
+ const result = await req.deps.orgs.listOrgMembers(ctx, orgId, {
36
+ limit: ROSTER_PAGE_LIMIT,
37
+ cursor
38
+ });
39
+ if (result.items.some((m) => m.role === 'owner' && m.userId !== exceptUserId && !m.deletedAt)) {
40
+ return true;
41
+ }
42
+ cursor = result.nextCursor;
43
+ if (!cursor)
44
+ return false;
45
+ }
46
+ // Cap hit without finding one: report "no other owner" so the caller refuses.
47
+ // Failing closed on an incomplete read is the safe direction for an invariant
48
+ // whose whole job is preventing an unadministrable org.
49
+ req.log.warn('hasAnotherOwner hit the page cap — treating as sole owner', {
50
+ orgId,
51
+ pages: MAX_ROSTER_PAGES
52
+ });
53
+ return false;
54
+ }
55
+ /**
56
+ * Projects in `orgId` where `userId` is the only live owner — the ones that
57
+ * will have no owner once `removeOrgMember` cascades their membership rows.
58
+ *
59
+ * **Report, do not block.** Blocking puts an unbounded chore in front of
60
+ * offboarding — the cost scales with how many projects the departing person
61
+ * owned, which is backwards, because the most prolific people are the ones
62
+ * whose departure most needs to be clean. An offboarding that fails halfway is
63
+ * worse than an ownerless project: the person stays in the org while someone
64
+ * works through the backlog. Reclaim already exists to adopt an ownerless
65
+ * project, so the recovery path is built; what was missing was any signal that
66
+ * recovery was needed. Hence the event.
67
+ *
68
+ * MUST be called before the removal — afterwards the rows it reads are gone.
69
+ */
70
+ async function findProjectsLosingSoleOwner(req, ctx, orgId, userId) {
71
+ const projects = req.deps.projects;
72
+ const orgProjects = await projects.listProjects(ctx, orgId, { limit: ROSTER_PAGE_LIMIT });
73
+ const checks = await Promise.all(orgProjects.items.map(async (project) => {
74
+ const members = await projects.listProjectMembers(ctx, project.id, {
75
+ limit: ROSTER_PAGE_LIMIT
76
+ });
77
+ const owners = members.items.filter((m) => m.role === 'owner' && !m.deletedAt);
78
+ return owners.length === 1 && owners[0].userId === userId ? project : null;
79
+ }));
80
+ return checks.filter((p) => p !== null);
81
+ }
82
+ /**
83
+ * Body accepts either or both:
84
+ * - `{ role: OrgRole }` — change the member's role. Owner-only.
85
+ * - `{ permissions: OrgPermission[] }` — replace the member's grantable
86
+ * permissions. Owner/admin via `manage_org_members`. For `member`-role
87
+ * targets, only `MEMBER_ASSIGNABLE_PERMISSIONS` are accepted; promoting a
88
+ * member's permissions does NOT change their role.
89
+ */
90
+ export const updateOrgMember = async (req) => {
91
+ const { userId } = requireParams(req.params, 'orgId', 'userId');
92
+ // Owner+admin gate, covering permission edits. The role-change branch adds
93
+ // an owner-only gate below.
94
+ requireManageOrgMembers(req);
95
+ const { ctx, orgId } = requireActingOrg(req, req.params.orgId);
96
+ const patch = await parseBody(req.request, UpdateOrgMemberBodySchema);
97
+ const orgs = req.deps.orgs;
98
+ const [actorMember, target] = await Promise.all([
99
+ orgs.getOrgMember(ctx, orgId, ctx.userId),
100
+ orgs.getOrgMember(ctx, orgId, userId)
101
+ ]);
102
+ if (!target)
103
+ apiError(404, ApiErrorCode.NOT_FOUND, 'Member not found in this organization.');
104
+ if (!actorMember) {
105
+ apiError(403, ApiErrorCode.FORBIDDEN, 'You are not a member of this organization.');
106
+ }
107
+ if (patch.role !== undefined && patch.role !== target.role) {
108
+ // Gated on both the role being granted and the one being taken away —
109
+ // demoting an owner is an owner-only act even though `member` is not.
110
+ if (!canChangeOrgRole({ actorMember, role: patch.role }) ||
111
+ !canChangeOrgRole({ actorMember, role: target.role })) {
112
+ apiError(403, ApiErrorCode.FORBIDDEN, 'Only the org owner can change roles.');
113
+ }
114
+ if (target.role === 'owner' &&
115
+ patch.role !== 'owner' &&
116
+ !(await hasAnotherOwner(req, ctx, orgId, target.userId))) {
117
+ apiError(409, ApiErrorCode.CONFLICT, 'Cannot demote the sole owner of this organization. Promote another member to owner first.');
118
+ }
119
+ await orgs.updateOrgMemberRole(ctx, orgId, userId, patch.role);
120
+ }
121
+ if (patch.permissions !== undefined) {
122
+ // Validate against the role the member will have, not the one they had.
123
+ const effectiveRole = patch.role ?? target.role;
124
+ const valid = effectiveRole === 'member'
125
+ ? new Set(MEMBER_ASSIGNABLE_PERMISSIONS)
126
+ : new Set(ALL_ORG_PERMISSIONS);
127
+ const invalid = patch.permissions.filter((p) => !valid.has(p));
128
+ if (invalid.length > 0) {
129
+ apiError(400, ApiErrorCode.VALIDATION_FAILED, `Permissions [${invalid.join(', ')}] cannot be assigned to a ${effectiveRole}.`);
130
+ }
131
+ await orgs.updateOrgMemberPermissions(ctx, orgId, userId, patch.permissions);
132
+ }
133
+ return noContent();
134
+ };
135
+ /**
136
+ * Remove a member from the org. Owner/admin via `manage_org_members`, same
137
+ * tenancy check as PATCH, and the same sole-owner invariant — removing the last
138
+ * owner would leave the org unadministrable.
139
+ */
140
+ export const removeOrgMember = async (req) => {
141
+ const { userId } = requireParams(req.params, 'orgId', 'userId');
142
+ requireManageOrgMembers(req);
143
+ const { ctx, orgId } = requireActingOrg(req, req.params.orgId);
144
+ const orgs = req.deps.orgs;
145
+ const [actorMember, target] = await Promise.all([
146
+ orgs.getOrgMember(ctx, orgId, ctx.userId),
147
+ orgs.getOrgMember(ctx, orgId, userId)
148
+ ]);
149
+ if (!target)
150
+ apiError(404, ApiErrorCode.NOT_FOUND, 'Member not found in this organization.');
151
+ // Same owner-only gate PATCH applies to demotion. Removing an owner ends
152
+ // their role just as demoting them does, so an admin got 403 on the demote
153
+ // and 204 on the remove — the harder-to-reverse of the two.
154
+ if (!canChangeOrgRole({ actorMember, role: target.role })) {
155
+ apiError(403, ApiErrorCode.FORBIDDEN, 'Only the org owner can remove another owner.');
156
+ }
157
+ if (target.role === 'owner' && !(await hasAnotherOwner(req, ctx, orgId, target.userId))) {
158
+ apiError(409, ApiErrorCode.CONFLICT, 'Cannot remove the sole owner of this organization. Promote another member to owner first.');
159
+ }
160
+ // Must run before the removal: the cascade soft-deletes the very rows this
161
+ // reads. Reported, not blocked — see the docblock on `findProjectsLosingSoleOwner`.
162
+ const orphaned = await findProjectsLosingSoleOwner(req, ctx, orgId, userId);
163
+ await orgs.removeOrgMember(ctx, orgId, userId);
164
+ if (orphaned.length > 0) {
165
+ await req.deps.events.emit({
166
+ type: 'org_member.removed_orphaning_projects',
167
+ orgId,
168
+ userId,
169
+ projectIds: orphaned.map((p) => p.id),
170
+ actorId: actorFrom(ctx)
171
+ });
172
+ req.log.warn('Org member removal left projects without an owner', {
173
+ orgId,
174
+ userId,
175
+ actorId: ctx.userId,
176
+ projectCount: orphaned.length
177
+ });
178
+ }
179
+ // `removeOrgMember` cascades `project_members` but not invites, so a
180
+ // dormant invite would let the removed user walk straight back in at
181
+ // their original role. Best-effort: the removal itself has committed, and
182
+ // failing the request now would report an offboarding that did happen as
183
+ // one that didn't. A failure here leaves a live re-entry path, so it is
184
+ // logged rather than swallowed.
185
+ const email = (await req.deps.auth.getUser(userId))?.email;
186
+ if (email) {
187
+ try {
188
+ await req.deps.invites.revokePendingByEmail(ctx, orgId, email);
189
+ }
190
+ catch (err) {
191
+ req.log.error('Removed org member but failed to revoke their pending invites', {
192
+ orgId,
193
+ userId,
194
+ error: err instanceof Error ? err.message : String(err)
195
+ });
196
+ }
197
+ }
198
+ return noContent();
199
+ };
200
+ //# sourceMappingURL=orgMembers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orgMembers.js","sourceRoot":"","sources":["../../src/handlers/orgMembers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The acting org's record and roster.
3
+ *
4
+ * `requireActingOrg` is the tenancy gate on both: the URL `orgId` must equal
5
+ * `ctx.actingOrgId`, so the URL alone never decides which tenant is read.
6
+ */
7
+ import { type ApiHandler } from '../api/index.js';
8
+ /** The acting org's record — name, slug, and branding asset URLs. */
9
+ export declare const getOrg: ApiHandler;
10
+ /**
11
+ * List the org's members. Any member of the acting org may read this — the
12
+ * roster is what the team page renders, and `requireActingOrg` already confines
13
+ * the read to the caller's own tenant.
14
+ */
15
+ export declare const listOrgMembers: ApiHandler;
16
+ //# sourceMappingURL=orgs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orgs.d.ts","sourceRoot":"","sources":["../../src/handlers/orgs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAKN,KAAK,UAAU,EACf,MAAM,iBAAiB,CAAC;AAGzB,qEAAqE;AACrE,eAAO,MAAM,MAAM,EAAE,UAMpB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,UAI5B,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The acting org's record and roster.
3
+ *
4
+ * `requireActingOrg` is the tenancy gate on both: the URL `orgId` must equal
5
+ * `ctx.actingOrgId`, so the URL alone never decides which tenant is read.
6
+ */
7
+ import { apiError, ApiErrorCode, collection, parseListOptions } from '../api/index.js';
8
+ import { requireActingOrg } from '../access/index.js';
9
+ /** The acting org's record — name, slug, and branding asset URLs. */
10
+ export const getOrg = async (req) => {
11
+ const { ctx, orgId } = requireActingOrg(req, req.params.orgId);
12
+ const org = await req.deps.orgs.getOrg(ctx, orgId);
13
+ if (!org)
14
+ apiError(404, ApiErrorCode.NOT_FOUND, 'Organization not found');
15
+ return { body: org };
16
+ };
17
+ /**
18
+ * List the org's members. Any member of the acting org may read this — the
19
+ * roster is what the team page renders, and `requireActingOrg` already confines
20
+ * the read to the caller's own tenant.
21
+ */
22
+ export const listOrgMembers = async (req) => {
23
+ const { ctx, orgId } = requireActingOrg(req, req.params.orgId);
24
+ return collection(await req.deps.orgs.listOrgMembers(ctx, orgId, parseListOptions(req.url)));
25
+ };
26
+ //# sourceMappingURL=orgs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orgs.js","sourceRoot":"","sources":["../../src/handlers/orgs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACN,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,gBAAgB,EAEhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,qEAAqE;AACrE,MAAM,CAAC,MAAM,MAAM,GAAe,KAAK,EAAE,GAAG,EAAE,EAAE;IAC/C,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,gBAAgB,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE/D,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACnD,IAAI,CAAC,GAAG;QAAE,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC,SAAS,EAAE,wBAAwB,CAAC,CAAC;IAC1E,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AACtB,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAe,KAAK,EAAE,GAAG,EAAE,EAAE;IACvD,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,gBAAgB,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE/D,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,KAAK,EAAE,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC9F,CAAC,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Project membership: list, add, change role, remove.
3
+ *
4
+ * Every method here gates on `requireCanManage(req, id, 'members')`. The
5
+ * owner-count preconditions on PATCH and DELETE come from `checkOwnerRemoval`,
6
+ * a pure rules function, so demotion and removal cannot drift apart — a sole
7
+ * owner demoting themselves locks the project exactly as removing themselves
8
+ * would.
9
+ */
10
+ import type { ApiHandler } from '../api/index.js';
11
+ export declare const listProjectMembers: ApiHandler;
12
+ export declare const addProjectMember: ApiHandler;
13
+ export declare const updateProjectMemberRole: ApiHandler;
14
+ export declare const removeProjectMember: ApiHandler;
15
+ //# sourceMappingURL=projectMembers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projectMembers.d.ts","sourceRoot":"","sources":["../../src/handlers/projectMembers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAeH,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,iBAAiB,CAAC;AAI9D,eAAO,MAAM,kBAAkB,EAAE,UAMhC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,UAwB9B,CAAC;AAuCF,eAAO,MAAM,uBAAuB,EAAE,UAmBrC,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,UAejC,CAAC"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Project membership: list, add, change role, remove.
3
+ *
4
+ * Every method here gates on `requireCanManage(req, id, 'members')`. The
5
+ * owner-count preconditions on PATCH and DELETE come from `checkOwnerRemoval`,
6
+ * a pure rules function, so demotion and removal cannot drift apart — a sole
7
+ * owner demoting themselves locks the project exactly as removing themselves
8
+ * would.
9
+ */
10
+ import { AddProjectMemberBodySchema, apiError, ApiErrorCode, collection, created, noContent, parseBody, parseListOptions, requireCaller, requireParams, UpdateProjectMemberBodySchema } from '../api/index.js';
11
+ import { checkOwnerRemoval } from '@selvajs/platform';
12
+ import { requireCanManage, requireTargetIsOrgMember } from '../access/index.js';
13
+ export const listProjectMembers = async (req) => {
14
+ const { id } = requireParams(req.params, 'id');
15
+ const { ctx } = requireCaller(req);
16
+ await requireCanManage(req, id, 'members');
17
+ return collection(await req.deps.projects.listProjectMembers(ctx, id, parseListOptions(req.url)));
18
+ };
19
+ export const addProjectMember = async (req) => {
20
+ const { id } = requireParams(req.params, 'id');
21
+ const { ctx } = requireCaller(req);
22
+ await requireCanManage(req, id, 'members');
23
+ const input = await parseBody(req.request, AddProjectMemberBodySchema);
24
+ const project = await req.deps.projects.getProject(ctx, id);
25
+ if (!project)
26
+ apiError(404, ApiErrorCode.NOT_FOUND, 'Project not found');
27
+ await requireTargetIsOrgMember(req, project.orgId, input.userId);
28
+ const now = new Date().toISOString();
29
+ const member = {
30
+ projectId: id,
31
+ userId: input.userId,
32
+ role: input.role,
33
+ joinedAt: now,
34
+ updatedAt: now,
35
+ updatedBy: ctx.userId || input.userId,
36
+ deletedAt: null
37
+ };
38
+ await req.deps.projects.addProjectMember(ctx, member);
39
+ return created(member);
40
+ };
41
+ /**
42
+ * Guard the owner count before a demotion or removal goes through.
43
+ *
44
+ * `?confirm=true` is what distinguishes "I meant to do this" from a misclick;
45
+ * the sole-owner case has no confirmation, because there is no state the
46
+ * caller could confirm into that leaves the project manageable.
47
+ */
48
+ async function assertOwnerRemovable(req, projectId, target, action) {
49
+ const { ctx } = requireCaller(req);
50
+ const page = await req.deps.projects.listProjectMembers(ctx, projectId, { limit: 200 });
51
+ const decision = checkOwnerRemoval({
52
+ target: { role: target.role },
53
+ allMembers: page.items.map((m) => ({ role: m.role })),
54
+ confirmed: req.url.searchParams.get('confirm') === 'true'
55
+ });
56
+ if (decision === 'sole_owner') {
57
+ apiError(409, ApiErrorCode.CONFLICT, `Cannot ${action} the sole owner of a project. Assign another owner first, or use reclaim to add a co-owner.`);
58
+ }
59
+ if (decision === 'needs_confirm') {
60
+ apiError(409, ApiErrorCode.CONFLICT, `${action === 'demote' ? 'Demoting' : 'Removing'} another project owner requires explicit confirmation. Retry with ?confirm=true.`);
61
+ }
62
+ }
63
+ export const updateProjectMemberRole = async (req) => {
64
+ const { id, userId } = requireParams(req.params, 'id', 'userId');
65
+ const { ctx } = requireCaller(req);
66
+ await requireCanManage(req, id, 'members');
67
+ const { role } = await parseBody(req.request, UpdateProjectMemberBodySchema);
68
+ // Demoting an owner reduces the owner count exactly like removing one, so
69
+ // it runs the same guard DELETE does. Without this a sole owner can PATCH
70
+ // themselves to `viewer` and lock the project — `canManage`,
71
+ // `canEditProjectSettings` and `canEdit` all go false at once.
72
+ if (role !== 'owner') {
73
+ const target = await req.deps.projects.getProjectMember(ctx, id, userId);
74
+ if (target?.role === 'owner') {
75
+ await assertOwnerRemovable(req, id, target, 'demote');
76
+ }
77
+ }
78
+ await req.deps.projects.updateProjectMemberRole(ctx, id, userId, role);
79
+ return noContent();
80
+ };
81
+ export const removeProjectMember = async (req) => {
82
+ const { id, userId } = requireParams(req.params, 'id', 'userId');
83
+ const { ctx } = requireCaller(req);
84
+ await requireCanManage(req, id, 'members');
85
+ const target = await req.deps.projects.getProjectMember(ctx, id, userId);
86
+ // Idempotent: already gone, or never there.
87
+ if (!target)
88
+ return noContent();
89
+ if (target.role === 'owner') {
90
+ await assertOwnerRemovable(req, id, target, 'remove');
91
+ }
92
+ await req.deps.projects.removeProjectMember(ctx, id, userId);
93
+ return noContent();
94
+ };
95
+ //# sourceMappingURL=projectMembers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projectMembers.js","sourceRoot":"","sources":["../../src/handlers/projectMembers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Project handlers: the collection, and a single project's detail and settings.
3
+ *
4
+ * `listProjects` was authored against `resolveAccessibleProjects`, not lifted
5
+ * from the pre-v1 handler: that one listed every project in the acting org with
6
+ * no `canView` filter and had no UI caller, so it carried no evidence of being
7
+ * correct. The rest are lifted, logic intact, with reads moved onto `req.deps`.
8
+ */
9
+ import { type ApiHandler } from '../api/index.js';
10
+ export declare const listProjects: ApiHandler;
11
+ export declare const createProject: ApiHandler;
12
+ /**
13
+ * Project detail plus the caller's effective capabilities on it.
14
+ *
15
+ * A project the caller cannot view returns 404, not 403 — `403` would confirm
16
+ * the id exists to anyone probing.
17
+ */
18
+ export declare const getProject: ApiHandler;
19
+ export declare const updateProject: ApiHandler;
20
+ export declare const deleteProject: ApiHandler;
21
+ //# sourceMappingURL=projects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/handlers/projects.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAYN,KAAK,UAAU,EACf,MAAM,iBAAiB,CAAC;AAsBzB,eAAO,MAAM,YAAY,EAAE,UAkB1B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,UAiD3B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,UAiCxB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,UAuE3B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,UAO3B,CAAC"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Project handlers: the collection, and a single project's detail and settings.
3
+ *
4
+ * `listProjects` was authored against `resolveAccessibleProjects`, not lifted
5
+ * from the pre-v1 handler: that one listed every project in the acting org with
6
+ * no `canView` filter and had no UI caller, so it carried no evidence of being
7
+ * correct. The rest are lifted, logic intact, with reads moved onto `req.deps`.
8
+ */
9
+ import { randomUUID } from 'node:crypto';
10
+ import { apiError, ApiErrorCode, collection, created, noContent, parseBody, parseListOptions, requireCaller, requireParams, CreateProjectBodySchema, UpdateProjectBodySchema } from '../api/index.js';
11
+ import { canChangeVisibilityToPublic, canEdit, canSolve, canView, hasPermission, slugify, SYSTEM_CONTEXT, validateProjectFlags, withAdminBypass } from '@selvajs/platform';
12
+ import { resolveAccessibleProjects } from '../definitions/index.js';
13
+ import { projectAccessInputFromRowsWith, requireCanCreateProject, requireCanEditProjectSettings, requireCanManage } from '../access/index.js';
14
+ import { createProjectWithUniqueSlug } from '../projects/index.js';
15
+ export const listProjects = async (req) => {
16
+ const { ctx } = requireCaller(req);
17
+ const { projects } = await resolveAccessibleProjects(ctx, req.deps);
18
+ const { limit, cursor } = parseListOptions(req.url);
19
+ // The accessible set is resolved in-process rather than by the store, so the
20
+ // cursor is an index into it. Opaque to callers either way.
21
+ const start = cursor ? Number(cursor) : 0;
22
+ if (!Number.isInteger(start) || start < 0) {
23
+ apiError(400, ApiErrorCode.VALIDATION_FAILED, 'Invalid cursor');
24
+ }
25
+ const items = projects.slice(start, start + (limit ?? projects.length));
26
+ const nextIndex = start + items.length;
27
+ return collection({
28
+ items,
29
+ nextCursor: nextIndex < projects.length ? String(nextIndex) : undefined
30
+ });
31
+ };
32
+ export const createProject = async (req) => {
33
+ const { ctx, user } = requireCaller(req);
34
+ if (!ctx.actingOrgId)
35
+ apiError(400, ApiErrorCode.VALIDATION_FAILED, 'No active organization');
36
+ // Throws SvelteKit's `error()`; the host binding folds that into the
37
+ // envelope. Guards stay shared with the page loads that need them.
38
+ await requireCanCreateProject(req, ctx.actingOrgId);
39
+ const input = await parseBody(req.request, CreateProjectBodySchema);
40
+ // Same gate as PATCH: a `platform` project belongs to no org, so creating
41
+ // one here would put it beyond its own org's reach — and beyond anyone's
42
+ // with ENABLE_PLATFORM_PROJECTS off. Platform projects are created
43
+ // through /api/admin/projects.
44
+ if (input.visibility === 'platform' && !hasPermission(ctx, 'instance_admin')) {
45
+ apiError(403, ApiErrorCode.FORBIDDEN, 'Only a platform admin can create a project with platform visibility.');
46
+ }
47
+ const autoJoinOnUpload = input.autoJoinOnUpload ?? false;
48
+ // Same call PATCH makes, rather than a second hand-rolled copy of the
49
+ // flag/visibility invariant.
50
+ const flagIssues = validateProjectFlags({ visibility: input.visibility, autoJoinOnUpload });
51
+ if (flagIssues.length > 0) {
52
+ apiError(400, ApiErrorCode.VALIDATION_FAILED, flagIssues.map((i) => `${i.path}: ${i.message}`).join('; '));
53
+ }
54
+ const project = await createProjectWithUniqueSlug(req.deps.projects, {
55
+ id: randomUUID(),
56
+ orgId: ctx.actingOrgId,
57
+ name: input.name,
58
+ description: input.description,
59
+ visibility: input.visibility,
60
+ ownerId: user.id,
61
+ createdBy: user.id,
62
+ updatedBy: user.id,
63
+ autoJoinOnUpload
64
+ }, { writeCtx: ctx, fallbackSlug: 'project', conflictScope: 'this organization' });
65
+ return created(project);
66
+ };
67
+ /**
68
+ * Project detail plus the caller's effective capabilities on it.
69
+ *
70
+ * A project the caller cannot view returns 404, not 403 — `403` would confirm
71
+ * the id exists to anyone probing.
72
+ */
73
+ export const getProject = async (req) => {
74
+ const { id } = requireParams(req.params, 'id');
75
+ const { ctx } = requireCaller(req);
76
+ // Read as SYSTEM to decide visibility here rather than let the store decide:
77
+ // the 404-not-403 rule above needs the row before it can hide it.
78
+ const project = await req.deps.projects.getProject(SYSTEM_CONTEXT, id);
79
+ if (!project)
80
+ apiError(404, ApiErrorCode.NOT_FOUND, 'Project not found');
81
+ const [orgMembers, projectMembers, grants] = await Promise.all([
82
+ req.deps.orgs.getOrgMembersFor(SYSTEM_CONTEXT, [project.orgId], ctx.userId),
83
+ req.deps.projects.getProjectMembersFor(SYSTEM_CONTEXT, [project.id], ctx.userId),
84
+ project.visibility === 'platform'
85
+ ? req.deps.platformProjectGrants.listByProject(SYSTEM_CONTEXT, project.id)
86
+ : Promise.resolve([])
87
+ ]);
88
+ const member = projectMembers.get(project.id) ?? null;
89
+ const input = projectAccessInputFromRowsWith(req, ctx, project, {
90
+ member,
91
+ orgMember: orgMembers.get(project.orgId) ?? null,
92
+ platformGrants: grants
93
+ });
94
+ if (!canView(input))
95
+ apiError(404, ApiErrorCode.NOT_FOUND, 'Project not found');
96
+ return {
97
+ body: {
98
+ ...project,
99
+ role: member?.role ?? null,
100
+ canEdit: canEdit(input),
101
+ canSolve: canSolve(input)
102
+ }
103
+ };
104
+ };
105
+ export const updateProject = async (req) => {
106
+ const { id } = requireParams(req.params, 'id');
107
+ // `requireCanEditProjectSettings` is owner-only at the project level — that
108
+ // IS the gate for editing settings. The platform-scope `manage_projects`
109
+ // check that used to live here was redundant for org owners/admins (they
110
+ // always carry it via DEFAULT_ORG_PERMISSIONS) and a regression for plain
111
+ // org members who happened to own a project — they'd hit 403 on their own
112
+ // project's settings.
113
+ const { ctx, project: existing } = await requireCanEditProjectSettings(req, id);
114
+ const input = await parseBody(req.request, UpdateProjectBodySchema);
115
+ // `platform` takes a project out of its org entirely: no org member can
116
+ // see it, `canReclaim` returns false forever, and with
117
+ // ENABLE_PLATFORM_PROJECTS off (the default) every rule denies everyone,
118
+ // including admins. Escalating into it must be instance-admin only.
119
+ if (input.visibility === 'platform' && existing.visibility !== 'platform') {
120
+ if (!hasPermission(ctx, 'instance_admin')) {
121
+ apiError(403, ApiErrorCode.FORBIDDEN, 'Only a platform admin can give a project platform visibility.');
122
+ }
123
+ }
124
+ // Flipping *to* public is a disclosure action — a stricter gate than a
125
+ // normal edit.
126
+ if (input.visibility === 'public' && existing.visibility !== 'public') {
127
+ const orgMember = await req.deps.orgs.getOrgMember(ctx, existing.orgId, ctx.userId);
128
+ const passes = withAdminBypass(ctx.platformPermissions, () => canChangeVisibilityToPublic({ orgMember }));
129
+ if (!passes) {
130
+ apiError(403, ApiErrorCode.FORBIDDEN, 'Only org owners or admins can make a project public.');
131
+ }
132
+ }
133
+ // The flag/visibility invariant runs on the merged post-patch shape, so a
134
+ // partial update is validated against what the project will become.
135
+ const flagIssues = validateProjectFlags({
136
+ visibility: input.visibility ?? existing.visibility,
137
+ autoJoinOnUpload: input.autoJoinOnUpload ?? existing.autoJoinOnUpload
138
+ });
139
+ if (flagIssues.length > 0) {
140
+ apiError(400, ApiErrorCode.VALIDATION_FAILED, flagIssues.map((i) => `${i.path}: ${i.message}`).join('; '));
141
+ }
142
+ const patch = {};
143
+ // A renamed project gets a re-derived slug; the two must not drift apart.
144
+ if (input.name !== undefined) {
145
+ patch.name = input.name;
146
+ patch.slug = slugify(input.name);
147
+ }
148
+ if (input.description !== undefined)
149
+ patch.description = input.description ?? undefined;
150
+ if (input.visibility !== undefined)
151
+ patch.visibility = input.visibility;
152
+ if (input.autoJoinOnUpload !== undefined)
153
+ patch.autoJoinOnUpload = input.autoJoinOnUpload;
154
+ await req.deps.projects.updateProject(ctx, id, patch);
155
+ return noContent();
156
+ };
157
+ export const deleteProject = async (req) => {
158
+ const { id } = requireParams(req.params, 'id');
159
+ const { ctx } = requireCaller(req);
160
+ await requireCanManage(req, id);
161
+ await req.deps.projects.deleteProject(ctx, id);
162
+ return noContent();
163
+ };
164
+ //# sourceMappingURL=projects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects.js","sourceRoot":"","sources":["../../src/handlers/projects.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Org owner/admin escape hatch for a project they cannot otherwise reach.
3
+ *
4
+ * Adds the actor as a co-owner and does NOT demote the existing owner.
5
+ * Idempotent: reclaiming twice is a no-op, because `addProjectMember`
6
+ * reactivates a soft-deleted row in place.
7
+ */
8
+ import { type ApiHandler } from '../api/index.js';
9
+ export declare const reclaimProject: ApiHandler;
10
+ //# sourceMappingURL=reclaim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reclaim.d.ts","sourceRoot":"","sources":["../../src/handlers/reclaim.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAI1E,eAAO,MAAM,cAAc,EAAE,UA8B5B,CAAC"}