@classytic/arc 2.21.0 → 2.23.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 (115) hide show
  1. package/dist/{BaseController-BnLYgE9A.mjs → BaseController-BQ8KNvWC.mjs} +3 -2
  2. package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
  3. package/dist/{actionPermissions-ze70xXJi.mjs → actionPermissions-lszoJle1.mjs} +1 -1
  4. package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
  5. package/dist/audit/index.d.mts +2 -2
  6. package/dist/auth/index.d.mts +30 -2
  7. package/dist/auth/index.mjs +6 -3
  8. package/dist/cli/commands/describe.d.mts +1 -1
  9. package/dist/cli/commands/describe.mjs +1 -1
  10. package/dist/cli/commands/docs.mjs +2 -2
  11. package/dist/cli/commands/init.mjs +1 -1
  12. package/dist/cli/commands/introspect.mjs +1 -1
  13. package/dist/cli/index.mjs +1 -1
  14. package/dist/core/index.d.mts +2 -2
  15. package/dist/core/index.mjs +3 -3
  16. package/dist/{core-DZ6Xe1jR.mjs → core-DQ6rDTyH.mjs} +126 -68
  17. package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-BuOv-m4Y.mjs} +10 -6
  18. package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-CgJv_BU_.mjs} +3 -3
  19. package/dist/{createApp-Bi61XxqU.mjs → createApp-CjOeWJ9_.mjs} +252 -57
  20. package/dist/docs/index.d.mts +1 -1
  21. package/dist/docs/index.mjs +1 -1
  22. package/dist/encryption/index.mjs +9 -5
  23. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-Bq2OeEQL.mjs} +34 -10
  24. package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
  25. package/dist/events/index.mjs +5 -2
  26. package/dist/factory/index.d.mts +46 -9
  27. package/dist/factory/index.mjs +64 -13
  28. package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-g_f9omSc.d.mts} +2 -2
  29. package/dist/fields-D3lhgzhD.d.mts +121 -0
  30. package/dist/health-ScyjIVo5.mjs +202 -0
  31. package/dist/hooks/index.d.mts +1 -1
  32. package/dist/idempotency/index.d.mts +7 -0
  33. package/dist/idempotency/index.mjs +7 -17
  34. package/dist/{index-Dtrj6XZk.d.mts → index-BBsvQt7U.d.mts} +1310 -1226
  35. package/dist/{types-DpxoNX5v.d.mts → index-Ba10fdKp.d.mts} +196 -66
  36. package/dist/{index-B5N-NaHs.d.mts → index-CC5ruDGh.d.mts} +3 -3
  37. package/dist/{index-B5PoNbfu.d.mts → index-wMoWBLkn.d.mts} +68 -2
  38. package/dist/index.d.mts +6 -5
  39. package/dist/index.mjs +6 -5
  40. package/dist/{init-0YhHkXGk.mjs → init-CjO4p_q1.mjs} +123 -4
  41. package/dist/integrations/event-gateway.mjs +1 -1
  42. package/dist/integrations/index.d.mts +1 -1
  43. package/dist/integrations/mcp/index.d.mts +67 -4
  44. package/dist/integrations/mcp/index.mjs +1 -1
  45. package/dist/integrations/mcp/testing.d.mts +1 -1
  46. package/dist/integrations/mcp/testing.mjs +1 -1
  47. package/dist/integrations/webhooks.d.mts +131 -7
  48. package/dist/integrations/webhooks.mjs +169 -10
  49. package/dist/interface-n5hpKg1d.mjs +16 -0
  50. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  51. package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
  52. package/dist/logger/index.d.mts +26 -1
  53. package/dist/logger/index.mjs +39 -1
  54. package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-BtzIL_kM.mjs} +312 -39
  55. package/dist/middleware/index.d.mts +1 -1
  56. package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
  57. package/dist/{openapi-Kyod_di6.mjs → openapi-jSDx-VDv.mjs} +3 -3
  58. package/dist/permissions/index.d.mts +4 -3
  59. package/dist/permissions/index.mjs +3 -2
  60. package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
  61. package/dist/pipeline/index.d.mts +1 -1
  62. package/dist/plugins/index.d.mts +24 -7
  63. package/dist/plugins/index.mjs +14 -282
  64. package/dist/plugins/response-cache.mjs +1 -0
  65. package/dist/plugins/tracing-entry.mjs +7 -4
  66. package/dist/presets/filesUpload.d.mts +2 -2
  67. package/dist/presets/filesUpload.mjs +1 -1
  68. package/dist/presets/index.d.mts +1 -1
  69. package/dist/presets/index.mjs +1 -1
  70. package/dist/presets/multiTenant.d.mts +1 -1
  71. package/dist/presets/search.d.mts +2 -2
  72. package/dist/presets/search.mjs +1 -1
  73. package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
  74. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  75. package/dist/realtime-I8OrfnMR.mjs +375 -0
  76. package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-CWO6mdYe.mjs} +38 -18
  77. package/dist/registry/index.d.mts +2 -2
  78. package/dist/registry/index.mjs +1 -1
  79. package/dist/requestId-5BhTVzBv.mjs +113 -0
  80. package/dist/schemaConverter-DJvodiHj.mjs +171 -0
  81. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-Ca7ySNPx.mjs} +2 -2
  82. package/dist/scim/index.mjs +6 -7
  83. package/dist/scope/index.mjs +2 -76
  84. package/dist/scope-LONQ3mfZ.mjs +37 -0
  85. package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
  86. package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
  87. package/dist/sseStream-BQ7Qaynp.mjs +57 -0
  88. package/dist/testing/index.d.mts +83 -3
  89. package/dist/testing/index.mjs +70 -3
  90. package/dist/types/index.d.mts +2 -2
  91. package/dist/{types-DRj-Ut4n.d.mts → types-5OuqeTDX.d.mts} +37 -2
  92. package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
  93. package/dist/usage/index.d.mts +142 -0
  94. package/dist/usage/index.mjs +0 -0
  95. package/dist/utils/index.d.mts +101 -19
  96. package/dist/utils/index.mjs +5 -3
  97. package/dist/{utils-CO8a3B_1.mjs → utils-CZyeRXTh.mjs} +80 -82
  98. package/dist/{versioning-DQ4mCvg0.d.mts → versioning-D6PvCwpq.d.mts} +43 -9
  99. package/llms.txt +39 -0
  100. package/package.json +513 -447
  101. package/skills/arc/SKILL.md +14 -8
  102. package/skills/arc/references/api-reference.md +34 -0
  103. package/skills/arc/references/auth.md +21 -4
  104. package/skills/arc/references/mcp.md +1 -1
  105. package/skills/arc/references/production.md +734 -654
  106. package/skills/arc-code-review/SKILL.md +1 -1
  107. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  108. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  109. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  110. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  111. package/skills/arc-module-publishing/SKILL.md +2 -1
  112. package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
  113. package/skills/arc-module-publishing/references/package-manifest.md +4 -2
  114. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  115. package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
