-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathapproval.zod.ts
More file actions
493 lines (466 loc) · 24.9 KB
/
Copy pathapproval.zod.ts
File metadata and controls
493 lines (466 loc) · 24.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
import { lazySchema } from '../shared/lazy-schema';
/**
* Approval Step Approver Type
*/
export const ApproverType = z.enum([
'user', // Specific user(s)
// The better-auth ORG-MEMBERSHIP TIER (sys_member.role: owner / admin /
// member), spelled with the projection name ADR-0057 D7 mandates and
// ADR-0090 D3 assumes ("relabelled `org_membership_level` … its UI label is
// 'organization membership', never 'role'"). NOT an org position: a value
// like 'sales_manager' matches nobody — author `position` for those.
'org_membership_level',
// @deprecated ADR-0090 D3 — the pre-relabel spelling of
// `org_membership_level`. D3 makes "role" reserved-forbidden on platform
// surfaces; its exception covers better-auth's own `sys_member.role` column,
// NOT this enum (which is ours). Accepted for one deprecation window: the
// runtime resolves it identically and warns, `os lint` prescribes the
// rewrite. Removed in the next major.
'role',
'position', // Holders of a position (sys_user_position, ADR-0090 D3)
'team', // Members of a flat collaboration team (sys_team)
'department', // Members of a department + all descendant departments (sys_business_unit)
'manager', // Submitter's manager (sys_user.manager_id)
'field', // User ID defined in a record field
// @deprecated #3508 — declared-but-unenforced: `resolveApproverSpec` in
// `plugin-approvals` has no queue branch, so a queue approver resolves to
// nobody (the dead `queue:<value>` fallback literal). The sharing engine
// retired its `queue` recipient the same way (sys-sharing-rule). Still
// parses so stored flows keep rendering, but designers must not offer it
// (see NON_AUTHORABLE_APPROVER_TYPES). Re-admit only together with a real
// ownership-queue implementation (queue entity + membership + claim).
'queue',
/**
* #3447 P2: a CEL expression resolved AT NODE ENTRY against three explicit
* roots — `current.*` (the record's live state), `trigger.*` (the submit-time
* snapshot) and `vars.*` (flow variables, incl. upstream node outputs). The
* result (a user-id string, CSV, or string array — or intermediate ids
* re-expanded per `resolveAs`) becomes the approver slate. `record` and bare
* field names are deliberately NOT available: `record` means "the record at
* event time" everywhere else on the platform (flow conditions, hooks), and
* reusing it here would silently alias one of the two times.
*/
'expression'
]);
/**
* Deprecated approver-type spellings → their canonical replacement
* (ADR-0090 D3). The runtime and `os lint` both read this map, so a future
* removal is a one-line edit here plus the enum entry.
*/
export const DEPRECATED_APPROVER_TYPES = {
role: 'org_membership_level',
} as const satisfies Record<string, z.infer<typeof ApproverType>>;
/** Resolve a possibly-deprecated approver type to its canonical spelling. */
export function canonicalApproverType(type: string): string {
return (DEPRECATED_APPROVER_TYPES as Record<string, string>)[type] ?? type;
}
/**
* Approver types that still PARSE but must not be offered for new authoring.
* Published to designers as `xEnumDeprecated` on the approver `type` (see
* {@link ApprovalNodeApproverSchema}): a stored value keeps rendering, the
* dropdown just stops handing authors the entry. Two sources:
* - the deprecated spellings in {@link DEPRECATED_APPROVER_TYPES} (ADR-0090 D3);
* - `queue`, declared-but-unenforced in the runtime (#3508) — offering it
* would advertise a capability the engine doesn't deliver (Prime
* Directive #10: declared ≠ enforced).
*/
export const NON_AUTHORABLE_APPROVER_TYPES: readonly string[] = [
...Object.keys(DEPRECATED_APPROVER_TYPES),
'queue',
];
/**
* How a designer must source an approver row's `value`, per approver type —
* the single declaration of the "metadata registry vs data records" split
* (#3508). The engine (`plugin-approvals` `resolveApproverSpec` +
* `expand*Users`) resolves these against DATA rows in the system directory
* objects, so the matching picker is a RECORD lookup via the data API —
* `GET /api/v1/meta/user` lists metadata types, never `sys_user` rows.
*/
export type ApproverValueBinding =
/**
* `value` identifies a row of `object`; the designer stores the row's
* `valueField`. Note `position` routes by machine **name** (the engine
* filters `sys_user_position.position` by name — deliberate, names are
* portable across environments the way ids are not), and `department`
* resolves against `sys_business_unit`, not a `sys_department`.
*/
| { source: 'record'; object: string; valueField: 'id' | 'name' }
/** Closed value set — render a strict select, never free text. */
| { source: 'enum'; values: readonly string[] }
/**
* Resolved by the engine at runtime (submitter's manager via
* `sys_user.manager_id`); `value` is optional (an advanced override naming
* the record field that holds the subject user) and normally stays empty.
*/
| { source: 'auto' }
/** `value` names a field on the flow's trigger object. */
| { source: 'trigger-field' }
/**
* `value` is a CEL expression evaluated at node entry (#3447 P2) over the
* closed root set in `roots` — an expression editor, not a picker.
*/
| { source: 'expression'; roots: readonly string[] }
/** Declared-but-unenforced — do not offer for authoring (#3508). */
| { source: 'unsupported' };
/** Org-membership tiers (`sys_member.role`: better-auth's closed set). */
export const ORG_MEMBERSHIP_LEVELS = ['owner', 'admin', 'member'] as const;
/**
* The CLOSED root set an `expression` approver may reference (#3447 P2).
* Mirrored by `APPROVER_EXPRESSION_ROOTS` in `plugin-approvals` and the
* `approval-expression-invalid` lint, so what lints clean is what runs.
*/
export const APPROVER_EXPRESSION_ROOTS = ['current', 'trigger', 'vars'] as const;
/**
* The per-type value bindings. `satisfies` keeps this exhaustive: adding an
* {@link ApproverType} member without declaring how its value is sourced is a
* compile error, so the designer contract can never silently lag the enum.
*/
export const APPROVER_VALUE_BINDINGS = {
user: { source: 'record', object: 'sys_user', valueField: 'id' },
org_membership_level: { source: 'enum', values: ORG_MEMBERSHIP_LEVELS },
// Deprecated alias — same binding as its canonical spelling so a stored
// legacy row still renders with the right control during its window.
role: { source: 'enum', values: ORG_MEMBERSHIP_LEVELS },
position: { source: 'record', object: 'sys_position', valueField: 'name' },
team: { source: 'record', object: 'sys_team', valueField: 'id' },
department: { source: 'record', object: 'sys_business_unit', valueField: 'id' },
manager: { source: 'auto' },
field: { source: 'trigger-field' },
queue: { source: 'unsupported' },
expression: { source: 'expression', roots: APPROVER_EXPRESSION_ROOTS },
} as const satisfies Record<z.infer<typeof ApproverType>, ApproverValueBinding>;
// ==========================================================================
// Approval as a Flow Node (ADR-0019, canonical)
// ==========================================================================
//
// ADR-0019 collapsed the standalone approval *authoring* type into Flow. An
// approval is now authored as a flow with one or more **Approval nodes**
// (`type: 'approval'`); the engine rides the node's durable pause. The former
// process-level concepts re-home as:
// - `steps` → successive Approval nodes on the canvas
// - `entryCriteria` → the condition on the edge entering the node
// - `onApprove`/`onReject` → the nodes wired to the node's `approve`/`reject` edges
// - `rejectionBehavior: back_to_previous` → a back-edge to an earlier node
// - `lockRecord` / `approvalStatusField` / `escalation` / `behavior` / approvers
// → {@link ApprovalNodeConfigSchema} node config
// The process-driven schemas (ApprovalProcessSchema / ApprovalStepSchema /
// ApprovalActionSchema) were removed in ADR-0019 P4.
/**
* Registry node type for the Approval node. The `plugin-approvals` package
* registers an executor under this type (ADR-0018), so an approval rides the
* one flow engine as a durable-pause node rather than a second engine.
*/
export const APPROVAL_NODE_TYPE = 'approval' as const;
/**
* Canonical decisions an Approval node emits. The engine selects the
* downstream branch by matching these against out-edge `label`s
* (see {@link ApprovalNodeConfigSchema}).
*/
export const ApprovalDecision = z.enum(['approve', 'reject']);
export type ApprovalDecision = z.infer<typeof ApprovalDecision>;
/**
* Edge labels an Approval node's out-edges use to declare which branch a
* decision follows. `resume(runId, { branchLabel })` passes the matching
* label so the engine continues down the right edge.
*/
export const APPROVAL_BRANCH_LABELS = {
approve: 'approve',
reject: 'reject',
/**
* ADR-0044 send-back-for-revision: the request finalizes `returned` and the
* flow walks this edge to a wait point where the submitter reworks the
* record; a later resubmit re-enters the approval node via a declared
* back-edge (round N+1).
*/
revise: 'revise',
/**
* ADR-0044: informational label a resubmit resume passes so the wait node's
* out-edge selection is explicit when authors label the back-edge.
*/
resubmit: 'resubmit',
} as const;
/** A single approver assignment on an Approval node. */
export const ApprovalNodeApproverSchema = lazySchema(() => z.object({
// `xEnumDeprecated` lists enum members that still PARSE but must not be
// offered for new authoring. Without it the Studio designer derives its
// approver-type dropdown straight from this enum and keeps offering `role`
// — the exact trap ADR-0090 D3 is retiring — one click away from `position`
// — and `queue`, which the runtime never resolves (#3508). Renderers omit
// these from pickers while still rendering a stored value.
type: ApproverType.meta({
xEnumDeprecated: [...NON_AUTHORABLE_APPROVER_TYPES],
}),
/**
* The approver reference, interpreted per `type`: a user id (`user`), a
* membership tier — owner/admin/member (`org_membership_level`), a position
* machine name (`position`), team/department id (`team`/`department`), field
* name holding a user id (`field`), or queue id (`queue`). Omitted for
* `manager` (resolved from the submitter's `manager_id`).
*/
// `xRef` marks this string as a *polymorphic* typed reference (ADR-0018
// §configSchema): the concrete picker follows the sibling `type` column.
// How each kind is BACKED (a data-record lookup on a directory object, a
// closed enum, an auto-resolved value, a trigger-object field) is declared
// once in {@link APPROVER_VALUE_BINDINGS} — designers must source the
// record-backed kinds from the DATA API, not the metadata registry (#3508).
// A single `.meta()` carries both description and annotation.
//
// The `role` → `org-membership-level` picker kind is the deprecated alias's
// entry: it maps to the SAME picker as the canonical spelling, so a stored
// legacy node still renders correctly for its deprecation window. `manager`
// maps to a picker kind that renders as "auto-resolved" (no free text);
// `queue` stays mapped so stored rows keep rendering even though the type
// is no longer authorable (see NON_AUTHORABLE_APPROVER_TYPES).
// `expression` is intentionally ABSENT from the map: an unmapped type keeps
// the value as free text, so the designer renders a plain input for the CEL
// source until a dedicated expression editor lands (objectui follow-up).
value: z.string().optional().meta({
description: 'User id / membership tier / position / team / department / field — per `type`; '
+ 'for `expression`, a CEL expression over `current.*` / `trigger.*` / `vars.*`',
xRef: {
kindFrom: 'type',
objectSource: '$trigger',
map: {
user: 'user',
org_membership_level: 'org-membership-level',
role: 'org-membership-level',
position: 'position',
team: 'team',
department: 'department',
manager: 'manager',
field: 'object-field',
queue: 'queue',
},
},
}),
/**
* #3447 P2, `expression` approvers only: how the expression's resolved values
* are turned into people. `user` (default) treats each value as a user id.
* `department` / `position` / `team` treat each value as that kind of id and
* expand it through the same graph lookups the static approver types use —
* e.g. an expression yielding department ids + `resolveAs: 'department'`
* fans out into every member of every returned department. With
* `behavior: 'per_group'`, each intermediate value forms its own group (one
* sign-off per returned department), keyed by that value.
*/
resolveAs: z.enum(['user', 'department', 'position', 'team']).optional()
.describe("How an `expression` result is expanded into approvers (default 'user')"),
/**
* Optional group label (#3266). With `behavior: 'per_group'`, approvers that
* share a label form one group and the node advances only once EACH group has
* `minApprovals` approvals — e.g. one legal AND one finance sign-off. Ignored
* by other behaviors. Approvers without a label each form their own group
* (keyed by position), so a plain per-approver list still behaves sensibly.
*/
group: z.string().optional().describe('Group label for per_group sign-off (e.g. "legal", "finance")'),
}));
export type ApprovalNodeApprover = z.infer<typeof ApprovalNodeApproverSchema>;
/**
* A TYPED decision-output declaration (#3447 P2 follow-up). The bare-string
* form of a `decisionOutputs` entry renders as free text; this form tells the
* decision UI which picker to render and whether to collect one id or many.
* The runtime treats `key` as the whitelist entry either way — `type` and
* `multiple` only shape the INPUT WIDGET, never the accepted value.
*/
export const DecisionOutputDefSchema = lazySchema(() => z.object({
/** The output key — what the flow receives as `<nodeId>.<key>`. */
key: z.string().min(1).describe('Output key (the flow variable name under the node id)'),
/** Display label for the decision-dialog field; defaults to a title-cased key. */
label: z.string().optional().describe('Field label in the decision dialog'),
/**
* Input widget: `text` (default — free text), or a record picker —
* `user` (sys_user), `department` (sys_business_unit), `position`
* (sys_position), `team` (sys_team). Picker values are record ids.
*/
type: z.enum(['text', 'user', 'department', 'position', 'team']).optional()
.describe("Decision-dialog input widget (default 'text')"),
/** Collect an id array instead of a single value (multi-select picker). */
multiple: z.boolean().optional().describe('Collect multiple values (id array)'),
}));
export type DecisionOutputDef = z.infer<typeof DecisionOutputDefSchema>;
/**
* Normalize a `decisionOutputs` array (bare keys and typed declarations mixed)
* into the typed form. The ONE place both sides read the union: the runtime
* whitelists `normalizeDecisionOutputs(...).map(d => d.key)`, the request read
* surfaces the normalized defs for the decision UI — so a bare key and
* `{ key }` can never behave differently.
*/
export function normalizeDecisionOutputs(
declared: unknown,
): DecisionOutputDef[] {
if (!Array.isArray(declared)) return [];
const out: DecisionOutputDef[] = [];
for (const entry of declared) {
if (typeof entry === 'string') {
if (entry) out.push({ key: entry });
} else if (entry && typeof entry === 'object' && typeof (entry as any).key === 'string' && (entry as any).key) {
const e = entry as Record<string, unknown>;
out.push({
key: String(e.key),
...(typeof e.label === 'string' && e.label ? { label: e.label } : {}),
...(typeof e.type === 'string' && e.type !== 'text' ? { type: e.type as DecisionOutputDef['type'] } : {}),
...(e.multiple === true ? { multiple: true } : {}),
});
}
}
return out;
}
/**
* Per-node SLA escalation — carried on the Approval node itself, so each
* Approval step on the canvas defines its own SLA.
*/
export const ApprovalEscalationSchema = lazySchema(() => z.object({
enabled: z.boolean().default(false).describe('Enable SLA-based escalation for this node'),
timeoutHours: z.number().min(1).describe('Hours before escalation triggers'),
action: z.enum(['reassign', 'auto_approve', 'auto_reject', 'notify']).default('notify')
.describe('Action on escalation timeout'),
// Escalation hands the request to a position (the common case — e.g. an
// approvals supervisor); the Studio designer renders a position picker, but
// free text is still accepted for a specific user id. The engine expands a
// position machine name to its holders via `sys_user_position` (ADR-0090
// D3) and falls back to treating the value as a user id when nobody holds
// it. NOT a better-auth membership tier — same contract as ApproverType.
escalateTo: z.string().optional().meta({
description: 'User id or position machine name to escalate to',
xRef: { kind: 'position' },
}),
notifySubmitter: z.boolean().default(true).describe('Notify the original submitter on escalation'),
}));
export type ApprovalEscalation = z.infer<typeof ApprovalEscalationSchema>;
/**
* Config for an **Approval node** (`type: 'approval'`) on a flow — the ADR-0019
* replacement for an {@link ApprovalStepSchema}. The node opens an approval
* request on entry, suspends the run, and resumes down its `approve` / `reject`
* out-edge once a decision is recorded.
*
* What does NOT live here (re-homed to the flow graph, by design):
* - **entry criteria** → the condition on the edge entering this node
* - **on-approve / on-reject actions** → the nodes wired to the
* `approve` / `reject` out-edges
* - **back-to-previous rejection** → a back-edge to an earlier node
*
* Approval *state* (request/action rows, record lock, status mirror) remains
* first-class engine-adjacent state owned by `plugin-approvals`; this config
* only describes how the node behaves.
*/
export const ApprovalNodeConfigSchema = lazySchema(() => z.object({
/** Who may act on this step. */
approvers: z.array(ApprovalNodeApproverSchema).min(1).describe('Allowed approvers for this node'),
/**
* How multiple approvers combine (#3266):
* - `first_response` — any one approval (or rejection) finalizes the node.
* - `unanimous` — every resolved approver must approve.
* - `quorum` — `minApprovals` of N approvals finalize (M-of-N collective sign-off).
* - `per_group` — each `group` (see approver `group`) must reach `minApprovals`
* approvals (default 1), i.e. one-from-each-group sign-off (会签).
* In every mode a single rejection finalizes the node as `rejected` (one veto).
* Weighted voting and approval-matrix governance are enterprise
* (objectstack-ai/cloud#861), not here.
*/
behavior: z.enum(['first_response', 'unanimous', 'quorum', 'per_group']).default('first_response')
.describe('How to combine multiple approvers'),
/**
* Threshold for `quorum` (total approvals required, M of N) and `per_group`
* (approvals required from EACH group). Defaults to 1. Clamped at runtime so
* it can never exceed the resolvable approver count (no deadlock).
*/
minApprovals: z.number().int().min(1).optional()
.describe('Approvals required — total (quorum) or per group (per_group). Default 1'),
/** Lock the triggering record from edits while this node is pending. */
lockRecord: z.boolean().default(true).describe('Lock the record from editing while pending'),
/**
* Field on the business object to mirror the request status onto
* (`pending`/`approved`/`rejected`/`recalled`). Should be readonly on the
* object. Omitted ⇒ status is exposed only via `sys_approval_request`.
*/
approvalStatusField: z.string().optional()
// `xRef` marks this string as a typed reference (ADR-0018 §configSchema):
// the Studio designer renders an object-field picker instead of free text.
// `objectSource: '$trigger'` resolves the field catalog from the flow's
// trigger object (the record this approval acts on). A single `.meta()`
// carries both description and the annotation so neither is dropped.
.meta({
description: 'Business-object field to mirror request status onto',
xRef: { kind: 'object-field', objectSource: '$trigger' },
}),
/**
* #3447 P2: what happens when approver resolution yields NO concrete person
* at node entry (an empty expression result, an unstaffed position, an empty
* multi-select field, …):
* - `admin_rescue` (default) — open the request anyway and warn loudly; a
* privileged admin can take it over via Reassign (#3424). The only option
* that neither waves the record through nor kills the run — approval's job
* is to gatekeep, so an empty slate must never silently pass.
* - `fail` — the node fails (fault edge / run failure). Choose when an empty
* slate can only mean a configuration bug.
* - `auto_approve` — skip the request and continue down the `approve` edge
* with `output.autoApproved = true`. The DingTalk/Feishu default; opt-in
* here because it silently waves the record through.
*/
onEmptyApprovers: z.enum(['admin_rescue', 'fail', 'auto_approve']).default('admin_rescue')
.describe('Behavior when no concrete approver resolves at node entry'),
/**
* #3447 P2: keys a decision may carry as structured outputs
* (`decide(..., { outputs })`). The AUTHOR declares the keys; approvers only
* fill values — the same trust model as a `screen` node's author-defined
* fields. Accepted outputs resume the run as `<nodeId>.<key>` flow variables
* (never bare names, so an approver can't shadow author variables), where a
* later node's `expression` approver can read them
* (`vars.<nodeId>.picked_departments`) — "the previous approver picks the
* next step's approvers" without a record-field detour. A decision carrying
* undeclared keys is rejected; `decision` / `requestId` are reserved.
*
* An entry is either a bare key (a plain text input in the decision UI) or a
* TYPED declaration — `{ key, type, multiple }` — that tells the decision UI
* to render a record picker instead of free text: `user` → a sys_user
* picker, `department`/`position`/`team` → the matching system-object
* picker. `multiple: true` collects an id ARRAY (the natural fan-out shape
* for an `expression` approver downstream). The type describes the INPUT
* WIDGET only — the runtime accepts whatever value shape arrives (single id,
* id[], CSV) and hands it to the flow verbatim.
*/
decisionOutputs: z.array(z.union([
z.string(),
DecisionOutputDefSchema,
])).optional()
.describe('Author-declared decision outputs — bare keys or typed { key, type, multiple } declarations'),
/** Optional per-node SLA escalation. */
escalation: ApprovalEscalationSchema.optional().describe('Per-node SLA escalation'),
/**
* ADR-0044: maximum send-backs-for-revision per (run, node). A send-back
* that would exceed the budget auto-rejects instead (the run resumes down
* the `reject` edge with `output.autoRejected = true`), so instances cannot
* orbit the revise loop forever. `0` disables send-back (always
* auto-rejects). Only meaningful when the node has a `revise` out-edge.
*/
maxRevisions: z.number().int().min(0).default(3)
.describe('Max send-backs for revision before auto-reject (0 = send-back disabled)'),
}));
export type ApprovalNodeConfig = z.infer<typeof ApprovalNodeConfigSchema>;
/**
* JSON Schema for {@link ApprovalNodeConfigSchema}, memoized.
*
* Published on the Approval action descriptor's `configSchema`
* (ADR-0018/0019) so the **engine** is the single source of truth for the
* node's config contract: the Studio flow designer renders the Approval node's
* property form from this schema (rather than a hardcoded client form), and the
* same schema backs `registerFlow()` config validation. Derived with Zod v4's
* `z.toJSONSchema` in `input` mode (the author-facing shape — default-bearing
* fields are optional). Lazily computed so the wrapped schema is only resolved
* when a descriptor is actually built.
*/
let cachedApprovalNodeConfigJsonSchema: unknown;
export function getApprovalNodeConfigJsonSchema(): unknown {
if (cachedApprovalNodeConfigJsonSchema === undefined) {
cachedApprovalNodeConfigJsonSchema = z.toJSONSchema(ApprovalNodeConfigSchema, {
target: 'draft-2020-12',
io: 'input',
// Approval config has no unrepresentable constructs today; keep the
// designer resilient if one is ever added rather than throwing at boot.
unrepresentable: 'any',
});
}
return cachedApprovalNodeConfigJsonSchema;
}