@elevasis/core 0.8.0 → 0.8.3

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.
@@ -0,0 +1,386 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import jwt from 'jsonwebtoken';
3
+
4
+ // src/test-utils/rls/RLSTestContext.ts
5
+ var RLSTestContext = class {
6
+ adminClient;
7
+ testPrefix;
8
+ createdIds;
9
+ constructor() {
10
+ if (!process.env.SUPABASE_URL) {
11
+ throw new Error("SUPABASE_URL not configured in .env.development");
12
+ }
13
+ if (!process.env.SUPABASE_SERVICE_KEY) {
14
+ throw new Error("SUPABASE_SERVICE_KEY not configured in .env.development");
15
+ }
16
+ if (!process.env.SUPABASE_JWT_SECRET) {
17
+ throw new Error("SUPABASE_JWT_SECRET not configured in .env.development");
18
+ }
19
+ this.adminClient = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_KEY);
20
+ this.testPrefix = `test_${Date.now()}_${Math.random().toString(36).substring(7)}`;
21
+ this.createdIds = {
22
+ users: [],
23
+ organizations: [],
24
+ memberships: [],
25
+ apiKeys: [],
26
+ invitations: [],
27
+ taskSchedules: [],
28
+ commandQueue: [],
29
+ sessions: [],
30
+ sessionMessages: [],
31
+ executionLogs: [],
32
+ executionMetrics: [],
33
+ notifications: [],
34
+ credentials: [],
35
+ activities: []
36
+ };
37
+ }
38
+ /**
39
+ * Create a test organization
40
+ */
41
+ async createOrganization(name) {
42
+ const workosOrgId = `org_${this.testPrefix}_${Math.random().toString(36).substring(7)}`;
43
+ const { data, error } = await this.adminClient.from("organizations").insert({
44
+ workos_org_id: workosOrgId,
45
+ name: `${this.testPrefix}_${name}`,
46
+ is_test: true
47
+ }).select().single();
48
+ if (error) {
49
+ throw new Error(`Failed to create organization: ${error.message}`);
50
+ }
51
+ this.createdIds.organizations.push(data.id);
52
+ return data;
53
+ }
54
+ /**
55
+ * Create a test user with a WorkOS user ID
56
+ */
57
+ async createUser(email, isPlatformAdmin = false) {
58
+ const workosUserId = `user_${this.testPrefix}_${Math.random().toString(36).substring(7)}`;
59
+ const { data, error } = await this.adminClient.from("users").insert({
60
+ workos_user_id: workosUserId,
61
+ email: `${this.testPrefix}_${email}`,
62
+ first_name: "Test",
63
+ last_name: "User",
64
+ is_platform_admin: isPlatformAdmin
65
+ }).select("*").single();
66
+ if (error) {
67
+ throw new Error(`Failed to create user: ${error.message}`);
68
+ }
69
+ this.createdIds.users.push(data.id);
70
+ return {
71
+ id: data.id,
72
+ workos_user_id: workosUserId,
73
+ email: data.email,
74
+ is_platform_admin: data.is_platform_admin ?? false
75
+ };
76
+ }
77
+ /**
78
+ * Create a pre-provisioned test user (without WorkOS user ID)
79
+ * Used for testing invitation flows where users are created before they sign up
80
+ */
81
+ async createPreProvisionedUser(email, isPlatformAdmin = false) {
82
+ const { data, error } = await this.adminClient.from("users").insert({
83
+ workos_user_id: null,
84
+ // Key difference: NULL for pre-provisioned
85
+ email: `${this.testPrefix}_${email}`,
86
+ first_name: "Test",
87
+ last_name: "PreProvisioned",
88
+ is_platform_admin: isPlatformAdmin
89
+ }).select("*").single();
90
+ if (error) {
91
+ throw new Error(`Failed to create pre-provisioned user: ${error.message}`);
92
+ }
93
+ this.createdIds.users.push(data.id);
94
+ return {
95
+ id: data.id,
96
+ email: data.email,
97
+ is_platform_admin: data.is_platform_admin ?? false
98
+ };
99
+ }
100
+ /**
101
+ * Create an organization membership
102
+ */
103
+ async createMembership(userId, organizationId, role) {
104
+ const workosMembershipId = `om_${this.testPrefix}_${Math.random().toString(36).substring(7)}`;
105
+ const { data, error } = await this.adminClient.from("org_memberships").insert({
106
+ user_id: userId,
107
+ organization_id: organizationId,
108
+ workos_membership_id: workosMembershipId,
109
+ role_slug: role,
110
+ membership_status: "active"
111
+ }).select().single();
112
+ if (error) {
113
+ throw new Error(`Failed to create membership: ${error.message}`);
114
+ }
115
+ this.createdIds.memberships.push(data.id);
116
+ return {
117
+ id: data.id,
118
+ user_id: data.user_id,
119
+ organization_id: data.organization_id,
120
+ role_slug: data.role_slug
121
+ };
122
+ }
123
+ /**
124
+ * Create a pre-provisioned organization membership (without WorkOS membership ID)
125
+ * Used for testing invitation flows where memberships are created before user accepts
126
+ */
127
+ async createPreProvisionedMembership(userId, organizationId, role) {
128
+ const { data, error } = await this.adminClient.from("org_memberships").insert({
129
+ user_id: userId,
130
+ organization_id: organizationId,
131
+ workos_membership_id: null,
132
+ // Key difference: NULL for pre-provisioned
133
+ role_slug: role,
134
+ membership_status: "active"
135
+ // Pre-provisioned memberships are active from creation
136
+ }).select().single();
137
+ if (error) {
138
+ throw new Error(`Failed to create pre-provisioned membership: ${error.message}`);
139
+ }
140
+ this.createdIds.memberships.push(data.id);
141
+ return {
142
+ id: data.id,
143
+ user_id: data.user_id,
144
+ organization_id: data.organization_id,
145
+ role_slug: data.role_slug
146
+ };
147
+ }
148
+ /**
149
+ * Generate a JWT token for a test user
150
+ * Uses Supabase JWT secret so auth.jwt() in RLS policies can decode it
151
+ */
152
+ generateJWT(workosUserId, email) {
153
+ if (!process.env.SUPABASE_JWT_SECRET) {
154
+ throw new Error("SUPABASE_JWT_SECRET not configured");
155
+ }
156
+ const payload = {
157
+ sub: workosUserId,
158
+ // Supabase RLS policies use auth.jwt() ->> 'sub'
159
+ aud: "authenticated",
160
+ role: "authenticated",
161
+ iat: Math.floor(Date.now() / 1e3),
162
+ exp: Math.floor(Date.now() / 1e3) + 60 * 60,
163
+ // 1 hour expiry
164
+ ...email && { email }
165
+ // Add email claim if provided (for pre-provisioned user RLS)
166
+ };
167
+ return jwt.sign(payload, process.env.SUPABASE_JWT_SECRET, {
168
+ algorithm: "HS256"
169
+ });
170
+ }
171
+ /**
172
+ * Create a Supabase client for a specific user (respects RLS)
173
+ * This client will have the user's JWT token, so RLS policies will apply
174
+ */
175
+ createUserClient(workosUserId) {
176
+ if (!process.env.SUPABASE_URL || !process.env.SUPABASE_ANON_KEY) {
177
+ throw new Error("Test environment not configured");
178
+ }
179
+ const token = this.generateJWT(workosUserId);
180
+ const client = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, {
181
+ global: {
182
+ headers: {
183
+ Authorization: `Bearer ${token}`
184
+ }
185
+ }
186
+ });
187
+ return client;
188
+ }
189
+ /**
190
+ * Create a Supabase client for a pre-provisioned user (respects RLS)
191
+ * Uses a dummy workos_user_id but includes the email claim for RLS matching
192
+ * The email claim is what matters for pre-provisioned user RLS policies
193
+ */
194
+ createPreProvisionedUserClient(email) {
195
+ if (!process.env.SUPABASE_URL || !process.env.SUPABASE_ANON_KEY) {
196
+ throw new Error("Test environment not configured");
197
+ }
198
+ const dummyWorkosUserId = `preprov_${this.testPrefix}_${Math.random().toString(36).substring(7)}`;
199
+ const token = this.generateJWT(dummyWorkosUserId, email);
200
+ const client = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, {
201
+ global: {
202
+ headers: {
203
+ Authorization: `Bearer ${token}`
204
+ }
205
+ }
206
+ });
207
+ return client;
208
+ }
209
+ /** Create an organization with an admin user and authenticated client. */
210
+ async createOrgWithAdmin(name, email) {
211
+ const org = await this.createOrganization(name);
212
+ const user = await this.createUser(email, false);
213
+ const membership = await this.createMembership(user.id, org.id, "admin");
214
+ const client = this.createUserClient(user.workos_user_id);
215
+ return { org, user, membership, client };
216
+ }
217
+ /** Create two isolated organizations with admin users for cross-org isolation tests. */
218
+ async createCrossOrgFixture(nameA = "OrgA", nameB = "OrgB") {
219
+ const orgA = await this.createOrganization(nameA);
220
+ const orgB = await this.createOrganization(nameB);
221
+ const userA = await this.createUser(`${nameA.toLowerCase()}Admin@test.com`, false);
222
+ const userB = await this.createUser(`${nameB.toLowerCase()}Admin@test.com`, false);
223
+ const membershipA = await this.createMembership(userA.id, orgA.id, "admin");
224
+ const membershipB = await this.createMembership(userB.id, orgB.id, "admin");
225
+ const clientA = this.createUserClient(userA.workos_user_id);
226
+ const clientB = this.createUserClient(userB.workos_user_id);
227
+ return { orgA, orgB, userA, userB, membershipA, membershipB, clientA, clientB };
228
+ }
229
+ /**
230
+ * Clean up all test data created during the test run
231
+ * Called in afterAll() to prevent test pollution
232
+ *
233
+ * Cleanup order respects foreign key dependencies (child tables before parent tables):
234
+ *
235
+ * Level 1 (Deepest dependencies):
236
+ * - execution_metrics (FK: execution_logs)
237
+ * - session_messages (FK: sessions)
238
+ *
239
+ * Level 2 (Mid-level dependencies):
240
+ * - execution_logs (FK: sessions, users)
241
+ * - task_schedules (FK: organizations)
242
+ * - command_queue (FK: organizations, users)
243
+ * - notifications (FK: organizations, users)
244
+ * - credentials (FK: organizations, users)
245
+ * - sessions (FK: organizations, users)
246
+ * - activities (FK: organizations)
247
+ *
248
+ * Level 3 (Organization/User dependencies):
249
+ * - invitations (FK: organizations, users)
250
+ * - api_keys (FK: organizations)
251
+ * - memberships (FK: organizations, users)
252
+ *
253
+ * Level 4 (Base tables):
254
+ * - users
255
+ * - organizations
256
+ */
257
+ async cleanup() {
258
+ const errors = [];
259
+ if (this.createdIds.executionMetrics.length > 0) {
260
+ const { error } = await this.adminClient.from("execution_metrics").delete().in("execution_id", this.createdIds.executionMetrics);
261
+ if (error) {
262
+ errors.push(`Failed to delete execution_metrics: ${error.message}`);
263
+ }
264
+ }
265
+ if (this.createdIds.sessionMessages.length > 0) {
266
+ const { error } = await this.adminClient.from("session_messages").delete().in("id", this.createdIds.sessionMessages);
267
+ if (error) {
268
+ errors.push(`Failed to delete session_messages: ${error.message}`);
269
+ }
270
+ }
271
+ if (this.createdIds.executionLogs.length > 0) {
272
+ const { error } = await this.adminClient.from("execution_logs").delete().in("execution_id", this.createdIds.executionLogs);
273
+ if (error) {
274
+ errors.push(`Failed to delete execution_logs: ${error.message}`);
275
+ }
276
+ }
277
+ if (this.createdIds.taskSchedules.length > 0) {
278
+ const { error } = await this.adminClient.from("task_schedules").delete().in("id", this.createdIds.taskSchedules);
279
+ if (error) {
280
+ errors.push(`Failed to delete task_schedules: ${error.message}`);
281
+ }
282
+ }
283
+ if (this.createdIds.commandQueue.length > 0) {
284
+ const { error } = await this.adminClient.from("command_queue").delete().in("id", this.createdIds.commandQueue);
285
+ if (error) {
286
+ errors.push(`Failed to delete command_queue: ${error.message}`);
287
+ }
288
+ }
289
+ if (this.createdIds.notifications.length > 0) {
290
+ const { error } = await this.adminClient.from("notifications").delete().in("id", this.createdIds.notifications);
291
+ if (error) {
292
+ errors.push(`Failed to delete notifications: ${error.message}`);
293
+ }
294
+ }
295
+ if (this.createdIds.credentials.length > 0) {
296
+ const { error } = await this.adminClient.from("credentials").delete().in("id", this.createdIds.credentials);
297
+ if (error) {
298
+ errors.push(`Failed to delete credentials: ${error.message}`);
299
+ }
300
+ }
301
+ if (this.createdIds.sessions.length > 0) {
302
+ const { error } = await this.adminClient.from("sessions").delete().in("session_id", this.createdIds.sessions);
303
+ if (error) {
304
+ errors.push(`Failed to delete sessions: ${error.message}`);
305
+ }
306
+ }
307
+ if (this.createdIds.activities.length > 0) {
308
+ const { error } = await this.adminClient.from("activities").delete().in("id", this.createdIds.activities);
309
+ if (error) {
310
+ errors.push(`Failed to delete activities: ${error.message}`);
311
+ }
312
+ }
313
+ if (this.createdIds.invitations.length > 0) {
314
+ const { error } = await this.adminClient.from("org_invitations").delete().in("id", this.createdIds.invitations);
315
+ if (error) {
316
+ errors.push(`Failed to delete invitations: ${error.message}`);
317
+ }
318
+ }
319
+ if (this.createdIds.apiKeys.length > 0) {
320
+ const { error } = await this.adminClient.from("api_keys").delete().in("id", this.createdIds.apiKeys);
321
+ if (error) {
322
+ errors.push(`Failed to delete API keys: ${error.message}`);
323
+ }
324
+ }
325
+ if (this.createdIds.memberships.length > 0) {
326
+ const { error } = await this.adminClient.from("org_memberships").delete().in("id", this.createdIds.memberships);
327
+ if (error) {
328
+ errors.push(`Failed to delete memberships: ${error.message}`);
329
+ }
330
+ }
331
+ if (this.createdIds.users.length > 0) {
332
+ const { error } = await this.adminClient.from("users").delete().in("id", this.createdIds.users);
333
+ if (error) {
334
+ errors.push(`Failed to delete users: ${error.message}`);
335
+ }
336
+ }
337
+ if (this.createdIds.organizations.length > 0) {
338
+ const { error } = await this.adminClient.from("organizations").delete().in("id", this.createdIds.organizations);
339
+ if (error) {
340
+ errors.push(`Failed to delete organizations: ${error.message}`);
341
+ }
342
+ }
343
+ if (errors.length > 0) {
344
+ console.warn("\n\u26A0\uFE0F Cleanup warnings:", errors.join("\n"));
345
+ }
346
+ }
347
+ };
348
+
349
+ // src/test-utils/browser-mocks.ts
350
+ function setupMatchMedia() {
351
+ const win = globalThis.window;
352
+ if (typeof win === "undefined") return;
353
+ Object.defineProperty(win, "matchMedia", {
354
+ writable: true,
355
+ value: (query) => ({
356
+ matches: false,
357
+ media: query,
358
+ onchange: null,
359
+ addListener: () => {
360
+ },
361
+ removeListener: () => {
362
+ },
363
+ addEventListener: () => {
364
+ },
365
+ removeEventListener: () => {
366
+ },
367
+ dispatchEvent: () => true
368
+ })
369
+ });
370
+ }
371
+ function setupResizeObserver() {
372
+ globalThis.ResizeObserver = class ResizeObserver {
373
+ observe() {
374
+ }
375
+ unobserve() {
376
+ }
377
+ disconnect() {
378
+ }
379
+ };
380
+ }
381
+ function setupBrowserMocks() {
382
+ setupMatchMedia();
383
+ setupResizeObserver();
384
+ }
385
+
386
+ export { RLSTestContext, setupBrowserMocks, setupMatchMedia, setupResizeObserver };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elevasis/core",
3
- "version": "0.8.0",
3
+ "version": "0.8.3",
4
4
  "license": "MIT",
