@classytic/arc 2.20.0 → 2.22.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-D-MHZ27_.mjs → BaseController-C6FlRWxF.mjs} +2 -2
- package/dist/{EventTransport-IEn7Tn-Q.d.mts → EventTransport-QmY7A258.d.mts} +0 -1
- package/dist/{QueryCache-TRfXau3W.d.mts → QueryCache-Z1B38koh.d.mts} +0 -1
- package/dist/{actionPermissions-C9RMiIpc.mjs → actionPermissions-ze70xXJi.mjs} +1 -1
- package/dist/audit/index.d.mts +9 -5
- package/dist/auth/audit.d.mts +7 -4
- package/dist/auth/audit.mjs +5 -0
- package/dist/auth/index.d.mts +35 -14
- package/dist/auth/index.mjs +6 -3
- package/dist/auth/redis-session.d.mts +1 -2
- package/dist/cache/index.d.mts +2 -3
- package/dist/cli/commands/describe.d.mts +1 -2
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/context/index.d.mts +2 -2
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-CWhyGkiM.mjs → core-b6fgFgnd.mjs} +129 -17
- package/dist/{createActionRouter-HlQso0jO.mjs → createActionRouter-h1cEEaEX.mjs} +10 -6
- package/dist/{createAggregationRouter-5LzPzgw1.mjs → createAggregationRouter-B1C0kvTL.mjs} +3 -3
- package/dist/{createApp-D6uwzCKu.mjs → createApp-Ci7yG5gv.mjs} +390 -192
- package/dist/discovery/index.d.mts +0 -1
- package/dist/docs/index.d.mts +1 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{elevation-xGUBq6p_.d.mts → elevation-C9s5hFP_.d.mts} +0 -1
- package/dist/encryption/index.d.mts +2 -144
- package/dist/encryption/index.mjs +9 -5
- package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-BTzXSwIg.mjs} +24 -3
- package/dist/{errorHandler-BtUUDPEa.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -1
- package/dist/{errors-DOHkjQlD.d.mts → errors-DHcHVAgk.d.mts} +0 -1
- package/dist/{eventPlugin-DbBFd3x_.d.mts → eventPlugin-A6EbthXR.d.mts} +10 -6
- package/dist/events/index.d.mts +9 -6
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -2
- package/dist/factory/index.d.mts +30 -9
- package/dist/factory/index.mjs +35 -4
- package/dist/{fastify-augmentation-CCQJEDgh.d.mts → fastify-augmentation-9_fuetB4.d.mts} +2 -3
- 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 +4 -3
- package/dist/idempotency/index.mjs +2 -0
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{types-BHMKlMBU.d.mts → index-BEu4wjbL.d.mts} +241 -80
- package/dist/{index-DUa8WHz3.d.mts → index-D8MKYGDa.d.mts} +88 -7
- package/dist/{index-_--GyzcO.d.mts → index-DbtgCOCj.d.mts} +1324 -1228
- package/dist/{index-CdFkIMEd.d.mts → index-ogIHvOjo.d.mts} +36 -7
- package/dist/index.d.mts +8 -7
- package/dist/index.mjs +6 -6
- package/dist/{init-YH_nslQR.mjs → init-DleK3p9f.mjs} +272 -139
- package/dist/integrations/event-gateway.d.mts +2 -3
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/jobs.d.mts +0 -1
- package/dist/integrations/mcp/index.d.mts +6 -7
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -2
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +2 -2
- package/dist/integrations/webhooks.d.mts +8 -5
- package/dist/integrations/webhooks.mjs +9 -5
- package/dist/integrations/websocket-pushref-redis.d.mts +1 -2
- package/dist/integrations/websocket-redis.d.mts +1 -2
- package/dist/integrations/websocket.d.mts +2 -2
- package/dist/jsonBody-BlzAoGCe.mjs +43 -0
- package/dist/{loadResources-B8UmpITt.mjs → loadResources-DpU8pfjV.mjs} +4 -3
- package/dist/logger/index.d.mts +26 -1
- package/dist/logger/index.mjs +39 -1
- package/dist/{mcpPlugin-CuW6L_uD.mjs → mcpPlugin-ByA2tf6D.mjs} +8 -4
- package/dist/middleware/index.d.mts +1 -2
- package/dist/{module-JYH9V0I8.d.mts → module-B4u10iOJ.d.mts} +71 -1
- package/dist/{openapi-Dq6RbEGK.mjs → openapi-BRpMZD7C.mjs} +3 -3
- package/dist/permissions/index.d.mts +4 -3
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-CgbOLJZG.mjs → permissions-BJMuKMl7.mjs} +33 -1
- package/dist/pipeline/index.d.mts +1 -2
- package/dist/plugins/index.d.mts +96 -7
- package/dist/plugins/index.mjs +86 -268
- package/dist/plugins/response-cache.d.mts +5 -3
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +7 -4
- package/dist/presets/filesUpload.d.mts +4 -5
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +1 -2
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -2
- package/dist/presets/search.d.mts +2 -3
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-DWsYQdbV.mjs → presets-gw0ciIUD.mjs} +1 -1
- package/dist/{pushref-registry-DnCqL3aa.d.mts → pushref-registry-BbcnpNTm.d.mts} +4 -2
- package/dist/{queryCachePlugin-xzFyKAjT.d.mts → queryCachePlugin-CfYWq-Ak.d.mts} +1 -2
- package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
- package/dist/{redis-C3W0qbz8.d.mts → redis-UDtLaNj6.d.mts} +0 -1
- package/dist/{redis-stream-DsP1vqZB.d.mts → redis-stream-iVR2l7aS.d.mts} +1 -2
- package/dist/{registerRoute-DGSREwPw.mjs → registerRoute-xHc42fwB.mjs} +38 -18
- package/dist/registry/index.d.mts +2 -2
- package/dist/requestId-5BhTVzBv.mjs +113 -0
- package/dist/schemaConverter-Ds5reSGs.mjs +168 -0
- package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-B-6SPe7g.mjs} +2 -2
- package/dist/schemas/index.d.mts +0 -1
- package/dist/scim/index.d.mts +0 -1
- package/dist/scim/index.mjs +6 -7
- package/dist/scope/index.d.mts +2 -3
- package/dist/scope/index.mjs +2 -76
- package/dist/scope-k15RXfcq.mjs +64 -0
- package/dist/{sessionManager-BXG-HGwG.d.mts → sessionManager-BHOM3N_0.d.mts} +10 -6
- package/dist/{storage-Co_PQpeQ.d.mts → storage-DLUZ04kK.d.mts} +8 -4
- package/dist/testing/index.d.mts +83 -4
- package/dist/testing/index.mjs +74 -4
- package/dist/testing/storageContract.d.mts +1 -2
- package/dist/{tracing-D8jlGXP7.d.mts → tracing-CsGKQJGg.d.mts} +0 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-C9b2HR5W.d.mts → types-Bc42CUyk.d.mts} +1 -2
- package/dist/types-Cc91nXuA.d.mts +132 -0
- package/dist/{fields-C7ahdGYf.d.mts → types-Csxga7d3.d.mts} +2 -123
- package/dist/{types-LNebCoJx.d.mts → types-DH0GupI7.d.mts} +8 -4
- package/dist/usage/index.d.mts +142 -0
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +220 -144
- package/dist/utils/index.mjs +3 -3
- package/dist/{utils-ChjYQpjn.mjs → utils-FhJ88TJE.mjs} +129 -39
- package/dist/{versioning-D_eBCfNr.d.mts → versioning-DLwklPlG.d.mts} +1 -7
- package/dist/{websocket-MlYlrYq0.d.mts → websocket-uuWqM6UY.d.mts} +5 -4
- package/llms.txt +39 -0
- package/package.json +453 -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 +44 -2
- 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 +243 -0
- package/skills/arc-module-publishing/references/authoring-modules.md +327 -0
- package/skills/arc-module-publishing/references/package-manifest.md +157 -0
- package/skills/arc-module-publishing/references/publishing-workflow.md +182 -0
- package/dist/preloadResources-CHw3_oA_.mjs +0 -45
- package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
package/dist/docs/index.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { p as RegistryEntry } from "../index-
|
|
1
|
+
import { p as RegistryEntry } from "../index-DbtgCOCj.mjs";
|
|
2
2
|
import { t as ExternalOpenApiPaths } from "../externalPaths-BE6wFpYv.mjs";
|
|
3
3
|
import { FastifyPluginAsync } from "fastify";
|
|
4
|
-
|
|
5
4
|
//#region src/docs/openapi/types.d.ts
|
|
6
5
|
/**
|
|
7
6
|
* OpenAPI 3.0 type primitives used by arc's spec emitter.
|
package/dist/docs/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as getUserRoles } from "../types-XTApa8b6.mjs";
|
|
2
|
-
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-
|
|
2
|
+
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-BRpMZD7C.mjs";
|
|
3
3
|
import fp from "fastify-plugin";
|
|
4
4
|
//#region src/docs/scalar.ts
|
|
5
5
|
const scalarPlugin = async (fastify, opts = {}) => {
|
|
@@ -1,135 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
//#region src/encryption/types.d.ts
|
|
4
|
-
/** JWE key-management algorithm — the protected-header `alg`. */
|
|
5
|
-
type JweAlg = "RSA-OAEP-256" | "RSA-OAEP-384" | "RSA-OAEP-512" | "ECDH-ES" | "ECDH-ES+A256KW" | "A256GCMKW" | "dir";
|
|
6
|
-
/** JWE content-encryption algorithm — the protected-header `enc`. AEAD only. */
|
|
7
|
-
type JweEnc = "A128GCM" | "A192GCM" | "A256GCM";
|
|
8
|
-
/** Encryption strategy for a route's response. */
|
|
9
|
-
type EncryptionMode = "jwe" | "fields";
|
|
10
|
-
/**
|
|
11
|
-
* Opaque key material. For `jwe` mode this is a `jose` `KeyLike` /
|
|
12
|
-
* `CryptoKey` / `Uint8Array`; for `fields` mode a 32-byte secret
|
|
13
|
-
* (`Buffer` / `Uint8Array`). Kept `unknown` at the contract layer so the
|
|
14
|
-
* `KeyProvider` interface carries no hard `jose` type dependency — hosts
|
|
15
|
-
* without `jose` installed can still implement field-mode providers.
|
|
16
|
-
*/
|
|
17
|
-
type KeyMaterial = unknown;
|
|
18
|
-
/** A key paired with its identifier — what encrypt operations consume. */
|
|
19
|
-
interface ActiveKey {
|
|
20
|
-
/** Key id, stamped into the JWE `kid` header / field envelope for rotation. */
|
|
21
|
-
readonly kid: string;
|
|
22
|
-
/** The key material (public key for JWE, 32-byte secret for fields). */
|
|
23
|
-
readonly key: KeyMaterial;
|
|
24
|
-
/** Optional per-key `alg` override (else the plugin default applies). */
|
|
25
|
-
readonly alg?: JweAlg;
|
|
26
|
-
}
|
|
27
|
-
/** Context handed to the key provider so resolution can be request-scoped. */
|
|
28
|
-
interface KeyContext {
|
|
29
|
-
readonly request: FastifyRequest;
|
|
30
|
-
}
|
|
31
|
-
type MaybePromise<T> = T | Promise<T>;
|
|
32
|
-
/**
|
|
33
|
-
* Pluggable key resolution. Arc owns the request-time plumbing — `kid` →
|
|
34
|
-
* key, algorithm allowlist enforcement; the host owns the trust store —
|
|
35
|
-
* which keys are valid and where they live (file / env / AWS KMS / GCP KMS
|
|
36
|
-
* / Vault). Encryption and decryption are deliberately separate operations
|
|
37
|
-
* because they often use DIFFERENT keys (JWE: a public key encrypts the
|
|
38
|
-
* outbound response, a private key by `kid` decrypts the inbound request —
|
|
39
|
-
* exactly Visa's two-key-pair model).
|
|
40
|
-
*
|
|
41
|
-
* Rotation mirrors `@fastify/secure-session`: the active key encrypts; any
|
|
42
|
-
* key resolvable by `kid` can decrypt, so you can roll keys without dropping
|
|
43
|
-
* in-flight traffic.
|
|
44
|
-
*/
|
|
45
|
-
interface KeyProvider {
|
|
46
|
-
/** Active key for encrypting OUTBOUND responses. */
|
|
47
|
-
encryptionKey(ctx: KeyContext): MaybePromise<ActiveKey>;
|
|
48
|
-
/** Resolve a DECRYPTION key by the inbound message's `kid`. */
|
|
49
|
-
decryptionKey(kid: string, ctx: KeyContext): MaybePromise<KeyMaterial | undefined>;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Per-resource encryption directive — the typed slice plugins register on
|
|
53
|
-
* `ResourceExtensions`. Declared on a resource via
|
|
54
|
-
* `defineResource({ extensions: { encryption: { … } } })`; arc threads it
|
|
55
|
-
* to request time on `request.routeOptions.config.arcExtensions.encryption`.
|
|
56
|
-
*/
|
|
57
|
-
interface EncryptionDirective {
|
|
58
|
-
/** Strategy. Defaults to the plugin-level `mode` (default `'jwe'`). */
|
|
59
|
-
readonly mode?: EncryptionMode;
|
|
60
|
-
/**
|
|
61
|
-
* For `mode: 'fields'` — dot-paths of fields to encrypt, resolved against
|
|
62
|
-
* the serialized response body (e.g. `'cardNumber'`, `'account.iban'`).
|
|
63
|
-
* Ignored for `mode: 'jwe'` (the whole body is encrypted).
|
|
64
|
-
*/
|
|
65
|
-
readonly fields?: readonly string[];
|
|
66
|
-
/**
|
|
67
|
-
* Explicit opt-out — set `false` to disable encryption for this resource
|
|
68
|
-
* even when a plugin-level default (`routes`) would otherwise apply.
|
|
69
|
-
*/
|
|
70
|
-
readonly enabled?: boolean;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Plugin-level options. `keyProvider` is the only required field; everything
|
|
74
|
-
* else has a secure default.
|
|
75
|
-
*/
|
|
76
|
-
interface EncryptionOptions {
|
|
77
|
-
/**
|
|
78
|
-
* Master on/off. Default `true`. Set `false` to register the plugin as a
|
|
79
|
-
* typed no-op (config-gated environments) — same pattern as
|
|
80
|
-
* `idempotencyPlugin`.
|
|
81
|
-
*/
|
|
82
|
-
readonly enabled?: boolean;
|
|
83
|
-
/** Key provider — resolves `kid` → key for encrypt/decrypt. Required. */
|
|
84
|
-
readonly keyProvider: KeyProvider;
|
|
85
|
-
/** Default mode when a route opts in without naming one. Default `'jwe'`. */
|
|
86
|
-
readonly mode?: EncryptionMode;
|
|
87
|
-
/** JWE key-management algorithm. Default `'RSA-OAEP-256'`. */
|
|
88
|
-
readonly alg?: JweAlg;
|
|
89
|
-
/** JWE content-encryption algorithm. Default `'A256GCM'`. */
|
|
90
|
-
readonly enc?: JweEnc;
|
|
91
|
-
/**
|
|
92
|
-
* Inbound-decryption algorithm allowlists — block algorithm-substitution
|
|
93
|
-
* downgrade attacks. Default `[alg]` / `[enc]`. Set explicitly only when a
|
|
94
|
-
* client legitimately uses more than one algorithm.
|
|
95
|
-
*/
|
|
96
|
-
readonly allowedAlgs?: readonly JweAlg[];
|
|
97
|
-
readonly allowedEncs?: readonly JweEnc[];
|
|
98
|
-
/**
|
|
99
|
-
* Fallback opt-in for routes that don't declare
|
|
100
|
-
* `extensions.encryption`. Return a directive (or `true` for the default
|
|
101
|
-
* mode) to encrypt, `false`/`undefined` to skip. The per-resource
|
|
102
|
-
* `extensions.encryption` ALWAYS wins when present. Use this to scope ALE
|
|
103
|
-
* to sensitive prefixes without touching every resource.
|
|
104
|
-
*
|
|
105
|
-
* @example confine ALE to `/payments` and `/accounts`
|
|
106
|
-
* ```ts
|
|
107
|
-
* routes: (req) => /^\/(payments|accounts)\b/.test(req.url)
|
|
108
|
-
* ```
|
|
109
|
-
*/
|
|
110
|
-
readonly routes?: (request: FastifyRequest) => EncryptionDirective | boolean | undefined;
|
|
111
|
-
/** Decrypt inbound `application/jose` request bodies. Default `true`. */
|
|
112
|
-
readonly decryptRequests?: boolean;
|
|
113
|
-
/** Media type that signals an encrypted REQUEST body. Default `'application/jose'`. */
|
|
114
|
-
readonly contentType?: string;
|
|
115
|
-
/** Content-Type set on full-body JWE RESPONSES. Default `'application/jose'`. */
|
|
116
|
-
readonly responseContentType?: string;
|
|
117
|
-
/**
|
|
118
|
-
* Response header advertising that the body is encrypted, so generic
|
|
119
|
-
* client SDKs know to decrypt. Default `'x-encrypted'`; set `false` to omit.
|
|
120
|
-
*/
|
|
121
|
-
readonly flagHeader?: string | false;
|
|
122
|
-
}
|
|
123
|
-
/** Request decorations added by the plugin. */
|
|
124
|
-
declare module "fastify" {
|
|
125
|
-
interface FastifyRequest {
|
|
126
|
-
/** `true` when the inbound body was decrypted from a JWE by this plugin. */
|
|
127
|
-
requestDecrypted?: boolean;
|
|
128
|
-
/** `true` when the outbound body was encrypted by this plugin. */
|
|
129
|
-
responseEncrypted?: boolean;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
//#endregion
|
|
1
|
+
import { a as JweAlg, c as KeyMaterial, i as EncryptionOptions, l as KeyProvider, n as EncryptionDirective, o as JweEnc, r as EncryptionMode, s as KeyContext, t as ActiveKey } from "../types-Cc91nXuA.mjs";
|
|
2
|
+
import { FastifyPluginAsync } from "fastify";
|
|
133
3
|
//#region src/encryption/encryptionPlugin.d.ts
|
|
134
4
|
declare const encryptionPlugin: FastifyPluginAsync<EncryptionOptions>;
|
|
135
5
|
declare const _default: FastifyPluginAsync<EncryptionOptions>;
|
|
@@ -207,16 +77,4 @@ interface StaticKeyProviderOptions {
|
|
|
207
77
|
*/
|
|
208
78
|
declare function createStaticKeyProvider(options: StaticKeyProviderOptions): KeyProvider;
|
|
209
79
|
//#endregion
|
|
210
|
-
//#region src/encryption/index.d.ts
|
|
211
|
-
declare module "../types/resource.js" {
|
|
212
|
-
interface ResourceExtensions {
|
|
213
|
-
/**
|
|
214
|
-
* Encrypt this resource's responses — full-body JWE or specific fields.
|
|
215
|
-
* Threaded to request time on
|
|
216
|
-
* `request.routeOptions.config.arcExtensions.encryption`.
|
|
217
|
-
*/
|
|
218
|
-
encryption?: EncryptionDirective;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
//#endregion
|
|
222
80
|
export { type ActiveKey, type EncryptionDirective, type EncryptionMode, type EncryptionOptions, type JweAlg, type JweEnc, type KeyContext, type KeyMaterial, type KeyProvider, type ParsedFieldEnvelope, type StaticKeyProviderOptions, createStaticKeyProvider, decryptField, _default as default, encryptField, encryptionPlugin, parseFieldEnvelope };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { t as ArcError } from "../errors-DkO8Nk6-.mjs";
|
|
2
|
+
import { t as parseJsonBody } from "../jsonBody-BlzAoGCe.mjs";
|
|
1
3
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
2
4
|
import fp from "fastify-plugin";
|
|
3
5
|
//#region src/encryption/fieldCipher.ts
|
|
@@ -182,17 +184,19 @@ const encryptionPlugin = async (fastify, opts) => {
|
|
|
182
184
|
}
|
|
183
185
|
if (decryptRequests) fastify.addContentTypeParser(contentType, { parseAs: "string" }, async (request, body) => {
|
|
184
186
|
const provider = keyProvider;
|
|
185
|
-
|
|
186
|
-
request.requestDecrypted = true;
|
|
187
|
-
if (plaintext.length === 0) return void 0;
|
|
187
|
+
let plaintext;
|
|
188
188
|
try {
|
|
189
|
-
|
|
189
|
+
plaintext = await jweDecrypt(body, (kid) => provider.decryptionKey(kid, { request }), allowedAlgs, allowedEncs);
|
|
190
190
|
} catch (cause) {
|
|
191
|
-
throw
|
|
191
|
+
throw new ArcError("Invalid encrypted request body", {
|
|
192
|
+
code: "arc.bad_request",
|
|
192
193
|
statusCode: 400,
|
|
193
194
|
cause
|
|
194
195
|
});
|
|
195
196
|
}
|
|
197
|
+
request.requestDecrypted = true;
|
|
198
|
+
if (plaintext.length === 0) return void 0;
|
|
199
|
+
return parseJsonBody(plaintext, "Decrypted request body is not valid JSON");
|
|
196
200
|
});
|
|
197
201
|
fastify.addHook("preSerialization", async (request, reply, payload) => {
|
|
198
202
|
const directive = resolveDirective(request, defaultMode, fallback);
|
|
@@ -91,7 +91,7 @@ function mongooseValidationDetails(errors) {
|
|
|
91
91
|
}
|
|
92
92
|
async function errorHandlerPluginFn(fastify, options = {}) {
|
|
93
93
|
const isProduction = process.env.NODE_ENV === "production";
|
|
94
|
-
const { includeStack = !isProduction, onError, errorMap = {}, errorMappers = [], isDuplicateKeyError = defaultIsDuplicateKeyError } = options;
|
|
94
|
+
const { includeStack = !isProduction, exposeInternalMessages = !isProduction, onError, errorMap = {}, errorMappers = [], isDuplicateKeyError = defaultIsDuplicateKeyError } = options;
|
|
95
95
|
fastify.setErrorHandler(async (error, request, reply) => {
|
|
96
96
|
if (onError) try {
|
|
97
97
|
await onError(error, request);
|
|
@@ -112,6 +112,7 @@ async function errorHandlerPluginFn(fastify, options = {}) {
|
|
|
112
112
|
...Object.keys(meta).length > 0 ? { meta } : {}
|
|
113
113
|
};
|
|
114
114
|
const status = wire.status ?? 500;
|
|
115
|
+
if (status >= 500 && wire.code === "arc.internal_error" && !exposeInternalMessages) wire.message = "Internal Server Error";
|
|
115
116
|
if (status >= 500) request.log.error({
|
|
116
117
|
err: error,
|
|
117
118
|
status
|
|
@@ -120,11 +121,31 @@ async function errorHandlerPluginFn(fastify, options = {}) {
|
|
|
120
121
|
err: error,
|
|
121
122
|
status
|
|
122
123
|
}, "Client error");
|
|
123
|
-
if (isArcError(error) && error.cause)
|
|
124
|
+
if (isArcError(error) && error.cause) {
|
|
125
|
+
const causeLevel = status >= 500 ? "error" : "warn";
|
|
126
|
+
request.log[causeLevel]({ cause: error.cause }, "Error cause chain");
|
|
127
|
+
}
|
|
124
128
|
return reply.status(status).send(wire);
|
|
125
129
|
});
|
|
126
130
|
}
|
|
127
131
|
/**
|
|
132
|
+
* Is `code` a caller-defined DOMAIN error code that is safe to surface on the
|
|
133
|
+
* wire, as opposed to a framework/runtime code that must never leak?
|
|
134
|
+
*
|
|
135
|
+
* Rejects Fastify internals (`FST_*`) and Node system errnos (`ECONNREFUSED`,
|
|
136
|
+
* `ENOENT`, …). Domain codes in the wild use separators (`PURCHASE_NOT_FOUND`,
|
|
137
|
+
* `access.forbidden`) so they pass; a bare all-caps single word that collides
|
|
138
|
+
* with the Node-errno shape (e.g. `EXPIRED`) is conservatively treated as a
|
|
139
|
+
* runtime code — use a separatored code (`TOKEN_EXPIRED`) to surface it, or
|
|
140
|
+
* throw via `createDomainError` which bypasses this heuristic entirely.
|
|
141
|
+
*/
|
|
142
|
+
function isDomainErrorCode(code) {
|
|
143
|
+
if (typeof code !== "string" || code.length === 0) return false;
|
|
144
|
+
if (code.startsWith("FST_")) return false;
|
|
145
|
+
if (/^E[A-Z]{2,}$/.test(code)) return false;
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
128
149
|
* Single-pass error → `ErrorContract` classifier. Dispatch order is fixed:
|
|
129
150
|
*
|
|
130
151
|
* 1. Class-based mappers (`instanceof`) — user-registered domain errors
|
|
@@ -167,7 +188,7 @@ function classify(error, ctx) {
|
|
|
167
188
|
};
|
|
168
189
|
}
|
|
169
190
|
if (typeof fastifyErr.statusCode === "number") return {
|
|
170
|
-
code: statusToArcCode(fastifyErr.statusCode),
|
|
191
|
+
code: (isDomainErrorCode(fastifyErr.code) ? fastifyErr.code : void 0) ?? statusToArcCode(fastifyErr.statusCode),
|
|
171
192
|
message: error.message || "Error",
|
|
172
193
|
status: fastifyErr.statusCode
|
|
173
194
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ErrorDetail } from "@classytic/repo-core/errors";
|
|
2
2
|
import { FastifyInstance, FastifyRequest } from "fastify";
|
|
3
|
-
|
|
4
3
|
//#region src/plugins/errorHandler.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* Class-based error mapper — `instanceof` check converts a thrown class
|
|
@@ -19,6 +18,19 @@ interface ErrorMapper<T extends Error = Error> {
|
|
|
19
18
|
interface ErrorHandlerOptions {
|
|
20
19
|
/** Include `meta.stack` on the wire (defaults to `NODE_ENV !== 'production'`). */
|
|
21
20
|
includeStack?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Ship the raw `error.message` of UNCLASSIFIED 500s on the wire
|
|
23
|
+
* (defaults to `NODE_ENV !== 'production'`).
|
|
24
|
+
*
|
|
25
|
+
* Only the `arc.internal_error` fallback is affected — an unclassified
|
|
26
|
+
* throw carries whatever some driver/library put in the message (connection
|
|
27
|
+
* strings, file paths, SQL fragments), which must not reach clients in
|
|
28
|
+
* production. Deliberate 5xx contracts are untouched: `ArcError` /
|
|
29
|
+
* domain-coded throws (`payment.gateway.timeout`, …) keep their message
|
|
30
|
+
* because the thrower chose it for the client. The raw message is always
|
|
31
|
+
* logged via `request.log.error({ err })` regardless of this flag.
|
|
32
|
+
*/
|
|
33
|
+
exposeInternalMessages?: boolean;
|
|
22
34
|
/** Custom callback fired for every error — log to Sentry / Datadog / etc. */
|
|
23
35
|
onError?: (error: Error, request: FastifyRequest) => void | Promise<void>;
|
|
24
36
|
/** Map by `error.name` string. Lower priority than `errorMappers`. */
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "./EventTransport-
|
|
1
|
+
import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "./EventTransport-QmY7A258.mjs";
|
|
2
2
|
import { FastifyPluginAsync } from "fastify";
|
|
3
|
-
|
|
4
3
|
//#region src/events/defineEvent.d.ts
|
|
5
4
|
interface EventSchema {
|
|
6
5
|
type: "object";
|
|
@@ -249,7 +248,8 @@ interface EventPluginOptions {
|
|
|
249
248
|
* failed events are published to the `$deadLetter` event type by default.
|
|
250
249
|
*/
|
|
251
250
|
deadLetterQueue?: {
|
|
252
|
-
/** Custom store function. If omitted, publishes to '$deadLetter' event type. */
|
|
251
|
+
/** Custom store function. If omitted, publishes to '$deadLetter' event type. */
|
|
252
|
+
store?: (event: DomainEvent, errors: Error[]) => void | Promise<void>;
|
|
253
253
|
};
|
|
254
254
|
/** Callback after successful publish (for metrics/tracking) */
|
|
255
255
|
onPublish?: (event: DomainEvent) => void;
|
|
@@ -302,9 +302,13 @@ interface EventPluginOptions {
|
|
|
302
302
|
declare module "fastify" {
|
|
303
303
|
interface FastifyInstance {
|
|
304
304
|
events: {
|
|
305
|
-
/** Publish an event */
|
|
306
|
-
|
|
307
|
-
|
|
305
|
+
/** Publish an event */
|
|
306
|
+
publish: <T>(type: string, payload: T, meta?: Partial<DomainEvent["meta"]>) => Promise<void>;
|
|
307
|
+
/** Subscribe to events */
|
|
308
|
+
subscribe: (pattern: string, handler: EventHandler) => Promise<() => void>;
|
|
309
|
+
/** Get transport name */
|
|
310
|
+
transportName: string;
|
|
311
|
+
/** Event registry for introspection (undefined when no registry configured) */
|
|
308
312
|
registry?: EventRegistry;
|
|
309
313
|
};
|
|
310
314
|
}
|
package/dist/events/index.d.mts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { a as EventTransport, i as EventLogger, n as DomainEvent, o as MemoryEventTransport, r as EventHandler, s as MemoryEventTransportOptions, t as DeadLetteredEvent } from "../EventTransport-
|
|
2
|
-
import { a as withRetry, c as EventDefinitionOutput, d as EventSchema, f as ValidationResult, i as createDeadLetterPublisher, l as EventRegistry, m as defineEvent, n as eventPlugin, o as CustomValidator, p as createEventRegistry, r as RetryOptions, s as EventDefinitionInput, t as EventPluginOptions, u as EventRegistryOptions } from "../eventPlugin-
|
|
1
|
+
import { a as EventTransport, i as EventLogger, n as DomainEvent, o as MemoryEventTransport, r as EventHandler, s as MemoryEventTransportOptions, t as DeadLetteredEvent } from "../EventTransport-QmY7A258.mjs";
|
|
2
|
+
import { a as withRetry, c as EventDefinitionOutput, d as EventSchema, f as ValidationResult, i as createDeadLetterPublisher, l as EventRegistry, m as defineEvent, n as eventPlugin, o as CustomValidator, p as createEventRegistry, r as RetryOptions, s as EventDefinitionInput, t as EventPluginOptions, u as EventRegistryOptions } from "../eventPlugin-A6EbthXR.mjs";
|
|
3
3
|
import { RedisEventTransportOptions, RedisLike } from "./transports/redis.mjs";
|
|
4
|
-
import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-
|
|
4
|
+
import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-iVR2l7aS.mjs";
|
|
5
5
|
import { RepositoryLike } from "@classytic/repo-core/adapter";
|
|
6
|
-
|
|
7
6
|
//#region src/events/eventTypes.d.ts
|
|
8
7
|
/**
|
|
9
8
|
* Event Type Constants and Helpers
|
|
@@ -38,14 +37,18 @@ type CrudEventSuffix = (typeof CRUD_EVENT_SUFFIXES)[number];
|
|
|
38
37
|
declare function crudEventType(resource: string, suffix: CrudEventSuffix): string;
|
|
39
38
|
/** Arc framework lifecycle events — emitted automatically by the framework */
|
|
40
39
|
declare const ARC_LIFECYCLE_EVENTS: Readonly<{
|
|
41
|
-
/** Emitted when a resource plugin is registered */
|
|
40
|
+
/** Emitted when a resource plugin is registered */
|
|
41
|
+
readonly RESOURCE_REGISTERED: "arc.resource.registered";
|
|
42
|
+
/** Emitted when Arc is fully ready (all resources registered, onReady fired) */
|
|
42
43
|
readonly READY: "arc.ready";
|
|
43
44
|
}>;
|
|
44
45
|
/** Type for Arc lifecycle event names */
|
|
45
46
|
type ArcLifecycleEvent = (typeof ARC_LIFECYCLE_EVENTS)[keyof typeof ARC_LIFECYCLE_EVENTS];
|
|
46
47
|
/** Cache-specific event types for observability and external triggers */
|
|
47
48
|
declare const CACHE_EVENTS: Readonly<{
|
|
48
|
-
/** Emitted when a resource's cache version is bumped */
|
|
49
|
+
/** Emitted when a resource's cache version is bumped */
|
|
50
|
+
readonly VERSION_BUMPED: "arc.cache.version.bumped";
|
|
51
|
+
/** Emitted when a tag version is bumped */
|
|
49
52
|
readonly TAG_VERSION_BUMPED: "arc.cache.tag.bumped";
|
|
50
53
|
}>;
|
|
51
54
|
/** Type for cache event names */
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-
|
|
1
|
+
import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-iVR2l7aS.mjs";
|
|
2
2
|
export { type RedisStreamLike, RedisStreamTransport, type RedisStreamTransportOptions };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "../../EventTransport-
|
|
2
|
-
|
|
1
|
+
import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "../../EventTransport-QmY7A258.mjs";
|
|
3
2
|
//#region src/events/transports/redis.d.ts
|
|
4
3
|
interface RedisLike {
|
|
5
4
|
publish(channel: string, message: string): Promise<number>;
|
package/dist/factory/index.d.mts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
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
|
|
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-9_fuetB4.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-BEu4wjbL.mjs";
|
|
4
|
+
import { g as HealthCheck } from "../versioning-DLwklPlG.mjs";
|
|
3
5
|
import { FastifyInstance, FastifyServerOptions } from "fastify";
|
|
4
|
-
|
|
5
6
|
//#region src/factory/createApp.d.ts
|
|
6
7
|
/**
|
|
7
8
|
* Create a production-ready Fastify application with Arc framework.
|
|
8
9
|
*
|
|
9
10
|
* Boot order:
|
|
10
11
|
* ```
|
|
11
|
-
* 0. Logger, validation, preset merge
|
|
12
|
+
* 0. Logger, validation → beforeBoot() (DB connect etc.), preset merge
|
|
12
13
|
* 1. Create Fastify instance
|
|
13
14
|
* 2. Security plugins (Helmet, CORS, Rate Limit) — opt-out
|
|
14
15
|
* 3. Utility plugins (Under Pressure, Sensible, Multipart, Raw Body)
|
|
@@ -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
|
/**
|
|
@@ -91,9 +115,6 @@ declare const testingPreset: Partial<CreateAppOptions>;
|
|
|
91
115
|
* when using `toFetchHandler()` (routes through Fastify's `.inject()`).
|
|
92
116
|
*/
|
|
93
117
|
declare const edgePreset: Partial<CreateAppOptions>;
|
|
94
|
-
|
|
95
|
-
* Get preset by name
|
|
96
|
-
*/
|
|
97
|
-
declare function getPreset(name: "production" | "development" | "testing" | "edge"): Partial<CreateAppOptions>;
|
|
118
|
+
declare function getPreset(name: "production" | "development" | "testing" | "edge" | "worker"): Partial<CreateAppOptions>;
|
|
98
119
|
//#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 };
|
|
120
|
+
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,6 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { t as loadResources } from "../loadResources-
|
|
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-Ci7yG5gv.mjs";
|
|
2
|
+
import { t as healthPlugin } from "../health-ScyjIVo5.mjs";
|
|
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.
|
|
@@ -47,4 +78,4 @@ function toFetchHandler(app, options = {}) {
|
|
|
47
78
|
};
|
|
48
79
|
}
|
|
49
80
|
//#endregion
|
|
50
|
-
export { ArcFactory, createApp, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
|
|
81
|
+
export { ArcFactory, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { bn as HookSystem, z as ResourceRegistry } from "./index-
|
|
1
|
+
import { bn as HookSystem, z as ResourceRegistry } from "./index-DbtgCOCj.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
|
-
|
|
6
5
|
//#region src/core/arcCorePlugin.d.ts
|
|
7
6
|
interface ArcCorePluginOptions {
|
|
8
7
|
/** Enable event emission for CRUD operations (requires eventPlugin) */
|
|
@@ -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 };
|