@selvajs/platform 0.14.2 → 0.15.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/LICENSE +1 -1
  2. package/dist/auth.d.ts +60 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +12 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/compute.d.ts +47 -0
  7. package/dist/compute.d.ts.map +1 -0
  8. package/dist/compute.js +11 -0
  9. package/dist/compute.js.map +1 -0
  10. package/dist/computeServer/index.d.ts +2 -0
  11. package/dist/computeServer/index.d.ts.map +1 -1
  12. package/dist/computeServer/index.js +6 -0
  13. package/dist/computeServer/index.js.map +1 -1
  14. package/dist/computeServer/interface.d.ts +10 -0
  15. package/dist/computeServer/interface.d.ts.map +1 -1
  16. package/dist/computeServer/secrets.d.ts +53 -0
  17. package/dist/computeServer/secrets.d.ts.map +1 -0
  18. package/dist/computeServer/secrets.js +67 -0
  19. package/dist/computeServer/secrets.js.map +1 -0
  20. package/dist/context.d.ts +7 -0
  21. package/dist/context.d.ts.map +1 -1
  22. package/dist/context.js +13 -0
  23. package/dist/context.js.map +1 -1
  24. package/dist/data/interface.d.ts +33 -0
  25. package/dist/data/interface.d.ts.map +1 -1
  26. package/dist/definitions/interface.d.ts +18 -0
  27. package/dist/definitions/interface.d.ts.map +1 -1
  28. package/dist/definitions/schemas.d.ts +1 -0
  29. package/dist/definitions/schemas.d.ts.map +1 -1
  30. package/dist/definitions/schemas.js +3 -0
  31. package/dist/definitions/schemas.js.map +1 -1
  32. package/dist/definitions/service.d.ts +62 -0
  33. package/dist/definitions/service.d.ts.map +1 -0
  34. package/dist/definitions/service.js +149 -0
  35. package/dist/definitions/service.js.map +1 -0
  36. package/dist/definitions/types.d.ts +24 -1
  37. package/dist/definitions/types.d.ts.map +1 -1
  38. package/dist/definitions.d.ts +146 -0
  39. package/dist/definitions.d.ts.map +1 -0
  40. package/dist/definitions.js +23 -0
  41. package/dist/definitions.js.map +1 -0
  42. package/dist/errors/interface.d.ts +44 -0
  43. package/dist/errors/interface.d.ts.map +1 -0
  44. package/dist/errors/interface.js +9 -0
  45. package/dist/errors/interface.js.map +1 -0
  46. package/dist/events/interface.d.ts +23 -0
  47. package/dist/events/interface.d.ts.map +1 -1
  48. package/dist/events/interface.js.map +1 -1
  49. package/dist/index.d.ts +14 -5
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +7 -2
  52. package/dist/index.js.map +1 -1
  53. package/dist/invites/schemas.d.ts +24 -0
  54. package/dist/invites/schemas.d.ts.map +1 -0
  55. package/dist/invites/schemas.js +17 -0
  56. package/dist/invites/schemas.js.map +1 -0
  57. package/dist/metrics/interface.d.ts +6 -0
  58. package/dist/metrics/interface.d.ts.map +1 -1
  59. package/dist/metrics/interface.js.map +1 -1
  60. package/dist/organizations/index.d.ts +4 -3
  61. package/dist/organizations/index.d.ts.map +1 -1
  62. package/dist/organizations/index.js +2 -1
  63. package/dist/organizations/index.js.map +1 -1
  64. package/dist/organizations/interface.d.ts +1 -1
  65. package/dist/organizations/interface.d.ts.map +1 -1
  66. package/dist/organizations/paths.d.ts +22 -0
  67. package/dist/organizations/paths.d.ts.map +1 -0
  68. package/dist/organizations/paths.js +43 -0
  69. package/dist/organizations/paths.js.map +1 -0
  70. package/dist/organizations/schemas.d.ts +10 -0
  71. package/dist/organizations/schemas.d.ts.map +1 -1
  72. package/dist/organizations/schemas.js +6 -0
  73. package/dist/organizations/schemas.js.map +1 -1
  74. package/dist/organizations/types.d.ts +10 -1
  75. package/dist/organizations/types.d.ts.map +1 -1
  76. package/dist/organizations.d.ts +93 -0
  77. package/dist/organizations.d.ts.map +1 -0
  78. package/dist/organizations.js +14 -0
  79. package/dist/organizations.js.map +1 -0
  80. package/dist/solveCache/interface.d.ts +86 -0
  81. package/dist/solveCache/interface.d.ts.map +1 -0
  82. package/dist/solveCache/interface.js +12 -0
  83. package/dist/solveCache/interface.js.map +1 -0
  84. package/dist/storage/assetClasses.d.ts +59 -0
  85. package/dist/storage/assetClasses.d.ts.map +1 -0
  86. package/dist/storage/assetClasses.js +86 -0
  87. package/dist/storage/assetClasses.js.map +1 -0
  88. package/dist/storage/cacheBust.d.ts +21 -0
  89. package/dist/storage/cacheBust.d.ts.map +1 -0
  90. package/dist/storage/cacheBust.js +29 -0
  91. package/dist/storage/cacheBust.js.map +1 -0
  92. package/dist/storage/image.d.ts +6 -1
  93. package/dist/storage/image.d.ts.map +1 -1
  94. package/dist/storage/image.js +8 -3
  95. package/dist/storage/image.js.map +1 -1
  96. package/dist/storage/index.d.ts +3 -0
  97. package/dist/storage/index.d.ts.map +1 -1
  98. package/dist/storage/index.js +2 -0
  99. package/dist/storage/index.js.map +1 -1
  100. package/dist/testing/adapterSuite.d.ts +33 -0
  101. package/dist/testing/adapterSuite.d.ts.map +1 -0
  102. package/dist/testing/adapterSuite.js +166 -0
  103. package/dist/testing/adapterSuite.js.map +1 -0
  104. package/dist/testing/suites/definitionStoreSuite.d.ts.map +1 -1
  105. package/dist/testing/suites/definitionStoreSuite.js +87 -0
  106. package/dist/testing/suites/definitionStoreSuite.js.map +1 -1
  107. package/dist/testing/suites/eventSinkSuite.js +1 -0
  108. package/dist/testing/suites/eventSinkSuite.js.map +1 -1
  109. package/dist/testing/suites/orgStoreSuite.d.ts.map +1 -1
  110. package/dist/testing/suites/orgStoreSuite.js +12 -0
  111. package/dist/testing/suites/orgStoreSuite.js.map +1 -1
  112. package/dist/testing/suites/platformPermissionStoreSuite.d.ts +9 -0
  113. package/dist/testing/suites/platformPermissionStoreSuite.d.ts.map +1 -1
  114. package/dist/testing/suites/platformPermissionStoreSuite.js +34 -1
  115. package/dist/testing/suites/platformPermissionStoreSuite.js.map +1 -1
  116. package/dist/testing/suites/runner.d.ts +21 -0
  117. package/dist/testing/suites/runner.d.ts.map +1 -0
  118. package/dist/testing/suites/runner.js +7 -0
  119. package/dist/testing/suites/runner.js.map +1 -0
  120. package/dist/testing/suites/storageProviderSuite.d.ts.map +1 -1
  121. package/dist/testing/suites/storageProviderSuite.js +18 -0
  122. package/dist/testing/suites/storageProviderSuite.js.map +1 -1
  123. package/package.json +9 -24
  124. package/src/access/index.ts +0 -23
  125. package/src/access/rules.ts +0 -276
  126. package/src/auth/index.ts +0 -8
  127. package/src/auth/interface.ts +0 -279
  128. package/src/auth/types.ts +0 -32
  129. package/src/bindings/index.ts +0 -2
  130. package/src/bindings/interface.ts +0 -61
  131. package/src/computeServer/index.ts +0 -17
  132. package/src/computeServer/interface.ts +0 -62
  133. package/src/computeServer/types.ts +0 -74
  134. package/src/computeServer/utils.ts +0 -112
  135. package/src/config.ts +0 -99
  136. package/src/context.ts +0 -66
  137. package/src/data/index.ts +0 -1
  138. package/src/data/interface.ts +0 -70
  139. package/src/definitions/index.ts +0 -26
  140. package/src/definitions/interface.ts +0 -78
  141. package/src/definitions/paths.ts +0 -31
  142. package/src/definitions/schemas.ts +0 -64
  143. package/src/definitions/types.ts +0 -111
  144. package/src/errors.ts +0 -16
  145. package/src/events/audit.ts +0 -67
  146. package/src/events/index.ts +0 -9
  147. package/src/events/interface.ts +0 -102
  148. package/src/index.ts +0 -219
  149. package/src/invites/index.ts +0 -2
  150. package/src/invites/interface.ts +0 -38
  151. package/src/invites/types.ts +0 -39
  152. package/src/metrics/interface.ts +0 -86
  153. package/src/organizations/index.ts +0 -14
  154. package/src/organizations/interface.ts +0 -68
  155. package/src/organizations/schemas.ts +0 -52
  156. package/src/organizations/types.ts +0 -31
  157. package/src/pagination.ts +0 -45
  158. package/src/permissions/index.ts +0 -3
  159. package/src/permissions/interface.ts +0 -60
  160. package/src/permissions/types.ts +0 -21
  161. package/src/platformProjects/index.ts +0 -3
  162. package/src/platformProjects/interface.ts +0 -27
  163. package/src/platformProjects/schemas.ts +0 -13
  164. package/src/platformProjects/types.ts +0 -22
  165. package/src/projects/index.ts +0 -4
  166. package/src/projects/interface.ts +0 -58
  167. package/src/projects/schemas.ts +0 -28
  168. package/src/projects/types.ts +0 -36
  169. package/src/shareLinks/index.ts +0 -5
  170. package/src/shareLinks/interface.ts +0 -35
  171. package/src/shareLinks/schemas.ts +0 -21
  172. package/src/shareLinks/types.ts +0 -34
  173. package/src/storage/image.ts +0 -77
  174. package/src/storage/index.ts +0 -9
  175. package/src/storage/interface.ts +0 -27
  176. package/src/testing/index.ts +0 -47
  177. package/src/testing/suites/authProviderSuite.ts +0 -207
  178. package/src/testing/suites/computeServerStoreSuite.ts +0 -195
  179. package/src/testing/suites/definitionStoreSuite.ts +0 -585
  180. package/src/testing/suites/emailLinkAuthSuite.ts +0 -83
  181. package/src/testing/suites/eventSinkSuite.ts +0 -379
  182. package/src/testing/suites/helpers.ts +0 -87
  183. package/src/testing/suites/inviteStoreSuite.ts +0 -143
  184. package/src/testing/suites/orgStoreSuite.ts +0 -412
  185. package/src/testing/suites/platformPermissionStoreSuite.ts +0 -196
  186. package/src/testing/suites/platformProjectGrantStoreSuite.ts +0 -166
  187. package/src/testing/suites/projectStoreSuite.ts +0 -381
  188. package/src/testing/suites/shareLinkStoreSuite.ts +0 -203
  189. package/src/testing/suites/solveMetricSinkSuite.ts +0 -135
  190. package/src/testing/suites/storageProviderSuite.ts +0 -175
  191. package/src/testing/suites/userProfileStoreSuite.ts +0 -134
  192. package/src/userProfile/index.ts +0 -3
  193. package/src/userProfile/interface.ts +0 -48
  194. package/src/userProfile/types.ts +0 -24
  195. package/src/utils/audit.ts +0 -46
  196. package/src/utils/index.ts +0 -2
