@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,402 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, reactive, ref } from 'vue'
3
+ import { useIntegrationSecurityRuntime } from './runtime'
4
+
5
+ const runtime = useIntegrationSecurityRuntime()
6
+ const machineDialog = ref(false); const webhookDialog = ref(false); const attemptDialog = ref(false)
7
+ const machineForm = reactive({ name: '', scopes: '', expiresAt: '' })
8
+ const webhookForm = reactive({ name: '', url: '', events: '' })
9
+ const activeMachines = computed(() => runtime.state.machines.items.filter(item => item.status === 'active').length)
10
+ const activeWebhooks = computed(() => runtime.state.webhooks.items.filter(item => item.status === 'active').length)
11
+ const csv = (value: string) => [...new Set(value.split(',').map(item => item.trim()).filter(Boolean))]
12
+ const createMachine = async () => { await runtime.createMachine({ name: machineForm.name, scopes: csv(machineForm.scopes), expires_at: machineForm.expiresAt || null }); if (runtime.state.machines.error === null) machineDialog.value = false }
13
+ const createWebhook = async () => { await runtime.createWebhook({ name: webhookForm.name, url: webhookForm.url, events: csv(webhookForm.events) }); if (runtime.state.webhooks.error === null) webhookDialog.value = false }
14
+ const showAttempts = async (deliveryKey: string) => { attemptDialog.value = true; await runtime.loadAttempts(deliveryKey) }
15
+ onMounted(runtime.load)
16
+ </script>
17
+
18
+ <template>
19
+ <main class="integration-security-page">
20
+ <header class="page-header">
21
+ <div><h1>Integration security</h1><p>Machine credentials, outbound endpoints, delivery evidence, and signed-in devices</p></div>
22
+ <el-button @click="runtime.load">
23
+ Refresh
24
+ </el-button>
25
+ </header>
26
+ <el-alert
27
+ v-if="runtime.state.disclosure"
28
+ type="warning"
29
+ :closable="false"
30
+ show-icon
31
+ >
32
+ <template #title>
33
+ Store this {{ runtime.state.disclosure.kind === 'machine-token' ? 'token' : 'secret' }} now. It will not be shown again.
34
+ </template>
35
+ <code class="disclosure">{{ runtime.state.disclosure.value }}</code>
36
+ <el-button
37
+ text
38
+ @click="runtime.clearDisclosure"
39
+ >
40
+ Dismiss
41
+ </el-button>
42
+ </el-alert>
43
+ <section
44
+ class="summary"
45
+ aria-label="Security summary"
46
+ >
47
+ <div><strong>{{ activeMachines }}</strong><span>Active machines</span></div>
48
+ <div><strong>{{ activeWebhooks }}</strong><span>Active webhooks</span></div>
49
+ <div><strong>{{ runtime.state.deliveries.total }}</strong><span>Webhook deliveries</span></div>
50
+ <div><strong>{{ runtime.state.sessions.items.length }}</strong><span>Signed-in devices</span></div>
51
+ </section>
52
+
53
+ <section>
54
+ <div class="section-header">
55
+ <h2>Machine identities</h2><el-button
56
+ v-if="runtime.can.canManageMachines()"
57
+ @click="machineDialog = true"
58
+ >
59
+ Create
60
+ </el-button>
61
+ </div>
62
+ <el-alert
63
+ v-if="runtime.state.machines.error"
64
+ type="error"
65
+ :title="runtime.state.machines.error.message"
66
+ :closable="false"
67
+ />
68
+ <el-table
69
+ v-loading="runtime.state.machines.loading"
70
+ :data="runtime.state.machines.items"
71
+ empty-text="No machine identities"
72
+ >
73
+ <el-table-column
74
+ prop="name"
75
+ label="Name"
76
+ min-width="180"
77
+ /><el-table-column
78
+ prop="status"
79
+ label="Status"
80
+ width="120"
81
+ />
82
+ <el-table-column
83
+ label="Token"
84
+ min-width="180"
85
+ >
86
+ <template #default="{ row }">
87
+ {{ row.tokenPrefix }}...{{ row.tokenLastFour }}
88
+ </template>
89
+ </el-table-column>
90
+ <el-table-column
91
+ label="Scopes"
92
+ min-width="240"
93
+ >
94
+ <template #default="{ row }">
95
+ {{ row.scopes.join(', ') }}
96
+ </template>
97
+ </el-table-column>
98
+ <el-table-column
99
+ v-if="runtime.can.canManageMachines()"
100
+ label=""
101
+ width="170"
102
+ align="right"
103
+ >
104
+ <template #default="{ row }">
105
+ <el-button
106
+ text
107
+ :disabled="row.status !== 'active' || runtime.state.mutating"
108
+ @click="runtime.rotateMachine(row)"
109
+ >
110
+ Rotate
111
+ </el-button><el-button
112
+ text
113
+ type="danger"
114
+ :disabled="row.status !== 'active' || runtime.state.mutating"
115
+ @click="runtime.revokeMachine(row)"
116
+ >
117
+ Revoke
118
+ </el-button>
119
+ </template>
120
+ </el-table-column>
121
+ </el-table>
122
+ </section>
123
+
124
+ <section>
125
+ <div class="section-header">
126
+ <h2>Webhook endpoints</h2><el-button
127
+ v-if="runtime.can.canManageWebhooks()"
128
+ @click="webhookDialog = true"
129
+ >
130
+ Create
131
+ </el-button>
132
+ </div>
133
+ <el-alert
134
+ v-if="runtime.state.webhooks.error"
135
+ type="error"
136
+ :title="runtime.state.webhooks.error.message"
137
+ :closable="false"
138
+ />
139
+ <el-table
140
+ v-loading="runtime.state.webhooks.loading"
141
+ :data="runtime.state.webhooks.items"
142
+ empty-text="No webhook endpoints"
143
+ >
144
+ <el-table-column
145
+ prop="name"
146
+ label="Name"
147
+ min-width="160"
148
+ /><el-table-column
149
+ prop="url"
150
+ label="HTTPS destination"
151
+ min-width="280"
152
+ show-overflow-tooltip
153
+ />
154
+ <el-table-column
155
+ prop="status"
156
+ label="Status"
157
+ width="120"
158
+ /><el-table-column
159
+ label="Events"
160
+ min-width="220"
161
+ >
162
+ <template #default="{ row }">
163
+ {{ row.events.join(', ') }}
164
+ </template>
165
+ </el-table-column>
166
+ <el-table-column
167
+ v-if="runtime.can.canManageWebhooks()"
168
+ label=""
169
+ width="180"
170
+ align="right"
171
+ >
172
+ <template #default="{ row }">
173
+ <el-button
174
+ text
175
+ :disabled="row.status !== 'active' || runtime.state.mutating"
176
+ @click="runtime.rotateWebhook(row)"
177
+ >
178
+ Rotate secret
179
+ </el-button><el-button
180
+ text
181
+ type="danger"
182
+ :disabled="row.status !== 'active' || runtime.state.mutating"
183
+ @click="runtime.disableWebhook(row)"
184
+ >
185
+ Disable
186
+ </el-button>
187
+ </template>
188
+ </el-table-column>
189
+ </el-table>
190
+ </section>
191
+
192
+ <section>
193
+ <h2>Webhook deliveries</h2>
194
+ <el-alert
195
+ v-if="runtime.state.deliveries.error"
196
+ type="error"
197
+ :title="runtime.state.deliveries.error.message"
198
+ :closable="false"
199
+ />
200
+ <el-table
201
+ v-loading="runtime.state.deliveries.loading"
202
+ :data="runtime.state.deliveries.items"
203
+ empty-text="No webhook deliveries"
204
+ >
205
+ <el-table-column
206
+ prop="eventType"
207
+ label="Event"
208
+ min-width="180"
209
+ /><el-table-column
210
+ prop="status"
211
+ label="Status"
212
+ width="150"
213
+ />
214
+ <el-table-column
215
+ prop="attemptCount"
216
+ label="Attempts"
217
+ width="100"
218
+ /><el-table-column
219
+ prop="lastStatusCode"
220
+ label="HTTP"
221
+ width="90"
222
+ />
223
+ <el-table-column
224
+ prop="lastErrorCode"
225
+ label="Result"
226
+ min-width="190"
227
+ /><el-table-column
228
+ label=""
229
+ width="100"
230
+ align="right"
231
+ >
232
+ <template #default="{ row }">
233
+ <el-button
234
+ text
235
+ @click="showAttempts(row.deliveryKey)"
236
+ >
237
+ Attempts
238
+ </el-button>
239
+ </template>
240
+ </el-table-column>
241
+ </el-table>
242
+ <el-pagination
243
+ v-if="runtime.state.deliveries.total > runtime.state.deliveries.pageSize"
244
+ layout="prev, pager, next"
245
+ :page-size="runtime.state.deliveries.pageSize"
246
+ :total="runtime.state.deliveries.total"
247
+ :current-page="runtime.state.deliveries.page"
248
+ @current-change="runtime.loadDeliveries"
249
+ />
250
+ </section>
251
+
252
+ <section>
253
+ <h2>Signed-in devices</h2>
254
+ <el-alert
255
+ v-if="runtime.state.sessions.error"
256
+ type="error"
257
+ :title="runtime.state.sessions.error.message"
258
+ :closable="false"
259
+ />
260
+ <el-table
261
+ v-loading="runtime.state.sessions.loading"
262
+ :data="runtime.state.sessions.items"
263
+ empty-text="No sessions"
264
+ >
265
+ <el-table-column
266
+ label="Device"
267
+ min-width="180"
268
+ >
269
+ <template #default="{ row }">
270
+ {{ row.clientKey }}<span v-if="row.current"> (current)</span>
271
+ </template>
272
+ </el-table-column>
273
+ <el-table-column
274
+ prop="maskedIp"
275
+ label="Network"
276
+ min-width="140"
277
+ /><el-table-column
278
+ prop="lastSeenAt"
279
+ label="Last seen"
280
+ min-width="190"
281
+ />
282
+ <el-table-column
283
+ v-if="runtime.can.canRevokeSession()"
284
+ label=""
285
+ width="112"
286
+ align="right"
287
+ >
288
+ <template #default="{ row }">
289
+ <el-button
290
+ text
291
+ :disabled="row.status !== 'active' || runtime.state.mutating"
292
+ @click="runtime.revokeSession(row)"
293
+ >
294
+ Revoke
295
+ </el-button>
296
+ </template>
297
+ </el-table-column>
298
+ </el-table>
299
+ </section>
300
+
301
+ <el-dialog
302
+ v-model="machineDialog"
303
+ title="Create machine identity"
304
+ width="min(520px, 92vw)"
305
+ >
306
+ <el-form label-position="top">
307
+ <el-form-item label="Name">
308
+ <el-input v-model="machineForm.name" />
309
+ </el-form-item><el-form-item label="Scopes">
310
+ <el-input
311
+ v-model="machineForm.scopes"
312
+ placeholder="webhook.publish, data.export.read"
313
+ />
314
+ </el-form-item><el-form-item label="Expires at">
315
+ <el-input
316
+ v-model="machineForm.expiresAt"
317
+ placeholder="2030-01-01T00:00:00.000Z"
318
+ />
319
+ </el-form-item>
320
+ </el-form><template #footer>
321
+ <el-button @click="machineDialog = false">
322
+ Cancel
323
+ </el-button><el-button
324
+ type="primary"
325
+ :loading="runtime.state.mutating"
326
+ @click="createMachine"
327
+ >
328
+ Create
329
+ </el-button>
330
+ </template>
331
+ </el-dialog>
332
+ <el-dialog
333
+ v-model="webhookDialog"
334
+ title="Create webhook endpoint"
335
+ width="min(520px, 92vw)"
336
+ >
337
+ <el-form label-position="top">
338
+ <el-form-item label="Name">
339
+ <el-input v-model="webhookForm.name" />
340
+ </el-form-item><el-form-item label="HTTPS URL">
341
+ <el-input v-model="webhookForm.url" />
342
+ </el-form-item><el-form-item label="Events">
343
+ <el-input
344
+ v-model="webhookForm.events"
345
+ placeholder="audit.event.created"
346
+ />
347
+ </el-form-item>
348
+ </el-form><template #footer>
349
+ <el-button @click="webhookDialog = false">
350
+ Cancel
351
+ </el-button><el-button
352
+ type="primary"
353
+ :loading="runtime.state.mutating"
354
+ @click="createWebhook"
355
+ >
356
+ Create
357
+ </el-button>
358
+ </template>
359
+ </el-dialog>
360
+ <el-dialog
361
+ v-model="attemptDialog"
362
+ title="Delivery attempts"
363
+ width="min(720px, 94vw)"
364
+ >
365
+ <el-alert
366
+ v-if="runtime.state.attempts.error"
367
+ type="error"
368
+ :title="runtime.state.attempts.error.message"
369
+ :closable="false"
370
+ /><el-table
371
+ v-loading="runtime.state.attempts.loading"
372
+ :data="runtime.state.attempts.items"
373
+ >
374
+ <el-table-column
375
+ prop="attemptNumber"
376
+ label="#"
377
+ width="64"
378
+ /><el-table-column
379
+ prop="outcome"
380
+ label="Outcome"
381
+ min-width="150"
382
+ /><el-table-column
383
+ prop="responseStatus"
384
+ label="HTTP"
385
+ width="90"
386
+ /><el-table-column
387
+ prop="errorCode"
388
+ label="Result"
389
+ min-width="190"
390
+ /><el-table-column
391
+ prop="durationMs"
392
+ label="ms"
393
+ width="90"
394
+ />
395
+ </el-table>
396
+ </el-dialog>
397
+ </main>
398
+ </template>
399
+
400
+ <style scoped>
401
+ .integration-security-page{display:grid;gap:24px;max-width:1280px;margin:0 auto;padding:24px}.page-header,.section-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.page-header h1,.section-header h2{margin:0}.page-header h1{font-size:24px}.page-header p{margin:6px 0 0;color:var(--el-text-color-secondary)}.summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border:1px solid var(--el-border-color);border-radius:6px}.summary div{display:grid;gap:4px;padding:16px;border-right:1px solid var(--el-border-color)}.summary div:last-child{border-right:0}.summary strong{font-size:20px}.summary span{color:var(--el-text-color-secondary)}section h2{font-size:16px;margin:0 0 12px}.disclosure{display:block;overflow-wrap:anywhere;margin-top:8px}@media(max-width:720px){.integration-security-page{padding:16px}.summary{grid-template-columns:1fr}.summary div{border-right:0;border-bottom:1px solid var(--el-border-color)}.summary div:last-child{border-bottom:0}}
402
+ </style>
@@ -0,0 +1,171 @@
1
+ export type MachineStatus = 'active' | 'rotated' | 'revoked'
2
+ export type WebhookStatus = 'active' | 'disabled'
3
+ export type DeliveryStatus = 'pending' | 'delivering' | 'retryable' | 'delivered' | 'permanent_failed'
4
+
5
+ export interface MachineIdentity {
6
+ readonly identityKey: string; readonly name: string; readonly scopes: readonly string[]
7
+ readonly status: MachineStatus; readonly tokenPrefix: string; readonly tokenLastFour: string
8
+ readonly expiresAt: string | null; readonly lastUsedAt: string | null; readonly revision: number; readonly createdAt: string
9
+ }
10
+ export interface ProvisionedMachineIdentity { readonly identity: MachineIdentity; readonly token: string }
11
+ export interface WebhookEndpoint {
12
+ readonly endpointKey: string; readonly name: string; readonly url: string; readonly events: readonly string[]
13
+ readonly status: WebhookStatus; readonly revision: number; readonly createdAt: string
14
+ }
15
+ export interface ProvisionedWebhookEndpoint { readonly endpoint: WebhookEndpoint; readonly signingSecret: string }
16
+ export interface WebhookDeliveryRecord {
17
+ readonly deliveryKey: string; readonly endpointKey: string; readonly eventType: string; readonly status: DeliveryStatus
18
+ readonly attemptCount: number; readonly lastStatusCode: number | null; readonly lastErrorCode: string | null
19
+ readonly createdAt: string; readonly updatedAt: string; readonly deliveredAt: string | null
20
+ }
21
+ export interface WebhookAttemptRecord {
22
+ readonly attemptNumber: number; readonly outcome: 'retryable' | 'delivered' | 'permanent_failed'
23
+ readonly responseStatus: number | null; readonly errorCode: string | null; readonly durationMs: number; readonly attemptedAt: string
24
+ }
25
+ export interface SessionDevice {
26
+ readonly sessionKey: string; readonly clientKey: string; readonly status: 'active' | 'revoked' | 'expired'
27
+ readonly current: boolean; readonly maskedIp: string | null; readonly userAgentFingerprint: string | null
28
+ readonly issuedAt: string; readonly lastSeenAt: string; readonly absoluteExpiresAt: string; readonly revokedAt: string | null
29
+ }
30
+ export interface Page<T> { readonly items: T[]; readonly page: number; readonly pageSize: number; readonly total: number }
31
+ export interface TransportResult { readonly body: unknown; readonly headers: Headers; readonly status: number }
32
+ export interface IntegrationSecurityTransport {
33
+ machines: (signal: AbortSignal) => Promise<TransportResult>
34
+ createMachine: (input: { name: string; scopes: string[]; expires_at: string | null }, signal: AbortSignal) => Promise<TransportResult>
35
+ rotateMachine: (identityKey: string, revision: number, signal: AbortSignal) => Promise<TransportResult>
36
+ revokeMachine: (identityKey: string, revision: number, signal: AbortSignal) => Promise<TransportResult>
37
+ webhooks: (signal: AbortSignal) => Promise<TransportResult>
38
+ createWebhook: (input: { name: string; url: string; events: string[] }, signal: AbortSignal) => Promise<TransportResult>
39
+ rotateWebhook: (endpointKey: string, revision: number, signal: AbortSignal) => Promise<TransportResult>
40
+ disableWebhook: (endpointKey: string, revision: number, signal: AbortSignal) => Promise<TransportResult>
41
+ deliveries: (page: number, pageSize: number, signal: AbortSignal) => Promise<TransportResult>
42
+ deliveryAttempts: (deliveryKey: string, page: number, pageSize: number, signal: AbortSignal) => Promise<TransportResult>
43
+ sessions: (signal: AbortSignal) => Promise<TransportResult>
44
+ revokeSession: (sessionKey: string, signal: AbortSignal) => Promise<TransportResult>
45
+ }
46
+
47
+ const record = (value: unknown): Record<string, unknown> => {
48
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error('INTEGRATION_RESPONSE_INVALID')
49
+ return value as Record<string, unknown>
50
+ }
51
+ const exact = (value: Record<string, unknown>, keys: readonly string[]): void => {
52
+ const actual = Object.keys(value).sort(); const expected = [...keys].sort()
53
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) throw new Error('INTEGRATION_RESPONSE_INVALID')
54
+ }
55
+ const instant = (value: unknown): value is string => typeof value === 'string'
56
+ && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) && Number.isFinite(Date.parse(value))
57
+ const qualified = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/
58
+ const requestId = (value: unknown): value is string => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/.test(value)
59
+ const statusCode = (value: unknown): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value >= 100 && value <= 599
60
+ const safeCode = (value: unknown): value is string => typeof value === 'string' && /^[A-Z][A-Z0-9_]{2,63}$/.test(value)
61
+
62
+ export const parseMachine = (value: unknown): MachineIdentity => {
63
+ const item = record(value)
64
+ const scopes = item.scopes
65
+ exact(item, ['identity_key', 'name', 'scopes', 'status', 'token_prefix', 'token_last_four', 'expires_at', 'last_used_at', 'revision', 'created_at'])
66
+ if (typeof item.identity_key !== 'string' || !/^machine_[0-9a-f]{32}$/.test(item.identity_key)
67
+ || typeof item.name !== 'string' || item.name === '' || [...item.name].length > 120
68
+ || !Array.isArray(scopes) || scopes.length < 1 || scopes.length > 32 || scopes.some(scope => typeof scope !== 'string' || !qualified.test(scope))
69
+ || new Set(scopes).size !== scopes.length || [...scopes].sort().some((scope, index) => scope !== scopes[index])
70
+ || !['active', 'rotated', 'revoked'].includes(String(item.status))
71
+ || typeof item.token_prefix !== 'string' || !item.token_prefix.startsWith('pa_mi_')
72
+ || typeof item.token_last_four !== 'string' || !/^[A-Za-z0-9_-]{4}$/.test(item.token_last_four)
73
+ || (item.expires_at !== null && !instant(item.expires_at)) || (item.last_used_at !== null && !instant(item.last_used_at))
74
+ || typeof item.revision !== 'number' || !Number.isSafeInteger(item.revision) || item.revision < 1 || !instant(item.created_at)
75
+ ) throw new Error('INTEGRATION_RESPONSE_INVALID')
76
+ return { identityKey: item.identity_key, name: item.name, scopes: scopes as string[], status: item.status as MachineStatus, tokenPrefix: item.token_prefix, tokenLastFour: item.token_last_four, expiresAt: item.expires_at as string | null, lastUsedAt: item.last_used_at as string | null, revision: item.revision, createdAt: item.created_at }
77
+ }
78
+ export const parseProvisionedMachine = (value: unknown): ProvisionedMachineIdentity => {
79
+ const item = record(value); exact(item, ['identity', 'token'])
80
+ if (typeof item.token !== 'string' || !/^pa_mi_[A-Za-z0-9_-]{43}$/.test(item.token)) throw new Error('INTEGRATION_RESPONSE_INVALID')
81
+ return { identity: parseMachine(item.identity), token: item.token }
82
+ }
83
+ export const parseWebhook = (value: unknown): WebhookEndpoint => {
84
+ const item = record(value)
85
+ exact(item, ['endpoint_key', 'name', 'url', 'events', 'status', 'revision', 'created_at'])
86
+ if (typeof item.endpoint_key !== 'string' || !/^webhook_[0-9a-f]{32}$/.test(item.endpoint_key)
87
+ || typeof item.name !== 'string' || item.name === '' || [...item.name].length > 120
88
+ || typeof item.url !== 'string' || !item.url.startsWith('https://') || item.url.length > 2048
89
+ || !Array.isArray(item.events) || item.events.length < 1 || item.events.length > 32 || item.events.some(event => typeof event !== 'string' || !qualified.test(event))
90
+ || new Set(item.events).size !== item.events.length || !['active', 'disabled'].includes(String(item.status))
91
+ || typeof item.revision !== 'number' || !Number.isSafeInteger(item.revision) || item.revision < 1 || !instant(item.created_at)
92
+ ) throw new Error('INTEGRATION_RESPONSE_INVALID')
93
+ return { endpointKey: item.endpoint_key, name: item.name, url: item.url, events: item.events as string[], status: item.status as WebhookStatus, revision: item.revision, createdAt: item.created_at }
94
+ }
95
+ export const parseProvisionedWebhook = (value: unknown): ProvisionedWebhookEndpoint => {
96
+ const item = record(value); exact(item, ['endpoint', 'signing_secret'])
97
+ if (typeof item.signing_secret !== 'string' || !/^whsec_[A-Za-z0-9_-]{43}$/.test(item.signing_secret)) throw new Error('INTEGRATION_RESPONSE_INVALID')
98
+ return { endpoint: parseWebhook(item.endpoint), signingSecret: item.signing_secret }
99
+ }
100
+ export const parseDelivery = (value: unknown): WebhookDeliveryRecord => {
101
+ const item = record(value)
102
+ exact(item, ['delivery_key', 'endpoint_key', 'event_type', 'status', 'attempt_count', 'last_status_code', 'last_error_code', 'created_at', 'updated_at', 'delivered_at'])
103
+ if (typeof item.delivery_key !== 'string' || !/^delivery_[0-9a-f]{32}$/.test(item.delivery_key)
104
+ || typeof item.endpoint_key !== 'string' || !/^webhook_[0-9a-f]{32}$/.test(item.endpoint_key)
105
+ || typeof item.event_type !== 'string' || !qualified.test(item.event_type)
106
+ || !['pending', 'delivering', 'retryable', 'delivered', 'permanent_failed'].includes(String(item.status))
107
+ || typeof item.attempt_count !== 'number' || !Number.isSafeInteger(item.attempt_count) || item.attempt_count < 0 || item.attempt_count > 8
108
+ || (item.last_status_code !== null && !statusCode(item.last_status_code)) || (item.last_error_code !== null && !safeCode(item.last_error_code))
109
+ || !instant(item.created_at) || !instant(item.updated_at) || (item.delivered_at !== null && !instant(item.delivered_at))) throw new Error('INTEGRATION_RESPONSE_INVALID')
110
+ return { deliveryKey: item.delivery_key, endpointKey: item.endpoint_key, eventType: item.event_type, status: item.status as DeliveryStatus, attemptCount: item.attempt_count, lastStatusCode: item.last_status_code as number | null, lastErrorCode: item.last_error_code as string | null, createdAt: item.created_at, updatedAt: item.updated_at, deliveredAt: item.delivered_at as string | null }
111
+ }
112
+ export const parseAttempt = (value: unknown): WebhookAttemptRecord => {
113
+ const item = record(value); exact(item, ['attempt_number', 'outcome', 'response_status', 'error_code', 'duration_ms', 'attempted_at'])
114
+ if (typeof item.attempt_number !== 'number' || !Number.isSafeInteger(item.attempt_number) || item.attempt_number < 1 || item.attempt_number > 8
115
+ || !['retryable', 'delivered', 'permanent_failed'].includes(String(item.outcome))
116
+ || (item.response_status !== null && !statusCode(item.response_status)) || (item.error_code !== null && !safeCode(item.error_code))
117
+ || typeof item.duration_ms !== 'number' || !Number.isSafeInteger(item.duration_ms) || item.duration_ms < 0 || item.duration_ms > 30000 || !instant(item.attempted_at)) throw new Error('INTEGRATION_RESPONSE_INVALID')
118
+ return { attemptNumber: item.attempt_number, outcome: item.outcome as WebhookAttemptRecord['outcome'], responseStatus: item.response_status as number | null, errorCode: item.error_code as string | null, durationMs: item.duration_ms, attemptedAt: item.attempted_at }
119
+ }
120
+ export const parseSession = (value: unknown): SessionDevice => {
121
+ const item = record(value)
122
+ exact(item, ['session_key', 'client_key', 'status', 'current', 'masked_ip', 'user_agent_fingerprint', 'issued_at', 'last_seen_at', 'absolute_expires_at', 'revoked_at'])
123
+ if (typeof item.session_key !== 'string' || !/^[0-9A-HJKMNP-TV-Z]{26}$/.test(item.session_key) || item.client_key !== 'admin-web'
124
+ || !['active', 'revoked', 'expired'].includes(String(item.status)) || typeof item.current !== 'boolean'
125
+ || (item.masked_ip !== null && (typeof item.masked_ip !== 'string' || item.masked_ip.length > 45))
126
+ || (item.user_agent_fingerprint !== null && (typeof item.user_agent_fingerprint !== 'string' || !/^[0-9a-f]{12}$/.test(item.user_agent_fingerprint)))
127
+ || !instant(item.issued_at) || !instant(item.last_seen_at) || !instant(item.absolute_expires_at) || (item.revoked_at !== null && !instant(item.revoked_at))) throw new Error('INTEGRATION_RESPONSE_INVALID')
128
+ return { sessionKey: item.session_key, clientKey: item.client_key, status: item.status as SessionDevice['status'], current: item.current, maskedIp: item.masked_ip as string | null, userAgentFingerprint: item.user_agent_fingerprint as string | null, issuedAt: item.issued_at, lastSeenAt: item.last_seen_at, absoluteExpiresAt: item.absolute_expires_at, revokedAt: item.revoked_at as string | null }
129
+ }
130
+ export const parseList = <T>(value: unknown, parser: (item: unknown) => T): T[] => {
131
+ const body = record(value); exact(body, ['data', 'meta']); const data = record(body.data); const meta = record(body.meta)
132
+ exact(data, ['items']); exact(meta, ['request_id'])
133
+ if (!Array.isArray(data.items) || !requestId(meta.request_id)) throw new Error('INTEGRATION_RESPONSE_INVALID')
134
+ return data.items.map(parser)
135
+ }
136
+ export const parseItem = <T>(value: unknown, parser: (item: unknown) => T): T => {
137
+ const body = record(value); exact(body, ['data', 'meta']); const meta = record(body.meta); exact(meta, ['request_id'])
138
+ if (!requestId(meta.request_id)) throw new Error('INTEGRATION_RESPONSE_INVALID')
139
+ return parser(body.data)
140
+ }
141
+ export const parsePage = <T>(value: unknown, parser: (item: unknown) => T): Page<T> => {
142
+ const body = record(value); exact(body, ['data', 'meta']); const data = record(body.data); const meta = record(body.meta)
143
+ exact(data, ['items', 'page', 'page_size', 'total']); exact(meta, ['request_id'])
144
+ if (!Array.isArray(data.items) || !requestId(meta.request_id) || typeof data.page !== 'number' || !Number.isSafeInteger(data.page) || data.page < 1
145
+ || typeof data.page_size !== 'number' || !Number.isSafeInteger(data.page_size) || data.page_size < 1 || data.page_size > 100
146
+ || typeof data.total !== 'number' || !Number.isSafeInteger(data.total) || data.total < 0) throw new Error('INTEGRATION_RESPONSE_INVALID')
147
+ return { items: data.items.map(parser), page: data.page, pageSize: data.page_size, total: data.total }
148
+ }
149
+ const json = (value: unknown): string => JSON.stringify(value)
150
+ export const createIntegrationSecurityFetchTransport = (options: { readonly baseUrl: string; readonly fetch?: (request: Request) => Promise<Response> }): IntegrationSecurityTransport => {
151
+ const fetcher = options.fetch ?? fetch
152
+ const request = async (path: string, init: RequestInit): Promise<TransportResult> => {
153
+ const response = await fetcher(new Request(new URL(path, options.baseUrl), { credentials: 'include', ...init, headers: { Accept: 'application/json', ...init.headers } }))
154
+ return { body: await response.json(), headers: response.headers, status: response.status }
155
+ }
156
+ const write = (path: string, method: string, body: unknown, signal: AbortSignal) => request(path, { method, body: json(body), headers: { 'Content-Type': 'application/json' }, signal })
157
+ return {
158
+ machines: signal => request('/api/v1/integration-security/machine-identities', { method: 'GET', signal }),
159
+ createMachine: (input, signal) => write('/api/v1/integration-security/machine-identities', 'POST', input, signal),
160
+ rotateMachine: (key, revision, signal) => write(`/api/v1/integration-security/machine-identities/${encodeURIComponent(key)}/rotate`, 'POST', { revision }, signal),
161
+ revokeMachine: (key, revision, signal) => write(`/api/v1/integration-security/machine-identities/${encodeURIComponent(key)}`, 'DELETE', { revision }, signal),
162
+ webhooks: signal => request('/api/v1/integration-security/webhooks', { method: 'GET', signal }),
163
+ createWebhook: (input, signal) => write('/api/v1/integration-security/webhooks', 'POST', input, signal),
164
+ rotateWebhook: (key, revision, signal) => write(`/api/v1/integration-security/webhooks/${encodeURIComponent(key)}/rotate-secret`, 'POST', { revision }, signal),
165
+ disableWebhook: (key, revision, signal) => write(`/api/v1/integration-security/webhooks/${encodeURIComponent(key)}`, 'DELETE', { revision }, signal),
166
+ deliveries: (page, pageSize, signal) => request(`/api/v1/integration-security/deliveries?page=${page}&page_size=${pageSize}`, { method: 'GET', signal }),
167
+ deliveryAttempts: (key, page, pageSize, signal) => request(`/api/v1/integration-security/deliveries/${encodeURIComponent(key)}/attempts?page=${page}&page_size=${pageSize}`, { method: 'GET', signal }),
168
+ sessions: signal => request('/api/v1/integration-security/sessions', { method: 'GET', signal }),
169
+ revokeSession: (key, signal) => write(`/api/v1/integration-security/sessions/${encodeURIComponent(key)}/revoke`, 'POST', {}, signal),
170
+ }
171
+ }
@@ -0,0 +1,3 @@
1
+ export * from './contracts'
2
+ export * from './runtime'
3
+ export { default as IntegrationSecurityPage } from './IntegrationSecurityPage.vue'