@classytic/arc 2.9.1 → 2.10.8

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 (132) hide show
  1. package/README.md +20 -91
  2. package/dist/{BaseController-Vu2yc56T.mjs → BaseController-DVNKvoX4.mjs} +154 -170
  3. package/dist/{ResourceRegistry-Dq3_zBQP.mjs → ResourceRegistry-CcN2LVrc.mjs} +1 -1
  4. package/dist/actionPermissions-TUVR3uiZ.mjs +22 -0
  5. package/dist/adapters/index.d.mts +3 -3
  6. package/dist/adapters/index.mjs +2 -2
  7. package/dist/{adapters-BBqAVvPK.mjs → adapters-BXY4i-hw.mjs} +210 -41
  8. package/dist/audit/index.d.mts +38 -3
  9. package/dist/audit/index.mjs +54 -22
  10. package/dist/auth/index.d.mts +2 -2
  11. package/dist/auth/index.mjs +3 -3
  12. package/dist/cache/index.d.mts +17 -15
  13. package/dist/cache/index.mjs +16 -15
  14. package/dist/{caching-CjybdRwx.mjs → caching-3h93rkJM.mjs} +8 -3
  15. package/dist/cli/commands/describe.mjs +1 -1
  16. package/dist/cli/commands/docs.mjs +2 -2
  17. package/dist/cli/commands/init.mjs +1 -1
  18. package/dist/cli/commands/introspect.mjs +1 -1
  19. package/dist/context/index.d.mts +58 -0
  20. package/dist/context/index.mjs +2 -0
  21. package/dist/core/index.d.mts +2 -2
  22. package/dist/core/index.mjs +3 -4
  23. package/dist/{defineResource-C__jkwvs.mjs → core-3MWJosCH.mjs} +174 -94
  24. package/dist/{createActionRouter-DH1YFL9m.mjs → createActionRouter-C8UUB3Px.mjs} +1 -1
  25. package/dist/{createApp-CBJUJKGP.mjs → createApp-BwnEAO2h.mjs} +53 -19
  26. package/dist/docs/index.d.mts +1 -1
  27. package/dist/docs/index.mjs +2 -2
  28. package/dist/{elevation-DxQ6ACbt.mjs → elevation-Dci0AYLT.mjs} +2 -2
  29. package/dist/errorHandler-2ii4RIYr.d.mts +114 -0
  30. package/dist/{errorHandler-CZDW4EXS.mjs → errorHandler-CSxe7KIM.mjs} +1 -1
  31. package/dist/{eventPlugin-Dl7MoVWH.mjs → eventPlugin-ByU4Cv0e.mjs} +1 -1
  32. package/dist/{eventPlugin-BxvaCIZF.d.mts → eventPlugin-D1ThQ1Pp.d.mts} +1 -1
  33. package/dist/events/index.d.mts +8 -5
  34. package/dist/events/index.mjs +87 -52
  35. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  36. package/dist/events/transports/redis.d.mts +1 -1
  37. package/dist/factory/index.d.mts +1 -1
  38. package/dist/factory/index.mjs +1 -1
  39. package/dist/{types-DZi1aYhm.d.mts → fields-C8Y0XLAu.d.mts} +122 -2
  40. package/dist/hooks/index.d.mts +1 -1
  41. package/dist/idempotency/index.d.mts +5 -2
  42. package/dist/idempotency/index.mjs +46 -37
  43. package/dist/{interface-YrWsmKqE.d.mts → index-BGbpGVyM.d.mts} +2107 -2756
  44. package/dist/{index-CtGKT0lf.d.mts → index-BziRPS4H.d.mts} +81 -7
  45. package/dist/{index-C-xjcA6F.d.mts → index-C_Noptz-.d.mts} +284 -409
  46. package/dist/{index-Cibkchnx.d.mts → index-EqQN6p0W.d.mts} +3 -3
  47. package/dist/index.d.mts +6 -219
  48. package/dist/index.mjs +10 -131
  49. package/dist/integrations/event-gateway.d.mts +1 -1
  50. package/dist/integrations/event-gateway.mjs +1 -1
  51. package/dist/integrations/index.d.mts +1 -1
  52. package/dist/integrations/mcp/index.d.mts +2 -2
  53. package/dist/integrations/mcp/index.mjs +1 -1
  54. package/dist/integrations/mcp/testing.d.mts +1 -1
  55. package/dist/integrations/mcp/testing.mjs +1 -1
  56. package/dist/interface-yhyb_pLY.d.mts +77 -0
  57. package/dist/logger/index.d.mts +81 -0
  58. package/dist/{logger-CDjpjySd.mjs → logger/index.mjs} +1 -6
  59. package/dist/{memory-BFAYkf8H.mjs → memory-DqI-449b.mjs} +23 -8
  60. package/dist/middleware/index.d.mts +109 -0
  61. package/dist/middleware/index.mjs +70 -0
  62. package/dist/multipartBody-CUQGVlM_.mjs +123 -0
  63. package/dist/{openapi-CXuTG1M9.mjs → openapi-DpNpqBmo.mjs} +9 -7
  64. package/dist/org/index.d.mts +2 -2
  65. package/dist/permissions/index.d.mts +3 -4
  66. package/dist/permissions/index.mjs +5 -5
  67. package/dist/{permissions-oNZawnkR.mjs → permissions-wkqRwicB.mjs} +315 -397
  68. package/dist/pipe-CGJxqDGx.mjs +62 -0
  69. package/dist/pipeline/index.d.mts +62 -0
  70. package/dist/pipeline/index.mjs +53 -0
  71. package/dist/plugins/index.d.mts +23 -3
  72. package/dist/plugins/index.mjs +9 -11
  73. package/dist/plugins/response-cache.mjs +1 -1
  74. package/dist/plugins/tracing-entry.mjs +1 -1
  75. package/dist/presets/filesUpload.d.mts +3 -3
  76. package/dist/presets/filesUpload.mjs +255 -1
  77. package/dist/presets/index.d.mts +1 -1
  78. package/dist/presets/index.mjs +2 -2
  79. package/dist/presets/multiTenant.d.mts +1 -1
  80. package/dist/presets/multiTenant.mjs +43 -9
  81. package/dist/presets/search.d.mts +91 -4
  82. package/dist/presets/search.mjs +1 -1
  83. package/dist/{presets-hM4WhNWY.mjs → presets-CrwOvuXI.mjs} +1 -1
  84. package/dist/{queryCachePlugin-DbUVroUG.mjs → queryCachePlugin-ChLNZvFT.mjs} +9 -9
  85. package/dist/{queryCachePlugin-CnTZZTC5.d.mts → queryCachePlugin-Dumka73q.d.mts} +1 -1
  86. package/dist/{queryParser-Cs-6SHQK.mjs → queryParser-NR__Qiju.mjs} +69 -2
  87. package/dist/{redis-stream-Bz-4q96t.d.mts → redis-stream-bkO88VHx.d.mts} +1 -1
  88. package/dist/registry/index.d.mts +1 -1
  89. package/dist/registry/index.mjs +1 -1
  90. package/dist/{requestContext-DYtmNpm5.mjs → requestContext-C38GskNt.mjs} +1 -1
  91. package/dist/{resourceToTools-C3cWymnW.mjs → resourceToTools-BhF3JV5p.mjs} +8 -3
  92. package/dist/scope/index.d.mts +2 -2
  93. package/dist/scope/index.mjs +2 -2
  94. package/dist/{sse-CJpt7LGI.mjs → sse-D8UeDwis.mjs} +1 -1
  95. package/dist/{store-helpers-DFiZl5TL.mjs → store-helpers-DYYUQbQN.mjs} +4 -0
  96. package/dist/testing/index.d.mts +6 -5
  97. package/dist/testing/index.mjs +17 -10
  98. package/dist/types/index.d.mts +5 -5
  99. package/dist/types/index.mjs +1 -31
  100. package/dist/types-CDnTEpga.mjs +27 -0
  101. package/dist/{types-CoSzA-s-.d.mts → types-CVKBssX5.d.mts} +1 -1
  102. package/dist/{types-CunEX4UX.d.mts → types-CVdgPXBW.d.mts} +20 -7
  103. package/dist/utils/index.d.mts +277 -3
  104. package/dist/utils/index.mjs +4 -5
  105. package/dist/{utils-B7FuRr9w.mjs → utils-LMwVidKy.mjs} +303 -2
  106. package/dist/{versioning-Cm8qoFDg.mjs → versioning-B6mimogM.mjs} +3 -5
  107. package/dist/versioning-CeUXHfjw.d.mts +117 -0
  108. package/package.json +31 -18
  109. package/skills/arc/SKILL.md +8 -12
  110. package/skills/arc/references/production.md +0 -41
  111. package/dist/circuitBreaker-CvXkjfrW.d.mts +0 -206
  112. package/dist/circuitBreaker-l18oRgL5.mjs +0 -284
  113. package/dist/core-DNncu0xF.mjs +0 -34
  114. package/dist/dynamic/index.d.mts +0 -93
  115. package/dist/dynamic/index.mjs +0 -122
  116. package/dist/errorHandler-DixGcttC.d.mts +0 -218
  117. package/dist/fields-BC7zcmI9.d.mts +0 -121
  118. package/dist/filesUpload-q8oHt--L.mjs +0 -377
  119. package/dist/interface-DplgQO2e.d.mts +0 -54
  120. package/dist/policies/index.d.mts +0 -425
  121. package/dist/policies/index.mjs +0 -318
  122. package/dist/rpc/index.d.mts +0 -90
  123. package/dist/rpc/index.mjs +0 -248
  124. /package/dist/{EventTransport-CqZ8FyM_.d.mts → EventTransport-CfVEGaEl.d.mts} +0 -0
  125. /package/dist/{applyPermissionResult-bqGpo9ML.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
  126. /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
  127. /package/dist/{elevation-B6S5csVA.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
  128. /package/dist/{errors-CqWnSqM-.mjs → errors-BqdUDja_.mjs} +0 -0
  129. /package/dist/{fields-CU6FlaDV.mjs → fields-CTMWOUDt.mjs} +0 -0
  130. /package/dist/{keys-qcD-TVJl.mjs → keys-nWQGUTu1.mjs} +0 -0
  131. /package/dist/{types-ZUu_h0jp.mjs → types-D57iXYb8.mjs} +0 -0
  132. /package/dist/{types-BD85MlEK.d.mts → types-tgR4Pt8F.d.mts} +0 -0
@@ -1,6 +1,6 @@
1
- import { i as versionKey, n as hashParams, r as tagVersionKey, t as buildQueryKey } from "../keys-qcD-TVJl.mjs";
2
- import { t as MemoryCacheStore } from "../memory-BFAYkf8H.mjs";
3
- import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-DbUVroUG.mjs";
1
+ import { i as versionKey, n as hashParams, r as tagVersionKey, t as buildQueryKey } from "../keys-nWQGUTu1.mjs";
2
+ import { t as MemoryCacheStore } from "../memory-DqI-449b.mjs";
3
+ import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-ChLNZvFT.mjs";
4
4
  //#region src/cache/redis.ts
5
5
  /**
6
6
  * Redis-backed cache store.
@@ -11,14 +11,14 @@ var RedisCacheStore = class {
11
11
  name = "redis-cache";
12
12
  client;
13
13
  prefix;
14
- defaultTtlMs;
14
+ defaultTtlSeconds;
15
15
  maxEntryBytes;
16
16
  _hits = 0;
17
17
  _misses = 0;
18
18
  constructor(options) {
19
19
  this.client = options.client;
20
20
  this.prefix = options.prefix ?? "arc:cache:";
21
- this.defaultTtlMs = options.defaultTtlMs ?? 6e4;
21
+ this.defaultTtlSeconds = options.defaultTtlSeconds ?? 60;
22
22
  this.maxEntryBytes = options.maxEntryBytes ?? 0;
23
23
  }
24
24
  async get(key) {
@@ -36,22 +36,23 @@ var RedisCacheStore = class {
36
36
  return;
37
37
  }
38
38
  }
39
- async set(key, value, options = {}) {
40
- const ttlMs = options.ttlMs ?? this.defaultTtlMs;
41
- if (!Number.isFinite(ttlMs) || ttlMs <= 0) return;
39
+ async set(key, value, ttlSeconds) {
40
+ const effectiveTtlSeconds = ttlSeconds ?? this.defaultTtlSeconds;
41
+ if (!Number.isFinite(effectiveTtlSeconds) || effectiveTtlSeconds <= 0) return;
42
42
  const payload = JSON.stringify(value);
43
43
  if (this.maxEntryBytes > 0 && Buffer.byteLength(payload, "utf8") > this.maxEntryBytes) return;
44
- await this.client.set(this.withPrefix(key), payload, { PX: Math.ceil(ttlMs) });
44
+ await this.client.set(this.withPrefix(key), payload, { EX: Math.ceil(effectiveTtlSeconds) });
45
45
  }
46
46
  async delete(key) {
47
47
  await this.client.del(this.withPrefix(key));
48
48
  }
49
- async clear() {
50
- await this.scanAndDelete(`${this.prefix}*`);
51
- }
52
- /** Delete all keys matching `this.prefix + prefix + *`. Returns count deleted. */
53
- async deleteByPrefix(prefix) {
54
- return this.scanAndDelete(`${this.prefix}${prefix}*`);
49
+ /**
50
+ * Invalidate keys. Pass a glob pattern to delete a subset (`user:*:v2`);
51
+ * omit to clear every key under this store's prefix.
52
+ */
53
+ async clear(pattern) {
54
+ const scanPattern = pattern ? `${this.prefix}${pattern.includes("*") ? pattern : `${pattern}*`}` : `${this.prefix}*`;
55
+ await this.scanAndDelete(scanPattern);
55
56
  }
56
57
  stats() {
57
58
  return {
@@ -34,7 +34,7 @@ const cachingPlugin = async (fastify, opts = {}) => {
34
34
  if (rule?.staleWhileRevalidate) parts.push(`stale-while-revalidate=${rule.staleWhileRevalidate}`);
35
35
  return parts.join(", ");
36
36
  }
37
- fastify.addHook("onSend", async (request, reply, payload) => {
37
+ fastify.addHook("preSerialization", async (request, reply, payload) => {
38
38
  const url = request.url;
39
39
  if (exclude.some((p) => url.startsWith(p))) return payload;
40
40
  const method = request.method.toUpperCase();
@@ -48,13 +48,18 @@ const cachingPlugin = async (fastify, opts = {}) => {
48
48
  const rule = findRule(url);
49
49
  reply.header("cache-control", buildCacheControl(rule));
50
50
  }
51
- if (etag && payload) {
52
- const tag = `"${fnv1a(typeof payload === "string" ? payload : String(payload))}"`;
51
+ if (etag && payload != null) {
52
+ let body;
53
+ if (typeof payload === "string") body = payload;
54
+ else if (Buffer.isBuffer(payload)) body = payload.toString("utf-8");
55
+ else body = JSON.stringify(payload);
56
+ const tag = `"${fnv1a(body)}"`;
53
57
  reply.header("etag", tag);
54
58
  if (conditional) {
55
59
  const ifNoneMatch = request.headers["if-none-match"];
56
60
  if (ifNoneMatch && ifNoneMatch === tag) {
57
61
  reply.code(304);
62
+ reply.serializer((p) => typeof p === "string" ? p : "");
58
63
  return "";
59
64
  }
60
65
  }
@@ -1,4 +1,4 @@
1
- import { t as CRUD_OPERATIONS } from "../../constants-Cxde4rpC.mjs";
1
+ import { t as CRUD_OPERATIONS } from "../../constants-BhY1OHoH.mjs";
2
2
  import { resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  //#region src/cli/commands/describe.ts
@@ -1,5 +1,5 @@
1
- import { t as ResourceRegistry } from "../../ResourceRegistry-Dq3_zBQP.mjs";
2
- import { t as buildOpenApiSpec } from "../../openapi-CXuTG1M9.mjs";
1
+ import { t as ResourceRegistry } from "../../ResourceRegistry-CcN2LVrc.mjs";
2
+ import { t as buildOpenApiSpec } from "../../openapi-DpNpqBmo.mjs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { mkdirSync, writeFileSync } from "node:fs";
@@ -102,7 +102,7 @@ async function installDependencies(projectPath, config, pm) {
102
102
  ];
103
103
  if (config.auth === "better-auth") deps.push("better-auth@^1.6.0", "mongodb@latest");
104
104
  else deps.push("@fastify/jwt@latest", "bcryptjs@latest");
105
- if (config.adapter === "mongokit") deps.push("@classytic/mongokit@^3.5.5", "mongoose@^9.4.1");
105
+ if (config.adapter === "mongokit") deps.push("@classytic/mongokit@^3.11.0", "@classytic/repo-core@^0.2.0", "mongoose@^9.4.1");
106
106
  const devDeps = ["vitest@latest", "pino-pretty@latest"];
107
107
  if (config.typescript) devDeps.push("typescript@latest", "@types/node@latest", "tsx@latest");
108
108
  const installCmd = getInstallCommand(pm, deps, false);
@@ -1,4 +1,4 @@
1
- import { t as ResourceRegistry } from "../../ResourceRegistry-Dq3_zBQP.mjs";
1
+ import { t as ResourceRegistry } from "../../ResourceRegistry-CcN2LVrc.mjs";
2
2
  import { resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  //#region src/cli/commands/introspect.ts
@@ -0,0 +1,58 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ //#region src/context/requestContext.d.ts
4
+ /**
5
+ * Shape of the request-scoped context store.
6
+ * Populated by Arc's onRequest hook in arcCorePlugin.
7
+ */
8
+ interface RequestStore {
9
+ /** Unique request identifier */
10
+ requestId?: string;
11
+ /** Authenticated user (if any) */
12
+ user?: {
13
+ id?: string;
14
+ _id?: string;
15
+ roles?: string[];
16
+ [key: string]: unknown;
17
+ } | null;
18
+ /** Active organization ID (multi-tenant) */
19
+ organizationId?: string;
20
+ /** Active team ID (team-scoped resources) */
21
+ teamId?: string;
22
+ /** Current resource name (set by arcDecorator in CRUD routes) */
23
+ resourceName?: string;
24
+ /** Request start time (for timing) */
25
+ startTime: number;
26
+ /** Additional context — extensible by app */
27
+ [key: string]: unknown;
28
+ }
29
+ /**
30
+ * Request context API.
31
+ *
32
+ * - `get()` — returns current store or undefined if outside request scope
33
+ * - `run(store, fn)` — run a function with a specific store (used by Arc internals)
34
+ * - `getStore()` — alias for get() (matches Node.js API naming)
35
+ */
36
+ declare const requestContext: {
37
+ /**
38
+ * Get the current request context.
39
+ * Returns undefined if called outside a request lifecycle.
40
+ */
41
+ get(): RequestStore | undefined;
42
+ /**
43
+ * Alias for get() — matches Node.js AsyncLocalStorage API naming.
44
+ */
45
+ getStore(): RequestStore | undefined;
46
+ /**
47
+ * Run a function within a specific request context.
48
+ * Used internally by Arc's onRequest hook.
49
+ */
50
+ run<T>(store: RequestStore, fn: () => T): T;
51
+ /**
52
+ * The underlying AsyncLocalStorage instance.
53
+ * Exposed for advanced use cases (testing, custom integrations).
54
+ */
55
+ storage: AsyncLocalStorage<RequestStore>;
56
+ };
57
+ //#endregion
58
+ export { type RequestStore, requestContext };
@@ -0,0 +1,2 @@
1
+ import { t as requestContext } from "../requestContext-C38GskNt.mjs";
2
+ export { requestContext };
@@ -1,3 +1,3 @@
1
- import { At as BaseControllerOptions, Ft as AccessControl, It as AccessControlConfig, Kt as ResourceDefinition, Mt as QueryResolverConfig, Nt as BodySanitizer, Pt as BodySanitizerConfig, jt as QueryResolver, kt as BaseController, qt as defineResource } from "../interface-YrWsmKqE.mjs";
2
- import { C as MAX_REGEX_LENGTH, D as RESERVED_QUERY_PARAMS, E as MutationOperation, O as SYSTEM_FIELDS, S as MAX_FILTER_DEPTH, T as MUTATION_OPERATIONS, _ as DEFAULT_UPDATE_METHOD, a as getControllerScope, b as HookOperation, c as createCrudRouter, d as CrudOperation, f as DEFAULT_ID_FIELD, g as DEFAULT_TENANT_FIELD, h as DEFAULT_SORT, i as getControllerContext, l as createPermissionMiddleware, m as DEFAULT_MAX_LIMIT, n as createFastifyHandler, o as sendControllerResponse, p as DEFAULT_LIMIT, r as createRequestContext, s as defineResourceVariants, t as createCrudHandlers, u as CRUD_OPERATIONS, v as HOOK_OPERATIONS, w as MAX_SEARCH_LENGTH, x as HookPhase, y as HOOK_PHASES } from "../index-Cibkchnx.mjs";
1
+ import { B as ResourceDefinition, V as defineResource, an as BaseControllerOptions, cn as BodySanitizer, dn as AccessControlConfig, in as BaseController, ln as BodySanitizerConfig, on as QueryResolver, sn as QueryResolverConfig, un as AccessControl } from "../index-BGbpGVyM.mjs";
2
+ import { C as MAX_REGEX_LENGTH, D as RESERVED_QUERY_PARAMS, E as MutationOperation, O as SYSTEM_FIELDS, S as MAX_FILTER_DEPTH, T as MUTATION_OPERATIONS, _ as DEFAULT_UPDATE_METHOD, a as getControllerScope, b as HookOperation, c as createCrudRouter, d as CrudOperation, f as DEFAULT_ID_FIELD, g as DEFAULT_TENANT_FIELD, h as DEFAULT_SORT, i as getControllerContext, l as createPermissionMiddleware, m as DEFAULT_MAX_LIMIT, n as createFastifyHandler, o as sendControllerResponse, p as DEFAULT_LIMIT, r as createRequestContext, s as defineResourceVariants, t as createCrudHandlers, u as CRUD_OPERATIONS, v as HOOK_OPERATIONS, w as MAX_SEARCH_LENGTH, x as HookPhase, y as HOOK_PHASES } from "../index-EqQN6p0W.mjs";
3
3
  export { AccessControl, AccessControlConfig, BaseController, BaseControllerOptions, BodySanitizer, BodySanitizerConfig, CRUD_OPERATIONS, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineResource, defineResourceVariants, getControllerContext, getControllerScope, sendControllerResponse };
@@ -1,5 +1,4 @@
1
- import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "../constants-Cxde4rpC.mjs";
2
- import { i as AccessControl, n as QueryResolver, r as BodySanitizer, t as BaseController } from "../BaseController-Vu2yc56T.mjs";
3
- import { c as createCrudHandlers, d as getControllerContext, f as getControllerScope, l as createFastifyHandler, n as defineResource, o as createCrudRouter, p as sendControllerResponse, s as createPermissionMiddleware, t as ResourceDefinition, u as createRequestContext } from "../defineResource-C__jkwvs.mjs";
4
- import { t as defineResourceVariants } from "../core-DNncu0xF.mjs";
1
+ import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "../constants-BhY1OHoH.mjs";
2
+ import { i as AccessControl, n as QueryResolver, r as BodySanitizer, t as BaseController } from "../BaseController-DVNKvoX4.mjs";
3
+ import { c as createPermissionMiddleware, d as createRequestContext, f as getControllerContext, l as createCrudHandlers, m as sendControllerResponse, n as ResourceDefinition, p as getControllerScope, r as defineResource, s as createCrudRouter, t as defineResourceVariants, u as createFastifyHandler } from "../core-3MWJosCH.mjs";
5
4
  export { AccessControl, BaseController, BodySanitizer, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineResource, defineResourceVariants, getControllerContext, getControllerScope, sendControllerResponse };
@@ -1,73 +1,29 @@
1
- import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-Cxde4rpC.mjs";
2
- import { _ as isElevated, n as PUBLIC_SCOPE, v as isMember } from "./types-AOD8fxIw.mjs";
3
- import { t as BaseController } from "./BaseController-Vu2yc56T.mjs";
4
- import { i as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-CU6FlaDV.mjs";
5
- import { t as getUserRoles } from "./types-ZUu_h0jp.mjs";
6
- import { r as ForbiddenError } from "./errors-CqWnSqM-.mjs";
7
- import { t as requestContext } from "./requestContext-DYtmNpm5.mjs";
8
- import { n as normalizePermissionResult, t as applyPermissionResult } from "./applyPermissionResult-bqGpo9ML.mjs";
9
- import { i as getDefaultCrudSchemas } from "./utils-B7FuRr9w.mjs";
1
+ import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-BhY1OHoH.mjs";
2
+ import { _ as isElevated, n as PUBLIC_SCOPE, o as getOrgId, p as getUserId, v as isMember } from "./types-AOD8fxIw.mjs";
3
+ import { t as BaseController } from "./BaseController-DVNKvoX4.mjs";
4
+ import { i as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-CTMWOUDt.mjs";
5
+ import { t as getUserRoles } from "./types-D57iXYb8.mjs";
6
+ import { t as requestContext } from "./requestContext-C38GskNt.mjs";
7
+ import { n as normalizePermissionResult, t as applyPermissionResult } from "./applyPermissionResult-QhV1Pa-g.mjs";
8
+ import { i as getDefaultCrudSchemas } from "./utils-LMwVidKy.mjs";
10
9
  import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-BxFDdtXu.mjs";
11
10
  import { t as hasEvents } from "./typeGuards-Cj5Rgvlg.mjs";
12
- import { r as getAvailablePresets, t as applyPresets } from "./presets-hM4WhNWY.mjs";
13
- //#region src/pipeline/pipe.ts
11
+ import { t as executePipeline } from "./pipe-CGJxqDGx.mjs";
12
+ import { r as getAvailablePresets, t as applyPresets } from "./presets-CrwOvuXI.mjs";
13
+ import { t as resolveActionPermission } from "./actionPermissions-TUVR3uiZ.mjs";
14
+ //#region src/scope/projection.ts
14
15
  /**
15
- * Compose pipeline steps into an ordered array.
16
- * Accepts guards, transforms, and interceptors in any order.
16
+ * Compute the request-scope projection. Returns `undefined` when no
17
+ * scope is attached (public / unscoped routes) so hosts can idiomatically
18
+ * write `ctx.scope?.organizationId` without a double-null check.
17
19
  */
18
- function pipe(...steps) {
19
- return steps;
20
- }
21
- /**
22
- * Check if a step applies to the given operation.
23
- */
24
- function appliesTo(step, operation) {
25
- if (!step.operations || step.operations.length === 0) return true;
26
- return step.operations.includes(operation);
27
- }
28
- /**
29
- * Execute a pipeline against a request context.
30
- *
31
- * This is the core runtime that createCrudRouter uses to execute pipelines.
32
- * External usage is not needed — this is wired automatically when `pipe` is set.
33
- *
34
- * @param steps - Pipeline steps to execute
35
- * @param ctx - The pipeline context (extends IRequestContext)
36
- * @param handler - The actual controller method to call
37
- * @param operation - The CRUD operation name
38
- * @returns The controller response (possibly modified by interceptors)
39
- */
40
- async function executePipeline(steps, ctx, handler, operation) {
41
- const guards = [];
42
- const transforms = [];
43
- const interceptors = [];
44
- for (const step of steps) {
45
- if (!appliesTo(step, operation)) continue;
46
- switch (step._type) {
47
- case "guard":
48
- guards.push(step);
49
- break;
50
- case "transform":
51
- transforms.push(step);
52
- break;
53
- case "interceptor":
54
- interceptors.push(step);
55
- break;
56
- }
57
- }
58
- for (const g of guards) if (!await g.handler(ctx)) throw new ForbiddenError(`Guard '${g.name}' denied access`);
59
- let currentCtx = ctx;
60
- for (const t of transforms) {
61
- const result = await t.handler(currentCtx);
62
- if (result) currentCtx = result;
63
- }
64
- let chain = () => handler(currentCtx);
65
- for (let i = interceptors.length - 1; i >= 0; i--) {
66
- const interceptor = interceptors[i];
67
- const next = chain;
68
- chain = () => interceptor.handler(currentCtx, next);
69
- }
70
- return chain();
20
+ function buildRequestScopeProjection(scope) {
21
+ if (!scope) return void 0;
22
+ return {
23
+ organizationId: getOrgId(scope),
24
+ userId: getUserId(scope),
25
+ orgRoles: isMember(scope) ? scope.orgRoles : void 0
26
+ };
71
27
  }
72
28
  //#endregion
73
29
  //#region src/core/fastifyAdapter.ts
@@ -119,6 +75,8 @@ function createRequestContext(req) {
119
75
  queryCache: srv && "queryCache" in srv ? srv.queryCache : void 0,
120
76
  log: req.log
121
77
  };
78
+ const rawScope = reqWithExtras.scope;
79
+ const scopeProjection = buildRequestScopeProjection(rawScope);
122
80
  return {
123
81
  query: reqWithExtras.query ?? {},
124
82
  body: reqWithExtras.body ?? {},
@@ -135,10 +93,11 @@ function createRequestContext(req) {
135
93
  };
136
94
  })() : null,
137
95
  context: requestContext,
96
+ scope: scopeProjection,
138
97
  metadata: {
139
98
  ...reqWithExtras.context,
140
99
  arc: reqWithExtras.arc,
141
- _scope: reqWithExtras.scope,
100
+ _scope: rawScope,
142
101
  _ownershipCheck: reqWithExtras._ownershipCheck,
143
102
  _policyFilters: reqWithExtras._policyFilters ?? {},
144
103
  log: reqWithExtras.log
@@ -667,6 +626,52 @@ function createPermissionMiddleware(permission, resourceName, action) {
667
626
  return buildPermissionMiddleware(permission, resourceName, action);
668
627
  }
669
628
  //#endregion
629
+ //#region src/core/schemaOptions.ts
630
+ /**
631
+ * Inject the tenant-scoping field rule into `schemaOptions.fieldRules`:
632
+ *
633
+ * { [tenantField]: { systemManaged: true, preserveForElevated: true } }
634
+ *
635
+ * Why both flags: `systemManaged` tells `BodySanitizer` to strip the
636
+ * field from inbound bodies (so member clients can't forge a target
637
+ * tenant). `preserveForElevated` exempts elevated-admin scopes from the
638
+ * strip, so platform admins without a pinned org can still pick a target
639
+ * org via the request body (the only channel they have —
640
+ * `BaseController.create` can't re-stamp from scope when scope has no
641
+ * orgId).
642
+ *
643
+ * **Returns a new `RouteSchemaOptions`** — the input is never mutated.
644
+ * Callers should assign the return value to whatever config slot they
645
+ * read from downstream (always the `resolvedConfig`, never raw `config`).
646
+ *
647
+ * **No-op when:**
648
+ * - `tenantField` is `false` (platform-universal resource)
649
+ * - `tenantField` is undefined
650
+ * - The caller already declared `fieldRules[tenantField].systemManaged`
651
+ * (even as `false`) — explicit opt-outs are respected
652
+ *
653
+ * `preserveForElevated` defaults to `true` but is preserved verbatim
654
+ * when the caller set it explicitly.
655
+ */
656
+ function autoInjectTenantFieldRules(schemaOptions, tenantField) {
657
+ if (tenantField === false || tenantField === void 0) return schemaOptions;
658
+ const fieldName = tenantField || "organizationId";
659
+ const existing = schemaOptions?.fieldRules ?? {};
660
+ const existingRule = existing[fieldName];
661
+ if (existingRule && existingRule.systemManaged !== void 0) return schemaOptions;
662
+ return {
663
+ ...schemaOptions ?? {},
664
+ fieldRules: {
665
+ ...existing,
666
+ [fieldName]: {
667
+ ...existingRule ?? {},
668
+ systemManaged: true,
669
+ preserveForElevated: existingRule?.preserveForElevated ?? true
670
+ }
671
+ }
672
+ };
673
+ }
674
+ //#endregion
670
675
  //#region src/core/validateResourceConfig.ts
671
676
  /**
672
677
  * Resource Configuration Validator
@@ -708,7 +713,7 @@ function validateResourceConfig(config, options = {}) {
708
713
  else if (!config.adapter.repository) errors.push({
709
714
  field: "adapter.repository",
710
715
  message: "Adapter must provide a repository",
711
- suggestion: "Ensure your adapter returns a valid CrudRepository"
716
+ suggestion: "Ensure your adapter returns a valid StandardRepo (see @classytic/repo-core)"
712
717
  });
713
718
  } else if (!config.adapter && !config.routes?.length) warnings.push({
714
719
  field: "config",
@@ -926,6 +931,7 @@ function defineResource(config) {
926
931
  const originalPresets = (config.presets ?? []).map((p) => typeof p === "string" ? p : p.name);
927
932
  const resolvedConfig = config.presets?.length ? applyPresets(config, config.presets) : config;
928
933
  resolvedConfig._appliedPresets = originalPresets;
934
+ resolvedConfig.schemaOptions = autoInjectTenantFieldRules(resolvedConfig.schemaOptions, resolvedConfig.tenantField);
929
935
  let controller = resolvedConfig.controller;
930
936
  if (!controller && hasCrudRoutes && repository) {
931
937
  const qp = resolvedConfig.queryParser;
@@ -941,6 +947,7 @@ function defineResource(config) {
941
947
  maxLimit: maxLimitFromParser,
942
948
  tenantField: resolvedConfig.tenantField,
943
949
  idField: resolvedConfig.idField,
950
+ ...resolvedConfig.defaultSort !== void 0 ? { defaultSort: resolvedConfig.defaultSort } : {},
944
951
  matchesFilter: config.adapter?.matchesFilter,
945
952
  cache: resolvedConfig.cache,
946
953
  onFieldWriteDenied: resolvedConfig.onFieldWriteDenied,
@@ -965,11 +972,17 @@ function defineResource(config) {
965
972
  if (config.hooks) {
966
973
  const h = config.hooks;
967
974
  const inlineHooks = [];
968
- const toCtx = (ctx) => ({
969
- data: ctx.data ?? ctx.result ?? {},
970
- user: ctx.user,
971
- meta: ctx.meta
972
- });
975
+ const toCtx = (ctx) => {
976
+ const context = ctx.context;
977
+ const rawScope = context?._scope;
978
+ return {
979
+ data: ctx.data ?? ctx.result ?? {},
980
+ user: ctx.user,
981
+ context,
982
+ scope: buildRequestScopeProjection(rawScope),
983
+ meta: ctx.meta
984
+ };
985
+ };
973
986
  if (h.beforeCreate) {
974
987
  const fn = h.beforeCreate;
975
988
  inlineHooks.push({
@@ -1028,15 +1041,15 @@ function defineResource(config) {
1028
1041
  }
1029
1042
  if (!config.skipRegistry) try {
1030
1043
  let openApiSchemas;
1031
- if (config.adapter?.generateSchemas) {
1044
+ if (resolvedConfig.adapter?.generateSchemas) {
1032
1045
  const adapterContext = {
1033
- idField: config.idField,
1034
- resourceName: config.name
1046
+ idField: resolvedConfig.idField,
1047
+ resourceName: resolvedConfig.name
1035
1048
  };
1036
- const generated = config.adapter.generateSchemas(config.schemaOptions, adapterContext);
1049
+ const generated = resolvedConfig.adapter.generateSchemas(resolvedConfig.schemaOptions, adapterContext);
1037
1050
  if (generated) openApiSchemas = generated;
1038
1051
  }
1039
- if (config.idField && config.idField !== "_id" && openApiSchemas?.params && typeof openApiSchemas.params === "object") {
1052
+ if (resolvedConfig.idField && resolvedConfig.idField !== "_id" && openApiSchemas?.params && typeof openApiSchemas.params === "object") {
1040
1053
  const params = openApiSchemas.params;
1041
1054
  const properties = params.properties;
1042
1055
  const idProp = properties?.id;
@@ -1047,7 +1060,7 @@ function defineResource(config) {
1047
1060
  delete cleanedId.pattern;
1048
1061
  delete cleanedId.minLength;
1049
1062
  delete cleanedId.maxLength;
1050
- if (!cleanedId.description) cleanedId.description = `${config.idField} (custom ID field)`;
1063
+ if (!cleanedId.description) cleanedId.description = `${resolvedConfig.idField} (custom ID field)`;
1051
1064
  openApiSchemas = {
1052
1065
  ...openApiSchemas,
1053
1066
  params: {
@@ -1061,7 +1074,7 @@ function defineResource(config) {
1061
1074
  }
1062
1075
  }
1063
1076
  }
1064
- const queryParser = config.queryParser;
1077
+ const queryParser = resolvedConfig.queryParser;
1065
1078
  if (queryParser?.getQuerySchema) {
1066
1079
  const querySchema = queryParser.getQuerySchema();
1067
1080
  if (querySchema) openApiSchemas = {
@@ -1069,13 +1082,13 @@ function defineResource(config) {
1069
1082
  listQuery: querySchema
1070
1083
  };
1071
1084
  }
1072
- if (config.openApiSchemas) openApiSchemas = {
1085
+ if (resolvedConfig.openApiSchemas) openApiSchemas = {
1073
1086
  ...openApiSchemas,
1074
- ...config.openApiSchemas
1087
+ ...resolvedConfig.openApiSchemas
1075
1088
  };
1076
1089
  if (openApiSchemas) openApiSchemas = convertOpenApiSchemas(openApiSchemas);
1077
1090
  resource._registryMeta = {
1078
- module: config.module,
1091
+ module: resolvedConfig.module,
1079
1092
  openApiSchemas
1080
1093
  };
1081
1094
  } catch {}
@@ -1287,8 +1300,8 @@ var ResourceDefinition = class {
1287
1300
  fields: self.fields
1288
1301
  });
1289
1302
  if (self.actions && Object.keys(self.actions).length > 0) {
1290
- const { createActionRouter } = await import("./createActionRouter-DH1YFL9m.mjs").then((n) => n.n);
1291
- createActionRouter(instance, normalizeActionsToRouterConfig(self.actions, self.actionPermissions, self.tag));
1303
+ const { createActionRouter } = await import("./createActionRouter-C8UUB3Px.mjs").then((n) => n.n);
1304
+ createActionRouter(instance, normalizeActionsToRouterConfig(self.actions, self.actionPermissions, self.tag, self.permissions, self.name, typedInstance.log));
1292
1305
  }
1293
1306
  if (self.events && Object.keys(self.events).length > 0) typedInstance.log?.debug?.(`Resource '${self.name}' defined ${Object.keys(self.events).length} events`);
1294
1307
  }, { prefix: self.prefix });
@@ -1354,18 +1367,53 @@ function capitalize(str) {
1354
1367
  return str.charAt(0).toUpperCase() + str.slice(1);
1355
1368
  }
1356
1369
  /**
1357
- * Normalize ActionsMap into the ActionRouterConfig shape that createActionRouter expects.
1370
+ * Normalize `ActionsMap` into the `ActionRouterConfig` shape that
1371
+ * `createActionRouter` expects.
1372
+ *
1373
+ * **Permission fallback chain (fail-closed, v2.10.5):**
1374
+ * Actions mutate state, so "no permission declared" historically meant
1375
+ * "authenticated users can call it" — a silent authz hole for apps using
1376
+ * the function shorthand `actions: { send: async (id, data, req) => ... }`.
1377
+ *
1378
+ * The chain is now:
1379
+ * 1. `ActionDefinition.permissions` — explicit per-action check.
1380
+ * 2. Resource-level `actionPermissions` — explicit global-for-actions.
1381
+ * 3. Resource-level `permissions.update` — sensible default (actions mutate).
1382
+ * 4. Boot-time error — forces the author to pick an explicit gate.
1383
+ *
1384
+ * When step 3 fires, we log a warning (not a throw) so upgrading apps
1385
+ * aren't bricked by the behavior change, but the gap is visible. Apps
1386
+ * that genuinely want public actions must declare `allowPublic()`
1387
+ * explicitly — auth-by-accident is no longer a supported state.
1358
1388
  */
1359
- function normalizeActionsToRouterConfig(actions, globalAuth, tag) {
1389
+ function normalizeActionsToRouterConfig(actions, globalAuth, tag, resourcePermissions, resourceName, log) {
1360
1390
  const handlers = {};
1361
1391
  const permissions = {};
1362
1392
  const schemas = {};
1363
- for (const [name, entry] of Object.entries(actions)) if (typeof entry === "function") handlers[name] = entry;
1364
- else {
1365
- const def = entry;
1366
- handlers[name] = def.handler;
1367
- if (def.permissions) permissions[name] = def.permissions;
1368
- if (def.schema) schemas[name] = def.schema;
1393
+ for (const [name, entry] of Object.entries(actions)) {
1394
+ const explicit = typeof entry !== "function" && entry.permissions ? entry.permissions : void 0;
1395
+ if (typeof entry === "function") handlers[name] = entry;
1396
+ else {
1397
+ const def = entry;
1398
+ handlers[name] = def.handler;
1399
+ if (def.permissions) permissions[name] = def.permissions;
1400
+ if (def.schema) schemas[name] = def.schema;
1401
+ }
1402
+ const effective = resolveActionPermission({
1403
+ action: entry,
1404
+ resourcePermissions,
1405
+ resourceActionPermissions: void 0,
1406
+ globalAuth
1407
+ });
1408
+ if (!explicit && !globalAuth && effective && effective === resourcePermissions?.update) {
1409
+ permissions[name] = effective;
1410
+ log?.warn?.({
1411
+ resource: resourceName,
1412
+ action: name,
1413
+ fallback: "permissions.update"
1414
+ }, `[Arc] Action '${resourceName}.${name}' has no explicit permission — falling back to the resource's \`permissions.update\` gate. Declare \`actions.${name}.permissions\` (or resource \`actionPermissions\`) to silence this.`);
1415
+ }
1416
+ if (!effective) throw new Error(`[Arc] Resource '${resourceName}': action '${name}' has no permission gate and the resource defines no \`permissions.update\` fallback. Declare one of:\n - \`actions.${name}.permissions: <PermissionCheck>\` (per-action)\n - \`actionPermissions: <PermissionCheck>\` (resource-wide)\n - \`permissions.update: <PermissionCheck>\` (inherited by actions)\nUse \`allowPublic()\` if you genuinely want the action unauthenticated.`);
1369
1417
  }
1370
1418
  return {
1371
1419
  tag,
@@ -1376,4 +1424,36 @@ function normalizeActionsToRouterConfig(actions, globalAuth, tag) {
1376
1424
  };
1377
1425
  }
1378
1426
  //#endregion
1379
- export { validateResourceConfig as a, createCrudHandlers as c, getControllerContext as d, getControllerScope as f, formatValidationErrors as i, createFastifyHandler as l, pipe as m, defineResource as n, createCrudRouter as o, sendControllerResponse as p, assertValidConfig as r, createPermissionMiddleware as s, ResourceDefinition as t, createRequestContext as u };
1427
+ //#region src/core/defineResourceVariants.ts
1428
+ /**
1429
+ * Define multiple resources from a shared base config and per-variant overrides.
1430
+ *
1431
+ * Each variant is independently passed through `defineResource()` — the
1432
+ * returned `ResourceDefinition`s are real, fully-registered resources.
1433
+ * Register each one's plugin in your app:
1434
+ *
1435
+ * ```typescript
1436
+ * await app.register(articlePublic.toPlugin());
1437
+ * await app.register(articleAdmin.toPlugin());
1438
+ * ```
1439
+ *
1440
+ * @param base Shared config — adapter, queryParser, schemaOptions, hooks, etc.
1441
+ * Must NOT include `name` or `prefix` (those are per-variant).
1442
+ * @param variants Map of variant key → override. Each variant must declare
1443
+ * its own `name` and `prefix`. Other fields override the base.
1444
+ * @returns A record where each key from `variants` maps to a real
1445
+ * `ResourceDefinition` ready for `.toPlugin()` registration.
1446
+ */
1447
+ function defineResourceVariants(base, variants) {
1448
+ const out = {};
1449
+ for (const key of Object.keys(variants)) {
1450
+ const override = variants[key];
1451
+ out[key] = defineResource({
1452
+ ...base,
1453
+ ...override
1454
+ });
1455
+ }
1456
+ return out;
1457
+ }
1458
+ //#endregion
1459
+ export { formatValidationErrors as a, createPermissionMiddleware as c, createRequestContext as d, getControllerContext as f, assertValidConfig as i, createCrudHandlers as l, sendControllerResponse as m, ResourceDefinition as n, validateResourceConfig as o, getControllerScope as p, defineResource as r, createCrudRouter as s, defineResourceVariants as t, createFastifyHandler as u };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { n as normalizePermissionResult, t as applyPermissionResult } from "./applyPermissionResult-bqGpo9ML.mjs";
2
+ import { n as normalizePermissionResult, t as applyPermissionResult } from "./applyPermissionResult-QhV1Pa-g.mjs";
3
3
  import { a as toJsonSchema } from "./schemaConverter-BxFDdtXu.mjs";
4
4
  //#region src/core/createActionRouter.ts
5
5
  var createActionRouter_exports = /* @__PURE__ */ __exportAll({