@classytic/arc 2.23.0 → 2.28.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 (147) hide show
  1. package/README.md +2 -0
  2. package/dist/{BaseController-BQ8KNvWC.mjs → BaseController-B65zbz9m.mjs} +94 -23
  3. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  4. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  5. package/dist/{applyPermissionResult-Tw4iXtlf.mjs → applyPermissionResult-BnyVE9eD.mjs} +10 -0
  6. package/dist/audit/index.d.mts +32 -3
  7. package/dist/audit/index.mjs +7 -4
  8. package/dist/auth/index.d.mts +2 -2
  9. package/dist/auth/index.mjs +263 -259
  10. package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
  11. package/dist/cache/index.d.mts +41 -3
  12. package/dist/cache/index.mjs +33 -4
  13. package/dist/cleanup/index.d.mts +313 -0
  14. package/dist/cleanup/index.mjs +1267 -0
  15. package/dist/cli/commands/describe.d.mts +1 -1
  16. package/dist/cli/commands/docs.mjs +1 -1
  17. package/dist/cli/commands/generate.mjs +1 -1
  18. package/dist/cli/commands/init.mjs +1 -1
  19. package/dist/cli/index.mjs +2 -2
  20. package/dist/context/index.mjs +2 -64
  21. package/dist/core/index.d.mts +3 -3
  22. package/dist/core/index.mjs +5 -4
  23. package/dist/core-BGa8TXJR.mjs +221 -0
  24. package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-C-N7z2EL.mjs} +3 -2
  25. package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  26. package/dist/{createApp-CjOeWJ9_.mjs → createApp-Du5G54TM.mjs} +187 -96
  27. package/dist/{core-DQ6rDTyH.mjs → defineResource-2zvVVpxg.mjs} +297 -230
  28. package/dist/docs/index.d.mts +1 -1
  29. package/dist/docs/index.mjs +2 -2
  30. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  31. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  32. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  33. package/dist/events/index.d.mts +83 -314
  34. package/dist/events/index.mjs +183 -95
  35. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  36. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  37. package/dist/factory/index.d.mts +5 -5
  38. package/dist/factory/index.mjs +30 -8
  39. package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-OzYGIiU1.d.mts} +26 -2
  40. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  41. package/dist/headers-BvlQSDMB.mjs +45 -0
  42. package/dist/health-CB_m4S32.mjs +377 -0
  43. package/dist/hooks/index.d.mts +1 -1
  44. package/dist/idempotency/index.mjs +27 -3
  45. package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
  46. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  47. package/dist/index-CZY4EGga.d.mts +107 -0
  48. package/dist/{index-CC5ruDGh.d.mts → index-CjVg-Epg.d.mts} +37 -5
  49. package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
  50. package/dist/index.d.mts +6 -6
  51. package/dist/index.mjs +7 -6
  52. package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
  53. package/dist/integrations/event-gateway.d.mts +1 -1
  54. package/dist/integrations/event-gateway.mjs +1 -1
  55. package/dist/integrations/index.d.mts +3 -3
  56. package/dist/integrations/jobs/index.d.mts +2 -0
  57. package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
  58. package/dist/integrations/mcp/index.d.mts +4 -4
  59. package/dist/integrations/mcp/index.mjs +2 -2
  60. package/dist/integrations/mcp/testing.d.mts +1 -1
  61. package/dist/integrations/mcp/testing.mjs +1 -1
  62. package/dist/integrations/webhooks.d.mts +37 -30
  63. package/dist/integrations/webhooks.mjs +51 -62
  64. package/dist/integrations/websocket-redis.d.mts +1 -1
  65. package/dist/integrations/websocket.d.mts +1 -1
  66. package/dist/integrations/websocket.mjs +41 -14
  67. package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
  68. package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cs6nRGCt.mjs} +12 -10
  69. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  70. package/dist/middleware/index.d.mts +17 -1
  71. package/dist/middleware/index.mjs +1 -1
  72. package/dist/migrations/index.d.mts +94 -44
  73. package/dist/migrations/index.mjs +196 -44
  74. package/dist/module-_-M7C5ES.mjs +29 -0
  75. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  76. package/dist/{openapi-jSDx-VDv.mjs → openapi-BLV4OMXb.mjs} +3 -3
  77. package/dist/permissions/index.d.mts +2 -2
  78. package/dist/permissions/index.mjs +3 -3
  79. package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
  80. package/dist/pipeline/index.d.mts +1 -1
  81. package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BGRnV-To.mjs} +4 -27
  82. package/dist/plugins/index.d.mts +6 -77
  83. package/dist/plugins/index.mjs +11 -95
  84. package/dist/plugins/response-cache.mjs +1 -0
  85. package/dist/plugins/tracing-entry.mjs +1 -1
  86. package/dist/{index-Ba10fdKp.d.mts → preloadResources-CM4uUaFv.d.mts} +51 -50
  87. package/dist/preloadResources-DSY5HGpS.mjs +45 -0
  88. package/dist/presets/filesUpload.d.mts +3 -3
  89. package/dist/presets/filesUpload.mjs +3 -3
  90. package/dist/presets/index.d.mts +2 -1
  91. package/dist/presets/index.mjs +1 -1
  92. package/dist/presets/multiTenant.d.mts +1 -1
  93. package/dist/presets/multiTenant.mjs +1 -1
  94. package/dist/presets/search.d.mts +2 -2
  95. package/dist/presets/search.mjs +1 -1
  96. package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
  97. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  98. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  99. package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  100. package/dist/{realtime-I8OrfnMR.mjs → realtime-DEuDu9io.mjs} +3 -3
  101. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  102. package/dist/registerRoute-DTp18Ah_.mjs +108 -0
  103. package/dist/registry/index.d.mts +2 -2
  104. package/dist/registry/index.mjs +1 -1
  105. package/dist/{registry-vcOCtPij.mjs → registry-9km_w1aP.mjs} +15 -0
  106. package/dist/renewingLease-3cuQH7h8.mjs +44 -0
  107. package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
  108. package/dist/schedules-CjOd2MG9.mjs +113 -0
  109. package/dist/schemas/index.d.mts +67 -67
  110. package/dist/schemas/index.mjs +2 -3
  111. package/dist/scim/index.mjs +1 -2
  112. package/dist/scope/index.d.mts +2 -2
  113. package/dist/scope/index.mjs +4 -4
  114. package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
  115. package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
  116. package/dist/testing/cleanupStoreContract.d.mts +10 -0
  117. package/dist/testing/cleanupStoreContract.mjs +375 -0
  118. package/dist/testing/index.d.mts +5 -3
  119. package/dist/testing/index.mjs +4 -2
  120. package/dist/testing/outboxStoreContract.d.mts +13 -0
  121. package/dist/testing/outboxStoreContract.mjs +247 -0
  122. package/dist/transactionContext-PNdoyLLm.mjs +64 -0
  123. package/dist/types/index.d.mts +4 -4
  124. package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
  125. package/dist/types-C_ZZRcD6.d.mts +450 -0
  126. package/dist/{module-B4u10iOJ.d.mts → types-Cu6vVJmh.d.mts} +196 -60
  127. package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  128. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  129. package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
  130. package/dist/usage/index.d.mts +3 -52
  131. package/dist/usage/index.mjs +0 -0
  132. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  133. package/dist/utils/index.d.mts +58 -85
  134. package/dist/utils/index.mjs +4 -2
  135. package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
  136. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  137. package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
  138. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  139. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  140. package/package.json +541 -513
  141. package/skills/arc/references/api-reference.md +3 -1
  142. package/skills/arc/references/integrations.md +20 -11
  143. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  144. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  145. package/dist/health-ScyjIVo5.mjs +0 -202
  146. package/dist/requestId-5BhTVzBv.mjs +0 -113
  147. package/dist/types-XTApa8b6.mjs +0 -24
