@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,117 @@
1
+ <script setup lang="ts">
2
+ import { EmptyState } from '@peanut-admin/admin/shell'
3
+ import { ElButton } from 'element-plus'
4
+ import type { AssetCandidate } from './contracts'
5
+
6
+ withDefaults(defineProps<{
7
+ items: readonly AssetCandidate[]
8
+ selectedFileKey?: string | null
9
+ loading?: boolean
10
+ error?: string | null
11
+ disabled?: boolean
12
+ }>(), {
13
+ selectedFileKey: null,
14
+ loading: false,
15
+ error: null,
16
+ disabled: false,
17
+ })
18
+
19
+ const emit = defineEmits<{
20
+ select: [asset: AssetCandidate]
21
+ retry: []
22
+ }>()
23
+ </script>
24
+
25
+ <template>
26
+ <section
27
+ class="asset-selector"
28
+ aria-labelledby="asset-selector-title"
29
+ >
30
+ <header class="asset-selector__header">
31
+ <h2 id="asset-selector-title">
32
+ Media assets
33
+ </h2>
34
+ <ElButton
35
+ :loading="loading"
36
+ :disabled="disabled"
37
+ @click="emit('retry')"
38
+ >
39
+ Reload
40
+ </ElButton>
41
+ </header>
42
+ <div
43
+ v-if="error"
44
+ class="asset-selector__error"
45
+ role="alert"
46
+ >
47
+ <p>{{ error }}</p>
48
+ </div>
49
+ <div
50
+ v-else-if="loading"
51
+ role="status"
52
+ class="asset-selector__status"
53
+ >
54
+ Loading media assets...
55
+ </div>
56
+ <EmptyState
57
+ v-else-if="items.length === 0"
58
+ title="No media assets"
59
+ message="Upload an image before selecting an asset."
60
+ />
61
+ <ul
62
+ v-else
63
+ class="asset-selector__grid"
64
+ aria-label="Available media assets"
65
+ >
66
+ <li
67
+ v-for="asset in items"
68
+ :key="asset.fileKey"
69
+ class="asset-selector__item"
70
+ >
71
+ <button
72
+ type="button"
73
+ class="asset-selector__choice"
74
+ :class="{ 'is-selected': selectedFileKey === asset.fileKey }"
75
+ :aria-pressed="selectedFileKey === asset.fileKey"
76
+ :disabled="disabled"
77
+ @click="emit('select', asset)"
78
+ >
79
+ <img
80
+ v-if="asset.previewUri"
81
+ :src="asset.previewUri"
82
+ :alt="asset.originalName"
83
+ class="asset-selector__preview"
84
+ loading="lazy"
85
+ decoding="async"
86
+ referrerpolicy="no-referrer"
87
+ >
88
+ <span
89
+ v-else
90
+ class="asset-selector__placeholder"
91
+ aria-hidden="true"
92
+ >IMG</span>
93
+ <span class="asset-selector__name">{{ asset.originalName }}</span>
94
+ <span class="asset-selector__meta">{{ asset.width }} x {{ asset.height }}</span>
95
+ </button>
96
+ </li>
97
+ </ul>
98
+ </section>
99
+ </template>
100
+
101
+ <style scoped>
102
+ .asset-selector { min-width: 0; }
103
+ .asset-selector__header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
104
+ .asset-selector__header h2 { margin: 0; font-size: 16px; letter-spacing: 0; }
105
+ .asset-selector__status, .asset-selector__error { padding: 16px 0; }
106
+ .asset-selector__error { color: var(--el-color-danger); }
107
+ .asset-selector__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(144px, 1fr)); gap: 12px; padding: 0; margin: 0; list-style: none; }
108
+ .asset-selector__item { min-width: 0; }
109
+ .asset-selector__choice { width: 100%; padding: 8px; border: 1px solid var(--el-border-color); background: var(--el-bg-color); color: inherit; text-align: left; cursor: pointer; }
110
+ .asset-selector__choice.is-selected { border-color: var(--el-color-primary); box-shadow: 0 0 0 1px var(--el-color-primary) inset; }
111
+ .asset-selector__choice:disabled { cursor: not-allowed; opacity: .65; }
112
+ .asset-selector__preview, .asset-selector__placeholder { display: flex; width: 100%; aspect-ratio: 1; object-fit: cover; align-items: center; justify-content: center; background: var(--el-fill-color-light); }
113
+ .asset-selector__placeholder { color: var(--el-text-color-secondary); font-size: 13px; }
114
+ .asset-selector__name, .asset-selector__meta { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
115
+ .asset-selector__name { margin-top: 8px; font-weight: 600; }
116
+ .asset-selector__meta { margin-top: 2px; color: var(--el-text-color-secondary); font-size: 12px; }
117
+ </style>
@@ -0,0 +1,158 @@
1
+ <script setup lang="ts">
2
+ import { EmptyState, ForbiddenState, ModuleUnavailableState, PageContent, PageHeader, PageToolbar, SessionExpiredState } from '@peanut-admin/admin/shell'
3
+ import { ElButton } from 'element-plus'
4
+ import { computed, onMounted, ref } from 'vue'
5
+ import FileAssetSelector from './FileAssetSelector.vue'
6
+ import { useFileMediaRuntime } from './runtime'
7
+
8
+ const runtime = useFileMediaRuntime()
9
+ const state = runtime.state
10
+ const input = ref<HTMLInputElement | null>(null)
11
+ const canCreate = computed(runtime.canCreate)
12
+ const canDelete = computed(runtime.canDelete)
13
+ const selectedAssetKey = ref<string | null>(null)
14
+
15
+ const upload = async (event: Event): Promise<void> => {
16
+ const target = event.currentTarget
17
+ if (!(target instanceof HTMLInputElement) || target.files?.length !== 1) return
18
+ const file = target.files.item(0)
19
+ if (file !== null) await runtime.upload(file)
20
+ target.value = ''
21
+ }
22
+
23
+ onMounted(() => Promise.all([runtime.load(), runtime.loadAssets()]))
24
+ </script>
25
+
26
+ <template>
27
+ <PageContent class="file-media-page">
28
+ <PageHeader>
29
+ Files
30
+ <template #actions>
31
+ <input
32
+ ref="input"
33
+ class="file-input"
34
+ type="file"
35
+ aria-label="Choose private file"
36
+ @change="upload"
37
+ >
38
+ <ElButton
39
+ :disabled="!canCreate || state.mutating"
40
+ type="primary"
41
+ @click="input?.click()"
42
+ >
43
+ Upload
44
+ </ElButton>
45
+ <ElButton
46
+ :loading="state.loading"
47
+ :disabled="state.mutating"
48
+ @click="runtime.load"
49
+ >
50
+ Reload
51
+ </ElButton>
52
+ </template>
53
+ </PageHeader>
54
+ <PageToolbar label="File status">
55
+ <ElButton
56
+ :type="state.status === 'ready' ? 'primary' : 'default'"
57
+ @click="runtime.setStatus('ready')"
58
+ >
59
+ Ready
60
+ </ElButton>
61
+ <ElButton
62
+ :type="state.status === 'archived' ? 'primary' : 'default'"
63
+ @click="runtime.setStatus('archived')"
64
+ >
65
+ Archived
66
+ </ElButton>
67
+ </PageToolbar>
68
+
69
+ <FileAssetSelector
70
+ :items="state.assets"
71
+ :selected-file-key="selectedAssetKey"
72
+ :loading="state.assetsLoading"
73
+ :error="state.assetsError?.message ?? null"
74
+ :disabled="state.mutating"
75
+ @select="selectedAssetKey = $event.fileKey"
76
+ @retry="runtime.loadAssets"
77
+ />
78
+
79
+ <SessionExpiredState
80
+ v-if="state.error?.status === 401"
81
+ :message="state.error.message"
82
+ />
83
+ <ForbiddenState
84
+ v-else-if="state.error?.status === 403"
85
+ :message="state.error.message"
86
+ />
87
+ <ModuleUnavailableState
88
+ v-else-if="state.error?.status === 503"
89
+ :message="state.error.message"
90
+ @action="runtime.load"
91
+ />
92
+ <section
93
+ v-else-if="state.error"
94
+ role="alert"
95
+ class="file-state"
96
+ >
97
+ <h2>Unable to complete the file request</h2>
98
+ <p>{{ state.error.message }}</p>
99
+ <p v-if="state.error.requestId">
100
+ Request ID: {{ state.error.requestId }}
101
+ </p>
102
+ </section>
103
+ <div
104
+ v-else-if="state.loading"
105
+ class="file-state"
106
+ role="status"
107
+ >
108
+ Loading files...
109
+ </div>
110
+ <EmptyState
111
+ v-else-if="state.items.length === 0"
112
+ title="No files"
113
+ message="No private files match this status."
114
+ />
115
+ <div
116
+ v-else
117
+ class="file-table-wrap"
118
+ >
119
+ <table class="file-table">
120
+ <thead><tr><th>Name</th><th>Type</th><th>Size</th><th>Status</th><th>Revision</th><th>Actions</th></tr></thead>
121
+ <tbody>
122
+ <tr
123
+ v-for="file in state.items"
124
+ :key="file.fileKey"
125
+ >
126
+ <td>{{ file.originalName }}</td><td>{{ file.mediaType }}</td><td>{{ file.sizeBytes }}</td>
127
+ <td>{{ file.status }}</td><td>{{ file.revision }}</td>
128
+ <td>
129
+ <ElButton
130
+ v-if="file.status === 'ready'"
131
+ text
132
+ @click="runtime.download(file)"
133
+ >
134
+ Download
135
+ </ElButton>
136
+ <ElButton
137
+ v-if="file.status === 'ready'"
138
+ text
139
+ :disabled="!canDelete || state.mutating"
140
+ @click="runtime.archive(file)"
141
+ >
142
+ Archive
143
+ </ElButton>
144
+ </td>
145
+ </tr>
146
+ </tbody>
147
+ </table>
148
+ </div>
149
+ </PageContent>
150
+ </template>
151
+
152
+ <style scoped>
153
+ .file-input { display: none; }
154
+ .file-state { padding: 24px 0; }
155
+ .file-table-wrap { overflow-x: auto; }
156
+ .file-table { width: 100%; border-collapse: collapse; }
157
+ .file-table th, .file-table td { padding: 10px 8px; border-bottom: 1px solid var(--el-border-color); text-align: left; }
158
+ </style>
@@ -0,0 +1,220 @@
1
+ export type FileStatus = 'ready' | 'archived'
2
+
3
+ export interface FileObject {
4
+ readonly fileKey: string
5
+ readonly originalName: string
6
+ readonly mediaType: string
7
+ readonly sizeBytes: number
8
+ readonly sha256: string
9
+ readonly status: FileStatus
10
+ readonly revision: number
11
+ readonly createdAt: string
12
+ readonly updatedAt: string
13
+ readonly archivedAt: string | null
14
+ }
15
+
16
+ export interface FileList {
17
+ readonly items: readonly FileObject[]
18
+ readonly page: number
19
+ readonly pageSize: number
20
+ readonly total: number
21
+ }
22
+
23
+ export interface FileTransportResult {
24
+ readonly body: unknown
25
+ readonly headers: Headers
26
+ readonly status: number
27
+ }
28
+
29
+ export interface FileMediaTransport {
30
+ list: (status: FileStatus, page: number, pageSize: number, signal: AbortSignal) => Promise<FileTransportResult>
31
+ assets: (page: number, pageSize: number, signal: AbortSignal) => Promise<FileTransportResult>
32
+ upload: (file: File, signal: AbortSignal) => Promise<FileTransportResult>
33
+ download: (fileKey: string, signal: AbortSignal) => Promise<Response>
34
+ archive: (fileKey: string, etag: string, signal: AbortSignal) => Promise<FileTransportResult>
35
+ }
36
+
37
+ export interface ImageVariant {
38
+ readonly variantKey: string
39
+ readonly width: number
40
+ readonly height: number
41
+ readonly mediaType: 'image/jpeg' | 'image/png'
42
+ readonly deliveryUri: string | null
43
+ }
44
+
45
+ export interface AssetCandidate {
46
+ readonly fileKey: string
47
+ readonly originalName: string
48
+ readonly mediaType: 'image/jpeg' | 'image/png'
49
+ readonly width: number
50
+ readonly height: number
51
+ readonly previewUri: string | null
52
+ readonly variants: readonly ImageVariant[]
53
+ }
54
+
55
+ export interface AssetList {
56
+ readonly items: readonly AssetCandidate[]
57
+ readonly page: number
58
+ readonly pageSize: number
59
+ readonly total: number
60
+ }
61
+
62
+ const fileKeyPattern = /^file_[0-9a-f]{32}$/
63
+ const shaPattern = /^[0-9a-f]{64}$/
64
+
65
+ const record = (value: unknown): Record<string, unknown> => {
66
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error('FILE_MEDIA_RESPONSE_INVALID')
67
+ return value as Record<string, unknown>
68
+ }
69
+
70
+ const exactKeys = (value: Record<string, unknown>, keys: readonly string[]): void => {
71
+ const actual = Object.keys(value).sort()
72
+ const expected = [...keys].sort()
73
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
74
+ throw new Error('FILE_MEDIA_RESPONSE_INVALID')
75
+ }
76
+ }
77
+
78
+ const deliveryUri = (value: unknown): string | null => {
79
+ if (value === null) return null
80
+ if (typeof value !== 'string' || value === '' || value.length > 2048 || value.includes('#')) {
81
+ throw new Error('FILE_MEDIA_RESPONSE_INVALID')
82
+ }
83
+ if (value.startsWith('/')) {
84
+ if (value.startsWith('//') || !value.startsWith('/api/')) throw new Error('FILE_MEDIA_RESPONSE_INVALID')
85
+ return value
86
+ }
87
+ let parsed: URL
88
+ try { parsed = new URL(value) } catch { throw new Error('FILE_MEDIA_RESPONSE_INVALID') }
89
+ if (parsed.protocol !== 'https:' || parsed.username !== '' || parsed.password !== '') {
90
+ throw new Error('FILE_MEDIA_RESPONSE_INVALID')
91
+ }
92
+ return value
93
+ }
94
+
95
+ export const parseFileObject = (value: unknown): FileObject => {
96
+ const item = record(value)
97
+ exactKeys(item, [
98
+ 'file_key', 'original_name', 'media_type', 'size_bytes', 'sha256', 'status',
99
+ 'revision', 'created_at', 'updated_at', 'archived_at',
100
+ ])
101
+ if (
102
+ typeof item.file_key !== 'string' || !fileKeyPattern.test(item.file_key)
103
+ || typeof item.original_name !== 'string' || item.original_name === '' || [...item.original_name].length > 255
104
+ || typeof item.media_type !== 'string' || item.media_type === '' || item.media_type.length > 127
105
+ || typeof item.size_bytes !== 'number' || !Number.isSafeInteger(item.size_bytes) || item.size_bytes < 1
106
+ || typeof item.sha256 !== 'string' || !shaPattern.test(item.sha256)
107
+ || (item.status !== 'ready' && item.status !== 'archived')
108
+ || typeof item.revision !== 'number' || !Number.isSafeInteger(item.revision) || item.revision < 1
109
+ || typeof item.created_at !== 'string' || !Number.isFinite(Date.parse(item.created_at))
110
+ || typeof item.updated_at !== 'string' || !Number.isFinite(Date.parse(item.updated_at))
111
+ || (item.archived_at !== null && (typeof item.archived_at !== 'string' || !Number.isFinite(Date.parse(item.archived_at))))
112
+ || (item.status === 'ready') !== (item.archived_at === null)
113
+ ) throw new Error('FILE_MEDIA_RESPONSE_INVALID')
114
+
115
+ return {
116
+ fileKey: item.file_key,
117
+ originalName: item.original_name,
118
+ mediaType: item.media_type,
119
+ sizeBytes: item.size_bytes,
120
+ sha256: item.sha256,
121
+ status: item.status,
122
+ revision: item.revision,
123
+ createdAt: item.created_at,
124
+ updatedAt: item.updated_at,
125
+ archivedAt: item.archived_at,
126
+ }
127
+ }
128
+
129
+ export const parseFileResponse = (value: unknown): FileObject => {
130
+ const body = record(value)
131
+ exactKeys(body, ['data', 'meta'])
132
+ record(body.meta)
133
+ return parseFileObject(body.data)
134
+ }
135
+
136
+ export const parseFileList = (value: unknown): FileList => {
137
+ const body = record(value)
138
+ exactKeys(body, ['data', 'meta'])
139
+ const data = record(body.data)
140
+ exactKeys(data, ['items'])
141
+ const meta = record(body.meta)
142
+ if (!Array.isArray(data.items)) throw new Error('FILE_MEDIA_RESPONSE_INVALID')
143
+ for (const key of ['page', 'page_size', 'total'] as const) {
144
+ if (typeof meta[key] !== 'number' || !Number.isSafeInteger(meta[key]) || meta[key] < (key === 'total' ? 0 : 1)) {
145
+ throw new Error('FILE_MEDIA_RESPONSE_INVALID')
146
+ }
147
+ }
148
+ return {
149
+ items: data.items.map(parseFileObject),
150
+ page: meta.page as number,
151
+ pageSize: meta.page_size as number,
152
+ total: meta.total as number,
153
+ }
154
+ }
155
+
156
+ export const parseAssetCandidate = (value: unknown): AssetCandidate => {
157
+ const item = record(value)
158
+ exactKeys(item, ['file_key', 'original_name', 'media_type', 'width', 'height', 'preview_uri', 'variants'])
159
+ if (
160
+ typeof item.file_key !== 'string' || !fileKeyPattern.test(item.file_key)
161
+ || typeof item.original_name !== 'string' || item.original_name === '' || [...item.original_name].length > 255
162
+ || (item.media_type !== 'image/jpeg' && item.media_type !== 'image/png')
163
+ || typeof item.width !== 'number' || !Number.isSafeInteger(item.width) || item.width < 1 || item.width > 50000
164
+ || typeof item.height !== 'number' || !Number.isSafeInteger(item.height) || item.height < 1 || item.height > 50000
165
+ || item.width * item.height > 100_000_000
166
+ || !Array.isArray(item.variants) || item.variants.length > 16
167
+ ) throw new Error('FILE_MEDIA_RESPONSE_INVALID')
168
+ const keys = new Set<string>()
169
+ const variants = item.variants.map((value): ImageVariant => {
170
+ const variant = record(value)
171
+ exactKeys(variant, ['variant_key', 'width', 'height', 'media_type', 'delivery_uri'])
172
+ if (
173
+ typeof variant.variant_key !== 'string' || !/^[a-z][a-z0-9-]{0,31}$/.test(variant.variant_key)
174
+ || keys.has(variant.variant_key)
175
+ || typeof variant.width !== 'number' || !Number.isSafeInteger(variant.width) || variant.width < 1 || variant.width > 4096
176
+ || typeof variant.height !== 'number' || !Number.isSafeInteger(variant.height) || variant.height < 1 || variant.height > 4096
177
+ || (variant.media_type !== 'image/jpeg' && variant.media_type !== 'image/png')
178
+ ) throw new Error('FILE_MEDIA_RESPONSE_INVALID')
179
+ keys.add(variant.variant_key)
180
+ return {
181
+ variantKey: variant.variant_key,
182
+ width: variant.width,
183
+ height: variant.height,
184
+ mediaType: variant.media_type,
185
+ deliveryUri: deliveryUri(variant.delivery_uri),
186
+ }
187
+ })
188
+ return {
189
+ fileKey: item.file_key,
190
+ originalName: item.original_name,
191
+ mediaType: item.media_type,
192
+ width: item.width,
193
+ height: item.height,
194
+ previewUri: deliveryUri(item.preview_uri),
195
+ variants,
196
+ }
197
+ }
198
+
199
+ export const parseAssetList = (value: unknown): AssetList => {
200
+ const body = record(value)
201
+ exactKeys(body, ['data', 'meta'])
202
+ const data = record(body.data)
203
+ const meta = record(body.meta)
204
+ exactKeys(data, ['items'])
205
+ exactKeys(meta, ['request_id', 'page', 'page_size', 'total'])
206
+ if (!Array.isArray(data.items) || typeof meta.request_id !== 'string' || meta.request_id === '') {
207
+ throw new Error('FILE_MEDIA_RESPONSE_INVALID')
208
+ }
209
+ for (const key of ['page', 'page_size', 'total'] as const) {
210
+ if (typeof meta[key] !== 'number' || !Number.isSafeInteger(meta[key]) || meta[key] < (key === 'total' ? 0 : 1)) {
211
+ throw new Error('FILE_MEDIA_RESPONSE_INVALID')
212
+ }
213
+ }
214
+ return {
215
+ items: data.items.map(parseAssetCandidate),
216
+ page: meta.page as number,
217
+ pageSize: meta.page_size as number,
218
+ total: meta.total as number,
219
+ }
220
+ }
@@ -0,0 +1,19 @@
1
+ export const FILE_MEDIA_PACKAGE = '@peanut-admin/admin/file-media' as const
2
+ export const FILE_MEDIA_VERSION = '0.1.0' as const
3
+ export { default as FileAssetSelector } from './FileAssetSelector.vue'
4
+ export { parseAssetCandidate, parseFileList, parseFileObject, parseFileResponse } from './contracts'
5
+ export type { AssetCandidate, AssetList, FileList, FileMediaTransport, FileObject, FileStatus, FileTransportResult, ImageVariant } from './contracts'
6
+ export {
7
+ createFileMediaModuleContribution,
8
+ createFileMediaRuntime,
9
+ FILE_MEDIA_CREATE_PERMISSION,
10
+ FILE_MEDIA_DELETE_PERMISSION,
11
+ FILE_MEDIA_MODULE_KEY,
12
+ FILE_MEDIA_READ_PERMISSION,
13
+ FILE_MEDIA_ROUTE_NAME,
14
+ FILE_MEDIA_ROUTE_PATH,
15
+ FILE_MEDIA_STORE_KEY,
16
+ fileMediaRuntimeKey,
17
+ useFileMediaRuntime,
18
+ } from './runtime'
19
+ export type { FileMediaError, FileMediaRuntime, FileMediaRuntimeOptions, FileMediaState } from './runtime'