@elevasis/core 0.11.2 → 0.12.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 (36) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.js +8 -1
  3. package/dist/organization-model/index.d.ts +2 -1
  4. package/dist/organization-model/index.js +8 -1
  5. package/dist/test-utils/index.d.ts +10 -3
  6. package/dist/test-utils/index.js +6 -0
  7. package/package.json +1 -1
  8. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +27 -270
  9. package/src/auth/multi-tenancy/credentials/server/encryption.ts +83 -39
  10. package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +47 -0
  11. package/src/auth/multi-tenancy/index.ts +3 -0
  12. package/src/auth/multi-tenancy/invitations/api-schemas.ts +104 -107
  13. package/src/auth/multi-tenancy/memberships/api-schemas.ts +6 -5
  14. package/src/auth/multi-tenancy/memberships/membership.ts +130 -138
  15. package/src/auth/multi-tenancy/role-management/api-schemas.ts +78 -0
  16. package/src/auth/multi-tenancy/role-management/index.ts +16 -0
  17. package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +299 -293
  18. package/src/execution/engine/tools/integration/service.test.ts +214 -0
  19. package/src/execution/engine/tools/integration/service.ts +169 -161
  20. package/src/integrations/credentials/__tests__/api-schemas.test.ts +420 -496
  21. package/src/integrations/credentials/api-schemas.ts +127 -143
  22. package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +327 -318
  23. package/src/integrations/webhook-endpoints/api-schemas.ts +103 -102
  24. package/src/integrations/webhook-endpoints/types.ts +58 -51
  25. package/src/operations/activities/api-schemas.ts +80 -79
  26. package/src/operations/activities/types.ts +64 -63
  27. package/src/organization-model/contracts.ts +1 -0
  28. package/src/organization-model/defaults.ts +6 -0
  29. package/src/organization-model/domains/navigation.ts +37 -23
  30. package/src/organization-model/published.ts +2 -1
  31. package/src/platform/constants/versions.ts +1 -1
  32. package/src/reference/_generated/contracts.md +27 -270
  33. package/src/scaffold-registry/__tests__/index.test.ts +72 -7
  34. package/src/scaffold-registry/index.ts +159 -26
  35. package/src/server.ts +281 -272
  36. package/src/supabase/database.types.ts +7 -3
