@elevasis/core 0.10.0 → 0.11.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 (58) hide show
  1. package/dist/index.d.ts +69 -159
  2. package/dist/index.js +324 -613
  3. package/dist/organization-model/index.d.ts +69 -159
  4. package/dist/organization-model/index.js +324 -613
  5. package/dist/test-utils/index.d.ts +192 -45
  6. package/dist/test-utils/index.js +260 -600
  7. package/package.json +1 -1
  8. package/src/__tests__/template-core-compatibility.test.ts +73 -91
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +94 -182
  10. package/src/auth/multi-tenancy/index.ts +20 -17
  11. package/src/auth/multi-tenancy/memberships/api-schemas.ts +142 -126
  12. package/src/auth/multi-tenancy/memberships/index.ts +26 -22
  13. package/src/auth/multi-tenancy/permissions.test.ts +42 -0
  14. package/src/auth/multi-tenancy/permissions.ts +104 -0
  15. package/src/organization-model/README.md +102 -97
  16. package/src/organization-model/__tests__/defaults.test.ts +19 -6
  17. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +24 -93
  18. package/src/organization-model/__tests__/graph.test.ts +82 -894
  19. package/src/organization-model/__tests__/resolve.test.ts +59 -690
  20. package/src/organization-model/__tests__/schema.test.ts +83 -407
  21. package/src/organization-model/contracts.ts +4 -3
  22. package/src/organization-model/defaults.ts +277 -141
  23. package/src/organization-model/domains/features.ts +31 -22
  24. package/src/organization-model/domains/navigation.ts +27 -20
  25. package/src/organization-model/foundation.ts +42 -54
  26. package/src/organization-model/graph/build.ts +42 -217
  27. package/src/organization-model/graph/index.ts +4 -4
  28. package/src/organization-model/graph/link.ts +10 -0
  29. package/src/organization-model/graph/schema.ts +21 -16
  30. package/src/organization-model/graph/types.ts +10 -10
  31. package/src/organization-model/helpers.ts +74 -0
  32. package/src/organization-model/index.ts +7 -7
  33. package/src/organization-model/organization-graph.mdx +89 -272
  34. package/src/organization-model/organization-model.mdx +152 -320
  35. package/src/organization-model/published.ts +20 -19
  36. package/src/organization-model/resolve.ts +8 -33
  37. package/src/organization-model/schema.ts +63 -205
  38. package/src/organization-model/types.ts +12 -11
  39. package/src/platform/constants/versions.ts +3 -3
  40. package/src/platform/registry/__tests__/command-view.test.ts +6 -5
  41. package/src/platform/registry/__tests__/resource-link.test.ts +30 -0
  42. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +15 -15
  43. package/src/platform/registry/command-view.ts +10 -12
  44. package/src/platform/registry/index.ts +93 -93
  45. package/src/platform/registry/resource-link.ts +32 -0
  46. package/src/platform/registry/resource-registry.ts +917 -876
  47. package/src/platform/registry/serialization.ts +56 -73
  48. package/src/platform/registry/serialized-types.ts +17 -12
  49. package/src/platform/registry/types.ts +14 -43
  50. package/src/reference/_generated/contracts.md +94 -182
  51. package/src/reference/glossary.md +71 -105
  52. package/src/scaffold-registry/__tests__/index.test.ts +125 -1
  53. package/src/scaffold-registry/__tests__/schema.test.ts +48 -20
  54. package/src/scaffold-registry/index.ts +236 -188
  55. package/src/scaffold-registry/schema.ts +47 -22
  56. package/src/supabase/database.types.ts +2880 -2719
  57. package/src/test-utils/fixtures/memberships.ts +82 -80
  58. package/src/platform/registry/domains.ts +0 -165