@@ -1,4 +1,59 @@
1
+ import { t as startRenewingLease } from "../renewingLease-3cuQH7h8.mjs";
2
+ import { createHash, randomUUID } from "node:crypto";
1
3
  //#region src/migrations/index.ts
4
+ /**
5
+ * Schema Versioning and Migrations System
6
+ *
7
+ * Manages database schema changes over time with version tracking.
8
+ * Supports forward migrations, rollbacks, and schema compatibility layers.
9
+ *
10
+ * DB-agnostic: the `db` parameter is typed as `unknown` — the user passes
11
+ * whatever connection object their adapter uses (Mongoose db, Prisma client,
12
+ * Knex instance, etc.) and their `up`/`down` functions cast it internally.
13
+ *
14
+ * @example
15
+ * import { defineMigration, MigrationRunner } from '@classytic/arc/migrations';
16
+ *
17
+ * const productV2 = defineMigration({
18
+ * version: 2,
19
+ * resource: 'product',
20
+ * up: async (db) => {
21
+ * const mongo = db as import('mongoose').mongo.Db;
22
+ * await mongo.collection('products').updateMany(
23
+ * {},
24
+ * { $rename: { 'oldField': 'newField' } }
25
+ * );
26
+ * },
27
+ * down: async (db) => {
28
+ * const mongo = db as import('mongoose').mongo.Db;
29
+ * await mongo.collection('products').updateMany(
30
+ * {},
31
+ * { $rename: { 'newField': 'oldField' } }
32
+ * );
33
+ * },
34
+ * });
35
+ *
36
+ * const runner = new MigrationRunner(mongoose.connection.db, {
37
+ * store: new MongoMigrationStore(mongoose.connection.db),
38
+ * lock: createMongoLockAdapter({ connection }), // multi-replica safety
39
+ * });
40
+ * await runner.up(migrations);
41
+ *
42
+ * ## Deployment discipline
43
+ *
44
+ * Migrations should be executed by ONE owner — a deploy job, a CI step, or
45
+ * a leader — not by every replica on startup. The optional `lock` (any
46
+ * ecosystem `LockAdapter`) makes concurrent runners fail fast instead of
47
+ * double-applying, and the pending set is recomputed INSIDE the lock so a
48
+ * runner that waited on another deploy sees its writes. Two windows remain
49
+ * host concerns and are deliberately documented instead of hidden:
50
+ * - a crash between `up()` and `record()` re-executes that migration on
51
+ * the next run — write migrations to be idempotent (`$rename`,
52
+ * `IF NOT EXISTS`, upserts);
53
+ * - `up()` and `record()` are not one transaction — stores backed by a
54
+ * transactional DB can override `record()` to join the migration's
55
+ * transaction.
56
+ */
2
57
  /** Default logger that writes to stdout/stderr */
