@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,379 +0,0 @@
1
- /**
2
- * Adapter conformance suite for the event-sink contract (Permissions spec §9).
3
- *
4
- * Verifies that each provider's stores emit the expected `DomainEvent` shape
5
- * after the corresponding mutation succeeds, and that emit happens AFTER the
6
- * write (so a failed write does not emit). Both reference providers run the
7
- * same suite — drift between Local and Supabase emission shape gets caught
8
- * immediately.
9
- *
10
- * Local stays on `NoopEventSink`; Supabase wires `SupabaseEventSink` so events
11
- * persist to `public.audit_events`. The suite guarantees the seam exists and
12
- * the shape is consistent so future webhook / audit / analytics consumers can
13
- * plug in by swapping the sink.
14
- */
15
-
16
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
17
- import type {
18
- IDataProvider,
19
- DomainEvent,
20
- IEventSink,
21
- Organization,
22
- Project,
23
- OrgMember,
24
- DefinitionRecord,
25
- DefinitionVersion,
26
- ShareLink,
27
- RequestContext
28
- } from '../../index.js';
29
- import { ALL_ORG_PERMISSIONS, ALL_PLATFORM_PERMISSIONS } from '../../index.js';
30
- import { makeUuid } from './helpers.js';
31
-
32
- /** Records every event for assertion. Resets per test via `clear()`. */
33
- export class RecordingEventSink implements IEventSink {
34
- readonly events: DomainEvent[] = [];
35
-
36
- async emit(event: DomainEvent): Promise<void> {
37
- this.events.push(event);
38
- }
39
-
40
- clear(): void {
41
- this.events.length = 0;
42
- }
43
-
44
- last(): DomainEvent | undefined {
45
- return this.events[this.events.length - 1];
46
- }
47
-
48
- ofType<T extends DomainEvent['type']>(type: T): Extract<DomainEvent, { type: T }>[] {
49
- return this.events.filter((e): e is Extract<DomainEvent, { type: T }> => e.type === type);
50
- }
51
- }
52
-
53
- export interface EventSinkConformanceOptions {
54
- name: string;
55
- /**
56
- * Build a fresh data provider wired to the given sink. Adapters that need
57
- * to seed FK-related state (Supabase: `auth.users` rows) do it here.
58
- */
59
- createProvider: (sink: RecordingEventSink) => Promise<IDataProvider> | IDataProvider;
60
- /**
61
- * The actor user id that mutations will be performed as. The suite needs
62
- * this to be a real id in the adapter's auth backend (Supabase has FK).
63
- * Local can return any uuid.
64
- *
65
- * `sessionToken` is required for adapters with DB-side auth (Supabase RLS)
66
- * so the per-test `ctx` runs as that authenticated user. Local can omit.
67
- */
68
- createActorId: () => Promise<{ userId: string; sessionToken?: string }>;
69
- /**
70
- * Optional hook for cleaning up after the test (delete tempdir, truncate
71
- * Supabase tables, etc.). Called once at the end of each test.
72
- */
73
- cleanup?: () => Promise<void> | void;
74
- }
75
-
76
- export function runEventSinkConformance(opts: EventSinkConformanceOptions): void {
77
- const { name, createProvider, createActorId, cleanup } = opts;
78
-
79
- describe(`IEventSink conformance: ${name}`, () => {
80
- let sink: RecordingEventSink;
81
- let provider: IDataProvider;
82
- let actorId: string;
83
- let ctx: RequestContext;
84
-
85
- beforeEach(async () => {
86
- sink = new RecordingEventSink();
87
- provider = await createProvider(sink);
88
- const seeded = await createActorId();
89
- actorId = seeded.userId;
90
- ctx = {
91
- userId: actorId,
92
- platformPermissions: [...ALL_PLATFORM_PERMISSIONS],
93
- orgPermissions: [...ALL_ORG_PERMISSIONS],
94
- adapterContext: seeded.sessionToken ? { sessionToken: seeded.sessionToken } : undefined
95
- };
96
- });
97
-
98
- // Clean up after each test if the adapter supplies a hook. Always
99
- // registered (calling an undefined cleanup is the no-op path).
100
- afterEach(async () => {
101
- if (cleanup) await cleanup();
102
- });
103
-
104
- it('createOrg emits org.created', async () => {
105
- const orgId = makeUuid();
106
- await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
107
-
108
- const created = sink.ofType('org.created');
109
- expect(created).toHaveLength(1);
110
- expect(created[0]).toMatchObject({ orgId, actorId });
111
- });
112
-
113
- it('deleteOrg emits org.deleted', async () => {
114
- const orgId = makeUuid();
115
- await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
116
- sink.clear();
117
-
118
- await provider.orgs.deleteOrg(ctx, orgId);
119
-
120
- expect(sink.ofType('org.deleted')).toMatchObject([{ orgId, actorId }]);
121
- });
122
-
123
- it('addOrgMember emits org_member.added', async () => {
124
- const orgId = makeUuid();
125
- const { userId: newMemberId } = await createActorId();
126
- await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
127
- sink.clear();
128
-
129
- await provider.orgs.addOrgMember(ctx, makeOrgMember({ orgId, userId: newMemberId }));
130
-
131
- expect(sink.ofType('org_member.added')).toMatchObject([
132
- { orgId, userId: newMemberId, actorId }
133
- ]);
134
- });
135
-
136
- it('removeOrgMember emits org_member.removed', async () => {
137
- const orgId = makeUuid();
138
- const { userId: newMemberId } = await createActorId();
139
- await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
140
- await provider.orgs.addOrgMember(ctx, makeOrgMember({ orgId, userId: newMemberId }));
141
- sink.clear();
142
-
143
- await provider.orgs.removeOrgMember(ctx, orgId, newMemberId);
144
-
145
- expect(sink.ofType('org_member.removed')).toMatchObject([
146
- { orgId, userId: newMemberId, actorId }
147
- ]);
148
- });
149
-
150
- it('createProject emits project.created with the parent orgId', async () => {
151
- const orgId = makeUuid();
152
- const projectId = makeUuid();
153
- await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
154
- sink.clear();
155
-
156
- await provider.projects.createProject(
157
- ctx,
158
- makeProject({ id: projectId, orgId, ownerId: actorId })
159
- );
160
-
161
- expect(sink.ofType('project.created')).toMatchObject([{ projectId, orgId, actorId }]);
162
- });
163
-
164
- it('deleteProject emits project.deleted', async () => {
165
- const orgId = makeUuid();
166
- const projectId = makeUuid();
167
- await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
168
- await provider.projects.createProject(
169
- ctx,
170
- makeProject({ id: projectId, orgId, ownerId: actorId })
171
- );
172
- sink.clear();
173
-
174
- await provider.projects.deleteProject(ctx, projectId);
175
-
176
- expect(sink.ofType('project.deleted')).toMatchObject([{ projectId, actorId }]);
177
- });
178
-
179
- it('createDefinition + setLiveVersion emit created + published', async () => {
180
- const orgId = makeUuid();
181
- const projectId = makeUuid();
182
- const definitionId = makeUuid();
183
- const versionId = makeUuid();
184
-
185
- await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
186
- await provider.projects.createProject(
187
- ctx,
188
- makeProject({ id: projectId, orgId, ownerId: actorId })
189
- );
190
- sink.clear();
191
-
192
- await provider.definitions.create(
193
- ctx,
194
- makeDefinition({ guid: definitionId, projectId, ownerId: actorId })
195
- );
196
- await provider.definitions.createVersion(
197
- ctx,
198
- makeVersion({ id: versionId, definitionId, uploadedBy: actorId })
199
- );
200
- await provider.definitions.setLiveVersion(ctx, definitionId, versionId);
201
-
202
- expect(sink.ofType('definition.created')).toMatchObject([
203
- { definitionId, projectId, actorId }
204
- ]);
205
- expect(sink.ofType('definition_version.created')).toMatchObject([
206
- { versionId, definitionId, actorId }
207
- ]);
208
- expect(sink.ofType('definition.published')).toMatchObject([
209
- { definitionId, versionId, actorId }
210
- ]);
211
- });
212
-
213
- it('share-link create + revoke emit minted + revoked', async () => {
214
- const orgId = makeUuid();
215
- const projectId = makeUuid();
216
- const definitionId = makeUuid();
217
- const linkId = makeUuid();
218
-
219
- await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
220
- await provider.projects.createProject(
221
- ctx,
222
- makeProject({ id: projectId, orgId, ownerId: actorId })
223
- );
224
- await provider.definitions.create(
225
- ctx,
226
- makeDefinition({ guid: definitionId, projectId, ownerId: actorId })
227
- );
228
- sink.clear();
229
-
230
- await provider.shareLinks.create(
231
- ctx,
232
- makeShareLink({ id: linkId, definitionId, createdBy: actorId })
233
- );
234
- await provider.shareLinks.revoke(ctx, linkId);
235
-
236
- expect(sink.ofType('share_link.minted')).toMatchObject([{ linkId, definitionId, actorId }]);
237
- expect(sink.ofType('share_link.revoked')).toMatchObject([{ linkId, actorId }]);
238
- });
239
-
240
- it('failed write does not emit', async () => {
241
- const orgId = makeUuid();
242
- await provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }));
243
- sink.clear();
244
- // Duplicate-id create must throw and must not emit.
245
- await expect(
246
- provider.orgs.createOrg(ctx, makeOrg({ id: orgId, ownerId: actorId }))
247
- ).rejects.toThrow();
248
- expect(sink.ofType('org.created')).toHaveLength(0);
249
- });
250
-
251
- it('system-context mutations emit with actorId="system"', async () => {
252
- const sysCtx: RequestContext = {
253
- userId: '',
254
- platformPermissions: [...ALL_PLATFORM_PERMISSIONS],
255
- orgPermissions: [...ALL_ORG_PERMISSIONS],
256
- system: true
257
- };
258
- const orgId = makeUuid();
259
- await provider.orgs.createOrg(sysCtx, makeOrg({ id: orgId, ownerId: actorId }));
260
-
261
- const created = sink.ofType('org.created');
262
- expect(created).toHaveLength(1);
263
- expect(created[0].actorId).toBe('system');
264
- });
265
- });
266
- }
267
-
268
- // ============================================================================
269
- // Test data helpers
270
- // ============================================================================
271
- function nowIso(): string {
272
- return new Date().toISOString();
273
- }
274
-
275
- function makeOrg(overrides: { id: string; ownerId: string }): Organization {
276
- const now = nowIso();
277
- return {
278
- id: overrides.id,
279
- name: `Org ${overrides.id.slice(0, 6)}`,
280
- slug: `org-${overrides.id.slice(0, 8)}`,
281
- ownerId: overrides.ownerId,
282
- createdBy: overrides.ownerId,
283
- updatedBy: overrides.ownerId,
284
- createdAt: now,
285
- updatedAt: now,
286
- deletedAt: null
287
- };
288
- }
289
-
290
- function makeOrgMember(overrides: { orgId: string; userId: string }): OrgMember {
291
- const now = nowIso();
292
- return {
293
- orgId: overrides.orgId,
294
- userId: overrides.userId,
295
- role: 'member',
296
- permissions: [],
297
- joinedAt: now,
298
- updatedAt: now,
299
- updatedBy: overrides.userId,
300
- deletedAt: null
301
- };
302
- }
303
-
304
- function makeProject(overrides: { id: string; orgId: string; ownerId: string }): Project {
305
- const now = nowIso();
306
- return {
307
- id: overrides.id,
308
- orgId: overrides.orgId,
309
- name: `Project ${overrides.id.slice(0, 6)}`,
310
- slug: `proj-${overrides.id.slice(0, 8)}`,
311
- visibility: 'private',
312
- autoJoinOnUpload: false,
313
- ownerId: overrides.ownerId,
314
- createdBy: overrides.ownerId,
315
- updatedBy: overrides.ownerId,
316
- createdAt: now,
317
- updatedAt: now,
318
- deletedAt: null
319
- };
320
- }
321
-
322
- function makeDefinition(overrides: {
323
- guid: string;
324
- projectId: string;
325
- ownerId: string;
326
- }): DefinitionRecord {
327
- const now = nowIso();
328
- return {
329
- guid: overrides.guid,
330
- projectId: overrides.projectId,
331
- ownerId: overrides.ownerId,
332
- createdBy: overrides.ownerId,
333
- updatedBy: overrides.ownerId,
334
- displayName: `Def ${overrides.guid.slice(0, 6)}`,
335
- status: 'published',
336
- solveCount: 0,
337
- liveVersionId: null,
338
- draftVersionId: null,
339
- createdAt: now,
340
- updatedAt: now,
341
- deletedAt: null
342
- };
343
- }
344
-
345
- function makeVersion(overrides: {
346
- id: string;
347
- definitionId: string;
348
- uploadedBy: string;
349
- }): DefinitionVersion {
350
- return {
351
- id: overrides.id,
352
- definitionId: overrides.definitionId,
353
- versionNumber: 1,
354
- fileExt: 'gh',
355
- fileKey: `defs/${overrides.definitionId}/v/${overrides.id}/file.gh`,
356
- uploadedBy: overrides.uploadedBy,
357
- uploadedAt: nowIso()
358
- };
359
- }
360
-
361
- function makeShareLink(overrides: {
362
- id: string;
363
- definitionId: string;
364
- createdBy: string;
365
- }): ShareLink {
366
- return {
367
- id: overrides.id,
368
- definitionId: overrides.definitionId,
369
- channel: 'live',
370
- tokenHash: `hash-${overrides.id}`,
371
- createdBy: overrides.createdBy,
372
- createdAt: nowIso(),
373
- expiresAt: null,
374
- revokedAt: null,
375
- allowSolve: true,
376
- maxSolves: null,
377
- solveCount: 0
378
- };
379
- }
@@ -1,87 +0,0 @@
1
- import type { RequestContext } from '../../context.js';
2
- import { ALL_ORG_PERMISSIONS } from '../../organizations/schemas.js';
3
-
4
- /**
5
- * Build a test `RequestContext`. Defaults to a platform admin to keep existing
6
- * suites permissive; tests that verify permission gating should pass explicit
7
- * platform/org permission arrays.
8
- *
9
- * `sessionToken`, when supplied, is stuffed into `adapterContext.sessionToken`
10
- * so adapters that enforce auth at the DB layer (Supabase RLS) can run the
11
- * call as a real authenticated user. Local providers ignore it.
12
- */
13
- export function makeCtx(
14
- userId: string,
15
- opts: {
16
- actingOrgId?: string;
17
- platformPermissions?: RequestContext['platformPermissions'];
18
- orgPermissions?: RequestContext['orgPermissions'];
19
- sessionToken?: string;
20
- } = {}
21
- ): RequestContext {
22
- return {
23
- userId,
24
- actingOrgId: opts.actingOrgId,
25
- platformPermissions: opts.platformPermissions ?? ['instance_admin'],
26
- orgPermissions: opts.orgPermissions ?? [...ALL_ORG_PERMISSIONS],
27
- adapterContext: opts.sessionToken ? { sessionToken: opts.sessionToken } : undefined
28
- };
29
- }
30
-
31
- export function makeUuid(): string {
32
- return crypto.randomUUID();
33
- }
34
-
35
- /**
36
- * Optional hook adapters pass when their backend enforces referential
37
- * integrity on user ids (e.g. Supabase `auth.users`). Returns the id the
38
- * adapter actually stored AND, optionally, a session token the suite can
39
- * thread back into `makeCtx({ sessionToken })` so the resulting calls run
40
- * as that user.
41
- *
42
- * Adapters without user FKs (local JSON) return the suggested id as-is and
43
- * omit the token. Adapters with auth (Supabase) ignore the suggested id,
44
- * create a fresh user, sign in as them, and return the real id + access JWT.
45
- *
46
- * The conformance suite calls this to get a usable id *before* constructing
47
- * any record that references a user. It never assumes the suggested id is
48
- * the one used.
49
- */
50
- export type SeedUserResult = { userId: string; sessionToken?: string };
51
- export type SeedUserFn = (suggestedId: string) => Promise<SeedUserResult>;
52
-
53
- /** Default no-op for adapters that don't need to seed users. Echoes the id back, no token. */
54
- export const noopSeedUser: SeedUserFn = async (id) => ({ userId: id });
55
-
56
- /**
57
- * Wraps a `SeedUserFn` so suites can keep their existing `seed() → userId` and
58
- * `ctx(userId)` shape while transparently threading session tokens for adapters
59
- * that return them. Build one per test (Map is mutable & local).
60
- */
61
- export function makeSeedHelpers(seedUser: SeedUserFn) {
62
- const tokens = new Map<string, string>();
63
-
64
- async function seed(): Promise<string> {
65
- const result = await seedUser(makeUuid());
66
- if (result.sessionToken) tokens.set(result.userId, result.sessionToken);
67
- return result.userId;
68
- }
69
-
70
- /** Register a token for a user that wasn't created via `seed()` (e.g. one returned by `createStore`). */
71
- function registerToken(userId: string, sessionToken: string): void {
72
- tokens.set(userId, sessionToken);
73
- }
74
-
75
- function ctx(
76
- userId: string,
77
- opts: {
78
- actingOrgId?: string;
79
- platformPermissions?: RequestContext['platformPermissions'];
80
- orgPermissions?: RequestContext['orgPermissions'];
81
- } = {}
82
- ): RequestContext {
83
- return makeCtx(userId, { ...opts, sessionToken: tokens.get(userId) });
84
- }
85
-
86
- return { seed, ctx, registerToken, tokens };
87
- }
@@ -1,143 +0,0 @@
1
- /**
2
- * Adapter conformance suite for IInviteStore.
3
- *
4
- * Invites are the handoff between "admin wants to add Alice" and "Alice has
5
- * an account + an org membership". The contract is small but there are a few
6
- * state-machine rules adapters must honor:
7
- *
8
- * - `getByTokenHash` returns null for missing, expired, or already-consumed
9
- * invites. The store sees only HMAC digests; the route layer hashes raw
10
- * tokens before lookup.
11
- * - `markAccepted` is idempotent (no-op if already accepted).
12
- * - `revoke` removes pending invites but must not un-consume accepted ones.
13
- */
14
-
15
- import { describe, it, expect } from 'vitest';
16
- import type { IInviteStore } from '../../invites/interface.js';
17
- import type { Invite } from '../../invites/types.js';
18
- import { SYSTEM_CONTEXT } from '../../context.js';
19
- import { makeSeedHelpers, makeUuid, noopSeedUser, type SeedUserFn } from './helpers.js';
20
-
21
- export interface InviteTestScope {
22
- /** User that creates invites. Must be seeded + have `manage_org_members` in the org. */
23
- adminId: string;
24
- /**
25
- * Optional access token for `adminId`. Required for adapters with DB-side
26
- * auth (Supabase RLS); local providers ignore it.
27
- */
28
- adminSessionToken?: string;
29
- /** Org invites are issued for. Must be seeded with the admin as a member. */
30
- orgId: string;
31
- }
32
-
33
- export interface InviteStoreConformanceOptions {
34
- name: string;
35
- createStore: () => Promise<IInviteStore> | IInviteStore;
36
- createScope?: () => Promise<InviteTestScope> | InviteTestScope;
37
- seedUser?: SeedUserFn;
38
- }
39
-
40
- function invite(scope: InviteTestScope, overrides: Partial<Invite> = {}): Invite {
41
- const now = new Date();
42
- return {
43
- id: overrides.id ?? makeUuid(),
44
- // Conformance treats `tokenHash` as opaque — the suite stores whatever
45
- // the adapter receives and looks it up by the same value. The route
46
- // layer is the one that mints raw tokens and HMACs them; the store
47
- // contract is "lookup by stored digest".
48
- tokenHash:
49
- overrides.tokenHash ?? `hash-${makeUuid()}-${Math.random().toString(36).slice(2, 8)}`,
50
- email: overrides.email ?? `invitee-${Math.random().toString(36).slice(2, 6)}@test.local`,
51
- orgId: overrides.orgId ?? scope.orgId,
52
- orgRole: overrides.orgRole ?? 'member',
53
- orgPermissions: overrides.orgPermissions ?? [],
54
- invitedBy: overrides.invitedBy ?? scope.adminId,
55
- createdAt: overrides.createdAt ?? now.toISOString(),
56
- expiresAt: overrides.expiresAt ?? new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),
57
- acceptedAt: overrides.acceptedAt,
58
- acceptedByUserId: overrides.acceptedByUserId
59
- };
60
- }
61
-
62
- const DEFAULT_SCOPE: InviteTestScope = {
63
- adminId: 'admin-1',
64
- orgId: 'org-1'
65
- };
66
-
67
- export function runInviteStoreConformance(opts: InviteStoreConformanceOptions): void {
68
- const { name, createStore, createScope, seedUser = noopSeedUser } = opts;
69
- const { ctx, registerToken } = makeSeedHelpers(seedUser);
70
- const scopeFor = async (): Promise<InviteTestScope> => {
71
- const s = createScope ? await createScope() : DEFAULT_SCOPE;
72
- if (s.adminSessionToken) registerToken(s.adminId, s.adminSessionToken);
73
- return s;
74
- };
75
-
76
- describe(`IInviteStore conformance: ${name}`, () => {
77
- it('create + getByTokenHash round-trips', async () => {
78
- const store = await createStore();
79
- const scope = await scopeFor();
80
- const inv = invite(scope);
81
- await store.create(ctx(scope.adminId), inv);
82
- const got = await store.getByTokenHash(SYSTEM_CONTEXT, inv.tokenHash);
83
- expect(got?.id).toBe(inv.id);
84
- expect(got?.email).toBe(inv.email);
85
- });
86
-
87
- it('getByTokenHash returns null for unknown hash', async () => {
88
- const store = await createStore();
89
- const got = await store.getByTokenHash(SYSTEM_CONTEXT, 'nope-' + makeUuid());
90
- expect(got).toBeNull();
91
- });
92
-
93
- it('getByTokenHash returns null for expired invite', async () => {
94
- const store = await createStore();
95
- const scope = await scopeFor();
96
- const inv = invite(scope, {
97
- expiresAt: new Date(Date.now() - 60_000).toISOString()
98
- });
99
- await store.create(ctx(scope.adminId), inv);
100
- const got = await store.getByTokenHash(SYSTEM_CONTEXT, inv.tokenHash);
101
- expect(got).toBeNull();
102
- });
103
-
104
- it('markAccepted sets acceptedAt + acceptedByUserId; getByTokenHash then returns null', async () => {
105
- const store = await createStore();
106
- const scope = await scopeFor();
107
- const { userId: acceptor } = await seedUser(makeUuid());
108
- const inv = invite(scope);
109
- await store.create(ctx(scope.adminId), inv);
110
- await store.markAccepted(SYSTEM_CONTEXT, inv.id, acceptor);
111
- const got = await store.getByTokenHash(SYSTEM_CONTEXT, inv.tokenHash);
112
- expect(got).toBeNull();
113
- });
114
-
115
- it('markAccepted is idempotent (second call is a no-op)', async () => {
116
- const store = await createStore();
117
- const scope = await scopeFor();
118
- const { userId: acceptor } = await seedUser(makeUuid());
119
- const inv = invite(scope);
120
- await store.create(ctx(scope.adminId), inv);
121
- await store.markAccepted(SYSTEM_CONTEXT, inv.id, acceptor);
122
- await store.markAccepted(SYSTEM_CONTEXT, inv.id, acceptor);
123
- });
124
-
125
- it('listByOrg returns invites for the org', async () => {
126
- const store = await createStore();
127
- const scope = await scopeFor();
128
- for (let i = 0; i < 3; i++) await store.create(ctx(scope.adminId), invite(scope));
129
- const page = await store.listByOrg(ctx(scope.adminId), scope.orgId, { limit: 100 });
130
- expect(page.items.length).toBeGreaterThanOrEqual(3);
131
- });
132
-
133
- it('revoke removes a pending invite', async () => {
134
- const store = await createStore();
135
- const scope = await scopeFor();
136
- const inv = invite(scope);
137
- await store.create(ctx(scope.adminId), inv);
138
- await store.revoke(ctx(scope.adminId), inv.id);
139
- const got = await store.getByTokenHash(SYSTEM_CONTEXT, inv.tokenHash);
140
- expect(got).toBeNull();
141
- });
142
- });
143
- }