@classytic/arc 2.25.0 → 2.30.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/README.md +2 -0
- package/dist/{BaseController--M8EMzt0.mjs → BaseController-B5DwufbB.mjs} +15 -11
- package/dist/EventTransport-C6YA8xY9.mjs +103 -0
- package/dist/{HookSystem-C1vB_Gc9.mjs → HookSystem-WkIougo6.mjs} +15 -35
- package/dist/applyPermissionResult-Cf6zUCvb.mjs +257 -0
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +1 -1
- package/dist/{buildHandler-Dtwd8fdz.mjs → buildHandler-BYS3Vkfz.mjs} +32 -6
- package/dist/cleanup/index.d.mts +313 -0
- package/dist/cleanup/index.mjs +1267 -0
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +5 -3
- package/dist/core-poalJLvp.mjs +175 -0
- package/dist/{createActionRouter-B6GvAuAX.mjs → createActionRouter-BxuywVRB.mjs} +2 -2
- package/dist/{createAggregationRouter-wzwFa_dW.mjs → createAggregationRouter-BiY-lGdY.mjs} +7 -20
- package/dist/{preloadResources-DfyngIN9.mjs → createApp-CXrKFciH.mjs} +25 -76
- package/dist/defineAggregation-ufWjUwml.mjs +47 -0
- package/dist/{core-EUNu0u07.mjs → defineResource-CxWqDCAy.mjs} +36 -261
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/{eventPlugin-DrQ_FesK.mjs → eventPlugin-D2NNR4py.mjs} +2 -103
- package/dist/events/index.d.mts +28 -2
- package/dist/events/index.mjs +24 -343
- package/dist/factory/index.d.mts +4 -3
- package/dist/factory/index.mjs +3 -1
- package/dist/{fastify-augmentation-dPNBVeIj.d.mts → fastify-augmentation-0WhPnUH7.d.mts} +3 -2
- package/dist/{applyPermissionResult-Tw4iXtlf.mjs → fields-CQ_7KQWw.mjs} +1 -97
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.mjs +1 -1
- package/dist/index-CCzVR-N3.d.mts +107 -0
- package/dist/{index-BMnbzvtO.d.mts → index-Ch16aAah.d.mts} +201 -21
- package/dist/{index-f4PTrk3B.d.mts → index-Cx7bro4R.d.mts} +4 -4
- package/dist/{index-DPRH0Cme.d.mts → index-Q5gzEIWY.d.mts} +97 -66
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +8 -6
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +6 -6
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/{mcpPlugin-Cb7olw0j.mjs → mcpPlugin-nRzm5g2s.mjs} +36 -43
- package/dist/middleware/index.d.mts +1 -1
- package/dist/migrations/index.d.mts +0 -3
- package/dist/migrations/index.mjs +0 -3
- package/dist/module-_-M7C5ES.mjs +29 -0
- package/dist/{openapi-DrLtog_B.mjs → openapi-B8Vkbxbn.mjs} +1 -1
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +4 -3
- package/dist/{permissions-BrtiBKGs.mjs → permissions-Bfu_8y1U.mjs} +318 -289
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{pipeline-BN5G05rd.mjs → pipeline-BJnpkY9i.mjs} +4 -18
- package/dist/plugins/index.d.mts +4 -4
- package/dist/plugins/index.mjs +2 -2
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{preloadResources-0oe-JSSS.d.mts → preloadResources-DCfa2egz.d.mts} +3 -2
- package/dist/preloadResources-DSY5HGpS.mjs +45 -0
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +14 -2
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-DwFDVKP0.mjs → presets-CJOEXLuk.mjs} +25 -9
- package/dist/projection-UBDgfMwu.mjs +17 -0
- package/dist/{realtime-Cuw_deA-.mjs → realtime-B9nmIY5V.mjs} +2 -1
- package/dist/{registerRoute-v0Wb6Plt.mjs → registerRoute-JkxIEBJq.mjs} +3 -1
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/{registry-vcOCtPij.mjs → registry-BYXUUg21.mjs} +16 -1
- package/dist/repositoryFilter-Dz8k3S4P.mjs +44 -0
- package/dist/subscribe-helpers-DfzsivM6.mjs +339 -0
- package/dist/testing/cleanupStoreContract.d.mts +10 -0
- package/dist/testing/cleanupStoreContract.mjs +375 -0
- package/dist/testing/index.d.mts +34 -5
- package/dist/testing/index.mjs +316 -211
- package/dist/testing/outboxStoreContract.d.mts +13 -0
- package/dist/testing/outboxStoreContract.mjs +247 -0
- package/dist/types/index.d.mts +3 -3
- package/dist/{types-QFGc3Cjv.d.mts → types-Bmmn-fO0.d.mts} +1 -1
- package/dist/types-C_ZZRcD6.d.mts +450 -0
- package/dist/{types-Dmw-6Qeo.d.mts → types-ChvvVNP8.d.mts} +93 -88
- package/dist/{index-KUoxjIq1.d.mts → types-D_n8L6VY.d.mts} +33 -106
- package/dist/usage/index.d.mts +1 -1
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +2 -5
- package/package.json +539 -524
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { PurgeActor, PurgeEvidence, PurgeResourceResult, PurgeStrategyKind as PurgeStrategyKind$1, PurgeVerificationSummary } from "@classytic/primitives/retention";
|
|
2
|
+
//#region src/cleanup/types.d.ts
|
|
3
|
+
/** Ambient context every recipe method receives. Host-injected — never global. */
|
|
4
|
+
interface CleanupContext {
|
|
5
|
+
/** Who requested the operation. */
|
|
6
|
+
readonly actor: PurgeActor;
|
|
7
|
+
/** Injected clock — recipes never call `new Date()` directly (testability). */
|
|
8
|
+
readonly now: Date;
|
|
9
|
+
/** Cooperative cancellation — checked between chunks/steps. */
|
|
10
|
+
readonly signal?: AbortSignal | undefined;
|
|
11
|
+
/** Optional structured logger. */
|
|
12
|
+
readonly logger?: CleanupLogger | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Opaque host-provided ambient values (e.g. the resolved company/branch
|
|
15
|
+
* scope). MUST be JSON-serializable — it is persisted on the run so a worker
|
|
16
|
+
* (different process) can rebuild the exact operation context. Recipes read
|
|
17
|
+
* what they declared they need; the framework never inspects it.
|
|
18
|
+
*/
|
|
19
|
+
readonly ambient?: Readonly<Record<string, unknown>> | undefined;
|
|
20
|
+
}
|
|
21
|
+
interface CleanupLogger {
|
|
22
|
+
info(msg: string, meta?: Record<string, unknown>): void;
|
|
23
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
24
|
+
error(msg: string, meta?: Record<string, unknown>): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Execution-time context — adds progress reporting + the run/operation ids +
|
|
28
|
+
* cooperative cancellation.
|
|
29
|
+
*/
|
|
30
|
+
interface CleanupExecutionContext extends CleanupContext {
|
|
31
|
+
/** Durable run id this execution belongs to. */
|
|
32
|
+
readonly runId: string;
|
|
33
|
+
/** Stable operation id threaded through logs, audit, and evidence. */
|
|
34
|
+
readonly operationId: string;
|
|
35
|
+
/**
|
|
36
|
+
* Report one committed chunk. Updates the run's BOUNDED progress summary
|
|
37
|
+
* (counts / current resource / cursor / heartbeat) — it does NOT retain
|
|
38
|
+
* every step in memory. Also refreshes the cancellation view: after a chunk
|
|
39
|
+
* commits, call this so the executor learns of a cancel request promptly.
|
|
40
|
+
*/
|
|
41
|
+
onStep(step: CleanupStepResult): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Throw `CleanupCancelled` if a cancel has been requested for this run. Cheap
|
|
44
|
+
* to call between chunks; backed by the durable `cancelRequested` flag (the
|
|
45
|
+
* source of truth) plus the in-process `signal`.
|
|
46
|
+
*/
|
|
47
|
+
throwIfCancelled(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
/** Whether a recipe may run right now, and why not. */
|
|
50
|
+
interface Availability {
|
|
51
|
+
readonly available: boolean;
|
|
52
|
+
/** Required when `available` is false — surfaced to the operator. */
|
|
53
|
+
readonly reason?: string | undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Operator-supplied recipe parameters (branch, module set, before-date, …). */
|
|
56
|
+
interface CleanupInput {
|
|
57
|
+
readonly parameters?: Readonly<Record<string, unknown>> | undefined;
|
|
58
|
+
}
|
|
59
|
+
/** One logical line in the preview — a business record class, never a collection. */
|
|
60
|
+
interface CleanupPlanItem {
|
|
61
|
+
/** Logical module / resource label, e.g. `'orders'`, `'journal entries'`. */
|
|
62
|
+
readonly resource: string;
|
|
63
|
+
/** Estimated records affected. */
|
|
64
|
+
readonly estimated: number;
|
|
65
|
+
/** What is retained for this line (e.g. `'measures kept, PII redacted'`). */
|
|
66
|
+
readonly retained?: string | undefined;
|
|
67
|
+
/** Domain blockers preventing this line (e.g. `'OPEN_TRANSFER'`). */
|
|
68
|
+
readonly blockers?: readonly string[] | undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A recipe's plan BEFORE the framework seals it — the recipe returns this; the
|
|
72
|
+
* service computes {@link CleanupPlan.digest} + stamps {@link CleanupPlan.recipeId}.
|
|
73
|
+
*/
|
|
74
|
+
interface CleanupPlanDraft {
|
|
75
|
+
readonly items: readonly CleanupPlanItem[];
|
|
76
|
+
/** Top-level categories the recipe explicitly RETAINS. */
|
|
77
|
+
readonly retains?: readonly string[] | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* Top-level blockers. A NON-EMPTY blockers list is a HARD STOP — the service
|
|
80
|
+
* refuses `execute` with `CLEANUP_BLOCKED`. Preview surfaces them so the
|
|
81
|
+
* operator resolves them (or the recipe stops listing them) before running.
|
|
82
|
+
*/
|
|
83
|
+
readonly blockers?: readonly string[] | undefined;
|
|
84
|
+
/** Projection/scaffolding rebuilds the recipe will perform after cleanup. */
|
|
85
|
+
readonly rebuildActions?: readonly string[] | undefined;
|
|
86
|
+
/** Non-blocking warnings for the operator. */
|
|
87
|
+
readonly warnings?: readonly string[] | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* The exact phrase the operator must type to confirm a destructive run.
|
|
90
|
+
* Defaults (in the service) to the recipe id when omitted.
|
|
91
|
+
*/
|
|
92
|
+
readonly confirmationPhrase?: string | undefined;
|
|
93
|
+
}
|
|
94
|
+
/** A sealed, digest-stamped plan. The digest makes confirmation tamper-evident. */
|
|
95
|
+
interface CleanupPlan {
|
|
96
|
+
readonly recipeId: string;
|
|
97
|
+
readonly parameters: Readonly<Record<string, unknown>>;
|
|
98
|
+
readonly items: readonly CleanupPlanItem[];
|
|
99
|
+
readonly retains: readonly string[];
|
|
100
|
+
readonly blockers: readonly string[];
|
|
101
|
+
readonly rebuildActions: readonly string[];
|
|
102
|
+
readonly warnings: readonly string[];
|
|
103
|
+
readonly estimatedTotal: number;
|
|
104
|
+
readonly confirmationPhrase: string;
|
|
105
|
+
/** Deterministic checksum of the plan's material content — see `computePlanDigest`. */
|
|
106
|
+
readonly digest: string;
|
|
107
|
+
}
|
|
108
|
+
/** One committed step's outcome — reported via `onStep`, folded into the summary. */
|
|
109
|
+
interface CleanupStepResult {
|
|
110
|
+
readonly resource: string;
|
|
111
|
+
readonly processed: number;
|
|
112
|
+
readonly ok: boolean;
|
|
113
|
+
readonly error?: string | undefined;
|
|
114
|
+
/** Opaque resume cursor the recipe may surface for observability. */
|
|
115
|
+
readonly cursor?: string | undefined;
|
|
116
|
+
readonly startedAt?: Date | undefined;
|
|
117
|
+
readonly completedAt?: Date | undefined;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* BOUNDED progress summary persisted on the run (design "Node.js memory"): a
|
|
121
|
+
* fixed-size aggregate, never an unbounded per-chunk array. Detailed steps, if a
|
|
122
|
+
* host wants them, go to a separate capped sink — not this document.
|
|
123
|
+
*/
|
|
124
|
+
interface CleanupProgressSummary {
|
|
125
|
+
/** Running total of processed records across all steps. */
|
|
126
|
+
readonly processed: number;
|
|
127
|
+
/** Count of steps reported so far. */
|
|
128
|
+
readonly steps: number;
|
|
129
|
+
/** Resource the last step touched. */
|
|
130
|
+
readonly currentResource?: string | undefined;
|
|
131
|
+
/** Last resume cursor reported. */
|
|
132
|
+
readonly lastCursor?: string | undefined;
|
|
133
|
+
/** When the last step landed — liveness/heartbeat. */
|
|
134
|
+
readonly heartbeatAt?: Date | undefined;
|
|
135
|
+
/** Count of failed steps (a run with any is never a clean success). */
|
|
136
|
+
readonly failed?: number | undefined;
|
|
137
|
+
}
|
|
138
|
+
/** One verification invariant's outcome. */
|
|
139
|
+
interface VerificationCheck {
|
|
140
|
+
readonly name: string;
|
|
141
|
+
readonly ok: boolean;
|
|
142
|
+
readonly detail?: string | undefined;
|
|
143
|
+
}
|
|
144
|
+
/** The recipe's own post-checks — a delete count alone is never success (§9). */
|
|
145
|
+
interface VerificationResult {
|
|
146
|
+
readonly ok: boolean;
|
|
147
|
+
readonly checks: readonly VerificationCheck[];
|
|
148
|
+
}
|
|
149
|
+
type CleanupOutcomeStatus = "completed" | "partial" | "failed" | "cancelled";
|
|
150
|
+
/** What `execute` returns — the per-step results + a terminal status. */
|
|
151
|
+
interface CleanupResult {
|
|
152
|
+
readonly status: Exclude<CleanupOutcomeStatus, "cancelled">;
|
|
153
|
+
readonly results: readonly CleanupStepResult[];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The finalization payload persisted ON the run BEFORE it enters `finalizing`
|
|
157
|
+
* (same CAS patch). This is what makes a `finalizing` crash recoverable
|
|
158
|
+
* WITHOUT re-executing the recipe: a recovering worker replays ONLY
|
|
159
|
+
* `evidenceStore.finalize()` + the terminal CAS from this durable snapshot —
|
|
160
|
+
* never `recipe.execute()` again.
|
|
161
|
+
*/
|
|
162
|
+
interface CleanupFinalizationPayload {
|
|
163
|
+
readonly status: CleanupOutcomeStatus;
|
|
164
|
+
readonly results: readonly CleanupStepResult[];
|
|
165
|
+
readonly verification: VerificationResult;
|
|
166
|
+
/** Count of verification checks dropped by `maxChecks` truncation. */
|
|
167
|
+
readonly checksTruncated?: number | undefined;
|
|
168
|
+
readonly failureReason?: string | undefined;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* A cleanup recipe — the unit operators choose. Pure provider: the four
|
|
172
|
+
* methods are the only surface arc calls. A recipe delegates the actual
|
|
173
|
+
* domain work to kernel-provided steps; it never runs a raw Mongo cascade.
|
|
174
|
+
*/
|
|
175
|
+
interface CleanupRecipe {
|
|
176
|
+
/** Stable machine id, e.g. `'cleanup.pre-live-reset'`. Unique in a registry. */
|
|
177
|
+
readonly id: string;
|
|
178
|
+
/** Plain-language name for the UI. */
|
|
179
|
+
readonly label: string;
|
|
180
|
+
/** Whether this recipe removes data (gates confirmation + single-run fence). */
|
|
181
|
+
readonly destructive: boolean;
|
|
182
|
+
/**
|
|
183
|
+
* Recipe/plan-schema version. Persisted on the run; a retry that finds a
|
|
184
|
+
* different current version requires a fresh preview rather than replaying an
|
|
185
|
+
* incompatible sealed plan. Defaults to `'1'`.
|
|
186
|
+
*/
|
|
187
|
+
readonly version?: string;
|
|
188
|
+
/**
|
|
189
|
+
* The purge strategy this recipe's evidence records (`'hard'` deletion,
|
|
190
|
+
* `'soft'` deactivation, `'anonymize'` redaction). Defaults to `'hard'`.
|
|
191
|
+
* A pure projection-rebuild recipe should declare `'soft'` (nothing about the
|
|
192
|
+
* SOURCE data is removed) or, better, the closest honest label — evidence
|
|
193
|
+
* must never overstate destructiveness.
|
|
194
|
+
*/
|
|
195
|
+
readonly evidenceStrategy?: PurgeStrategyKind$1;
|
|
196
|
+
/** Whether this recipe may run right now (e.g. pre-go-live only). */
|
|
197
|
+
available(ctx: CleanupContext): Promise<Availability>;
|
|
198
|
+
/** Compute a preview WITHOUT mutating anything. Idempotent + side-effect-free. */
|
|
199
|
+
plan(input: CleanupInput, ctx: CleanupContext): Promise<CleanupPlanDraft>;
|
|
200
|
+
/** Execute the sealed plan. Must be chunked, idempotent, and cancellation-aware. */
|
|
201
|
+
execute(plan: CleanupPlan, ctx: CleanupExecutionContext): Promise<CleanupResult>;
|
|
202
|
+
/** Verify the post-state — the recipe owns what "clean" means. */
|
|
203
|
+
verify(plan: CleanupPlan, ctx: CleanupContext): Promise<VerificationResult>;
|
|
204
|
+
}
|
|
205
|
+
type CleanupRunStatus = "planned" | "queued" | "running" | "finalizing" | "completed" | "failed" | "cancelled";
|
|
206
|
+
/** Terminal states — no further transition is valid. */
|
|
207
|
+
declare const CLEANUP_TERMINAL_STATUSES: readonly CleanupRunStatus[];
|
|
208
|
+
interface CleanupRun {
|
|
209
|
+
readonly id: string;
|
|
210
|
+
readonly recipeId: string;
|
|
211
|
+
readonly recipeVersion: string;
|
|
212
|
+
readonly status: CleanupRunStatus;
|
|
213
|
+
readonly planDigest: string;
|
|
214
|
+
/** The immutable sealed plan — replayed verbatim by the worker / a retry. */
|
|
215
|
+
readonly sealedPlan: CleanupPlan;
|
|
216
|
+
/** The operator-supplied parameters (also inside sealedPlan; kept explicit). */
|
|
217
|
+
readonly parameters: Readonly<Record<string, unknown>>;
|
|
218
|
+
/** The authorizing actor — persisted so a worker attributes the op correctly. */
|
|
219
|
+
readonly actor: PurgeActor;
|
|
220
|
+
/** Serializable ambient scope captured at request time (branch/company). */
|
|
221
|
+
readonly ambient?: Readonly<Record<string, unknown>> | undefined;
|
|
222
|
+
readonly requestedBy: string;
|
|
223
|
+
readonly reason: string;
|
|
224
|
+
readonly operationId: string;
|
|
225
|
+
/**
|
|
226
|
+
* Whether the recipe was destructive at request time — persisted so a
|
|
227
|
+
* GENERIC store can enforce the single-destructive-run policy without
|
|
228
|
+
* consulting the registry (and so non-destructive rebuilds need not
|
|
229
|
+
* serialize behind it).
|
|
230
|
+
*/
|
|
231
|
+
readonly destructive: boolean;
|
|
232
|
+
/**
|
|
233
|
+
* Admission-control key the store serializes on (e.g. `'global-destructive'`).
|
|
234
|
+
* Absent for runs that need no mutual exclusion. Hosts may shard it later
|
|
235
|
+
* (`'branch:<id>'`) without an arc change.
|
|
236
|
+
*/
|
|
237
|
+
readonly concurrencyKey?: string | undefined;
|
|
238
|
+
/** How many times a worker has claimed this run (1 = first execution). */
|
|
239
|
+
readonly attempt: number;
|
|
240
|
+
/**
|
|
241
|
+
* Exclusive worker lease. A run in `running`/`finalizing` with an UNEXPIRED
|
|
242
|
+
* lease is owned — `claim` refuses it. Progress + status writes carry the
|
|
243
|
+
* token so a stalled ex-owner that wakes past expiry cannot clobber the new
|
|
244
|
+
* owner's writes.
|
|
245
|
+
*/
|
|
246
|
+
readonly leaseToken?: string | undefined;
|
|
247
|
+
readonly leaseExpiresAt?: Date | undefined;
|
|
248
|
+
readonly progress: CleanupProgressSummary;
|
|
249
|
+
/** Durable cancellation request — the source of truth the executor polls. */
|
|
250
|
+
readonly cancelRequested: boolean;
|
|
251
|
+
/** Who requested the cancel + why + when (audit — §8, cancel evidence). */
|
|
252
|
+
readonly cancelRequestedBy?: PurgeActor | undefined;
|
|
253
|
+
readonly cancelReason?: string | undefined;
|
|
254
|
+
readonly cancelRequestedAt?: Date | undefined;
|
|
255
|
+
/** Durable finalization snapshot — see {@link CleanupFinalizationPayload}. */
|
|
256
|
+
readonly finalization?: CleanupFinalizationPayload | undefined;
|
|
257
|
+
readonly queuedAt?: Date | undefined;
|
|
258
|
+
readonly startedAt?: Date | undefined;
|
|
259
|
+
readonly completedAt?: Date | undefined;
|
|
260
|
+
readonly failureReason?: string | undefined;
|
|
261
|
+
}
|
|
262
|
+
/** Immutable proof of a completed run (§5) — deterministically checksummed. */
|
|
263
|
+
interface CleanupManifest {
|
|
264
|
+
readonly runId: string;
|
|
265
|
+
readonly recipeId: string;
|
|
266
|
+
readonly recipeVersion: string;
|
|
267
|
+
readonly operationId: string;
|
|
268
|
+
readonly planDigest: string;
|
|
269
|
+
readonly status: CleanupOutcomeStatus;
|
|
270
|
+
readonly actor: PurgeActor;
|
|
271
|
+
readonly reason: string;
|
|
272
|
+
readonly results: readonly CleanupStepResult[];
|
|
273
|
+
readonly verification: VerificationResult;
|
|
274
|
+
/**
|
|
275
|
+
* Count of verification checks DROPPED by the `maxChecks` cap. Present and
|
|
276
|
+
* non-zero whenever `verification.checks` is not the complete set — evidence
|
|
277
|
+
* must say so rather than silently show only the retained checks.
|
|
278
|
+
*/
|
|
279
|
+
readonly checksTruncated?: number | undefined;
|
|
280
|
+
readonly completedAt: Date;
|
|
281
|
+
/**
|
|
282
|
+
* Integrity CHECKSUM of the manifest content (not tamper-PROOF: plain SHA-256
|
|
283
|
+
* does not stop an attacker with write access from replacing the record and
|
|
284
|
+
* recomputing the digest — pair with access control / keyed signatures for
|
|
285
|
+
* that). Detects accidental corruption + drift.
|
|
286
|
+
*/
|
|
287
|
+
readonly manifestDigest: string;
|
|
288
|
+
}
|
|
289
|
+
type CleanupRunCreateResult = {
|
|
290
|
+
readonly created: true;
|
|
291
|
+
} | {
|
|
292
|
+
readonly created: false;
|
|
293
|
+
readonly activeRunId: string;
|
|
294
|
+
};
|
|
295
|
+
/** Fields a status transition may also patch. */
|
|
296
|
+
interface CleanupRunTransitionPatch {
|
|
297
|
+
readonly queuedAt?: Date;
|
|
298
|
+
readonly startedAt?: Date;
|
|
299
|
+
readonly completedAt?: Date;
|
|
300
|
+
readonly failureReason?: string;
|
|
301
|
+
readonly progress?: CleanupProgressSummary;
|
|
302
|
+
/** Reset the cancellation flag — used when a terminal run is re-armed on retry. */
|
|
303
|
+
readonly cancelRequested?: boolean;
|
|
304
|
+
/** Persist the durable finalization snapshot (set entering `finalizing`). */
|
|
305
|
+
readonly finalization?: CleanupFinalizationPayload;
|
|
306
|
+
}
|
|
307
|
+
/** An exclusive worker lease on a run. */
|
|
308
|
+
interface CleanupLease {
|
|
309
|
+
/** Opaque token identifying THIS worker's ownership of the run. */
|
|
310
|
+
readonly token: string;
|
|
311
|
+
/** When the lease lapses and the run becomes claimable again. */
|
|
312
|
+
readonly expiresAt: Date;
|
|
313
|
+
}
|
|
314
|
+
/** Metadata persisted with a durable cancel request (audit). */
|
|
315
|
+
interface CleanupCancelRequest {
|
|
316
|
+
readonly actor?: PurgeActor | undefined;
|
|
317
|
+
readonly reason?: string | undefined;
|
|
318
|
+
readonly requestedAt: Date;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Durable store for cleanup runs. The host owns persistence (Mongo/SQL/…) AND
|
|
322
|
+
* the atomicity of these operations — the framework's safety rests on them:
|
|
323
|
+
*
|
|
324
|
+
* - `createIfPermitted` MUST be atomic: for a run with a `concurrencyKey` it
|
|
325
|
+
* inserts ONLY if no other run with the same key is in a non-terminal
|
|
326
|
+
* state, backed by a unique partial index / conditional insert / lock. A
|
|
327
|
+
* check-then-create is NOT sufficient. Runs WITHOUT a key are always
|
|
328
|
+
* admitted.
|
|
329
|
+
* - `claim` MUST be atomic and EXCLUSIVE: grant the lease ONLY if the run is
|
|
330
|
+
* `queued`, OR is `running`/`finalizing` with an ABSENT or EXPIRED lease
|
|
331
|
+
* (`leaseExpiresAt <= now`). On grant it sets `leaseToken`/`leaseExpiresAt`,
|
|
332
|
+
* increments `attempt`, and returns the run (status UNCHANGED — the service
|
|
333
|
+
* branches on it: `finalizing` recovers finalization only, never
|
|
334
|
+
* re-executes). A run whose lease is live is OWNED — return `null`.
|
|
335
|
+
* - `compareAndTransition` MUST be a compare-and-set: apply the transition
|
|
336
|
+
* ONLY if the current status is one of `expected` AND, when `leaseToken`
|
|
337
|
+
* is given, the run's current lease token matches. This is the ONLY way
|
|
338
|
+
* status changes — it makes cancel-vs-complete AND stale-ex-owner races
|
|
339
|
+
* safe.
|
|
340
|
+
* - `reArmIfPermitted` MUST apply the same admission policy as
|
|
341
|
+
* `createIfPermitted` atomically with the terminal→`queued` transition
|
|
342
|
+
* (a retry must not slip past the single-destructive-run guard).
|
|
343
|
+
*/
|
|
344
|
+
interface CleanupRunStore {
|
|
345
|
+
/** Atomic conditional insert (admission control by `concurrencyKey`, §8). */
|
|
346
|
+
createIfPermitted(run: CleanupRun): Promise<CleanupRunCreateResult>;
|
|
347
|
+
get(id: string): Promise<CleanupRun | null>;
|
|
348
|
+
/**
|
|
349
|
+
* Atomically acquire the exclusive worker lease (see contract above).
|
|
350
|
+
* Returns the claimed run with the lease applied, or `null` if the run is
|
|
351
|
+
* owned, terminal, or missing.
|
|
352
|
+
*/
|
|
353
|
+
claim(id: string, lease: CleanupLease): Promise<CleanupRun | null>;
|
|
354
|
+
/**
|
|
355
|
+
* CAS the status: set `status=to` (+ `patch`) ONLY if current status ∈
|
|
356
|
+
* `expected` and (when given) `leaseToken` matches the run's current lease.
|
|
357
|
+
* Returns the updated run, or `null` if the CAS lost the race.
|
|
358
|
+
*/
|
|
359
|
+
compareAndTransition(id: string, expected: readonly CleanupRunStatus[], to: CleanupRunStatus, patch?: CleanupRunTransitionPatch, leaseToken?: string): Promise<CleanupRun | null>;
|
|
360
|
+
/**
|
|
361
|
+
* Atomically re-arm a terminal (`failed`/`cancelled`) run back to `queued`
|
|
362
|
+
* under the SAME admission policy as `createIfPermitted` (its
|
|
363
|
+
* `concurrencyKey` must not collide with another non-terminal run). Clears
|
|
364
|
+
* the cancel flag/metadata + lease. Returns `{created:false}`-style refusal
|
|
365
|
+
* via `null` + the blocking run id in the second tuple slot is NOT modeled —
|
|
366
|
+
* a `null` simply means "not permitted or not in a terminal state."
|
|
367
|
+
*/
|
|
368
|
+
reArmIfPermitted(id: string, patch: CleanupRunTransitionPatch): Promise<CleanupRun | null>;
|
|
369
|
+
/**
|
|
370
|
+
* Set `cancelRequested = true` (+ audit metadata, idempotent — first request
|
|
371
|
+
* wins). Does NOT change `status`.
|
|
372
|
+
*/
|
|
373
|
+
requestCancel(id: string, request?: CleanupCancelRequest): Promise<CleanupRun | null>;
|
|
374
|
+
/**
|
|
375
|
+
* Overwrite the small bounded progress summary (frequent, non-status).
|
|
376
|
+
* When `lease` is given, ALSO extend `leaseExpiresAt` (heartbeat renewal) —
|
|
377
|
+
* and apply ONLY if the run's current lease token matches `lease.token`.
|
|
378
|
+
*/
|
|
379
|
+
saveProgress(id: string, progress: CleanupProgressSummary, lease?: CleanupLease): Promise<void>;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Durable store for the evidence + manifest a run leaves behind. `finalize`
|
|
383
|
+
* MUST be idempotent by `evidence.operationId` — a retry / restart that
|
|
384
|
+
* re-finalizes the same operation is a no-op (or an upsert), never a duplicate.
|
|
385
|
+
* Ideally the host writes both in one transaction (with the terminal run
|
|
386
|
+
* transition); the framework tolerates a `finalizing` intermediate + re-run.
|
|
387
|
+
*/
|
|
388
|
+
interface CleanupEvidenceStore {
|
|
389
|
+
finalize(input: {
|
|
390
|
+
evidence: PurgeEvidence;
|
|
391
|
+
manifest: CleanupManifest;
|
|
392
|
+
}): Promise<void>;
|
|
393
|
+
}
|
|
394
|
+
/** A serializable job — everything the worker needs is the run id. */
|
|
395
|
+
interface CleanupJob {
|
|
396
|
+
readonly runId: string;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Durable job queue (§8). `execute()` enqueues a `{ runId }`; a worker later
|
|
400
|
+
* dequeues and calls `service.processRun(runId)` — a DIFFERENT process can do
|
|
401
|
+
* this because the run carries the whole sealed operation. The default in-proc
|
|
402
|
+
* queue defers to a microtask (still off the request path within one process);
|
|
403
|
+
* production injects BullMQ / SQS / a repo-backed queue for cross-process
|
|
404
|
+
* durability, claim/lease, and restart recovery.
|
|
405
|
+
*/
|
|
406
|
+
interface CleanupJobQueue {
|
|
407
|
+
enqueue(job: CleanupJob): Promise<void>;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* A company/target write fence (§8) — blocks new writes for the duration of a
|
|
411
|
+
* destructive run. Acquire/release keyed by the operation id so a crashed run's
|
|
412
|
+
* fence is recoverable by op id.
|
|
413
|
+
*
|
|
414
|
+
* `acquire` MAY return a fencing token; when it does, the service passes the
|
|
415
|
+
* SAME token back to `release`, and the fence MUST refuse a release carrying a
|
|
416
|
+
* stale token. This closes the distributed race where a stalled ex-owner wakes
|
|
417
|
+
* after lease expiry and releases the fence out from under the new owner.
|
|
418
|
+
* Implementations without cross-process workers may return `undefined` and
|
|
419
|
+
* ignore the token.
|
|
420
|
+
*/
|
|
421
|
+
interface CleanupWriteFence {
|
|
422
|
+
acquire(operationId: string): Promise<string | undefined | void>;
|
|
423
|
+
release(operationId: string, token?: string): Promise<void>;
|
|
424
|
+
}
|
|
425
|
+
interface CleanupPermissions {
|
|
426
|
+
/** May preview/observe runs. */
|
|
427
|
+
view: CleanupPermissionCheck;
|
|
428
|
+
/** May execute a run. Superadmin in the reference host. */
|
|
429
|
+
execute: CleanupPermissionCheck;
|
|
430
|
+
/** May cancel/retry a run. Defaults to `execute` when omitted. */
|
|
431
|
+
manage?: CleanupPermissionCheck;
|
|
432
|
+
}
|
|
433
|
+
/** Host permission predicate — receives whatever the host passes as the request scope. */
|
|
434
|
+
type CleanupPermissionCheck = (scope: unknown) => boolean | Promise<boolean>;
|
|
435
|
+
/** Framework size caps that keep runs + documents bounded (design "memory"). */
|
|
436
|
+
interface CleanupLimits {
|
|
437
|
+
/** Max plan items. Default 500. */
|
|
438
|
+
readonly maxPlanItems: number;
|
|
439
|
+
/** Max per-run result rows folded into the manifest. Default 1000. */
|
|
440
|
+
readonly maxResults: number;
|
|
441
|
+
/** Max verification checks. Default 200. */
|
|
442
|
+
readonly maxChecks: number;
|
|
443
|
+
/** Max reason string length. Default 2000. */
|
|
444
|
+
readonly maxReasonLength: number;
|
|
445
|
+
/** Max nesting depth of the parameters object. Default 8. */
|
|
446
|
+
readonly maxParamDepth: number;
|
|
447
|
+
}
|
|
448
|
+
declare const DEFAULT_CLEANUP_LIMITS: CleanupLimits;
|
|
449
|
+
//#endregion
|
|
450
|
+
export { CleanupWriteFence as A, CleanupResult as C, CleanupRunStore as D, CleanupRunStatus as E, PurgeStrategyKind$1 as F, PurgeVerificationSummary as I, VerificationCheck as L, PurgeActor as M, PurgeEvidence as N, CleanupRunTransitionPatch as O, PurgeResourceResult as P, VerificationResult as R, CleanupRecipe as S, CleanupRunCreateResult as T, CleanupPermissions as _, CleanupEvidenceStore as a, CleanupPlanItem as b, CleanupInput as c, CleanupLease as d, CleanupLimits as f, CleanupPermissionCheck as g, CleanupOutcomeStatus as h, CleanupContext as i, DEFAULT_CLEANUP_LIMITS as j, CleanupStepResult as k, CleanupJob as l, CleanupManifest as m, CLEANUP_TERMINAL_STATUSES as n, CleanupExecutionContext as o, CleanupLogger as p, CleanupCancelRequest as r, CleanupFinalizationPayload as s, Availability as t, CleanupJobQueue as u, CleanupPlan as v, CleanupRun as w, CleanupProgressSummary as x, CleanupPlanDraft as y };
|
|
@@ -95,6 +95,56 @@ declare function getUserRoles(user: {
|
|
|
95
95
|
} | null | undefined): string[];
|
|
96
96
|
//#endregion
|
|
97
97
|
//#region src/permissions/types.d.ts
|
|
98
|
+
/**
|
|
99
|
+
* Row-level data policy — the constraint every returned/queried row must
|
|
100
|
+
* satisfy. This is arc's **Mongo-style record dialect** (`{ ownerId }`,
|
|
101
|
+
* `{ $or: [...] }`, `{ status: { $ne: "archived" } }`), the ONE canonical
|
|
102
|
+
* internal representation every permission helper emits and every enforcement
|
|
103
|
+
* seam composes (`conjoinPolicyFilters`).
|
|
104
|
+
*
|
|
105
|
+
* It is compiled to the portable repo-core `Filter` IR at the repository
|
|
106
|
+
* boundary (`core/repositoryFilter.ts` → `policyRecordToFilter`) so it runs
|
|
107
|
+
* identically on Mongo / SQL / any kit — arc's answer to oso-style "data
|
|
108
|
+
* filtering" + Postgres RLS, but datastore-portable and in-process. The IR is a
|
|
109
|
+
* boundary/compile concern, NOT the decision surface: keeping the decision in
|
|
110
|
+
* one record dialect means composition (`allOf`/`anyOf`/`conjoin`) has a single
|
|
111
|
+
* well-defined shape instead of a `Filter | Record` union the composer can't
|
|
112
|
+
* honor.
|
|
113
|
+
*/
|
|
114
|
+
type DataPolicy = Record<string, unknown>;
|
|
115
|
+
/**
|
|
116
|
+
* The canonical authorization decision (arc 2.30). A single typed value that
|
|
117
|
+
* cleanly separates the concerns a permission check expresses:
|
|
118
|
+
*
|
|
119
|
+
* - `effect` — allow / deny (Cedar / XACML decision)
|
|
120
|
+
* - `reason` — human-readable diagnostic for the denial (explainability)
|
|
121
|
+
* - `policy` — row-level {@link DataPolicy} to enforce at the repository
|
|
122
|
+
* - `scope` — identity/context to install on the request (PIP)
|
|
123
|
+
*
|
|
124
|
+
* Build one with the `allow()` / `deny()` constructors. A check may also return
|
|
125
|
+
* a bare `boolean` (terse allow/deny), normalized via `normalizeToDecision`.
|
|
126
|
+
*
|
|
127
|
+
* NOTE: arc deliberately does NOT model XACML-style "obligations" on the
|
|
128
|
+
* decision. Audit and field redaction are first-class arc subsystems already
|
|
129
|
+
* (`audit: true` on a resource; field-level read/write permissions), so an
|
|
130
|
+
* obligations channel would be a second, unenforced way to ask for the same
|
|
131
|
+
* effects. If a future need arises it will ship WITH a dispatcher that every
|
|
132
|
+
* enforcement surface runs — never as a declared-but-ignored contract.
|
|
133
|
+
*/
|
|
134
|
+
interface AuthorizationDecision {
|
|
135
|
+
/** Allow or deny. */
|
|
136
|
+
effect: "allow" | "deny";
|
|
137
|
+
/** Human-readable denial reason (surfaced in error messages / explain). */
|
|
138
|
+
reason?: string;
|
|
139
|
+
/** Row-level data policy to enforce at the repository layer. */
|
|
140
|
+
policy?: DataPolicy;
|
|
141
|
+
/**
|
|
142
|
+
* Scope to install on `request.scope` when allowed — flows to
|
|
143
|
+
* `metadata._scope` for tenant-field filtering. The clean integration point
|
|
144
|
+
* for custom auth (API keys, service accounts, gateway headers).
|
|
145
|
+
*/
|
|
146
|
+
scope?: RequestScope;
|
|
147
|
+
}
|
|
98
148
|
/**
|
|
99
149
|
* User base interface - minimal shape Arc expects
|
|
100
150
|
* Your actual User can have any additional fields
|
|
@@ -107,25 +157,33 @@ interface UserBase {
|
|
|
107
157
|
[key: string]: unknown;
|
|
108
158
|
}
|
|
109
159
|
/**
|
|
110
|
-
*
|
|
160
|
+
* Context passed to permission check functions.
|
|
111
161
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* -
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
* Normalize a raw role value (string, comma-separated string, or array) into a string[].
|
|
119
|
-
* Shared low-level helper used by both getUserRoles() and the Better Auth adapter.
|
|
120
|
-
*/
|
|
121
|
-
/**
|
|
122
|
-
* Context passed to permission check functions
|
|
162
|
+
* arc 2.30: checks read FACTS off this context — never off a raw request. Use
|
|
163
|
+
* `scopeOf(ctx)` (from `./context.js`) to read the scope; it prefers the
|
|
164
|
+
* first-class {@link PermissionContext.scope} and lets combinators thread an
|
|
165
|
+
* updated scope to child checks WITHOUT mutating the request. `request` is a
|
|
166
|
+
* transport escape hatch (present for HTTP; absent for MCP/jobs/ws) and is being
|
|
167
|
+
* retired as the primary channel.
|
|
123
168
|
*/
|
|
124
169
|
interface PermissionContext<TDoc = Record<string, unknown>> {
|
|
125
170
|
/** Authenticated user or null if unauthenticated */
|
|
126
171
|
user: UserBase | null;
|
|
127
|
-
/**
|
|
128
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Resolved request scope (identity + tenant/org context). First-class so
|
|
174
|
+
* combinators can thread an accumulated scope purely and non-HTTP transports
|
|
175
|
+
* need no request. Read via `scopeOf(ctx)`.
|
|
176
|
+
*/
|
|
177
|
+
scope?: RequestScope;
|
|
178
|
+
/**
|
|
179
|
+
* Raw Fastify request — transport escape hatch, present ONLY on the HTTP
|
|
180
|
+
* surface. It is `undefined` for MCP, jobs, and any non-Fastify transport, so
|
|
181
|
+
* a permission check must NOT depend on it: read identity via `scopeOf(ctx)`,
|
|
182
|
+
* inputs via `ctx.data` / `ctx.params`, and host facts via `ctx.attributes`.
|
|
183
|
+
* Kept solely for genuinely HTTP-coupled checks (e.g. a usage meter on
|
|
184
|
+
* `request.server`) which are inherently transport-specific.
|
|
185
|
+
*/
|
|
186
|
+
request?: FastifyRequest;
|
|
129
187
|
/** Resource name being accessed */
|
|
130
188
|
resource: string;
|
|
131
189
|
/** Action being performed (list, get, create, update, delete, or custom operation name) */
|
|
@@ -136,93 +194,40 @@ interface PermissionContext<TDoc = Record<string, unknown>> {
|
|
|
136
194
|
params?: Record<string, string>;
|
|
137
195
|
/** Request body data */
|
|
138
196
|
data?: Partial<TDoc> | Record<string, unknown>;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Result from a permission check.
|
|
142
|
-
*
|
|
143
|
-
* Permission checks can do three things:
|
|
144
|
-
* 1. **Grant or deny** access (`granted`, `reason`)
|
|
145
|
-
* 2. **Attach row-level filters** (`filters`) — these merge into `_policyFilters`
|
|
146
|
-
* and narrow subsequent queries (e.g. `{ userId: ctx.user.id }` for ownership)
|
|
147
|
-
* 3. **Install the request scope** (`scope`) — when a custom authenticator wants
|
|
148
|
-
* to set tenant/identity context directly from the permission layer, without
|
|
149
|
-
* relying on a separate auth plugin
|
|
150
|
-
*
|
|
151
|
-
* The `scope` field is the clean integration point for custom auth strategies
|
|
152
|
-
* (API keys, service accounts, gateway headers). When present, Arc writes it to
|
|
153
|
-
* `request.scope` which then flows through the normal tenant-filtering pipeline
|
|
154
|
-
* (QueryResolver + AccessControl). This is the idiomatic way to wire non-Better-Auth
|
|
155
|
-
* identity providers into Arc's multi-tenancy without touching the auth plugin layer.
|
|
156
|
-
*
|
|
157
|
-
* @example
|
|
158
|
-
* ```typescript
|
|
159
|
-
* // Custom API-key auth — grant access AND install a service scope in one step
|
|
160
|
-
* export function requireApiKey(): PermissionCheck {
|
|
161
|
-
* return async ({ request }) => {
|
|
162
|
-
* const apiKey = request.headers['x-api-key'] as string | undefined;
|
|
163
|
-
* if (!apiKey) return { granted: false, reason: 'Missing API key' };
|
|
164
|
-
*
|
|
165
|
-
* const client = await ClientModel.findOne({ apiKey });
|
|
166
|
-
* if (!client) return { granted: false, reason: 'Invalid API key' };
|
|
167
|
-
*
|
|
168
|
-
* return {
|
|
169
|
-
* granted: true,
|
|
170
|
-
* // Install service scope — Arc writes this to request.scope automatically,
|
|
171
|
-
* // and tenantField filtering picks it up via metadata._scope
|
|
172
|
-
* scope: {
|
|
173
|
-
* kind: 'service',
|
|
174
|
-
* clientId: String(client._id),
|
|
175
|
-
* organizationId: String(client.companyId),
|
|
176
|
-
* scopes: client.allowedScopes,
|
|
177
|
-
* },
|
|
178
|
-
* // Optional row-level narrowing (e.g. per-project API keys)
|
|
179
|
-
* filters: client.projectId ? { projectId: client.projectId } : undefined,
|
|
180
|
-
* };
|
|
181
|
-
* };
|
|
182
|
-
* }
|
|
183
|
-
* ```
|
|
184
|
-
*/
|
|
185
|
-
interface PermissionResult {
|
|
186
|
-
/** Whether access is granted */
|
|
187
|
-
granted: boolean;
|
|
188
|
-
/** Reason for denial (for error messages) */
|
|
189
|
-
reason?: string;
|
|
190
|
-
/** Query filters to apply (for ownership / row-level security patterns) */
|
|
191
|
-
filters?: Record<string, unknown>;
|
|
192
197
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* tenant-field filtering. Use this to wire custom auth (API keys, service
|
|
196
|
-
* accounts, gateway headers) into Arc's multi-tenancy without a separate
|
|
197
|
-
* auth plugin.
|
|
198
|
+
* Transport/host-supplied attributes a check may need for ABAC decisions
|
|
199
|
+
* (headers, claims, feature flags) without reaching into the raw request.
|
|
198
200
|
*/
|
|
199
|
-
|
|
201
|
+
attributes?: Readonly<Record<string, unknown>>;
|
|
200
202
|
}
|
|
201
203
|
/**
|
|
202
|
-
* Permission Check Function
|
|
204
|
+
* Permission Check Function — THE way to define permissions in Arc.
|
|
203
205
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
+
* Returns a `boolean` (terse allow/deny) or an {@link AuthorizationDecision}
|
|
207
|
+
* (effect + optional data `policy` and `scope`). Build decisions with the
|
|
208
|
+
* `allow()` / `deny()` constructors from `@classytic/arc/permissions`.
|
|
206
209
|
*
|
|
207
210
|
* @example
|
|
208
211
|
* ```typescript
|
|
209
|
-
* // Simple boolean
|
|
212
|
+
* // Simple boolean
|
|
210
213
|
* const isAdmin: PermissionCheck = (ctx) => getUserRoles(ctx.user).includes('admin');
|
|
211
214
|
*
|
|
212
|
-
* //
|
|
213
|
-
* const ownedByUser: PermissionCheck = (ctx) =>
|
|
214
|
-
*
|
|
215
|
-
* filters: { userId: ctx.user?.id }
|
|
216
|
-
* });
|
|
215
|
+
* // Grant + row-level data policy (ownership)
|
|
216
|
+
* const ownedByUser: PermissionCheck = (ctx) =>
|
|
217
|
+
* allow({ policy: { userId: ctx.user?.id } });
|
|
217
218
|
*
|
|
218
|
-
* //
|
|
219
|
-
* const
|
|
220
|
-
* const
|
|
221
|
-
*
|
|
219
|
+
* // Custom API-key auth — grant AND install a service scope
|
|
220
|
+
* const requireApiKey: PermissionCheck = async ({ request }) => {
|
|
221
|
+
* const client = await lookup(request.headers['x-api-key']);
|
|
222
|
+
* if (!client) return deny('Invalid API key');
|
|
223
|
+
* return allow({
|
|
224
|
+
* scope: { kind: 'service', clientId: String(client._id), organizationId: String(client.companyId) },
|
|
225
|
+
* ...(client.projectId ? { policy: { projectId: client.projectId } } : {}),
|
|
226
|
+
* });
|
|
222
227
|
* };
|
|
223
228
|
* ```
|
|
224
229
|
*/
|
|
225
|
-
type PermissionCheck<TDoc = Record<string, unknown>> = ((context: PermissionContext<TDoc>) => boolean |
|
|
230
|
+
type PermissionCheck<TDoc = Record<string, unknown>> = ((context: PermissionContext<TDoc>) => boolean | AuthorizationDecision | Promise<boolean | AuthorizationDecision>) & PermissionCheckMeta;
|
|
226
231
|
/**
|
|
227
232
|
* Optional metadata attached to permission check functions.
|
|
228
233
|
* Used for OpenAPI data, introspection, and route-level auth decisions.
|
|
@@ -287,4 +292,4 @@ interface PermissionCheckMeta {
|
|
|
287
292
|
};
|
|
288
293
|
}
|
|
289
294
|
//#endregion
|
|
290
|
-
export {
|
|
295
|
+
export { UserBase as a, normalizeRoles as c, JWTPayload as d, ObjectId as f, PermissionContext as i, AnyRecord as l, UserOrganization as m, DataPolicy as n, getUserId as o, UserLike as p, PermissionCheck as r, getUserRoles as s, AuthorizationDecision as t, ArcRequest as u };
|