@classytic/arc-approval 0.1.0
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/dist/index.d.mts +137 -0
- package/dist/index.mjs +249 -0
- package/package.json +49 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ApprovalChain, DecisionInput } from "@classytic/primitives/approval";
|
|
2
|
+
import { PermissionCheck } from "@classytic/arc/permissions";
|
|
3
|
+
import { ActionDefinition, RequestWithExtras } from "@classytic/arc/types";
|
|
4
|
+
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
/** Minimum surface every approval-bearing subject doc exposes (P7). */
|
|
7
|
+
interface ApprovableDoc {
|
|
8
|
+
readonly _id: unknown;
|
|
9
|
+
readonly approvals?: ApprovalChain | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Structural repository port — satisfied by any kit repository. The preset
|
|
13
|
+
* only reads one doc and patches one doc; it takes no dependency on how.
|
|
14
|
+
* `organizationId` is absent when the preset runs with `tenancy: 'off'`
|
|
15
|
+
* (single-tenant hosts) — kits without a tenant plugin simply ignore it.
|
|
16
|
+
*/
|
|
17
|
+
interface ApprovalSubjectRepository<TDoc extends ApprovableDoc> {
|
|
18
|
+
getById(id: string, options: {
|
|
19
|
+
organizationId?: string;
|
|
20
|
+
throwOnNotFound: false;
|
|
21
|
+
lean: true;
|
|
22
|
+
}): Promise<TDoc | null>;
|
|
23
|
+
update(id: string, patch: Record<string, unknown>, options: {
|
|
24
|
+
organizationId?: string;
|
|
25
|
+
lean: true;
|
|
26
|
+
}): Promise<TDoc | null>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Context the policy resolver matches against — host-extensible.
|
|
30
|
+
* `branchId` is the tenant dimension when the host is multi-tenant; optional
|
|
31
|
+
* so single-tenant hosts (`tenancy: 'off'`) can resolve policies without one.
|
|
32
|
+
* (Name kept for API stability with existing consumers; the generic rename
|
|
33
|
+
* ships with the preset's extraction to its own package.)
|
|
34
|
+
*/
|
|
35
|
+
interface ApprovalEvaluationContext {
|
|
36
|
+
readonly branchId?: string;
|
|
37
|
+
readonly amount?: number;
|
|
38
|
+
readonly category?: string;
|
|
39
|
+
readonly [extra: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
/** A chain resolved from an approval-policy matrix, with provenance. */
|
|
42
|
+
interface ResolvedApprovalChain {
|
|
43
|
+
readonly chain: ApprovalChain;
|
|
44
|
+
readonly policyId: string | null;
|
|
45
|
+
readonly policyVersion: number | null;
|
|
46
|
+
}
|
|
47
|
+
interface PersistContext {
|
|
48
|
+
/** Absent under `tenancy: 'off'` (single-tenant hosts). */
|
|
49
|
+
readonly organizationId?: string;
|
|
50
|
+
readonly actorId: string | null;
|
|
51
|
+
readonly requestId: string | undefined;
|
|
52
|
+
}
|
|
53
|
+
interface WithApprovalChainConfig<TDoc extends ApprovableDoc, TSubject extends string = string> {
|
|
54
|
+
/** Stable subject identifier — must match the host's ApprovalPolicy.subjectType. */
|
|
55
|
+
readonly subjectType: TSubject;
|
|
56
|
+
readonly repository: ApprovalSubjectRepository<TDoc>;
|
|
57
|
+
/** Statuses in which `submit_for_approval` is accepted. Omit → any state. */
|
|
58
|
+
readonly allowedSubmitStatus?: readonly string[];
|
|
59
|
+
/** Reads the lifecycle status off the doc. Default reads `status`. */
|
|
60
|
+
readonly getStatus?: (doc: TDoc) => string | undefined;
|
|
61
|
+
readonly permissions: {
|
|
62
|
+
readonly submit: PermissionCheck;
|
|
63
|
+
readonly decide: PermissionCheck;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Delegation / identity-mapping hook for `decide`.
|
|
67
|
+
*
|
|
68
|
+
* By default a decision is recorded as the AUTHENTICATED actor:
|
|
69
|
+
* `approverId` defaults to the actor id, and a caller-supplied
|
|
70
|
+
* `approverId` that differs from it is rejected with 403
|
|
71
|
+
* `approval.approver_mismatch` — otherwise anyone holding the broad
|
|
72
|
+
* `decide` permission could spoof `{ approverId: 'boss' }` and decide as
|
|
73
|
+
* any approver listed in the chain.
|
|
74
|
+
*
|
|
75
|
+
* Provide this hook to authorize the mismatch explicitly: delegation
|
|
76
|
+
* ("decide on behalf of"), or hosts whose chain approver ids are NOT auth
|
|
77
|
+
* user ids (employee-profile ids, role ids) and need an identity mapping.
|
|
78
|
+
* Return `true` to allow `actorId` to decide as `approverId`.
|
|
79
|
+
*/
|
|
80
|
+
readonly canActAsApprover?: (actorId: string | null, approverId: string, req: RequestWithExtras) => boolean | Promise<boolean>;
|
|
81
|
+
/**
|
|
82
|
+
* Tenancy mode. `'required'` (default) rejects requests without an org
|
|
83
|
+
* context with 400 `approval.no_organization_context` and threads
|
|
84
|
+
* `organizationId` into every repository call. `'off'` skips the org
|
|
85
|
+
* requirement entirely — for single-tenant hosts.
|
|
86
|
+
*/
|
|
87
|
+
readonly tenancy?: "required" | "off";
|
|
88
|
+
/**
|
|
89
|
+
* Policy-provenance persistence. `'persist'` (default) writes
|
|
90
|
+
* `approvalPolicyId` / `approvalPolicyVersion` onto the subject patch;
|
|
91
|
+
* subjects whose schema omits those fields rely on the storage kit's
|
|
92
|
+
* unknown-field policy (Mongoose strict mode DROPS them; a kit that throws
|
|
93
|
+
* on unknown fields must use `'omit'`). `'omit'` never writes them.
|
|
94
|
+
*/
|
|
95
|
+
readonly provenance?: "persist" | "omit";
|
|
96
|
+
/** Required for `useMatrix: true` — builds the policy evaluation context. */
|
|
97
|
+
readonly toEvaluationContext?: (doc: TDoc) => ApprovalEvaluationContext;
|
|
98
|
+
/** Pluggable matrix resolver (host policy). Absent → literal chains only. */
|
|
99
|
+
readonly resolveChain?: (subjectType: TSubject, evalCtx: ApprovalEvaluationContext) => Promise<ResolvedApprovalChain | null>;
|
|
100
|
+
/** Coarse-status side effects — return a doc to make it the response. */
|
|
101
|
+
readonly onSubmitted?: (doc: TDoc, ctx: PersistContext) => Promise<TDoc | void>;
|
|
102
|
+
readonly onApproved?: (doc: TDoc, ctx: PersistContext) => Promise<TDoc | void>;
|
|
103
|
+
readonly onRejected?: (doc: TDoc, input: DecisionInput, ctx: PersistContext) => Promise<TDoc | void>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build the `{ submit_for_approval, decide }` slice of a resource's
|
|
107
|
+
* `actions:` map. Spread into the resource's actions block:
|
|
108
|
+
*
|
|
109
|
+
* ```ts
|
|
110
|
+
* actions: {
|
|
111
|
+
* ...withApprovalChain({ subjectType: 'budget', repository, permissions, … }),
|
|
112
|
+
* post: { … }, // subject-specific verbs stay here
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
declare function withApprovalChain<TDoc extends ApprovableDoc, TSubject extends string = string>(config: WithApprovalChainConfig<TDoc, TSubject>): Record<"submit_for_approval" | "decide", ActionDefinition>;
|
|
117
|
+
/** Standalone name for the `canActAsApprover` hook shape. */
|
|
118
|
+
type CanActAsApprover = (actorId: string | null, approverId: string, req: RequestWithExtras) => boolean | Promise<boolean>;
|
|
119
|
+
/**
|
|
120
|
+
* `canActAsApprover` policy: callers holding one of the given PLATFORM roles
|
|
121
|
+
* (`req.user.role`, string or array) may decide as any chain approver.
|
|
122
|
+
*
|
|
123
|
+
* The operational escape hatch for role-expanded matrix chains — upstreamed
|
|
124
|
+
* from be-prod's `platformAdminMayDelegate` shim. The audit trail still
|
|
125
|
+
* records the real actor (`PersistContext.actorId`); this only authorizes the
|
|
126
|
+
* decision to be RECORDED against the named approver:
|
|
127
|
+
*
|
|
128
|
+
* ```ts
|
|
129
|
+
* withApprovalChain({
|
|
130
|
+
* // …
|
|
131
|
+
* canActAsApprover: delegateForRoles(["admin", "superadmin"]),
|
|
132
|
+
* });
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
declare function delegateForRoles(roles: readonly string[]): CanActAsApprover;
|
|
136
|
+
//#endregion
|
|
137
|
+
export { ApprovableDoc, ApprovalEvaluationContext, ApprovalSubjectRepository, CanActAsApprover, PersistContext, ResolvedApprovalChain, WithApprovalChainConfig, delegateForRoles, withApprovalChain };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { getOrgId, getUserId } from "@classytic/arc/scope";
|
|
2
|
+
import { NotFoundError, ValidationError, createDomainError } from "@classytic/arc/utils";
|
|
3
|
+
import { ApprovalError, applyDecision, createChain, isApproved, isRejected } from "@classytic/primitives/approval";
|
|
4
|
+
//#region src/index.ts
|
|
5
|
+
const STATUS_BY_CODE = {
|
|
6
|
+
EMPTY_STEPS: 400,
|
|
7
|
+
DUPLICATE_STEP_ID: 400,
|
|
8
|
+
EMPTY_APPROVERS: 400,
|
|
9
|
+
DUPLICATE_APPROVER_ID: 400,
|
|
10
|
+
INVALID_QUORUM: 400,
|
|
11
|
+
UNKNOWN_STEP: 422,
|
|
12
|
+
UNAUTHORIZED_APPROVER: 403,
|
|
13
|
+
STEP_NOT_ACTIVE: 422,
|
|
14
|
+
STEP_ALREADY_DECIDED_BY_APPROVER: 409
|
|
15
|
+
};
|
|
16
|
+
function rethrowApprovalError(fn) {
|
|
17
|
+
try {
|
|
18
|
+
return fn();
|
|
19
|
+
} catch (err) {
|
|
20
|
+
if (err instanceof ApprovalError) {
|
|
21
|
+
const status = STATUS_BY_CODE[err.code] ?? 422;
|
|
22
|
+
throw createDomainError(`approval.${err.code.toLowerCase()}`, err.message, status);
|
|
23
|
+
}
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build the `{ submit_for_approval, decide }` slice of a resource's
|
|
29
|
+
* `actions:` map. Spread into the resource's actions block:
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* actions: {
|
|
33
|
+
* ...withApprovalChain({ subjectType: 'budget', repository, permissions, … }),
|
|
34
|
+
* post: { … }, // subject-specific verbs stay here
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
function withApprovalChain(config) {
|
|
39
|
+
return {
|
|
40
|
+
submit_for_approval: {
|
|
41
|
+
handler: async (id, data, req) => submitHandler(config, id, data, req),
|
|
42
|
+
permissions: config.permissions.submit,
|
|
43
|
+
description: `Attach an approval chain to a ${config.subjectType}. Pass either a literal \`chain\` or \`useMatrix: true\` to resolve from policy.`,
|
|
44
|
+
schema: {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
useMatrix: {
|
|
48
|
+
type: "boolean",
|
|
49
|
+
description: "Resolve chain from ApprovalPolicy matrix."
|
|
50
|
+
},
|
|
51
|
+
chain: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
order: {
|
|
55
|
+
type: "string",
|
|
56
|
+
enum: ["sequential", "parallel"]
|
|
57
|
+
},
|
|
58
|
+
steps: {
|
|
59
|
+
type: "array",
|
|
60
|
+
minItems: 1,
|
|
61
|
+
items: {
|
|
62
|
+
type: "object",
|
|
63
|
+
required: ["id", "approvers"],
|
|
64
|
+
properties: {
|
|
65
|
+
id: {
|
|
66
|
+
type: "string",
|
|
67
|
+
minLength: 1
|
|
68
|
+
},
|
|
69
|
+
name: { type: "string" },
|
|
70
|
+
requiredApprovals: {
|
|
71
|
+
type: "integer",
|
|
72
|
+
minimum: 1
|
|
73
|
+
},
|
|
74
|
+
approvers: {
|
|
75
|
+
type: "array",
|
|
76
|
+
minItems: 1,
|
|
77
|
+
items: {
|
|
78
|
+
type: "object",
|
|
79
|
+
required: ["id"],
|
|
80
|
+
properties: {
|
|
81
|
+
id: {
|
|
82
|
+
type: "string",
|
|
83
|
+
minLength: 1
|
|
84
|
+
},
|
|
85
|
+
name: { type: "string" },
|
|
86
|
+
role: { type: "string" }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
additionalProperties: false
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
decide: {
|
|
100
|
+
handler: async (id, data, req) => decideHandler(config, id, data, req),
|
|
101
|
+
permissions: config.permissions.decide,
|
|
102
|
+
description: `Apply a single approver decision to the ${config.subjectType}'s active chain step.`,
|
|
103
|
+
schema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
required: ["stepId", "decision"],
|
|
106
|
+
properties: {
|
|
107
|
+
stepId: {
|
|
108
|
+
type: "string",
|
|
109
|
+
minLength: 1
|
|
110
|
+
},
|
|
111
|
+
approverId: {
|
|
112
|
+
type: "string",
|
|
113
|
+
minLength: 1,
|
|
114
|
+
description: "Defaults to the authenticated actor. A different id requires the host's canActAsApprover hook to authorize it (403 otherwise)."
|
|
115
|
+
},
|
|
116
|
+
decision: {
|
|
117
|
+
type: "string",
|
|
118
|
+
enum: ["approved", "rejected"]
|
|
119
|
+
},
|
|
120
|
+
note: {
|
|
121
|
+
type: "string",
|
|
122
|
+
maxLength: 1e3
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
additionalProperties: false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
async function submitHandler(config, id, body, req) {
|
|
131
|
+
const ctx = persistContext(req, config.tenancy ?? "required");
|
|
132
|
+
const doc = await loadSubject(config, id, ctx);
|
|
133
|
+
if (config.allowedSubmitStatus) {
|
|
134
|
+
const current = readStatus(config, doc);
|
|
135
|
+
if (!current || !config.allowedSubmitStatus.includes(current)) throw createDomainError("approval.invalid_status_for_submit", `Cannot submit ${config.subjectType} for approval in status '${current ?? "unknown"}'. Must be ${config.allowedSubmitStatus.join(" or ")}.`, 422);
|
|
136
|
+
}
|
|
137
|
+
const existingChain = doc.approvals;
|
|
138
|
+
if (existingChain && existingChain.status !== "rejected") throw createDomainError("approval.chain_already_attached", `Approval chain is already attached and ${existingChain.status}. Cancel or wait for the current workflow first.`, 409);
|
|
139
|
+
let chain;
|
|
140
|
+
let policyId = null;
|
|
141
|
+
let policyVersion = null;
|
|
142
|
+
if (body.useMatrix) {
|
|
143
|
+
if (!config.resolveChain || !config.toEvaluationContext) throw createDomainError("approval.matrix_unavailable", `Matrix-driven submit not configured for ${config.subjectType}. Provide a literal 'chain' instead.`, 422);
|
|
144
|
+
const resolved = await config.resolveChain(config.subjectType, config.toEvaluationContext(doc));
|
|
145
|
+
if (!resolved) throw createDomainError("approval.no_matching_policy", `No approval policy matched for ${config.subjectType}. Configure one or pass a literal 'chain'.`, 422);
|
|
146
|
+
chain = resolved.chain;
|
|
147
|
+
policyId = resolved.policyId;
|
|
148
|
+
policyVersion = resolved.policyVersion;
|
|
149
|
+
} else {
|
|
150
|
+
const literal = body.chain;
|
|
151
|
+
if (!literal || typeof literal !== "object") throw new ValidationError("Either `chain` (literal) or `useMatrix: true` is required.");
|
|
152
|
+
chain = rethrowApprovalError(() => createChain(literal));
|
|
153
|
+
}
|
|
154
|
+
const patch = { approvals: chain };
|
|
155
|
+
if (config.provenance !== "omit") {
|
|
156
|
+
if (policyId) patch.approvalPolicyId = policyId;
|
|
157
|
+
if (policyVersion !== null) patch.approvalPolicyVersion = policyVersion;
|
|
158
|
+
}
|
|
159
|
+
const updated = await config.repository.update(id, patch, {
|
|
160
|
+
...ctx.organizationId ? { organizationId: ctx.organizationId } : {},
|
|
161
|
+
lean: true
|
|
162
|
+
});
|
|
163
|
+
if (!updated) throw new NotFoundError(config.subjectType);
|
|
164
|
+
if (config.onSubmitted) {
|
|
165
|
+
const refined = await config.onSubmitted(updated, ctx);
|
|
166
|
+
if (refined) return refined;
|
|
167
|
+
}
|
|
168
|
+
return updated;
|
|
169
|
+
}
|
|
170
|
+
async function decideHandler(config, id, body, req) {
|
|
171
|
+
const ctx = persistContext(req, config.tenancy ?? "required");
|
|
172
|
+
if (!body.stepId || !body.decision) throw new ValidationError("stepId and decision are required.");
|
|
173
|
+
const approverId = body.approverId ?? ctx.actorId;
|
|
174
|
+
if (!approverId) throw new ValidationError("approverId is required when the request has no authenticated actor.");
|
|
175
|
+
if (approverId !== ctx.actorId) {
|
|
176
|
+
if (!(config.canActAsApprover ? await config.canActAsApprover(ctx.actorId, approverId, req) : false)) throw createDomainError("approval.approver_mismatch", `Cannot decide as approver '${approverId}': decisions are recorded as the authenticated actor unless canActAsApprover authorizes the delegation.`, 403);
|
|
177
|
+
}
|
|
178
|
+
const existing = (await loadSubject(config, id, ctx)).approvals;
|
|
179
|
+
if (!existing) throw createDomainError("approval.no_chain_attached", `No approval chain attached to this ${config.subjectType}. Submit one first.`, 422);
|
|
180
|
+
const decisionInput = {
|
|
181
|
+
stepId: body.stepId,
|
|
182
|
+
approverId,
|
|
183
|
+
decision: body.decision,
|
|
184
|
+
...body.note !== void 0 ? { note: body.note } : {}
|
|
185
|
+
};
|
|
186
|
+
const updatedChain = rethrowApprovalError(() => applyDecision(existing, decisionInput));
|
|
187
|
+
const updated = await config.repository.update(id, { approvals: updatedChain }, {
|
|
188
|
+
...ctx.organizationId ? { organizationId: ctx.organizationId } : {},
|
|
189
|
+
lean: true
|
|
190
|
+
});
|
|
191
|
+
if (!updated) throw new NotFoundError(config.subjectType);
|
|
192
|
+
if (isApproved(updatedChain) && !isApproved(existing) && config.onApproved) {
|
|
193
|
+
const refined = await config.onApproved(updated, ctx);
|
|
194
|
+
if (refined) return refined;
|
|
195
|
+
} else if (isRejected(updatedChain) && !isRejected(existing) && config.onRejected) {
|
|
196
|
+
const refined = await config.onRejected(updated, decisionInput, ctx);
|
|
197
|
+
if (refined) return refined;
|
|
198
|
+
}
|
|
199
|
+
return updated;
|
|
200
|
+
}
|
|
201
|
+
function persistContext(req, tenancy) {
|
|
202
|
+
const organizationId = getOrgId(req.scope) ?? "";
|
|
203
|
+
if (!organizationId && tenancy === "required") throw createDomainError("approval.no_organization_context", "Organization context is required for this action.", 400);
|
|
204
|
+
const actorId = getUserId(req.scope) ?? req.user?._id ?? req.user?.id ?? null;
|
|
205
|
+
return {
|
|
206
|
+
...organizationId ? { organizationId } : {},
|
|
207
|
+
actorId,
|
|
208
|
+
requestId: req.id
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function readStatus(config, doc) {
|
|
212
|
+
if (config.getStatus) return config.getStatus(doc);
|
|
213
|
+
const maybe = doc.status;
|
|
214
|
+
return typeof maybe === "string" ? maybe : void 0;
|
|
215
|
+
}
|
|
216
|
+
async function loadSubject(config, id, ctx) {
|
|
217
|
+
const doc = await config.repository.getById(id, {
|
|
218
|
+
...ctx.organizationId ? { organizationId: ctx.organizationId } : {},
|
|
219
|
+
throwOnNotFound: false,
|
|
220
|
+
lean: true
|
|
221
|
+
});
|
|
222
|
+
if (!doc) throw new NotFoundError(`${config.subjectType} not found`);
|
|
223
|
+
return doc;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* `canActAsApprover` policy: callers holding one of the given PLATFORM roles
|
|
227
|
+
* (`req.user.role`, string or array) may decide as any chain approver.
|
|
228
|
+
*
|
|
229
|
+
* The operational escape hatch for role-expanded matrix chains — upstreamed
|
|
230
|
+
* from be-prod's `platformAdminMayDelegate` shim. The audit trail still
|
|
231
|
+
* records the real actor (`PersistContext.actorId`); this only authorizes the
|
|
232
|
+
* decision to be RECORDED against the named approver:
|
|
233
|
+
*
|
|
234
|
+
* ```ts
|
|
235
|
+
* withApprovalChain({
|
|
236
|
+
* // …
|
|
237
|
+
* canActAsApprover: delegateForRoles(["admin", "superadmin"]),
|
|
238
|
+
* });
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
function delegateForRoles(roles) {
|
|
242
|
+
const allowed = new Set(roles);
|
|
243
|
+
return (_actorId, _approverId, req) => {
|
|
244
|
+
const raw = req.user?.role;
|
|
245
|
+
return (Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : []).some((role) => typeof role === "string" && allowed.has(role));
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
export { delegateForRoles, withApprovalChain };
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@classytic/arc-approval",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Approval-chain action preset for @classytic/arc — submit_for_approval + decide with policy-matrix resolution, actor-derived decision identity, and delegation hooks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.mts",
|
|
9
|
+
"default": "./dist/index.mjs"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.mjs",
|
|
13
|
+
"types": "./dist/index.d.mts",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsdown",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "biome check src/",
|
|
28
|
+
"prepublishOnly": "node ../../scripts/prepublish-gate.mjs"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@classytic/arc": ">=2.20.0",
|
|
32
|
+
"@classytic/primitives": ">=0.9.0"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"arc",
|
|
36
|
+
"fastify",
|
|
37
|
+
"approval",
|
|
38
|
+
"approval-chain",
|
|
39
|
+
"workflow",
|
|
40
|
+
"classytic"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"author": "Classytic",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/classytic/arc-ecosystem.git",
|
|
47
|
+
"directory": "packages/arc-approval"
|
|
48
|
+
}
|
|
49
|
+
}
|