@@ -1,39 +1,83 @@
1
- import crypto from 'crypto'
2
-
3
- const MASTER_KEY = process.env.SECRETS_ENCRYPTION_KEY
4
- const ALGORITHM = 'aes-256-gcm'
5
-
6
- interface EncryptedData {
7
- iv: string
8
- authTag: string
9
- data: string
10
- }
11
-
12
- export function encryptCredential(plaintext: string): string {
13
- if (!MASTER_KEY) {
14
- throw new Error('SECRETS_ENCRYPTION_KEY not configured')
15
- }
16
-
17
- const iv = crypto.randomBytes(16)
18
- const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(MASTER_KEY, 'hex'), iv)
19
- const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])
20
- const authTag = cipher.getAuthTag()
21
-
22
- return JSON.stringify({
23
- iv: iv.toString('base64'),
24
- authTag: authTag.toString('base64'),
25
- data: encrypted.toString('base64')
26
- })
27
- }
28
-
29
- export function decryptCredential(encrypted: string): string {
30
- if (!MASTER_KEY) {
31
- throw new Error('SECRETS_ENCRYPTION_KEY not configured')
32
- }
33
-
34
- const { iv, authTag, data } = JSON.parse(encrypted) as EncryptedData
35
- const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(MASTER_KEY, 'hex'), Buffer.from(iv, 'base64'))
36
- decipher.setAuthTag(Buffer.from(authTag, 'base64'))
37
-
38
- return Buffer.concat([decipher.update(Buffer.from(data, 'base64')), decipher.final()]).toString('utf8')
39
- }
1
+ import crypto from 'crypto'
2
+
3
+ const ALGORITHM = 'aes-256-gcm'
4
+
5
+ // keyId stamped on all newly encrypted ciphertexts.
6
+ export const CURRENT_KEY_ID = 'platform-v1'
7
+
8
+ // Implicit keyId for pre-Vault ciphertext rows that were encrypted before this
9
+ // field existed. The kek-loader registers the legacy SECRETS_ENCRYPTION_KEY env
10
+ // value under this id during the migration window so existing rows decrypt
11
+ // until the re-encryption script (B4) restamps them with CURRENT_KEY_ID.
12
+ export const LEGACY_KEY_ID = 'platform-v0-legacy'
13
+
14
+ interface EncryptedData {
15
+ iv: string
16
+ authTag: string
17
+ data: string
18
+ keyId?: string
19
+ }
20
+
21
+ const kekMap = new Map<string, Buffer>()
22
+
23
+ export function setKek(keyId: string, key: Buffer): void {
24
+ if (key.length !== 32) {
25
+ throw new Error(`KEK must be 32 bytes (256 bits); got ${key.length}`)
26
+ }
27
+ kekMap.set(keyId, key)
28
+ }
29
+
30
+ export function clearKeks(): void {
31
+ kekMap.clear()
32
+ }
33
+
34
+ function resolveKek(keyId: string): Buffer | undefined {
35
+ const cached = kekMap.get(keyId)
36
+ if (cached) return cached
37
+
38
+ // Non-production fallback: bootstrap from SECRETS_ENCRYPTION_KEY so tests and
39
+ // local dev keep working without an explicit setKek() boot step. Production
40
+ // must register KEKs via the kek-loader; the env var is removed in Wave B5.
41
+ if (process.env.NODE_ENV !== 'production' && process.env.SECRETS_ENCRYPTION_KEY) {
42
+ const envKey = Buffer.from(process.env.SECRETS_ENCRYPTION_KEY, 'hex')
43
+ if (envKey.length === 32) {
44
+ kekMap.set(keyId, envKey)
45
+ return envKey
46
+ }
47
+ }
48
+
49
+ return undefined
50
+ }
51
+
52
+ export function encryptCredential(plaintext: string): string {
53
+ const key = resolveKek(CURRENT_KEY_ID)
54
+ if (!key) {
55
+ throw new Error(`Encryption KEK '${CURRENT_KEY_ID}' not loaded; call setKek() at boot`)
56
+ }
57
+
58
+ const iv = crypto.randomBytes(16)
59
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
60
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])
61
+ const authTag = cipher.getAuthTag()
62
+
63
+ return JSON.stringify({
64
+ iv: iv.toString('base64'),
65
+ authTag: authTag.toString('base64'),
66
+ data: encrypted.toString('base64'),
67
+ keyId: CURRENT_KEY_ID
68
+ })
69
+ }
70
+
71
+ export function decryptCredential(encrypted: string): string {
72
+ const { iv, authTag, data, keyId } = JSON.parse(encrypted) as EncryptedData
73
+ const resolvedKeyId = keyId ?? LEGACY_KEY_ID
74
+ const key = resolveKek(resolvedKeyId)
75
+ if (!key) {
76
+ throw new Error(`Decryption KEK '${resolvedKeyId}' not loaded`)
77
+ }
78
+
79
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, Buffer.from(iv, 'base64'))
80
+ decipher.setAuthTag(Buffer.from(authTag, 'base64'))
81
+
82
+ return Buffer.concat([decipher.update(Buffer.from(data, 'base64')), decipher.final()]).toString('utf8')
83
+ }
@@ -0,0 +1,47 @@
1
+ import { getSupabaseClient } from '../../../../supabase/server/client'
2
+ import { setKek, CURRENT_KEY_ID, LEGACY_KEY_ID } from './encryption'
3
+
4
+ let loaded = false
5
+
6
+ /**
7
+ * Loads the platform credential KEK from Supabase Vault and registers it under
8
+ * `CURRENT_KEY_ID` ('platform-v1'). During the migration window, also registers
9
+ * the legacy `SECRETS_ENCRYPTION_KEY` env var under `LEGACY_KEY_ID` so existing
10
+ * ciphertext rows (no `keyId` field) can still be decrypted.
11
+ *
12
+ * Idempotent: subsequent calls are no-ops.
13
+ *
14
+ * Fails fast on missing / malformed Vault KEK so misconfigured deploys do not
15
+ * silently fall through to env-only encryption.
16
+ */
17
+ export async function loadCredentialKEKs(): Promise<void> {
18
+ if (loaded) return
19
+
20
+ const supabase = await getSupabaseClient()
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- RPC isn't in generated types yet (run `supabase gen types` post-merge)
22
+ const { data, error } = await (supabase.rpc as any)('get_platform_credential_kek')
23
+ if (error) {
24
+ throw new Error(
25
+ `Failed to load platform credential KEK from Vault: ${error.message}. ` +
26
+ `Did you run provision-credential-kek.sql against this environment?`
27
+ )
28
+ }
29
+ if (typeof data !== 'string' || data.length === 0) {
30
+ throw new Error('Vault returned null/empty platform credential KEK')
31
+ }
32
+ const vaultKek = Buffer.from(data, 'hex')
33
+ if (vaultKek.length !== 32) {
34
+ throw new Error(`Vault KEK is ${vaultKek.length} bytes, expected 32`)
35
+ }
36
+ setKek(CURRENT_KEY_ID, vaultKek)
37
+
38
+ const legacyHex = process.env.SECRETS_ENCRYPTION_KEY
39
+ if (legacyHex) {
40
+ const legacyKey = Buffer.from(legacyHex, 'hex')
41
+ if (legacyKey.length === 32) {
42
+ setKek(LEGACY_KEY_ID, legacyKey)
43
+ }
44
+ }
45
+
46
+ loaded = true
47
+ }
@@ -4,6 +4,9 @@ export * from './types'
4
4
  // Permission catalog (canonical PERMISSIONS constant + types)