@@ -1,6 +1,7 @@
1
- import { a as getModuleExports, c as LoadResourcesOptions, d as loadResources, i as defineModule, l as ResourceLike, n as ArcModuleInput, o as orderModules, r as ArcModuleRegistry, s as resolveModule, t as ArcModule, u as ResourceModule } from "../module-WeuU5jfd.mjs";
2
- import "../fastify-augmentation-DafigGQp.mjs";
3
- import { a as CustomPluginAuthOption, c as RawBodyOptions, d as preloadResourcesAsync, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as preloadResources } from "../types-DpxoNX5v.mjs";
1
+ import { a as getModuleExports, c as LoadResourcesOptions, d as loadResources, i as defineModule, l as ResourceLike, n as ArcModuleInput, o as orderModules, r as ArcModuleRegistry, s as resolveModule, t as ArcModule, u as ResourceModule } from "../module-B4u10iOJ.mjs";
2
+ import "../fastify-augmentation-g_f9omSc.mjs";
3
+ import { a as AuthOption, c as CustomPluginAuthOption, d as preloadResourcesAsync, i as UnderPressureOptions, l as JwtAuthOption, n as MultipartOptions, o as BetterAuthOption, r as RawBodyOptions, s as CustomAuthenticatorOption, t as CreateAppOptions, u as preloadResources } from "../index-Ba10fdKp.mjs";
4
+ import { b as HealthCheck } from "../versioning-D6PvCwpq.mjs";
4
5
  import { FastifyInstance, FastifyServerOptions } from "fastify";
5
6
  //#region src/factory/createApp.d.ts
6
7
  /**
@@ -32,6 +33,29 @@ declare const ArcFactory: {
32
33
  testing(options: Omit<CreateAppOptions, "preset">): Promise<FastifyInstance>;
33
34
  };
34
35
  //#endregion
36
+ //#region src/factory/createWorker.d.ts
37
+ interface WorkerHealthOptions {
38
+ /** Port for the worker's ONLY listener (liveness/readiness probes). */
39
+ port: number;
40
+ /** Bind host. Default `0.0.0.0`. */
41
+ host?: string;
42
+ /** Readiness checks — same contract as the API role's healthPlugin. */
43
+ checks?: HealthCheck[];
44
+ }
45
+ interface CreateWorkerOptions {
46
+ /** Opt-in probe listener. Omit = the worker binds NOTHING. */
47
+ health?: WorkerHealthOptions;
48
+ }
49
+ interface ArcWorker {
50
+ /** The underlying Fastify instance (decorations, inject, logs). */
51
+ app: FastifyInstance;
52
+ /** Typed module-export accessor — same contract as `getModuleExports`. */
53
+ exports<TExports = unknown>(name: string): TExports;
54
+ /** Close app → module onClose (reverse) → app onClose. Idempotent. */
55
+ close(): Promise<void>;
56
+ }
57
+ declare function createWorker(options: CreateAppOptions, workerOptions?: CreateWorkerOptions): Promise<ArcWorker>;
58
+ //#endregion
35
59
  //#region src/factory/edge.d.ts
