@peanut-admin/admin 0.1.0-alpha.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/LICENSE +202 -0
- package/admin-core/src/access/access.ts +27 -0
- package/admin-core/src/api/client.ts +200 -0
- package/admin-core/src/api/problem.ts +67 -0
- package/admin-core/src/api/refresh.ts +122 -0
- package/admin-core/src/auth/stores.ts +121 -0
- package/admin-core/src/generated/api.d.ts +22106 -0
- package/admin-core/src/governance/audit.ts +56 -0
- package/admin-core/src/governance/catalog.ts +86 -0
- package/admin-core/src/governance/index.ts +26 -0
- package/admin-core/src/governance/menu.ts +63 -0
- package/admin-core/src/governance/roles.ts +144 -0
- package/admin-core/src/governance/types.ts +64 -0
- package/admin-core/src/index.ts +105 -0
- package/admin-core/src/lifecycle/tenant.ts +59 -0
- package/admin-core/src/module/contribution.ts +124 -0
- package/admin-core/src/runtime/config.ts +55 -0
- package/admin-core/src/runtime/errors.ts +81 -0
- package/admin-core/src/runtime/guard.ts +40 -0
- package/admin-core/src/runtime/navigation.ts +105 -0
- package/admin-core/src/runtime/overrides.ts +214 -0
- package/admin-core/src/targets/store.ts +153 -0
- package/admin-shell/src/config.ts +84 -0
- package/admin-shell/src/index.ts +40 -0
- package/admin-shell/src/layout.ts +332 -0
- package/admin-shell/src/overrides.ts +53 -0
- package/admin-shell/src/states.ts +93 -0
- package/admin-shell/src/targets.ts +128 -0
- package/admin-shell/src/theme.ts +15 -0
- package/client-core/src/index.ts +325 -0
- package/client-nuxt/src/index.ts +40 -0
- package/client-uniapp/src/index.ts +50 -0
- package/file-media/src/FileAssetSelector.vue +117 -0
- package/file-media/src/FileMediaPage.vue +158 -0
- package/file-media/src/contracts.ts +220 -0
- package/file-media/src/index.ts +19 -0
- package/file-media/src/runtime.ts +210 -0
- package/import-export/src/ImportExportPage.vue +155 -0
- package/import-export/src/contracts.ts +96 -0
- package/import-export/src/index.ts +3 -0
- package/import-export/src/runtime.ts +128 -0
- package/integration-security/src/IntegrationSecurityPage.vue +402 -0
- package/integration-security/src/contracts.ts +171 -0
- package/integration-security/src/index.ts +3 -0
- package/integration-security/src/runtime.ts +180 -0
- package/notification-sms/src/NotificationInboxPage.vue +266 -0
- package/notification-sms/src/contracts.ts +195 -0
- package/notification-sms/src/index.ts +4 -0
- package/notification-sms/src/runtime.ts +143 -0
- package/ops-console/src/OpsConsolePage.vue +337 -0
- package/ops-console/src/contracts.ts +169 -0
- package/ops-console/src/index.ts +3 -0
- package/ops-console/src/runtime.ts +199 -0
- package/package.json +108 -0
- package/reference-codes/src/ReferenceCodesPage.vue +942 -0
- package/reference-codes/src/contracts.ts +484 -0
- package/reference-codes/src/index.ts +53 -0
- package/reference-codes/src/runtime.ts +855 -0
- package/settings/src/SettingsPage.vue +536 -0
- package/settings/src/contracts.ts +331 -0
- package/settings/src/index.ts +45 -0
- package/settings/src/runtime.ts +545 -0
- package/task-job/src/TaskJobPage.vue +120 -0
- package/task-job/src/contracts.ts +117 -0
- package/task-job/src/index.ts +2 -0
- package/task-job/src/runtime.ts +105 -0
- package/testing/src/index.ts +141 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { GovernanceAudience } from './types'
|
|
2
|
+
|
|
3
|
+
export type GovernanceAuditOutcome = 'success' | 'denied' | 'error'
|
|
4
|
+
|
|
5
|
+
export interface GovernanceAuditFilter {
|
|
6
|
+
eventType?: string
|
|
7
|
+
action?: string
|
|
8
|
+
outcome?: GovernanceAuditOutcome
|
|
9
|
+
requestId?: string
|
|
10
|
+
targetType?: string
|
|
11
|
+
targetId?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GovernanceAuditDetailInput {
|
|
15
|
+
id: string
|
|
16
|
+
audience: GovernanceAudience
|
|
17
|
+
eventType: string
|
|
18
|
+
action: string
|
|
19
|
+
outcome: GovernanceAuditOutcome
|
|
20
|
+
requestId: string
|
|
21
|
+
occurredAt: string
|
|
22
|
+
metadata: Readonly<Record<string, unknown>>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const safeFilter = (value: string): boolean => value !== ''
|
|
26
|
+
&& value.length <= 160
|
|
27
|
+
&& !/[\u0000-\u001f\u007f]/.test(value)
|
|
28
|
+
|
|
29
|
+
export const normalizeAuditFilter = (input: GovernanceAuditFilter): GovernanceAuditFilter => {
|
|
30
|
+
for (const value of [input.eventType, input.action, input.requestId, input.targetType, input.targetId]) {
|
|
31
|
+
if (value !== undefined && !safeFilter(value)) throw new Error('AUDIT_FILTER_INVALID')
|
|
32
|
+
}
|
|
33
|
+
if ((input.targetType === undefined) !== (input.targetId === undefined)) {
|
|
34
|
+
throw new Error('AUDIT_TARGET_FILTER_INCOMPLETE')
|
|
35
|
+
}
|
|
36
|
+
return { ...input }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const projectAuditDetail = (
|
|
40
|
+
input: GovernanceAuditDetailInput,
|
|
41
|
+
metadataAllowlist: readonly string[],
|
|
42
|
+
) => {
|
|
43
|
+
const metadata: Record<string, boolean | number | string | null> = {}
|
|
44
|
+
if (metadataAllowlist.some(key => (
|
|
45
|
+
!/^[a-z][a-z0-9_]{0,63}$/.test(key)
|
|
46
|
+
|| /token|secret|cookie|password|sql|target_set/i.test(key)
|
|
47
|
+
))) throw new Error('AUDIT_METADATA_ALLOWLIST_INVALID')
|
|
48
|
+
const allowed = new Set(metadataAllowlist)
|
|
49
|
+
for (const key of [...allowed].sort()) {
|
|
50
|
+
const value = input.metadata[key]
|
|
51
|
+
if (value === null || ['boolean', 'number', 'string'].includes(typeof value)) {
|
|
52
|
+
metadata[key] = value as boolean | number | string | null
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { ...input, metadata }
|
|
56
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GovernanceAudience,
|
|
3
|
+
GovernanceCatalog,
|
|
4
|
+
GovernanceCatalogInput,
|
|
5
|
+
GovernancePermissionDefinition,
|
|
6
|
+
} from './types'
|
|
7
|
+
|
|
8
|
+
const permissionPattern = /^[a-z][a-z0-9]*(?:[.-][a-z][a-z0-9-]*)+$/
|
|
9
|
+
const modulePattern = /^[a-z][a-z0-9]*(?:[.-][a-z][a-z0-9-]*)*$/
|
|
10
|
+
|
|
11
|
+
const fail = (code: string): never => { throw new Error(code) }
|
|
12
|
+
|
|
13
|
+
const canonicalRoutePath = (audience: GovernanceAudience, path: string): string => {
|
|
14
|
+
if (audience !== 'tenant' && audience !== 'platform') fail('GOVERNANCE_ROUTE_INVALID')
|
|
15
|
+
const prefix = audience === 'tenant' ? '/app' : '/platform'
|
|
16
|
+
if (path === ''
|
|
17
|
+
|| !path.startsWith('/')
|
|
18
|
+
|| path.endsWith('/')
|
|
19
|
+
|| path.includes('//')
|
|
20
|
+
|| /[\\%?#\u0000-\u0020\u007f]/.test(path)
|
|
21
|
+
|| path.split('/').some(segment => segment === '.' || segment === '..')
|
|
22
|
+
|| (path !== prefix && !path.startsWith(`${prefix}/`))) fail('GOVERNANCE_ROUTE_INVALID')
|
|
23
|
+
return path
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const createGovernanceCatalog = (input: GovernanceCatalogInput): GovernanceCatalog => {
|
|
27
|
+
const permissions = new Map<string, GovernancePermissionDefinition>()
|
|
28
|
+
for (const permission of input.permissions) {
|
|
29
|
+
if (!['tenant', 'platform'].includes(permission.audience)
|
|
30
|
+
|| !permissionPattern.test(permission.key)
|
|
31
|
+
|| !modulePattern.test(permission.moduleKey)
|
|
32
|
+
|| (permission.audience === 'platform') !== permission.key.startsWith('platform.')
|
|
33
|
+
|| permissions.has(permission.key)) fail('GOVERNANCE_PERMISSION_INVALID')
|
|
34
|
+
permissions.set(permission.key, { ...permission })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const routes = new Map<string, GovernanceCatalogInput['routes'][number]>()
|
|
38
|
+
const paths = new Set<string>()
|
|
39
|
+
for (const route of input.routes) {
|
|
40
|
+
const path = canonicalRoutePath(route.audience, route.path)
|
|
41
|
+
if (route.name === ''
|
|
42
|
+
|| route.componentKey === ''
|
|
43
|
+
|| route.permissionKeys.length === 0
|
|
44
|
+
|| route.clientKeys.length === 0
|
|
45
|
+
|| new Set(route.permissionKeys).size !== route.permissionKeys.length
|
|
46
|
+
|| new Set(route.clientKeys).size !== route.clientKeys.length
|
|
47
|
+
|| routes.has(route.name)
|
|
48
|
+
|| paths.has(path)) fail('GOVERNANCE_ROUTE_INVALID')
|
|
49
|
+
const expectedModule = route.moduleKey ?? (route.audience === 'platform' ? 'platform' : 'core')
|
|
50
|
+
for (const permissionKey of route.permissionKeys) {
|
|
51
|
+
const permission = permissions.get(permissionKey) ?? fail('GOVERNANCE_PERMISSION_UNDECLARED')
|
|
52
|
+
if (permission.audience !== route.audience) fail('GOVERNANCE_PERMISSION_AUDIENCE_MISMATCH')
|
|
53
|
+
if (!permission.active) fail('GOVERNANCE_PERMISSION_INACTIVE')
|
|
54
|
+
if (permission.moduleKey !== expectedModule) fail('GOVERNANCE_PERMISSION_MODULE_MISMATCH')
|
|
55
|
+
}
|
|
56
|
+
routes.set(route.name, {
|
|
57
|
+
...route,
|
|
58
|
+
path,
|
|
59
|
+
permissionKeys: [...route.permissionKeys],
|
|
60
|
+
clientKeys: [...route.clientKeys],
|
|
61
|
+
})
|
|
62
|
+
paths.add(path)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const icons = new Map<string, GovernanceCatalogInput['icons'][string]>()
|
|
66
|
+
for (const [key, icon] of Object.entries(input.icons)) {
|
|
67
|
+
if (!/^[A-Z][A-Za-z0-9]{0,63}$/.test(key)
|
|
68
|
+
|| icon.label.trim() === ''
|
|
69
|
+
|| icon.glyph.trim() === ''
|
|
70
|
+
|| icons.has(key)) fail('GOVERNANCE_ICON_INVALID')
|
|
71
|
+
icons.set(key, { ...icon })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { permissions, routes, icons }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const requireGovernancePermission = (
|
|
78
|
+
catalog: GovernanceCatalog,
|
|
79
|
+
key: string,
|
|
80
|
+
audience: GovernanceAudience,
|
|
81
|
+
): GovernancePermissionDefinition => {
|
|
82
|
+
const permission = catalog.permissions.get(key) ?? fail('GOVERNANCE_PERMISSION_UNDECLARED')
|
|
83
|
+
if (permission.audience !== audience) fail('GOVERNANCE_PERMISSION_AUDIENCE_MISMATCH')
|
|
84
|
+
if (!permission.active) fail('GOVERNANCE_PERMISSION_INACTIVE')
|
|
85
|
+
return permission
|
|
86
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export { createGovernanceCatalog, requireGovernancePermission } from './catalog'
|
|
2
|
+
export { normalizeAuditFilter, projectAuditDetail } from './audit'
|
|
3
|
+
export { explainMenuVisibility } from './menu'
|
|
4
|
+
export { createDataPolicyDraft, createRolePermissionDraft, requireRevision } from './roles'
|
|
5
|
+
export type {
|
|
6
|
+
GovernanceAuditDetailInput,
|
|
7
|
+
GovernanceAuditFilter,
|
|
8
|
+
GovernanceAuditOutcome,
|
|
9
|
+
} from './audit'
|
|
10
|
+
export type {
|
|
11
|
+
CreateDataPolicyDraftInput,
|
|
12
|
+
DataPolicyDraftInput,
|
|
13
|
+
RolePermissionDraftInput,
|
|
14
|
+
UpdateDataPolicyDraftInput,
|
|
15
|
+
} from './roles'
|
|
16
|
+
export type {
|
|
17
|
+
GovernanceAudience,
|
|
18
|
+
GovernanceCatalog,
|
|
19
|
+
GovernanceCatalogInput,
|
|
20
|
+
GovernanceIconDefinition,
|
|
21
|
+
GovernanceMenuExplanation,
|
|
22
|
+
GovernanceMenuInput,
|
|
23
|
+
GovernancePermissionDefinition,
|
|
24
|
+
GovernanceRouteDefinition,
|
|
25
|
+
GovernanceVisibilityContext,
|
|
26
|
+
} from './types'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { requireGovernancePermission } from './catalog'
|
|
2
|
+
import type {
|
|
3
|
+
GovernanceCatalog,
|
|
4
|
+
GovernanceMenuExplanation,
|
|
5
|
+
GovernanceMenuInput,
|
|
6
|
+
GovernanceVisibilityContext,
|
|
7
|
+
} from './types'
|
|
8
|
+
|
|
9
|
+
const fail = (code: string): never => { throw new Error(code) }
|
|
10
|
+
|
|
11
|
+
const sameStrings = (left: readonly string[], right: readonly string[]): boolean => (
|
|
12
|
+
[...new Set(left)].sort().join('\u0000') === [...new Set(right)].sort().join('\u0000')
|
|
13
|
+
&& new Set(left).size === left.length
|
|
14
|
+
&& new Set(right).size === right.length
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
export const explainMenuVisibility = (
|
|
18
|
+
menu: GovernanceMenuInput,
|
|
19
|
+
context: GovernanceVisibilityContext,
|
|
20
|
+
catalog: GovernanceCatalog,
|
|
21
|
+
): GovernanceMenuExplanation => {
|
|
22
|
+
const route = menu.routeName === null ? null : catalog.routes.get(menu.routeName)
|
|
23
|
+
if (menu.type === 'page' && route === undefined) fail('GOVERNANCE_ROUTE_UNDECLARED')
|
|
24
|
+
if (route !== null && route !== undefined) {
|
|
25
|
+
if (menu.audience !== context.audience || route.audience !== menu.audience) fail('GOVERNANCE_ROUTE_AUDIENCE_MISMATCH')
|
|
26
|
+
const expectedModule = menu.audience === 'platform' && menu.moduleKey === 'core' ? 'platform' : menu.moduleKey
|
|
27
|
+
const routeModule = route.moduleKey ?? (route.audience === 'platform' ? 'platform' : 'core')
|
|
28
|
+
if (routeModule !== expectedModule) fail('GOVERNANCE_ROUTE_MODULE_MISMATCH')
|
|
29
|
+
if (route.path !== menu.routePath
|
|
30
|
+
|| route.componentKey !== menu.componentKey
|
|
31
|
+
|| !sameStrings(route.clientKeys, menu.clientKeys)) fail('GOVERNANCE_ROUTE_CONTRACT_MISMATCH')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const permission = menu.requiredPermission === null
|
|
35
|
+
? null
|
|
36
|
+
: requireGovernancePermission(catalog, menu.requiredPermission, context.audience)
|
|
37
|
+
const expectedPermissionModule = menu.audience === 'platform' && menu.moduleKey === 'core' ? 'platform' : menu.moduleKey
|
|
38
|
+
if (permission !== null && permission.moduleKey !== expectedPermissionModule) fail('GOVERNANCE_PERMISSION_MODULE_MISMATCH')
|
|
39
|
+
if (route !== null && route !== undefined && permission !== null
|
|
40
|
+
&& !route.permissionKeys.includes(permission.key)) fail('GOVERNANCE_ROUTE_PERMISSION_MISMATCH')
|
|
41
|
+
|
|
42
|
+
const icon = menu.icon === null ? null : catalog.icons.get(menu.icon)
|
|
43
|
+
if (menu.icon !== null && icon === undefined) fail('GOVERNANCE_ICON_UNDECLARED')
|
|
44
|
+
|
|
45
|
+
let reason = 'visible'
|
|
46
|
+
if (!menu.clientKeys.includes(context.clientKey)) {
|
|
47
|
+
reason = 'client_unavailable'
|
|
48
|
+
} else if (menu.moduleKey !== 'core' && !context.deploymentModules.has(menu.moduleKey)) {
|
|
49
|
+
reason = 'deployment_module_unavailable'
|
|
50
|
+
} else if (menu.moduleKey !== 'core' && context.audience === 'tenant' && !context.tenantModules.has(menu.moduleKey)) {
|
|
51
|
+
reason = 'tenant_module_disabled'
|
|
52
|
+
} else if (permission !== null && !context.permissions.has(permission.key)) {
|
|
53
|
+
reason = 'permission_not_granted'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
key: menu.key,
|
|
58
|
+
visible: reason === 'visible',
|
|
59
|
+
reason,
|
|
60
|
+
trustedPath: route?.path ?? null,
|
|
61
|
+
icon: icon ?? null,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { components } from '../generated/api'
|
|
2
|
+
import { requireGovernancePermission } from './catalog'
|
|
3
|
+
import type { GovernanceAudience, GovernanceCatalog } from './types'
|
|
4
|
+
|
|
5
|
+
const fail = (code: string): never => { throw new Error(code) }
|
|
6
|
+
|
|
7
|
+
const canonicalId = (value: string): string => {
|
|
8
|
+
if (!/^[1-9][0-9]*$/.test(value)) fail('GOVERNANCE_ROLE_INVALID')
|
|
9
|
+
return value
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const requireRevision = (ifMatch: string, currentRevision: number): number => {
|
|
13
|
+
if (ifMatch === '') fail('PRECONDITION_REQUIRED')
|
|
14
|
+
const match = /^"rev-([1-9][0-9]*)"$/.exec(ifMatch) ?? fail('PRECONDITION_INVALID')
|
|
15
|
+
const revision = Number(match[1])
|
|
16
|
+
if (!Number.isSafeInteger(revision) || revision !== currentRevision) fail('REVISION_MISMATCH')
|
|
17
|
+
return revision
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RolePermissionDraftInput {
|
|
21
|
+
audience: GovernanceAudience
|
|
22
|
+
roleId: string
|
|
23
|
+
currentRevision: number
|
|
24
|
+
ifMatch: string
|
|
25
|
+
permissionKeys: readonly string[]
|
|
26
|
+
availableModules: ReadonlySet<string>
|
|
27
|
+
catalog: GovernanceCatalog
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const createRolePermissionDraft = (input: RolePermissionDraftInput) => {
|
|
31
|
+
const keys = [...new Set(input.permissionKeys)].sort()
|
|
32
|
+
for (const key of keys) {
|
|
33
|
+
const permission = requireGovernancePermission(input.catalog, key, input.audience)
|
|
34
|
+
if (!['core', 'platform'].includes(permission.moduleKey)
|
|
35
|
+
&& !input.availableModules.has(permission.moduleKey)) fail('GOVERNANCE_PERMISSION_MODULE_UNAVAILABLE')
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
kind: 'validated-draft' as const,
|
|
39
|
+
audience: input.audience,
|
|
40
|
+
roleId: canonicalId(input.roleId),
|
|
41
|
+
expectedRevision: requireRevision(input.ifMatch, input.currentRevision),
|
|
42
|
+
payload: { permission_keys: keys } satisfies components['schemas']['ReplaceRolePermissionsRequest'],
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type ReplaceDataPolicyRequest = components['schemas']['ReplaceDataPolicyRequest']
|
|
47
|
+
type DataPolicyGroupWrite = components['schemas']['DataPolicyGroupWrite']
|
|
48
|
+
type DataPolicyConditionWrite = components['schemas']['DataPolicyConditionWrite']
|
|
49
|
+
type DataPolicyTargetSetWrite = components['schemas']['DataPolicyTargetSetWrite']
|
|
50
|
+
|
|
51
|
+
interface DataPolicyDraftBase {
|
|
52
|
+
audience: GovernanceAudience
|
|
53
|
+
roleId: string
|
|
54
|
+
resourceKey: string
|
|
55
|
+
operation: string
|
|
56
|
+
payload: ReplaceDataPolicyRequest
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CreateDataPolicyDraftInput extends DataPolicyDraftBase {
|
|
60
|
+
mode: 'create'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface UpdateDataPolicyDraftInput extends DataPolicyDraftBase {
|
|
64
|
+
mode: 'update'
|
|
65
|
+
currentRevision: number
|
|
66
|
+
ifMatch: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type DataPolicyDraftInput = CreateDataPolicyDraftInput | UpdateDataPolicyDraftInput
|
|
70
|
+
|
|
71
|
+
const text = (value: string, limit: number, code: string): string => {
|
|
72
|
+
const canonical = value.trim()
|
|
73
|
+
if (canonical === '' || canonical.length > limit || /[\u0000-\u001f\u007f]/.test(canonical)) fail(code)
|
|
74
|
+
return canonical
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const targetSetDraft = (input: DataPolicyTargetSetWrite): DataPolicyTargetSetWrite => {
|
|
78
|
+
const targets = input.targets.map(target => ({ target_id: text(target.target_id, 128, 'DATA_POLICY_TARGETS_INVALID') }))
|
|
79
|
+
if (targets.length === 0 || targets.length > 500
|
|
80
|
+
|| new Set(targets.map(target => target.target_id)).size !== targets.length) fail('DATA_POLICY_TARGETS_INVALID')
|
|
81
|
+
return {
|
|
82
|
+
name: text(input.name, 120, 'DATA_POLICY_TARGET_SET_INVALID'),
|
|
83
|
+
target_resource_key: text(input.target_resource_key, 160, 'DATA_POLICY_TARGET_SET_INVALID'),
|
|
84
|
+
targets,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const conditionDraft = (input: DataPolicyConditionWrite): DataPolicyConditionWrite => ({
|
|
89
|
+
condition_key: text(input.condition_key, 160, 'DATA_POLICY_CONDITION_INVALID'),
|
|
90
|
+
...(input.target_set === undefined
|
|
91
|
+
? {}
|
|
92
|
+
: { target_set: input.target_set === null ? null : targetSetDraft(input.target_set) }),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const groupDraft = (input: DataPolicyGroupWrite): DataPolicyGroupWrite => {
|
|
96
|
+
if (input.conditions.length === 0 || input.conditions.length > 20) fail('DATA_POLICY_CONDITIONS_INVALID')
|
|
97
|
+
return {
|
|
98
|
+
name: text(input.name, 120, 'DATA_POLICY_GROUP_INVALID'),
|
|
99
|
+
conditions: input.conditions.map(conditionDraft),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const optionalDate = (value: string | null | undefined): string | null => {
|
|
104
|
+
if (value === undefined || value === null) return null
|
|
105
|
+
if (!Number.isFinite(Date.parse(value))) fail('DATA_POLICY_PERIOD_INVALID')
|
|
106
|
+
return value
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const createDataPolicyDraft = (input: DataPolicyDraftInput) => {
|
|
110
|
+
if (input.audience !== 'tenant') fail('GOVERNANCE_DATA_POLICY_AUDIENCE_MISMATCH')
|
|
111
|
+
const resourceKey = text(input.resourceKey, 160, 'GOVERNANCE_OPERATION_INVALID')
|
|
112
|
+
const operation = text(input.operation, 160, 'GOVERNANCE_OPERATION_INVALID')
|
|
113
|
+
const reason = input.payload.reason === undefined || input.payload.reason === null
|
|
114
|
+
? null
|
|
115
|
+
: text(input.payload.reason, 300, 'DATA_POLICY_REASON_INVALID')
|
|
116
|
+
const validFrom = optionalDate(input.payload.valid_from)
|
|
117
|
+
const validUntil = optionalDate(input.payload.valid_until)
|
|
118
|
+
if (validFrom !== null && validUntil !== null && Date.parse(validUntil) <= Date.parse(validFrom)) {
|
|
119
|
+
fail('DATA_POLICY_PERIOD_INVALID')
|
|
120
|
+
}
|
|
121
|
+
if (input.payload.groups.length > 50
|
|
122
|
+
|| (input.payload.status === 'active' && input.payload.groups.length === 0)) fail('DATA_POLICY_GROUPS_INVALID')
|
|
123
|
+
const groups = input.payload.groups.map(groupDraft)
|
|
124
|
+
if (new Set(groups.map(group => group.name)).size !== groups.length) fail('DATA_POLICY_GROUP_INVALID')
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
kind: 'validated-draft' as const,
|
|
128
|
+
mode: input.mode,
|
|
129
|
+
audience: 'tenant' as const,
|
|
130
|
+
roleId: canonicalId(input.roleId),
|
|
131
|
+
expectedRevision: input.mode === 'create'
|
|
132
|
+
? null
|
|
133
|
+
: requireRevision(input.ifMatch, input.currentRevision),
|
|
134
|
+
resourceKey,
|
|
135
|
+
operation,
|
|
136
|
+
payload: {
|
|
137
|
+
status: input.payload.status,
|
|
138
|
+
reason,
|
|
139
|
+
valid_from: validFrom,
|
|
140
|
+
valid_until: validUntil,
|
|
141
|
+
groups,
|
|
142
|
+
} satisfies ReplaceDataPolicyRequest,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AdminNavigationRoute } from '../runtime/navigation'
|
|
2
|
+
|
|
3
|
+
export type GovernanceAudience = 'tenant' | 'platform'
|
|
4
|
+
|
|
5
|
+
export interface GovernancePermissionDefinition {
|
|
6
|
+
key: string
|
|
7
|
+
moduleKey: string
|
|
8
|
+
audience: GovernanceAudience
|
|
9
|
+
active: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GovernanceRouteDefinition extends AdminNavigationRoute {
|
|
13
|
+
audience: GovernanceAudience
|
|
14
|
+
moduleKey?: string
|
|
15
|
+
permissionKeys: readonly string[]
|
|
16
|
+
componentKey: string
|
|
17
|
+
clientKeys: readonly string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GovernanceIconDefinition {
|
|
21
|
+
label: string
|
|
22
|
+
glyph: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface GovernanceCatalog {
|
|
26
|
+
permissions: ReadonlyMap<string, GovernancePermissionDefinition>
|
|
27
|
+
routes: ReadonlyMap<string, GovernanceRouteDefinition>
|
|
28
|
+
icons: ReadonlyMap<string, GovernanceIconDefinition>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface GovernanceCatalogInput {
|
|
32
|
+
permissions: readonly GovernancePermissionDefinition[]
|
|
33
|
+
routes: readonly GovernanceRouteDefinition[]
|
|
34
|
+
icons: Readonly<Record<string, GovernanceIconDefinition>>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface GovernanceMenuInput {
|
|
38
|
+
key: string
|
|
39
|
+
type: 'group' | 'page' | 'link'
|
|
40
|
+
audience: GovernanceAudience
|
|
41
|
+
routeName: string | null
|
|
42
|
+
routePath: string | null
|
|
43
|
+
componentKey: string | null
|
|
44
|
+
requiredPermission: string | null
|
|
45
|
+
moduleKey: string
|
|
46
|
+
clientKeys: readonly string[]
|
|
47
|
+
icon: string | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface GovernanceVisibilityContext {
|
|
51
|
+
audience: GovernanceAudience
|
|
52
|
+
clientKey: string
|
|
53
|
+
deploymentModules: ReadonlySet<string>
|
|
54
|
+
tenantModules: ReadonlySet<string>
|
|
55
|
+
permissions: ReadonlySet<string>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface GovernanceMenuExplanation {
|
|
59
|
+
key: string
|
|
60
|
+
visible: boolean
|
|
61
|
+
reason: string
|
|
62
|
+
trustedPath: string | null
|
|
63
|
+
icon: GovernanceIconDefinition | null
|
|
64
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export const ADMIN_CORE_PACKAGE = '@peanut-admin/admin/core' as const
|
|
2
|
+
export const ADMIN_CORE_VERSION = '0.1.0' as const
|
|
3
|
+
|
|
4
|
+
export { createPlatformApiClient, createProtectedFetch, createTenantApiClient } from './api/client'
|
|
5
|
+
export type {
|
|
6
|
+
ApiAudience,
|
|
7
|
+
AudienceApiClient,
|
|
8
|
+
AudienceApiClientOptions,
|
|
9
|
+
ProtectedFetchOptions,
|
|
10
|
+
} from './api/client'
|
|
11
|
+
export { createBrowserRefreshCoordinator, createMemoryRefreshCoordinator } from './api/refresh'
|
|
12
|
+
export type { RefreshAttempt, RefreshCoordinator } from './api/refresh'
|
|
13
|
+
export { isProblemCode, parseProblemDetails } from './api/problem'
|
|
14
|
+
export type { ProblemDetails, ProblemFieldError } from './api/problem'
|
|
15
|
+
export { hasAllPermissions, hasPermission, useAccess } from './access/access'
|
|
16
|
+
export type { AccessHints } from './access/access'
|
|
17
|
+
export {
|
|
18
|
+
usePlatformAuth,
|
|
19
|
+
usePlatformContext,
|
|
20
|
+
useTenantAuth,
|
|
21
|
+
useTenantContext,
|
|
22
|
+
} from './auth/stores'
|
|
23
|
+
export type { PlatformContextData, TenantContextData } from './auth/stores'
|
|
24
|
+
export { disposeTenantState, registerTenantDisposer } from './lifecycle/tenant'
|
|
25
|
+
export { createTenantLifecycle } from './lifecycle/tenant'
|
|
26
|
+
export type { TenantDisposer, TenantLifecycle, TenantLifecycleTicket } from './lifecycle/tenant'
|
|
27
|
+
export { createMenuRouteRegistry, defineAdminModule } from './module/contribution'
|
|
28
|
+
export type {
|
|
29
|
+
AdminModuleContribution,
|
|
30
|
+
AdminModuleLocaleContribution,
|
|
31
|
+
AdminModuleRoute,
|
|
32
|
+
AdminModuleShellSlotContribution,
|
|
33
|
+
AdminModuleShellSlotName,
|
|
34
|
+
AdminModuleStoreContribution,
|
|
35
|
+
AdminRouteAccess,
|
|
36
|
+
MenuRouteRegistry,
|
|
37
|
+
} from './module/contribution'
|
|
38
|
+
export { defineAdminHostConfig } from './runtime/config'
|
|
39
|
+
export type { AdminAudienceHostConfig, AdminHostConfig } from './runtime/config'
|
|
40
|
+
export { mapAdminRuntimeError } from './runtime/errors'
|
|
41
|
+
export type { AdminRuntimeErrorKind, AdminRuntimeErrorState } from './runtime/errors'
|
|
42
|
+
export { runAdminRouteGuard } from './runtime/guard'
|
|
43
|
+
export type {
|
|
44
|
+
AdminRouteGuardDependencies,
|
|
45
|
+
AdminRouteGuardInput,
|
|
46
|
+
AdminRouteGuardResult,
|
|
47
|
+
} from './runtime/guard'
|
|
48
|
+
export { createAdminNavigationRegistry } from './runtime/navigation'
|
|
49
|
+
export type {
|
|
50
|
+
AdminNavigationMenuInput,
|
|
51
|
+
AdminNavigationRegistry,
|
|
52
|
+
AdminNavigationRegistryInput,
|
|
53
|
+
AdminNavigationRoute,
|
|
54
|
+
} from './runtime/navigation'
|
|
55
|
+
export {
|
|
56
|
+
createAdminOverrideRegistry,
|
|
57
|
+
defineAdminOverrideSlot,
|
|
58
|
+
} from './runtime/overrides'
|
|
59
|
+
export type {
|
|
60
|
+
AdminOverride,
|
|
61
|
+
AdminOverrideKind,
|
|
62
|
+
AdminOverrideRegistry,
|
|
63
|
+
AdminOverrideRegistryInput,
|
|
64
|
+
AdminOverrideResolution,
|
|
65
|
+
AdminOverrideResolutionMetadata,
|
|
66
|
+
AdminOverrideSlot,
|
|
67
|
+
AdminOverrideSource,
|
|
68
|
+
} from './runtime/overrides'
|
|
69
|
+
export { useOperationTargets } from './targets/store'
|
|
70
|
+
export type {
|
|
71
|
+
OperationTargetScope,
|
|
72
|
+
TargetCardinality,
|
|
73
|
+
TargetCandidate,
|
|
74
|
+
TypedTarget,
|
|
75
|
+
TypedTargetSet,
|
|
76
|
+
} from './targets/store'
|
|
77
|
+
export type { components, operations, paths } from './generated/api'
|
|
78
|
+
export {
|
|
79
|
+
createDataPolicyDraft,
|
|
80
|
+
createGovernanceCatalog,
|
|
81
|
+
createRolePermissionDraft,
|
|
82
|
+
explainMenuVisibility,
|
|
83
|
+
normalizeAuditFilter,
|
|
84
|
+
projectAuditDetail,
|
|
85
|
+
requireGovernancePermission,
|
|
86
|
+
requireRevision,
|
|
87
|
+
} from './governance/index'
|
|
88
|
+
export type {
|
|
89
|
+
GovernanceAuditDetailInput,
|
|
90
|
+
GovernanceAuditFilter,
|
|
91
|
+
GovernanceAuditOutcome,
|
|
92
|
+
GovernanceAudience,
|
|
93
|
+
GovernanceCatalog,
|
|
94
|
+
GovernanceCatalogInput,
|
|
95
|
+
GovernanceIconDefinition,
|
|
96
|
+
GovernanceMenuExplanation,
|
|
97
|
+
GovernanceMenuInput,
|
|
98
|
+
GovernancePermissionDefinition,
|
|
99
|
+
GovernanceRouteDefinition,
|
|
100
|
+
GovernanceVisibilityContext,
|
|
101
|
+
CreateDataPolicyDraftInput,
|
|
102
|
+
DataPolicyDraftInput,
|
|
103
|
+
RolePermissionDraftInput,
|
|
104
|
+
UpdateDataPolicyDraftInput,
|
|
105
|
+
} from './governance/index'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type TenantDisposer = () => void | Promise<void>
|
|
2
|
+
|
|
3
|
+
export interface TenantLifecycleTicket {
|
|
4
|
+
generation: number
|
|
5
|
+
signal: AbortSignal
|
|
6
|
+
isCurrent: () => boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TenantLifecycle {
|
|
10
|
+
current: () => number
|
|
11
|
+
capture: () => TenantLifecycleTicket
|
|
12
|
+
invalidate: () => number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const tenantDisposers = new Map<string, TenantDisposer>()
|
|
16
|
+
|
|
17
|
+
export const registerTenantDisposer = (key: string, disposer: TenantDisposer): (() => void) => {
|
|
18
|
+
if (key === '' || tenantDisposers.has(key)) {
|
|
19
|
+
throw new Error(`TENANT_DISPOSER_DUPLICATE: ${key}`)
|
|
20
|
+
}
|
|
21
|
+
tenantDisposers.set(key, disposer)
|
|
22
|
+
|
|
23
|
+
return () => {
|
|
24
|
+
if (tenantDisposers.get(key) === disposer) {
|
|
25
|
+
tenantDisposers.delete(key)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const disposeTenantState = async (): Promise<void> => {
|
|
31
|
+
const disposers = [...tenantDisposers.values()]
|
|
32
|
+
const results = await Promise.allSettled(disposers.map(async disposer => disposer()))
|
|
33
|
+
const failure = results.find((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
34
|
+
if (failure !== undefined) throw failure.reason
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const createTenantLifecycle = (): TenantLifecycle => {
|
|
38
|
+
let generation = 0
|
|
39
|
+
let controller = new AbortController()
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
current: () => generation,
|
|
43
|
+
capture: () => {
|
|
44
|
+
const capturedGeneration = generation
|
|
45
|
+
const signal = controller.signal
|
|
46
|
+
return {
|
|
47
|
+
generation: capturedGeneration,
|
|
48
|
+
signal,
|
|
49
|
+
isCurrent: () => capturedGeneration === generation && !signal.aborted,
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
invalidate: () => {
|
|
53
|
+
controller.abort()
|
|
54
|
+
controller = new AbortController()
|
|
55
|
+
generation += 1
|
|
56
|
+
return generation
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
}
|