@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.
- package/LICENSE +202 -0
- package/admin-core/src/access/access.ts +27 -0
- package/admin-core/src/access/permission-policy.ts +37 -0
- package/admin-core/src/api/client.ts +200 -0
- package/admin-core/src/api/problem.ts +67 -0
- package/admin-core/src/api/refresh.ts +122 -0
- package/admin-core/src/auth/stores.ts +121 -0
- package/admin-core/src/auth/tenant-session.ts +32 -0
- package/admin-core/src/generated/api.d.ts +22106 -0
- package/admin-core/src/governance/audit.ts +56 -0
- package/admin-core/src/governance/catalog.ts +86 -0
- package/admin-core/src/governance/index.ts +26 -0
- package/admin-core/src/governance/menu.ts +63 -0
- package/admin-core/src/governance/roles.ts +144 -0
- package/admin-core/src/governance/types.ts +64 -0
- package/admin-core/src/index.ts +122 -0
- package/admin-core/src/lifecycle/tenant.ts +59 -0
- package/admin-core/src/module/contribution.ts +124 -0
- package/admin-core/src/module/plugin-contribution-policy.ts +51 -0
- package/admin-core/src/module/tenant-modules.ts +20 -0
- package/admin-core/src/runtime/config.ts +55 -0
- package/admin-core/src/runtime/errors.ts +81 -0
- package/admin-core/src/runtime/guard.ts +40 -0
- package/admin-core/src/runtime/navigation.ts +105 -0
- package/admin-core/src/runtime/overrides.ts +214 -0
- package/admin-core/src/targets/store.ts +153 -0
- package/admin-shell/src/config.ts +84 -0
- package/admin-shell/src/deployment-mode.ts +36 -0
- package/admin-shell/src/index.ts +44 -0
- package/admin-shell/src/layout.ts +332 -0
- package/admin-shell/src/overrides.ts +53 -0
- package/admin-shell/src/states.ts +93 -0
- package/admin-shell/src/tabs.ts +31 -0
- package/admin-shell/src/targets.ts +128 -0
- package/admin-shell/src/theme.ts +15 -0
- package/client-core/src/index.ts +415 -0
- package/client-nuxt/src/index.ts +48 -0
- package/client-uniapp/src/index.ts +50 -0
- package/file-media/src/FileAssetSelector.vue +117 -0
- package/file-media/src/FileMediaPage.vue +158 -0
- package/file-media/src/contracts.ts +220 -0
- package/file-media/src/index.ts +19 -0
- package/file-media/src/runtime.ts +210 -0
- package/import-export/src/ImportExportPage.vue +155 -0
- package/import-export/src/contracts.ts +96 -0
- package/import-export/src/index.ts +3 -0
- package/import-export/src/runtime.ts +128 -0
- package/integration-security/src/IntegrationSecurityPage.vue +402 -0
- package/integration-security/src/contracts.ts +171 -0
- package/integration-security/src/index.ts +3 -0
- package/integration-security/src/runtime.ts +180 -0
- package/notification-sms/src/NotificationInboxPage.vue +266 -0
- package/notification-sms/src/contracts.ts +195 -0
- package/notification-sms/src/index.ts +4 -0
- package/notification-sms/src/runtime.ts +143 -0
- package/ops-console/src/OpsConsolePage.vue +337 -0
- package/ops-console/src/contracts.ts +169 -0
- package/ops-console/src/index.ts +3 -0
- package/ops-console/src/runtime.ts +199 -0
- package/package.json +139 -0
- package/reference-codes/src/ReferenceCodesPage.vue +942 -0
- package/reference-codes/src/contracts.ts +484 -0
- package/reference-codes/src/index.ts +53 -0
- package/reference-codes/src/runtime.ts +855 -0
- package/settings/src/SettingsPage.vue +536 -0
- package/settings/src/contracts.ts +331 -0
- package/settings/src/index.ts +45 -0
- package/settings/src/runtime.ts +545 -0
- package/task-job/src/TaskJobPage.vue +120 -0
- package/task-job/src/contracts.ts +117 -0
- package/task-job/src/index.ts +2 -0
- package/task-job/src/runtime.ts +105 -0
- package/testing/src/index.ts +141 -0
|
@@ -0,0 +1,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
|
+
}
|