@@ -1,412 +0,0 @@
1
- /**
2
- * Adapter conformance suite for IOrgStore.
3
- *
4
- * Tests organization CRUD and org membership management to ensure all
5
- * adapters behave identically.
6
- */
7
-
8
- import { describe, it, expect } from 'vitest';
9
- import type { IOrgStore } from '../../organizations/interface.js';
10
- import type { IComputeServerStore } from '../../computeServer/interface.js';
11
- import type { IInviteStore } from '../../invites/interface.js';
12
- import type { Organization, OrgMember, OrgRole, Invite, ComputeServerConfig } from '../../index.js';
13
- import { DEFAULT_ORG_PERMISSIONS } from '../../organizations/schemas.js';
14
- import { SYSTEM_CONTEXT } from '../../context.js';
15
- import { makeSeedHelpers, makeUuid, noopSeedUser, type SeedUserFn } from './helpers.js';
16
-
17
- export interface OrgStoreConformanceOptions {
18
- /** Name to show in test output (e.g. "local-provider"). */
19
- name: string;
20
- /** Factory that returns a fresh, empty store per test. */
21
- createStore: () => Promise<IOrgStore> | IOrgStore;
22
- /**
23
- * Hook adapters with user FK constraints use to seed `auth.users` before
24
- * a conformance test references a user id. Returns the id the adapter
25
- * actually stored — callers use that, not the suggested one.
26
- */
27
- seedUser?: SeedUserFn;
28
- /** If true, skip createOrg/deleteOrg tests (e.g. single-org providers). */
29
- singleOrgMode?: boolean;
30
- /** If true, run ctx-isolation tests (adapters with row-level security). */
31
- ctxIsolation?: boolean;
32
- /**
33
- * When supplied, an extra `deleteOrg` cascade test runs that asserts
34
- * pending invites and the org's compute override are cleaned up too.
35
- * The factories must return stores wired to the same underlying state
36
- * as `createStore` (i.e. the same data provider instance for local,
37
- * the same Supabase project for the remote adapter).
38
- */
39
- createCompanionStores?: () =>
40
- | Promise<{ invites: IInviteStore; computeServer: IComputeServerStore }>
41
- | { invites: IInviteStore; computeServer: IComputeServerStore };
42
- }
43
-
44
- function org(ownerId: string, overrides: Partial<Organization> = {}): Organization {
45
- const now = new Date().toISOString();
46
- return {
47
- id: overrides.id ?? makeUuid(),
48
- name: overrides.name ?? 'Test Org',
49
- slug: overrides.slug ?? `test-${Math.random().toString(36).slice(2, 8)}`,
50
- ownerId: overrides.ownerId ?? ownerId,
51
- createdBy: overrides.createdBy ?? ownerId,
52
- updatedBy: overrides.updatedBy ?? ownerId,
53
- createdAt: overrides.createdAt ?? now,
54
- updatedAt: overrides.updatedAt ?? now,
55
- deletedAt: overrides.deletedAt ?? null,
56
- ...overrides
57
- };
58
- }
59
-
60
- function member(orgId: string, userId: string, role: OrgRole = 'member'): OrgMember {
61
- const now = new Date().toISOString();
62
- return {
63
- orgId,
64
- userId,
65
- role,
66
- permissions: [...DEFAULT_ORG_PERMISSIONS[role]],
67
- joinedAt: now,
68
- updatedAt: now,
69
- updatedBy: userId,
70
- deletedAt: null
71
- };
72
- }
73
-
74
- export function runOrgStoreConformance(opts: OrgStoreConformanceOptions): void {
75
- const {
76
- name,
77
- createStore,
78
- seedUser = noopSeedUser,
79
- singleOrgMode = false,
80
- ctxIsolation = false,
81
- createCompanionStores
82
- } = opts;
83
- const { seed, ctx } = makeSeedHelpers(seedUser);
84
-
85
- describe(`IOrgStore conformance: ${name}`, () => {
86
- // ============================================================================
87
- // Organizations
88
- // ============================================================================
89
-
90
- if (!singleOrgMode) {
91
- it('createOrg + getOrg returns the organization', async () => {
92
- const store = await createStore();
93
- const u1 = await seed();
94
- const orgId = makeUuid();
95
- await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: 'acme' }));
96
- const got = await store.getOrg(ctx(u1), orgId);
97
- expect(got?.name).toBe('Test Org');
98
- expect(got?.slug).toBe('acme');
99
- });
100
-
101
- it('getOrg returns null for missing id', async () => {
102
- const store = await createStore();
103
- const u1 = await seed();
104
- const got = await store.getOrg(ctx(u1), makeUuid());
105
- expect(got).toBeNull();
106
- });
107
-
108
- it('getOrgBySlug finds org by slug', async () => {
109
- const store = await createStore();
110
- const u1 = await seed();
111
- const orgId = makeUuid();
112
- await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: 'acme-corp' }));
113
- const got = await store.getOrgBySlug(ctx(u1), 'acme-corp');
114
- expect(got?.id).toBe(orgId);
115
- });
116
-
117
- it('getOrgBySlug returns null for missing slug', async () => {
118
- const store = await createStore();
119
- const u1 = await seed();
120
- const got = await store.getOrgBySlug(ctx(u1), 'nonexistent');
121
- expect(got).toBeNull();
122
- });
123
-
124
- it('updateOrg modifies name and slug', async () => {
125
- const store = await createStore();
126
- const u1 = await seed();
127
- const orgId = makeUuid();
128
- await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: 'old-slug' }));
129
- await store.updateOrg(ctx(u1), orgId, { name: 'New Name', slug: 'new-slug' });
130
- const got = await store.getOrg(ctx(u1), orgId);
131
- expect(got?.name).toBe('New Name');
132
- expect(got?.slug).toBe('new-slug');
133
- });
134
-
135
- it('deleteOrg removes the organization', async () => {
136
- const store = await createStore();
137
- const u1 = await seed();
138
- const orgId = makeUuid();
139
- await store.createOrg(ctx(u1), org(u1, { id: orgId }));
140
- await store.deleteOrg(ctx(u1), orgId);
141
- const got = await store.getOrg(ctx(u1), orgId);
142
- expect(got).toBeNull();
143
- });
144
-
145
- it('listOrgs returns all organizations with pagination', async () => {
146
- const store = await createStore();
147
- const u1 = await seed();
148
- for (let i = 0; i < 3; i++) {
149
- await store.createOrg(ctx(u1), org(u1));
150
- }
151
- const page = await store.listOrgs(ctx(u1), { limit: 2 });
152
- expect(page.items.length).toBe(2);
153
- expect(page.nextCursor).toBeTruthy();
154
- });
155
- } else {
156
- it('getOrg returns the default organization', async () => {
157
- const store = await createStore();
158
- const u1 = await seed();
159
- const orgs = await store.listOrgs(ctx(u1));
160
- expect(orgs.items.length > 0).toBe(true);
161
- });
162
- }
163
-
164
- // ============================================================================
165
- // Org Members
166
- // ============================================================================
167
-
168
- async function setupOrg(store: IOrgStore): Promise<{ u1: string; orgId: string }> {
169
- const u1 = await seed();
170
- const orgs = await store.listOrgs(ctx(u1));
171
- let orgId: string;
172
- if (singleOrgMode) {
173
- orgId = orgs.items[0].id;
174
- } else {
175
- orgId = makeUuid();
176
- await store.createOrg(ctx(u1), org(u1, { id: orgId }));
177
- }
178
- return { u1, orgId };
179
- }
180
-
181
- it('addOrgMember + getOrgMember returns the member', async () => {
182
- const store = await createStore();
183
- const { u1, orgId } = await setupOrg(store);
184
- const u2 = await seed();
185
- await store.addOrgMember(ctx(u1), member(orgId, u2));
186
- const got = await store.getOrgMember(ctx(u1), orgId, u2);
187
- expect(got?.role).toBe('member');
188
- });
189
-
190
- it('getOrgMember returns null for missing member', async () => {
191
- const store = await createStore();
192
- const { u1, orgId } = await setupOrg(store);
193
- const got = await store.getOrgMember(ctx(u1), orgId, makeUuid());
194
- expect(got).toBeNull();
195
- });
196
-
197
- it('updateOrgMemberRole changes role', async () => {
198
- const store = await createStore();
199
- const { u1, orgId } = await setupOrg(store);
200
- const u2 = await seed();
201
- await store.addOrgMember(ctx(u1), member(orgId, u2));
202
- await store.updateOrgMemberRole(ctx(u1), orgId, u2, 'admin');
203
- const got = await store.getOrgMember(ctx(u1), orgId, u2);
204
- expect(got?.role).toBe('admin');
205
- });
206
-
207
- it('removeOrgMember deletes the member', async () => {
208
- const store = await createStore();
209
- const { u1, orgId } = await setupOrg(store);
210
- const u2 = await seed();
211
- await store.addOrgMember(ctx(u1), member(orgId, u2));
212
- await store.removeOrgMember(ctx(u1), orgId, u2);
213
- const got = await store.getOrgMember(ctx(u1), orgId, u2);
214
- expect(got).toBeNull();
215
- });
216
-
217
- it('findUserMembership returns one (org, member) for a member', async () => {
218
- const store = await createStore();
219
- const { u1, orgId } = await setupOrg(store);
220
- const u2 = await seed();
221
- await store.addOrgMember(ctx(u1), member(orgId, u2));
222
-
223
- // Looked up via SYSTEM_CONTEXT — the bootstrap path is the primary
224
- // caller and runs before a per-user ctx exists.
225
- const found = await store.findUserMembership(SYSTEM_CONTEXT, u2);
226
- expect(found).not.toBeNull();
227
- expect(found!.org.id).toBe(orgId);
228
- expect(found!.member.userId).toBe(u2);
229
- expect(found!.member.role).toBe('member');
230
- });
231
-
232
- it('findUserMembership returns null for a user with no memberships', async () => {
233
- const store = await createStore();
234
- await setupOrg(store); // ensure at least one org exists
235
- const stranger = await seed();
236
-
237
- const found = await store.findUserMembership(SYSTEM_CONTEXT, stranger);
238
- expect(found).toBeNull();
239
- });
240
-
241
- it('findUserMembership skips soft-deleted memberships', async () => {
242
- const store = await createStore();
243
- const { u1, orgId } = await setupOrg(store);
244
- const u2 = await seed();
245
- await store.addOrgMember(ctx(u1), member(orgId, u2));
246
- await store.removeOrgMember(ctx(u1), orgId, u2);
247
-
248
- const found = await store.findUserMembership(SYSTEM_CONTEXT, u2);
249
- expect(found).toBeNull();
250
- });
251
-
252
- it('findUserMembership skips memberships in soft-deleted orgs', async () => {
253
- const store = await createStore();
254
- const { u1, orgId } = await setupOrg(store);
255
- // u1 is a member of `orgId` — kill the org and verify the lookup
256
- // returns null (membership in a dead org should not surface).
257
- await store.deleteOrg(ctx(u1), orgId);
258
-
259
- const found = await store.findUserMembership(SYSTEM_CONTEXT, u1);
260
- expect(found).toBeNull();
261
- });
262
-
263
- it('listOrgMembers returns members with pagination', async () => {
264
- const store = await createStore();
265
- const { u1, orgId } = await setupOrg(store);
266
- for (let i = 0; i < 3; i++) {
267
- const u = await seed();
268
- await store.addOrgMember(ctx(u1), member(orgId, u));
269
- }
270
- const page = await store.listOrgMembers(ctx(u1), orgId, { limit: 2 });
271
- expect(page.items.length).toBe(2);
272
- });
273
-
274
- // ============================================================================
275
- // Audit fields + soft delete
276
- // ============================================================================
277
-
278
- if (!singleOrgMode) {
279
- it('createOrg populates createdBy and updatedBy', async () => {
280
- const store = await createStore();
281
- const u1 = await seed();
282
- const orgId = makeUuid();
283
- await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: `audit-${orgId.slice(0, 8)}` }));
284
- const got = await store.getOrg(ctx(u1), orgId);
285
- expect(got?.createdBy).toBe(u1);
286
- expect(got?.updatedBy).toBe(u1);
287
- expect(got?.deletedAt ?? null).toBeNull();
288
- });
289
-
290
- it('updateOrg advances updatedBy to the caller, preserves createdBy', async () => {
291
- const store = await createStore();
292
- const u1 = await seed();
293
- const u2 = await seed();
294
- const orgId = makeUuid();
295
- await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: `u-${orgId.slice(0, 8)}` }));
296
- // Seed u2 as admin so (if auth is enforced) they can mutate the org.
297
- await store.addOrgMember(ctx(u1), member(orgId, u2, 'admin'));
298
- await store.updateOrg(ctx(u2), orgId, { name: 'Renamed' });
299
- const got = await store.getOrg(ctx(u1), orgId);
300
- expect(got?.updatedBy).toBe(u2);
301
- expect(got?.createdBy).toBe(u1);
302
- });
303
-
304
- it('deleteOrg soft-deletes — org stops appearing in reads', async () => {
305
- const store = await createStore();
306
- const u1 = await seed();
307
- const orgId = makeUuid();
308
- const slug = `sd-${orgId.slice(0, 8)}`;
309
- await store.createOrg(ctx(u1), org(u1, { id: orgId, slug }));
310
- await store.deleteOrg(ctx(u1), orgId);
311
- expect(await store.getOrg(ctx(u1), orgId)).toBeNull();
312
- expect(await store.getOrgBySlug(ctx(u1), slug)).toBeNull();
313
- });
314
-
315
- it('deleteOrg cascades soft-delete to org members', async () => {
316
- const store = await createStore();
317
- const u1 = await seed();
318
- const u2 = await seed();
319
- const orgId = makeUuid();
320
- await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: `c-${orgId.slice(0, 8)}` }));
321
- await store.addOrgMember(ctx(u1), member(orgId, u2));
322
- await store.deleteOrg(ctx(u1), orgId);
323
- expect(await store.getOrgMember(ctx(u1), orgId, u2)).toBeNull();
324
- });
325
-
326
- if (createCompanionStores) {
327
- it('deleteOrg cascades into invites and the org compute override', async () => {
328
- const store = await createStore();
329
- const { invites, computeServer } = await createCompanionStores();
330
- const u1 = await seed();
331
- const orgId = makeUuid();
332
- await store.createOrg(ctx(u1), org(u1, { id: orgId, slug: `cc-${orgId.slice(0, 8)}` }));
333
-
334
- // Pending invite + org compute override.
335
- const invite: Invite = {
336
- id: makeUuid(),
337
- tokenHash: `hash-${makeUuid()}`,
338
- email: 'pending@example.com',
339
- orgId,
340
- orgRole: 'member',
341
- orgPermissions: [],
342
- invitedBy: u1,
343
- createdAt: new Date().toISOString(),
344
- expiresAt: new Date(Date.now() + 86_400_000).toISOString()
345
- };
346
- await invites.create(ctx(u1), invite);
347
- const orgCtx = { ...ctx(u1), actingOrgId: orgId };
348
- const server: ComputeServerConfig = {
349
- id: makeUuid(),
350
- scope: 'org',
351
- ownerOrgId: orgId,
352
- label: 'BYO',
353
- serverUrl: 'https://compute.example.com',
354
- apiKey: 'secret'
355
- };
356
- await computeServer.saveOrgServers(orgCtx, orgId, [server], server.id);
357
-
358
- await store.deleteOrg(ctx(u1), orgId);
359
-
360
- expect(await invites.getByTokenHash(ctx(u1), invite.tokenHash)).toBeNull();
361
- const remainingConfig = await computeServer.getConfig(orgCtx);
362
- const orgRows = remainingConfig.servers.filter(
363
- (s) => s.scope === 'org' && s.ownerOrgId === orgId
364
- );
365
- expect(orgRows).toEqual([]);
366
- expect(remainingConfig.orgDefaults?.[orgId]).toBeUndefined();
367
- });
368
- }
369
- }
370
-
371
- it('removeOrgMember soft-deletes — the member stops appearing in reads', async () => {
372
- const store = await createStore();
373
- const { u1, orgId } = await setupOrg(store);
374
- const u2 = await seed();
375
- await store.addOrgMember(ctx(u1), member(orgId, u2));
376
- await store.removeOrgMember(ctx(u1), orgId, u2);
377
- expect(await store.getOrgMember(ctx(u1), orgId, u2)).toBeNull();
378
- const page = await store.listOrgMembers(ctx(u1), orgId, { limit: 100 });
379
- expect(page.items.map((m) => m.userId)).not.toContain(u2);
380
- });
381
-
382
- if (ctxIsolation) {
383
- it('ctx isolation: orgs created by one user are not visible to another', async () => {
384
- const store = await createStore();
385
- const userA = await seed();
386
- const userB = await seed();
387
- const orgA = makeUuid();
388
- await store.createOrg(
389
- ctx(userA),
390
- org(userA, { id: orgA, slug: `org-a-${orgA.slice(0, 8)}` })
391
- );
392
-
393
- const page = await store.listOrgs(ctx(userB));
394
- expect(page.items.map((o) => o.id)).not.toContain(orgA);
395
- });
396
-
397
- it('ctx isolation: user cannot get an org they do not own', async () => {
398
- const store = await createStore();
399
- const userA = await seed();
400
- const userB = await seed();
401
- const orgA = makeUuid();
402
- await store.createOrg(
403
- ctx(userA),
404
- org(userA, { id: orgA, slug: `org-a-${orgA.slice(0, 8)}` })
405
- );
406
-
407
- const got = await store.getOrg(ctx(userB), orgA);
408
- expect(got).toBeNull();
409
- });
410
- }
411
- });
412
- }
@@ -1,196 +0,0 @@
1
- /**
2
- * Adapter conformance suite for IPlatformPermissionStore.
3
- *
4
- * Covers `getFor` / `getForBatch` / `set` / `hasInstanceAdmin` /
5
- * `countInstanceAdminsExcluding`, plus the §2 sole-`instance_admin`
6
- * invariant — `set` must return `'last_admin'` when dropping the only admin,
7
- * and `countInstanceAdminsExcluding` must exclude the named user.
8
- *
9
- * These tests used to live on the auth-provider conformance suite when the
10
- * invariant was enforced there; the move from `IAuthProvider` to
11
- * `IPlatformPermissionStore` brought them with it.
12
- */
13
-
14
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
15
- import type { IPlatformPermissionStore, RequestContext } from '../../index.js';
16
- import { ALL_ORG_PERMISSIONS, ALL_PLATFORM_PERMISSIONS } from '../../index.js';
17
-
18
- export interface PlatformPermissionStoreConformanceOptions {
19
- name: string;
20
- /**
21
- * Build a fresh store and a `seedUser()` helper that creates a user the
22
- * test can manage. Adapters with FK-enforced user IDs (Supabase) seed via
23
- * the auth backend; local can return any uuid.
24
- */
25
- createStore: () => Promise<{
26
- store: IPlatformPermissionStore;
27
- seedUser: () => Promise<string>;
28
- }>;
29
- cleanup?: () => Promise<void> | void;
30
- }
31
-
32
- export function runPlatformPermissionStoreConformance(
33
- opts: PlatformPermissionStoreConformanceOptions
34
- ): void {
35
- const { name, createStore, cleanup } = opts;
36
-
37
- function adminCtx(userId: string): RequestContext {
38
- return {
39
- userId,
40
- platformPermissions: [...ALL_PLATFORM_PERMISSIONS],
41
- orgPermissions: [...ALL_ORG_PERMISSIONS]
42
- };
43
- }
44
-
45
- function selfCtx(userId: string): RequestContext {
46
- return {
47
- userId,
48
- platformPermissions: [],
49
- orgPermissions: []
50
- };
51
- }
52
-
53
- describe(`IPlatformPermissionStore conformance: ${name}`, () => {
54
- beforeEach(() => {
55
- // Every test creates its own store via createStore — no shared state.
56
- });
57
-
58
- afterEach(async () => {
59
- if (cleanup) await cleanup();
60
- });
61
-
62
- it('getFor returns empty array for users with no grants', async () => {
63
- const { store, seedUser } = await createStore();
64
- const userId = await seedUser();
65
- const perms = await store.getFor(adminCtx(userId), userId);
66
- expect(perms).toEqual([]);
67
- });
68
-
69
- it('set then getFor round-trips every platform permission', async () => {
70
- const { store, seedUser } = await createStore();
71
- const userId = await seedUser();
72
- const all = ['manage_compute', 'manage_instance_users', 'manage_updates'] as const;
73
- const result = await store.set(adminCtx(userId), userId, [...all]);
74
- expect(result).toBe('ok');
75
- const fetched = await store.getFor(adminCtx(userId), userId);
76
- expect(fetched).toEqual(expect.arrayContaining([...all]));
77
- });
78
-
79
- it('set returns not_found for unknown user', async () => {
80
- const { store, seedUser } = await createStore();
81
- const adminId = await seedUser();
82
- // Random UUID that doesn't correspond to a real user
83
- const ghost = '00000000-0000-4000-8000-000000000000';
84
- const result = await store.set(adminCtx(adminId), ghost, ['instance_admin']);
85
- expect(result).toBe('not_found');
86
- });
87
-
88
- it('non-admin caller cannot set permissions for someone else', async () => {
89
- const { store, seedUser } = await createStore();
90
- const target = await seedUser();
91
- const attacker = await seedUser();
92
- await expect(store.set(selfCtx(attacker), target, ['instance_admin'])).rejects.toThrow();
93
- });
94
-
95
- it('non-admin caller cannot read another user’s permissions', async () => {
96
- const { store, seedUser } = await createStore();
97
- const target = await seedUser();
98
- const attacker = await seedUser();
99
- await expect(store.getFor(selfCtx(attacker), target)).rejects.toThrow();
100
- });
101
-
102
- it('user can read their own permissions', async () => {
103
- const { store, seedUser } = await createStore();
104
- const userId = await seedUser();
105
- const perms = await store.getFor(selfCtx(userId), userId);
106
- expect(perms).toEqual([]);
107
- });
108
-
109
- it('hasInstanceAdmin returns false on a fresh instance, true after granting', async () => {
110
- const { store, seedUser } = await createStore();
111
- const adminId = await seedUser();
112
- const sysCtx: RequestContext = {
113
- userId: '',
114
- platformPermissions: [],
115
- orgPermissions: [],
116
- system: true
117
- };
118
- expect(await store.hasInstanceAdmin(sysCtx)).toBe(false);
119
- await store.set(adminCtx(adminId), adminId, ['instance_admin']);
120
- expect(await store.hasInstanceAdmin(sysCtx)).toBe(true);
121
- });
122
-
123
- // §2 invariant — the load-bearing test.
124
- it('set refuses to remove instance_admin from the sole admin', async () => {
125
- const { store, seedUser } = await createStore();
126
- const adminId = await seedUser();
127
- await store.set(adminCtx(adminId), adminId, ['instance_admin']);
128
- const result = await store.set(adminCtx(adminId), adminId, []);
129
- expect(result).toBe('last_admin');
130
- const after = await store.getFor(adminCtx(adminId), adminId);
131
- expect(after).toContain('instance_admin');
132
- });
133
-
134
- it('set allows demotion when another instance_admin exists', async () => {
135
- const { store, seedUser } = await createStore();
136
- const adminA = await seedUser();
137
- const adminB = await seedUser();
138
- await store.set(adminCtx(adminA), adminA, ['instance_admin']);
139
- await store.set(adminCtx(adminA), adminB, ['instance_admin']);
140
- const result = await store.set(adminCtx(adminA), adminA, []);
141
- expect(result).toBe('ok');
142
- // adminB still holds it
143
- const after = await store.getFor(adminCtx(adminA), adminB);
144
- expect(after).toContain('instance_admin');
145
- });
146
-
147
- it('countInstanceAdminsExcluding excludes the named user', async () => {
148
- const { store, seedUser } = await createStore();
149
- const adminA = await seedUser();
150
- const adminB = await seedUser();
151
- await store.set(adminCtx(adminA), adminA, ['instance_admin']);
152
- await store.set(adminCtx(adminA), adminB, ['instance_admin']);
153
- const sysCtx: RequestContext = {
154
- userId: '',
155
- platformPermissions: [],
156
- orgPermissions: [],
157
- system: true
158
- };
159
- // Excluding adminA: one admin (adminB) remains
160
- expect(await store.countInstanceAdminsExcluding(sysCtx, adminA)).toBe(1);
161
- // Excluding adminB: one admin (adminA) remains
162
- expect(await store.countInstanceAdminsExcluding(sysCtx, adminB)).toBe(1);
163
- });
164
-
165
- it('countInstanceAdminsExcluding returns 0 when target is the sole admin', async () => {
166
- const { store, seedUser } = await createStore();
167
- const adminId = await seedUser();
168
- await store.set(adminCtx(adminId), adminId, ['instance_admin']);
169
- const sysCtx: RequestContext = {
170
- userId: '',
171
- platformPermissions: [],
172
- orgPermissions: [],
173
- system: true
174
- };
175
- expect(await store.countInstanceAdminsExcluding(sysCtx, adminId)).toBe(0);
176
- });
177
-
178
- it('getForBatch returns a map keyed by userId', async () => {
179
- const { store, seedUser } = await createStore();
180
- const adminId = await seedUser();
181
- const memberId = await seedUser();
182
- await store.set(adminCtx(adminId), adminId, ['instance_admin']);
183
- await store.set(adminCtx(adminId), memberId, ['manage_compute']);
184
- const map = await store.getForBatch(adminCtx(adminId), [adminId, memberId]);
185
- expect(map.get(adminId)).toContain('instance_admin');
186
- expect(map.get(memberId)).toContain('manage_compute');
187
- });
188
-
189
- it('getForBatch from a non-admin caller is rejected', async () => {
190
- const { store, seedUser } = await createStore();
191
- const attacker = await seedUser();
192
- const target = await seedUser();
193
- await expect(store.getForBatch(selfCtx(attacker), [target])).rejects.toThrow();
194
- });
195
- });
196
- }