@elevasis/core 0.29.0 → 0.30.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/auth/index.d.ts +5289 -0
- package/dist/auth/index.js +595 -0
- package/dist/test-utils/index.d.ts +20 -0
- package/package.json +5 -1
- package/src/__tests__/publish.test.ts +8 -7
- package/src/auth/__tests__/access-key-coverage.test.ts +42 -0
- package/src/auth/__tests__/access-key-scan.ts +117 -0
- package/src/auth/__tests__/access-keys.test.ts +81 -0
- package/src/auth/__tests__/access-model.test.ts +257 -0
- package/src/auth/__tests__/access-test-fixtures.ts +50 -0
- package/src/auth/__tests__/key-catalog-drift.test.ts +33 -0
- package/src/auth/__tests__/platform-admin-bypass-parity.test.ts +67 -0
- package/src/auth/access-keys.ts +173 -0
- package/src/auth/access-model.ts +185 -0
- package/src/auth/index.ts +6 -2
- package/src/auth/multi-tenancy/memberships/membership.ts +2 -4
- package/src/auth/multi-tenancy/permissions.ts +1 -1
- package/src/auth/multi-tenancy/types.ts +3 -12
- package/src/reference/glossary.md +8 -6
- package/src/supabase/database.types.ts +10 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { listAllSystems } from '../organization-model/helpers'
|
|
3
|
+
import type { OrganizationModel } from '../organization-model/types'
|
|
4
|
+
import { PERMISSIONS, type PermissionKey } from './multi-tenancy/permissions'
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_ACCESS_ACTION = 'view' as const
|
|
7
|
+
export const PLATFORM_ADMIN_ACCESS_KEY = 'platform.admin' as const
|
|
8
|
+
export const PLATFORM_ADMIN_ACCESS_KEY_SHORTHAND = 'platformAdmin' as const
|
|
9
|
+
|
|
10
|
+
export const AccessActionSchema = z.enum(['view', 'manage'])
|
|
11
|
+
|
|
12
|
+
export const AccessKeyObjectSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
systemPath: z.string().trim().min(1),
|
|
15
|
+
action: AccessActionSchema.default(DEFAULT_ACCESS_ACTION)
|
|
16
|
+
})
|
|
17
|
+
.strict()
|
|
18
|
+
|
|
19
|
+
export const AccessKeyInputSchema = z.union([z.string().trim().min(1), AccessKeyObjectSchema])
|
|
20
|
+
export const NormalizedAccessKeySchema = AccessKeyObjectSchema
|
|
21
|
+
export const AccessKeySchema = AccessKeyInputSchema
|
|
22
|
+
|
|
23
|
+
export type AccessAction = z.infer<typeof AccessActionSchema>
|
|
24
|
+
export type AccessKeyObject = z.infer<typeof AccessKeyObjectSchema>
|
|
25
|
+
export type AccessKeyInput = z.input<typeof AccessKeyInputSchema>
|
|
26
|
+
export type NormalizedAccessKey = z.infer<typeof NormalizedAccessKeySchema>
|
|
27
|
+
export type AccessKey = NormalizedAccessKey
|
|
28
|
+
|
|
29
|
+
export type AccessCatalogEntrySource = 'om-system' | 'diagnostic' | 'platform' | 'permission'
|
|
30
|
+
|
|
31
|
+
export interface AccessCatalogEntry {
|
|
32
|
+
key: NormalizedAccessKey
|
|
33
|
+
source: AccessCatalogEntrySource
|
|
34
|
+
rolePermission?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AccessKeyCatalog {
|
|
38
|
+
bySystemPath: ReadonlyMap<string, readonly AccessCatalogEntry[]>
|
|
39
|
+
entries: readonly AccessCatalogEntry[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const DIAGNOSTIC_VIEW_ACCESS_KEYS = [
|
|
43
|
+
'diagnostic.operations.overview',
|
|
44
|
+
'diagnostic.operations.recent-executions',
|
|
45
|
+
'diagnostic.monitoring.execution-logs',
|
|
46
|
+
'diagnostic.monitoring.notifications'
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
export const AccessKeys = {
|
|
50
|
+
platformAdmin: { systemPath: PLATFORM_ADMIN_ACCESS_KEY, action: DEFAULT_ACCESS_ACTION },
|
|
51
|
+
organizationManage: { systemPath: 'permission.org', action: 'manage' },
|
|
52
|
+
membersManage: { systemPath: 'permission.members', action: 'manage' },
|
|
53
|
+
rolesManage: { systemPath: 'permission.roles', action: 'manage' },
|
|
54
|
+
secretsManage: { systemPath: 'permission.secrets', action: 'manage' },
|
|
55
|
+
operationsRead: { systemPath: 'permission.operations', action: DEFAULT_ACCESS_ACTION },
|
|
56
|
+
operationsManage: { systemPath: 'permission.operations', action: 'manage' },
|
|
57
|
+
acquisitionManage: { systemPath: 'permission.acquisition', action: 'manage' },
|
|
58
|
+
projectsManage: { systemPath: 'permission.projects', action: 'manage' },
|
|
59
|
+
clientsManage: { systemPath: 'permission.clients', action: 'manage' },
|
|
60
|
+
operationsOverview: { systemPath: 'diagnostic.operations.overview', action: DEFAULT_ACCESS_ACTION },
|
|
61
|
+
operationsRecentExecutions: { systemPath: 'diagnostic.operations.recent-executions', action: DEFAULT_ACCESS_ACTION },
|
|
62
|
+
monitoringExecutionLogs: { systemPath: 'diagnostic.monitoring.execution-logs', action: DEFAULT_ACCESS_ACTION },
|
|
63
|
+
monitoringNotifications: { systemPath: 'diagnostic.monitoring.notifications', action: DEFAULT_ACCESS_ACTION }
|
|
64
|
+
} as const satisfies Record<string, NormalizedAccessKey>
|
|
65
|
+
|
|
66
|
+
const PERMISSION_ACCESS_KEY_DEFINITIONS: readonly {
|
|
67
|
+
key: NormalizedAccessKey
|
|
68
|
+
rolePermission: PermissionKey
|
|
69
|
+
}[] = [
|
|
70
|
+
{ key: AccessKeys.organizationManage, rolePermission: PERMISSIONS.ORG_MANAGE },
|
|
71
|
+
{ key: AccessKeys.membersManage, rolePermission: PERMISSIONS.MEMBERS_MANAGE },
|
|
72
|
+
{ key: AccessKeys.rolesManage, rolePermission: PERMISSIONS.ROLES_MANAGE },
|
|
73
|
+
{ key: AccessKeys.secretsManage, rolePermission: PERMISSIONS.SECRETS_MANAGE },
|
|
74
|
+
{ key: AccessKeys.operationsRead, rolePermission: PERMISSIONS.OPERATIONS_READ },
|
|
75
|
+
{ key: AccessKeys.operationsManage, rolePermission: PERMISSIONS.OPERATIONS_MANAGE },
|
|
76
|
+
{ key: AccessKeys.acquisitionManage, rolePermission: PERMISSIONS.ACQUISITION_MANAGE },
|
|
77
|
+
{ key: AccessKeys.projectsManage, rolePermission: PERMISSIONS.PROJECTS_MANAGE },
|
|
78
|
+
{ key: AccessKeys.clientsManage, rolePermission: PERMISSIONS.CLIENTS_MANAGE }
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
export function normalizeAccessKey(input: AccessKeyInput): NormalizedAccessKey {
|
|
82
|
+
const parsed = AccessKeyInputSchema.parse(input)
|
|
83
|
+
const normalized =
|
|
84
|
+
typeof parsed === 'string'
|
|
85
|
+
? {
|
|
86
|
+
systemPath: parsed === PLATFORM_ADMIN_ACCESS_KEY_SHORTHAND ? PLATFORM_ADMIN_ACCESS_KEY : parsed,
|
|
87
|
+
action: DEFAULT_ACCESS_ACTION
|
|
88
|
+
}
|
|
89
|
+
: parsed
|
|
90
|
+
|
|
91
|
+
return NormalizedAccessKeySchema.parse(normalized)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function accessKeyToString(input: AccessKeyInput): string {
|
|
95
|
+
const key = normalizeAccessKey(input)
|
|
96
|
+
return `${key.systemPath}:${key.action}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function rolePermissionForAccessKey(key: NormalizedAccessKey): string | undefined {
|
|
100
|
+
if (key.action === DEFAULT_ACCESS_ACTION) return undefined
|
|
101
|
+
return `${key.systemPath}.${key.action}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface DeriveAccessKeyCatalogOptions {
|
|
105
|
+
diagnosticKeys?: readonly string[]
|
|
106
|
+
includeManageActions?: boolean
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function groupCatalogEntries(entries: readonly AccessCatalogEntry[]): ReadonlyMap<string, readonly AccessCatalogEntry[]> {
|
|
110
|
+
const grouped = new Map<string, AccessCatalogEntry[]>()
|
|
111
|
+
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const existing = grouped.get(entry.key.systemPath) ?? []
|
|
114
|
+
existing.push(entry)
|
|
115
|
+
grouped.set(entry.key.systemPath, existing)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return grouped
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildCatalogEntry(systemPath: string, action: AccessAction, source: AccessCatalogEntrySource): AccessCatalogEntry {
|
|
122
|
+
const key = normalizeAccessKey({ systemPath, action })
|
|
123
|
+
return {
|
|
124
|
+
key,
|
|
125
|
+
source,
|
|
126
|
+
rolePermission: rolePermissionForAccessKey(key)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function deriveAccessKeyCatalog(
|
|
131
|
+
organizationModel: OrganizationModel,
|
|
132
|
+
options: DeriveAccessKeyCatalogOptions = {}
|
|
133
|
+
): AccessKeyCatalog {
|
|
134
|
+
const { diagnosticKeys = DIAGNOSTIC_VIEW_ACCESS_KEYS, includeManageActions = true } = options
|
|
135
|
+
const entries: AccessCatalogEntry[] = [
|
|
136
|
+
buildCatalogEntry(PLATFORM_ADMIN_ACCESS_KEY, DEFAULT_ACCESS_ACTION, 'platform')
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
for (const { path } of listAllSystems(organizationModel)) {
|
|
140
|
+
entries.push(buildCatalogEntry(path, DEFAULT_ACCESS_ACTION, 'om-system'))
|
|
141
|
+
if (includeManageActions) {
|
|
142
|
+
entries.push(buildCatalogEntry(path, 'manage', 'om-system'))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const { key, rolePermission } of PERMISSION_ACCESS_KEY_DEFINITIONS) {
|
|
147
|
+
entries.push({
|
|
148
|
+
key,
|
|
149
|
+
source: 'permission',
|
|
150
|
+
rolePermission
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const systemPath of diagnosticKeys) {
|
|
155
|
+
entries.push(buildCatalogEntry(systemPath, DEFAULT_ACCESS_ACTION, 'diagnostic'))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
bySystemPath: groupCatalogEntries(entries),
|
|
160
|
+
entries
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function findAccessCatalogEntry(
|
|
165
|
+
catalog: AccessKeyCatalog,
|
|
166
|
+
key: NormalizedAccessKey
|
|
167
|
+
): AccessCatalogEntry | undefined {
|
|
168
|
+
return catalog.bySystemPath.get(key.systemPath)?.find((entry) => entry.key.action === key.action)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function listAccessKeys(catalog: AccessKeyCatalog): NormalizedAccessKey[] {
|
|
172
|
+
return catalog.entries.map((entry) => entry.key)
|
|
173
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ACCESS_ACTION,
|
|
3
|
+
PLATFORM_ADMIN_ACCESS_KEY,
|
|
4
|
+
deriveAccessKeyCatalog,
|
|
5
|
+
findAccessCatalogEntry,
|
|
6
|
+
normalizeAccessKey,
|
|
7
|
+
type AccessKeyCatalog,
|
|
8
|
+
type AccessKeyInput,
|
|
9
|
+
type NormalizedAccessKey
|
|
10
|
+
} from './access-keys'
|
|
11
|
+
import { getSystem } from '../organization-model/helpers'
|
|
12
|
+
import type { OrganizationModel, OrganizationModelSystemLifecycle } from '../organization-model/types'
|
|
13
|
+
|
|
14
|
+
export type AccessRestrictedBy =
|
|
15
|
+
| 'catalog'
|
|
16
|
+
| 'membership'
|
|
17
|
+
| 'system-lifecycle'
|
|
18
|
+
| 'role-permission'
|
|
19
|
+
| 'diagnostic-allowlist'
|
|
20
|
+
| null
|
|
21
|
+
|
|
22
|
+
export type AccessReason =
|
|
23
|
+
| 'allowed'
|
|
24
|
+
| 'platform-admin-bypass'
|
|
25
|
+
| 'invalid-access-key'
|
|
26
|
+
| 'unknown-access-key'
|
|
27
|
+
| 'organization-mismatch'
|
|
28
|
+
| 'missing-membership'
|
|
29
|
+
| 'system-not-active'
|
|
30
|
+
| 'role-permission-denied'
|
|
31
|
+
| 'diagnostic-key-not-allowed'
|
|
32
|
+
|
|
33
|
+
export interface AccessAnswer {
|
|
34
|
+
allowed: boolean
|
|
35
|
+
restrictedBy: AccessRestrictedBy
|
|
36
|
+
reason: AccessReason
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AccessContextMembership {
|
|
40
|
+
id?: string
|
|
41
|
+
organizationId: string
|
|
42
|
+
role?: string | null
|
|
43
|
+
effectivePermissions?: readonly string[] | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AccessContextProfile {
|
|
47
|
+
id?: string
|
|
48
|
+
isPlatformAdmin?: boolean | null
|
|
49
|
+
is_platform_admin?: boolean | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AccessContext {
|
|
53
|
+
organizationId: string
|
|
54
|
+
organizationModel: OrganizationModel
|
|
55
|
+
membership?: AccessContextMembership | null
|
|
56
|
+
profile?: AccessContextProfile | null
|
|
57
|
+
permissions?: readonly string[] | null
|
|
58
|
+
diagnosticAllowlist?: ReadonlySet<string> | readonly string[]
|
|
59
|
+
accessCatalog?: AccessKeyCatalog
|
|
60
|
+
betaAccessEnabled?: boolean
|
|
61
|
+
isDevelopment?: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface AccessModel {
|
|
65
|
+
catalog: AccessKeyCatalog
|
|
66
|
+
checkAccess: (key: AccessKeyInput, ctx: Omit<AccessContext, 'accessCatalog'>) => AccessAnswer
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ALLOWED: AccessAnswer = { allowed: true, restrictedBy: null, reason: 'allowed' }
|
|
70
|
+
const PLATFORM_ADMIN_BYPASS: AccessAnswer = {
|
|
71
|
+
allowed: true,
|
|
72
|
+
restrictedBy: null,
|
|
73
|
+
reason: 'platform-admin-bypass'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function deny(restrictedBy: Exclude<AccessRestrictedBy, null>, reason: Exclude<AccessReason, 'allowed'>): AccessAnswer {
|
|
77
|
+
return { allowed: false, restrictedBy, reason }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isPlatformAdmin(profile: AccessContextProfile | null | undefined): boolean {
|
|
81
|
+
return profile?.isPlatformAdmin === true || profile?.is_platform_admin === true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function diagnosticAllowlistHas(
|
|
85
|
+
allowlist: AccessContext['diagnosticAllowlist'],
|
|
86
|
+
systemPath: string
|
|
87
|
+
): boolean {
|
|
88
|
+
if (allowlist === undefined) return false
|
|
89
|
+
return 'has' in allowlist ? allowlist.has(systemPath) : allowlist.includes(systemPath)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function lifecycleAllowsAccess(
|
|
93
|
+
lifecycle: OrganizationModelSystemLifecycle | undefined,
|
|
94
|
+
ctx: Pick<AccessContext, 'betaAccessEnabled' | 'isDevelopment'>
|
|
95
|
+
): boolean {
|
|
96
|
+
if (lifecycle === 'active') return true
|
|
97
|
+
if (lifecycle === 'beta') return ctx.betaAccessEnabled === true || ctx.isDevelopment === true
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getPermissionSource(ctx: AccessContext): readonly string[] | null | undefined {
|
|
102
|
+
return ctx.permissions ?? ctx.membership?.effectivePermissions
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hasRequiredPermission(key: NormalizedAccessKey, rolePermission: string | undefined, ctx: AccessContext): boolean {
|
|
106
|
+
const permissionSource = getPermissionSource(ctx)
|
|
107
|
+
if (permissionSource === undefined || permissionSource === null) return true
|
|
108
|
+
|
|
109
|
+
const requiredPermission = rolePermission ?? `${key.systemPath}.${key.action}`
|
|
110
|
+
return permissionSource.includes(requiredPermission)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hasExplicitRequiredPermission(rolePermission: string | undefined, ctx: AccessContext): boolean {
|
|
114
|
+
const permissionSource = getPermissionSource(ctx)
|
|
115
|
+
return rolePermission !== undefined && permissionSource !== undefined && permissionSource !== null
|
|
116
|
+
? permissionSource.includes(rolePermission)
|
|
117
|
+
: false
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function checkAccess(input: AccessKeyInput, ctx: AccessContext): AccessAnswer {
|
|
121
|
+
if (isPlatformAdmin(ctx.profile)) return PLATFORM_ADMIN_BYPASS
|
|
122
|
+
|
|
123
|
+
const parsed = (() => {
|
|
124
|
+
try {
|
|
125
|
+
return normalizeAccessKey(input)
|
|
126
|
+
} catch {
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
})()
|
|
130
|
+
|
|
131
|
+
if (parsed === null) return deny('catalog', 'invalid-access-key')
|
|
132
|
+
|
|
133
|
+
const catalog = ctx.accessCatalog ?? deriveAccessKeyCatalog(ctx.organizationModel)
|
|
134
|
+
const catalogEntry = findAccessCatalogEntry(catalog, parsed)
|
|
135
|
+
|
|
136
|
+
if (catalogEntry === undefined) return deny('catalog', 'unknown-access-key')
|
|
137
|
+
|
|
138
|
+
const membership = ctx.membership
|
|
139
|
+
if (membership === undefined || membership === null) return deny('membership', 'missing-membership')
|
|
140
|
+
if (membership.organizationId !== ctx.organizationId) return deny('membership', 'organization-mismatch')
|
|
141
|
+
|
|
142
|
+
if (parsed.systemPath === PLATFORM_ADMIN_ACCESS_KEY) {
|
|
143
|
+
return deny('role-permission', 'role-permission-denied')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (catalogEntry.source === 'diagnostic') {
|
|
147
|
+
if (!diagnosticAllowlistHas(ctx.diagnosticAllowlist, parsed.systemPath)) {
|
|
148
|
+
return deny('diagnostic-allowlist', 'diagnostic-key-not-allowed')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (parsed.action !== DEFAULT_ACCESS_ACTION && !hasRequiredPermission(parsed, catalogEntry.rolePermission, ctx)) {
|
|
152
|
+
return deny('role-permission', 'role-permission-denied')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return ALLOWED
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (catalogEntry.source === 'permission') {
|
|
159
|
+
if (!hasExplicitRequiredPermission(catalogEntry.rolePermission, ctx)) {
|
|
160
|
+
return deny('role-permission', 'role-permission-denied')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return ALLOWED
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const system = getSystem(ctx.organizationModel, parsed.systemPath)
|
|
167
|
+
if (system === undefined || !lifecycleAllowsAccess(system.lifecycle, ctx)) {
|
|
168
|
+
return deny('system-lifecycle', 'system-not-active')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (parsed.action !== DEFAULT_ACCESS_ACTION && !hasRequiredPermission(parsed, catalogEntry.rolePermission, ctx)) {
|
|
172
|
+
return deny('role-permission', 'role-permission-denied')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return ALLOWED
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function createAccessModel(organizationModel: OrganizationModel): AccessModel {
|
|
179
|
+
const catalog = deriveAccessKeyCatalog(organizationModel)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
catalog,
|
|
183
|
+
checkAccess: (key, ctx) => checkAccess(key, { ...ctx, organizationModel, accessCatalog: catalog })
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/auth/index.ts
CHANGED
|
@@ -4,5 +4,9 @@
|
|
|
4
4
|
* Identity and multi-tenancy types
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
// Multi-tenancy types (browser-safe only)
|
|
8
|
-
export * from './multi-tenancy/index'
|
|
7
|
+
// Multi-tenancy types (browser-safe only)
|
|
8
|
+
export * from './multi-tenancy/index'
|
|
9
|
+
|
|
10
|
+
// Unified access model primitives (browser-safe only)
|
|
11
|
+
export * from './access-keys'
|
|
12
|
+
export * from './access-model'
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { type MembershipStatus } from './api-schemas.js'
|
|
1
|
+
import { type MembershipStatus } from './api-schemas.js'
|
|
3
2
|
|
|
4
3
|
export type { MembershipStatus }
|
|
5
4
|
|
|
@@ -75,8 +74,7 @@ export interface MembershipWithDetails extends OrganizationMembership {
|
|
|
75
74
|
metadata?: Record<string, unknown>
|
|
76
75
|
config?: Record<string, unknown>
|
|
77
76
|
}
|
|
78
|
-
|
|
79
|
-
}
|
|
77
|
+
}
|
|
80
78
|
|
|
81
79
|
/**
|
|
82
80
|
* UI-specific membership table row data
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Source of truth for the permission keys used by:
|
|
5
5
|
* - RLS policies in Supabase (via has_org_permission(org_id, key))
|
|
6
6
|
* - API middleware (via requireOrganizationPermission(key))
|
|
7
|
-
* - UI
|
|
7
|
+
* - UI access checks (via useAccess() permission-backed AccessKeys)
|
|
8
8
|
*
|
|
9
9
|
* The DB table `org_rol_permissions` mirrors this constant. There is no
|
|
10
10
|
* runtime reconciler; parity is enforced two ways:
|
|
@@ -3,23 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Config is stored in dedicated `config` columns (NOT nested in metadata):
|
|
5
5
|
* - organizations.config: Org-level config (no feature toggles -- all features available by default)
|
|
6
|
-
* - org_memberships.config:
|
|
6
|
+
* - org_memberships.config: Reserved for non-access membership configuration
|
|
7
7
|
* - users.config: User-global config
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ThemePresetName } from './theme-presets'
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
*
|
|
14
|
-
* Controls which features a specific member can access within their org.
|
|
15
|
-
* Keys are feature IDs from the organization model (e.g. crm, lead-gen, projects, seo).
|
|
16
|
-
*/
|
|
17
|
-
export interface MembershipFeatureConfig {
|
|
18
|
-
features?: Record<string, boolean>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* User-global config (stored in users.config)
|
|
12
|
+
/**
|
|
13
|
+
* User-global config (stored in users.config)
|
|
23
14
|
* Theme and onboarding are user-specific, NOT org-specific
|
|
24
15
|
*/
|
|
25
16
|
export interface UserConfig {
|
|
@@ -7,7 +7,9 @@ description: Terminology disambiguation for Organization OS concepts used in the
|
|
|
7
7
|
|
|
8
8
|
## Terms
|
|
9
9
|
|
|
10
|
-
**
|
|
10
|
+
**AccessGuard** -- route-level and section-level access wrapper from `@elevasis/ui/auth`. It consumes the unified Access Model and gates on an `accessKey`.
|
|
11
|
+
|
|
12
|
+
**AccessKeys** -- exported access-key constants from `@elevasis/core/auth` / `@repo/core/auth` for platform-admin, diagnostic, and permission-backed gates.
|
|
11
13
|
|
|
12
14
|
**Contract** -- the publishable boundary a consumer depends on: Zod schemas, TypeScript types, provider props, resource definitions, or workflow I/O schemas.
|
|
13
15
|
|
|
@@ -15,7 +17,7 @@ description: Terminology disambiguation for Organization OS concepts used in the
|
|
|
15
17
|
|
|
16
18
|
**Feature** -- legacy or UI-package wording for a platform capability area. In the current Organization Model, use **System** for semantic ownership and **navigation surface** for shell placement. Keep "feature" only when referring to existing UI package folders, exported manifest names, or legacy compatibility recipes.
|
|
17
19
|
|
|
18
|
-
**SystemGuard** -- route-level System gate
|
|
20
|
+
**SystemGuard** -- retired route-level System gate. Use **AccessGuard** with a System path or `AccessKeys` constant.
|
|
19
21
|
|
|
20
22
|
**SystemModule** -- manifest contract a shell module provides to `ElevasisSystemsProvider`. Key fields are `key`, optional `systemId`, optional `routePrefixes`, optional `capabilityIds`, optional `icon`, optional `sidebar`, and optional `sidebarWidth`. Graph bridge metadata is compatibility-only; new semantic bindings belong in the Organization Model System, ontology, navigation, and Resource descriptors.
|
|
21
23
|
|
|
@@ -29,13 +31,13 @@ description: Terminology disambiguation for Organization OS concepts used in the
|
|
|
29
31
|
|
|
30
32
|
**Manifest** -- a runtime declaration for a feature or resource collection.
|
|
31
33
|
|
|
32
|
-
**MembershipFeatureConfig** --
|
|
34
|
+
**MembershipFeatureConfig** -- retired per-member feature override config. The migration is complete: access is resolved through the unified Access Model using Organization Model System lifecycle, role permissions, diagnostic allowlists, membership scope, and platform-admin bypass.
|
|
33
35
|
|
|
34
36
|
**OrganizationModel** -- top-level semantic contract for an organization. Current primary fields include `version`, `domainMetadata`, `branding`, `navigation`, `ontology`, `systems`, `resources`, `topology`, `identity`, `customers`, `offerings`, `roles`, `goals`, `policies`, `statuses`, and `knowledge`. Legacy domain keys such as `sales`, `prospecting`, `projects`, `actions`, and `entities` remain compatibility inputs while projects migrate to System-owned ontology/config/resource contracts.
|
|
35
37
|
|
|
36
38
|
**OrganizationModelSystemEntry** -- System node in `OrganizationModel.systems`. Primary authoring fields include `id`, `label`, `description`, `parentSystemId`, `systems`, `lifecycle`, `ui`, `requiresAdmin`, `devOnly`, `responsibleRoleId`, `governedByKnowledge`, `drivesGoals`, `actions`, `policies`, `ontology`, `config`, and `order`. `subsystems` and `content` are retained compatibility inputs for older projects and should not be used for new recursive Systems, schemas, catalogs, or config.
|
|
37
39
|
|
|
38
|
-
**Provider / ElevasisSystemsProvider** -- runtime that registers System modules, resolves System
|
|
40
|
+
**Provider / ElevasisSystemsProvider** -- runtime that registers System modules, resolves System lifecycle against the org model, projects sidebar navigation, and exposes shell helpers through `useElevasisSystems()`.
|
|
39
41
|
|
|
40
42
|
**Resource** -- governance-only descriptor in `OrganizationModel.resources` for a workflow, agent, integration, or script. Runtime code derives `resourceId` and kind from the descriptor, then attaches executable behavior in operations.
|
|
41
43
|
|
|
@@ -62,13 +64,13 @@ description: Terminology disambiguation for Organization OS concepts used in the
|
|
|
62
64
|
- `OrganizationModel`, `OrganizationModelSystemEntry`
|
|
63
65
|
- `SystemEntry`, `ResourceEntry`
|
|
64
66
|
- `resolveOrganizationModel`, `defineOrganizationModel`, `DEFAULT_ORGANIZATION_MODEL`
|
|
65
|
-
- `
|
|
67
|
+
- `AccessKeys`, `checkAccess`, `createAccessModel`
|
|
66
68
|
- `DeploymentSpec`, `ResourceLink`, `ResourceCategory`
|
|
67
69
|
|
|
68
70
|
**`@elevasis/ui`**
|
|
69
71
|
|
|
70
72
|
- `SystemModule`
|
|
71
|
-
- `
|
|
73
|
+
- `AccessGuard`, `ProtectedRoute`
|
|
72
74
|
- `ElevasisSystemsProvider`, `ElevasisCoreProvider`, `useElevasisSystems`
|
|
73
75
|
|
|
74
76
|
**External project source**
|
|
@@ -2965,6 +2965,10 @@ export type Database = {
|
|
|
2965
2965
|
Returns: boolean
|
|
2966
2966
|
}
|
|
2967
2967
|
current_user_is_platform_admin: { Args: never; Returns: boolean }
|
|
2968
|
+
current_user_shares_org_with: {
|
|
2969
|
+
Args: { other_user_id: string }
|
|
2970
|
+
Returns: boolean
|
|
2971
|
+
}
|
|
2968
2972
|
current_user_supabase_id: { Args: never; Returns: string }
|
|
2969
2973
|
detect_stalled_executions: { Args: never; Returns: undefined }
|
|
2970
2974
|
execute_session_turn: {
|
|
@@ -2985,6 +2989,12 @@ export type Database = {
|
|
|
2985
2989
|
get_platform_credential_kek: { Args: never; Returns: string }
|
|
2986
2990
|
get_storage_org_id: { Args: { file_path: string }; Returns: string }
|
|
2987
2991
|
get_workos_user_id: { Args: never; Returns: string }
|
|
2992
|
+
has_org_access:
|
|
2993
|
+
| {
|
|
2994
|
+
Args: { action?: string; org_id: string; system_path: string }
|
|
2995
|
+
Returns: boolean
|
|
2996
|
+
}
|
|
2997
|
+
| { Args: { action?: string; system_path: string }; Returns: boolean }
|
|
2988
2998
|
has_org_permission: {
|
|
2989
2999
|
Args: { org_id: string; perm_key: string }
|
|
2990
3000
|
Returns: boolean
|