36
60
  interface FetchHandlerOptions {
37
61
  /**
@@ -40,15 +64,31 @@ interface FetchHandlerOptions {
40
64
  * @default true
41
65
  */
42
66
  autoReady?: boolean;
67
+ /**
68
+ * Reject request bodies larger than this many bytes with 413
69
+ * `arc.payload_too_large`. Enforcement precision matters: bodies with a
70
+ * declared `Content-Length` are rejected BEFORE buffering; chunked or
71
+ * undeclared bodies are necessarily buffered first and checked AFTER
72
+ * (`arrayBuffer()` allocates the full body — a consequence of the
73
+ * buffered-adapter design, not a bug). Platform/gateway body limits
74
+ * remain the real pre-allocation protection for hostile senders.
75
+ * Fastify's own `bodyLimit` still applies after this gate. Set to `0`
76
+ * to disable the gate.
77
+ * @default 1_048_576 (1 MiB — matches Fastify's default bodyLimit)
78
+ */
79
+ maxRequestBytes?: number;
43
80
  }
44
81
  /**
45
82
  * Convert a Fastify app into a Web Standards fetch handler.
46
83
  *
47
84
  * The returned function accepts a Web Standard `Request` and returns
48
- * a Web Standard `Response` — the universal serverless/edge contract.
85
+ * a Web Standard `Response` — the universal serverless contract.
49
86
  *
50
87
  * Internally uses `app.inject()` which processes the request through
51
88
  * the full Fastify pipeline (hooks, plugins, routes) without TCP.
89
+ * Bodies are read as raw bytes (`arrayBuffer()`), so binary payloads
90
+ * (protobuf, images, non-UTF-8 charsets) survive intact — the
91
+ * content-type parser decides how to decode, same as a real socket.
52
92
  */
53
93
  declare function toFetchHandler(app: FastifyInstance, options?: FetchHandlerOptions): (request: Request) => Promise<Response>;
54
94
  //#endregion