5
5
  export * from './permissions'
6
6
 
7
+ // Role management schemas
8
+ export * from './role-management/index'
9
+
7
10
  // Organization types
8
11
  export * from './organizations/index'
9
12
 
@@ -1,107 +1,104 @@
1
- /**
2
- * Invitations Domain - Zod Validation Schemas
3
- *
4
- * Validation schemas for invitation management endpoints.
5
- * Includes request bodies, query params, and path params.
6
- *
7
- * Security:
8
- * - All schemas use .strict() to prevent mass assignment attacks
9
- * - Email validation prevents header injection
10
- * - Role enum validation prevents privilege escalation
11
- * - organizationId from JWT (not accepted in body for protected routes)
12
- */
13
-
14
- import { z } from 'zod'
15
- import { EmailSchema } from '../../../platform/utils/validation'
16
- import { MembershipRoleSchema } from '../memberships/api-schemas'
17
-
18
- // ============================================================================
19
- // Path Parameters
20
- // ============================================================================
21
-
22
- /**
23
- * Validate invitation ID in URL path
24
- * Used by: GET/DELETE /invitations/:id
25
- */
26
- export const InvitationIdParamSchema = z
27
- .object({
28
- id: z.string().min(1) // WorkOS invitation IDs
29
- })
30
- .strict()
31
-
32
- // ============================================================================
33
- // Request Bodies
34
- // ============================================================================
35
-
36
- /**
37
- * Send new invitation
38
- * POST /invitations
39
- *
40
- * Security:
41
- * - Email format validated (prevents header injection)
42
- * - Role enum prevents privilege escalation
43
- * - expiresInDays bounded (1-90 days)
44
- * - organizationId NOT in body (from JWT via requireOrganization middleware)
45
- */
46
- export const SendInvitationSchema = z
47
- .object({
48
- email: EmailSchema,
49
- organizationId: z.string().optional(), // For WorkOS API - but typically from JWT
50
- roleSlug: MembershipRoleSchema.default('member'),
51
- expiresInDays: z.number().int().min(1).max(90).default(7)
52
- })
53
- .strict()
54
-
55
- /**
56
- * Accept invitation by token
57
- * POST /invitations/accept
58
- *
59
- * Security:
60
- * - Token validated (non-empty string)
61
- */
62
- export const AcceptInvitationSchema = z
63
- .object({
64
- invitation_token: z.string().min(1, 'Invitation token is required')
65
- })
66
- .strict()
67
-
68
- // ============================================================================
69
- // Query Parameters
70
- // ============================================================================
71
-
72
- /**
73
- * List invitations with filters
74
- * GET /invitations
75
- *
76
- * Filters:
77
- * - organizationId: Filter by organization
78
- * - email: Filter by email
79
- *
80
- * Security:
81
- * - Requires organizationId or userId filter
82
- * - Email validated
83
- */
84
- export const ListInvitationsQuerySchema = z
85
- .object({
86
- organizationId: z.string().optional(),
87
- userId: z.string().optional(),
88
- email: EmailSchema.optional(),
89
- limit: z.coerce.number().int().min(1).max(100).optional(),
90
- before: z.string().optional(), // WorkOS pagination cursor
91
- after: z.string().optional() // WorkOS pagination cursor
92
- })
93
- .strict()
94
- .refine(
95
- (data) => data.organizationId || data.userId,
96
- { message: 'Either organizationId or userId must be provided' }
97
- )
98
-
99
- // ============================================================================
100
- // TypeScript Type Exports
101
- // ============================================================================
102
-
103
- // Export inferred types for use in route handlers
104
- export type SendInvitationInput = z.infer<typeof SendInvitationSchema>
105
- export type AcceptInvitationInput = z.infer<typeof AcceptInvitationSchema>
106
- export type ListInvitationsQuery = z.infer<typeof ListInvitationsQuerySchema>
107
- export type InvitationIdParam = z.infer<typeof InvitationIdParamSchema>
1
+ /**
2
+ * Invitations Domain - Zod Validation Schemas
3
+ *
4
+ * Validation schemas for invitation management endpoints.
5
+ * Includes request bodies, query params, and path params.
6
+ *
7
+ * Security:
8
+ * - All schemas use .strict() to prevent mass assignment attacks
9
+ * - Email validation prevents header injection
10
+ * - Role enum validation prevents privilege escalation
11
+ * - organizationId from JWT (not accepted in body for protected routes)
12
+ */
13
+
14
+ import { z } from 'zod'
15
+ import { EmailSchema } from '../../../platform/utils/validation'
16
+ import { MembershipRoleSchema } from '../memberships/api-schemas'
17
+
18
+ // ============================================================================
19
+ // Path Parameters
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Validate invitation ID in URL path
24
+ * Used by: GET/DELETE /invitations/:id
25
+ */
26
+ export const InvitationIdParamSchema = z
27
+ .object({
28
+ id: z.string().min(1) // WorkOS invitation IDs
29
+ })
30
+ .strict()
31
+
32
+ // ============================================================================
33
+ // Request Bodies
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Send new invitation
38
+ * POST /invitations
39
+ *
40
+ * Security:
41
+ * - Email format validated (prevents header injection)
42
+ * - roleSlug validated as non-empty slug; service layer checks against org_rol_definitions
43
+ * - expiresInDays bounded (1-90 days)
44
+ * - organizationId NOT in body (from JWT via requireOrganization middleware)
45
+ */
46
+ export const SendInvitationSchema = z
47
+ .object({
48
+ email: EmailSchema,
49
+ organizationId: z.string().optional(), // For WorkOS API - but typically from JWT
50
+ roleSlug: MembershipRoleSchema.default('member'),
51
+ expiresInDays: z.number().int().min(1).max(90).default(7)
52
+ })
53
+ .strict()
54
+
55
+ /**
56
+ * Accept invitation by token
57
+ * POST /invitations/accept
58
+ *
59
+ * Security:
60
+ * - Token validated (non-empty string)
61
+ */
62
+ export const AcceptInvitationSchema = z
63
+ .object({
64
+ invitation_token: z.string().min(1, 'Invitation token is required')
65
+ })
66
+ .strict()
67
+
68
+ // ============================================================================
69
+ // Query Parameters
70
+ // ============================================================================
71
+
72
+ /**
73
+ * List invitations with filters
74
+ * GET /invitations
75
+ *
76
+ * Filters:
77
+ * - organizationId: Filter by organization
78
+ * - email: Filter by email
79
+ *
80
+ * Security:
81
+ * - Requires organizationId or userId filter
82
+ * - Email validated
83
+ */
84
+ export const ListInvitationsQuerySchema = z
85
+ .object({
86
+ organizationId: z.string().optional(),
87
+ userId: z.string().optional(),
88
+ email: EmailSchema.optional(),
89
+ limit: z.coerce.number().int().min(1).max(100).optional(),
90
+ before: z.string().optional(), // WorkOS pagination cursor
91
+ after: z.string().optional() // WorkOS pagination cursor
92
+ })
93
+ .strict()
94
+ .refine((data) => data.organizationId || data.userId, { message: 'Either organizationId or userId must be provided' })
95
+
96
+ // ============================================================================
97
+ // TypeScript Type Exports
98
+ // ============================================================================
99
+
100
+ // Export inferred types for use in route handlers
101
+ export type SendInvitationInput = z.infer<typeof SendInvitationSchema>
102
+ export type AcceptInvitationInput = z.infer<typeof AcceptInvitationSchema>
103
+ export type ListInvitationsQuery = z.infer<typeof ListInvitationsQuerySchema>
104
+ export type InvitationIdParam = z.infer<typeof InvitationIdParamSchema>
@@ -19,11 +19,12 @@ import { z } from 'zod'
19
19
 
