@classytic/arc 2.15.4 → 2.16.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.
Files changed (158) hide show
  1. package/README.md +1 -0
  2. package/bin/arc.js +12 -0
  3. package/dist/{BaseController-dx3m2J8V.mjs → BaseController-DlCCTIxJ.mjs} +61 -19
  4. package/dist/{HookSystem-Iiebom92.mjs → HookSystem-Cmf7-Etp.mjs} +8 -4
  5. package/dist/{QueryCache-D41bfdBB.d.mts → QueryCache-SvmT_9ti.d.mts} +1 -1
  6. package/dist/{ResourceRegistry-CTERg_2x.mjs → ResourceRegistry-f48hFk3m.mjs} +52 -9
  7. package/dist/audit/index.d.mts +1 -1
  8. package/dist/audit/index.mjs +4 -2
  9. package/dist/auth/index.d.mts +4 -4
  10. package/dist/auth/index.mjs +4 -4
  11. package/dist/auth/redis-session.d.mts +1 -1
  12. package/dist/{betterAuthOpenApi--M_i87dQ.mjs → betterAuthOpenApi-ClWxaceA.mjs} +10 -6
  13. package/dist/buildHandler-BZX6zzDM.mjs +300 -0
  14. package/dist/cache/index.d.mts +3 -3
  15. package/dist/cache/index.mjs +3 -3
  16. package/dist/{caching-SM8gghN6.mjs → caching-TeHE8G-v.mjs} +1 -1
  17. package/dist/cli/commands/describe.d.mts +35 -1
  18. package/dist/cli/commands/describe.mjs +52 -12
  19. package/dist/cli/commands/docs.d.mts +1 -4
  20. package/dist/cli/commands/docs.mjs +4 -16
  21. package/dist/cli/commands/generate.d.mts +2 -20
  22. package/dist/cli/commands/generate.mjs +1 -546
  23. package/dist/cli/commands/init.d.mts +2 -40
  24. package/dist/cli/commands/init.mjs +1 -3045
  25. package/dist/cli/commands/introspect.mjs +53 -64
  26. package/dist/cli/index.d.mts +2 -2
  27. package/dist/cli/index.mjs +2 -2
  28. package/dist/{constants-Cxde4rpC.mjs → constants-TrJVIJl0.mjs} +7 -0
  29. package/dist/core/index.d.mts +3 -3
  30. package/dist/core/index.mjs +5 -5
  31. package/dist/{core-CvmOqEms.mjs → core-DBJ_j6rX.mjs} +222 -44
  32. package/dist/createActionRouter-DUpN3Dd1.mjs +288 -0
  33. package/dist/{createAggregationRouter-B0bPDf5b.mjs → createAggregationRouter-Dq-TUCuY.mjs} +3 -2
  34. package/dist/{createApp-PFegs47-.mjs → createApp-DNccuhyI.mjs} +16 -14
  35. package/dist/{defineEvent-D5h7EvAx.mjs → defineEvent-DRwY0fYm.mjs} +1 -1
  36. package/dist/docs/index.d.mts +2 -2
  37. package/dist/docs/index.mjs +1 -1
  38. package/dist/{errorHandler-Bk-AGhkU.mjs → errorHandler-DpoXQHZ9.mjs} +17 -14
  39. package/dist/errors-C1lX_jlm.d.mts +91 -0
  40. package/dist/{eventPlugin-CaKTYkYM.mjs → eventPlugin-C2cGqtRO.mjs} +1 -1
  41. package/dist/{eventPlugin-qXpqTebY.d.mts → eventPlugin-CtHC_av1.d.mts} +1 -1
  42. package/dist/events/index.d.mts +3 -3
  43. package/dist/events/index.mjs +5 -5
  44. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  45. package/dist/events/transports/redis.d.mts +1 -1
  46. package/dist/factory/index.d.mts +1 -1
  47. package/dist/factory/index.mjs +2 -2
  48. package/dist/{fields-COhcH3fk.d.mts → fields-Anj0xdih.d.mts} +1 -1
  49. package/dist/generate-BWFwgcCM.d.mts +38 -0
  50. package/dist/generate-CYac-OLv.mjs +654 -0
  51. package/dist/hooks/index.d.mts +1 -1
  52. package/dist/hooks/index.mjs +1 -1
  53. package/dist/idempotency/index.d.mts +2 -2
  54. package/dist/idempotency/index.mjs +1 -1
  55. package/dist/idempotency/redis.d.mts +1 -1
  56. package/dist/{index-BTqLEvhu.d.mts → index-3oIimXQn.d.mts} +12 -12
  57. package/dist/{index-BstGxcc3.d.mts → index-B-ulKx5P.d.mts} +55 -4
  58. package/dist/{index-BswOSJCE.d.mts → index-CkW0flkU.d.mts} +355 -16
  59. package/dist/index.d.mts +6 -6
  60. package/dist/index.mjs +7 -8
  61. package/dist/init-Dv71MsJr.d.mts +71 -0
  62. package/dist/init-HDvoO9L5.mjs +3098 -0
  63. package/dist/integrations/event-gateway.d.mts +2 -2
  64. package/dist/integrations/event-gateway.mjs +1 -1
  65. package/dist/integrations/index.d.mts +2 -2
  66. package/dist/integrations/jobs.mjs +3 -3
  67. package/dist/integrations/mcp/index.d.mts +239 -7
  68. package/dist/integrations/mcp/index.mjs +2 -528
  69. package/dist/integrations/mcp/testing.d.mts +2 -2
  70. package/dist/integrations/mcp/testing.mjs +6 -10
  71. package/dist/integrations/streamline.mjs +26 -1
  72. package/dist/integrations/websocket-redis.d.mts +1 -1
  73. package/dist/integrations/websocket.d.mts +1 -1
  74. package/dist/integrations/websocket.mjs +1 -0
  75. package/dist/loadResourcesFromEntry-BLMEI2Xa.mjs +51 -0
  76. package/dist/{resourceToTools-tFYUNmM0.mjs → mcpPlugin-7vGV51ED.mjs} +1021 -318
  77. package/dist/{memory-UBydS5ku.mjs → memory-QOLe11D5.mjs} +2 -0
  78. package/dist/middleware/index.d.mts +1 -1
  79. package/dist/middleware/index.mjs +1 -1
  80. package/dist/{openapi-BHXhoX8O.mjs → openapi-34T9yNwd.mjs} +47 -36
  81. package/dist/permissions/index.d.mts +2 -2
  82. package/dist/permissions/index.mjs +1 -1
  83. package/dist/{permissions-ohQyv50e.mjs → permissions-CTxMrreC.mjs} +2 -2
  84. package/dist/{pipe-Zr0KXjQe.mjs → pipe-DiCyvyPN.mjs} +1 -0
  85. package/dist/pipeline/index.d.mts +1 -1
  86. package/dist/pipeline/index.mjs +1 -1
  87. package/dist/plugins/index.d.mts +5 -5
  88. package/dist/plugins/index.mjs +10 -10
  89. package/dist/plugins/response-cache.mjs +5 -5
  90. package/dist/plugins/tracing-entry.d.mts +1 -1
  91. package/dist/plugins/tracing-entry.mjs +1 -1
  92. package/dist/{pluralize-DQgqgifU.mjs → pluralize-B9M8xvy-.mjs} +2 -1
  93. package/dist/presets/filesUpload.d.mts +4 -4
  94. package/dist/presets/filesUpload.mjs +2 -2
  95. package/dist/presets/index.d.mts +1 -1
  96. package/dist/presets/index.mjs +1 -1
  97. package/dist/presets/multiTenant.d.mts +1 -1
  98. package/dist/presets/multiTenant.mjs +4 -3
  99. package/dist/presets/search.d.mts +2 -2
  100. package/dist/presets/search.mjs +1 -1
  101. package/dist/{presets-BbkjdPeH.mjs → presets-C9BE6WaZ.mjs} +2 -2
  102. package/dist/{queryCachePlugin-m1XsgAIJ.mjs → queryCachePlugin-B4XMSSe7.mjs} +2 -2
  103. package/dist/{queryCachePlugin-CqMdLI2-.d.mts → queryCachePlugin-Biqzfbi5.d.mts} +2 -2
  104. package/dist/{redis-DiMkdHEl.d.mts → redis-Cyzrz6SX.d.mts} +1 -1
  105. package/dist/{redis-stream-D6HzR1Z_.d.mts → redis-stream-DT-YjzrB.d.mts} +1 -1
  106. package/dist/registry/index.d.mts +319 -2
  107. package/dist/registry/index.mjs +3 -3
  108. package/dist/registry-BBE23CDj.mjs +576 -0
  109. package/dist/{routerShared-DrOa-26E.mjs → routerShared-CZV5aabX.mjs} +3 -3
  110. package/dist/scope/index.d.mts +3 -3
  111. package/dist/scope/index.mjs +3 -3
  112. package/dist/{sse-Bz-5ZeTt.mjs → sse-BY6sTy4P.mjs} +1 -1
  113. package/dist/testing/index.d.mts +2 -2
  114. package/dist/testing/index.mjs +16 -7
  115. package/dist/testing/storageContract.d.mts +1 -1
  116. package/dist/types/index.d.mts +5 -5
  117. package/dist/types/storage.d.mts +1 -1
  118. package/dist/{types-C_s5moIu.mjs → types-Bi0r0vjG.mjs} +53 -1
  119. package/dist/{types-BQsjgQzS.d.mts → types-BsJMEQ4D.d.mts} +106 -12
  120. package/dist/{types-DrBaUwyV.d.mts → types-D-fYtKjb.d.mts} +33 -10
  121. package/dist/{types-CTYvcwHe.d.mts → types-DVfpSfx2.d.mts} +42 -1
  122. package/dist/utils/index.d.mts +1286 -2
  123. package/dist/utils/index.mjs +1 -1
  124. package/dist/{utils-_h9B3c57.mjs → utils-DC5ycPfr.mjs} +89 -40
  125. package/dist/{buildHandler-CcFOpJLh.mjs → validate-By96rH0r.mjs} +8 -299
  126. package/dist/{versioning-hmkPcDlX.d.mts → versioning-ZwX9tmbS.d.mts} +1 -1
  127. package/package.json +21 -28
  128. package/skills/arc/SKILL.md +300 -706
  129. package/skills/arc/references/auth.md +19 -7
  130. package/skills/arc-code-review/SKILL.md +1 -1
  131. package/skills/arc-code-review/references/arc-cheatsheet.md +100 -322
  132. package/dist/createActionRouter-S3MLVYot.mjs +0 -220
  133. package/dist/index-bRjYu21O.d.mts +0 -1320
  134. package/dist/org/index.d.mts +0 -66
  135. package/dist/org/index.mjs +0 -486
  136. package/dist/org/types.d.mts +0 -82
  137. package/dist/org/types.mjs +0 -1
  138. package/dist/registry-I-ogLgL9.mjs +0 -46
  139. /package/dist/{EventTransport-CT_52aWU.d.mts → EventTransport-C-2oAHtw.d.mts} +0 -0
  140. /package/dist/{EventTransport-DLWoUMHy.mjs → EventTransport-Hxvv5QQz.mjs} +0 -0
  141. /package/dist/{actionPermissions-CyUkQu6O.mjs → actionPermissions-Bjmvn7Eb.mjs} +0 -0
  142. /package/dist/{elevation-BXOWoGCF.d.mts → elevation-0YBpa663.d.mts} +0 -0
  143. /package/dist/{elevation-DgoeTyfX.mjs → elevation-Dci0AYLT.mjs} +0 -0
  144. /package/dist/{errorHandler-DFr45ZG4.d.mts → errorHandler-mHuyWzZE.d.mts} +0 -0
  145. /package/dist/{externalPaths-BD5nw6St.d.mts → externalPaths-DFg-2KTp.d.mts} +0 -0
  146. /package/dist/{interface-beEtJyWM.d.mts → interface-CH0OQudo.d.mts} +0 -0
  147. /package/dist/{interface-DfLGcus7.d.mts → interface-NwJ_qPlY.d.mts} +0 -0
  148. /package/dist/{keys-CGcCbNyu.mjs → keys-DopsCuyQ.mjs} +0 -0
  149. /package/dist/{loadResources-DBMQg_Aj.mjs → loadResources-ChQEj8ih.mjs} +0 -0
  150. /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-TuOmguhi.mjs} +0 -0
  151. /package/dist/{replyHelpers-CK-FNO8E.mjs → replyHelpers-C-gD32oF.mjs} +0 -0
  152. /package/dist/{schemaIR-lYhC2gE5.mjs → schemaIR-Ctc89DSn.mjs} +0 -0
  153. /package/dist/{sessionManager-C4Le_UB3.d.mts → sessionManager-BqFegc0W.d.mts} +0 -0
  154. /package/dist/{storage-Dfzt4VTl.d.mts → storage-D2KZJAmn.d.mts} +0 -0
  155. /package/dist/{store-helpers-BkIN9-vu.mjs → store-helpers-B0sunfZZ.mjs} +0 -0
  156. /package/dist/{tracing-QJVprktp.d.mts → tracing-Dm8n7Cnn.d.mts} +0 -0
  157. /package/dist/{versioning-BUrT5aP4.mjs → versioning-B6mimogM.mjs} +0 -0
  158. /package/dist/{websocket-ChC2rqe1.d.mts → websocket-BkjeGZRn.d.mts} +0 -0