@@ -91,9 +131,6 @@ declare const testingPreset: Partial<CreateAppOptions>;
91
131
  * when using `toFetchHandler()` (routes through Fastify's `.inject()`).
92
132
  */
93
133
  declare const edgePreset: Partial<CreateAppOptions>;
94
- /**
95
- * Get preset by name
96
- */
97
- declare function getPreset(name: "production" | "development" | "testing" | "edge"): Partial<CreateAppOptions>;
134
+ declare function getPreset(name: "production" | "development" | "testing" | "edge" | "worker"): Partial<CreateAppOptions>;
98
135
  //#endregion
99
- export { ArcFactory, type ArcModule, type ArcModuleInput, type ArcModuleRegistry, type AuthOption, type BetterAuthOption, type CreateAppOptions, type CustomAuthenticatorOption, type CustomPluginAuthOption, type FetchHandlerOptions, type JwtAuthOption, type LoadResourcesOptions, type MultipartOptions, type RawBodyOptions, type ResourceLike, type ResourceModule, type UnderPressureOptions, createApp, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
136
+ export { ArcFactory, type ArcModule, type ArcModuleInput, type ArcModuleRegistry, type ArcWorker, type AuthOption, type BetterAuthOption, type CreateAppOptions, type CreateWorkerOptions, type CustomAuthenticatorOption, type CustomPluginAuthOption, type FetchHandlerOptions, type JwtAuthOption, type LoadResourcesOptions, type MultipartOptions, type RawBodyOptions, type ResourceLike, type ResourceModule, type UnderPressureOptions, type WorkerHealthOptions, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
@@ -1,33 +1,73 @@
1
- import { n as preloadResourcesAsync, t as preloadResources } from "../preloadResources-CHw3_oA_.mjs";
2
- import { a as edgePreset, c as testingPreset, d as orderModules, f as resolveModule, i as developmentPreset, l as defineModule, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory, u as getModuleExports } from "../createApp-Bi61XxqU.mjs";
1
+ import { a as edgePreset, c as testingPreset, d as getModuleExports, f as orderModules, h as preloadResourcesAsync, i as developmentPreset, l as workerPreset, m as preloadResources, n as createApp, o as getPreset, p as resolveModule, s as productionPreset, t as ArcFactory, u as defineModule } from "../createApp-CjOeWJ9_.mjs";
2
+ import { t as healthPlugin } from "../health-ScyjIVo5.mjs";
3
3
  import { t as loadResources } from "../loadResources-DpU8pfjV.mjs";
4
+ //#region src/factory/createWorker.ts
5
+ async function createWorker(options, workerOptions = {}) {
6
+ const { auth: _auth, helmet: _helmet, cors: _cors, rateLimit: _rateLimit, multipart: _multipart, rawBody: _rawBody, sensible: _sensible, underPressure: _underPressure, replyHelpers: _replyHelpers, ...runtimeOptions } = options;
7
+ const app = await createApp({
8
+ ...runtimeOptions,
9
+ preset: "worker",
10
+ ...runtimeOptions.arcPlugins ? { arcPlugins: {
11
+ ...workerPreset.arcPlugins,
12
+ ...runtimeOptions.arcPlugins
13
+ } } : {}
14
+ });
15
+ if (workerOptions.health) {
16
+ const { port, host = "0.0.0.0", checks = [] } = workerOptions.health;
17
+ await app.register(healthPlugin, { checks });
18
+ await app.listen({
19
+ port,
20
+ host
21
+ });
22
+ } else await app.ready();
23
+ let closed = false;
24
+ return {
25
+ app,
26
+ exports: (name) => getModuleExports(app, name),
27
+ close: async () => {
28
+ if (closed) return;
29
+ closed = true;
30
+ await app.close();
31
+ }
32
+ };
33
+ }
34
+ //#endregion
4
35
  //#region src/factory/edge.ts
5
36
  /**
6
37
  * Convert a Fastify app into a Web Standards fetch handler.
7
38
  *
8
39
  * The returned function accepts a Web Standard `Request` and returns
9
- * a Web Standard `Response` — the universal serverless/edge contract.
40
+ * a Web Standard `Response` — the universal serverless contract.
10
41
  *
11
42
  * Internally uses `app.inject()` which processes the request through
12
43
  * the full Fastify pipeline (hooks, plugins, routes) without TCP.
44
+ * Bodies are read as raw bytes (`arrayBuffer()`), so binary payloads
45
+ * (protobuf, images, non-UTF-8 charsets) survive intact — the
46
+ * content-type parser decides how to decode, same as a real socket.
13
47
  */
14
48
  function toFetchHandler(app, options = {}) {
15
- const { autoReady = true } = options;
16
- let ready = false;
49
+ const { autoReady = true, maxRequestBytes = 1048576 } = options;
50
+ let readyPromise = null;
17
51
  return async (request) => {
18
- if (autoReady && !ready) {
19
- await app.ready();
20
- ready = true;
52
+ if (autoReady) {
53
+ readyPromise ??= Promise.resolve(app.ready());
54
+ await readyPromise;
21
55
  }
22
56
  const url = new URL(request.url);
23
57
  const headers = {};
24
58
  request.headers.forEach((value, key) => {
25
59
  headers[key] = value;
26
60
  });
61
+ if (maxRequestBytes > 0) {
62
+ const declared = Number(request.headers.get("content-length"));
63
+ if (Number.isFinite(declared) && declared > maxRequestBytes) return payloadTooLarge(maxRequestBytes);
64
+ }
27
65
  let payload;
28
- if (request.method !== "GET" && request.method !== "HEAD") try {
29
- payload = await request.text();
30
- } catch {}
66
+ if (request.method !== "GET" && request.method !== "HEAD" && request.body !== null) {
67
+ const bytes = await request.arrayBuffer();
68
+ if (maxRequestBytes > 0 && bytes.byteLength > maxRequestBytes) return payloadTooLarge(maxRequestBytes);
69
+ if (bytes.byteLength > 0) payload = Buffer.from(bytes);
70
+ }
31
71
  const response = await app.inject({
32
72
  method: request.method,
33
73
  url: url.pathname + url.search,
@@ -40,11 +80,22 @@ function toFetchHandler(app, options = {}) {
40
80
  if (Array.isArray(value)) for (const v of value) responseHeaders.append(key, v);
41
81
  else responseHeaders.set(key, String(value));
42
82
  }
43
- return new Response(response.payload, {
83
+ return new Response(response.rawPayload, {
44
84
  status: response.statusCode,
45
85
  headers: responseHeaders
46
86
  });
47
87
  };
48
88
  }
89
+ /** Canonical ErrorContract shape for the pre-dispatch 413. */
90
+ function payloadTooLarge(limit) {
91
+ return new Response(JSON.stringify({
92
+ code: "arc.payload_too_large",
93
+ message: `Request body exceeds ${limit} bytes`,
94
+ status: 413
95
+ }), {
96
+ status: 413,
97
+ headers: { "content-type": "application/json" }
98
+ });
99
+ }
49
100
  //#endregion
50
- export { ArcFactory, createApp, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
101
+ export { ArcFactory, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
@@ -1,6 +1,6 @@
1
- import { bn as HookSystem, z as ResourceRegistry } from "./index-Dtrj6XZk.mjs";
1
+ import { xn as HookSystem, z as ResourceRegistry } from "./index-BBsvQt7U.mjs";
2
2
  import { t as ExternalOpenApiPaths } from "./externalPaths-BE6wFpYv.mjs";
3
- import { r as ArcModuleRegistry } from "./module-WeuU5jfd.mjs";
3
+ import { r as ArcModuleRegistry } from "./module-B4u10iOJ.mjs";
4
4
  import { FastifyPluginAsync } from "fastify";
5
5
  //#region src/core/arcCorePlugin.d.ts
6
6
  interface ArcCorePluginOptions {
@@ -0,0 +1,121 @@
1
+ //#region src/permissions/fields.d.ts
2
+ /**
3
+ * Field-Level Permissions
4
+ *
5
+ * Control field visibility and writability per role.
6
+ * Integrated into the response path (read) and sanitization path (write).
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { fields, defineResource } from '@classytic/arc';
11
+ *
12
+ * const userResource = defineResource({
13
+ * name: 'user',
14
+ * adapter: userAdapter,
15
+ * fields: {
16
+ * salary: fields.visibleTo(['admin', 'hr']),
17
+ * internalNotes: fields.writableBy(['admin']),
18
+ * email: fields.redactFor(['viewer']),
19
+ * password: fields.hidden(),
20
+ * },
21
+ * });
22
+ * ```
23
+ */
24
+ type FieldPermissionType = "hidden" | "visibleTo" | "writableBy" | "redactFor";
25
+ interface FieldPermission {
26
+ readonly _type: FieldPermissionType;
27
+ readonly roles?: readonly string[];
28
+ readonly redactValue?: unknown;
29
+ }
30
+ type FieldPermissionMap = Record<string, FieldPermission>;
31
+ declare const fields: {
32
+ /**
33
+ * Field is never included in responses. Not writable via API.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * fields: { password: fields.hidden() }
38
+ * ```
39
+ */
40
+ hidden(): FieldPermission;
41
+ /**
42
+ * Field is only visible to users with specified roles.
43
+ * Other users don't see the field at all.
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * fields: { salary: fields.visibleTo(['admin', 'hr']) }
48
+ * ```
49
+ */
50
+ visibleTo(roles: readonly string[]): FieldPermission;
51
+ /**
52
+ * Field is only writable by users with specified roles.
53
+ * All users can still read the field. Users without the role
54
+ * have the field silently stripped from write operations.
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * fields: { role: fields.writableBy(['admin']) }
59
+ * ```
60
+ */
61
+ writableBy(roles: readonly string[]): FieldPermission;
62
+ /**
63
+ * Field is redacted (replaced with a placeholder) for specified roles.
64
+ * Other users see the real value.
65
+ *
66
+ * @param roles - Roles that see the redacted value
67
+ * @param redactValue - Replacement value (default: '***')
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * fields: {
72
+ * email: fields.redactFor(['viewer']),
73
+ * ssn: fields.redactFor(['basic'], '***-**-****'),
74
+ * }
75
+ * ```
76
+ */
77
+ redactFor(roles: readonly string[], redactValue?: unknown): FieldPermission;
78
+ };
79
+ /**
80
+ * Apply field-level READ permissions to a response object.
81
+ * Strips hidden fields, enforces visibility, and applies redaction.
82
+ *
83
+ * @param data - The response object (mutated in place for performance)
84
+ * @param fieldPermissions - Field permission map from resource config
85
+ * @param userRoles - Current user's roles (empty array for unauthenticated)
86
+ * @returns The filtered object
87
+ */
88
+ declare function applyFieldReadPermissions<T extends Record<string, unknown>>(data: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): T;
89
+ /**
90
+ * Result of applying write permissions — includes both the filtered body
91
+ * and the list of fields that were stripped so callers can decide whether
92
+ * to reject the request (secure default) or silently strip (legacy).
93
+ */
94
+ interface FieldWritePermissionResult<T extends Record<string, unknown>> {
95
+ readonly body: T;
96
+ readonly deniedFields: readonly string[];
97
+ }
98
+ /**
99
+ * Apply field-level WRITE permissions to request body.
100
+ *
101
+ * Returns both the filtered body and the list of denied fields. Callers are
102
+ * expected to reject the request when `deniedFields.length > 0` — silently
103
+ * stripping fields hides misconfigurations and real attacks. See
104
+ * `BodySanitizer` for the default policy.
105
+ *
106
+ * @param body - The request body (returns a new filtered copy)
107
+ * @param fieldPermissions - Field permission map from resource config
108
+ * @param userRoles - Current user's roles
109
+ */
110
+ declare function applyFieldWritePermissions<T extends Record<string, unknown>>(body: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): FieldWritePermissionResult<T>;
111
+ /**
112
+ * Resolve effective roles by merging global user roles with org-level roles.
113
+ *
114
+ * Global roles come from `req.user.role` (normalized via getUserRoles()).
115
+ * Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
116
+ *
117
+ * When no org context exists, returns global roles only — backward compatible.
118
+ */
119
+ declare function resolveEffectiveRoles(userRoles: readonly string[], orgRoles: readonly string[]): string[];
120
+ //#endregion
121
+ export { applyFieldWritePermissions as a, applyFieldReadPermissions as i, FieldPermissionMap as n, fields as o, FieldPermissionType as r, resolveEffectiveRoles as s, FieldPermission as t };
@@ -0,0 +1,202 @@
1
+ import fp from "fastify-plugin";
2
+ //#region src/plugins/health.ts
3
+ function createHttpMetrics() {
4
+ return {
5
+ requestsTotal: {},
6
+ requestDurations: [],
7
+ _ringIndex: 0,
8
+ startTime: Date.now()
9
+ };
10
+ }
11
+ const healthPlugin = async (fastify, opts = {}) => {
12
+ const { prefix = "/_health", checks = [], metrics = false, metricsCollector, version, collectHttpMetrics = metrics } = opts;
13
+ const httpMetrics = createHttpMetrics();
14
+ fastify.get(prefix, { schema: {
15
+ tags: ["Health"],
16
+ summary: "Liveness probe (alias for /live)",
17
+ description: "Returns 200 if the process is alive",
18
+ response: { 200: {
19
+ type: "object",
20
+ properties: {
21
+ status: {
22
+ type: "string",
23
+ enum: ["ok"]
24
+ },
25
+ timestamp: { type: "string" },
26
+ version: { type: "string" }
27
+ }
28
+ } }
29
+ } }, async () => ({
30
+ status: "ok",
31
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
32
+ ...version ? { version } : {}
33
+ }));
34
+ fastify.get(`${prefix}/live`, { schema: {
35
+ tags: ["Health"],
36
+ summary: "Liveness probe",
37
+ description: "Returns 200 if the process is alive",
38
+ response: { 200: {
39
+ type: "object",
40
+ properties: {
41
+ status: {
42
+ type: "string",
43
+ enum: ["ok"]
44
+ },
45
+ timestamp: { type: "string" },
46
+ version: { type: "string" }
47
+ }
48
+ } }
49
+ } }, async () => {
50
+ return {
51
+ status: "ok",
52
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
53
+ ...version ? { version } : {}
54
+ };
55
+ });
56
+ fastify.get(`${prefix}/ready`, { schema: {
57
+ tags: ["Health"],
58
+ summary: "Readiness probe",
59
+ description: "Returns 200 if all dependencies are healthy",
60
+ response: {
61
+ 200: {
62
+ type: "object",
63
+ properties: {
64
+ status: {
65
+ type: "string",
66
+ enum: ["ready", "not_ready"]
67
+ },
68
+ timestamp: { type: "string" },
69
+ checks: {
70
+ type: "array",
71
+ items: {
72
+ type: "object",
73
+ properties: {
74
+ name: { type: "string" },
75
+ healthy: { type: "boolean" },
76
+ duration: { type: "number" },
77
+ error: { type: "string" }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ },
83
+ 503: {
84
+ type: "object",
85
+ properties: {
86
+ status: {
87
+ type: "string",
88
+ enum: ["not_ready"]
89
+ },
90
+ timestamp: { type: "string" },
91
+ checks: { type: "array" }
92
+ }
93
+ }
94
+ }
95
+ } }, async (_, reply) => {
96
+ const results = await runChecks(checks);
97
+ const criticalFailed = results.some((r) => !r.healthy && (checks.find((c) => c.name === r.name)?.critical ?? true));
98
+ const response = {
99
+ status: criticalFailed ? "not_ready" : "ready",
100
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
101
+ checks: results
102
+ };
103
+ if (criticalFailed) reply.code(503);
104
+ return response;
105
+ });
106
+ if (metrics) fastify.get(`${prefix}/metrics`, async (_, reply) => {
107
+ reply.type("text/plain; charset=utf-8");
108
+ if (metricsCollector) return await metricsCollector();
109
+ const uptime = process.uptime();
110
+ const memory = process.memoryUsage();
111
+ const cpu = process.cpuUsage();
112
+ const lines = [
113
+ "# HELP process_uptime_seconds Process uptime in seconds",
114
+ "# TYPE process_uptime_seconds gauge",
115
+ `process_uptime_seconds ${uptime.toFixed(2)}`,
116
+ "",
117
+ "# HELP process_memory_heap_bytes Heap memory usage in bytes",
118
+ "# TYPE process_memory_heap_bytes gauge",
119
+ `process_memory_heap_bytes{type="used"} ${memory.heapUsed}`,
120
+ `process_memory_heap_bytes{type="total"} ${memory.heapTotal}`,
121
+ "",
122
+ "# HELP process_memory_rss_bytes RSS memory in bytes",
123
+ "# TYPE process_memory_rss_bytes gauge",
124
+ `process_memory_rss_bytes ${memory.rss}`,
125
+ "",
126
+ "# HELP process_memory_external_bytes External memory in bytes",
127
+ "# TYPE process_memory_external_bytes gauge",
128
+ `process_memory_external_bytes ${memory.external}`,
129
+ "",
130
+ "# HELP process_cpu_user_microseconds User CPU time in microseconds",
131
+ "# TYPE process_cpu_user_microseconds counter",
132
+ `process_cpu_user_microseconds ${cpu.user}`,
133
+ "",
134
+ "# HELP process_cpu_system_microseconds System CPU time in microseconds",
135
+ "# TYPE process_cpu_system_microseconds counter",
136
+ `process_cpu_system_microseconds ${cpu.system}`,
137
+ ""
138
+ ];
139
+ if (collectHttpMetrics && Object.keys(httpMetrics.requestsTotal).length > 0) {
140
+ lines.push("# HELP http_requests_total Total HTTP requests by status code", "# TYPE http_requests_total counter");
141
+ for (const [status, count] of Object.entries(httpMetrics.requestsTotal)) lines.push(`http_requests_total{status="${status}"} ${count}`);
142
+ lines.push("");
143
+ if (httpMetrics.requestDurations.length > 0) {
144
+ const sorted = [...httpMetrics.requestDurations].sort((a, b) => a - b);
145
+ const p50 = sorted[Math.floor(sorted.length * .5)] || 0;
146
+ const p95 = sorted[Math.floor(sorted.length * .95)] || 0;
147
+ const p99 = sorted[Math.floor(sorted.length * .99)] || 0;
148
+ const sum = sorted.reduce((a, b) => a + b, 0);
149
+ lines.push("# HELP http_request_duration_milliseconds HTTP request duration", "# TYPE http_request_duration_milliseconds summary", `http_request_duration_milliseconds{quantile="0.5"} ${p50.toFixed(2)}`, `http_request_duration_milliseconds{quantile="0.95"} ${p95.toFixed(2)}`, `http_request_duration_milliseconds{quantile="0.99"} ${p99.toFixed(2)}`, `http_request_duration_milliseconds_sum ${sum.toFixed(2)}`, `http_request_duration_milliseconds_count ${sorted.length}`, "");
150
+ }
151
+ }
152
+ return lines.join("\n");
153
+ });
154
+ if (collectHttpMetrics) fastify.addHook("onResponse", async (_request, reply) => {
155
+ const duration = reply.elapsedTime;
156
+ const statusBucket = `${Math.floor(reply.statusCode / 100)}xx`;
157
+ httpMetrics.requestsTotal[statusBucket] = (httpMetrics.requestsTotal[statusBucket] || 0) + 1;
158
+ if (httpMetrics.requestDurations.length < 1e4) httpMetrics.requestDurations.push(duration);
159
+ else httpMetrics.requestDurations[httpMetrics._ringIndex % 1e4] = duration;
160
+ httpMetrics._ringIndex = httpMetrics._ringIndex + 1;
161
+ });
162
+ fastify.log?.debug?.(`Health plugin registered at ${prefix} (alias), ${prefix}/live, ${prefix}/ready${metrics ? `, ${prefix}/metrics` : ""}`);
163
+ };
164
+ /**
165
+ * Run all health checks with timeout
166
+ */
167
+ async function runChecks(checks) {
168
+ const results = [];
169
+ for (const check of checks) {
170
+ const start = Date.now();
171
+ const timeout = check.timeout ?? 5e3;
172
+ let timer;
173
+ try {
174
+ const checkPromise = Promise.resolve(check.check());
175
+ const timeoutPromise = new Promise((_, reject) => {
176
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Health check timeout")), timeout);
177
+ });
178
+ const healthy = await Promise.race([checkPromise, timeoutPromise]);
179
+ results.push({
180
+ name: check.name,
181
+ healthy: Boolean(healthy),
182
+ duration: Date.now() - start
183
+ });
184
+ } catch (err) {
185
+ results.push({
186
+ name: check.name,
187
+ healthy: false,
188
+ duration: Date.now() - start,
189
+ error: err.message
190
+ });
191
+ } finally {
192
+ if (timer) clearTimeout(timer);
193
+ }
194
+ }
195
+ return results;
196
+ }
197
+ var health_default = fp(healthPlugin, {
198
+ name: "arc-health",
199
+ fastify: "5.x"
200
+ });
201
+ //#endregion
202
+ export { health_default as n, healthPlugin as t };
@@ -1,2 +1,2 @@
1
- import { Cn as afterDelete, Dn as beforeUpdate, En as beforeDelete, On as createHookSystem, Sn as afterCreate, Tn as beforeCreate, _n as HookOperation, bn as HookSystem, gn as HookHandler, hn as HookContext, kn as defineHook, mn as DefineHookOptions, vn as HookPhase, wn as afterUpdate, xn as HookSystemOptions, yn as HookRegistration } from "../index-Dtrj6XZk.mjs";
1
+ import { An as defineHook, Cn as afterCreate, Dn as beforeDelete, En as beforeCreate, On as beforeUpdate, Sn as HookSystemOptions, Tn as afterUpdate, _n as HookHandler, bn as HookRegistration, gn as HookContext, hn as DefineHookOptions, kn as createHookSystem, vn as HookOperation, wn as afterDelete, xn as HookSystem, yn as HookPhase } from "../index-BBsvQt7U.mjs";
2
2
  export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -77,6 +77,13 @@ declare module "fastify" {
77
77
  * ```
78
78
  */
79
79
  middleware: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
80
+ /**
81
+ * The resolved backing store. Exposed so non-HTTP executors (MCP
82
+ * tools via `mcpPlugin`'s execution wiring, jobs, workflows) can run
83
+ * the same check → lock → execute → record protocol against the same
84
+ * backend. `undefined` when the plugin is disabled.
85
+ */
86
+ store?: IdempotencyStore;
80
87
  };
81
88
  }
82
89
  }
@@ -1,4 +1,5 @@
1
1
  import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-DY6xq4sM.mjs";
2
+ import { t as createIdempotencyResult } from "../interface-n5hpKg1d.mjs";
2
3
  import { createHash } from "node:crypto";
3
4
  import fp from "fastify-plugin";
4
5
  import { and, eq, exists, gt, lt, or, startsWith } from "@classytic/repo-core/filter";
@@ -100,21 +101,6 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
100
101
  };
101
102
  }
102
103
  //#endregion
103
- //#region src/idempotency/stores/interface.ts
104
- /**
105
- * Helper to create a result object
106
- */
107
- function createIdempotencyResult(statusCode, body, headers, ttlMs) {
108
- const now = /* @__PURE__ */ new Date();
109
- return {
110
- statusCode,
111
- headers,
112
- body,
113
- createdAt: now,
114
- expiresAt: new Date(now.getTime() + ttlMs)
115
- };
116
- }
117
- //#endregion
118
104
  //#region src/idempotency/stores/memory.ts
119
105
  var MemoryIdempotencyStore = class {
120
106
  name = "memory";
@@ -283,16 +269,19 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
283
269
  fastify.decorate("idempotency", {
284
270
  invalidate: async () => {},
285
271
  has: async () => false,
286
- middleware: async () => {}
272
+ middleware: async () => {},
273
+ store: void 0
287
274
  });
288
275
  fastify.decorateRequest("idempotencyKey", void 0);
289
276
  fastify.decorateRequest("idempotencyReplayed", false);
277
+ fastify.decorateRequest("_idempotencyFullKey", void 0);
290
278
  fastify.log?.debug?.("Idempotency plugin disabled");
291
279
  return;
292
280
  }
293
281
  const methodSet = new Set(methods.map((m) => m.toUpperCase()));
294
282
  fastify.decorateRequest("idempotencyKey", void 0);
295
283
  fastify.decorateRequest("idempotencyReplayed", false);
284
+ fastify.decorateRequest("_idempotencyFullKey", void 0);
296
285
  /**
297
286
  * Check if this request should use idempotency
298
287
  */
@@ -378,7 +367,8 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
378
367
  has: async (key) => {
379
368
  return !!await store.findByPrefix(`${key}:`);
380
369
  },
381
- middleware: idempotencyMiddleware
370
+ middleware: idempotencyMiddleware,
371
+ store
382
372
  });
383
373
  fastify.addHook("preSerialization", async (request, reply, payload) => {
384
374
  if (request.idempotencyReplayed) return payload;