@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.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -1
- package/dist/organization-model/index.d.ts +2 -1
- package/dist/organization-model/index.js +8 -1
- package/dist/test-utils/index.d.ts +10 -3
- package/dist/test-utils/index.js +6 -0
- package/package.json +1 -1
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +27 -270
- package/src/auth/multi-tenancy/credentials/server/encryption.ts +83 -39
- package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +47 -0
- package/src/auth/multi-tenancy/index.ts +3 -0
- package/src/auth/multi-tenancy/invitations/api-schemas.ts +104 -107
- package/src/auth/multi-tenancy/memberships/api-schemas.ts +6 -5
- package/src/auth/multi-tenancy/memberships/membership.ts +130 -138
- package/src/auth/multi-tenancy/role-management/api-schemas.ts +78 -0
- package/src/auth/multi-tenancy/role-management/index.ts +16 -0
- package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +299 -293
- package/src/execution/engine/tools/integration/service.test.ts +214 -0
- package/src/execution/engine/tools/integration/service.ts +169 -161
- package/src/integrations/credentials/__tests__/api-schemas.test.ts +420 -496
- package/src/integrations/credentials/api-schemas.ts +127 -143
- package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +327 -318
- package/src/integrations/webhook-endpoints/api-schemas.ts +103 -102
- package/src/integrations/webhook-endpoints/types.ts +58 -51
- package/src/operations/activities/api-schemas.ts +80 -79
- package/src/operations/activities/types.ts +64 -63
- package/src/organization-model/contracts.ts +1 -0
- package/src/organization-model/defaults.ts +6 -0
- package/src/organization-model/domains/navigation.ts +37 -23
- package/src/organization-model/published.ts +2 -1
- package/src/platform/constants/versions.ts +1 -1
- package/src/reference/_generated/contracts.md +27 -270
- package/src/scaffold-registry/__tests__/index.test.ts +72 -7
- package/src/scaffold-registry/index.ts +159 -26
- package/src/server.ts +281 -272
- package/src/supabase/database.types.ts +7 -3
|
@@ -1,39 +1,83 @@
|
|
|
1
|
-
import crypto from 'crypto'
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
* -
|
|
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()
|
|
92
|
-
})
|
|
93
|
-
.strict()
|
|
94
|
-
.refine(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
export type
|
|
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
|
-
*
|
|
22
|
+
* Accepts any non-empty role slug (max 64 chars).
|
|
23
23
|
*
|
|
24
|
-
*
|
|
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.
|
|
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
|
|
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
|
-
* -
|
|
94
|
+
* - Service layer validates slug against org_rol_definitions
|
|
94
95
|
*/
|
|
95
96
|
export const UpdateMembershipSchema = z
|
|
96
97
|
.object({
|