@peanut-admin/admin 0.1.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +202 -0
  2. package/admin-core/src/access/access.ts +27 -0
  3. package/admin-core/src/access/permission-policy.ts +37 -0
  4. package/admin-core/src/api/client.ts +200 -0
  5. package/admin-core/src/api/problem.ts +67 -0
  6. package/admin-core/src/api/refresh.ts +122 -0
  7. package/admin-core/src/auth/stores.ts +121 -0
  8. package/admin-core/src/auth/tenant-session.ts +32 -0
  9. package/admin-core/src/generated/api.d.ts +22106 -0
  10. package/admin-core/src/governance/audit.ts +56 -0
  11. package/admin-core/src/governance/catalog.ts +86 -0
  12. package/admin-core/src/governance/index.ts +26 -0
  13. package/admin-core/src/governance/menu.ts +63 -0
  14. package/admin-core/src/governance/roles.ts +144 -0
  15. package/admin-core/src/governance/types.ts +64 -0
  16. package/admin-core/src/index.ts +122 -0
  17. package/admin-core/src/lifecycle/tenant.ts +59 -0
  18. package/admin-core/src/module/contribution.ts +124 -0
  19. package/admin-core/src/module/plugin-contribution-policy.ts +51 -0
  20. package/admin-core/src/module/tenant-modules.ts +20 -0
  21. package/admin-core/src/runtime/config.ts +55 -0
  22. package/admin-core/src/runtime/errors.ts +81 -0
  23. package/admin-core/src/runtime/guard.ts +40 -0
  24. package/admin-core/src/runtime/navigation.ts +105 -0
  25. package/admin-core/src/runtime/overrides.ts +214 -0
  26. package/admin-core/src/targets/store.ts +153 -0
  27. package/admin-shell/src/config.ts +84 -0
  28. package/admin-shell/src/deployment-mode.ts +36 -0
  29. package/admin-shell/src/index.ts +44 -0
  30. package/admin-shell/src/layout.ts +332 -0
  31. package/admin-shell/src/overrides.ts +53 -0
  32. package/admin-shell/src/states.ts +93 -0
  33. package/admin-shell/src/tabs.ts +31 -0
  34. package/admin-shell/src/targets.ts +128 -0
  35. package/admin-shell/src/theme.ts +15 -0
  36. package/client-core/src/index.ts +415 -0
  37. package/client-nuxt/src/index.ts +48 -0
  38. package/client-uniapp/src/index.ts +50 -0
  39. package/file-media/src/FileAssetSelector.vue +117 -0
  40. package/file-media/src/FileMediaPage.vue +158 -0
  41. package/file-media/src/contracts.ts +220 -0
  42. package/file-media/src/index.ts +19 -0
  43. package/file-media/src/runtime.ts +210 -0
  44. package/import-export/src/ImportExportPage.vue +155 -0
  45. package/import-export/src/contracts.ts +96 -0
  46. package/import-export/src/index.ts +3 -0
  47. package/import-export/src/runtime.ts +128 -0
  48. package/integration-security/src/IntegrationSecurityPage.vue +402 -0
  49. package/integration-security/src/contracts.ts +171 -0
  50. package/integration-security/src/index.ts +3 -0
  51. package/integration-security/src/runtime.ts +180 -0
  52. package/notification-sms/src/NotificationInboxPage.vue +266 -0
  53. package/notification-sms/src/contracts.ts +195 -0
  54. package/notification-sms/src/index.ts +4 -0
  55. package/notification-sms/src/runtime.ts +143 -0
  56. package/ops-console/src/OpsConsolePage.vue +337 -0
  57. package/ops-console/src/contracts.ts +169 -0
  58. package/ops-console/src/index.ts +3 -0
  59. package/ops-console/src/runtime.ts +199 -0
  60. package/package.json +139 -0
  61. package/reference-codes/src/ReferenceCodesPage.vue +942 -0
  62. package/reference-codes/src/contracts.ts +484 -0
  63. package/reference-codes/src/index.ts +53 -0
  64. package/reference-codes/src/runtime.ts +855 -0
  65. package/settings/src/SettingsPage.vue +536 -0
  66. package/settings/src/contracts.ts +331 -0
  67. package/settings/src/index.ts +45 -0
  68. package/settings/src/runtime.ts +545 -0
  69. package/task-job/src/TaskJobPage.vue +120 -0
  70. package/task-job/src/contracts.ts +117 -0
  71. package/task-job/src/index.ts +2 -0
  72. package/task-job/src/runtime.ts +105 -0
  73. 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,122 @@
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
+ evaluateRequiredPermissions,
19
+ permissionEvaluatorSlot,
20
+ PERMISSION_EVALUATOR_OVERRIDE_KEY,
21
+ } from './access/permission-policy'
22
+ export type { PermissionEvaluator } from './access/permission-policy'
23
+ export {
24
+ usePlatformAuth,
25
+ usePlatformContext,
26
+ useTenantAuth,
27
+ useTenantContext,
28
+ } from './auth/stores'
29
+ export type { PlatformContextData, TenantContextData } from './auth/stores'
30
+ export { isMultiTenantDeployment, isTenantAccessToken } from './auth/tenant-session'
31
+ export type {
32
+ TenantAuthentication,
33
+ TenantChoice,
34
+ TenantSelection,
35
+ TenantSessionOutcome,
36
+ } from './auth/tenant-session'
37
+ export { disposeTenantState, registerTenantDisposer } from './lifecycle/tenant'
38
+ export { createTenantLifecycle } from './lifecycle/tenant'
39
+ export type { TenantDisposer, TenantLifecycle, TenantLifecycleTicket } from './lifecycle/tenant'
40
+ export { createMenuRouteRegistry, defineAdminModule } from './module/contribution'
41
+ export { collectPluginContributions, routesForTenantModules } from './module/plugin-contribution-policy'
42
+ export { enabledTenantModulesFromRoutes } from './module/tenant-modules'
43
+ export type {
44
+ AdminModuleContribution,
45
+ AdminModuleLocaleContribution,
46
+ AdminModuleRoute,
47
+ AdminModuleShellSlotContribution,
48
+ AdminModuleShellSlotName,
49
+ AdminModuleStoreContribution,
50
+ AdminRouteAccess,
51
+ MenuRouteRegistry,
52
+ } from './module/contribution'
53
+ export type { PluginFrontendContribution, PluginFrontendRoute } from './module/plugin-contribution-policy'
54
+ export type { TenantModuleRoute } from './module/tenant-modules'
55
+ export { defineAdminHostConfig } from './runtime/config'
56
+ export type { AdminAudienceHostConfig, AdminHostConfig } from './runtime/config'
57
+ export { mapAdminRuntimeError } from './runtime/errors'
58
+ export type { AdminRuntimeErrorKind, AdminRuntimeErrorState } from './runtime/errors'
59
+ export { runAdminRouteGuard } from './runtime/guard'
60
+ export type {
61
+ AdminRouteGuardDependencies,
62
+ AdminRouteGuardInput,
63
+ AdminRouteGuardResult,
64
+ } from './runtime/guard'
65
+ export { createAdminNavigationRegistry } from './runtime/navigation'
66
+ export type {
67
+ AdminNavigationMenuInput,
68
+ AdminNavigationRegistry,
69
+ AdminNavigationRegistryInput,
70
+ AdminNavigationRoute,
71
+ } from './runtime/navigation'
72
+ export {
73
+ createAdminOverrideRegistry,
74
+ defineAdminOverrideSlot,
75
+ } from './runtime/overrides'
76
+ export type {
77
+ AdminOverride,
78
+ AdminOverrideKind,
79
+ AdminOverrideRegistry,
80
+ AdminOverrideRegistryInput,
81
+ AdminOverrideResolution,
82
+ AdminOverrideResolutionMetadata,
83
+ AdminOverrideSlot,
84
+ AdminOverrideSource,
85
+ } from './runtime/overrides'
86
+ export { useOperationTargets } from './targets/store'
87
+ export type {
88
+ OperationTargetScope,
89
+ TargetCardinality,
90
+ TargetCandidate,
91
+ TypedTarget,
92
+ TypedTargetSet,
93
+ } from './targets/store'
94
+ export type { components, operations, paths } from './generated/api'
95
+ export {
96
+ createDataPolicyDraft,
97
+ createGovernanceCatalog,
98
+ createRolePermissionDraft,
99
+ explainMenuVisibility,
100
+ normalizeAuditFilter,
101
+ projectAuditDetail,
102
+ requireGovernancePermission,
103
+ requireRevision,
104
+ } from './governance/index'
105
+ export type {
106
+ GovernanceAuditDetailInput,
107
+ GovernanceAuditFilter,
108
+ GovernanceAuditOutcome,
109
+ GovernanceAudience,
110
+ GovernanceCatalog,
111
+ GovernanceCatalogInput,
112
+ GovernanceIconDefinition,
113
+ GovernanceMenuExplanation,
114
+ GovernanceMenuInput,
115
+ GovernancePermissionDefinition,
116
+ GovernanceRouteDefinition,
117
+ GovernanceVisibilityContext,
118
+ CreateDataPolicyDraftInput,
119
+ DataPolicyDraftInput,
120
+ RolePermissionDraftInput,
121
+ UpdateDataPolicyDraftInput,
122
+ } 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
+ }