20
20
  /**
21
21
  * Membership role validation
22
- * Restricts to valid role slugs only
22
+ * Accepts any non-empty role slug (max 64 chars).
23
23
  *
24
- * Security: Prevents privilege escalation by limiting role values
24
+ * Roles are now DB-driven via `org_rol_definitions`. Runtime validation
25
+ * against valid slugs happens at the service layer, not here.
25
26
  */
26
- export const MembershipRoleSchema = z.enum(['admin', 'member'])
27
+ export const MembershipRoleSchema = z.string().min(1).max(64)
27
28
 
28
29
  /**
29
30
  * Membership status validation
@@ -73,7 +74,7 @@ export const MyOrgPermissionsResponseSchema = z.object({
73
74
  * Security:
74
75
  * - userId must be valid (string format for WorkOS)
75
76
  * - organizationId must be valid (string format for WorkOS)
76
- * - roleSlug enum prevents privilege escalation
77
+ * - roleSlug validated as non-empty slug; service layer checks against org_rol_definitions
77
78
  * - Strict mode prevents injection
78
79
  */
79
80
  export const CreateMembershipSchema = z
@@ -90,7 +91,7 @@ export const CreateMembershipSchema = z
90
91
  *
91
92
  * Security:
92
93
  * - Only roleSlug can be updated
93
- * - Enum validation prevents privilege escalation
94
+ * - Service layer validates slug against org_rol_definitions
94
95
  */
95
96
  export const UpdateMembershipSchema = z
96
97
  .object({