@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.
- package/dist/{BaseController-BnLYgE9A.mjs → BaseController-BQ8KNvWC.mjs} +3 -2
- package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
- package/dist/{actionPermissions-ze70xXJi.mjs → actionPermissions-lszoJle1.mjs} +1 -1
- package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
- package/dist/audit/index.d.mts +2 -2
- package/dist/auth/index.d.mts +30 -2
- package/dist/auth/index.mjs +6 -3
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -3
- package/dist/{core-DZ6Xe1jR.mjs → core-DQ6rDTyH.mjs} +126 -68
- package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-BuOv-m4Y.mjs} +10 -6
- package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-CgJv_BU_.mjs} +3 -3
- package/dist/{createApp-Bi61XxqU.mjs → createApp-CjOeWJ9_.mjs} +252 -57
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/encryption/index.mjs +9 -5
- package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-Bq2OeEQL.mjs} +34 -10
- package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
- package/dist/events/index.mjs +5 -2
- package/dist/factory/index.d.mts +46 -9
- package/dist/factory/index.mjs +64 -13
- package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-g_f9omSc.d.mts} +2 -2
- package/dist/fields-D3lhgzhD.d.mts +121 -0
- package/dist/health-ScyjIVo5.mjs +202 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +7 -0
- package/dist/idempotency/index.mjs +7 -17
- package/dist/{index-Dtrj6XZk.d.mts → index-BBsvQt7U.d.mts} +1310 -1226
- package/dist/{types-DpxoNX5v.d.mts → index-Ba10fdKp.d.mts} +196 -66
- package/dist/{index-B5N-NaHs.d.mts → index-CC5ruDGh.d.mts} +3 -3
- package/dist/{index-B5PoNbfu.d.mts → index-wMoWBLkn.d.mts} +68 -2
- package/dist/index.d.mts +6 -5
- package/dist/index.mjs +6 -5
- package/dist/{init-0YhHkXGk.mjs → init-CjO4p_q1.mjs} +123 -4
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +67 -4
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +131 -7
- package/dist/integrations/webhooks.mjs +169 -10
- package/dist/interface-n5hpKg1d.mjs +16 -0
- package/dist/jsonBody-BlzAoGCe.mjs +43 -0
- package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
- package/dist/logger/index.d.mts +26 -1
- package/dist/logger/index.mjs +39 -1
- package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-BtzIL_kM.mjs} +312 -39
- package/dist/middleware/index.d.mts +1 -1
- package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
- package/dist/{openapi-Kyod_di6.mjs → openapi-jSDx-VDv.mjs} +3 -3
- package/dist/permissions/index.d.mts +4 -3
- package/dist/permissions/index.mjs +3 -2
- package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +24 -7
- package/dist/plugins/index.mjs +14 -282
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +7 -4
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
- package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
- package/dist/realtime-I8OrfnMR.mjs +375 -0
- package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-CWO6mdYe.mjs} +38 -18
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/requestId-5BhTVzBv.mjs +113 -0
- package/dist/schemaConverter-DJvodiHj.mjs +171 -0
- package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-Ca7ySNPx.mjs} +2 -2
- package/dist/scim/index.mjs +6 -7
- package/dist/scope/index.mjs +2 -76
- package/dist/scope-LONQ3mfZ.mjs +37 -0
- package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
- package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
- package/dist/sseStream-BQ7Qaynp.mjs +57 -0
- package/dist/testing/index.d.mts +83 -3
- package/dist/testing/index.mjs +70 -3
- package/dist/types/index.d.mts +2 -2
- package/dist/{types-DRj-Ut4n.d.mts → types-5OuqeTDX.d.mts} +37 -2
- package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
- package/dist/usage/index.d.mts +142 -0
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +101 -19
- package/dist/utils/index.mjs +5 -3
- package/dist/{utils-CO8a3B_1.mjs → utils-CZyeRXTh.mjs} +80 -82
- package/dist/{versioning-DQ4mCvg0.d.mts → versioning-D6PvCwpq.d.mts} +43 -9
- package/llms.txt +39 -0
- package/package.json +513 -447
- package/skills/arc/SKILL.md +14 -8
- package/skills/arc/references/api-reference.md +34 -0
- package/skills/arc/references/auth.md +21 -4
- package/skills/arc/references/mcp.md +1 -1
- package/skills/arc/references/production.md +734 -654
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/anti-patterns.md +71 -18
- package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
- package/skills/arc-code-review/references/migration-recipes.md +7 -7
- package/skills/arc-code-review/references/mongokit-migration.md +4 -3
- package/skills/arc-module-publishing/SKILL.md +2 -1
- package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
- package/skills/arc-module-publishing/references/package-manifest.md +4 -2
- package/dist/preloadResources-CHw3_oA_.mjs +0 -45
- package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
package/dist/factory/index.d.mts
CHANGED
|
@@ -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-
|
|
2
|
-
import "../fastify-augmentation-
|
|
3
|
-
import { a as
|
|
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
|
|
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 };
|
package/dist/factory/index.mjs
CHANGED
|
@@ -1,33 +1,73 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
49
|
+
const { autoReady = true, maxRequestBytes = 1048576 } = options;
|
|
50
|
+
let readyPromise = null;
|
|
17
51
|
return async (request) => {
|
|
18
|
-
if (autoReady
|
|
19
|
-
|
|
20
|
-
|
|
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")
|
|
29
|
-
|
|
30
|
-
|
|
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.
|
|
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 {
|
|
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-
|
|
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 };
|
package/dist/hooks/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Cn as
|
|
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;
|