@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
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-DNre0wRg.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { d as createDomainError, f as createError, i as NotFoundError, r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { n as getUserRoles, t as getUserId } from "./userHelpers-Cg_y3fBz.mjs";
|
|
5
|
+
import { b as isElevated, c as getOrgId, n as PUBLIC_SCOPE, x as isMember } from "./types-CSyCxl2V.mjs";
|
|
6
|
+
import { t as simpleEqualityMatcher } from "./simpleEqualityMatcher-Cg05HPsn.mjs";
|
|
7
|
+
import { a as applyFieldWritePermissions, s as resolveEffectiveRoles } from "./applyPermissionResult-Tw4iXtlf.mjs";
|
|
8
|
+
import { g as ArcQueryParser, n as scheduleBackground } from "./utils-taUVD4p3.mjs";
|
|
8
9
|
import { t as buildQueryKey } from "./keys-Cd316u_y.mjs";
|
|
9
10
|
//#region src/core/AccessControl.ts
|
|
10
|
-
const log = arcLog("access-control");
|
|
11
|
+
const log$1 = arcLog("access-control");
|
|
11
12
|
var AccessControl = class {
|
|
12
13
|
tenantField;
|
|
13
14
|
idField;
|
|
@@ -87,7 +88,7 @@ var AccessControl = class {
|
|
|
87
88
|
_warnNoMatcher(policyFilters) {
|
|
88
89
|
if (this._warnedNoMatcher) return;
|
|
89
90
|
this._warnedNoMatcher = true;
|
|
90
|
-
log.warn("`_policyFilters` contains operator-shaped entries (e.g. `$in`, `$ne`, `$regex`) but `DataAdapter.matchesFilter` is not set. Arc's flat-equality fallback cannot evaluate operators and will reject these items on non-compound fetches (`validateItemAccess`, `getBySlug`, cache revalidation). Wire up `matchesFilter` on your adapter — use `matchFilter` from `@classytic/repo-core/filter` for IR-based adapters, or your DB's native predicate engine.", { policyFilterKeys: Object.keys(policyFilters) });
|
|
91
|
+
log$1.warn("`_policyFilters` contains operator-shaped entries (e.g. `$in`, `$ne`, `$regex`) but `DataAdapter.matchesFilter` is not set. Arc's flat-equality fallback cannot evaluate operators and will reject these items on non-compound fetches (`validateItemAccess`, `getBySlug`, cache revalidation). Wire up `matchesFilter` on your adapter — use `matchFilter` from `@classytic/repo-core/filter` for IR-based adapters, or your DB's native predicate engine.", { policyFilterKeys: Object.keys(policyFilters) });
|
|
91
92
|
}
|
|
92
93
|
/**
|
|
93
94
|
* Check org/tenant scope for a document — uses configurable tenantField.
|
|
@@ -617,6 +618,20 @@ const defaultParser = new ArcQueryParser();
|
|
|
617
618
|
function getDefaultQueryParser() {
|
|
618
619
|
return defaultParser;
|
|
619
620
|
}
|
|
621
|
+
const log = arcLog("query");
|
|
622
|
+
/**
|
|
623
|
+
* Deny-by-default (2.24 flip): `populate=` / `lookup=` with NO allowlist
|
|
624
|
+
* configured is rejected — client-driven joins/population are an easy
|
|
625
|
+
* accidental N+1 / expensive-join surface, and "absent means all" made
|
|
626
|
+
* every resource opt into it silently. A one-shot dev log explains the
|
|
627
|
+
* drop the first time a request hits it.
|
|
628
|
+
*/
|
|
629
|
+
let notedDeniedJoins = false;
|
|
630
|
+
function noteDeniedJoins(kind) {
|
|
631
|
+
if (notedDeniedJoins || process.env.NODE_ENV === "production") return;
|
|
632
|
+
notedDeniedJoins = true;
|
|
633
|
+
log.warn(`client requested ${kind} but no allowlist is configured — DENIED (2.24 default flip; it was allow-all). Set schemaOptions.query.allowedPopulate / allowedLookups to enable client-driven joins. (dev-only note, shown once)`);
|
|
634
|
+
}
|
|
620
635
|
var QueryResolver = class {
|
|
621
636
|
queryParser;
|
|
622
637
|
maxLimit;
|
|
@@ -708,7 +723,10 @@ var QueryResolver = class {
|
|
|
708
723
|
const allowedPopulate = schemaOptions.query?.allowedPopulate;
|
|
709
724
|
const requested = typeof populate === "string" ? populate.split(",").map((p) => p.trim()) : Array.isArray(populate) ? populate.map(String) : [];
|
|
710
725
|
if (requested.length === 0) return void 0;
|
|
711
|
-
if (!allowedPopulate)
|
|
726
|
+
if (!allowedPopulate) {
|
|
727
|
+
noteDeniedJoins("populate");
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
712
730
|
const sanitized = requested.filter((p) => allowedPopulate.includes(p));
|
|
713
731
|
return sanitized.length > 0 ? sanitized : void 0;
|
|
714
732
|
}
|
|
@@ -716,25 +734,32 @@ var QueryResolver = class {
|
|
|
716
734
|
sanitizePopulateOptions(options, schemaOptions) {
|
|
717
735
|
if (!options || options.length === 0) return void 0;
|
|
718
736
|
const allowedPopulate = schemaOptions.query?.allowedPopulate;
|
|
719
|
-
if (!allowedPopulate)
|
|
737
|
+
if (!allowedPopulate) {
|
|
738
|
+
noteDeniedJoins("populate");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
720
741
|
const sanitized = options.filter((opt) => allowedPopulate.includes(opt.path));
|
|
721
742
|
return sanitized.length > 0 ? sanitized : void 0;
|
|
722
743
|
}
|
|
723
744
|
/**
|
|
724
|
-
* Sanitize lookup/join options.
|
|
725
|
-
*
|
|
726
|
-
*
|
|
745
|
+
* Sanitize lookup/join options. Deny-by-default: `allowedLookups` must be
|
|
746
|
+
* configured for any client-driven lookup to pass; listed collections are
|
|
747
|
+
* then structurally validated to prevent injection.
|
|
727
748
|
*/
|
|
728
749
|
sanitizeLookups(lookups, schemaOptions) {
|
|
729
750
|
if (!lookups || lookups.length === 0) return void 0;
|
|
730
751
|
const allowedLookups = schemaOptions.query?.allowedLookups;
|
|
752
|
+
if (!allowedLookups) {
|
|
753
|
+
noteDeniedJoins("lookups");
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
731
756
|
const validFieldName = /^[a-zA-Z_][a-zA-Z0-9_.]*$/;
|
|
732
757
|
const sanitized = lookups.filter((lookup) => {
|
|
733
758
|
if (!lookup.from || !lookup.localField || !lookup.foreignField) return false;
|
|
734
759
|
if (!validFieldName.test(lookup.from)) return false;
|
|
735
760
|
if (!validFieldName.test(lookup.localField)) return false;
|
|
736
761
|
if (!validFieldName.test(lookup.foreignField)) return false;
|
|
737
|
-
if (
|
|
762
|
+
if (!allowedLookups.includes(lookup.from)) return false;
|
|
738
763
|
return true;
|
|
739
764
|
});
|
|
740
765
|
return sanitized.length > 0 ? sanitized : void 0;
|
|
@@ -1082,6 +1107,45 @@ var BaseCrudController = class {
|
|
|
1082
1107
|
async runAfterHook(req, op, data, meta) {
|
|
1083
1108
|
return executeAfterHook(this.hookOpContext(req), op, data, meta);
|
|
1084
1109
|
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Per-instance single-flight for cache fills — concurrent requests for
|
|
1112
|
+
* the SAME cache key coalesce onto one repository query instead of each
|
|
1113
|
+
* issuing its own (a popular entry expiring under 1,000 concurrent
|
|
1114
|
+
* readers must produce 1 DB query, not 1,000). Keys already encode the
|
|
1115
|
+
* resolved query + user/org scope + resource version, so coalescing can
|
|
1116
|
+
* never cross tenants, scopes, or invalidation generations. Per-process
|
|
1117
|
+
* by design; distributed refresh leases are a store-level concern
|
|
1118
|
+
* (planned with the repo-core CacheEngine façade, which ships
|
|
1119
|
+
* single-flight natively).
|
|
1120
|
+
*/
|
|
1121
|
+
inFlightCacheFills = /* @__PURE__ */ new Map();
|
|
1122
|
+
cacheSingleFlight(key, fill) {
|
|
1123
|
+
const existing = this.inFlightCacheFills.get(key);
|
|
1124
|
+
if (existing) return existing;
|
|
1125
|
+
const flight = fill().finally(() => {
|
|
1126
|
+
this.inFlightCacheFills.delete(key);
|
|
1127
|
+
});
|
|
1128
|
+
this.inFlightCacheFills.set(key, flight);
|
|
1129
|
+
return flight;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Register a background SWR refresh for `key` unless one is already in
|
|
1133
|
+
* flight. Registration is SYNCHRONOUS (before the deferred tick) so a
|
|
1134
|
+
* burst of stale readers arriving in the same tick schedules exactly one
|
|
1135
|
+
* revalidation.
|
|
1136
|
+
*/
|
|
1137
|
+
scheduleCacheRefresh(key, refresh) {
|
|
1138
|
+
if (this.inFlightCacheFills.has(key)) return;
|
|
1139
|
+
const flight = new Promise((resolve) => {
|
|
1140
|
+
scheduleBackground(() => {
|
|
1141
|
+
refresh().catch(() => {}).finally(() => {
|
|
1142
|
+
this.inFlightCacheFills.delete(key);
|
|
1143
|
+
resolve();
|
|
1144
|
+
});
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
this.inFlightCacheFills.set(key, flight);
|
|
1148
|
+
}
|
|
1085
1149
|
/** Cached `list()` flow with SWR semantics. Returns null when cache is disabled. */
|
|
1086
1150
|
async withListCache(req, options) {
|
|
1087
1151
|
const cacheConfig = this.resolveCacheConfig("list");
|
|
@@ -1094,13 +1158,17 @@ var BaseCrudController = class {
|
|
|
1094
1158
|
const { data, status } = await qc.get(key);
|
|
1095
1159
|
if (status === "fresh") return this.cacheResponse(data, "HIT");
|
|
1096
1160
|
if (status === "stale") {
|
|
1097
|
-
|
|
1098
|
-
this.executeListQuery(options, req)
|
|
1161
|
+
this.scheduleCacheRefresh(key, async () => {
|
|
1162
|
+
const fresh = await this.executeListQuery(options, req);
|
|
1163
|
+
await qc.set(key, fresh, cacheConfig);
|
|
1099
1164
|
});
|
|
1100
1165
|
return this.cacheResponse(data, "STALE");
|
|
1101
1166
|
}
|
|
1102
|
-
const result = await this.
|
|
1103
|
-
|
|
1167
|
+
const result = await this.cacheSingleFlight(key, async () => {
|
|
1168
|
+
const fresh = await this.executeListQuery(options, req);
|
|
1169
|
+
await qc.set(key, fresh, cacheConfig);
|
|
1170
|
+
return fresh;
|
|
1171
|
+
});
|
|
1104
1172
|
return this.cacheResponse(result, "MISS");
|
|
1105
1173
|
}
|
|
1106
1174
|
/** Cached `get()` flow with SWR semantics. Returns null when cache is disabled. */
|
|
@@ -1118,16 +1186,18 @@ var BaseCrudController = class {
|
|
|
1118
1186
|
const { data, status } = await qc.get(key);
|
|
1119
1187
|
if (status === "fresh") return this.cacheResponse(data, "HIT");
|
|
1120
1188
|
if (status === "stale") {
|
|
1121
|
-
|
|
1122
|
-
this.executeGetQuery(id, options, req)
|
|
1123
|
-
|
|
1124
|
-
}).catch(() => {});
|
|
1189
|
+
this.scheduleCacheRefresh(key, async () => {
|
|
1190
|
+
const { doc: fresh } = await this.executeGetQuery(id, options, req);
|
|
1191
|
+
if (fresh) await qc.set(key, fresh, cacheConfig);
|
|
1125
1192
|
});
|
|
1126
1193
|
return this.cacheResponse(data, "STALE");
|
|
1127
1194
|
}
|
|
1128
|
-
const { doc, reason } = await this.
|
|
1195
|
+
const { doc, reason } = await this.cacheSingleFlight(key, async () => {
|
|
1196
|
+
const result = await this.executeGetQuery(id, options, req);
|
|
1197
|
+
if (result.doc) await qc.set(key, result.doc, cacheConfig);
|
|
1198
|
+
return result;
|
|
1199
|
+
});
|
|
1129
1200
|
if (!doc) this.throwNotFound(reason);
|
|
1130
|
-
await qc.set(key, doc, cacheConfig);
|
|
1131
1201
|
return this.cacheResponse(doc, "MISS");
|
|
1132
1202
|
}
|
|
1133
1203
|
async list(req) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { r as CacheStore } from "./interface-BGZCzOGL.mjs";
|
|
2
|
+
//#region src/cache/QueryCache.d.ts
|
|
3
|
+
/** Metadata wrapper stored in CacheStore */
|
|
4
|
+
interface CacheEnvelope<T = unknown> {
|
|
5
|
+
data: T;
|
|
6
|
+
createdAt: number;
|
|
7
|
+
staleAfter: number;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
tags: string[];
|
|
10
|
+
}
|
|
11
|
+
interface QueryCacheConfig {
|
|
12
|
+
/** Seconds data is "fresh" (no revalidation). Default: 0 */
|
|
13
|
+
staleTime?: number;
|
|
14
|
+
/** Seconds stale data stays cached (SWR window). Default: 60 */
|
|
15
|
+
gcTime?: number;
|
|
16
|
+
/** Tags for group invalidation */
|
|
17
|
+
tags?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Randomized freshness jitter (0–1, default 0 = off). Each write varies
|
|
20
|
+
* its effective `staleTime` by ±(jitter × staleTime), so entries cached
|
|
21
|
+
* together don't all go stale in the same instant — spreading
|
|
22
|
+
* revalidation load instead of synchronizing a refresh burst.
|
|
23
|
+
* Single-flight coalescing (BaseCrudController) handles same-key
|
|
24
|
+
* stampedes; jitter de-synchronizes ACROSS keys. 0.1 is a sane value.
|
|
25
|
+
*/
|
|
26
|
+
jitter?: number;
|
|
27
|
+
}
|
|
28
|
+
type CacheStatus = "fresh" | "stale" | "miss";
|
|
29
|
+
interface CacheResult<T> {
|
|
30
|
+
data: T;
|
|
31
|
+
status: CacheStatus;
|
|
32
|
+
}
|
|
33
|
+
declare class QueryCache {
|
|
34
|
+
private readonly store;
|
|
35
|
+
constructor(store: CacheStore);
|
|
36
|
+
get<T>(key: string): Promise<CacheResult<T>>;
|
|
37
|
+
set<T>(key: string, data: T, config: QueryCacheConfig): Promise<void>;
|
|
38
|
+
invalidate(key: string): Promise<void>;
|
|
39
|
+
/** Get current version for a resource (defaults to 0 if not set) */
|
|
40
|
+
getResourceVersion(resource: string): Promise<number>;
|
|
41
|
+
/** Bump resource version — orphans all cached queries for this resource */
|
|
42
|
+
bumpResourceVersion(resource: string): Promise<void>;
|
|
43
|
+
/** Get current version for a tag */
|
|
44
|
+
getTagVersion(tag: string): Promise<number>;
|
|
45
|
+
/** Bump tag version — orphans all cached queries tagged with this tag */
|
|
46
|
+
bumpTagVersion(tag: string): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Version-bump semantics:
|
|
49
|
+
*
|
|
50
|
+
* - `store.increment` present (Redis `INCR`) — atomic and strictly
|
|
51
|
+
* monotonic across replicas; concurrent bumps can never collide or
|
|
52
|
+
* go backwards.
|
|
53
|
+
* - fallback — read-modify-write with `max(now, current + 1)`: two
|
|
54
|
+
* bumps in the same millisecond and clock regressions still advance
|
|
55
|
+
* the version. Concurrent bumps from DIFFERENT replicas can lose one
|
|
56
|
+
* write, which is why distributed hosts should use an incrementing
|
|
57
|
+
* store (`runtime: 'distributed'` already requires a shared one).
|
|
58
|
+
*
|
|
59
|
+
* Version keys must outlive every derived cache entry — that invariant
|
|
60
|
+
* is what makes version invalidation sound (a version key expiring back
|
|
61
|
+
* to 0 would make entries written under the pre-reset version
|
|
62
|
+
* addressable again). The 1-year TTL applies with the repo-core
|
|
63
|
+
* TTL-ON-CREATE semantics, so a version key expires one year after its
|
|
64
|
+
* FIRST bump — safe, because a revived collision would need an entry to
|
|
65
|
+
* outlive that reset and entry TTLs are minutes-scale (staleTime +
|
|
66
|
+
* gcTime). The read-modify-write fallback uses plain `set`, which
|
|
67
|
+
* refreshes the TTL per bump — strictly longer-lived, equally safe.
|
|
68
|
+
*/
|
|
69
|
+
private bumpVersion;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { QueryCacheConfig as a, QueryCache as i, CacheResult as n, CacheStatus as r, CacheEnvelope as t };
|
|
@@ -71,6 +71,7 @@ var ResourceRegistry = class {
|
|
|
71
71
|
disabledRoutes: resource.disabledRoutes ? [...resource.disabledRoutes] : void 0,
|
|
72
72
|
openApiSchemas: options.openApiSchemas,
|
|
73
73
|
fieldPermissions: extractFieldPermissions(resource.fields),
|
|
74
|
+
fields: resource.fields && Object.keys(resource.fields).length > 0 ? resource.fields : void 0,
|
|
74
75
|
pipelineSteps: extractPipelineSteps(resource.pipe),
|
|
75
76
|
rateLimit: resource.rateLimit,
|
|
76
77
|
audit: resource.audit,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/core/actionPermissions.ts
|
|
2
|
+
/**
|
|
3
|
+
* Return the effective `PermissionCheck` for a single action, or `undefined`
|
|
4
|
+
* when the resource declares no gate at any level.
|
|
5
|
+
*
|
|
6
|
+
* Callers decide what "no gate" means:
|
|
7
|
+
* - HTTP: boot-time throw in `normalizeActionsToRouterConfig`.
|
|
8
|
+
* - MCP: tool-generation throw in `resourceToTools` (mirrors HTTP — the
|
|
9
|
+
* two surfaces fail closed identically so MCP can't expose an
|
|
10
|
+
* unauthenticated mutating tool when the HTTP plugin lifecycle hasn't
|
|
11
|
+
* run).
|
|
12
|
+
* - OpenAPI: docs advertise the endpoint as unauthenticated.
|
|
13
|
+
*/
|
|
14
|
+
function resolveActionPermission(input) {
|
|
15
|
+
const { action, resourcePermissions, resourceActionPermissions, globalAuth } = input;
|
|
16
|
+
const explicit = typeof action !== "function" && action.permissions ? action.permissions : void 0;
|
|
17
|
+
if (explicit) return explicit;
|
|
18
|
+
if (resourceActionPermissions) return resourceActionPermissions;
|
|
19
|
+
if (globalAuth) return globalAuth;
|
|
20
|
+
const updateFallback = resourcePermissions?.update;
|
|
21
|
+
if (updateFallback) return updateFallback;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { resolveActionPermission as t };
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
//#region src/permissions/fields.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 guard for Mongoose-like documents with toObject() */
|
|
25
|
+
function isMongooseDoc(obj) {
|
|
26
|
+
return !!obj && typeof obj === "object" && "toObject" in obj && typeof obj.toObject === "function";
|
|
27
|
+
}
|
|
28
|
+
const fields = {
|
|
29
|
+
/**
|
|
30
|
+
* Field is never included in responses. Not writable via API.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* fields: { password: fields.hidden() }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
hidden() {
|
|
38
|
+
return { _type: "hidden" };
|
|
39
|
+
},
|
|
40
|
+
/**
|
|
41
|
+
* Field is only visible to users with specified roles.
|
|
42
|
+
* Other users don't see the field at all.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* fields: { salary: fields.visibleTo(['admin', 'hr']) }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
visibleTo(roles) {
|
|
50
|
+
return {
|
|
51
|
+
_type: "visibleTo",
|
|
52
|
+
roles
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
/**
|
|
56
|
+
* Field is only writable by users with specified roles.
|
|
57
|
+
* All users can still read the field. Users without the role
|
|
58
|
+
* have the field silently stripped from write operations.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* fields: { role: fields.writableBy(['admin']) }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
writableBy(roles) {
|
|
66
|
+
return {
|
|
67
|
+
_type: "writableBy",
|
|
68
|
+
roles
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* Field is redacted (replaced with a placeholder) for specified roles.
|
|
73
|
+
* Other users see the real value.
|
|
74
|
+
*
|
|
75
|
+
* @param roles - Roles that see the redacted value
|
|
76
|
+
* @param redactValue - Replacement value (default: '***')
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* fields: {
|
|
81
|
+
* email: fields.redactFor(['viewer']),
|
|
82
|
+
* ssn: fields.redactFor(['basic'], '***-**-****'),
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
redactFor(roles, redactValue = "***") {
|
|
87
|
+
return {
|
|
88
|
+
_type: "redactFor",
|
|
89
|
+
roles,
|
|
90
|
+
redactValue
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Apply field-level READ permissions to a response object.
|
|
96
|
+
* Strips hidden fields, enforces visibility, and applies redaction.
|
|
97
|
+
*
|
|
98
|
+
* @param data - The response object (mutated in place for performance)
|
|
99
|
+
* @param fieldPermissions - Field permission map from resource config
|
|
100
|
+
* @param userRoles - Current user's roles (empty array for unauthenticated)
|
|
101
|
+
* @returns The filtered object
|
|
102
|
+
*/
|
|
103
|
+
function applyFieldReadPermissions(data, fieldPermissions, userRoles) {
|
|
104
|
+
if (!data || typeof data !== "object") return data;
|
|
105
|
+
const result = { ...isMongooseDoc(data) ? data.toObject() : data };
|
|
106
|
+
for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
|
|
107
|
+
case "hidden":
|
|
108
|
+
delete result[field];
|
|
109
|
+
break;
|
|
110
|
+
case "visibleTo":
|
|
111
|
+
if (!perm.roles?.some((r) => userRoles.includes(r))) delete result[field];
|
|
112
|
+
break;
|
|
113
|
+
case "redactFor":
|
|
114
|
+
if (perm.roles?.some((r) => userRoles.includes(r))) result[field] = perm.redactValue ?? "***";
|
|
115
|
+
break;
|
|
116
|
+
case "writableBy": break;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Apply field-level WRITE permissions to request body.
|
|
122
|
+
*
|
|
123
|
+
* Returns both the filtered body and the list of denied fields. Callers are
|
|
124
|
+
* expected to reject the request when `deniedFields.length > 0` — silently
|
|
125
|
+
* stripping fields hides misconfigurations and real attacks. See
|
|
126
|
+
* `BodySanitizer` for the default policy.
|
|
127
|
+
*
|
|
128
|
+
* @param body - The request body (returns a new filtered copy)
|
|
129
|
+
* @param fieldPermissions - Field permission map from resource config
|
|
130
|
+
* @param userRoles - Current user's roles
|
|
131
|
+
*/
|
|
132
|
+
function applyFieldWritePermissions(body, fieldPermissions, userRoles) {
|
|
133
|
+
const result = { ...body };
|
|
134
|
+
const deniedFields = [];
|
|
135
|
+
for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
|
|
136
|
+
case "hidden":
|
|
137
|
+
if (field in result) {
|
|
138
|
+
deniedFields.push(field);
|
|
139
|
+
delete result[field];
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
case "writableBy":
|
|
143
|
+
if (field in result && !perm.roles?.some((r) => userRoles.includes(r))) {
|
|
144
|
+
deniedFields.push(field);
|
|
145
|
+
delete result[field];
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
body: result,
|
|
151
|
+
deniedFields
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolve effective roles by merging global user roles with org-level roles.
|
|
156
|
+
*
|
|
157
|
+
* Global roles come from `req.user.role` (normalized via getUserRoles()).
|
|
158
|
+
* Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
|
|
159
|
+
*
|
|
160
|
+
* When no org context exists, returns global roles only — backward compatible.
|
|
161
|
+
*/
|
|
162
|
+
function resolveEffectiveRoles(userRoles, orgRoles) {
|
|
163
|
+
if (orgRoles.length === 0) return [...userRoles];
|
|
164
|
+
if (userRoles.length === 0) return [...orgRoles];
|
|
165
|
+
return [.../* @__PURE__ */ new Set([...userRoles, ...orgRoles])];
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/permissions/applyPermissionResult.ts
|
|
169
|
+
/**
|
|
170
|
+
* Normalize a permission check return value (`boolean | PermissionResult`)
|
|
171
|
+
* into a concrete `PermissionResult`. This is the only place in Arc that
|
|
172
|
+
* promotes booleans to results — keeps the type narrowing honest everywhere.
|
|
173
|
+
*/
|
|
174
|
+
function normalizePermissionResult(result) {
|
|
175
|
+
if (typeof result === "boolean") return { granted: result };
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Apply a granted `PermissionResult` to a Fastify request — merges row-level
|
|
180
|
+
* filters into `_policyFilters` and conditionally installs the scope.
|
|
181
|
+
*
|
|
182
|
+
* **Scope install rule:** only writes `scope` when the current request scope
|
|
183
|
+
* is absent or `public`. This prevents downgrading an already-authenticated
|
|
184
|
+
* request (e.g. Better Auth set `member`, then a permission check returns a
|
|
185
|
+
* narrower `service` scope — the original `member` wins because it came from
|
|
186
|
+
* a more authoritative source).
|
|
187
|
+
*
|
|
188
|
+
* Safe to call with a non-granted result — it simply no-ops. Callers should
|
|
189
|
+
* still check `result.granted` and send an error response before reaching here,
|
|
190
|
+
* but this function tolerates the misuse defensively.
|
|
191
|
+
*/
|
|
192
|
+
function applyPermissionResult(result, request) {
|
|
193
|
+
if (!result.granted) return;
|
|
194
|
+
if (result.filters) request._policyFilters = {
|
|
195
|
+
...request._policyFilters ?? {},
|
|
196
|
+
...result.filters
|
|
197
|
+
};
|
|
198
|
+
if (result.scope) {
|
|
199
|
+
const current = request.scope;
|
|
200
|
+
if (!current || current.kind === "public") request.scope = result.scope;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Max length of a `PermissionResult.reason` string before we fall back to the
|
|
205
|
+
* generic default message. Upstream checks can return arbitrary strings; we
|
|
206
|
+
* clamp to prevent accidental leakage of internal diagnostics or oversized
|
|
207
|
+
* payloads via the 4xx response body.
|
|
208
|
+
*/
|
|
209
|
+
const MAX_DENIAL_REASON_LENGTH = 100;
|
|
210
|
+
/**
|
|
211
|
+
* End-to-end evaluator: runs the permission check, catches throws, normalizes
|
|
212
|
+
* the result, sends a 401/403 response on denial, and applies side-effects on
|
|
213
|
+
* grant. Returns `true` if the caller should continue, `false` if a response
|
|
214
|
+
* has been sent and the caller should return.
|
|
215
|
+
*
|
|
216
|
+
* This is the single source of truth for the 5-step sequence shared by the
|
|
217
|
+
* CRUD router, action router, and MCP tool handlers:
|
|
218
|
+
*
|
|
219
|
+
* 1. `try { await check(ctx) } catch { reply 403 }`
|
|
220
|
+
* 2. `normalizePermissionResult(result)`
|
|
221
|
+
* 3. If denied → 401 (no user) or 403 (user) with clamped reason
|
|
222
|
+
* 4. If granted → `applyPermissionResult` (filters + scope)
|
|
223
|
+
* 5. Return true/false so the caller knows whether to keep going
|
|
224
|
+
*
|
|
225
|
+
* Context construction, pre-check auth gating, and success-path handler
|
|
226
|
+
* invocation stay at the callsite — those are genuinely different per router
|
|
227
|
+
* and don't belong here.
|
|
228
|
+
*
|
|
229
|
+
* @returns `true` if authorized (caller continues), `false` if a response was sent
|
|
230
|
+
*/
|
|
231
|
+
async function evaluateAndApplyPermission(check, context, request, reply, opts) {
|
|
232
|
+
let result;
|
|
233
|
+
try {
|
|
234
|
+
result = await check(context);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
request.log?.warn?.({
|
|
237
|
+
err,
|
|
238
|
+
resource: context.resource,
|
|
239
|
+
action: context.action
|
|
240
|
+
}, "Permission check threw");
|
|
241
|
+
reply.code(403).send({
|
|
242
|
+
code: "arc.forbidden",
|
|
243
|
+
message: "Permission denied",
|
|
244
|
+
status: 403
|
|
245
|
+
});
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const permResult = normalizePermissionResult(result);
|
|
249
|
+
if (!permResult.granted) {
|
|
250
|
+
const defaultMsg = opts?.defaultDenialMessage?.(context.user) ?? (context.user ? "Permission denied" : "Authentication required");
|
|
251
|
+
const reason = permResult.reason && permResult.reason.length <= MAX_DENIAL_REASON_LENGTH ? permResult.reason : defaultMsg;
|
|
252
|
+
const status = context.user ? 403 : 401;
|
|
253
|
+
reply.code(status).send({
|
|
254
|
+
code: context.user ? "arc.forbidden" : "arc.unauthorized",
|
|
255
|
+
message: reason,
|
|
256
|
+
status
|
|
257
|
+
});
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
applyPermissionResult(permResult, request);
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
//#endregion
|
|
264
|
+
export { applyFieldWritePermissions as a, applyFieldReadPermissions as i, evaluateAndApplyPermission as n, fields as o, normalizePermissionResult as r, resolveEffectiveRoles as s, applyPermissionResult as t };
|
package/dist/audit/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import "../index-
|
|
2
|
-
import { i as UserBase } from "../types-
|
|
1
|
+
import "../index-DPRH0Cme.mjs";
|
|
2
|
+
import { i as UserBase } from "../types-Dmw-6Qeo.mjs";
|
|
3
3
|
import { FastifyPluginAsync } from "fastify";
|
|
4
4
|
import { RepositoryLike } from "@classytic/repo-core/adapter";
|
|
5
5
|
//#region src/audit/stores/interface.d.ts
|
|
@@ -131,6 +131,17 @@ interface AuditPluginOptions {
|
|
|
131
131
|
/** Interval between purges in ms. Default 86_400_000 (24h). 0 disables the timer. */
|
|
132
132
|
purgeIntervalMs?: number;
|
|
133
133
|
};
|
|
134
|
+
/**
|
|
135
|
+
* Unit-of-Work session provider for the built-in `repository` store. When a
|
|
136
|
+
* request runs inside a `transactionContext.run(session, …)` scope, the audit
|
|
137
|
+
* row is written **with that session**, so it commits/rolls back atomically
|
|
138
|
+
* with the domain write. Defaults to `() => transactionContext.get()`, so
|
|
139
|
+
* atomic audit works out of the box for kits that honor a write session
|
|
140
|
+
* (mongokit). Outside a transaction scope the provider returns `undefined`
|
|
141
|
+
* and the audit write proceeds session-less (unchanged). Pass `false` (or
|
|
142
|
+
* `() => undefined`) to opt out.
|
|
143
|
+
*/
|
|
144
|
+
sessionProvider?: (() => unknown) | false;
|
|
134
145
|
/**
|
|
135
146
|
* Automatically audit CRUD operations via the hook system (default: true when enabled).
|
|
136
147
|
* When enabled, create/update/delete operations are auto-logged without manual calls.
|
|
@@ -207,7 +218,25 @@ declare const auditPlugin: FastifyPluginAsync<AuditPluginOptions>;
|
|
|
207
218
|
declare const _default: FastifyPluginAsync<AuditPluginOptions>;
|
|
208
219
|
//#endregion
|
|
209
220
|
//#region src/audit/repository-audit-adapter.d.ts
|
|
210
|
-
|
|
221
|
+
/** Options for {@link repositoryAsAuditStore}. */
|
|
222
|
+
interface RepositoryAuditStoreOptions {
|
|
223
|
+
/**
|
|
224
|
+
* Unit-of-Work session provider (AsyncLocalStorage integration), mirroring
|
|
225
|
+
* {@link EventOutbox}'s `sessionProvider`. When set, `log()` calls it to get
|
|
226
|
+
* the active DB session and writes the audit row **with that session**, so
|
|
227
|
+
* the audit entry commits/rolls back **atomically with the domain write**
|
|
228
|
+
* that produced it — no unaudited successful write, and no orphan audit for a
|
|
229
|
+
* rolled-back write. Returns `undefined` outside a transaction scope, in
|
|
230
|
+
* which case the write proceeds session-less (unchanged legacy behaviour).
|
|
231
|
+
*
|
|
232
|
+
* Wire with arc's `transactionContext` (auditPlugin does this by default):
|
|
233
|
+
* ```ts
|
|
234
|
+
* repositoryAsAuditStore(repo, { sessionProvider: () => transactionContext.get() })
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
readonly sessionProvider?: () => unknown;
|
|
238
|
+
}
|
|
239
|
+
declare function repositoryAsAuditStore(repository: RepositoryLike, options?: RepositoryAuditStoreOptions): AuditStore;
|
|
211
240
|
//#endregion
|
|
212
241
|
//#region src/audit/stores/memory.d.ts
|
|
213
242
|
interface MemoryAuditStoreOptions {
|
package/dist/audit/index.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { t as transactionContext } from "../transactionContext-PNdoyLLm.mjs";
|
|
1
2
|
import fp from "fastify-plugin";
|
|
2
3
|
import { and, anyOf, eq, gte, lt, lte } from "@classytic/repo-core/filter";
|
|
3
4
|
//#region src/audit/repository-audit-adapter.ts
|
|
4
|
-
function repositoryAsAuditStore(repository) {
|
|
5
|
+
function repositoryAsAuditStore(repository, options = {}) {
|
|
5
6
|
const idField = repository.idField ?? "_id";
|
|
7
|
+
const { sessionProvider } = options;
|
|
6
8
|
return {
|
|
7
9
|
name: "repository",
|
|
8
10
|
async log(entry) {
|
|
@@ -22,7 +24,8 @@ function repositoryAsAuditStore(repository) {
|
|
|
22
24
|
metadata: entry.metadata,
|
|
23
25
|
timestamp: entry.timestamp
|
|
24
26
|
};
|
|
25
|
-
|
|
27
|
+
const session = sessionProvider?.();
|
|
28
|
+
await repository.create(doc, session != null ? { session } : void 0);
|
|
26
29
|
},
|
|
27
30
|
async purgeOlderThan(cutoff) {
|
|
28
31
|
if (!repository.deleteMany) return 0;
|
|
@@ -173,7 +176,7 @@ var MemoryAuditStore = class {
|
|
|
173
176
|
//#endregion
|
|
174
177
|
//#region src/audit/auditPlugin.ts
|
|
175
178
|
const auditPlugin = async (fastify, opts = {}) => {
|
|
176
|
-
const { enabled = false, repository, customStores = [] } = opts;
|
|
179
|
+
const { enabled = false, repository, customStores = [], sessionProvider = () => transactionContext.get() } = opts;
|
|
177
180
|
if (!enabled) {
|
|
178
181
|
fastify.decorate("audit", createNoopLogger());
|
|
179
182
|
fastify.decorateRequest("auditContext", void 0);
|
|
@@ -181,7 +184,7 @@ const auditPlugin = async (fastify, opts = {}) => {
|
|
|
181
184
|
return;
|
|
182
185
|
}
|
|
183
186
|
const stores = [];
|
|
184
|
-
if (repository) stores.push(repositoryAsAuditStore(repository));
|
|
187
|
+
if (repository) stores.push(repositoryAsAuditStore(repository, { sessionProvider: sessionProvider === false ? void 0 : sessionProvider }));
|
|
185
188
|
stores.push(...customStores);
|
|
186
189
|
if (stores.length === 0) stores.push(new MemoryAuditStore());
|
|
187
190
|
async function logToStores(entry) {
|
package/dist/auth/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Ht as
|
|
2
|
-
import { t as PermissionCheck } from "../types-
|
|
1
|
+
import { Ht as AuthHelpers, Ut as AuthPluginOptions } from "../index-DPRH0Cme.mjs";
|
|
2
|
+
import { t as PermissionCheck } from "../types-Dmw-6Qeo.mjs";
|
|
3
3
|
import { t as ExternalOpenApiPaths } from "../externalPaths-BE6wFpYv.mjs";
|
|
4
4
|
import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-BHOM3N_0.mjs";
|
|
5
5
|
import { FastifyPluginAsync, FastifyReply as FastifyReply$1, FastifyRequest } from "fastify";
|