@@ -0,0 +1,576 @@
1
+ import { t as CRUD_OPERATIONS } from "./constants-TrJVIJl0.mjs";
2
+ import fp from "fastify-plugin";
3
+ //#region src/registry/assertNoTenantData.ts
4
+ /**
5
+ * Walk every cascading resource, run a tenant-scoped count, compare
6
+ * against the strategy's expected outcome.
7
+ *
8
+ * Designed for use inside compliance tests:
9
+ *
10
+ * ```ts
11
+ * import { assertNoTenantData } from '@classytic/arc/registry';
12
+ *
13
+ * it('after org delete, no tenant data leaks', async () => {
14
+ * await cascadeDeleteForOrganization(arc.registry, { organizationId: 'test-org' });
15
+ * const report = await assertNoTenantData(arc.registry, { organizationId: 'test-org' });
16
+ * expect(report.ok).toBe(true);
17
+ * expect(report.leaks).toHaveLength(0);
18
+ * });
19
+ * ```
20
+ */
21
+ async function assertNoTenantData(registry, options) {
22
+ const { organizationId, only, skipAnonymize = true } = options;
23
+ if (!organizationId) throw new Error("assertNoTenantData: `organizationId` is required");
24
+ const onlySet = only ? new Set(only) : void 0;
25
+ const leaks = [];
26
+ const skipped = [];
27
+ let checked = 0;
28
+ for (const r of registry.getAll()) {
29
+ const resolved = r.resolvedTenantPurge;
30
+ if (!resolved || resolved.source === "disabled") continue;
31
+ if (onlySet && !onlySet.has(r.name)) continue;
32
+ const tenantField = typeof r.tenantField === "string" && r.tenantField || "organizationId";
33
+ const strategy = resolved.strategy;
34
+ if (strategy.type === "skip") {
35
+ skipped.push({
36
+ resource: r.name,
37
+ reason: strategy.reason
38
+ });
39
+ continue;
40
+ }
41
+ if (strategy.type === "anonymize" && skipAnonymize) {
42
+ skipped.push({
43
+ resource: r.name,
44
+ reason: "anonymize (rows legitimately retained)"
45
+ });
46
+ continue;
47
+ }
48
+ const repo = registry.getAdapter(r.name)?.repository;
49
+ if (!repo?.count) {
50
+ skipped.push({
51
+ resource: r.name,
52
+ reason: "adapter repository has no `count` method — cannot verify"
53
+ });
54
+ continue;
55
+ }
56
+ const actual = await repo.count({ [tenantField]: organizationId });
57
+ checked++;
58
+ const expected = 0;
59
+ if (actual !== expected) leaks.push({
60
+ resource: r.name,
61
+ tenantField,
62
+ strategy: strategy.type,
63
+ expected,
64
+ actual
65
+ });
66
+ }
67
+ return {
68
+ ok: leaks.length === 0,
69
+ organizationId,
70
+ checked,
71
+ skipped,
72
+ leaks
73
+ };
74
+ }
75
+ //#endregion
76
+ //#region src/registry/purgeResource.ts
77
+ /**
78
+ * Run the resolved strategy against one resource's repository.
79
+ *
80
+ * - `skip` strategy: returns `path: 'skipped'` without touching the repo.
81
+ * - `purgeByField` present: routes through it (chunked, progress, abort).
82
+ * - `purgeByField` absent + hard strategy: legacy `deleteMany` fallback.
83
+ * - `purgeByField` absent + non-hard strategy: returns `path: 'unsupported'`
84
+ * with a clear error — soft/anonymize require the new primitive.
85
+ */
86
+ async function purgeResource(resourceName, tenantField, organizationId, resolved, repo, options = {}) {
87
+ const { strategy } = resolved;
88
+ if (strategy.type === "skip") return {
89
+ resource: resourceName,
90
+ tenantField,
91
+ strategy: "skip",
92
+ processed: 0,
93
+ ok: true,
94
+ path: "skipped",
95
+ skipReason: strategy.reason
96
+ };
97
+ if (typeof repo.purgeByField === "function") {
98
+ const result = await repo.purgeByField(tenantField, organizationId, strategy, {
99
+ batchSize: options.batchSize ?? resolved.batchSize,
100
+ onProgress: options.onProgress,
101
+ signal: options.signal
102
+ });
103
+ return {
104
+ resource: resourceName,
105
+ tenantField,
106
+ strategy: result.strategy,
107
+ processed: result.processed,
108
+ ok: result.ok,
109
+ path: "purgeByField",
110
+ durationMs: result.durationMs,
111
+ ...result.error ? { error: result.error } : {}
112
+ };
113
+ }
114
+ if (strategy.type !== "hard") return {
115
+ resource: resourceName,
116
+ tenantField,
117
+ strategy: strategy.type,
118
+ processed: 0,
119
+ ok: false,
120
+ path: "unsupported",
121
+ error: {
122
+ code: "arc.purge.unsupported_strategy",
123
+ message: `Resource '${resourceName}' uses strategy '${strategy.type}' but its adapter repository does not implement \`purgeByField\`. Upgrade the adapter (@classytic/mongokit ≥ 3.13.4 / @classytic/sqlitekit ≥ 0.3.4) or change the strategy to 'hard'.`
124
+ }
125
+ };
126
+ const op = repo.deleteMany ?? repo.deleteByFilter ?? repo.removeMany;
127
+ if (!op) return {
128
+ resource: resourceName,
129
+ tenantField,
130
+ strategy: "hard",
131
+ processed: 0,
132
+ ok: false,
133
+ path: "unsupported",
134
+ error: {
135
+ code: "arc.purge.no_bulk_op",
136
+ message: `Resource '${resourceName}' adapter exposes neither \`purgeByField\` nor \`deleteMany\` / \`deleteByFilter\` / \`removeMany\` — bulk cleanup is not supported.`
137
+ }
138
+ };
139
+ try {
140
+ const start = Date.now();
141
+ const result = await op.call(repo, { [tenantField]: organizationId });
142
+ return {
143
+ resource: resourceName,
144
+ tenantField,
145
+ strategy: "hard",
146
+ processed: typeof result === "number" ? result : typeof result?.deletedCount === "number" ? result.deletedCount : typeof result?.count === "number" ? result.count : -1,
147
+ ok: true,
148
+ path: "legacy-deleteMany",
149
+ durationMs: Date.now() - start
150
+ };
151
+ } catch (err) {
152
+ return {
153
+ resource: resourceName,
154
+ tenantField,
155
+ strategy: "hard",
156
+ processed: 0,
157
+ ok: false,
158
+ path: "legacy-deleteMany",
159
+ error: {
160
+ code: err?.code,
161
+ message: err instanceof Error ? err.message : String(err)
162
+ }
163
+ };
164
+ }
165
+ }
166
+ //#endregion
167
+ //#region src/registry/cascadeOrgDelete.ts
168
+ /**
169
+ * Names of resources flagged for cascade — used by audit scripts.
170
+ * A resource is cascading when its resolved strategy isn't `disabled`
171
+ * (i.e. the host declared `onTenantDelete`).
172
+ */
173
+ function getCascadingResources(registry) {
174
+ return registry.getAll().filter((r) => isResourceCascading(r)).map((r) => r.name);
175
+ }
176
+ /**
177
+ * Rich introspection — returns the resolved strategy + source per
178
+ * cascading resource. Use for audit dashboards that answer "what
179
+ * happens to this resource on org-delete?" without grepping the
180
+ * source.
181
+ */
182
+ function getCascadingResourcesWithMetadata(registry) {
183
+ return registry.getAll().filter((r) => isResourceCascading(r)).map((r) => {
184
+ const resolved = r.resolvedTenantPurge;
185
+ const tenantField = typeof r.tenantField === "string" && r.tenantField || "organizationId";
186
+ const r0 = resolved ?? {
187
+ strategy: { type: "hard" },
188
+ priority: 100,
189
+ source: "declared"
190
+ };
191
+ return {
192
+ name: r.name,
193
+ tenantField,
194
+ strategy: r0.strategy.type,
195
+ source: r0.source,
196
+ priority: r0.priority
197
+ };
198
+ });
199
+ }
200
+ /** A resource cascades iff its resolved strategy isn't `disabled`. */
201
+ function isResourceCascading(r) {
202
+ return r.resolvedTenantPurge ? r.resolvedTenantPurge.source !== "disabled" : false;
203
+ }
204
+ /**
205
+ * Cascade-cleanup every tenant-scoped resource for the given organization.
206
+ * Walks the registry in **ascending priority order** (`onTenantDelete.priority`
207
+ * — leaf data first, references last), runs each resource's resolved
208
+ * strategy via `purgeByField` when available (chunked, progress, abort),
209
+ * and returns a structured report.
210
+ *
211
+ * **Strategy resolution** lives in `resolveTenantPurge.ts` — this runner
212
+ * just reads `resource.resolvedTenantPurge` (computed once at boot).
213
+ *
214
+ * **Per-resource execution** lives in `purgeResource.ts` — preferred
215
+ * path is the kit's `purgeByField` (chunked + plugin-composed); falls
216
+ * back to legacy `deleteMany` only for `hard` strategy on adapters that
217
+ * haven't been upgraded.
218
+ *
219
+ * **Failure semantics**: continues on per-resource error, returns the
220
+ * full report. Hosts decide whether a partial cascade is a hard
221
+ * failure (re-throw) or degraded mode (log + alert).
222
+ *
223
+ * @param registry The arc resource registry (`fastify.arc.registry`).
224
+ * @param options Org id + filters + logger + progress + signal.
225
+ */
226
+ async function cascadeDeleteForOrganization(registry, options) {
227
+ const { organizationId, skip, only, logger, onProgress, signal, batchSize, concurrency = 1, checkpoint } = options;
228
+ if (!organizationId) throw new Error("cascadeDeleteForOrganization: `organizationId` is required");
229
+ if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error("cascadeDeleteForOrganization: `concurrency` must be a positive integer");
230
+ const skipSet = skip ? new Set(skip) : void 0;
231
+ const onlySet = only ? new Set(only) : void 0;
232
+ const start = Date.now();
233
+ const resumeState = await checkpoint?.read();
234
+ const completedSet = resumeState ? new Set(resumeState.completedResources) : void 0;
235
+ const flagged = registry.getAll().filter((r) => {
236
+ if (!r.resolvedTenantPurge || r.resolvedTenantPurge.source === "disabled") return false;
237
+ if (skipSet?.has(r.name)) return false;
238
+ if (onlySet && !onlySet.has(r.name)) return false;
239
+ if (completedSet?.has(r.name)) return false;
240
+ return true;
241
+ }).sort((a, b) => {
242
+ return (a.resolvedTenantPurge?.priority ?? 100) - (b.resolvedTenantPurge?.priority ?? 100);
243
+ });
244
+ const results = [];
245
+ const completed = resumeState ? [...resumeState.completedResources] : [];
246
+ const groups = groupByPriority(flagged);
247
+ for (const group of groups) {
248
+ if (signal?.aborted) break;
249
+ const groupResults = await runWithConcurrency(group, concurrency, async (r) => {
250
+ if (signal?.aborted) return;
251
+ return runOneResource(r, registry, organizationId, {
252
+ onProgress,
253
+ signal,
254
+ batchSize,
255
+ logger
256
+ });
257
+ });
258
+ for (const report of groupResults) {
259
+ if (!report) continue;
260
+ results.push(report);
261
+ if (!report.error) {
262
+ completed.push(report.resource);
263
+ if (checkpoint) await checkpoint.write({ completedResources: [...completed] });
264
+ }
265
+ }
266
+ }
267
+ const successes = results.filter((row) => !row.error);
268
+ return {
269
+ organizationId,
270
+ resources: results,
271
+ successes,
272
+ failures: results.filter((row) => row.error),
273
+ totalDeleted: successes.reduce((sum, r) => sum + (r.deletedCount > 0 ? r.deletedCount : 0), 0),
274
+ durationMs: Date.now() - start
275
+ };
276
+ }
277
+ /** Process a single registry entry under its resolved strategy. */
278
+ async function runOneResource(r, registry, organizationId, ctx) {
279
+ const tenantField = typeof r.tenantField === "string" && r.tenantField || "organizationId";
280
+ const repo = registry.getAdapter(r.name)?.repository;
281
+ if (!repo) {
282
+ const report = {
283
+ resource: r.name,
284
+ tenantField,
285
+ deletedCount: 0,
286
+ path: "unsupported",
287
+ error: {
288
+ code: "arc.no_adapter",
289
+ message: `Resource '${r.name}' has no adapter repository — cascade skipped`
290
+ }
291
+ };
292
+ ctx.logger?.warn?.(report, "[Arc/Cascade] resource skipped (no adapter)");
293
+ return report;
294
+ }
295
+ const resourceProgress = ctx.onProgress ? (event) => ctx.onProgress?.({
296
+ ...event,
297
+ resource: r.name
298
+ }) : void 0;
299
+ const outcome = await purgeResource(r.name, tenantField, organizationId, r.resolvedTenantPurge ?? {
300
+ strategy: { type: "hard" },
301
+ priority: 100,
302
+ source: "declared"
303
+ }, repo, {
304
+ onProgress: resourceProgress,
305
+ signal: ctx.signal,
306
+ batchSize: ctx.batchSize
307
+ });
308
+ const report = {
309
+ resource: outcome.resource,
310
+ tenantField: outcome.tenantField,
311
+ deletedCount: outcome.processed,
312
+ strategy: outcome.strategy,
313
+ strategySource: r.resolvedTenantPurge?.source,
314
+ path: outcome.path,
315
+ ...outcome.skipReason ? { skipReason: outcome.skipReason } : {},
316
+ ...outcome.error ? { error: outcome.error } : {}
317
+ };
318
+ if (outcome.ok) ctx.logger?.info?.(report, "[Arc/Cascade] resource processed");
319
+ else ctx.logger?.error?.({ report }, "[Arc/Cascade] resource cascade failed");
320
+ return report;
321
+ }
322
+ /**
323
+ * Group a priority-sorted list of resources into priority barriers.
324
+ * Each returned sub-array contains resources of identical priority;
325
+ * the outer array preserves ascending order. The cascade runner uses
326
+ * this so all priority-N resources finish before any priority-(N+M)
327
+ * starts — leaf-before-references invariant survives concurrency.
328
+ */
329
+ function groupByPriority(sorted) {
330
+ if (sorted.length === 0) return [];
331
+ const first = sorted[0];
332
+ if (!first) return [];
333
+ const groups = [];
334
+ let current = [first];
335
+ let currentPriority = first.resolvedTenantPurge?.priority ?? 100;
336
+ for (let i = 1; i < sorted.length; i++) {
337
+ const item = sorted[i];
338
+ if (!item) continue;
339
+ const p = item.resolvedTenantPurge?.priority ?? 100;
340
+ if (p === currentPriority) current.push(item);
341
+ else {
342
+ groups.push(current);
343
+ current = [item];
344
+ currentPriority = p;
345
+ }
346
+ }
347
+ groups.push(current);
348
+ return groups;
349
+ }
350
+ /**
351
+ * Run `fn` over `items` with bounded concurrency. Order of results
352
+ * matches input order. Simpler shape than a worker-pool because the
353
+ * group sizes are small (typically <=12 resources per priority band).
354
+ */
355
+ async function runWithConcurrency(items, concurrency, fn) {
356
+ if (concurrency === 1 || items.length <= 1) {
357
+ const results = [];
358
+ for (const item of items) results.push(await fn(item));
359
+ return results;
360
+ }
361
+ const results = new Array(items.length);
362
+ let cursor = 0;
363
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
364
+ while (true) {
365
+ const i = cursor++;
366
+ if (i >= items.length) break;
367
+ const item = items[i];
368
+ if (item === void 0) continue;
369
+ results[i] = await fn(item);
370
+ }
371
+ });
372
+ await Promise.all(workers);
373
+ return results;
374
+ }
375
+ //#endregion
376
+ //#region src/registry/introspectionPlugin.ts
377
+ const introspectionPlugin = async (fastify, opts = {}) => {
378
+ const { prefix = "/_resources", authRoles = ["superadmin"], enabled = true } = opts;
379
+ if (!enabled) {
380
+ fastify.log?.debug?.("Introspection plugin disabled");
381
+ return;
382
+ }
383
+ const typedFastify = fastify;
384
+ const authMiddleware = authRoles.length > 0 && typedFastify.authenticate ? [typedFastify.authenticate, typedFastify.authorize?.(...authRoles)].filter(Boolean) : [];
385
+ const getRegistry = () => fastify.arc?.registry;
386
+ await fastify.register(async (instance) => {
387
+ instance.get("/", { preHandler: authMiddleware }, async (_req, _reply) => {
388
+ return getRegistry()?.getIntrospection() ?? {
389
+ resources: [],
390
+ stats: {},
391
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
392
+ };
393
+ });
394
+ instance.get("/stats", { preHandler: authMiddleware }, async (_req, _reply) => {
395
+ return getRegistry()?.getStats() ?? {
396
+ totalResources: 0,
397
+ byModule: {},
398
+ presetUsage: {},
399
+ totalRoutes: 0,
400
+ totalEvents: 0
401
+ };
402
+ });
403
+ instance.get("/:name", {
404
+ schema: { params: {
405
+ type: "object",
406
+ properties: { name: { type: "string" } },
407
+ required: ["name"]
408
+ } },
409
+ preHandler: authMiddleware
410
+ }, async (req, reply) => {
411
+ const resource = getRegistry()?.get(req.params.name);
412
+ if (!resource) return reply.code(404).send({ error: `Resource '${req.params.name}' not found` });
413
+ return resource;
414
+ });
415
+ }, { prefix });
416
+ fastify.log?.debug?.(`Introspection API at ${prefix}`);
417
+ };
418
+ var introspectionPlugin_default = fp(introspectionPlugin, { name: "arc-introspection" });
419
+ //#endregion
420
+ //#region src/registry/manifest.ts
421
+ /**
422
+ * Resource manifest for frontend code generators (v2.15.5).
423
+ *
424
+ * Every host that hand-rolls a `createCrudApi('foo')` helper on the frontend
425
+ * ends up rewriting the same shim for declarative actions: `postAction(id,
426
+ * name, body)` — and forgets to add a method for every NEW action declared
427
+ * on the resource. The OpenAI-team report flagged this in fajr's
428
+ * `invoices.js`, and now matches needs the same.
429
+ *
430
+ * Arc owns the resource definition, so arc can ship the metadata FE codegen
431
+ * needs in one canonical shape:
432
+ *
433
+ * {
434
+ * name: 'invoice',
435
+ * prefix: '/invoices',
436
+ * idField: '_id',
437
+ * crudOps: ['list', 'get', 'create', 'update', 'delete'],
438
+ * actions: [
439
+ * { name: 'recordPayment', mount: '/:id/action', requiresId: true, description: '...' },
440
+ * { name: 'propose', mount: '/action', requiresId: false, description: '...' },
441
+ * ],
442
+ * }
443
+ *
444
+ * Hosts feed this into their FE generator (or a runtime `createActionsApi`
445
+ * shim) and every action is wired automatically. New actions land on the FE
446
+ * the same moment they're declared on the resource — no parallel list to
447
+ * keep in sync.
448
+ *
449
+ * The helper is BE-side only (lives under `@classytic/arc/registry`) — arc
450
+ * stays a backend framework and doesn't ship FE runtime code. The host
451
+ * decides whether the manifest powers a fetch-based helper, a TanStack
452
+ * Query factory, an RPC client, or static `.d.ts` generation.
453
+ *
454
+ * @example BE → emit JSON at build time / on a route
455
+ * ```ts
456
+ * // Build-time codegen
457
+ * import { buildResourceManifest } from '@classytic/arc/registry';
458
+ * import { invoiceResource } from './invoice.resource.js';
459
+ * writeFileSync('./fe-gen/invoice.manifest.json',
460
+ * JSON.stringify(buildResourceManifest(invoiceResource)));
461
+ *
462
+ * // Runtime introspection endpoint
463
+ * fastify.get('/manifest/:name', async (req) => {
464
+ * const r = arc.registry.get(req.params.name);
465
+ * return r ? buildResourceManifestFromRegistry(r) : reply.code(404);
466
+ * });
467
+ * ```
468
+ *
469
+ * @example FE → generate fetch methods from the manifest
470
+ * ```ts
471
+ * import manifest from './fe-gen/invoice.manifest.json';
472
+ * import { createActionsApi } from './my-fe-shim.js'; // host-owned
473
+ *
474
+ * export const invoicesApi = {
475
+ * ...createCrudApi(manifest.prefix),
476
+ * ...createActionsApi(manifest.prefix, manifest.actions),
477
+ * // → invoicesApi.recordPayment(id, body)
478
+ * // → invoicesApi.propose(body) // id-less
479
+ * };
480
+ * ```
481
+ */
482
+ /**
483
+ * Build a manifest from a `ResourceDefinition` (the value returned by
484
+ * `defineResource(...)`). Use at build time when you have direct access
485
+ * to the resource module — typically the simplest codegen path.
486
+ */
487
+ function buildResourceManifest(resource) {
488
+ const disabled = new Set(resource.disabledRoutes ?? []);
489
+ const crudOps = resource.disableDefaultRoutes ? [] : CRUD_OPERATIONS.filter((op) => !disabled.has(op));
490
+ const actions = [];
491
+ for (const [name, entry] of Object.entries(resource.actions ?? {})) {
492
+ const requiresId = typeof entry === "function" ? true : entry.id !== false;
493
+ actions.push({
494
+ name,
495
+ mount: requiresId ? "/:id/action" : "/action",
496
+ requiresId,
497
+ ...typeof entry !== "function" && entry.description ? { description: entry.description } : {}
498
+ });
499
+ }
500
+ const aggregations = [];
501
+ for (const [name, agg] of Object.entries(resource.aggregations ?? {})) aggregations.push({
502
+ name,
503
+ path: `${resource.prefix}/aggregations/${name}`,
504
+ ...agg.summary ? { summary: agg.summary } : {},
505
+ ...agg.description ? { description: agg.description } : {}
506
+ });
507
+ const customRoutes = [];
508
+ for (const route of resource.routes ?? []) customRoutes.push({
509
+ method: route.method,
510
+ path: `${resource.prefix}${route.path}`,
511
+ ...route.operation ? { operation: route.operation } : {},
512
+ ...route.summary ? { summary: route.summary } : {}
513
+ });
514
+ return {
515
+ name: resource.name,
516
+ displayName: resource.displayName,
517
+ prefix: resource.prefix,
518
+ idField: resource.idField ?? "_id",
519
+ updateMethod: resource.updateMethod ?? "PATCH",
520
+ crudOps,
521
+ actions,
522
+ aggregations,
523
+ customRoutes,
524
+ ...resource.tenantField !== void 0 ? { tenantField: resource.tenantField } : {}
525
+ };
526
+ }
527
+ /**
528
+ * Build a manifest from a `RegistryEntry` — use when the resource is
529
+ * already registered (introspection endpoint, runtime audit script).
530
+ *
531
+ * Equivalent to {@link buildResourceManifest} but reads from the projected
532
+ * registry shape, so a host that doesn't have the original `defineResource`
533
+ * value in scope (e.g. an FE-gen sidecar that scrapes a running server's
534
+ * `/_resources` endpoint) gets the same output without dragging the resource
535
+ * module into the codegen path.
536
+ */
537
+ function buildResourceManifestFromRegistry(entry) {
538
+ const disabled = new Set(entry.disabledRoutes ?? []);
539
+ const crudOps = entry.disableDefaultRoutes ? [] : CRUD_OPERATIONS.filter((op) => !disabled.has(op));
540
+ const actions = (entry.actions ?? []).map((a) => {
541
+ const requiresId = a.id !== false;
542
+ return {
543
+ name: a.name,
544
+ mount: requiresId ? "/:id/action" : "/action",
545
+ requiresId,
546
+ ...a.description ? { description: a.description } : {}
547
+ };
548
+ });
549
+ const aggregations = (entry.aggregations ?? []).map((agg) => ({
550
+ name: agg.name,
551
+ path: `${entry.prefix}/aggregations/${agg.name}`,
552
+ ...agg.summary ? { summary: agg.summary } : {},
553
+ ...agg.description ? { description: agg.description } : {}
554
+ }));
555
+ const customRoutes = (entry.customRoutes ?? []).map((r) => ({
556
+ method: r.method,
557
+ path: `${entry.prefix}${r.path}`,
558
+ ...r.operation ? { operation: r.operation } : {},
559
+ ...r.summary ? { summary: r.summary } : {}
560
+ }));
561
+ const updateMethod = entry.updateMethod ?? "PATCH";
562
+ return {
563
+ name: entry.name,
564
+ displayName: entry.displayName ?? entry.name,
565
+ prefix: entry.prefix,
566
+ idField: "_id",
567
+ updateMethod,
568
+ crudOps,
569
+ actions,
570
+ aggregations,
571
+ customRoutes,
572
+ ...entry.tenantField !== void 0 ? { tenantField: entry.tenantField } : {}
573
+ };
574
+ }
575
+ //#endregion
576
+ export { cascadeDeleteForOrganization as a, assertNoTenantData as c, introspectionPlugin_default as i, buildResourceManifestFromRegistry as n, getCascadingResources as o, introspectionPlugin as r, getCascadingResourcesWithMetadata as s, buildResourceManifest as t };
@@ -1,9 +1,9 @@
1
1
  import { f as createError } from "./errors-j4aJm1Wg.mjs";