5
5
  "description": "Minimal shared constants across Elevasis monorepo",
6
6
  "sideEffects": false,
@@ -16,6 +16,10 @@
16
16
  "types": "./dist/index.d.ts",
17
17
  "import": "./dist/index.js"
18
18
  },
19
+ "./test-utils": {
20
+ "types": "./dist/test-utils/index.d.ts",
21
+ "import": "./dist/test-utils/index.js"
22
+ },
19
23
  "./organization-model": {
20
24
  "types": "./dist/organization-model/index.d.ts",
21
25
  "import": "./dist/organization-model/index.js"
@@ -43,6 +47,7 @@
43
47
  "@googleapis/gmail": "^15.0.0",
44
48
  "@googleapis/sheets": "^12.0.0",
45
49
  "@sentry/node": "^8.55.0",
50
+ "@supabase/supabase-js": "^2.80.0",
46
51
  "@workos-inc/node": "^7.69.2",
47
52
  "croner": "^10.0.1",
48
53
  "date-fns-tz": "^3.2.0",
package/src/README.md CHANGED
@@ -6,17 +6,20 @@ This package is the source of truth for shared types, schemas, and contract help
6
6
 
7
7
  ## Import Rules
8
8
 
9
- - Use `@elevasis/core` (root export) for browser-safe shared types and schemas.
10
- - Use `@elevasis/core/organization-model` for the semantic contract layer.
11
- - These are the only two subpaths available to external consumers of the published npm package.
12
- - Paths like `@elevasis/core/server`, `@elevasis/core/test-utils`, `@elevasis/core/platform`, etc. are internal monorepo paths (`@repo/core/...`) and are NOT available to external consumers.
9
+ - Use `@elevasis/core` (root export) for browser-safe shared types and schemas.
10
+ - Use `@elevasis/core/organization-model` for the semantic contract layer.
11
+ - Use `@elevasis/core/entities` for the published base entity contracts.
12
+ - Use `@elevasis/core/test-utils` for shared test fixtures, mocks, and test helpers.
13
+ - Paths like `@elevasis/core/server` and `@elevasis/core/platform` are internal monorepo paths (`@repo/core/...`) and are NOT available to external consumers.
13
14
 
14
15
  ## Published Surface Groups
15
16
 
16
- The published `@elevasis/core` npm package exposes exactly two subpaths:
17
-
18
- - `.` (`@elevasis/core`) - browser-safe shared types, schemas, and constants.
19
- - `./organization-model` (`@elevasis/core/organization-model`) - the semantic contract layer for CRM, lead gen, delivery, features, branding, and navigation.
17
+ The published `@elevasis/core` npm package exposes these subpaths:
18
+
19
+ - `.` (`@elevasis/core`) - browser-safe shared types, schemas, and constants.
20
+ - `./organization-model` (`@elevasis/core/organization-model`) - the semantic contract layer for CRM, lead gen, delivery, features, branding, and navigation.
21
+ - `./entities` (`@elevasis/core/entities`) - published base entity contracts generic over project metadata extensions.
22
+ - `./test-utils` (`@elevasis/core/test-utils`) - test fixtures, mocks, and helpers for downstream automated tests.
20
23
 
21
24
  Within the monorepo, the internal `@repo/core` package exposes additional subpaths for use by `apps/` and other packages:
22
25
 
@@ -30,9 +33,9 @@ Within the monorepo, the internal `@repo/core` package exposes additional subpat
30
33
  - `@repo/core/integrations/...` - OAuth and credential contracts.
31
34
  - `@repo/core/projects/api-schemas` - project management request and response schemas.
32
35
  - `@repo/core/content` - published content metadata types.
33
- - `@repo/core/test-utils` - test fixtures and mocks (internal use only).
34
-
35
- These `@repo/core/*` subpaths are NOT available in the published `@elevasis/core` package.
36
+ - `@repo/core/test-utils` - source of the published test fixtures and mocks surface.
37
+
38
+ Other `@repo/core/*` subpaths remain monorepo-only unless they are explicitly listed above in the published `@elevasis/core` surface.
36
39
 
37
40
  ## When To Read Deeper
38
41
 
@@ -10,9 +10,14 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {
10
10
  }
11
11
  }
12
12
 
13
- describe('core publish surface', () => {
14
- it('publishes the curated @elevasis/core organization-model wrapper', () => {
15
- expect(packageJson.publishConfig?.name).toBe('@elevasis/core')
16
- expect(Object.keys(packageJson.publishConfig?.exports ?? {})).toEqual(['.', './organization-model', './entities'])
17
- })
18
- })
13
+ describe('core publish surface', () => {
14
+ it('publishes the curated @elevasis/core organization-model wrapper', () => {
15
+ expect(packageJson.publishConfig?.name).toBe('@elevasis/core')
16
+ expect(Object.keys(packageJson.publishConfig?.exports ?? {})).toEqual([
17
+ '.',
18
+ './test-utils',
19
+ './organization-model',
20
+ './entities'
21
+ ])
22
+ })
23
+ })