@@ -1,126 +1,142 @@
1
- /**
2
- * Memberships Domain - Zod Validation Schemas
3
- *
4
- * Validation schemas for membership 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
- * - UUID validation prevents invalid references
10
- * - Role enum validation prevents privilege escalation
11
- * - organizationId never accepted in body (from JWT when needed)
12
- */
13
-
14
- import { z } from 'zod'
15
-
16
- // ============================================================================
17
- // Shared Schemas
18
- // ============================================================================
19
-
20
- /**
21
- * Membership role validation
22
- * Restricts to valid role slugs only
23
- *
24
- * Security: Prevents privilege escalation by limiting role values
25
- */
26
- export const MembershipRoleSchema = z.enum(['admin', 'member'])
27
-
28
- /**
29
- * Membership status validation
30
- * Note: Database constraint only allows 'active' | 'inactive'
31
- */
32
- export const MembershipStatusSchema = z.enum(['active', 'inactive'])
33
-
34
- // ============================================================================
35
- // Path Parameters
36
- // ============================================================================
37
-
38
- /**
39
- * Validate membership ID in URL path
40
- * Used by: GET/PUT/DELETE /memberships/:id
41
- */
42
- export const MembershipIdParamSchema = z
43
- .object({
44
- id: z.string().min(1) // WorkOS membership IDs can be various formats
45
- })
46
- .strict()
47
-
48
- // ============================================================================
49
- // Request Bodies
50
- // ============================================================================
51
-
52
- /**
53
- * Create new membership
54
- * POST /memberships
55
- *
56
- * Security:
57
- * - userId must be valid (string format for WorkOS)
58
- * - organizationId must be valid (string format for WorkOS)
59
- * - roleSlug enum prevents privilege escalation
60
- * - Strict mode prevents injection
61
- */
62
- export const CreateMembershipSchema = z
63
- .object({
64
- userId: z.string().min(1),
65
- organizationId: z.string().min(1),
66
- roleSlug: MembershipRoleSchema.default('member')
67
- })
68
- .strict()
69
-
70
- /**
71
- * Update membership role
72
- * PUT /memberships/:id
73
- *
74
- * Security:
75
- * - Only roleSlug can be updated
76
- * - Enum validation prevents privilege escalation
77
- */
78
- export const UpdateMembershipSchema = z
79
- .object({
80
- roleSlug: MembershipRoleSchema
81
- })
82
- .strict()
83
-
84
- // ============================================================================
85
- // Query Parameters
86
- // ============================================================================
87
-
88
- /**
89
- * List memberships with filters
90
- * GET /memberships
91
- *
92
- * Filters:
93
- * - userId: Filter by user
94
- * - organizationId: Filter by organization
95
- *
96
- * Security:
97
- * - Requires at least one filter parameter
98
- * - String IDs validated for WorkOS format
99
- */
100
- export const ListMembershipsQuerySchema = z
101
- .object({
102
- userId: z.string().optional(),
103
- organizationId: z.string().optional(),
104
- limit: z.coerce.number().int().min(1).max(100).optional(),
105
- before: z.string().optional(), // WorkOS pagination cursor
106
- after: z.string().optional() // WorkOS pagination cursor
107
- })
108
- .strict()
109
- .refine(
110
- (data) => data.userId || data.organizationId,
111
- {
112
- message: 'Either userId or organizationId must be provided'
113
- }
114
- )
115
-
116
- // ============================================================================
117
- // TypeScript Type Exports
118
- // ============================================================================
119
-
120
- // Export inferred types for use in route handlers
121
- export type CreateMembershipInput = z.infer<typeof CreateMembershipSchema>
122
- export type UpdateMembershipInput = z.infer<typeof UpdateMembershipSchema>
123
- export type ListMembershipsQuery = z.infer<typeof ListMembershipsQuerySchema>
124
- export type MembershipIdParam = z.infer<typeof MembershipIdParamSchema>
125
- export type MembershipRole = z.infer<typeof MembershipRoleSchema>
126
- export type MembershipStatus = z.infer<typeof MembershipStatusSchema>
1
+ /**
2
+ * Memberships Domain - Zod Validation Schemas
3
+ *
4
+ * Validation schemas for membership 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
+ * - UUID validation prevents invalid references
10
+ * - Role enum validation prevents privilege escalation
11
+ * - organizationId never accepted in body (from JWT when needed)
12
+ */
13
+
14
+ import { z } from 'zod'
15
+
16
+ // ============================================================================
17
+ // Shared Schemas
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Membership role validation
22
+ * Restricts to valid role slugs only
23
+ *
24
+ * Security: Prevents privilege escalation by limiting role values
25
+ */
26
+ export const MembershipRoleSchema = z.enum(['admin', 'member'])
27
+
28
+ /**
29
+ * Membership status validation
30
+ * Note: Database constraint only allows 'active' | 'inactive'
31
+ */
32
+ export const MembershipStatusSchema = z.enum(['active', 'inactive'])
33
+
34
+ // ============================================================================
35
+ // Path Parameters
36
+ // ============================================================================
37
+
38
+ /**
39
+ * Validate membership ID in URL path
40
+ * Used by: GET/PUT/DELETE /memberships/:id
41
+ */
42
+ export const MembershipIdParamSchema = z
43
+ .object({
44
+ id: z.string().min(1) // WorkOS membership IDs can be various formats
45
+ })
46
+ .strict()
47
+
48
+ /**
49
+ * Validate organization ID (Supabase UUID) in URL path
50
+ * Used by: GET /memberships/my-permissions/:orgId
51
+ */
52
+ export const OrgIdParamSchema = z
53
+ .object({
54
+ orgId: z.string().uuid()
55
+ })
56
+ .strict()
57
+
58
+ /**
59
+ * Response shape for GET /memberships/my-permissions/:orgId
60
+ */
61
+ export const MyOrgPermissionsResponseSchema = z.object({
62
+ permissions: z.array(z.string())
63
+ })
64
+
65
+ // ============================================================================
66
+ // Request Bodies
67
+ // ============================================================================
68
+
69
+ /**
70
+ * Create new membership
71
+ * POST /memberships
72
+ *
73
+ * Security:
74
+ * - userId must be valid (string format for WorkOS)
75
+ * - organizationId must be valid (string format for WorkOS)
76
+ * - roleSlug enum prevents privilege escalation
77
+ * - Strict mode prevents injection
78
+ */
79
+ export const CreateMembershipSchema = z
80
+ .object({
81
+ userId: z.string().min(1),
82
+ organizationId: z.string().min(1),
83
+ roleSlug: MembershipRoleSchema.default('member')
84
+ })
85
+ .strict()
86
+
87
+ /**
88
+ * Update membership role
89
+ * PUT /memberships/:id
90
+ *
91
+ * Security:
92
+ * - Only roleSlug can be updated
93
+ * - Enum validation prevents privilege escalation
94
+ */
95
+ export const UpdateMembershipSchema = z
96
+ .object({
97
+ roleSlug: MembershipRoleSchema
98
+ })
99
+ .strict()
100
+
101
+ // ============================================================================
102
+ // Query Parameters
103
+ // ============================================================================
104
+
105
+ /**
106
+ * List memberships with filters
107
+ * GET /memberships
108
+ *
109
+ * Filters:
110
+ * - userId: Filter by user
111
+ * - organizationId: Filter by organization
112
+ *
113
+ * Security:
114
+ * - Requires at least one filter parameter
115
+ * - String IDs validated for WorkOS format
116
+ */
117
+ export const ListMembershipsQuerySchema = z
118
+ .object({
119
+ userId: z.string().optional(),
120
+ organizationId: z.string().optional(),
121
+ limit: z.coerce.number().int().min(1).max(100).optional(),
122
+ before: z.string().optional(), // WorkOS pagination cursor
123
+ after: z.string().optional() // WorkOS pagination cursor
124
+ })
125
+ .strict()
126
+ .refine((data) => data.userId || data.organizationId, {
127
+ message: 'Either userId or organizationId must be provided'
128
+ })
129
+
130
+ // ============================================================================
131
+ // TypeScript Type Exports
132
+ // ============================================================================
133
+
134
+ // Export inferred types for use in route handlers
135
+ export type CreateMembershipInput = z.infer<typeof CreateMembershipSchema>
136
+ export type UpdateMembershipInput = z.infer<typeof UpdateMembershipSchema>
137
+ export type ListMembershipsQuery = z.infer<typeof ListMembershipsQuerySchema>
138
+ export type MembershipIdParam = z.infer<typeof MembershipIdParamSchema>
139
+ export type MembershipRole = z.infer<typeof MembershipRoleSchema>
140
+ export type MembershipStatus = z.infer<typeof MembershipStatusSchema>
141
+ export type OrgIdParam = z.infer<typeof OrgIdParamSchema>
142
+ export type MyOrgPermissionsResponse = z.infer<typeof MyOrgPermissionsResponseSchema>
@@ -1,22 +1,26 @@
1
- /**
2
- * Membership types - browser-safe exports only
3
- * For WorkOS types and transforms, use @repo/core/server
4
- */
5
- export * from './membership'
6
- export * from './supabase'
7
-
8
- // Validation schemas
9
- export {
10
- CreateMembershipSchema,
11
- UpdateMembershipSchema,
12
- ListMembershipsQuerySchema,
13
- MembershipIdParamSchema,
14
- MembershipRoleSchema,
15
- MembershipStatusSchema,
16
- type CreateMembershipInput,
17
- type UpdateMembershipInput,
18
- type ListMembershipsQuery,
19
- type MembershipIdParam,
20
- type MembershipRole,
21
- type MembershipStatus
22
- } from './api-schemas'
1
+ /**
2
+ * Membership types - browser-safe exports only
3
+ * For WorkOS types and transforms, use @repo/core/server
4
+ */
5
+ export * from './membership'
6
+ export * from './supabase'
7
+
8
+ // Validation schemas
9
+ export {
10
+ CreateMembershipSchema,
11
+ UpdateMembershipSchema,
12
+ ListMembershipsQuerySchema,
13
+ MembershipIdParamSchema,
14
+ MembershipRoleSchema,
15
+ MembershipStatusSchema,
16
+ OrgIdParamSchema,
17
+ MyOrgPermissionsResponseSchema,
18
+ type CreateMembershipInput,
19
+ type UpdateMembershipInput,
20
+ type ListMembershipsQuery,
21
+ type MembershipIdParam,
22
+ type MembershipRole,
23
+ type MembershipStatus,
24
+ type OrgIdParam,
25
+ type MyOrgPermissionsResponse
26
+ } from './api-schemas'
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { PERMISSIONS, PERMISSION_CATALOG, isPermissionKey } from './permissions'
3
+
4
+ describe('permissions catalog', () => {
5
+ const permissionValues = Object.values(PERMISSIONS)
6
+ const catalogKeys = PERMISSION_CATALOG.map((p) => p.key)
7
+
8
+ it('every PERMISSIONS value appears as a key in PERMISSION_CATALOG', () => {
9
+ for (const value of permissionValues) {
10
+ expect(catalogKeys).toContain(value)
11
+ }
12
+ })
13
+
14
+ it('every PERMISSION_CATALOG key exists as a value in PERMISSIONS', () => {
15
+ for (const key of catalogKeys) {
16
+ expect(permissionValues).toContain(key)
17
+ }
18
+ })
19
+
20
+ it('every catalog entry has a non-empty description and a boolean isOrgGrantable', () => {
21
+ for (const entry of PERMISSION_CATALOG) {
22
+ expect(typeof entry.description).toBe('string')
23
+ expect(entry.description.trim().length).toBeGreaterThan(0)
24
+ expect(typeof entry.isOrgGrantable).toBe('boolean')
25
+ }
26
+ })
27
+
28
+ it('catalog has no duplicate keys', () => {
29
+ const seen = new Set<string>()
30
+ for (const key of catalogKeys) {
31
+ expect(seen.has(key), `duplicate catalog key: ${key}`).toBe(false)
32
+ seen.add(key)
33
+ }
34
+ })
35
+
36
+ it('isPermissionKey returns true for every catalog key and false for unknown strings', () => {
37
+ for (const key of catalogKeys) {
38
+ expect(isPermissionKey(key)).toBe(true)
39
+ }
40
+ expect(isPermissionKey('not.a.permission')).toBe(false)
41
+ })
42
+ })
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Canonical permission catalog.
3
+ *
4
+ * Source of truth for the permission keys used by:
5
+ * - RLS policies in Supabase (via has_org_permission(org_id, key))
6
+ * - API middleware (via requireOrganizationPermission(key))
7
+ * - UI hooks (via useOrganizationPermissions().hasPermission(key))
8
+ *
9
+ * The DB table `org_rol_permissions` mirrors this constant. Reconciliation
10
+ * runs at API boot (insert-or-update only — never auto-delete; see the
11
+ * deletion runbook in the auth-role-system-redesign doc).
12
+ *
13
+ * Adding a permission:
14
+ * 1. Add an entry below.
15
+ * 2. Add a row to the migration / via reconcilePermissionCatalog at boot.
16
+ * 3. Reference it in RLS / middleware as needed.
17
+ * 4. Optionally grant it to one or more system roles in org_rol_grants.
18
+ *
19
+ * Removing a permission: follow the deletion runbook — never just delete
20
+ * the entry. Existing role grants and policy references must be cleared first.
21
+ */
22
+
23
+ export const PERMISSIONS = {
24
+ ORG_READ: 'org.read',
25
+ ORG_MANAGE: 'org.manage',
26
+ ORG_DELETE: 'org.delete',
27
+ MEMBERS_MANAGE: 'members.manage',
28
+ ROLES_MANAGE: 'roles.manage',
29
+ SECRETS_MANAGE: 'secrets.manage',
30
+ OPERATIONS_READ: 'operations.read',
31
+ OPERATIONS_MANAGE: 'operations.manage',
32
+ WORK_MANAGE: 'work.manage'
33
+ } as const
34
+
35
+ export type PermissionKey = (typeof PERMISSIONS)[keyof typeof PERMISSIONS]
36
+
37
+ /**
38
+ * Static metadata for each permission. Mirrored into org_rol_permissions on
39
+ * boot reconciliation. is_org_grantable=false means the permission is reserved
40
+ * to system roles only — custom roles cannot include it (privilege-escalation guard).
41
+ */
42
+ export interface PermissionDescriptor {
43
+ key: PermissionKey
44
+ description: string
45
+ isOrgGrantable: boolean
46
+ }
47
+
48
+ export const PERMISSION_CATALOG: readonly PermissionDescriptor[] = [
49
+ {
50
+ key: 'org.read',
51
+ description: 'Read organization profile and listings',
52
+ isOrgGrantable: true
53
+ },
54
+ {
55
+ key: 'org.manage',
56
+ description: 'Update organization settings',
57
+ isOrgGrantable: false
58
+ },
59
+ {
60
+ key: 'org.delete',
61
+ description: 'Delete the organization (owner-only)',
62
+ isOrgGrantable: false
63
+ },
64
+ {
65
+ key: 'members.manage',
66
+ description: 'Invite, remove, and reassign roles for members',
67
+ isOrgGrantable: false
68
+ },
69
+ {
70
+ key: 'roles.manage',
71
+ description: 'Grant or revoke privileged system roles (owner, admin) within the organization',
72
+ isOrgGrantable: false
73
+ },
74
+ {
75
+ key: 'secrets.manage',
76
+ description: 'Create, update, and delete API keys and credentials',
77
+ isOrgGrantable: false
78
+ },
79
+ {
80
+ key: 'operations.read',
81
+ description: 'View executions, sessions, schedules, and command queue',
82
+ isOrgGrantable: true
83
+ },
84
+ {
85
+ key: 'operations.manage',
86
+ description: 'Run and modify executions, sessions, schedules, queue',
87
+ isOrgGrantable: true
88
+ },
89
+ {
90
+ key: 'work.manage',
91
+ description: 'Create and edit business-domain records (acq_*, prj_*)',
92
+ isOrgGrantable: true
93
+ }
94
+ ] as const
95
+
96
+ const PERMISSION_KEY_SET: ReadonlySet<string> = new Set(PERMISSION_CATALOG.map((p) => p.key))
97
+
98
+ /**
99
+ * Type guard. Use at trust boundaries (request input, third-party data) before
100
+ * passing a string to `has_org_permission` / `requireOrganizationPermission`.
101
+ */
102
+ export function isPermissionKey(value: unknown): value is PermissionKey {
103
+ return typeof value === 'string' && PERMISSION_KEY_SET.has(value)
104
+ }