2
- import { b as isMember, c as getOrgId, h as getUserId, n as PUBLIC_SCOPE, y as isElevated } from "./types-C_s5moIu.mjs";
2
+ import { b as isElevated, c as getOrgId, g as getUserId, n as PUBLIC_SCOPE, x as isMember } from "./types-Bi0r0vjG.mjs";
3
3
  import { t as requestContext } from "./requestContext-SSaaTgW8.mjs";
4
- import { I as evaluateAndApplyPermission, p as resolveEffectiveRoles, u as applyFieldReadPermissions } from "./permissions-ohQyv50e.mjs";
4
+ import { I as evaluateAndApplyPermission, p as resolveEffectiveRoles, u as applyFieldReadPermissions } from "./permissions-CTxMrreC.mjs";
5
5
  import { t as getUserRoles } from "./types-D57iXYb8.mjs";
6
- import { t as executePipeline } from "./pipe-Zr0KXjQe.mjs";
6
+ import { t as executePipeline } from "./pipe-DiCyvyPN.mjs";
7
7
  import { isPaginatedResult, toCanonicalList } from "@classytic/repo-core/pagination";
8
8
  //#region src/scope/projection.ts
9
9
  /**
@@ -1,5 +1,5 @@
1
- import { C as isOrgInScope, D as requireTeamId, E as requireOrgId, O as requireUserId, S as isMember, T as requireClientId, _ as getUserId, a as getAncestorOrgIds, b as isAuthenticated, c as getMandate, d as getOrgRoles, f as getRequestScope, g as getTeamId, h as getServiceScopes, i as RequestScope, l as getOrgContext, m as getScopeContextMap, n as Mandate, o as getClientId, p as getScopeContext, r as PUBLIC_SCOPE, s as getDPoPJkt, t as AUTHENTICATED_SCOPE, u as getOrgId, v as getUserRoles, w as isService, x as isElevated, y as hasOrgAccess } from "../types-CTYvcwHe.mjs";
2
- import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-BXOWoGCF.mjs";
1
+ import { C as isMember, D as requireOrgId, E as requireClientId, O as requireTeamId, S as isElevated, T as isService, _ as getTenantFromRequest, a as getAncestorOrgIds, b as hasOrgAccess, c as getMandate, d as getOrgRoles, f as getRequestScope, g as getTeamId, h as getServiceScopes, i as RequestScope, k as requireUserId, l as getOrgContext, m as getScopeContextMap, n as Mandate, o as getClientId, p as getScopeContext, r as PUBLIC_SCOPE, s as getDPoPJkt, t as AUTHENTICATED_SCOPE, u as getOrgId, v as getUserId, w as isOrgInScope, x as isAuthenticated, y as getUserRoles } from "../types-DVfpSfx2.mjs";
2
+ import { i as elevationPlugin, n as ElevationOptions, r as _default, t as ElevationEvent } from "../elevation-0YBpa663.mjs";
3
3
  import { FastifyReply, FastifyRequest } from "fastify";
4
4
 
5
5
  //#region src/scope/rateLimitKey.d.ts
@@ -30,4 +30,4 @@ interface ResolveOrgFromHeaderOptions {
30
30
  */
