@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.
Files changed (67) hide show
  1. package/LICENSE +202 -0
  2. package/admin-core/src/access/access.ts +27 -0
  3. package/admin-core/src/api/client.ts +200 -0
  4. package/admin-core/src/api/problem.ts +67 -0
  5. package/admin-core/src/api/refresh.ts +122 -0
  6. package/admin-core/src/auth/stores.ts +121 -0
  7. package/admin-core/src/generated/api.d.ts +22106 -0
  8. package/admin-core/src/governance/audit.ts +56 -0
  9. package/admin-core/src/governance/catalog.ts +86 -0
  10. package/admin-core/src/governance/index.ts +26 -0
  11. package/admin-core/src/governance/menu.ts +63 -0
  12. package/admin-core/src/governance/roles.ts +144 -0
  13. package/admin-core/src/governance/types.ts +64 -0
  14. package/admin-core/src/index.ts +105 -0
  15. package/admin-core/src/lifecycle/tenant.ts +59 -0
  16. package/admin-core/src/module/contribution.ts +124 -0
  17. package/admin-core/src/runtime/config.ts +55 -0
  18. package/admin-core/src/runtime/errors.ts +81 -0
  19. package/admin-core/src/runtime/guard.ts +40 -0
  20. package/admin-core/src/runtime/navigation.ts +105 -0
  21. package/admin-core/src/runtime/overrides.ts +214 -0
  22. package/admin-core/src/targets/store.ts +153 -0
  23. package/admin-shell/src/config.ts +84 -0
  24. package/admin-shell/src/index.ts +40 -0
  25. package/admin-shell/src/layout.ts +332 -0
  26. package/admin-shell/src/overrides.ts +53 -0
  27. package/admin-shell/src/states.ts +93 -0
  28. package/admin-shell/src/targets.ts +128 -0
  29. package/admin-shell/src/theme.ts +15 -0
  30. package/client-core/src/index.ts +325 -0
  31. package/client-nuxt/src/index.ts +40 -0
  32. package/client-uniapp/src/index.ts +50 -0
  33. package/file-media/src/FileAssetSelector.vue +117 -0
  34. package/file-media/src/FileMediaPage.vue +158 -0
  35. package/file-media/src/contracts.ts +220 -0
  36. package/file-media/src/index.ts +19 -0
  37. package/file-media/src/runtime.ts +210 -0
  38. package/import-export/src/ImportExportPage.vue +155 -0
  39. package/import-export/src/contracts.ts +96 -0
  40. package/import-export/src/index.ts +3 -0
  41. package/import-export/src/runtime.ts +128 -0
  42. package/integration-security/src/IntegrationSecurityPage.vue +402 -0
  43. package/integration-security/src/contracts.ts +171 -0
  44. package/integration-security/src/index.ts +3 -0
  45. package/integration-security/src/runtime.ts +180 -0
  46. package/notification-sms/src/NotificationInboxPage.vue +266 -0
  47. package/notification-sms/src/contracts.ts +195 -0
  48. package/notification-sms/src/index.ts +4 -0
  49. package/notification-sms/src/runtime.ts +143 -0
  50. package/ops-console/src/OpsConsolePage.vue +337 -0
  51. package/ops-console/src/contracts.ts +169 -0
  52. package/ops-console/src/index.ts +3 -0
  53. package/ops-console/src/runtime.ts +199 -0
  54. package/package.json +108 -0
  55. package/reference-codes/src/ReferenceCodesPage.vue +942 -0
  56. package/reference-codes/src/contracts.ts +484 -0
  57. package/reference-codes/src/index.ts +53 -0
  58. package/reference-codes/src/runtime.ts +855 -0
  59. package/settings/src/SettingsPage.vue +536 -0
  60. package/settings/src/contracts.ts +331 -0
  61. package/settings/src/index.ts +45 -0
  62. package/settings/src/runtime.ts +545 -0
  63. package/task-job/src/TaskJobPage.vue +120 -0
  64. package/task-job/src/contracts.ts +117 -0
  65. package/task-job/src/index.ts +2 -0
  66. package/task-job/src/runtime.ts +105 -0
  67. package/testing/src/index.ts +141 -0
