@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.
- package/dist/{BaseController-C6FlRWxF.mjs → BaseController--M8EMzt0.mjs} +92 -22
- package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
- package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
- package/dist/actionPermissions-CAphcmUG.mjs +24 -0
- package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
- package/dist/audit/index.d.mts +32 -3
- package/dist/audit/index.mjs +7 -4
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +264 -260
- package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
- package/dist/cache/index.d.mts +41 -3
- package/dist/cache/index.mjs +33 -4
- 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/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.mjs +2 -2
- package/dist/context/index.mjs +2 -64
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-b6fgFgnd.mjs → core-EUNu0u07.mjs} +298 -12
- package/dist/{createActionRouter-h1cEEaEX.mjs → createActionRouter-B6GvAuAX.mjs} +4 -3
- package/dist/{createAggregationRouter-B1C0kvTL.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
- package/dist/{errorHandler-BTzXSwIg.mjs → errorHandler-Bq2OeEQL.mjs} +10 -7
- package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
- package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
- package/dist/events/index.d.mts +57 -314
- package/dist/events/index.mjs +168 -93
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +24 -2
- package/dist/factory/index.d.mts +21 -6
- package/dist/factory/index.mjs +58 -18
- package/dist/{fastify-augmentation-9_fuetB4.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
- package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
- package/dist/headers-BvlQSDMB.mjs +45 -0
- package/dist/health-CB_m4S32.mjs +377 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +7 -0
- package/dist/idempotency/index.mjs +32 -20
- package/dist/{index-ogIHvOjo.d.mts → index-BMnbzvtO.d.mts} +70 -4
- package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
- package/dist/{index-DbtgCOCj.d.mts → index-DPRH0Cme.d.mts} +46 -94
- package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
- package/dist/{index-D8MKYGDa.d.mts → index-f4PTrk3B.d.mts} +37 -5
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +6 -5
- package/dist/{init-DleK3p9f.mjs → init-tlRSt667.mjs} +206 -205
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/jobs/index.d.mts +2 -0
- package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
- package/dist/integrations/mcp/index.d.mts +68 -5
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +149 -22
- package/dist/integrations/webhooks.mjs +186 -42
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +41 -14
- package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
- package/dist/interface-n5hpKg1d.mjs +16 -0
- package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
- package/dist/{mcpPlugin-ByA2tf6D.mjs → mcpPlugin-Cb7olw0j.mjs} +316 -45
- package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
- package/dist/middleware/index.d.mts +17 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/migrations/index.d.mts +94 -44
- package/dist/migrations/index.mjs +196 -44
- package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
- package/dist/{openapi-BRpMZD7C.mjs → openapi-DrLtog_B.mjs} +4 -4
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +4 -3
- package/dist/{permissions-BJMuKMl7.mjs → permissions-BrtiBKGs.mjs} +305 -492
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{actionPermissions-ze70xXJi.mjs → pipeline-BN5G05rd.mjs} +4 -27
- package/dist/plugins/index.d.mts +7 -78
- package/dist/plugins/index.mjs +14 -97
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{index-BEu4wjbL.d.mts → preloadResources-0oe-JSSS.d.mts} +59 -50
- package/dist/{createApp-Ci7yG5gv.mjs → preloadResources-DfyngIN9.mjs} +240 -68
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +3 -3
- package/dist/presets/index.d.mts +2 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-gw0ciIUD.mjs → presets-DwFDVKP0.mjs} +2 -2
- package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
- package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
- package/dist/{registerRoute-xHc42fwB.mjs → rateLimit-DrED-C4v.mjs} +2 -109
- package/dist/realtime-Cuw_deA-.mjs +375 -0
- package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
- package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/renewingLease-3cuQH7h8.mjs +44 -0
- package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
- package/dist/schedules-CjOd2MG9.mjs +113 -0
- package/dist/{schemaConverter-Ds5reSGs.mjs → schemaConverter-DJvodiHj.mjs} +4 -1
- package/dist/{schemaIR-B-6SPe7g.mjs → schemaIR-Ca7ySNPx.mjs} +1 -1
- package/dist/schemas/index.d.mts +67 -67
- package/dist/schemas/index.mjs +2 -3
- package/dist/scim/index.mjs +1 -2
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +4 -4
- package/dist/scope-DfaPntpo.mjs +36 -0
- package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
- package/dist/{sse-CfUjwjDk.mjs → sse-BQ1w0dfl.mjs} +8 -31
- package/dist/sseStream-BQ7Qaynp.mjs +57 -0
- package/dist/testing/index.d.mts +4 -3
- package/dist/testing/index.mjs +2 -2
- package/dist/transactionContext-PNdoyLLm.mjs +64 -0
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
- package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
- package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
- package/dist/{types-Bc42CUyk.d.mts → types-QFGc3Cjv.d.mts} +37 -2
- package/dist/usage/index.d.mts +3 -52
- package/dist/usage/index.mjs +0 -0
- package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
- package/dist/utils/index.d.mts +90 -57
- package/dist/utils/index.mjs +7 -3
- package/dist/{utils-FhJ88TJE.mjs → utils-taUVD4p3.mjs} +47 -385
- package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
- package/dist/{versioning-DLwklPlG.d.mts → versioning-DXgY5BUm.d.mts} +52 -31
- package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
- package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
- package/package.json +526 -453
- package/skills/arc/references/api-reference.md +3 -1
- package/skills/arc/references/integrations.md +20 -11
- package/skills/arc/references/production.md +734 -696
- package/dist/QueryCache-Z1B38koh.d.mts +0 -40
- package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
- package/dist/health-ScyjIVo5.mjs +0 -202
- package/dist/requestId-5BhTVzBv.mjs +0 -113
- package/dist/scope-k15RXfcq.mjs +0 -64
- 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 };
|
package/dist/hooks/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Cn as
|
|
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)
|
|
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
|
|
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-
|
|
2
|
-
import { i as RequestScope, n as Mandate } from "./types-
|
|
3
|
-
import { n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-
|
|
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 {
|
|
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 };
|