@elevasis/core 0.11.0 → 0.11.2
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 +3 -2
- package/dist/index.js +8 -13
- package/dist/organization-model/index.d.ts +3 -2
- package/dist/organization-model/index.js +8 -13
- package/dist/test-utils/index.d.ts +175 -0
- package/dist/test-utils/index.js +8 -8
- package/package.json +1 -1
- package/src/__tests__/template-core-compatibility.test.ts +6 -15
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +36 -38
- package/src/auth/multi-tenancy/index.ts +20 -17
- package/src/auth/multi-tenancy/memberships/api-schemas.ts +142 -126
- package/src/auth/multi-tenancy/memberships/index.ts +26 -22
- package/src/auth/multi-tenancy/permissions.test.ts +42 -0
- package/src/auth/multi-tenancy/permissions.ts +104 -0
- package/src/organization-model/README.md +1 -0
- package/src/organization-model/__tests__/defaults.test.ts +19 -6
- package/src/organization-model/contracts.ts +3 -3
- package/src/organization-model/defaults.ts +3 -8
- package/src/organization-model/domains/navigation.ts +26 -32
- package/src/organization-model/foundation.ts +2 -3
- package/src/organization-model/organization-model.mdx +3 -0
- package/src/organization-model/published.ts +5 -5
- package/src/platform/constants/versions.ts +3 -3
- package/src/platform/registry/index.ts +86 -91
- package/src/platform/registry/resource-registry.ts +905 -866
- package/src/reference/_generated/contracts.md +36 -38
- package/src/scaffold-registry/__tests__/index.test.ts +125 -1
- package/src/scaffold-registry/__tests__/schema.test.ts +48 -20
- package/src/scaffold-registry/index.ts +236 -188
- package/src/scaffold-registry/schema.ts +47 -22
- package/src/supabase/database.types.ts +2880 -2719
- package/src/test-utils/fixtures/memberships.ts +82 -80
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* -
|
|
76
|
-
* -
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* -
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
type
|
|
19
|
-
type
|
|
20
|
-
type
|
|
21
|
-
type
|
|
22
|
-
|
|
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
|
+
}
|
|
@@ -66,6 +66,7 @@ features: [
|
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
Containers omit `path`; leaves provide `path`. `uiPosition`, `requiresAdmin`, and `devOnly` inherit from ancestors.
|
|
69
|
+
Development-only features remain in the contract with `enabled: true` and `devOnly: true`; shell consumers hide them and guard their paths outside development mode.
|
|
69
70
|
|
|
70
71
|
## Graph IDs
|
|
71
72
|
|
|
@@ -69,12 +69,25 @@ describe('organization-model defaults', () => {
|
|
|
69
69
|
expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
it('resolveOrganizationModel with no override equals resolveOrganizationModel with DEFAULT_ORGANIZATION_MODEL', () => {
|
|
73
|
-
const fromUndefined = resolveOrganizationModel(undefined)
|
|
74
|
-
const fromDefault = resolveOrganizationModel(DEFAULT_ORGANIZATION_MODEL)
|
|
75
|
-
expect(fromDefault).toEqual(fromUndefined)
|
|
76
|
-
})
|
|
77
|
-
|
|
72
|
+
it('resolveOrganizationModel with no override equals resolveOrganizationModel with DEFAULT_ORGANIZATION_MODEL', () => {
|
|
73
|
+
const fromUndefined = resolveOrganizationModel(undefined)
|
|
74
|
+
const fromDefault = resolveOrganizationModel(DEFAULT_ORGANIZATION_MODEL)
|
|
75
|
+
expect(fromDefault).toEqual(fromUndefined)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('keeps Command View enabled but development-only in the default Operations navigation', () => {
|
|
79
|
+
const result = resolveOrganizationModel(undefined)
|
|
80
|
+
const commandViewFeature = result.features.find((feature) => feature.id === 'operations.command-view')
|
|
81
|
+
const commandViewSurface = DEFAULT_ORGANIZATION_MODEL_NAVIGATION.surfaces.find(
|
|
82
|
+
(surface) => surface.id === 'operations.command-view'
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
expect(commandViewFeature?.enabled).toBe(true)
|
|
86
|
+
expect(commandViewFeature?.devOnly).toBe(true)
|
|
87
|
+
expect(commandViewSurface?.enabled).toBe(true)
|
|
88
|
+
expect(commandViewSurface?.devOnly).toBe(true)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
78
91
|
|
|
79
92
|
describe.each(domainCases)('$name (domain sub-constant)', ({ name: _name, constant, schema }) => {
|
|
80
93
|
it('passes its domain schema parse without throwing', () => {
|
|
@@ -9,6 +9,6 @@ export const MONITORING_FEATURE_ID = 'monitoring' as const
|
|
|
9
9
|
export const SETTINGS_FEATURE_ID = 'settings' as const
|
|
10
10
|
export const SEO_FEATURE_ID = 'seo' as const
|
|
11
11
|
|
|
12
|
-
export const SALES_PIPELINE_SURFACE_ID = 'crm.pipeline' as const
|
|
13
|
-
export const PROSPECTING_LISTS_SURFACE_ID = 'lead-gen.lists' as const
|
|
14
|
-
export const
|
|
12
|
+
export const SALES_PIPELINE_SURFACE_ID = 'crm.pipeline' as const
|
|
13
|
+
export const PROSPECTING_LISTS_SURFACE_ID = 'lead-gen.lists' as const
|
|
14
|
+
export const OPERATIONS_COMMAND_VIEW_SURFACE_ID = 'operations.command-view' as const
|
|
@@ -68,17 +68,12 @@ export const DEFAULT_ORGANIZATION_MODEL: OrganizationModel = {
|
|
|
68
68
|
icon: 'operations',
|
|
69
69
|
uiPosition: 'sidebar-primary'
|
|
70
70
|
},
|
|
71
|
-
{
|
|
72
|
-
id: 'operations.graph',
|
|
73
|
-
label: 'Organization Graph',
|
|
74
|
-
enabled: true,
|
|
75
|
-
path: '/operations/organization-graph'
|
|
76
|
-
},
|
|
77
71
|
{
|
|
78
72
|
id: 'operations.command-view',
|
|
79
73
|
label: 'Command View',
|
|
80
|
-
enabled:
|
|
81
|
-
path: '/operations/command-view'
|
|
74
|
+
enabled: true,
|
|
75
|
+
path: '/operations/command-view',
|
|
76
|
+
devOnly: true
|
|
82
77
|
},
|
|
83
78
|
{
|
|
84
79
|
id: 'operations.overview',
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
OPERATIONS_COMMAND_VIEW_SURFACE_ID,
|
|
4
|
+
PROJECTS_VIEW_CAPABILITY_ID,
|
|
5
|
+
PROJECTS_FEATURE_ID,
|
|
6
|
+
PROJECTS_INDEX_SURFACE_ID
|
|
7
|
+
} from '../contracts'
|
|
3
8
|
import { DescriptionSchema, IconNameSchema, LabelSchema, ModelIdSchema, PathSchema, ReferenceIdsSchema } from './shared'
|
|
4
9
|
|
|
5
10
|
export const SurfaceTypeSchema = z.enum(['page', 'dashboard', 'graph', 'detail', 'list', 'settings'])
|
|
@@ -9,9 +14,10 @@ export const SurfaceDefinitionSchema = z.object({
|
|
|
9
14
|
label: LabelSchema,
|
|
10
15
|
path: PathSchema,
|
|
11
16
|
surfaceType: SurfaceTypeSchema,
|
|
12
|
-
description: DescriptionSchema.optional(),
|
|
13
|
-
enabled: z.boolean().default(true),
|
|
14
|
-
|
|
17
|
+
description: DescriptionSchema.optional(),
|
|
18
|
+
enabled: z.boolean().default(true),
|
|
19
|
+
devOnly: z.boolean().optional(),
|
|
20
|
+
icon: IconNameSchema.optional(),
|
|
15
21
|
featureId: ModelIdSchema.optional(),
|
|
16
22
|
featureIds: ReferenceIdsSchema,
|
|
17
23
|
entityIds: ReferenceIdsSchema,
|
|
@@ -85,27 +91,16 @@ export const DEFAULT_ORGANIZATION_MODEL_NAVIGATION: z.infer<typeof OrganizationM
|
|
|
85
91
|
resourceIds: [],
|
|
86
92
|
capabilityIds: [PROJECTS_VIEW_CAPABILITY_ID]
|
|
87
93
|
},
|
|
88
|
-
{
|
|
89
|
-
id:
|
|
90
|
-
label: '
|
|
91
|
-
path: '/operations/
|
|
92
|
-
surfaceType: 'graph',
|
|
93
|
-
enabled: true,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
capabilityIds: ['operations.organization-graph']
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id: 'operations.command-view',
|
|
102
|
-
label: 'Command View',
|
|
103
|
-
path: '/operations/command-view',
|
|
104
|
-
surfaceType: 'graph',
|
|
105
|
-
enabled: false,
|
|
106
|
-
featureId: 'operations',
|
|
107
|
-
featureIds: ['operations'],
|
|
108
|
-
entityIds: [],
|
|
94
|
+
{
|
|
95
|
+
id: OPERATIONS_COMMAND_VIEW_SURFACE_ID,
|
|
96
|
+
label: 'Command View',
|
|
97
|
+
path: '/operations/command-view',
|
|
98
|
+
surfaceType: 'graph',
|
|
99
|
+
enabled: true,
|
|
100
|
+
devOnly: true,
|
|
101
|
+
featureId: 'operations',
|
|
102
|
+
featureIds: ['operations'],
|
|
103
|
+
entityIds: [],
|
|
109
104
|
resourceIds: [],
|
|
110
105
|
capabilityIds: ['operations.command-view']
|
|
111
106
|
},
|
|
@@ -347,13 +342,12 @@ export const DEFAULT_ORGANIZATION_MODEL_NAVIGATION: z.infer<typeof OrganizationM
|
|
|
347
342
|
},
|
|
348
343
|
{
|
|
349
344
|
id: 'primary-operations',
|
|
350
|
-
label: 'Operations',
|
|
351
|
-
placement: 'primary',
|
|
352
|
-
surfaceIds: [
|
|
353
|
-
|
|
354
|
-
'operations.
|
|
355
|
-
'operations.
|
|
356
|
-
'operations.resources',
|
|
345
|
+
label: 'Operations',
|
|
346
|
+
placement: 'primary',
|
|
347
|
+
surfaceIds: [
|
|
348
|
+
OPERATIONS_COMMAND_VIEW_SURFACE_ID,
|
|
349
|
+
'operations.overview',
|
|
350
|
+
'operations.resources',
|
|
357
351
|
'operations.command-queue',
|
|
358
352
|
'operations.sessions',
|
|
359
353
|
'operations.task-scheduler'
|
|
@@ -44,14 +44,13 @@ export function createFoundationOrganizationModel(override: DeepPartial<Organiza
|
|
|
44
44
|
return feature
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const
|
|
47
|
+
const operationsFeature = requireCoreFeature('operations')
|
|
48
48
|
const projectsFeature = requireCoreFeature('projects')
|
|
49
49
|
const leadGenFeature = requireCoreFeature('sales.lead-gen')
|
|
50
50
|
const crmFeature = requireCoreFeature('sales.crm')
|
|
51
51
|
|
|
52
52
|
const navigationSurfaces: FoundationNavigationSurface[] = [
|
|
53
|
-
{ ...
|
|
54
|
-
{ ...operationsGraphFeature, path: '/operations', icon: 'operations', surfaceType: 'graph' },
|
|
53
|
+
{ ...operationsFeature, id: 'operations', path: '/operations', icon: 'operations', surfaceType: 'dashboard' },
|
|
55
54
|
{ ...projectsFeature, icon: 'projects', surfaceType: 'list' },
|
|
56
55
|
{ ...leadGenFeature, id: 'lead-gen', icon: 'lead-gen', surfaceType: 'list' },
|
|
57
56
|
{ ...crmFeature, id: 'crm', icon: 'crm', surfaceType: 'graph' },
|
|
@@ -67,6 +67,9 @@ Authored fields:
|
|
|
67
67
|
- `devOnly`
|
|
68
68
|
|
|
69
69
|
Containers omit `path`; leaves provide `path`. `uiPosition`, `requiresAdmin`, and `devOnly` inherit from ancestors.
|
|
70
|
+
Development-only features remain defined and enabled with `devOnly: true`; shell consumers hide those entries and route paths outside development mode while preserving the semantic contract.
|
|
71
|
+
|
|
72
|
+
Navigation surfaces may also carry `devOnly` for compatibility with the legacy/domain navigation model. `operations.command-view` is intentionally development-only while its graph visualization modes continue to mature.
|
|
70
73
|
|
|
71
74
|
## Graph IDs and Resource Links
|
|
72
75
|
|