@classytic/arc 2.22.0 → 2.25.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/dist/{BaseController-C6FlRWxF.mjs → BaseController--M8EMzt0.mjs} +92 -22
  2. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  3. package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
  4. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  5. package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -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 +264 -260
  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/cli/commands/describe.d.mts +1 -1
  14. package/dist/cli/commands/describe.mjs +1 -1
  15. package/dist/cli/commands/docs.mjs +2 -2
  16. package/dist/cli/commands/generate.mjs +1 -1
  17. package/dist/cli/commands/init.mjs +1 -1
  18. package/dist/cli/commands/introspect.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 +4 -4
  23. package/dist/{core-b6fgFgnd.mjs → core-EUNu0u07.mjs} +298 -12
  24. package/dist/{createActionRouter-h1cEEaEX.mjs → createActionRouter-B6GvAuAX.mjs} +4 -3
  25. package/dist/{createAggregationRouter-B1C0kvTL.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  26. package/dist/docs/index.d.mts +1 -1
  27. package/dist/docs/index.mjs +2 -2
  28. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  29. package/dist/{errorHandler-BTzXSwIg.mjs → errorHandler-Bq2OeEQL.mjs} +10 -7
  30. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  31. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  32. package/dist/events/index.d.mts +57 -314
  33. package/dist/events/index.mjs +168 -93
  34. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  35. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  36. package/dist/factory/index.d.mts +21 -6
  37. package/dist/factory/index.mjs +58 -18
  38. package/dist/{fastify-augmentation-9_fuetB4.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
  39. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  40. package/dist/headers-BvlQSDMB.mjs +45 -0
  41. package/dist/health-CB_m4S32.mjs +377 -0
  42. package/dist/hooks/index.d.mts +1 -1
  43. package/dist/idempotency/index.d.mts +7 -0
  44. package/dist/idempotency/index.mjs +32 -20
  45. package/dist/{index-ogIHvOjo.d.mts → index-BMnbzvtO.d.mts} +70 -4
  46. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  47. package/dist/{index-DbtgCOCj.d.mts → index-DPRH0Cme.d.mts} +46 -94
  48. package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
  49. package/dist/{index-D8MKYGDa.d.mts → index-f4PTrk3B.d.mts} +37 -5
  50. package/dist/index.d.mts +6 -6
  51. package/dist/index.mjs +6 -5
  52. package/dist/{init-DleK3p9f.mjs → init-tlRSt667.mjs} +206 -205
  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 +68 -5
  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 +149 -22
  63. package/dist/integrations/webhooks.mjs +186 -42
  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/interface-n5hpKg1d.mjs +16 -0
  69. package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
  70. package/dist/{mcpPlugin-ByA2tf6D.mjs → mcpPlugin-Cb7olw0j.mjs} +316 -45
  71. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  72. package/dist/middleware/index.d.mts +17 -1
  73. package/dist/middleware/index.mjs +1 -1
  74. package/dist/migrations/index.d.mts +94 -44
  75. package/dist/migrations/index.mjs +196 -44
  76. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  77. package/dist/{openapi-BRpMZD7C.mjs → openapi-DrLtog_B.mjs} +4 -4
  78. package/dist/permissions/index.d.mts +3 -3
  79. package/dist/permissions/index.mjs +4 -3
  80. package/dist/{permissions-BJMuKMl7.mjs → permissions-BrtiBKGs.mjs} +305 -492
  81. package/dist/pipeline/index.d.mts +1 -1
  82. package/dist/{actionPermissions-ze70xXJi.mjs → pipeline-BN5G05rd.mjs} +4 -27
  83. package/dist/plugins/index.d.mts +7 -78
  84. package/dist/plugins/index.mjs +14 -97
  85. package/dist/plugins/response-cache.mjs +1 -0
  86. package/dist/plugins/tracing-entry.mjs +1 -1
  87. package/dist/{index-BEu4wjbL.d.mts → preloadResources-0oe-JSSS.d.mts} +59 -50
  88. package/dist/{createApp-Ci7yG5gv.mjs → preloadResources-DfyngIN9.mjs} +240 -68
  89. package/dist/presets/filesUpload.d.mts +3 -3
  90. package/dist/presets/filesUpload.mjs +3 -3
  91. package/dist/presets/index.d.mts +2 -1
  92. package/dist/presets/index.mjs +1 -1
  93. package/dist/presets/multiTenant.d.mts +1 -1
  94. package/dist/presets/multiTenant.mjs +1 -1
  95. package/dist/presets/search.d.mts +2 -2
  96. package/dist/presets/search.mjs +1 -1
  97. package/dist/{presets-gw0ciIUD.mjs → presets-DwFDVKP0.mjs} +2 -2
  98. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  99. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  100. package/dist/{registerRoute-xHc42fwB.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  101. package/dist/realtime-Cuw_deA-.mjs +375 -0
  102. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  103. package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
  104. package/dist/registry/index.d.mts +2 -2
  105. package/dist/registry/index.mjs +1 -1
  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/{schemaConverter-Ds5reSGs.mjs → schemaConverter-DJvodiHj.mjs} +4 -1
  110. package/dist/{schemaIR-B-6SPe7g.mjs → schemaIR-Ca7ySNPx.mjs} +1 -1
  111. package/dist/schemas/index.d.mts +67 -67
  112. package/dist/schemas/index.mjs +2 -3
  113. package/dist/scim/index.mjs +1 -2
  114. package/dist/scope/index.d.mts +2 -2
  115. package/dist/scope/index.mjs +4 -4
  116. package/dist/scope-DfaPntpo.mjs +36 -0
  117. package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
  118. package/dist/{sse-CfUjwjDk.mjs → sse-BQ1w0dfl.mjs} +8 -31
  119. package/dist/sseStream-BQ7Qaynp.mjs +57 -0
  120. package/dist/testing/index.d.mts +4 -3
  121. package/dist/testing/index.mjs +2 -2
  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-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  126. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  127. package/dist/{types-Bc42CUyk.d.mts → types-QFGc3Cjv.d.mts} +37 -2
  128. package/dist/usage/index.d.mts +3 -52
  129. package/dist/usage/index.mjs +0 -0
  130. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  131. package/dist/utils/index.d.mts +90 -57
  132. package/dist/utils/index.mjs +7 -3
  133. package/dist/{utils-FhJ88TJE.mjs → utils-taUVD4p3.mjs} +47 -385
  134. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  135. package/dist/{versioning-DLwklPlG.d.mts → versioning-DXgY5BUm.d.mts} +52 -31
  136. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  137. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  138. package/package.json +526 -453
  139. package/skills/arc/references/api-reference.md +3 -1
  140. package/skills/arc/references/integrations.md +20 -11
  141. package/skills/arc/references/production.md +734 -696
  142. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  143. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  144. package/dist/health-ScyjIVo5.mjs +0 -202
  145. package/dist/requestId-5BhTVzBv.mjs +0 -113
  146. package/dist/scope-k15RXfcq.mjs +0 -64
  147. package/dist/types-XTApa8b6.mjs +0 -24
@@ -0,0 +1,45 @@
1
+ import { u as ValidationError } from "./errors-DkO8Nk6-.mjs";
2
+ //#region src/utils/headers.ts
3
+ /**
4
+ * Canonical header typing + accessor.
5
+ *
6
+ * Node/Fastify header values are `string | string[] | undefined` — repeated
7
+ * headers arrive as arrays. `IRequestContext.headers` is DECLARED as
8
+ * `Record<string, string | undefined>` for ergonomic host code, but the HTTP
9
+ * adapter passes Fastify's headers object through unchanged, so `string[]`
10
+ * CAN appear at runtime behind the narrower type (the declared type is
11
+ * scheduled to widen to {@link ArcHeaders} in v3). Until then, read headers
12
+ * through {@link getHeader} instead of indexing — it's honest about arrays
13
+ * and deterministic about duplicates.
14
+ */
15
+ /**
16
+ * Read a single header value. Lowercases the name (Node normalizes incoming
17
+ * header names to lowercase) and returns the FIRST value when the header
18
+ * was repeated.
19
+ *
20
+ * Duplicate policy: first-value is deterministic, but for security-sensitive
21
+ * headers (authorization material, forwarded-for chains, content-length) a
22
+ * duplicate usually signals smuggling or a broken proxy — consumers that
23
+ * care should check `Array.isArray(headers[name])` and REJECT instead of
24
+ * picking a winner.
25
+ */
26
+ function getHeader(headers, name) {
27
+ if (!headers) return void 0;
28
+ const value = headers[name.toLowerCase()];
29
+ if (Array.isArray(value)) return value[0];
30
+ return value;
31
+ }
32
+ /**
33
+ * Read an identity-bearing or protocol header and reject repeated values.
34
+ *
35
+ * Repeated security-sensitive headers are ambiguous at proxy boundaries and
36
+ * may indicate request smuggling. Callers must not silently choose a winner.
37
+ */
38
+ function requireSingleHeaderValue(headers, name) {
39
+ if (!headers) return void 0;
40
+ const value = headers[name.toLowerCase()];
41
+ if (Array.isArray(value)) throw new ValidationError(`Duplicate '${name}' header`);
42
+ return value;
43
+ }
44
+ //#endregion
45
+ export { requireSingleHeaderValue as n, getHeader as t };
@@ -0,0 +1,377 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fp from "fastify-plugin";
3
+ //#region src/plugins/requestId.ts
4
+ /**
5
+ * Request ID Plugin
6
+ *
7
+ * Propagates request IDs for distributed tracing.
8
+ * - Accepts incoming x-request-id header (sanitized)
9
+ * - Generates UUID if not provided
10
+ * - Echoes the id on the response header + W3C trace-context propagation
11
+ *
12
+ * ID RESOLUTION LIVES AT THE SERVER LEVEL, NOT HERE. Fastify binds
13
+ * `request.log = logger.child({ reqId: request.id })` at request
14
+ * construction — before any hook runs — so an id assigned inside a hook
15
+ * never reaches the logs. `createApp` therefore wires
16
+ * {@link createRequestIdGenerator} into Fastify's `genReqId` option, and
17
+ * this plugin simply adopts `request.id`. Standalone registration on a
18
+ * bare Fastify instance falls back to the legacy in-hook resolution
19
+ * (header echo / UUID), with the documented limitation that
20
+ * `request.log`'s `reqId` binding predates the overwrite — pass
21
+ * `genReqId: createRequestIdGenerator()` to `Fastify()` to fix that.
22
+ *
23
+ * @example
24
+ * import { requestIdPlugin, createRequestIdGenerator } from '@classytic/arc';
25
+ *
26
+ * const fastify = Fastify({ genReqId: createRequestIdGenerator() });
27
+ * await fastify.register(requestIdPlugin);
28
+ *
29
+ * fastify.get('/', async (request) => {
30
+ * request.log.info('traced'); // carries the same reqId as the header
31
+ * return { id: request.id };
32
+ * });
33
+ */
34
+ /**
35
+ * Sanitize an incoming request-id header value: max 128 chars, alphanumeric
36
+ * plus dashes/underscores/dots/colons only. Returns `undefined` for anything
37
+ * else (crafted values could pollute logs or response headers). Array values
38
+ * (repeated headers) are rejected outright — a client sending multiple
39
+ * request-id headers is not tracing in good faith.
40
+ */
41
+ function sanitizeRequestId(value) {
42
+ if (typeof value !== "string") return void 0;
43
+ const trimmed = value.trim();
44
+ return trimmed.length > 0 && trimmed.length <= 128 && /^[\w.:-]+$/.test(trimmed) ? trimmed : void 0;
45
+ }
46
+ /**
47
+ * Build a Fastify `genReqId` function: adopt a sanitized incoming request-id
48
+ * header, or generate one. Wire this into the `Fastify()` constructor so
49
+ * `request.id`, `request.log`'s `reqId` binding, and the echoed response
50
+ * header all agree — `createApp` does this automatically.
51
+ */
52
+ function createRequestIdGenerator(opts = {}) {
53
+ const { header = "x-request-id", generator = randomUUID } = opts;
54
+ const headerName = header.toLowerCase();
55
+ return (req) => sanitizeRequestId(req.headers[headerName]) ?? generator();
56
+ }
57
+ /** Fastify's default request ids are `req-<base36 counter>`. */
58
+ const FASTIFY_DEFAULT_ID = /^req-[0-9a-z]+$/;
59
+ /** W3C traceparent format: `version-traceid-parentid-flags` */
60
+ const TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
61
+ /** W3C spec: version ff is reserved and invalid; trace/parent IDs must not be all zeros */
62
+ const TRACEID_ZEROS = "0".repeat(32);
63
+ const PARENTID_ZEROS = "0".repeat(16);
64
+ /** tracestate: printable ASCII, <= 512 bytes */
65
+ const TRACESTATE_MAX = 512;
66
+ function isValidTraceparent(tp) {
67
+ const m = TRACEPARENT_RE.exec(tp);
68
+ if (!m) return false;
69
+ const version = m[1] ?? "";
70
+ const traceId = m[2] ?? "";
71
+ const parentId = m[3] ?? "";
72
+ if (version.toLowerCase() === "ff") return false;
73
+ if (traceId.toLowerCase() === TRACEID_ZEROS) return false;
74
+ if (parentId.toLowerCase() === PARENTID_ZEROS) return false;
75
+ return true;
76
+ }
77
+ const requestIdPlugin = async (fastify, opts = {}) => {
78
+ const { header = "x-request-id", generator = randomUUID, setResponseHeader = true, propagateTraceContext = true } = opts;
79
+ if (!fastify.hasRequestDecorator("requestId")) fastify.decorateRequest("requestId", "");
80
+ if (!fastify.hasRequestDecorator("traceContext")) fastify.decorateRequest("traceContext", void 0);
81
+ fastify.addHook("onRequest", async (request, reply) => {
82
+ const incoming = sanitizeRequestId(request.headers[header]);
83
+ let requestId = String(request.id);
84
+ if (incoming && incoming !== requestId) requestId = incoming;
85
+ else if (!incoming && FASTIFY_DEFAULT_ID.test(requestId)) requestId = generator();
86
+ if (requestId !== request.id) request.id = requestId;
87
+ request.requestId = requestId;
88
+ if (setResponseHeader) reply.header(header, requestId);
89
+ if (propagateTraceContext) {
90
+ const rawTraceparent = request.headers.traceparent;
91
+ const rawTracestate = request.headers.tracestate;
92
+ if (typeof rawTraceparent === "string") {
93
+ const tp = rawTraceparent.trim();
94
+ if (isValidTraceparent(tp)) {
95
+ const ts = typeof rawTracestate === "string" && rawTracestate.length > 0 && rawTracestate.length <= TRACESTATE_MAX ? rawTracestate : void 0;
96
+ request.traceContext = {
97
+ traceparent: tp,
98
+ ...ts ? { tracestate: ts } : {}
99
+ };
100
+ reply.header("traceparent", tp);
101
+ if (ts) reply.header("tracestate", ts);
102
+ }
103
+ }
104
+ }
105
+ });
106
+ fastify.log?.debug?.("Request ID plugin registered");
107
+ };
108
+ var requestId_default = fp(requestIdPlugin, {
109
+ name: "arc-request-id",
110
+ fastify: "5.x"
111
+ });
112
+ //#endregion
113
+ //#region src/plugins/health.ts
114
+ /**
115
+ * Merge health checks without losing ownership information.
116
+ *
117
+ * Check names are the identity used in readiness responses and criticality
118
+ * lookup, so duplicates are ambiguous and fail at boot instead of silently
119
+ * picking the first declaration. Input order is preserved.
120
+ */
121
+ function mergeHealthChecks(groups) {
122
+ const owners = /* @__PURE__ */ new Map();
123
+ const merged = [];
124
+ for (const group of groups) for (const check of group.checks ?? []) {
125
+ const prior = owners.get(check.name);
126
+ if (prior !== void 0) throw new Error(`[arc] duplicate health-check name "${check.name}" — declared by ${prior} and ${group.owner}. Health-check names must be unique.`);
127
+ owners.set(check.name, group.owner);
128
+ merged.push(check);
129
+ }
130
+ return merged;
131
+ }
132
+ /**
133
+ * Fixed histogram bucket upper bounds (ms). Recording is O(#buckets) per
134
+ * request; the scrape derives quantile UPPER BOUNDS from cumulative counts
135
+ * in O(#buckets) — no sample retention, no per-scrape sort (the previous
136
+ * 10k-sample ring buffer copied + sorted on EVERY scrape), and bucket
137
+ * counts aggregate correctly across instances.
138
+ */
139
+ const DURATION_BUCKETS_MS = [
140
+ 1,
141
+ 2,
142
+ 5,
143
+ 10,
144
+ 25,
145
+ 50,
146
+ 100,
147
+ 250,
148
+ 500,
149
+ 1e3,
150
+ 2500,
151
+ 5e3,
152
+ 1e4
153
+ ];
154
+ function createHttpMetrics() {
155
+ return {
156
+ requestsTotal: {},
157
+ durationBuckets: new Array(DURATION_BUCKETS_MS.length + 1).fill(0),
158
+ durationCount: 0,
159
+ durationSumMs: 0,
160
+ startTime: Date.now()
161
+ };
162
+ }
163
+ /** Smallest bucket upper bound whose cumulative count reaches `q × total`. */
164
+ function bucketQuantile(metrics, q) {
165
+ const target = Math.ceil(metrics.durationCount * q);
166
+ let cumulative = 0;
167
+ for (let i = 0; i < metrics.durationBuckets.length; i++) {
168
+ cumulative += metrics.durationBuckets[i] ?? 0;
169
+ if (cumulative >= target) return DURATION_BUCKETS_MS[i] ?? DURATION_BUCKETS_MS[DURATION_BUCKETS_MS.length - 1] ?? 0;
170
+ }
171
+ return DURATION_BUCKETS_MS[DURATION_BUCKETS_MS.length - 1] ?? 0;
172
+ }
173
+ const healthPlugin = async (fastify, opts = {}) => {
174
+ const { prefix = "/_health", checks = [], metrics = false, metricsCollector, version, collectHttpMetrics = metrics } = opts;
175
+ const readinessChecks = mergeHealthChecks([{
176
+ owner: "healthPlugin options",
177
+ checks
178
+ }]);
179
+ const httpMetrics = createHttpMetrics();
180
+ fastify.get(prefix, { schema: {
181
+ tags: ["Health"],
182
+ summary: "Liveness probe (alias for /live)",
183
+ description: "Returns 200 if the process is alive",
184
+ response: { 200: {
185
+ type: "object",
186
+ properties: {
187
+ status: {
188
+ type: "string",
189
+ enum: ["ok"]
190
+ },
191
+ timestamp: { type: "string" },
192
+ version: { type: "string" }
193
+ }
194
+ } }
195
+ } }, async () => ({
196
+ status: "ok",
197
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
198
+ ...version ? { version } : {}
199
+ }));
200
+ fastify.get(`${prefix}/live`, { schema: {
201
+ tags: ["Health"],
202
+ summary: "Liveness probe",
203
+ description: "Returns 200 if the process is alive",
204
+ response: { 200: {
205
+ type: "object",
206
+ properties: {
207
+ status: {
208
+ type: "string",
209
+ enum: ["ok"]
210
+ },
211
+ timestamp: { type: "string" },
212
+ version: { type: "string" }
213
+ }
214
+ } }
215
+ } }, async () => {
216
+ return {
217
+ status: "ok",
218
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
219
+ ...version ? { version } : {}
220
+ };
221
+ });
222
+ fastify.get(`${prefix}/ready`, { schema: {
223
+ tags: ["Health"],
224
+ summary: "Readiness probe",
225
+ description: "Returns 200 if all dependencies are healthy",
226
+ response: {
227
+ 200: {
228
+ type: "object",
229
+ properties: {
230
+ status: {
231
+ type: "string",
232
+ enum: ["ready", "not_ready"]
233
+ },
234
+ timestamp: { type: "string" },
235
+ checks: {
236
+ type: "array",
237
+ items: {
238
+ type: "object",
239
+ properties: {
240
+ name: { type: "string" },
241
+ healthy: { type: "boolean" },
242
+ duration: { type: "number" },
243
+ error: { type: "string" }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ },
249
+ 503: {
250
+ type: "object",
251
+ properties: {
252
+ status: {
253
+ type: "string",
254
+ enum: ["not_ready"]
255
+ },
256
+ timestamp: { type: "string" },
257
+ checks: { type: "array" }
258
+ }
259
+ }
260
+ }
261
+ } }, async (_, reply) => {
262
+ const results = await runChecks(readinessChecks);
263
+ const criticalFailed = results.some((result, index) => !result.healthy && (readinessChecks[index]?.critical ?? true));
264
+ const response = {
265
+ status: criticalFailed ? "not_ready" : "ready",
266
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
267
+ checks: results
268
+ };
269
+ if (criticalFailed) reply.code(503);
270
+ return response;
271
+ });
272
+ if (metrics) fastify.get(`${prefix}/metrics`, async (_, reply) => {
273
+ reply.type("text/plain; charset=utf-8");
274
+ if (metricsCollector) return await metricsCollector();
275
+ const uptime = process.uptime();
276
+ const memory = process.memoryUsage();
277
+ const cpu = process.cpuUsage();
278
+ const lines = [
279
+ "# HELP process_uptime_seconds Process uptime in seconds",
280
+ "# TYPE process_uptime_seconds gauge",
281
+ `process_uptime_seconds ${uptime.toFixed(2)}`,
282
+ "",
283
+ "# HELP process_memory_heap_bytes Heap memory usage in bytes",
284
+ "# TYPE process_memory_heap_bytes gauge",
285
+ `process_memory_heap_bytes{type="used"} ${memory.heapUsed}`,
286
+ `process_memory_heap_bytes{type="total"} ${memory.heapTotal}`,
287
+ "",
288
+ "# HELP process_memory_rss_bytes RSS memory in bytes",
289
+ "# TYPE process_memory_rss_bytes gauge",
290
+ `process_memory_rss_bytes ${memory.rss}`,
291
+ "",
292
+ "# HELP process_memory_external_bytes External memory in bytes",
293
+ "# TYPE process_memory_external_bytes gauge",
294
+ `process_memory_external_bytes ${memory.external}`,
295
+ "",
296
+ "# HELP process_cpu_user_microseconds User CPU time in microseconds",
297
+ "# TYPE process_cpu_user_microseconds counter",
298
+ `process_cpu_user_microseconds ${cpu.user}`,
299
+ "",
300
+ "# HELP process_cpu_system_microseconds System CPU time in microseconds",
301
+ "# TYPE process_cpu_system_microseconds counter",
302
+ `process_cpu_system_microseconds ${cpu.system}`,
303
+ ""
304
+ ];
305
+ if (collectHttpMetrics && Object.keys(httpMetrics.requestsTotal).length > 0) {
306
+ lines.push("# HELP http_requests_total Total HTTP requests by status code", "# TYPE http_requests_total counter");
307
+ for (const [status, count] of Object.entries(httpMetrics.requestsTotal)) lines.push(`http_requests_total{status="${status}"} ${count}`);
308
+ lines.push("");
309
+ if (httpMetrics.durationCount > 0) {
310
+ const p50 = bucketQuantile(httpMetrics, .5);
311
+ const p95 = bucketQuantile(httpMetrics, .95);
312
+ const p99 = bucketQuantile(httpMetrics, .99);
313
+ lines.push("# HELP http_request_duration_milliseconds HTTP request duration (bucket upper bounds)", "# 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 ${httpMetrics.durationSumMs.toFixed(2)}`, `http_request_duration_milliseconds_count ${httpMetrics.durationCount}`, "");
314
+ }
315
+ }
316
+ return lines.join("\n");
317
+ });
318
+ if (collectHttpMetrics) fastify.addHook("onResponse", async (_request, reply) => {
319
+ const duration = reply.elapsedTime;
320
+ const statusBucket = `${Math.floor(reply.statusCode / 100)}xx`;
321
+ httpMetrics.requestsTotal[statusBucket] = (httpMetrics.requestsTotal[statusBucket] || 0) + 1;
322
+ let bucket = DURATION_BUCKETS_MS.length;
323
+ for (let i = 0; i < DURATION_BUCKETS_MS.length; i++) {
324
+ const bound = DURATION_BUCKETS_MS[i];
325
+ if (bound !== void 0 && duration <= bound) {
326
+ bucket = i;
327
+ break;
328
+ }
329
+ }
330
+ httpMetrics.durationBuckets[bucket] = (httpMetrics.durationBuckets[bucket] ?? 0) + 1;
331
+ httpMetrics.durationCount++;
332
+ httpMetrics.durationSumMs += duration;
333
+ });
334
+ fastify.log?.debug?.(`Health plugin registered at ${prefix} (alias), ${prefix}/live, ${prefix}/ready${metrics ? `, ${prefix}/metrics` : ""}`);
335
+ };
336
+ /**
337
+ * Run all health checks with timeout
338
+ */
339
+ async function runChecks(checks) {
340
+ return Promise.all(checks.map(async (check) => {
341
+ const start = Date.now();
342
+ const timeout = check.timeout ?? 5e3;
343
+ let timer;
344
+ const abort = new AbortController();
345
+ try {
346
+ const checkPromise = Promise.resolve(check.check(abort.signal));
347
+ const timeoutPromise = new Promise((_, reject) => {
348
+ timer = setTimeout(() => {
349
+ const err = /* @__PURE__ */ new Error("Health check timeout");
350
+ abort.abort(err);
351
+ reject(err);
352
+ }, timeout);
353
+ });
354
+ const healthy = await Promise.race([checkPromise, timeoutPromise]);
355
+ return {
356
+ name: check.name,
357
+ healthy: Boolean(healthy),
358
+ duration: Date.now() - start
359
+ };
360
+ } catch (err) {
361
+ return {
362
+ name: check.name,
363
+ healthy: false,
364
+ duration: Date.now() - start,
365
+ error: err.message
366
+ };
367
+ } finally {
368
+ if (timer) clearTimeout(timer);
369
+ }
370
+ }));
371
+ }
372
+ var health_default = fp(healthPlugin, {
373
+ name: "arc-health",
374
+ fastify: "5.x"
375
+ });
376
+ //#endregion
377
+ export { requestIdPlugin as a, createRequestIdGenerator as i, health_default as n, requestId_default as o, mergeHealthChecks as r, sanitizeRequestId as s, 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-DbtgCOCj.mjs";
1
+ import { Cn as defineHook, Sn as createHookSystem, _n as afterDelete, bn as beforeDelete, cn as DefineHookOptions, dn as HookOperation, fn as HookPhase, gn as afterCreate, hn as HookSystemOptions, ln as HookContext, mn as HookSystem, pn as HookRegistration, un as HookHandler, vn as afterUpdate, xn as beforeUpdate, yn as beforeCreate } from "../index-DPRH0Cme.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,6 @@
1
+ import { n as requireSingleHeaderValue } from "../headers-BvlQSDMB.mjs";
1
2
  import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-DY6xq4sM.mjs";
3
+ import { t as createIdempotencyResult } from "../interface-n5hpKg1d.mjs";
2
4
  import { createHash } from "node:crypto";
3
5
  import fp from "fastify-plugin";
4
6
  import { and, eq, exists, gt, lt, or, startsWith } from "@classytic/repo-core/filter";
@@ -100,21 +102,6 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
100
102
  };
101
103
  }
102
104
  //#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
105
  //#region src/idempotency/stores/memory.ts
119
106
  var MemoryIdempotencyStore = class {
120
107
  name = "memory";
@@ -263,6 +250,26 @@ var MemoryIdempotencyStore = class {
263
250
  * // Idempotency-Key: order-123-abc
264
251
  *
265
252
  * // If same key sent again within TTL, returns cached response
253
+ *
254
+ * ## Guarantee: best-effort effectively-once, NOT exactly-once
255
+ *
256
+ * The plugin gives strong request replay + overlap protection: a
257
+ * distributed lock guards execution (concurrent same-key requests get
258
+ * 409), the store key includes a body/caller fingerprint (a different
259
+ * body under the same key executes as its own operation instead of
260
+ * replaying someone else's response), and a completed result replays on
261
+ * hit. It does NOT make the business effect exactly-once, because the
262
+ * domain write and the idempotency record live in different systems:
263
+ *
264
+ * - a crash after the DB mutation but before the result is recorded
265
+ * (preSerialization) lets a retry re-execute the mutation;
266
+ * - a handler that outruns `lockTimeoutMs` can overlap its own retry —
267
+ * size the lock timeout above the slowest protected handler.
268
+ *
269
+ * For mutations where re-execution is unacceptable, write the idempotency
270
+ * record in the SAME transaction as the business write inside the handler
271
+ * (the store contract is public — `@classytic/arc/idempotency` exports it),
272
+ * or make the handler naturally idempotent (upserts, compare-and-set).
266
273
  */
267
274
  const HEADER_IDEMPOTENCY_REPLAYED = "x-idempotency-replayed";
268
275
  const HEADER_IDEMPOTENCY_KEY = "x-idempotency-key";
@@ -283,7 +290,8 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
283
290
  fastify.decorate("idempotency", {
284
291
  invalidate: async () => {},
285
292
  has: async () => false,
286
- middleware: async () => {}
293
+ middleware: async () => {},
294
+ store: void 0
287
295
  });
288
296
  fastify.decorateRequest("idempotencyKey", void 0);
289
297
  fastify.decorateRequest("idempotencyReplayed", false);
@@ -319,7 +327,11 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
319
327
  */
320
328
  function normalizeBody(obj, depth = 0) {
321
329
  if (obj === null || typeof obj !== "object") return obj;
322
- if (depth >= MAX_FINGERPRINT_DEPTH) return "<truncated>";
330
+ if (depth >= MAX_FINGERPRINT_DEPTH) try {
331
+ return `<deep:${createHash("sha256").update(JSON.stringify(obj)).digest("hex").slice(0, 16)}>`;
332
+ } catch {
333
+ return "<truncated>";
334
+ }
323
335
  if (Array.isArray(obj)) return obj.map((item) => normalizeBody(item, depth + 1));
324
336
  const sorted = {};
325
337
  const keys = Object.keys(obj).sort();
@@ -348,8 +360,7 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
348
360
  }
349
361
  const idempotencyMiddleware = async (request, reply) => {
350
362
  if (!shouldApplyIdempotency(request)) return;
351
- const keyHeader = request.headers[headerName.toLowerCase()];
352
- const idempotencyKey = typeof keyHeader === "string" ? keyHeader.trim() : void 0;
363
+ const idempotencyKey = requireSingleHeaderValue(request.headers, headerName)?.trim();
353
364
  if (!idempotencyKey) return;
354
365
  request.idempotencyKey = idempotencyKey;
355
366
  const fullKey = `${idempotencyKey}:${getRequestFingerprint(request)}`;
@@ -380,7 +391,8 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
380
391
  has: async (key) => {
381
392
  return !!await store.findByPrefix(`${key}:`);
382
393
  },
383
- middleware: idempotencyMiddleware
394
+ middleware: idempotencyMiddleware,
395
+ store
384
396
  });
385
397
  fastify.addHook("preSerialization", async (request, reply, payload) => {
386
398
  if (request.idempotencyReplayed) return payload;
@@ -1,6 +1,6 @@
1
- import { r as CacheStore, t as CacheLogger } from "./interface-C1_WLTMK.mjs";
2
- import { i as RequestScope, n as Mandate } from "./types-DH0GupI7.mjs";
3
- import { n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-Csxga7d3.mjs";
1
+ import { r as CacheStore, t as CacheLogger } from "./interface-BGZCzOGL.mjs";
2
+ import { i as RequestScope, n as Mandate } from "./types-DjC-KUCz.mjs";
3
+ import { n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-Dmw-6Qeo.mjs";
4
4
  import { FastifyReply, FastifyRequest } from "fastify";
5
5
  //#region src/permissions/agent.d.ts
6
6
  /**
@@ -449,6 +449,72 @@ declare function createOrgPermissions(config: {
449
449
  */
450
450
  declare function createDynamicPermissionMatrix(config: DynamicPermissionMatrixConfig): DynamicPermissionMatrix;
451
451
  //#endregion
452
+ //#region src/permissions/grants.d.ts
453
+ /**
454
+ * The grant-mode lattice, weakest → strongest. Holding a mode satisfies
455
+ * every mode at or below it (`write` ⊇ `read` ⊇ `list` ⊇ `see`); `manage`
456
+ * (grant/revoke rights) satisfies everything. Same semantics as Puter's
457
+ * `see < list < read < write` hierarchy with `manage` folded into the
458
+ * lattice top instead of a separate string namespace.
459
+ */
460
+ declare const GRANT_MODES: readonly ["see", "list", "read", "write", "manage"];
461
+ /** One mode from the {@link GRANT_MODES} lattice. */
462
+ type GrantMode = (typeof GRANT_MODES)[number];
463
+ /**
464
+ * Does a HELD mode satisfy a REQUIRED mode? Lattice implication —
465
+ * `modeSatisfies('write', 'read') === true`, `modeSatisfies('see', 'read')
466
+ * === false`. Unknown strings (a corrupted grant row) never satisfy
467
+ * anything: fail-closed.
468
+ */
469
+ declare function modeSatisfies(held: string | undefined, required: GrantMode): boolean;
470
+ /**
471
+ * What a grant `resolve` callback may return:
472
+ *
473
+ * - `boolean` — the host did the whole check itself (already compared modes).
474
+ * - `{ mode }` — the strongest mode the caller holds on `ctx.resourceId`;
475
+ * the combinator applies the lattice against the required mode.
476
+ * - `{ filters }` — list-shaped resolution: row filters that scope the
477
+ * query to visible records (flows through `PermissionResult.filters` →
478
+ * `_policyFilters` → the repository query). Grants access with the
479
+ * filters attached.
480
+ * - `{ mode, filters }` — mode gates; filters ride along when it passes.
481
+ *
482
+ * An empty object (neither `mode` nor `filters`) and a thrown error both
483
+ * DENY — grant resolution is fail-closed, same stance as `isRevoked`.
484
+ */
485
+ type GrantResolution = boolean | {
486
+ mode?: GrantMode | string;
487
+ filters?: Record<string, unknown>;
488
+ /** Optional denial reason surfaced on the 403 (clamped by the framework). */
489
+ reason?: string;
490
+ };
491
+ interface RequireGrantOptions {
492
+ /** The mode this operation needs (lattice-checked against the held mode). */
493
+ mode: GrantMode;
494
+ /**
495
+ * Look up the caller's grant. Receives the full `PermissionContext`
496
+ * (`resourceId` for single-record ops, `request` for headers/scope —
497
+ * e.g. a share-link token). May be sync or async.
498
+ *
499
+ * NOTE: unauthenticated callers are NOT rejected up front — share links
500
+ * grant access to anonymous callers by design. If your grants are
501
+ * session-only, return `false` when `ctx.user` is null (or compose with
502
+ * `requireAuth()` via `allOf`).
503
+ */
504
+ resolve: (ctx: PermissionContext) => GrantResolution | Promise<GrantResolution>;
505
+ /** Roles that skip the grant check entirely (platform admins). */
506
+ bypassRoles?: readonly string[];
507
+ }
508
+ /**
509
+ * Per-record grant gate. Same combinator tier as `requireOwnership` —
510
+ * compose with `anyOf`/`allOf` like any other `PermissionCheck`.
511
+ *
512
+ * Fail-closed contract: resolver throws → denied (logged via
513
+ * `ctx.request.log`); resolution carries neither `mode` nor `filters` →
514
+ * denied; unknown/corrupted mode strings → denied.
515
+ */
516
+ declare function requireGrant(options: RequireGrantOptions): PermissionCheck;
517
+ //#endregion
452
518
  //#region src/permissions/roleHierarchy.d.ts
453
519
  /**
454
520
  * Role Hierarchy — Composable RBAC Inheritance
@@ -706,4 +772,4 @@ declare function fullPublic<TDoc = Record<string, unknown>>(overrides?: Permissi
706
772
  */
707
773
  declare function readOnly<TDoc = Record<string, unknown>>(overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
708
774
  //#endregion
709
- export { requireOwnership as A, requireMandate as B, createOrgPermissions as C, denyAll as D, anyOf as E, normalizePermissionResult as F, RequireAgentScopeOptions as I, RequireMandateOptions as L, roles as M, when as N, not as O, applyPermissionResult as P, requireAgentScope as R, createDynamicPermissionMatrix as S, allowPublic as T, createRoleHierarchy as _, permissionMatrix as a, DynamicPermissionMatrixConfig as b, publicReadAdminWrite as c, requireOrgMembership as d, requireOrgRole as f, RoleHierarchy as g, requireTeamMembership as h, ownerWithAdminBypass as i, requireRoles as j, requireAuth as k, readOnly as l, requireServiceScope as m, authenticated as n, presets_d_exports as o, requireScopeContext as p, fullPublic as r, publicRead as s, adminOnly as t, requireOrgInScope as u, ConnectEventsOptions as v, allOf as w, PermissionEventBus as x, DynamicPermissionMatrix as y, requireDPoP as z };
775
+ export { allOf as A, applyPermissionResult as B, requireGrant as C, PermissionEventBus as D, DynamicPermissionMatrixConfig as E, requireAuth as F, requireDPoP as G, RequireAgentScopeOptions as H, requireOwnership as I, requireMandate as K, requireRoles as L, anyOf as M, denyAll as N, createDynamicPermissionMatrix as O, not as P, roles as R, modeSatisfies as S, DynamicPermissionMatrix as T, RequireMandateOptions as U, normalizePermissionResult as V, requireAgentScope as W, createRoleHierarchy as _, permissionMatrix as a, GrantResolution as b, publicReadAdminWrite as c, requireOrgMembership as d, requireOrgRole as f, RoleHierarchy as g, requireTeamMembership as h, ownerWithAdminBypass as i, allowPublic as j, createOrgPermissions as k, readOnly as l, requireServiceScope as m, authenticated as n, presets_d_exports as o, requireScopeContext as p, fullPublic as r, publicRead as s, adminOnly as t, requireOrgInScope as u, GRANT_MODES as v, ConnectEventsOptions as w, RequireGrantOptions as x, GrantMode as y, when as z };