@@ -0,0 +1,331 @@
1
+ export type SettingSourceScope = 'deployment' | 'tenant' | 'default' | null
2
+ export type SettingEditorKind = 'boolean' | 'enum' | 'number' | 'secret' | 'string' | 'unsupported'
3
+ export type SettingScalar = boolean | number | string
4
+ export type SettingSchemaType = 'array' | 'boolean' | 'integer' | 'null' | 'number' | 'object' | 'string'
5
+
6
+ export interface SettingSchema {
7
+ readonly type: SettingSchemaType | readonly SettingSchemaType[]
8
+ readonly enum?: readonly unknown[]
9
+ readonly [key: string]: unknown
10
+ }
11
+
12
+ export interface SettingRecord {
13
+ readonly moduleKey: string
14
+ readonly settingKey: string
15
+ readonly name: string
16
+ readonly description: string
17
+ readonly schema: SettingSchema
18
+ readonly required: boolean
19
+ readonly secret: boolean
20
+ readonly configured: boolean
21
+ readonly sourceScope: SettingSourceScope
22
+ readonly value: unknown
23
+ readonly effectiveAt: string | null
24
+ readonly expiresAt: string | null
25
+ readonly revision: string
26
+ readonly etag: string | null
27
+ }
28
+
29
+ export interface SettingGroup {
30
+ readonly moduleKey: string
31
+ readonly definitions: readonly SettingRecord[]
32
+ }
33
+
34
+ export interface SettingsTransportResult {
35
+ readonly body: unknown
36
+ readonly headers: Headers
37
+ readonly status: number
38
+ }
39
+
40
+ export type SettingPrecondition =
41
+ | { readonly kind: 'create' }
42
+ | { readonly kind: 'replace'; readonly etag: string }
43
+
44
+ export interface ReplaceSettingRequest {
45
+ readonly value: unknown
46
+ readonly idempotencyKey: string
47
+ readonly precondition: SettingPrecondition
48
+ readonly signal: AbortSignal
49
+ }
50
+
51
+ export interface UnsetSettingRequest {
52
+ readonly idempotencyKey: string
53
+ readonly etag: string
54
+ readonly signal: AbortSignal
55
+ }
56
+
57
+ export interface SettingsTransport {
58
+ list: (signal: AbortSignal) => Promise<SettingsTransportResult>
59
+ replace: (
60
+ moduleKey: string,
61
+ settingKey: string,
62
+ request: ReplaceSettingRequest,
63
+ ) => Promise<SettingsTransportResult>
64
+ unset: (
65
+ moduleKey: string,
66
+ settingKey: string,
67
+ request: UnsetSettingRequest,
68
+ ) => Promise<SettingsTransportResult>
69
+ }
70
+
71
+ const moduleKeyPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)*$/
72
+ const settingKeyPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
73
+ const revisionPattern = /^[1-9][0-9]*$/
74
+ const strongEtagPattern = /^"[^"\r\n]+"$/
75
+ const schemaTypes = new Set<SettingSchemaType>(['array', 'boolean', 'integer', 'null', 'number', 'object', 'string'])
76
+ const sourceScopes = new Set<Exclude<SettingSourceScope, null>>(['deployment', 'tenant', 'default'])
77
+
78
+ const invalidResponse = (): never => {
79
+ throw new Error('SETTINGS_RESPONSE_INVALID')
80
+ }
81
+
82
+ const isRecord = (value: unknown): value is Record<string, unknown> => (
83
+ typeof value === 'object' && value !== null && !Array.isArray(value)
84
+ )
85
+
86
+ const stringField = (value: unknown, maximum: number): string => {
87
+ if (typeof value !== 'string' || value === '' || value.length > maximum) return invalidResponse()
88
+ return value
89
+ }
90
+
91
+ const nullableDate = (value: unknown): string | null => {
92
+ if (value === null) return null
93
+ if (typeof value !== 'string' || !value.endsWith('Z') || Number.isNaN(Date.parse(value))) {
94
+ return invalidResponse()
95
+ }
96
+ return value
97
+ }
98
+
99
+ const strongEtag = (value: unknown): string | null => {
100
+ if (value === null) return null
101
+ if (typeof value !== 'string' || !strongEtagPattern.test(value)) return invalidResponse()
102
+ return value
103
+ }
104
+
105
+ const enumValues = (schema: Record<string, unknown>, type: SettingSchemaType): readonly unknown[] | undefined => {
106
+ if (!Object.hasOwn(schema, 'enum')) return undefined
107
+ if (!Array.isArray(schema.enum) || schema.enum.length === 0) return invalidResponse()
108
+ if (type === 'array' || type === 'object' || type === 'null') return [...schema.enum]
109
+
110
+ const values: SettingScalar[] = []
111
+ const seen = new Set<string>()
112
+ for (const value of schema.enum) {
113
+ const valid = type === 'string'
114
+ ? typeof value === 'string'
115
+ : type === 'boolean'
116
+ ? typeof value === 'boolean'
117
+ : type === 'number'
118
+ ? typeof value === 'number' && Number.isFinite(value)
119
+ : type === 'integer'
120
+ ? typeof value === 'number' && Number.isInteger(value)
121
+ : false
122
+ if (!valid) return invalidResponse()
123
+ const digest = `${typeof value}:${String(value)}`
124
+ if (seen.has(digest)) return invalidResponse()
125
+ seen.add(digest)
126
+ values.push(value as SettingScalar)
127
+ }
128
+ return values
129
+ }
130
+
131
+ const parseSchema = (value: unknown): SettingSchema => {
132
+ if (!isRecord(value)) return invalidResponse()
133
+ const type = typeof value.type === 'string'
134
+ ? schemaTypes.has(value.type as SettingSchemaType) ? value.type as SettingSchemaType : invalidResponse()
135
+ : Array.isArray(value.type)
136
+ && value.type.length > 0
137
+ && new Set(value.type).size === value.type.length
138
+ && value.type.every(item => typeof item === 'string' && schemaTypes.has(item as SettingSchemaType))
139
+ ? value.type as SettingSchemaType[]
140
+ : invalidResponse()
141
+ const parsedEnum = Array.isArray(type)
142
+ ? Object.hasOwn(value, 'enum')
143
+ ? Array.isArray(value.enum) && value.enum.length > 0 ? [...value.enum] : invalidResponse()
144
+ : undefined
145
+ : enumValues(value, type)
146
+
147
+ return parsedEnum === undefined
148
+ ? { ...value, type }
149
+ : { ...value, type, enum: parsedEnum }
150
+ }
151
+
152
+ const parseSourceScope = (value: unknown): SettingSourceScope => {
153
+ if (value === null) return null
154
+ if (typeof value !== 'string' || !sourceScopes.has(value as Exclude<SettingSourceScope, null>)) {
155
+ return invalidResponse()
156
+ }
157
+ return value as Exclude<SettingSourceScope, null>
158
+ }
159
+
160
+ const parseSettingValue = (value: unknown, schema: SettingSchema, allowsMissingValue: boolean): unknown => {
161
+ if (Array.isArray(schema.type) || schema.type === 'array' || schema.type === 'object' || schema.type === 'null') {
162
+ return value
163
+ }
164
+ if (value === null) return allowsMissingValue ? null : invalidResponse()
165
+
166
+ const valid = schema.type === 'boolean'
167
+ ? typeof value === 'boolean'
168
+ : schema.type === 'string'
169
+ ? typeof value === 'string'
170
+ : schema.type === 'number'
171
+ ? typeof value === 'number' && Number.isFinite(value)
172
+ : typeof value === 'number' && Number.isInteger(value)
173
+ if (!valid) return invalidResponse()
174
+ if (schema.enum !== undefined && !schema.enum.some(candidate => Object.is(candidate, value))) {
175
+ return invalidResponse()
176
+ }
177
+ return value
178
+ }
179
+
180
+ export const parseSettingRecord = (value: unknown): SettingRecord => {
181
+ if (!isRecord(value)) return invalidResponse()
182
+ const moduleKey = stringField(value.module_key, 96)
183
+ const settingKey = stringField(value.setting_key, 64)
184
+ if (!moduleKeyPattern.test(moduleKey) || !settingKeyPattern.test(settingKey)) return invalidResponse()
185
+ if (
186
+ typeof value.required !== 'boolean'
187
+ || typeof value.secret !== 'boolean'
188
+ || typeof value.configured !== 'boolean'
189
+ || typeof value.revision !== 'string'
190
+ || !revisionPattern.test(value.revision)
191
+ ) {
192
+ return invalidResponse()
193
+ }
194
+
195
+ const schema = parseSchema(value.schema)
196
+ const sourceScope = parseSourceScope(value.source_scope)
197
+ if (value.secret && (schema.type !== 'string' || value.value !== undefined)) {
198
+ throw new Error('SETTINGS_SECRET_RESPONSE_EXPOSED')
199
+ }
200
+ if (!value.secret && !Object.hasOwn(value, 'value')) return invalidResponse()
201
+
202
+ return {
203
+ moduleKey,
204
+ settingKey,
205
+ name: stringField(value.name, 160),
206
+ description: stringField(value.description, 500),
207
+ schema,
208
+ required: value.required,
209
+ secret: value.secret,
210
+ configured: value.configured,
211
+ sourceScope,
212
+ value: value.secret
213
+ ? null
214
+ : parseSettingValue(value.value, schema, !value.required && !value.configured && sourceScope === null),
215
+ effectiveAt: nullableDate(value.effective_at),
216
+ expiresAt: nullableDate(value.expires_at),
217
+ revision: value.revision,
218
+ etag: strongEtag(value.etag),
219
+ }
220
+ }
221
+
222
+ export const parseSettingsList = (value: unknown): SettingRecord[] => {
223
+ if (!isRecord(value) || !isRecord(value.data) || !Array.isArray(value.data.items)) {
224
+ return invalidResponse()
225
+ }
226
+ return value.data.items.map(parseSettingRecord)
227
+ }
228
+
229
+ export const parseSettingResponse = (value: unknown, responseEtag?: string): SettingRecord => {
230
+ if (!isRecord(value) || !isRecord(value.data)) return invalidResponse()
231
+ return parseSettingRecord(responseEtag === undefined ? value.data : { ...value.data, etag: responseEtag })
232
+ }
233
+
234
+ export const groupSettingRecords = (records: readonly SettingRecord[]): SettingGroup[] => {
235
+ const groups = new Map<string, SettingRecord[]>()
236
+ for (const record of records) {
237
+ const definitions = groups.get(record.moduleKey) ?? []
238
+ definitions.push(record)
239
+ groups.set(record.moduleKey, definitions)
240
+ }
241
+
242
+ return [...groups.entries()]
243
+ .sort(([left], [right]) => left.localeCompare(right))
244
+ .map(([moduleKey, definitions]) => ({
245
+ moduleKey,
246
+ definitions: [...definitions].sort((left, right) => left.settingKey.localeCompare(right.settingKey)),
247
+ }))
248
+ }
249
+
250
+ export const settingEditorKind = (record: SettingRecord): SettingEditorKind => {
251
+ if (record.secret) return 'secret'
252
+ if (Array.isArray(record.schema.type)) return 'unsupported'
253
+ if (record.schema.type !== 'array' && record.schema.type !== 'object' && record.schema.enum !== undefined) return 'enum'
254
+ if (record.schema.type === 'boolean') return 'boolean'
255
+ if (record.schema.type === 'integer' || record.schema.type === 'number') return 'number'
256
+ if (record.schema.type === 'string') return 'string'
257
+ return 'unsupported'
258
+ }
259
+
260
+ export interface SettingsFetchTransportOptions {
261
+ readonly baseUrl: string
262
+ readonly fetch: (request: Request) => Promise<Response>
263
+ }
264
+
265
+ const responseBody = async (response: Response): Promise<unknown> => {
266
+ const text = await response.text()
267
+ if (text === '') return null
268
+ try {
269
+ return JSON.parse(text) as unknown
270
+ } catch {
271
+ return null
272
+ }
273
+ }
274
+
275
+ const transportResult = async (response: Response): Promise<SettingsTransportResult> => ({
276
+ body: await responseBody(response),
277
+ headers: response.headers,
278
+ status: response.status,
279
+ })
280
+
281
+ const settingUrl = (baseUrl: string, moduleKey?: string, settingKey?: string): URL => {
282
+ const path = moduleKey === undefined || settingKey === undefined
283
+ ? '/api/v1/settings'
284
+ : `/api/v1/settings/${encodeURIComponent(moduleKey)}/${encodeURIComponent(settingKey)}`
285
+ return new URL(path, baseUrl)
286
+ }
287
+
288
+ export const createSettingsFetchTransport = (options: SettingsFetchTransportOptions): SettingsTransport => ({
289
+ async list(signal) {
290
+ return transportResult(await options.fetch(new Request(settingUrl(options.baseUrl), {
291
+ credentials: 'include',
292
+ headers: { Accept: 'application/json' },
293
+ method: 'GET',
294
+ redirect: 'manual',
295
+ signal,
296
+ })))
297
+ },
298
+ async replace(moduleKey, settingKey, request) {
299
+ const headers = new Headers({
300
+ Accept: 'application/json',
301
+ 'Content-Type': 'application/json',
302
+ 'Idempotency-Key': request.idempotencyKey,
303
+ })
304
+ if (request.precondition.kind === 'create') {
305
+ headers.set('If-None-Match', '*')
306
+ } else {
307
+ headers.set('If-Match', request.precondition.etag)
308
+ }
309
+ return transportResult(await options.fetch(new Request(settingUrl(options.baseUrl, moduleKey, settingKey), {
310
+ body: JSON.stringify({ value: request.value }),
311
+ credentials: 'include',
312
+ headers,
313
+ method: 'PUT',
314
+ redirect: 'manual',
315
+ signal: request.signal,
316
+ })))
317
+ },
318
+ async unset(moduleKey, settingKey, request) {
319
+ return transportResult(await options.fetch(new Request(settingUrl(options.baseUrl, moduleKey, settingKey), {
320
+ credentials: 'include',
321
+ headers: {
322
+ Accept: 'application/json',
323
+ 'Idempotency-Key': request.idempotencyKey,
324
+ 'If-Match': request.etag,
325
+ },
326
+ method: 'DELETE',
327
+ redirect: 'manual',
328
+ signal: request.signal,
329
+ })))
330
+ },
331
+ })
@@ -0,0 +1,45 @@
1
+ export const SETTINGS_PACKAGE = '@peanut-admin/admin/settings' as const
2
+ export const SETTINGS_VERSION = '0.1.0' as const
3
+
4
+ export {
5
+ createSettingsFetchTransport,
6
+ groupSettingRecords,
7
+ parseSettingRecord,
8
+ parseSettingResponse,
9
+ parseSettingsList,
10
+ settingEditorKind,
11
+ } from './contracts'
12
+ export type {
13
+ ReplaceSettingRequest,
14
+ SettingEditorKind,
15
+ SettingGroup,
16
+ SettingPrecondition,
17
+ SettingRecord,
18
+ SettingScalar,
19
+ SettingSchema,
20
+ SettingSourceScope,
21
+ SettingsFetchTransportOptions,
22
+ SettingsTransport,
23
+ SettingsTransportResult,
24
+ UnsetSettingRequest,
25
+ } from './contracts'
26
+ export {
27
+ createSettingsModuleContribution,
28
+ createSettingsRuntime,
29
+ SETTINGS_MANAGE_PERMISSION,
30
+ SETTINGS_MODULE_KEY,
31
+ SETTINGS_READ_PERMISSION,
32
+ SETTINGS_ROUTE_NAME,
33
+ SETTINGS_ROUTE_PATH,
34
+ SETTINGS_STORE_KEY,
35
+ settingsRuntimeKey,
36
+ useSettingsRuntime,
37
+ } from './runtime'
38
+ export type {
39
+ SettingConflictState,
40
+ SettingFormState,
41
+ SettingRequestError,
42
+ SettingsRuntime,
43
+ SettingsRuntimeOptions,
44
+ SettingsRuntimeState,
45
+ } from './runtime'