31
31
  declare function resolveOrgFromHeader(options: ResolveOrgFromHeaderOptions): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
32
32
  //#endregion
33
- export { AUTHENTICATED_SCOPE, type ElevationEvent, type ElevationOptions, type Mandate, PUBLIC_SCOPE, type RateLimitKeyContext, type RequestScope, type ResolveOrgFromHeaderOptions, type TenantKeyGeneratorOptions, createTenantKeyGenerator, _default as elevationPlugin, elevationPlugin as elevationPluginFn, getAncestorOrgIds, getClientId, getDPoPJkt, getMandate, getOrgContext, getOrgId, getOrgRoles, getRequestScope, getScopeContext, getScopeContextMap, getServiceScopes, getTeamId, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, isOrgInScope, isService, requireClientId, requireOrgId, requireTeamId, requireUserId, resolveOrgFromHeader };
33
+ export { AUTHENTICATED_SCOPE, type ElevationEvent, type ElevationOptions, type Mandate, PUBLIC_SCOPE, type RateLimitKeyContext, type RequestScope, type ResolveOrgFromHeaderOptions, type TenantKeyGeneratorOptions, createTenantKeyGenerator, _default as elevationPlugin, elevationPlugin as elevationPluginFn, getAncestorOrgIds, getClientId, getDPoPJkt, getMandate, getOrgContext, getOrgId, getOrgRoles, getRequestScope, getScopeContext, getScopeContextMap, getServiceScopes, getTeamId, getTenantFromRequest, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, isOrgInScope, isService, requireClientId, requireOrgId, requireTeamId, requireUserId, resolveOrgFromHeader };
@@ -1,6 +1,6 @@
1
- import { C as requireClientId, E as requireUserId, S as isService, T as requireTeamId, _ as hasOrgAccess, a as getDPoPJkt, b as isMember, c as getOrgId, d as getScopeContext, f as getScopeContextMap, g as getUserRoles, h as getUserId, i as getClientId, l as getOrgRoles, m as getTeamId, n as PUBLIC_SCOPE, o as getMandate, p as getServiceScopes, r as getAncestorOrgIds, s as getOrgContext, t as AUTHENTICATED_SCOPE, u as getRequestScope, v as isAuthenticated, w as requireOrgId, x as isOrgInScope, y as isElevated } from "../types-C_s5moIu.mjs";
1
+ import { C as isService, D as requireUserId, E as requireTeamId, S as isOrgInScope, T as requireOrgId, _ as getUserRoles, a as getDPoPJkt, b as isElevated, c as getOrgId, d as getScopeContext, f as getScopeContextMap, g as getUserId, h as getTenantFromRequest, i as getClientId, l as getOrgRoles, m as getTeamId, n as PUBLIC_SCOPE, o as getMandate, p as getServiceScopes, r as getAncestorOrgIds, s as getOrgContext, t as AUTHENTICATED_SCOPE, u as getRequestScope, v as hasOrgAccess, w as requireClientId, x as isMember, y as isAuthenticated } from "../types-Bi0r0vjG.mjs";
2
2
  import { n as normalizeRoles } from "../types-D57iXYb8.mjs";
