@objectstack/plugin-approvals 16.1.0 → 17.0.0-rc.1
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/CHANGELOG.md +1462 -0
- package/dist/index.d.mts +2853 -3925
- package/dist/index.d.ts +2853 -3925
- package/dist/index.js +1819 -240
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1817 -230
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -7
- package/.turbo/turbo-build.log +0 -22
- package/scripts/i18n-extract.config.ts +0 -33
- package/src/action-link-pages.ts +0 -102
- package/src/approval-node.test.ts +0 -196
- package/src/approval-node.ts +0 -139
- package/src/approval-revise.test.ts +0 -411
- package/src/approval-service.test.ts +0 -1490
- package/src/approval-service.ts +0 -2360
- package/src/approvals-plugin.ts +0 -263
- package/src/index.ts +0 -39
- package/src/lifecycle-hooks.ts +0 -179
- package/src/nav-contribution.test.ts +0 -50
- package/src/sys-approval-action.object.ts +0 -140
- package/src/sys-approval-approver.object.ts +0 -85
- package/src/sys-approval-delegation.object.ts +0 -142
- package/src/sys-approval-request.object.test.ts +0 -103
- package/src/sys-approval-request.object.ts +0 -401
- package/src/sys-approval-token.object.ts +0 -101
- package/src/translations/en.objects.generated.ts +0 -205
- package/src/translations/es-ES.objects.generated.ts +0 -205
- package/src/translations/index.ts +0 -23
- package/src/translations/ja-JP.objects.generated.ts +0 -205
- package/src/translations/zh-CN.objects.generated.ts +0 -205
- package/tsconfig.json +0 -10
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import { ObjectSchema, Field } from '@objectstack/spec/data';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* sys_approval_request — Live approval instance.
|
|
7
|
-
*
|
|
8
|
-
* ADR-0019: opened by a flow's **Approval node** when the run reaches it; the
|
|
9
|
-
* run suspends until a decision is recorded. The row's lifecycle:
|
|
10
|
-
*
|
|
11
|
-
* `pending` → (per-approver decisions) → `approved` | `rejected`
|
|
12
|
-
* `pending` → recalled by submitter → `recalled`
|
|
13
|
-
*
|
|
14
|
-
* `flow_run_id` / `flow_node_id` tie the request back to the suspended run so a
|
|
15
|
-
* decision can resume it; `current_step` mirrors the node id. `node_config_json`
|
|
16
|
-
* snapshots the Approval node config (approvers / behaviour) the request was
|
|
17
|
-
* opened with.
|
|
18
|
-
*
|
|
19
|
-
* `payload_json` captures a snapshot of the target record at submission
|
|
20
|
-
* time — used by notifications so they can render before the record is
|
|
21
|
-
* locked or changed.
|
|
22
|
-
*
|
|
23
|
-
* @namespace sys
|
|
24
|
-
*/
|
|
25
|
-
export const SysApprovalRequest = ObjectSchema.create({
|
|
26
|
-
name: 'sys_approval_request',
|
|
27
|
-
label: 'Approval Request',
|
|
28
|
-
pluralLabel: 'Approval Requests',
|
|
29
|
-
icon: 'inbox',
|
|
30
|
-
isSystem: true,
|
|
31
|
-
managedBy: 'engine-owned',
|
|
32
|
-
description: 'Live approval instance tracked per submission',
|
|
33
|
-
displayNameField: 'id',
|
|
34
|
-
nameField: 'id', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
|
|
35
|
-
titleFormat: '{process_name} · {record_id}',
|
|
36
|
-
highlightFields: ['process_name', 'object_name', 'record_id', 'status', 'current_step', 'submitter_id', 'updated_at'],
|
|
37
|
-
|
|
38
|
-
// Curated built-in list views — render as segmented tabs in the console.
|
|
39
|
-
// Filters use {current_user_id} substitution wired by the console.
|
|
40
|
-
listViews: {
|
|
41
|
-
my_pending: {
|
|
42
|
-
type: 'grid',
|
|
43
|
-
name: 'my_pending',
|
|
44
|
-
label: 'My Pending',
|
|
45
|
-
data: { provider: 'object', object: 'sys_approval_request' },
|
|
46
|
-
columns: ['process_name', 'object_name', 'record_id', 'current_step', 'submitter_id', 'updated_at'],
|
|
47
|
-
filter: [
|
|
48
|
-
{ field: 'status', operator: 'equals', value: 'pending' },
|
|
49
|
-
{ field: 'pending_approvers', operator: 'contains', value: '{current_user_id}' },
|
|
50
|
-
],
|
|
51
|
-
sort: [{ field: 'updated_at', order: 'desc' }],
|
|
52
|
-
pagination: { pageSize: 25 },
|
|
53
|
-
emptyState: { title: 'No pending approvals', message: 'You\'re all caught up.' },
|
|
54
|
-
},
|
|
55
|
-
submitted_by_me: {
|
|
56
|
-
type: 'grid',
|
|
57
|
-
name: 'submitted_by_me',
|
|
58
|
-
label: 'I Submitted',
|
|
59
|
-
data: { provider: 'object', object: 'sys_approval_request' },
|
|
60
|
-
columns: ['process_name', 'object_name', 'record_id', 'status', 'current_step', 'updated_at'],
|
|
61
|
-
filter: [{ field: 'submitter_id', operator: 'equals', value: '{current_user_id}' }],
|
|
62
|
-
sort: [{ field: 'updated_at', order: 'desc' }],
|
|
63
|
-
pagination: { pageSize: 25 },
|
|
64
|
-
},
|
|
65
|
-
completed: {
|
|
66
|
-
type: 'grid',
|
|
67
|
-
name: 'completed',
|
|
68
|
-
label: 'Completed',
|
|
69
|
-
data: { provider: 'object', object: 'sys_approval_request' },
|
|
70
|
-
columns: ['process_name', 'object_name', 'record_id', 'status', 'submitter_id', 'completed_at'],
|
|
71
|
-
filter: [{ field: 'status', operator: 'in', value: ['approved', 'rejected', 'recalled'] }],
|
|
72
|
-
sort: [{ field: 'completed_at', order: 'desc' }],
|
|
73
|
-
pagination: { pageSize: 25 },
|
|
74
|
-
},
|
|
75
|
-
all_requests: {
|
|
76
|
-
type: 'grid',
|
|
77
|
-
name: 'all_requests',
|
|
78
|
-
label: 'All',
|
|
79
|
-
data: { provider: 'object', object: 'sys_approval_request' },
|
|
80
|
-
columns: ['process_name', 'object_name', 'record_id', 'status', 'current_step', 'submitter_id', 'updated_at'],
|
|
81
|
-
sort: [{ field: 'updated_at', order: 'desc' }],
|
|
82
|
-
pagination: { pageSize: 50 },
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
fields: {
|
|
87
|
-
id: Field.text({ label: 'Request ID', required: true, readonly: true, group: 'System' }),
|
|
88
|
-
|
|
89
|
-
organization_id: Field.lookup('sys_organization', {
|
|
90
|
-
label: 'Organization',
|
|
91
|
-
required: false,
|
|
92
|
-
group: 'System',
|
|
93
|
-
description: 'Tenant that owns this approval request (propagated from submitter context)',
|
|
94
|
-
}),
|
|
95
|
-
|
|
96
|
-
process_name: Field.text({
|
|
97
|
-
label: 'Source',
|
|
98
|
-
required: true,
|
|
99
|
-
maxLength: 100,
|
|
100
|
-
description: 'Origin of the request — `flow:<flowName|nodeId>` for node-driven approvals',
|
|
101
|
-
group: 'Target',
|
|
102
|
-
}),
|
|
103
|
-
|
|
104
|
-
object_name: Field.text({
|
|
105
|
-
label: 'Object',
|
|
106
|
-
required: true,
|
|
107
|
-
maxLength: 100,
|
|
108
|
-
group: 'Target',
|
|
109
|
-
}),
|
|
110
|
-
|
|
111
|
-
record_id: Field.text({
|
|
112
|
-
label: 'Record ID',
|
|
113
|
-
required: true,
|
|
114
|
-
maxLength: 100,
|
|
115
|
-
group: 'Target',
|
|
116
|
-
}),
|
|
117
|
-
|
|
118
|
-
submitter_id: Field.lookup('sys_user', {
|
|
119
|
-
label: 'Submitter',
|
|
120
|
-
required: false,
|
|
121
|
-
group: 'Target',
|
|
122
|
-
}),
|
|
123
|
-
|
|
124
|
-
submitter_comment: Field.textarea({
|
|
125
|
-
label: 'Submitter Comment',
|
|
126
|
-
required: false,
|
|
127
|
-
group: 'Target',
|
|
128
|
-
}),
|
|
129
|
-
|
|
130
|
-
status: Field.select(
|
|
131
|
-
// Keep in sync with `ApprovalStatus` (spec/contracts). `returned` =
|
|
132
|
-
// sent back for revision (ADR-0044) — terminal for this round.
|
|
133
|
-
['pending', 'approved', 'rejected', 'recalled', 'returned'],
|
|
134
|
-
{
|
|
135
|
-
label: 'Status',
|
|
136
|
-
required: true,
|
|
137
|
-
defaultValue: 'pending',
|
|
138
|
-
description: 'Lifecycle state of the request',
|
|
139
|
-
group: 'State',
|
|
140
|
-
},
|
|
141
|
-
),
|
|
142
|
-
|
|
143
|
-
current_step: Field.text({
|
|
144
|
-
label: 'Current Step',
|
|
145
|
-
required: false,
|
|
146
|
-
maxLength: 100,
|
|
147
|
-
description: 'Machine name of the step awaiting approval',
|
|
148
|
-
group: 'State',
|
|
149
|
-
}),
|
|
150
|
-
|
|
151
|
-
current_step_index: Field.number({
|
|
152
|
-
label: 'Current Step Index',
|
|
153
|
-
required: false,
|
|
154
|
-
defaultValue: 0,
|
|
155
|
-
group: 'State',
|
|
156
|
-
}),
|
|
157
|
-
|
|
158
|
-
pending_approvers: Field.textarea({
|
|
159
|
-
label: 'Pending Approvers',
|
|
160
|
-
required: false,
|
|
161
|
-
description: 'Comma-separated user ids who can act on the current step',
|
|
162
|
-
group: 'State',
|
|
163
|
-
}),
|
|
164
|
-
|
|
165
|
-
payload_json: Field.textarea({
|
|
166
|
-
label: 'Snapshot',
|
|
167
|
-
required: false,
|
|
168
|
-
description: 'Record snapshot at submission time',
|
|
169
|
-
group: 'State',
|
|
170
|
-
}),
|
|
171
|
-
|
|
172
|
-
// ── ADR-0019: approval-as-flow-node correlation ──────────────────
|
|
173
|
-
// When a request is opened by an Approval *node* (rather than a standalone
|
|
174
|
-
// process), these tie it back to the suspended flow run so a decision can
|
|
175
|
-
// resume it. Null for legacy process-driven requests.
|
|
176
|
-
flow_run_id: Field.text({
|
|
177
|
-
label: 'Flow Run',
|
|
178
|
-
required: false,
|
|
179
|
-
maxLength: 100,
|
|
180
|
-
readonly: true,
|
|
181
|
-
description: 'Suspended automation run id this request gates (ADR-0019). The decision resumes it.',
|
|
182
|
-
group: 'State',
|
|
183
|
-
}),
|
|
184
|
-
|
|
185
|
-
flow_node_id: Field.text({
|
|
186
|
-
label: 'Flow Node',
|
|
187
|
-
required: false,
|
|
188
|
-
maxLength: 100,
|
|
189
|
-
readonly: true,
|
|
190
|
-
description: 'Approval node id within the flow that opened this request (ADR-0019).',
|
|
191
|
-
group: 'State',
|
|
192
|
-
}),
|
|
193
|
-
|
|
194
|
-
node_config_json: Field.textarea({
|
|
195
|
-
label: 'Node Config',
|
|
196
|
-
required: false,
|
|
197
|
-
readonly: true,
|
|
198
|
-
description: 'Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019).',
|
|
199
|
-
group: 'State',
|
|
200
|
-
}),
|
|
201
|
-
|
|
202
|
-
completed_at: Field.datetime({
|
|
203
|
-
label: 'Completed At',
|
|
204
|
-
required: false,
|
|
205
|
-
group: 'State',
|
|
206
|
-
}),
|
|
207
|
-
|
|
208
|
-
created_at: Field.datetime({
|
|
209
|
-
label: 'Created At',
|
|
210
|
-
required: true,
|
|
211
|
-
defaultValue: 'NOW()',
|
|
212
|
-
readonly: true,
|
|
213
|
-
group: 'System',
|
|
214
|
-
}),
|
|
215
|
-
|
|
216
|
-
updated_at: Field.datetime({ label: 'Updated At', required: false, group: 'System' }),
|
|
217
|
-
},
|
|
218
|
-
|
|
219
|
-
indexes: [
|
|
220
|
-
// Look up "is there a pending request for this record?" — common
|
|
221
|
-
// guard on submit and on edit-while-locked checks.
|
|
222
|
-
{ fields: ['object_name', 'record_id'] },
|
|
223
|
-
{ fields: ['status', 'object_name'] },
|
|
224
|
-
// Status-windowed listings (escalation sweep, "All" tab ordering).
|
|
225
|
-
// "My approvals" matching no longer scans this table: the service keeps
|
|
226
|
-
// a normalized per-approver index in `sys_approval_approver` (#1745) and
|
|
227
|
-
// resolves approver filters there; `pending_approvers` stays the
|
|
228
|
-
// human-readable CSV source of truth only.
|
|
229
|
-
{ fields: ['status', 'updated_at'] },
|
|
230
|
-
{ fields: ['submitter_id', 'status'] },
|
|
231
|
-
],
|
|
232
|
-
|
|
233
|
-
// Server-declared decision actions (objectui#2678 P2-4). The console's
|
|
234
|
-
// generic action runtime renders and executes these wherever this object is
|
|
235
|
-
// surfaced — the approvals inbox included — so new decision capabilities
|
|
236
|
-
// (and their params) ship as metadata, not as hand-written buttons. Each
|
|
237
|
-
// targets the existing approvals REST route; `{id}` resolves from the row
|
|
238
|
-
// and `actorId` defaults to the caller server-side. The service remains the
|
|
239
|
-
// authority on who may act; `visible` gates on the server-computed
|
|
240
|
-
// per-viewer block (#3310): approver actions on `record.viewer.can_act`
|
|
241
|
-
// (the caller is a current pending approver — same check the service
|
|
242
|
-
// authorizes a decision with, so position/team approvers resolve correctly),
|
|
243
|
-
// submitter actions on `record.viewer.is_submitter`. `viewer` is attached by
|
|
244
|
-
// getRequest/listRequests; where it is absent the predicate fails closed.
|
|
245
|
-
actions: [
|
|
246
|
-
{
|
|
247
|
-
name: 'approval_approve',
|
|
248
|
-
label: 'Approve',
|
|
249
|
-
icon: 'check-circle',
|
|
250
|
-
type: 'api',
|
|
251
|
-
method: 'POST',
|
|
252
|
-
target: '/api/v1/approvals/requests/{id}/approve',
|
|
253
|
-
params: [
|
|
254
|
-
{ name: 'comment', label: 'Comment', type: 'textarea', required: false },
|
|
255
|
-
// Decision attachments (#3266). The console renders `type:'file'` params
|
|
256
|
-
// through the shared upload widget and POSTs the resolved `attachments:
|
|
257
|
-
// string[]`; the decision route persists them on `sys_approval_action`.
|
|
258
|
-
{ name: 'attachments', label: 'Attachments', type: 'file', multiple: true, required: false },
|
|
259
|
-
],
|
|
260
|
-
visible: 'record.viewer.can_act',
|
|
261
|
-
locations: ['record_section', 'list_item'],
|
|
262
|
-
successMessage: 'Approved.',
|
|
263
|
-
refreshAfter: true,
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
name: 'approval_reject',
|
|
267
|
-
label: 'Reject',
|
|
268
|
-
icon: 'x-circle',
|
|
269
|
-
type: 'api',
|
|
270
|
-
method: 'POST',
|
|
271
|
-
target: '/api/v1/approvals/requests/{id}/reject',
|
|
272
|
-
params: [
|
|
273
|
-
{ name: 'comment', label: 'Comment', type: 'textarea', required: false },
|
|
274
|
-
{ name: 'attachments', label: 'Attachments', type: 'file', multiple: true, required: false },
|
|
275
|
-
],
|
|
276
|
-
visible: 'record.viewer.can_act',
|
|
277
|
-
confirmText: 'Reject this request? A rejection is final for every approver.',
|
|
278
|
-
locations: ['record_section', 'list_item'],
|
|
279
|
-
successMessage: 'Rejected.',
|
|
280
|
-
refreshAfter: true,
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
name: 'approval_reassign',
|
|
284
|
-
label: 'Reassign',
|
|
285
|
-
icon: 'arrow-right-left',
|
|
286
|
-
type: 'api',
|
|
287
|
-
method: 'POST',
|
|
288
|
-
target: '/api/v1/approvals/requests/{id}/reassign',
|
|
289
|
-
params: [
|
|
290
|
-
// Field-backed on `submitter_id` (the object's only `sys_user` lookup):
|
|
291
|
-
// the console resolves its lookup config (`reference_to: sys_user`) so the
|
|
292
|
-
// dialog renders a real user picker, while `name: 'to'` overrides the
|
|
293
|
-
// request-body key to the `to` the reassign route expects. This is a
|
|
294
|
-
// config-borrow, not a submitter pre-fill (`defaultFromRow` stays off).
|
|
295
|
-
{ field: 'submitter_id', name: 'to', label: 'New approver', required: true, helpText: 'User to hand this step to' },
|
|
296
|
-
{ name: 'comment', label: 'Comment', type: 'textarea', required: false },
|
|
297
|
-
],
|
|
298
|
-
visible: 'record.viewer.can_act',
|
|
299
|
-
locations: ['record_section'],
|
|
300
|
-
successMessage: 'Reassigned.',
|
|
301
|
-
refreshAfter: true,
|
|
302
|
-
},
|
|
303
|
-
|
|
304
|
-
// ── Approver secondary decisions ────────────────────────────────
|
|
305
|
-
// Send back for revision / request more info (ADR-0044). Both are approver
|
|
306
|
-
// actions, so `visible` gates on `record.viewer.can_act` (a current pending
|
|
307
|
-
// approver) — same as approve/reject. The service stays the authority.
|
|
308
|
-
{
|
|
309
|
-
name: 'approval_send_back',
|
|
310
|
-
label: 'Send back',
|
|
311
|
-
icon: 'corner-up-left',
|
|
312
|
-
type: 'api',
|
|
313
|
-
method: 'POST',
|
|
314
|
-
target: '/api/v1/approvals/requests/{id}/revise',
|
|
315
|
-
params: [
|
|
316
|
-
{ name: 'comment', label: 'Reason', type: 'textarea', required: false },
|
|
317
|
-
],
|
|
318
|
-
visible: 'record.viewer.can_act',
|
|
319
|
-
locations: ['record_section'],
|
|
320
|
-
successMessage: 'Sent back for revision.',
|
|
321
|
-
refreshAfter: true,
|
|
322
|
-
},
|
|
323
|
-
{
|
|
324
|
-
name: 'approval_request_info',
|
|
325
|
-
label: 'Request info',
|
|
326
|
-
icon: 'help-circle',
|
|
327
|
-
type: 'api',
|
|
328
|
-
method: 'POST',
|
|
329
|
-
target: '/api/v1/approvals/requests/{id}/request-info',
|
|
330
|
-
params: [
|
|
331
|
-
{ name: 'comment', label: 'What do you need?', type: 'textarea', required: true },
|
|
332
|
-
],
|
|
333
|
-
visible: 'record.viewer.can_act',
|
|
334
|
-
locations: ['record_section'],
|
|
335
|
-
successMessage: 'Information requested.',
|
|
336
|
-
refreshAfter: true,
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
// ── Submitter continuity actions ────────────────────────────────
|
|
340
|
-
// Remind / recall (pending) and resubmit / recall (returned). These are the
|
|
341
|
-
// submitter's own levers, so `visible` gates on `record.viewer.is_submitter`
|
|
342
|
-
// (server-computed on the current viewer). The service re-checks ownership;
|
|
343
|
-
// the predicate keeps a non-submitter from ever seeing a button they cannot
|
|
344
|
-
// use.
|
|
345
|
-
{
|
|
346
|
-
name: 'approval_remind',
|
|
347
|
-
label: 'Send reminder',
|
|
348
|
-
icon: 'bell-ring',
|
|
349
|
-
type: 'api',
|
|
350
|
-
method: 'POST',
|
|
351
|
-
target: '/api/v1/approvals/requests/{id}/remind',
|
|
352
|
-
params: [
|
|
353
|
-
{ name: 'comment', label: 'Note', type: 'textarea', required: false },
|
|
354
|
-
],
|
|
355
|
-
visible: 'record.status == "pending" && record.viewer.is_submitter',
|
|
356
|
-
locations: ['record_section'],
|
|
357
|
-
successMessage: 'Reminder sent.',
|
|
358
|
-
refreshAfter: true,
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
name: 'approval_recall',
|
|
362
|
-
label: 'Recall',
|
|
363
|
-
icon: 'undo-2',
|
|
364
|
-
type: 'api',
|
|
365
|
-
method: 'POST',
|
|
366
|
-
target: '/api/v1/approvals/requests/{id}/recall',
|
|
367
|
-
params: [
|
|
368
|
-
{ name: 'comment', label: 'Comment', type: 'textarea', required: false },
|
|
369
|
-
],
|
|
370
|
-
// Recall applies while the request is live for the submitter — pending
|
|
371
|
-
// (withdraw) or returned (abandon the revision instead of resubmitting).
|
|
372
|
-
visible: '(record.status == "pending" || record.status == "returned") && record.viewer.is_submitter',
|
|
373
|
-
confirmText: 'Recall this request? Approvers can no longer act on it and the record is unlocked.',
|
|
374
|
-
locations: ['record_section'],
|
|
375
|
-
successMessage: 'Recalled.',
|
|
376
|
-
refreshAfter: true,
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
name: 'approval_resubmit',
|
|
380
|
-
label: 'Resubmit',
|
|
381
|
-
icon: 'refresh-cw',
|
|
382
|
-
type: 'api',
|
|
383
|
-
method: 'POST',
|
|
384
|
-
target: '/api/v1/approvals/requests/{id}/resubmit',
|
|
385
|
-
params: [
|
|
386
|
-
{ name: 'comment', label: 'What changed?', type: 'textarea', required: false },
|
|
387
|
-
],
|
|
388
|
-
visible: 'record.status == "returned" && record.viewer.is_submitter',
|
|
389
|
-
locations: ['record_section'],
|
|
390
|
-
successMessage: 'Resubmitted.',
|
|
391
|
-
refreshAfter: true,
|
|
392
|
-
},
|
|
393
|
-
],
|
|
394
|
-
|
|
395
|
-
enable: {
|
|
396
|
-
// [ADR-0103] Engine-owned: the approval engine owns the request lifecycle
|
|
397
|
-
// (SYSTEM_CTX); users act via domain actions (Submit/Approve/Recall), never
|
|
398
|
-
// generic CRUD. Reads stay open.
|
|
399
|
-
apiMethods: ['get', 'list'],
|
|
400
|
-
},
|
|
401
|
-
});
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import { ObjectSchema, Field } from '@objectstack/spec/data';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* sys_approval_token — single-use actionable-link tokens (ADR-0043).
|
|
7
|
-
*
|
|
8
|
-
* One row per issued approve/reject link. Only the SHA-256 **hash** of the
|
|
9
|
-
* raw token is stored — a database leak yields no usable links. A token is
|
|
10
|
-
* dead once any of these holds: `consumed_at` set, `expires_at` passed, the
|
|
11
|
-
* request left `pending`, or the bound approver no longer holds a slot
|
|
12
|
-
* (the last two are re-checked at redemption, not materialized here).
|
|
13
|
-
*
|
|
14
|
-
* @namespace sys
|
|
15
|
-
*/
|
|
16
|
-
export const SysApprovalToken = ObjectSchema.create({
|
|
17
|
-
name: 'sys_approval_token',
|
|
18
|
-
label: 'Approval Action Token',
|
|
19
|
-
pluralLabel: 'Approval Action Tokens',
|
|
20
|
-
icon: 'key',
|
|
21
|
-
isSystem: true,
|
|
22
|
-
managedBy: 'engine-owned',
|
|
23
|
-
description: 'Single-use tokens behind actionable approval links',
|
|
24
|
-
displayNameField: 'id',
|
|
25
|
-
nameField: 'id', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
|
|
26
|
-
|
|
27
|
-
fields: {
|
|
28
|
-
id: Field.text({ label: 'Token ID', required: true, readonly: true, group: 'System' }),
|
|
29
|
-
|
|
30
|
-
organization_id: Field.lookup('sys_organization', {
|
|
31
|
-
label: 'Organization',
|
|
32
|
-
required: false,
|
|
33
|
-
group: 'System',
|
|
34
|
-
}),
|
|
35
|
-
|
|
36
|
-
token_hash: Field.text({
|
|
37
|
-
label: 'Token Hash',
|
|
38
|
-
required: true,
|
|
39
|
-
maxLength: 100,
|
|
40
|
-
readonly: true,
|
|
41
|
-
description: 'SHA-256 hex of the raw token — the raw value is never stored',
|
|
42
|
-
group: 'Token',
|
|
43
|
-
}),
|
|
44
|
-
|
|
45
|
-
request_id: Field.text({
|
|
46
|
-
label: 'Request',
|
|
47
|
-
required: true,
|
|
48
|
-
maxLength: 100,
|
|
49
|
-
readonly: true,
|
|
50
|
-
group: 'Token',
|
|
51
|
-
}),
|
|
52
|
-
|
|
53
|
-
action: Field.select(['approve', 'reject'], {
|
|
54
|
-
label: 'Action',
|
|
55
|
-
required: true,
|
|
56
|
-
readonly: true,
|
|
57
|
-
group: 'Token',
|
|
58
|
-
}),
|
|
59
|
-
|
|
60
|
-
approver_id: Field.text({
|
|
61
|
-
label: 'Approver',
|
|
62
|
-
required: true,
|
|
63
|
-
maxLength: 200,
|
|
64
|
-
readonly: true,
|
|
65
|
-
description: 'Identity the token is bound to; the decision is audited as this approver',
|
|
66
|
-
group: 'Token',
|
|
67
|
-
}),
|
|
68
|
-
|
|
69
|
-
expires_at: Field.datetime({
|
|
70
|
-
label: 'Expires At',
|
|
71
|
-
required: true,
|
|
72
|
-
readonly: true,
|
|
73
|
-
group: 'Lifecycle',
|
|
74
|
-
}),
|
|
75
|
-
|
|
76
|
-
consumed_at: Field.datetime({
|
|
77
|
-
label: 'Consumed At',
|
|
78
|
-
required: false,
|
|
79
|
-
group: 'Lifecycle',
|
|
80
|
-
}),
|
|
81
|
-
|
|
82
|
-
created_at: Field.datetime({
|
|
83
|
-
label: 'Created At',
|
|
84
|
-
required: true,
|
|
85
|
-
defaultValue: 'NOW()',
|
|
86
|
-
readonly: true,
|
|
87
|
-
group: 'System',
|
|
88
|
-
}),
|
|
89
|
-
},
|
|
90
|
-
|
|
91
|
-
indexes: [
|
|
92
|
-
{ fields: ['token_hash'] },
|
|
93
|
-
{ fields: ['request_id'] },
|
|
94
|
-
],
|
|
95
|
-
|
|
96
|
-
enable: {
|
|
97
|
-
// [ADR-0103] Engine-owned: one-time email-approval tokens are minted and
|
|
98
|
-
// consumed by the approval engine (SYSTEM_CTX), never via the data API.
|
|
99
|
-
apiMethods: ['get', 'list'],
|
|
100
|
-
},
|
|
101
|
-
});
|