3
58
  const defaultLogger = {
4
59
  info: (msg) => process.stdout.write(`${msg}\n`),
@@ -14,13 +69,14 @@ var MongoMigrationStore = class {
14
69
  async getApplied() {
15
70
  return await this.db.collection(this.collectionName).find({}).sort({ appliedAt: 1 }).toArray();
16
71
  }
17
- async record(migration, executionTime) {
72
+ async record(migration, executionTime, meta) {
18
73
  await this.db.collection(this.collectionName).insertOne({
19
74
  version: migration.version,
20
75
  resource: migration.resource,
21
76
  description: migration.description,
22
77
  appliedAt: /* @__PURE__ */ new Date(),
23
- executionTime
78
+ executionTime,
79
+ ...meta?.checksum ? { checksum: meta.checksum } : {}
24
80
  });
25
81
  }
26
82
  async remove(migration) {
@@ -30,6 +86,16 @@ var MongoMigrationStore = class {
30
86
  });
31
87
  }
32
88
  };
89
+ const MIGRATION_LOCK_NAME = "arc:migrations";
90
+ const DEFAULT_LOCK_LEASE_MS = 600 * 1e3;
91
+ /**
92
+ * Source checksum for drift detection — SHA-256 over the migration's
93
+ * `up`/`down` source. Stable for a given build; a transpile-target change
94
+ * can legitimately shift it (hence `checksumMismatch: 'warn'` default).
95
+ */
96
+ function computeMigrationChecksum(migration) {
97
+ return createHash("sha256").update(migration.up.toString()).update("\n").update(migration.down.toString()).digest("hex");
98
+ }
33
99
  /**
34
100
  * Define a migration
35
101
  */
@@ -62,62 +128,148 @@ var MigrationRunner = class {
62
128
  db;
63
129
  store;
64
130
  log;
131
+ lock;
132
+ lockLeaseMs;
133
+ holderId;
134
+ checksumMismatch;
65
135
  constructor(db, opts) {
66
136
  this.db = db;
67
137
  this.store = opts.store;
68
138
  this.log = opts.logger ?? defaultLogger;
139
+ this.lock = opts.lock;
140
+ this.lockLeaseMs = opts.lockLeaseMs ?? DEFAULT_LOCK_LEASE_MS;
141
+ this.holderId = opts.holderId ?? `migration-runner-${randomUUID()}`;
142
+ this.checksumMismatch = opts.checksumMismatch ?? "warn";
69
143
  }
70
144
  /**
71
- * Run all pending migrations
145
+ * Acquire the migration lease (when a lock is configured) and run `fn`.
146
+ * Fail-fast on contention: a concurrent deploy should error loudly, not
147
+ * queue — the OTHER runner is applying the same set.
148
+ *
149
+ * The lease renews while `fn` runs (`startRenewingLease` — serialized,
150
+ * awaited teardown), so a migration slower than `lockLeaseMs` can't let
151
+ * a second deployment acquire the lock mid-run. Ownership loss is
152
+ * ENFORCED, not just logged: `fn` receives a `leaseLost` probe to refuse
153
+ * starting further migrations, and even a successful `fn` fails the run
154
+ * when the lease was lost — a concurrent runner may have interleaved, so
155
+ * the applied state must be verified, not trusted. (A stale holder's
156
+ * in-flight write can only be truly fenced by a token from the lock
157
+ * contract — planned repo-core evolution.)
72
158
  */
73
- async up(migrations) {
74
- const applied = await this.store.getApplied();
75
- const appliedVersions = new Set(applied.map((m) => `${m.resource}:${m.version}`));
76
- const pending = migrations.filter((m) => !appliedVersions.has(`${m.resource}:${m.version}`)).sort((a, b) => a.version - b.version);
77
- if (pending.length === 0) {
78
- this.log.info("No pending migrations");
79
- return;
159
+ async withLock(fn) {
160
+ const lock = this.lock;
161
+ if (!lock) return fn(() => false);
162
+ if (!await lock.tryAcquire(MIGRATION_LOCK_NAME, this.holderId, this.lockLeaseMs)) throw new Error(`MigrationRunner: another runner holds the migration lock — refusing to run concurrently. If a previous runner crashed, the lease expires on its own (leaseMs: ${this.lockLeaseMs}).`);
163
+ const lease = startRenewingLease({
164
+ lock,
165
+ name: MIGRATION_LOCK_NAME,
166
+ holderId: this.holderId,
167
+ leaseMs: this.lockLeaseMs,
168
+ onLost: () => this.log.error("MigrationRunner: lease renewal lost — another holder took the migration lock; no further migrations will start and this run will fail."),
169
+ onError: (error) => this.log.error(`MigrationRunner: lease renewal errored (lease may lapse mid-run): ${error.message}`)
170
+ });
171
+ try {
172
+ const result = await fn(() => lease.lost);
173
+ if (lease.lost) throw new Error("MigrationRunner: migration lock ownership was lost mid-run — a concurrent runner may have executed migrations in parallel. Verify the applied state (getAppliedMigrations) before re-running.");
174
+ return result;
175
+ } finally {
176
+ await lease.stop();
177
+ try {
178
+ await lock.release(MIGRATION_LOCK_NAME, this.holderId);
179
+ } catch (error) {
180
+ this.log.error(`MigrationRunner: lock release failed: ${error.message}`);
181
+ }
80
182
  }
81
- this.log.info(`Running ${pending.length} migration(s)...`);
82
- for (const migration of pending) await this.runMigration(migration, "up");
83
- this.log.info("All migrations completed successfully");
84
183
  }
85
184
  /**
86
- * Rollback last migration
185
+ * Drift detection: compare each APPLIED record's stored checksum against
186
+ * the current source of the same migration. A mismatch means the file was
187
+ * edited after being applied — its `down()` may no longer invert what
188
+ * actually ran.
87
189
  */
88
- async down(migrations) {
89
- const applied = await this.store.getApplied();
90
- if (applied.length === 0) {
91
- this.log.info("No migrations to rollback");
92
- return;
93
- }
94
- const last = applied[applied.length - 1];
95
- if (!last) {
96
- this.log.info("No migrations to rollback");
97
- return;
190
+ verifyChecksums(applied, migrations) {
191
+ for (const record of applied) {
192
+ if (!record.checksum) continue;
193
+ const current = migrations.find((m) => m.resource === record.resource && m.version === record.version);
194
+ if (!current) continue;
195
+ const checksum = computeMigrationChecksum(current);
196
+ if (checksum !== record.checksum) {
197
+ const msg = `Migration ${record.resource}:${record.version} source changed after it was applied (checksum ${record.checksum.slice(0, 12)}… → ${checksum.slice(0, 12)}…). Ship a NEW migration instead of editing an applied one.`;
198
+ if (this.checksumMismatch === "error") throw new Error(`MigrationRunner: ${msg}`);
199
+ this.log.error(`WARNING: ${msg}`);
200
+ }
98
201
  }
99
- const migration = migrations.find((m) => m.resource === last.resource && m.version === last.version);
100
- if (!migration) throw new Error(`Migration ${last.resource}:${last.version} not found in migration files`);
101
- this.log.info(`Rolling back ${migration.resource} v${migration.version}...`);
102
- await this.runMigration(migration, "down", true);
103
- this.log.info("Rollback completed");
104
202
  }
105
203
  /**
106
- * Rollback to specific version
204
+ * Run all pending migrations.
205
+ *
206
+ * With a `lock` configured, the applied set is (re)read INSIDE the lease —
207
+ * a runner that raced another deploy computes pending against the winner's
208
+ * completed writes, not a stale pre-lock snapshot.
107
209
  */
108
- async downTo(migrations, targetVersion) {
109
- const toRollback = (await this.store.getApplied()).filter((m) => m.version > targetVersion).reverse();
110
- if (toRollback.length === 0) {
111
- this.log.info(`Already at or below version ${targetVersion}`);
112
- return;
113
- }
114
- this.log.info(`Rolling back ${toRollback.length} migration(s)...`);
115
- for (const record of toRollback) {
116
- const migration = migrations.find((m) => m.resource === record.resource && m.version === record.version);
117
- if (!migration) throw new Error(`Migration ${record.resource}:${record.version} not found`);
210
+ async up(migrations) {
211
+ await this.withLock(async (leaseLost) => {
212
+ const applied = await this.store.getApplied();
213
+ this.verifyChecksums(applied, migrations);
214
+ const appliedVersions = new Set(applied.map((m) => `${m.resource}:${m.version}`));
215
+ const pending = migrations.filter((m) => !appliedVersions.has(`${m.resource}:${m.version}`)).sort((a, b) => a.version - b.version);
216
+ if (pending.length === 0) {
217
+ this.log.info("No pending migrations");
218
+ return;
219
+ }
220
+ this.log.info(`Running ${pending.length} migration(s)...`);
221
+ for (const migration of pending) {
222
+ if (leaseLost()) throw new Error("MigrationRunner: refusing to start the next migration — lock ownership lost.");
223
+ await this.runMigration(migration, "up");
224
+ }
225
+ this.log.info("All migrations completed successfully");
226
+ });
227
+ }
228
+ /**
229
+ * Rollback last migration
230
+ */
231
+ async down(migrations) {
232
+ await this.withLock(async () => {
233
+ const applied = await this.store.getApplied();
234
+ const last = applied[applied.length - 1];
235
+ if (!last) {
236
+ this.log.info("No migrations to rollback");
237
+ return;
238
+ }
239
+ const migration = migrations.find((m) => m.resource === last.resource && m.version === last.version);
240
+ if (!migration) throw new Error(`Migration ${last.resource}:${last.version} not found in migration files`);
241
+ this.log.info(`Rolling back ${migration.resource} v${migration.version}...`);
118
242
  await this.runMigration(migration, "down", true);
119
- }
120
- this.log.info("Rollback completed");
243
+ this.log.info("Rollback completed");
244
+ });
245
+ }
246
+ /**
247
+ * Rollback to a specific version.
248
+ *
249
+ * Versions are RESOURCE-scoped, so a bare numeric threshold is only
250
+ * meaningful within one resource. When the applied set spans multiple
251
+ * resources, `options.resource` is required — rolling every resource back
252
+ * across a shared number silently reverts unrelated schemas.
253
+ */
254
+ async downTo(migrations, targetVersion, options) {
255
+ await this.withLock(async (leaseLost) => {
256
+ const applied = await this.store.getApplied();
257
+ const resources = new Set(applied.map((m) => m.resource));
258
+ if (!options?.resource && resources.size > 1) throw new Error(`MigrationRunner.downTo: applied migrations span ${resources.size} resources (${[...resources].sort().join(", ")}) — versions are resource-scoped, so pass { resource: '<name>' } to select which one to roll back.`);
259
+ const toRollback = (options?.resource ? applied.filter((m) => m.resource === options.resource) : applied).filter((m) => m.version > targetVersion).reverse();
260
+ if (toRollback.length === 0) {
261
+ this.log.info(`Already at or below version ${targetVersion}`);
262
+ return;
263
+ }
264
+ this.log.info(`Rolling back ${toRollback.length} migration(s)...`);
265
+ for (const record of toRollback) {
266
+ if (leaseLost()) throw new Error("MigrationRunner: refusing to start the next rollback — lock ownership lost.");
267
+ const migration = migrations.find((m) => m.resource === record.resource && m.version === record.version);
268
+ if (!migration) throw new Error(`Migration ${record.resource}:${record.version} not found`);
269
+ await this.runMigration(migration, "down", true);
270
+ }
271
+ this.log.info("Rollback completed");
272
+ });
121
273
  }
122
274
  /**
123
275
  * Get all applied migrations
@@ -154,7 +306,7 @@ var MigrationRunner = class {
154
306
  if (migration.validate) {
155
307
  if (!await migration.validate(this.db)) throw new Error("Migration validation failed");
156
308
  }
157
- await this.store.record(migration, Date.now() - start);
309
+ await this.store.record(migration, Date.now() - start, { checksum: computeMigrationChecksum(migration) });
158
310
  } else {
159
311
  await migration.down(this.db);
160
312
  if (isRollback) await this.store.remove(migration);
@@ -232,4 +384,4 @@ var MigrationRegistry = class {
232
384
  }
233
385
  };
234
386
  //#endregion
235
- export { MigrationRegistry, MigrationRunner, MongoMigrationStore, defineMigration, withSchemaVersion };
387
+ export { MigrationRegistry, MigrationRunner, MongoMigrationStore, computeMigrationChecksum, defineMigration, withSchemaVersion };
@@ -0,0 +1,29 @@
1
+ //#region src/factory/module/index.ts
2
+ /**
3
+ * Identity helper for authoring a typed module — mirrors `defineResource`.
4
+ * Gives inference + a single obvious construction site; does no work.
5
+ *
6
+ * `TExports` is inferred from the `bootstrap` return value, so a module
7
+ * author gets a typed public export for free:
8
+ *
9
+ * ```ts
10
+ * export const accountingModule = (deps: Deps) =>
11
+ * defineModule({
12
+ * name: "accounting",
13
+ * bootstrap: async () => createAccountingEngine(deps), // TExports inferred
14
+ * });
15
+ * ```
16
+ */
17
+ function defineModule(module) {
18
+ return module;
19
+ }
20
+ function getModuleExports(fastify, name) {
21
+ const modules = fastify.arc?.modules;
22
+ if (!modules || !Object.hasOwn(modules, name)) {
23
+ const available = modules ? Object.keys(modules) : [];
24
+ throw new Error(`[arc] no public export recorded for module "${name}".\nA module's export is its bootstrap() return value; it is readable only AFTER that module's bootstrap ran (dependsOn / list order = init order).\nModules with recorded exports: ${available.length ? available.join(", ") : "(none)"}.`);
25
+ }
26
+ return modules[name];
27
+ }
28
+ //#endregion
29
+ export { getModuleExports as n, defineModule as t };
@@ -2,6 +2,7 @@ import sjson from "secure-json-parse";
2
2
  //#region src/middleware/multipartBody.ts
3
3
  const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
4
4
  const DEFAULT_MAX_FILES = 5;
5
+ const DEFAULT_MAX_TOTAL_BYTES = 25 * 1024 * 1024;
5
6
  const DEFAULT_FILES_KEY = "_files";
6
7
  /**
7
8
  * Build a matcher for MIME allow-lists that supports exact (e.g. `image/png`),
@@ -45,10 +46,19 @@ function buildMimeMatcher(allowed) {
45
46
  * For non-multipart requests (regular JSON), this is a no-op — the request
46
47
  * passes through unchanged. This makes it safe to add to create/update
47
48
  * middlewares without breaking JSON clients.
49
+ *
50
+ * **BUFFERED — for small uploads only.** Every accepted file is held fully
51
+ * in process memory for the request's lifetime; memory cost is
52
+ * `concurrency × maxTotalBytes`. Suitable for avatars, documents, and
53
+ * form attachments. For large files use PRESIGNED direct-to-object-storage
54
+ * uploads (see the files-upload preset / production guide) and pair upload
55
+ * routes with strict rate limits — buffering video through the app tier is
56
+ * the wrong architecture regardless of limits.
48
57
  */
49
58
  function multipartBody(options = {}) {
50
59
  const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
51
60
  const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
61
+ const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
52
62
  const mimeMatcher = buildMimeMatcher(options.allowedMimeTypes);
53
63
  const filesKey = options.filesKey ?? DEFAULT_FILES_KEY;
54
64
  const requiredFields = options.requiredFields && options.requiredFields.length > 0 ? options.requiredFields : void 0;
@@ -61,6 +71,7 @@ function multipartBody(options = {}) {
61
71
  const body = {};
62
72
  const files = {};
63
73
  let fileCount = 0;
74
+ let totalBytes = 0;
64
75
  try {
65
76
  const parts = request.parts();
66
77
  for await (const part of parts) if (part.type === "file") {
@@ -76,6 +87,12 @@ function multipartBody(options = {}) {
76
87
  message: `File '${part.filename}' exceeds maximum size of ${Math.round(maxFileSize / 1024 / 1024)}MB`,
77
88
  status: 413
78
89
  });
90
+ totalBytes += buffer.length;
91
+ if (totalBytes > maxTotalBytes) return reply.code(413).send({
92
+ code: "arc.payload_too_large",
93
+ message: `Combined upload size exceeds ${Math.round(maxTotalBytes / 1024 / 1024)}MB per request`,
94
+ status: 413
95
+ });
79
96
  files[part.fieldname] = {
80
97
  filename: part.filename,
81
98
  mimetype: part.mimetype,
@@ -1,8 +1,8 @@
1
- import { t as getUserRoles } from "./types-XTApa8b6.mjs";
1
+ import { n as getUserRoles } from "./userHelpers-Cg_y3fBz.mjs";
2
2
  import { n as convertRouteSchema } from "./schemaConverter-DJvodiHj.mjs";
3
- import { t as resolveActionPermission } from "./actionPermissions-lszoJle1.mjs";
4
3
  import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
5
- import { t as buildActionBodySchema } from "./createActionRouter-BuOv-m4Y.mjs";
4
+ import { t as resolveActionPermission } from "./actionPermissions-CAphcmUG.mjs";
5
+ import { t as buildActionBodySchema } from "./createActionRouter-C-N7z2EL.mjs";
6
6
  import fp from "fastify-plugin";
7
7
  //#region src/docs/openapi/canonical-schemas.ts
8
8
  /**
@@ -1,4 +1,4 @@
1
- import { a as getUserRoles, i as UserBase, n as PermissionContext, o as normalizeRoles, r as PermissionResult, t as PermissionCheck } from "../types-Csxga7d3.mjs";
1
+ import { i as UserBase, n as PermissionContext, o as getUserRoles, r as PermissionResult, s as normalizeRoles, t as PermissionCheck } from "../types-Dmw-6Qeo.mjs";
2
2
  import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, n as FieldPermissionMap, o as fields, r as FieldPermissionType, s as resolveEffectiveRoles, t as FieldPermission } from "../fields-D3lhgzhD.mjs";
3
- import { A as allOf, B as applyPermissionResult, C as requireGrant, D as PermissionEventBus, E as DynamicPermissionMatrixConfig, F as requireAuth, G as requireDPoP, H as RequireAgentScopeOptions, I as requireOwnership, K as requireMandate, L as requireRoles, M as anyOf, N as denyAll, O as createDynamicPermissionMatrix, P as not, R as roles, S as modeSatisfies, T as DynamicPermissionMatrix, U as RequireMandateOptions, V as normalizePermissionResult, W as requireAgentScope, _ as createRoleHierarchy, a as permissionMatrix, b as GrantResolution, c as publicReadAdminWrite, d as requireOrgMembership, f as requireOrgRole, g as RoleHierarchy, h as requireTeamMembership, i as ownerWithAdminBypass, j as allowPublic, k as createOrgPermissions, l as readOnly, m as requireServiceScope, n as authenticated, o as presets_d_exports, p as requireScopeContext, r as fullPublic, s as publicRead, t as adminOnly, u as requireOrgInScope, v as GRANT_MODES, w as ConnectEventsOptions, x as RequireGrantOptions, y as GrantMode, z as when } from "../index-wMoWBLkn.mjs";
3
+ import { A as allOf, B as applyPermissionResult, C as requireGrant, D as PermissionEventBus, E as DynamicPermissionMatrixConfig, F as requireAuth, G as requireDPoP, H as RequireAgentScopeOptions, I as requireOwnership, K as requireMandate, L as requireRoles, M as anyOf, N as denyAll, O as createDynamicPermissionMatrix, P as not, R as roles, S as modeSatisfies, T as DynamicPermissionMatrix, U as RequireMandateOptions, V as normalizePermissionResult, W as requireAgentScope, _ as createRoleHierarchy, a as permissionMatrix, b as GrantResolution, c as publicReadAdminWrite, d as requireOrgMembership, f as requireOrgRole, g as RoleHierarchy, h as requireTeamMembership, i as ownerWithAdminBypass, j as allowPublic, k as createOrgPermissions, l as readOnly, m as requireServiceScope, n as authenticated, o as presets_d_exports, p as requireScopeContext, r as fullPublic, s as publicRead, t as adminOnly, u as requireOrgInScope, v as GRANT_MODES, w as ConnectEventsOptions, x as RequireGrantOptions, y as GrantMode, z as when } from "../index-BMnbzvtO.mjs";
4
4
  export { type ConnectEventsOptions, type DynamicPermissionMatrix, type DynamicPermissionMatrixConfig, type FieldPermission, type FieldPermissionMap, type FieldPermissionType, GRANT_MODES, type GrantMode, type GrantResolution, type PermissionCheck, type PermissionContext, type PermissionEventBus, type PermissionResult, type RequireAgentScopeOptions, type RequireGrantOptions, type RequireMandateOptions, type RoleHierarchy, type UserBase, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, modeSatisfies, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, permissionMatrix, presets_d_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAgentScope, requireAuth, requireDPoP, requireGrant, requireMandate, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };
@@ -1,4 +1,4 @@
1
- import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, o as fields, r as normalizePermissionResult, s as resolveEffectiveRoles, t as applyPermissionResult } from "../applyPermissionResult-Tw4iXtlf.mjs";
2
- import { n as normalizeRoles, t as getUserRoles } from "../types-XTApa8b6.mjs";
3
- import { A as roles, C as allowPublic, D as requireAuth, E as not, M as requireAgentScope, N as requireDPoP, O as requireOwnership, P as requireMandate, S as allOf, T as denyAll, _ as requireOrgMembership, a as permissionMatrix, b as requireServiceScope, c as publicReadAdminWrite, d as GRANT_MODES, f as modeSatisfies, g as requireOrgInScope, h as createOrgPermissions, i as ownerWithAdminBypass, j as when, k as requireRoles, l as readOnly, m as createDynamicPermissionMatrix, n as authenticated, o as presets_exports, p as requireGrant, r as fullPublic, s as publicRead, t as adminOnly, u as createRoleHierarchy, v as requireOrgRole, w as anyOf, x as requireTeamMembership, y as requireScopeContext } from "../permissions-tbxFHxjZ.mjs";
1
+ import { n as getUserRoles, r as normalizeRoles } from "../userHelpers-Cg_y3fBz.mjs";
2
+ import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, o as fields, r as normalizePermissionResult, s as resolveEffectiveRoles, t as applyPermissionResult } from "../applyPermissionResult-BnyVE9eD.mjs";
3
+ import { A as requireAuth, C as requireDPoP, D as anyOf, E as allowPublic, M as requireRoles, N as roles, O as denyAll, P as when, S as requireAgentScope, T as allOf, _ as requireOrgMembership, a as permissionMatrix, b as requireServiceScope, c as publicReadAdminWrite, d as GRANT_MODES, f as modeSatisfies, g as requireOrgInScope, h as createOrgPermissions, i as ownerWithAdminBypass, j as requireOwnership, k as not, l as readOnly, m as createDynamicPermissionMatrix, n as authenticated, o as presets_exports, p as requireGrant, r as fullPublic, s as publicRead, t as adminOnly, u as createRoleHierarchy, v as requireOrgRole, w as requireMandate, x as requireTeamMembership, y as requireScopeContext } from "../permissions-BrtiBKGs.mjs";
4
4
  export { GRANT_MODES, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, modeSatisfies, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, permissionMatrix, presets_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAgentScope, requireAuth, requireDPoP, requireGrant, requireMandate, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };