@elevasis/core 0.29.0 → 0.31.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/index.d.ts +1 -2
- package/dist/index.js +4 -105
- package/dist/organization-model/index.d.ts +1 -2
- package/dist/organization-model/index.js +4 -105
- package/dist/test-utils/index.d.ts +20 -0
- package/dist/test-utils/index.js +3 -104
- package/package.json +5 -1
- package/src/__tests__/publish.test.ts +8 -7
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +376 -446
- 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/business/acquisition/api-schemas.test.ts +69 -5
- package/src/business/acquisition/crm-state-actions.test.ts +24 -6
- package/src/business/pdf/sections/__tests__/proposal-document.test.ts +146 -0
- package/src/business/pdf/sections/acceptance.ts +114 -112
- package/src/business/pdf/sections/proposal-document.ts +206 -200
- package/src/execution/engine/index.ts +440 -439
- package/src/execution/engine/tools/integration/types/clickup.ts +57 -0
- package/src/execution/engine/tools/integration/types/index.ts +20 -19
- package/src/execution/engine/tools/tool-maps.ts +16 -0
- package/src/organization-model/__tests__/domains/entities.test.ts +35 -56
- package/src/organization-model/domains/entities.ts +0 -103
- package/src/organization-model/domains/sales.test.ts +35 -28
- package/src/organization-model/domains/sales.ts +0 -85
- package/src/organization-model/published.ts +0 -1
- package/src/organization-model/schema.ts +2 -2
- package/src/reference/_generated/contracts.md +0 -94
- 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 {
|
|
@@ -1,9 +1,73 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { type CrmPriorityRuleConfig, type StatefulPipelineDefinition } from '../../organization-model/domains/sales'
|
|
3
|
+
|
|
4
|
+
// Inline fixture for lead-gen pipeline stage/state validation tests.
|
|
5
|
+
// The canonical constants live in @repo/elevasis-core; @repo/core cannot depend on it.
|
|
6
|
+
const LEAD_GEN_PIPELINE_DEFINITIONS: Record<string, StatefulPipelineDefinition[]> = {
|
|
7
|
+
'acq.list-member': [
|
|
8
|
+
{
|
|
9
|
+
pipelineKey: 'lead-gen',
|
|
10
|
+
label: 'Lead Generation',
|
|
11
|
+
entityKey: 'acq.list-member',
|
|
12
|
+
stages: [
|
|
13
|
+
{
|
|
14
|
+
stageKey: 'outreach',
|
|
15
|
+
label: 'Outreach',
|
|
16
|
+
states: [
|
|
17
|
+
{ stateKey: 'pending', label: 'Pending' },
|
|
18
|
+
{ stateKey: 'personalized', label: 'Personalized' },
|
|
19
|
+
{ stateKey: 'uploaded', label: 'Uploaded' },
|
|
20
|
+
{ stateKey: 'interested', label: 'Interested' }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
stageKey: 'prospecting',
|
|
25
|
+
label: 'Prospecting',
|
|
26
|
+
states: [
|
|
27
|
+
{ stateKey: 'pending', label: 'Pending' },
|
|
28
|
+
{ stateKey: 'discovered', label: 'Discovered' },
|
|
29
|
+
{ stateKey: 'verified', label: 'Verified' }
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{ stageKey: 'qualification', label: 'Qualification', states: [{ stateKey: 'pending', label: 'Pending' }] }
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
'acq.list-company': [
|
|
37
|
+
{
|
|
38
|
+
pipelineKey: 'lead-gen',
|
|
39
|
+
label: 'Lead Generation',
|
|
40
|
+
entityKey: 'acq.list-company',
|
|
41
|
+
stages: [
|
|
42
|
+
{
|
|
43
|
+
stageKey: 'outreach',
|
|
44
|
+
label: 'Outreach',
|
|
45
|
+
states: [
|
|
46
|
+
{ stateKey: 'pending', label: 'Pending' },
|
|
47
|
+
{ stateKey: 'uploaded', label: 'Uploaded' }
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
stageKey: 'prospecting',
|
|
52
|
+
label: 'Prospecting',
|
|
53
|
+
states: [
|
|
54
|
+
{ stateKey: 'pending', label: 'Pending' },
|
|
55
|
+
{ stateKey: 'populated', label: 'Populated' },
|
|
56
|
+
{ stateKey: 'extracted', label: 'Extracted' }
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
stageKey: 'qualification',
|
|
61
|
+
label: 'Qualification',
|
|
62
|
+
states: [
|
|
63
|
+
{ stateKey: 'pending', label: 'Pending' },
|
|
64
|
+
{ stateKey: 'qualified', label: 'Qualified' }
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
7
71
|
import { DEFAULT_ORGANIZATION_MODEL } from '../../organization-model/defaults'
|
|
8
72
|
import type { OrganizationModel } from '../../organization-model/types'
|
|
9
73
|
import { CrmPriorityOverrideSchema, evaluateCrmDealPriority, resolveCrmPriorityRuleConfig } from './crm-priority'
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
LEAD_GEN_PIPELINE_DEFINITIONS,
|
|
4
|
-
type StatefulPipelineDefinition
|
|
5
|
-
} from '../../organization-model/domains/sales'
|
|
2
|
+
import { type StatefulPipelineDefinition } from '../../organization-model/domains/sales'
|
|
6
3
|
import { ActivityEventSchema } from './activity-events'
|
|
7
4
|
import { DealStageSchema, TransitionItemRequestSchema } from './api-schemas'
|
|
8
5
|
import { deriveActions } from './derive-actions'
|
|
@@ -188,8 +185,29 @@ describe('CRM stage and transition vocabulary contracts', () => {
|
|
|
188
185
|
}
|
|
189
186
|
})
|
|
190
187
|
|
|
191
|
-
it('accepts
|
|
192
|
-
|
|
188
|
+
it('accepts caller-supplied lead-gen stage/state pairs in transition requests', () => {
|
|
189
|
+
const leadGenPipelines: Record<string, StatefulPipelineDefinition[]> = {
|
|
190
|
+
'acq.list-member': [
|
|
191
|
+
{
|
|
192
|
+
pipelineKey: 'lead-gen',
|
|
193
|
+
label: 'Lead Gen',
|
|
194
|
+
entityKey: 'leadgen.contact',
|
|
195
|
+
stages: [
|
|
196
|
+
{
|
|
197
|
+
stageKey: 'outreach',
|
|
198
|
+
label: 'Outreach',
|
|
199
|
+
color: 'blue',
|
|
200
|
+
states: [
|
|
201
|
+
{ stateKey: 'personalized', label: 'Personalized' },
|
|
202
|
+
{ stateKey: 'contacted', label: 'Contacted' }
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const pipelineDefinitions of Object.values(leadGenPipelines)) {
|
|
193
211
|
for (const pipeline of pipelineDefinitions) {
|
|
194
212
|
for (const stage of pipeline.stages) {
|
|
195
213
|
for (const state of stage.states) {
|