3
- import { n as elevation_default, t as elevationPlugin } from "../elevation-DgoeTyfX.mjs";
3
+ import { n as elevation_default, t as elevationPlugin } from "../elevation-Dci0AYLT.mjs";
4
4
  //#region src/scope/rateLimitKey.ts
5
5
  function createTenantKeyGenerator(opts) {
6
6
  if (opts?.strategy) return opts.strategy;
@@ -76,4 +76,4 @@ function resolveOrgFromHeader(options) {
76
76
  };
77
77
  }
78
78
  //#endregion
79
- export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, createTenantKeyGenerator, elevation_default as elevationPlugin, elevationPlugin as elevationPluginFn, getAncestorOrgIds, getClientId, getDPoPJkt, getMandate, getOrgContext, getOrgId, getOrgRoles, getRequestScope, getScopeContext, getScopeContextMap, getServiceScopes, getTeamId, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, isOrgInScope, isService, requireClientId, requireOrgId, requireTeamId, requireUserId, resolveOrgFromHeader };
79
+ export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, createTenantKeyGenerator, elevation_default as elevationPlugin, elevationPlugin as elevationPluginFn, getAncestorOrgIds, getClientId, getDPoPJkt, getMandate, getOrgContext, getOrgId, getOrgRoles, getRequestScope, getScopeContext, getScopeContextMap, getServiceScopes, getTeamId, getTenantFromRequest, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, isOrgInScope, isService, requireClientId, requireOrgId, requireTeamId, requireUserId, resolveOrgFromHeader };
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
2
  import { arcLog } from "./logger/index.mjs";
3
- import { c as getOrgId, n as PUBLIC_SCOPE } from "./types-C_s5moIu.mjs";
3
+ import { c as getOrgId, n as PUBLIC_SCOPE } from "./types-Bi0r0vjG.mjs";
4
4
  import fp from "fastify-plugin";
5
5
  //#region src/plugins/sse.ts
6
6
  var sse_exports = /* @__PURE__ */ __exportAll({
@@ -1,5 +1,5 @@
1
- import { V as ResourceDefinition, Wt as AnyRecord } from "../index-BswOSJCE.mjs";
2
- import { d as ResourceLike, r as CreateAppOptions } from "../types-DrBaUwyV.mjs";
1
+ import { Kt as AnyRecord, V as ResourceDefinition } from "../index-CkW0flkU.mjs";
2
+ import { d as ResourceLike, r as CreateAppOptions } from "../types-D-fYtKjb.mjs";
3
3
  import { StorageContractSetup, StorageContractSetupResult, runStorageContract } from "./storageContract.mjs";
4
4
  import { FastifyInstance, FastifyServerOptions } from "fastify";
5
5
  import { Mock } from "vitest";