@classytic/arc 2.31.0 → 2.33.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/README.md +1 -1
- package/bin/arc.js +58 -17
- package/dist/{BaseController-De7GqHck.mjs → BaseController-CaQC5EXX.mjs} +298 -35
- package/dist/{EventTransport-2uVV34WU.d.mts → EventTransport-BEVP50ka.d.mts} +16 -0
- package/dist/{EventTransport-9mlTgqzE.mjs → EventTransport-tq4CEB-q.mjs} +39 -2
- package/dist/{HookSystem-BcmPyq5I.mjs → HookSystem-DqXkMXkB.mjs} +2 -2
- package/dist/{ResourceRegistry-qq0mn2s9.mjs → ResourceRegistry-DoALUlOO.mjs} +18 -15
- package/dist/{actionPermissions-BE5NwaQk.mjs → actionPermissions-CUeCmpUd.mjs} +1 -1
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +2 -2
- package/dist/auth/audit.mjs +1 -1
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +44 -11
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-ChvUQGG1.mjs → authorizationDecision-B3mP_QTy.mjs} +2 -2
- package/dist/{types-B7XeLEgK.d.mts → base-Csjc2oF-.d.mts} +74 -74
- package/dist/{buildHandler-CDrCJzmB.mjs → buildHandler-DLE9xX-V.mjs} +6 -5
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-BFHA6Scf.mjs → caching-BcGmvnHh.mjs} +2 -2
- package/dist/cleanup/index.d.mts +52 -18
- package/dist/cleanup/index.mjs +281 -32
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +4 -6
- package/dist/cli/commands/docs.mjs +3 -3
- package/dist/cli/commands/doctor.mjs +1 -1
- package/dist/cli/commands/generate.mjs +2 -2
- package/dist/cli/commands/init.mjs +2 -2
- package/dist/cli/commands/introspect.mjs +2 -2
- package/dist/cli/index.mjs +3 -3
- package/dist/{constants-D3Dv4LAV.mjs → constants-BxCoxmiN.mjs} +1 -1
- package/dist/context/index.mjs +3 -3
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +7 -7
- package/dist/{core-BGpIxINN.mjs → core-dxhcHhUD.mjs} +3 -3
- package/dist/{createActionRouter-CDXEdasl.mjs → createActionRouter-DVpoA0do.mjs} +14 -11
- package/dist/{createAggregationRouter-KrQDzx-k.mjs → createAggregationRouter-B_B48JZ-.mjs} +10 -8
- package/dist/{createApp-Df89BEni.mjs → createApp-CLGegqiG.mjs} +287 -62
- package/dist/{defineAggregation-DbPPS8RI.mjs → defineAggregation-Nv-DwfOf.mjs} +1 -1
- package/dist/{defineResource-Bv6EtN1x.mjs → defineResource-DR9vsUYy.mjs} +318 -35
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +3 -3
- package/dist/{elevation-D2l1mh4g.mjs → elevation-B0tlY8y1.mjs} +4 -4
- package/dist/encryption/index.mjs +5 -4
- package/dist/{errorHandler-C5NeD6bT.mjs → errorHandler-C7xBqHsI.mjs} +42 -5
- package/dist/{errorHandler-CpYUJmPA.d.mts → errorHandler-DI5AN2b5.d.mts} +14 -3
- package/dist/{errors-Cj42AntO.mjs → errors-Cd_yy7aW.mjs} +1 -1
- package/dist/{eventPlugin-GF5nnBIq.d.mts → eventPlugin-DSNEZWfV.d.mts} +29 -1
- package/dist/{eventPlugin-CkffEihm.mjs → eventPlugin-Dar8qRaj.mjs} +68 -11
- package/dist/events/index.d.mts +18 -297
- package/dist/events/index.mjs +7 -758
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +14 -10
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/events/transports/redis.mjs +1 -1
- package/dist/factory/index.d.mts +20 -5
- package/dist/factory/index.mjs +131 -8
- package/dist/fastify-augmentation-DVsTAy6X.d.mts +179 -0
- package/dist/{fields-CB5Cuzk9.mjs → fields-CojeUkS4.mjs} +6 -11
- package/dist/{generate-EcA6ui4i.mjs → generate-DiB0nHPR.mjs} +2 -2
- package/dist/{headers-3zvE0rM3.mjs → headers-CveNO9gR.mjs} +2 -2
- package/dist/{health-DjZYjM7H.mjs → health-CasG2A28.mjs} +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +2 -2
- package/dist/idempotency/index.d.mts +45 -1
- package/dist/idempotency/index.mjs +95 -11
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-CEJNZFtX.d.mts → index-B-DvUzsr.d.mts} +82 -3
- package/dist/{index-CpzxqePp.d.mts → index-BcR2bFZQ.d.mts} +707 -373
- package/dist/{index-Bc2ox8_U.d.mts → index-Brin0ODU.d.mts} +4 -4
- package/dist/{index-CFThn1K9.d.mts → index-C5NnMEKF.d.mts} +70 -4
- package/dist/{index-C-481Zf2.d.mts → index-ugr1tQlW.d.mts} +17 -1
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +11 -11
- package/dist/{init-BEiNZBsJ.mjs → init-mdJ-v4Xp.mjs} +24 -16
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +2 -2
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/index.mjs +1 -1
- package/dist/integrations/jobs/index.d.mts +1 -1
- package/dist/integrations/jobs/index.mjs +100 -98
- package/dist/integrations/mcp/index.d.mts +12 -3
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.mjs +3 -3
- package/dist/integrations/webhooks.d.mts +124 -1
- package/dist/integrations/webhooks.mjs +180 -2
- package/dist/integrations/websocket-pushref-redis.mjs +1 -1
- package/dist/integrations/websocket-redis.mjs +1 -1
- package/dist/integrations/websocket.mjs +38 -35
- package/dist/{interface-DYzM0q-u.mjs → interface-DznJrm9R.mjs} +1 -1
- package/dist/{jsonBody-BYbLDi5D.mjs → jsonBody-CfGH-GxO.mjs} +2 -2
- package/dist/{keys-C0_pa1tY.mjs → keys-WeKvY1dE.mjs} +1 -1
- package/dist/{loadResources-yawi9tAE.mjs → loadResources-Bj-aSsXi.mjs} +4 -3
- package/dist/{loadResourcesFromEntry-BAjYZonx.mjs → loadResourcesFromEntry-DwaxNcE2.mjs} +2 -2
- package/dist/logger/index.mjs +1 -1
- package/dist/{mcpPlugin-B_es0hiF.mjs → mcpPlugin-aJg5TqHi.mjs} +76 -36
- package/dist/{memory-DR6wpa9F.mjs → memory-qYSB7rMQ.mjs} +5 -5
- package/dist/{metrics-CUdw_wLB.mjs → metrics-D7uchLjN.mjs} +2 -2
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +3 -3
- package/dist/migrations/index.mjs +3 -3
- package/dist/{module-0pFJA6YW.mjs → module-CwKsgZXp.mjs} +1 -1
- package/dist/{multipartBody-wOgj_fnj.mjs → multipartBody-4w-S3-PU.mjs} +3 -3
- package/dist/{openapi-B2L4js1v.mjs → openapi-BN5cQaO2.mjs} +6 -6
- package/dist/outbox-admin/index.d.mts +85 -0
- package/dist/outbox-admin/index.mjs +115 -0
- package/dist/outbox-module-CXB79bvt.mjs +839 -0
- package/dist/outbox-module-D-VFq37T.d.mts +368 -0
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +6 -6
- package/dist/{permissions-L--QDAir.mjs → permissions-BAfwgBxc.mjs} +120 -8
- package/dist/{pipe-DRDNsI2T.mjs → pipe-BzzoEhHj.mjs} +3 -5
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +2 -2
- package/dist/{pipeline-UPQFatit.mjs → pipeline-q69DZzgG.mjs} +17 -15
- package/dist/plugins/index.d.mts +6 -6
- package/dist/plugins/index.mjs +16 -16
- package/dist/plugins/response-cache.mjs +2 -2
- package/dist/plugins/tracing-entry.mjs +2 -2
- package/dist/{pluralize-O0FZgESl.mjs → pluralize-B7xf8qZF.mjs} +1 -1
- package/dist/{preloadResources-Dz6BjzM6.mjs → preloadResources-BkyqeTDz.mjs} +1 -1
- package/dist/{preloadResources-CyoJMSiO.d.mts → preloadResources-g07ldtzh.d.mts} +30 -7
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +6 -6
- package/dist/presets/index.d.mts +2 -2
- package/dist/presets/index.mjs +2 -2
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +3 -3
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +2 -2
- package/dist/{presets-C4-g4_4P.mjs → presets-D-h0uWtP.mjs} +6 -5
- package/dist/projection-BKM0Ui-s.mjs +42 -0
- package/dist/{queryCachePlugin-BZr8v46E.mjs → queryCachePlugin-DhsZ0ZIc.mjs} +50 -8
- package/dist/{rateLimit-CMPY5Qms.mjs → rateLimit-D4-ASwsI.mjs} +2 -2
- package/dist/{rateLimitKey-Dn5JNmkj.mjs → rateLimitKey-XngqVNEV.mjs} +1 -1
- package/dist/{realtime-C5NoxWFY.mjs → realtime-CTkv3O59.mjs} +10 -10
- package/dist/{redis-stream-qN6A-RFv.d.mts → redis-stream-mwLWlfHG.d.mts} +1 -1
- package/dist/{registerRoute-CBN84fc7.mjs → registerRoute-DNnd3vkz.mjs} +10 -8
- package/dist/registry/index.d.mts +3 -3
- package/dist/registry/index.mjs +3 -3
- package/dist/{registry-4213U4zb.mjs → registry-CLxCR5LF.mjs} +2 -2
- package/dist/{renewingLease-Dll8mGgc.mjs → renewingLease-BnWtbA2U.mjs} +1 -1
- package/dist/{replyHelpers-DIiZKGUt.mjs → replyHelpers-BSBVCwL3.mjs} +2 -2
- package/dist/{repositoryFilter-f9eAfq_l.mjs → repositoryFilter-DKny7PDn.mjs} +1 -1
- package/dist/{requestContext-2TxXOJ7T.mjs → requestContext-uYE35JxI.mjs} +1 -1
- package/dist/{rolldown-runtime-CUsjNX9R.mjs → rolldown-runtime-DNg-KNCk.mjs} +1 -1
- package/dist/runtimeCapabilities-Dm88DaTW.mjs +49 -0
- package/dist/{schedules-DdtlICTf.mjs → schedules-CAcZgia0.mjs} +3 -3
- package/dist/{schemaConverter-D--wdJM-.mjs → schemaConverter-XH1RZRyu.mjs} +1 -1
- package/dist/{schemaIR-Bm2enbnh.mjs → schemaIR-CDvclAew.mjs} +2 -2
- package/dist/schemas/index.mjs +1 -1
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.mjs +5 -5
- package/dist/{scope-DHx5Fous.mjs → scope-BaWcqBvr.mjs} +4 -4
- package/dist/{simpleEqualityMatcher-DjQJ7FKA.mjs → simpleEqualityMatcher-Se8VrsaP.mjs} +1 -1
- package/dist/{sse-CUTo9y0X.mjs → sse-Cf0jUHsh.mjs} +7 -6
- package/dist/{sseStream-B_5XcVVd.mjs → sseStream-Bvpi-UXQ.mjs} +2 -2
- package/dist/{store-helpers-D1MP_gRG.mjs → store-helpers-B88p99Z2.mjs} +1 -1
- package/dist/{streaming-PSZwi8JV.mjs → streaming-DsT1B5zJ.mjs} +1 -1
- package/dist/{subscribe-helpers-C0Tb2aUp.mjs → subscribe-helpers-CLIOHsOK.mjs} +2 -2
- package/dist/sync/changelog.mjs +1 -1
- package/dist/testing/cleanupStoreContract.d.mts +1 -1
- package/dist/testing/cleanupStoreContract.mjs +12 -6
- package/dist/testing/index.d.mts +52 -13
- package/dist/testing/index.mjs +110 -40
- package/dist/testing/outboxStoreContract.d.mts +11 -2
- package/dist/testing/outboxStoreContract.mjs +110 -21
- package/dist/testing/storageContract.mjs +11 -6
- package/dist/{transactionContext-dAfBVUrk.mjs → transactionContext-BYtWnwAS.mjs} +1 -1
- package/dist/{typeGuards-B0ryPo3I.mjs → typeGuards-jjKJDN13.mjs} +1 -1
- package/dist/types/index.d.mts +3 -3
- package/dist/types/index.mjs +1 -1
- package/dist/types/storage.mjs +1 -1
- package/dist/{types-CBgvkp8O.d.mts → types-BcTumAz_.d.mts} +128 -18
- package/dist/{types-CwcUQ610.d.mts → types-BfC83KsP.d.mts} +15 -1
- package/dist/{types-Cys0ko9Z.mjs → types-CoqIRrkY.mjs} +4 -4
- package/dist/{types-C4QE8qnm.d.mts → types-DVLddu2q.d.mts} +144 -1
- package/dist/usage/index.d.mts +1 -1
- package/dist/usage/index.mjs +0 -0
- package/dist/{userHelpers-ChYJ6CdK.mjs → userHelpers-CWF5pbZD.mjs} +1 -1
- package/dist/utils/index.d.mts +57 -5
- package/dist/utils/index.mjs +12 -11
- package/dist/{utils-vL8BrDE2.mjs → utils-h1V_sT-t.mjs} +38 -7
- package/dist/{validate-_KbifpHF.mjs → validate-CdQlz52F.mjs} +2 -2
- package/dist/{versioning-D_rhHfZm.d.mts → versioning-DEs-_hFi.d.mts} +1 -1
- package/dist/{versioning-CHDTor3P.mjs → versioning-rZbyLVD-.mjs} +4 -4
- package/package.json +31 -20
- package/skills/arc/SKILL.md +96 -4
- package/skills/arc/references/api-reference.md +19 -0
- package/skills/arc/references/production.md +2 -2
- package/dist/fastify-augmentation-zh0_AYHa.d.mts +0 -85
- package/dist/projection-BHw6xlOQ.mjs +0 -18
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { m as RESERVED_QUERY_PARAMS } from "./constants-
|
|
1
|
+
/*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
|
|
2
|
+
import { m as RESERVED_QUERY_PARAMS } from "./constants-BxCoxmiN.mjs";
|
|
3
3
|
import { arcLog } from "./logger/index.mjs";
|
|
4
|
-
import { t as ArcError } from "./errors-
|
|
4
|
+
import { t as ArcError, u as ValidationError } from "./errors-Cd_yy7aW.mjs";
|
|
5
5
|
import { errorContractSchema as errorContractSchema$1, errorDetailSchema as errorDetailSchema$1 } from "@classytic/repo-core/errors";
|
|
6
6
|
import { scheduleBackground } from "@classytic/repo-core/cache";
|
|
7
7
|
//#region src/utils/circuitBreaker.ts
|
|
@@ -591,6 +591,11 @@ const DANGEROUS_REGEX_PATTERNS = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\(.+\))\+|\(\?:|\
|
|
|
591
591
|
* For advanced MongoDB features ($lookup, aggregations), use MongoKit's QueryParser.
|
|
592
592
|
*/
|
|
593
593
|
var ArcQueryParser = class {
|
|
594
|
+
/**
|
|
595
|
+
* PUBLIC, deliberately: `QueryResolver` reads it so it does not apply a second cap
|
|
596
|
+
* on top of this one. Three layers capping independently, lowest winning silently,
|
|
597
|
+
* is what made a 696-row chart of accounts render as 100.
|
|
598
|
+
*/
|
|
594
599
|
maxLimit;
|
|
595
600
|
defaultLimit;
|
|
596
601
|
maxRegexLength;
|
|
@@ -599,6 +604,7 @@ var ArcQueryParser = class {
|
|
|
599
604
|
_allowedFilterFields;
|
|
600
605
|
_allowedSortFields;
|
|
601
606
|
_allowedOperators;
|
|
607
|
+
strictFilterFields;
|
|
602
608
|
/** Allowed filter fields (used by MCP for auto-derive) */
|
|
603
609
|
allowedFilterFields;
|
|
604
610
|
/** Allowed sort fields (used by MCP for sort descriptions) */
|
|
@@ -621,6 +627,7 @@ var ArcQueryParser = class {
|
|
|
621
627
|
this.maxRegexLength = options.maxRegexLength ?? 200;
|
|
622
628
|
this.maxSearchLength = options.maxSearchLength ?? 200;
|
|
623
629
|
this.maxFilterDepth = options.maxFilterDepth ?? 10;
|
|
630
|
+
this.strictFilterFields = options.strictFilterFields ?? process.env.ARC_STRICT_QUERY_PARAMS === "true";
|
|
624
631
|
if (options.allowedFilterFields) {
|
|
625
632
|
this._allowedFilterFields = new Set(options.allowedFilterFields);
|
|
626
633
|
this.allowedFilterFields = options.allowedFilterFields;
|
|
@@ -787,7 +794,10 @@ var ArcQueryParser = class {
|
|
|
787
794
|
if (value === void 0 || value === null) return;
|
|
788
795
|
if (!/^[a-zA-Z_][a-zA-Z0-9_.]*(?:\[[a-z]+\])?$/.test(key)) return;
|
|
789
796
|
const bareKey = key.replace(/\[[a-z]+\]$/, "");
|
|
790
|
-
if (this._allowedFilterFields && !this._allowedFilterFields.has(bareKey))
|
|
797
|
+
if (this._allowedFilterFields && !this._allowedFilterFields.has(bareKey)) {
|
|
798
|
+
if (this.strictFilterFields) throw new ValidationError(`Unknown filter field: ${bareKey}. Allowed: ${[...this._allowedFilterFields].sort().join(", ")}`);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
791
801
|
if (this.exceedsDepth(value)) return;
|
|
792
802
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
793
803
|
const operatorObj = value;
|
|
@@ -879,10 +889,31 @@ var ArcQueryParser = class {
|
|
|
879
889
|
},
|
|
880
890
|
limit: {
|
|
881
891
|
type: "integer",
|
|
882
|
-
|
|
892
|
+
/**
|
|
893
|
+
* The cap is DOCUMENTED here, not enforced — deliberately.
|
|
894
|
+
*
|
|
895
|
+
* Both layers that handle `limit` CLAMP it: this parser
|
|
896
|
+
* (`Math.min(…, this.maxLimit)`) and `QueryResolver`. Emitting
|
|
897
|
+
* `maximum` contradicted them, because arc layers this schema as the
|
|
898
|
+
* route's `listQuery` and Fastify validates the querystring BEFORE
|
|
899
|
+
* either runs — so the clamp was unreachable and `?limit=200` became a
|
|
900
|
+
* hard 400.
|
|
901
|
+
*
|
|
902
|
+
* That combination is worse than either policy alone. Asking for more
|
|
903
|
+
* rows than allowed is a benign over-ask with an obvious right answer
|
|
904
|
+
* (give the maximum); turning it into a validation failure means a UI
|
|
905
|
+
* passing a generous page size gets NOTHING, and a caller that does not
|
|
906
|
+
* inspect the error body renders it as an empty list — a 400 disguised
|
|
907
|
+
* as "no results", which is the hardest kind to find.
|
|
908
|
+
*
|
|
909
|
+
* A deployment wanting refusal instead of clamping should say so in the
|
|
910
|
+
* parser's policy, where both halves can agree, not in a schema that
|
|
911
|
+
* silently outranks them. Mirrors the same fix in mongokit's
|
|
912
|
+
* `buildQuerySchema`.
|
|
913
|
+
*/
|
|
914
|
+
description: `Number of items per page (values above ${this.maxLimit} are capped to ${this.maxLimit})`,
|
|
883
915
|
default: this.defaultLimit,
|
|
884
|
-
minimum: 1
|
|
885
|
-
maximum: this.maxLimit
|
|
916
|
+
minimum: 1
|
|
886
917
|
},
|
|
887
918
|
sort: {
|
|
888
919
|
type: "string",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { t as __exportAll } from "./rolldown-runtime-
|
|
1
|
+
/*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as __exportAll } from "./rolldown-runtime-DNg-KNCk.mjs";
|
|
3
3
|
//#region src/core/aggregation/normalize.ts
|
|
4
4
|
/**
|
|
5
5
|
* Framework cap on URL-driven aggregation limits when the host opts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { t as __exportAll } from "./rolldown-runtime-
|
|
3
|
-
import { d as createDomainError } from "./errors-
|
|
1
|
+
/*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as __exportAll } from "./rolldown-runtime-DNg-KNCk.mjs";
|
|
3
|
+
import { d as createDomainError } from "./errors-Cd_yy7aW.mjs";
|
|
4
4
|
import fp from "fastify-plugin";
|
|
5
5
|
//#region src/plugins/versioning.ts
|
|
6
6
|
var versioning_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -31,7 +31,7 @@ const versioningPlugin = async (fastify, opts) => {
|
|
|
31
31
|
reply.header(responseHeader, version);
|
|
32
32
|
if (deprecatedSet.has(version)) {
|
|
33
33
|
reply.header("deprecation", "true");
|
|
34
|
-
reply.header("sunset", sunset ?? new Date(Date.now() +
|
|
34
|
+
reply.header("sunset", sunset ?? new Date(Date.now() + 7776e6).toISOString());
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/arc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.33.0",
|
|
4
4
|
"description": "Resource-oriented backend framework for Fastify - clean, minimal, powerful, tree-shakable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -220,6 +220,10 @@
|
|
|
220
220
|
"types": "./dist/cleanup/index.d.mts",
|
|
221
221
|
"default": "./dist/cleanup/index.mjs"
|
|
222
222
|
},
|
|
223
|
+
"./outbox-admin": {
|
|
224
|
+
"types": "./dist/outbox-admin/index.d.mts",
|
|
225
|
+
"default": "./dist/outbox-admin/index.mjs"
|
|
226
|
+
},
|
|
223
227
|
"./testing/outbox": {
|
|
224
228
|
"types": "./dist/testing/outboxStoreContract.d.mts",
|
|
225
229
|
"default": "./dist/testing/outboxStoreContract.mjs"
|
|
@@ -290,7 +294,8 @@
|
|
|
290
294
|
"./auth/audit": "stable",
|
|
291
295
|
"./sync": "experimental",
|
|
292
296
|
"./usage": "experimental",
|
|
293
|
-
"./testing/outbox": "stable"
|
|
297
|
+
"./testing/outbox": "stable",
|
|
298
|
+
"./outbox-admin": "experimental"
|
|
294
299
|
}
|
|
295
300
|
},
|
|
296
301
|
"files": [
|
|
@@ -331,21 +336,23 @@
|
|
|
331
336
|
"test:smoke:redis": "node scripts/smoke-ws-redis.mjs",
|
|
332
337
|
"smoke": "node scripts/smoke-test.mjs",
|
|
333
338
|
"smoke:consumer": "node scripts/smoke-test.mjs --consumer",
|
|
339
|
+
"smoke:mcp": "node scripts/mcp-e2e.mjs",
|
|
334
340
|
"push": "classytic-push",
|
|
335
341
|
"release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
|
|
336
342
|
"release": "npm run push -- main && npm run release:tag && npm publish",
|
|
337
343
|
"check:peer-skew": "node scripts/check-peer-skew.mjs",
|
|
338
344
|
"check:docs-drift": "node scripts/check-docs-drift.mjs",
|
|
339
345
|
"check:boundaries": "node scripts/check-boundaries.mjs",
|
|
340
|
-
"prepublishOnly": "npm run check:peer-skew && npm run check:docs-drift && npm run check:route-api && npm run check:boundaries && npm run typecheck && npm run lint && npm run build && npm run check:api-surface && npm run check:package && npm run test:ci && npm run smoke:consumer",
|
|
341
|
-
"check:route-api": "node scripts/check-removed-route-api.mjs"
|
|
346
|
+
"prepublishOnly": "npm run check:peer-skew && npm run check:docs-drift && npm run check:route-api && npm run check:boundaries && npm run typecheck && npm run lint && npm run build && npm run check:api-surface && npm run check:forgotten-exports && npm run check:package && npm run test:ci && npm run smoke:consumer && npm run smoke:mcp",
|
|
347
|
+
"check:route-api": "node scripts/check-removed-route-api.mjs",
|
|
348
|
+
"check:forgotten-exports": "node scripts/check-forgotten-exports.mjs"
|
|
342
349
|
},
|
|
343
350
|
"engines": {
|
|
344
351
|
"node": ">=22"
|
|
345
352
|
},
|
|
346
353
|
"peerDependencies": {
|
|
347
|
-
"@classytic/primitives": ">=0.
|
|
348
|
-
"@classytic/repo-core": ">=0.
|
|
354
|
+
"@classytic/primitives": ">=0.23.0",
|
|
355
|
+
"@classytic/repo-core": ">=0.24.0",
|
|
349
356
|
"@classytic/streamline": ">=2.8.0",
|
|
350
357
|
"@fastify/cors": ">=11.0.0",
|
|
351
358
|
"@fastify/helmet": ">=13.0.0",
|
|
@@ -353,6 +360,7 @@
|
|
|
353
360
|
"@fastify/multipart": ">=9.0.0",
|
|
354
361
|
"@fastify/rate-limit": ">=10.0.0",
|
|
355
362
|
"@fastify/sensible": ">=6.0.0",
|
|
363
|
+
"@fastify/static": ">=8.0.0",
|
|
356
364
|
"@fastify/type-provider-typebox": ">=6.0.0",
|
|
357
365
|
"@fastify/under-pressure": ">=9.0.0",
|
|
358
366
|
"@fastify/websocket": ">=11.0.0",
|
|
@@ -372,8 +380,8 @@
|
|
|
372
380
|
"jose": ">=5.0.0",
|
|
373
381
|
"pino-pretty": ">=13.0.0",
|
|
374
382
|
"typebox": ">=1.0.0",
|
|
375
|
-
"
|
|
376
|
-
"
|
|
383
|
+
"vitest": ">=3.0.0",
|
|
384
|
+
"zod": ">=4.4.0"
|
|
377
385
|
},
|
|
378
386
|
"peerDependenciesMeta": {
|
|
379
387
|
"better-auth": {
|
|
@@ -456,6 +464,9 @@
|
|
|
456
464
|
},
|
|
457
465
|
"@fastify/static": {
|
|
458
466
|
"optional": true
|
|
467
|
+
},
|
|
468
|
+
"vitest": {
|
|
469
|
+
"optional": true
|
|
459
470
|
}
|
|
460
471
|
},
|
|
461
472
|
"dependencies": {
|
|
@@ -467,14 +478,14 @@
|
|
|
467
478
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
468
479
|
"@better-auth/drizzle-adapter": "^1.6.19",
|
|
469
480
|
"@better-auth/mongo-adapter": "^1.6.19",
|
|
470
|
-
"@biomejs/biome": "^2.5.
|
|
481
|
+
"@biomejs/biome": "^2.5.7",
|
|
471
482
|
"@classytic/dev-tools": "^0.2.0",
|
|
472
483
|
"@classytic/gitkit": ">=0.1.0",
|
|
473
|
-
"@classytic/mongokit": "^3.
|
|
484
|
+
"@classytic/mongokit": "^3.33.0",
|
|
474
485
|
"@classytic/pgkit": "^0.1.0",
|
|
475
|
-
"@classytic/primitives": "^0.
|
|
476
|
-
"@classytic/repo-core": "^0.
|
|
477
|
-
"@classytic/sqlitekit": "^0.
|
|
486
|
+
"@classytic/primitives": "^0.23.0",
|
|
487
|
+
"@classytic/repo-core": "^0.24.0",
|
|
488
|
+
"@classytic/sqlitekit": "^0.9.0",
|
|
478
489
|
"@classytic/streamline": "^2.8.2",
|
|
479
490
|
"@electric-sql/pglite": "^0.5.4",
|
|
480
491
|
"@fastify/cors": "^11.2.0",
|
|
@@ -494,21 +505,21 @@
|
|
|
494
505
|
"@vitest/coverage-v8": "^4.1.9",
|
|
495
506
|
"ajv": "^8.20.0",
|
|
496
507
|
"better-auth": "^1.6.25",
|
|
497
|
-
"better-sqlite3": "^
|
|
498
|
-
"bullmq": "^
|
|
508
|
+
"better-sqlite3": "^13.0.3",
|
|
509
|
+
"bullmq": "^6.1.2",
|
|
499
510
|
"dotenv": "^17.4.2",
|
|
500
511
|
"drizzle-orm": "^0.45.2",
|
|
501
512
|
"fast-check": "^4.8.0",
|
|
502
|
-
"fastify": "^5.
|
|
503
|
-
"fastify-raw-body": "^
|
|
513
|
+
"fastify": "^5.12.0",
|
|
514
|
+
"fastify-raw-body": "^6.0.1",
|
|
504
515
|
"h3-js": "^4.4.0",
|
|
505
|
-
"ioredis": "^
|
|
516
|
+
"ioredis": "^6.0.0",
|
|
506
517
|
"jose": "^6.2.3",
|
|
507
518
|
"jsonwebtoken": "^9.0.3",
|
|
508
|
-
"knip": "^6.
|
|
519
|
+
"knip": "^6.31.0",
|
|
509
520
|
"mongodb": "^7.3.0",
|
|
510
521
|
"mongodb-memory-server": "^11.2.0",
|
|
511
|
-
"mongoose": "^9.
|
|
522
|
+
"mongoose": "^9.9.1",
|
|
512
523
|
"publint": "^0.3.21",
|
|
513
524
|
"tsdown": "^0.22.14",
|
|
514
525
|
"typebox": "^1.3.6",
|
package/skills/arc/SKILL.md
CHANGED
|
@@ -69,6 +69,14 @@ await app.listen({ port: 8040, host: '0.0.0.0' });
|
|
|
69
69
|
|
|
70
70
|
**Boot order (fixed):** `beforeBoot` → modules resolve → `plugins` → `bootstrap[]` → `resources` → `afterResources` → `onReady`.
|
|
71
71
|
|
|
72
|
+
**`role` — one image, N deployments (2.34).** `createApp({ role })` declares what a process is FOR, so the same codebase ships as api / worker / relay / scheduler without per-deployment builds:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
createApp({ role: process.env.ARC_ROLE as 'api' | 'worker' | 'relay' | 'scheduler' | 'all' })
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`api` and `worker` mount NO module schedule arms; `relay` mounts only `kind: 'relay'` arms (the outbox relay, durable webhook delivery); `scheduler` mounts all of them. Declaring `relay`/`scheduler` together with `resources`/`resourceDir` is BOOT-FATAL — a relay process must not serve routes. Default `all` is the pre-2.34 behaviour, unchanged. `createWorker()` forwards `role`; without one it still runs everything non-HTTP.
|
|
79
|
+
|
|
72
80
|
`beforeBoot()` (2.21) runs before Fastify exists and before module thunks / resource files import — connect the DB there instead of dynamic-import ordering hacks.
|
|
73
81
|
|
|
74
82
|
For async-booted engines, pass `resources` as a factory so it runs after `bootstrap[]`:
|
|
@@ -162,7 +170,7 @@ defineResource({ name: 'health', customRoutesOnly: true, routes: [...] });
|
|
|
162
170
|
defineResource({ name: 'pipeline', adapter, defaultLimit: 200, maxLimit: 500 });
|
|
163
171
|
```
|
|
164
172
|
|
|
165
|
-
|
|
173
|
+
A `?limit=` above the cap is **clamped to it**, not rejected (2.33 — see Query parsing). The effective cap rides in the OpenAPI parameter description.
|
|
166
174
|
|
|
167
175
|
## fieldRules
|
|
168
176
|
|
|
@@ -304,12 +312,18 @@ Decorates `app.authenticate` / `app.optionalAuthenticate` / `app.authorize`.
|
|
|
304
312
|
## Authoritative gotchas
|
|
305
313
|
|
|
306
314
|
- **Field-write reject (default).** Requests carrying non-writable fields → 403 with denied list. Opt into silent strip: `defineResource({ onFieldWriteDenied: 'strip' })`.
|
|
315
|
+
- **Immutable writes are STRIPPED, not refused (default).** `PATCH {"type":"rent"}` on an `immutable` field returns 200 with the value unchanged. Opt into 403: `defineResource({ onImmutableWrite: 'reject' })`, or `ARC_STRICT_IMMUTABLE_WRITES=true`. Separate from `onFieldWriteDenied` because a full-object PATCH echoing an immutable field back UNCHANGED is legitimate, and the sanitizer has no stored document to tell that from a real change — turn it on if you send partial patches.
|
|
316
|
+
- **Log redaction is REPLACED, not merged.** Setting `logger.redact` makes yours the whole policy; arc's defaults stop applying. Superset them: `import { DEFAULT_LOGGER_REDACT_PATHS } from '@classytic/arc/factory'` and spread it into your list, rather than hand-copying (a copy drifts — one host's had lost `*.passwordHash`).
|
|
317
|
+
- **Error disclosure derives from `preset`, falling back to the environment.** `includeStack` and `exposeInternalMessages` are both derived, and host keys MERGE over them — stating one no longer resets the other. Direct `errorHandlerPlugin` registration skips the preset derivation entirely.
|
|
307
318
|
- **multiTenant injects org on UPDATE.** Body `organizationId` is overwritten with caller's scope (closes tenant-hop).
|
|
308
319
|
- **`request.user`** is `Record<string, unknown> | undefined`. Guard with `if (req.user)` on public routes.
|
|
309
320
|
- **Arc reads singular `user.role`** (string, comma-separated, or array). Don't put a plural `roles` field on the model.
|
|
310
321
|
- **`verifySignature(body, …)`** throws on parsed body — pass `req.rawBody`.
|
|
311
322
|
- **MCP tools fail-closed.** A resource action without per-action perm + no `actionPermissions` + no `permissions.update` fallback → throws at tool generation. Declare `allowPublic()` to opt into unauthenticated.
|
|
312
323
|
- **Better Auth roles.** BA stores `member.role = "admin,recruiter"` (comma-separated). Arc splits into `scope.orgRoles = ['admin', 'recruiter']`; `?role=admin` won't match — use `role[like]=admin`.
|
|
324
|
+
- **Read-only repositories refuse write ROUTES at boot (2.34).** Kits' Better Auth overlays are sealed by default (`capabilities.readOnly`) — BA owns hashing, session revocation, org cascades. Mounting `create`/`update`/`delete` over one is boot-fatal: disable those routes, or pass the kit's `unsafeWritable: true` for administrative repair only. Normal identity mutations go through `auth.api`.
|
|
325
|
+
- **Arc closes only stores/transports it BUILT (2.34).** One you hand to `idempotencyPlugin` / `eventPlugin` / `queryCachePlugin` stays yours to close — arc used to close every one, which wiped a co-resident app's live idempotency records.
|
|
326
|
+
- **Idempotency self-checks its store at boot (2.34).** A probe write must read back under its own key and NOT under another; registration throws otherwise. Catches a filter-stripping repository (a pathless Mongoose schema under `strictQuery: true` strips filters to `{}`), which otherwise replays cached responses across keys AND users. `selfCheck: false` to disable; the adapter's per-read identity checks remain.
|
|
313
327
|
|
|
314
328
|
## routeGuards + defineGuard
|
|
315
329
|
|
|
@@ -369,6 +383,28 @@ defineResource({
|
|
|
369
383
|
});
|
|
370
384
|
```
|
|
371
385
|
|
|
386
|
+
**An unknown filter key is DROPPED by default, and dropping WIDENS.** `?filters[status]=pending`
|
|
387
|
+
against a parser that does not whitelist `status` returns 200 with every row unfiltered — the
|
|
388
|
+
caller asked to narrow and got a broader set. Refuse instead:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
new QueryParser({ allowedFilterFields: [...], strictFilterFields: true }) // 400 ValidationError
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Off by default (an unconditional throw would reject requests that work today). Fleet-wide via
|
|
395
|
+
`ARC_STRICT_QUERY_PARAMS=true`; an explicit `false` beats the env. Only the FILTER whitelist —
|
|
396
|
+
a dropped sort or operator degrades ordering, it does not widen the row set.
|
|
397
|
+
|
|
398
|
+
**`?limit` above the cap CLAMPS, it does not 400** (2.33). The parser and `QueryResolver` both
|
|
399
|
+
clamp, and the querystring schema no longer emits `maximum` — it used to reject first, making the
|
|
400
|
+
documented clamp unreachable and turning a benign over-ask into a 400 that callers render as an
|
|
401
|
+
empty list. The cap is in the parameter description instead, so OpenAPI still shows it.
|
|
402
|
+
|
|
403
|
+
**One cap, one owner.** Page size is capped by the parser, the repository's pagination engine and
|
|
404
|
+
arc — lowest wins silently. Arc defers to the parser's `maxLimit` when the host states none, and
|
|
405
|
+
WARNS at boot when the parser allows more than the repository (`pagination-cap-mismatch`).
|
|
406
|
+
Configure both to the same number.
|
|
407
|
+
|
|
372
408
|
## Aggregations
|
|
373
409
|
|
|
374
410
|
`GET /:prefix/aggregations/:name` per entry. Portable `$match → $group → $project → $sort → $limit` against `repo.aggregate(req, options)` — same shape across kits.
|
|
@@ -453,6 +489,31 @@ defineResource({
|
|
|
453
489
|
|
|
454
490
|
POST/PATCH/DELETE bumps resource version. Response header: `x-cache: HIT | STALE | MISS`. `runtime: 'distributed'` requires `stores.queryCache: RedisCacheStore`.
|
|
455
491
|
|
|
492
|
+
## Transactional writes (2.34)
|
|
493
|
+
|
|
494
|
+
`transactional: true` runs the PERSISTENCE step inside `withTransaction`. Write verbs receive the tx-bound repository plus `ctx.uow` — the join point for work outside the repository:
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
defineResource({
|
|
498
|
+
name: 'order',
|
|
499
|
+
adapter,
|
|
500
|
+
transactional: true,
|
|
501
|
+
writes: {
|
|
502
|
+
create: async (data, ctx) => {
|
|
503
|
+
const order = await ctx.repository.create(data); // tx-bound
|
|
504
|
+
await outbox.store(event, { session: ctx.uow?.session }); // SAME transaction
|
|
505
|
+
return order;
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
- Hooks stay OUTSIDE the retry — before/after run once; only persistence re-runs.
|
|
512
|
+
- Transient conflicts re-run with jittered backoff; `VersionConflictError` surfaces (409), never retried.
|
|
513
|
+
- **Retry authority is the KIT's.** mongokit declares `transactionRetry: 'managed'` (its driver already retries internally) and is called exactly once; sqlitekit declares `'caller'` and gets the envelope's loop. Arc never stacks a second policy.
|
|
514
|
+
- **BOOT-FATAL** without `withTransaction`, and at REGISTRATION when the repository reports `capabilities.transactions !== true` (standalone Mongo, D1, unconfirmed topology). The capability is checked at registration, not at `defineResource()`, because that usually runs at module import — before `beforeBoot()` connects.
|
|
515
|
+
- `ctx.uow.session` is empty on connection-bound backends (SQLite); the tx-bound repo is the join point there.
|
|
516
|
+
|
|
456
517
|
## Events
|
|
457
518
|
|
|
458
519
|
CRUD events auto-emit: `{resource}.created` / `{resource}.updated` / `{resource}.deleted`. Manual publish:
|
|
@@ -464,13 +525,27 @@ await app.events.subscribe('order.*', async (event) => { … });
|
|
|
464
525
|
|
|
465
526
|
Transports: Memory · Redis Pub/Sub (fire-and-forget) · Redis Streams (durable, consumer groups, DLQ). Event types live in `@classytic/primitives/events` (`createEvent`, `createChildEvent`, `matchEventPattern`, …); arc re-exports the runtime `MemoryEventTransport` only.
|
|
466
527
|
|
|
467
|
-
**Outbox** (at-least-once via transactional outbox):
|
|
528
|
+
**Outbox** (at-least-once via transactional outbox) — prefer the MODULE, so the store is a slot other modules depend on rather than a value threaded by hand:
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
createApp({ modules: [createOutboxModule({ store: repositoryAsOutboxStore(outboxRepo) })] });
|
|
532
|
+
// Dev: createOutboxModule({ store: new MemoryOutboxStore() })
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
The relay arm is `kind: 'relay'`, so `role: 'relay'` deployments drain it. It resolves the RAW transport, never the `fastify.events` facade (the facade fails open, which would acknowledge rows whose publish failed) — no transport is boot-fatal. **Fencing (2.34):** stores that support `claimPendingFenced` mint a token in the claim CAS; a relay that lost its lease mid-batch cannot ack or re-schedule a row a successor already owns. Feature-detected end to end.
|
|
536
|
+
|
|
537
|
+
**Durable webhooks (2.34)** — delivery survives crash and deploy, using the outbox machinery rather than a second queue:
|
|
468
538
|
|
|
469
539
|
```typescript
|
|
470
|
-
const
|
|
471
|
-
|
|
540
|
+
const store = repositoryAsOutboxStore(deliveryRepo);
|
|
541
|
+
createApp({
|
|
542
|
+
modules: [createDurableWebhookModule({ store })], // relay does the signed POST
|
|
543
|
+
plugins: async (f) => f.register(webhookPlugin, { durable: { store } }), // enqueue, don't POST
|
|
544
|
+
});
|
|
472
545
|
```
|
|
473
546
|
|
|
547
|
+
One row per (event × subscription), deterministic id so an at-least-once upstream enqueues each delivery exactly once. Transient failures (network/timeout/429/5xx) retry with backoff; 4xx dead-letters on the FIRST attempt. `durable` + `retry` together is a registration error.
|
|
548
|
+
|
|
474
549
|
Full event recipes (retry, DLQ, dual-publish warnings, idempotency keys) → [references/events.md](references/events.md).
|
|
475
550
|
|
|
476
551
|
## Hooks
|
|
@@ -652,6 +727,23 @@ await app.audit.custom('order', req.params.id, 'refund', { reason }, { user });
|
|
|
652
727
|
|
|
653
728
|
`@classytic/arc/usage` — `usagePlugin` decorates `fastify.usage` with per-actor, per-UTC-month counters (`record`/`summary`/`period`/`actorOf`; actor chain org → user → client → ip). Auto-tracks `api.requests` (default on) + `api.egress.bytes` (opt-in); recording is fail-safe (a throwing `UsageStore` never fails a request; `MemoryUsageStore` built in, Redis/kit backends are ~5 lines). Pair with `createApp({ rateLimit: { plan: { resolve, limits: { free: { max: 60 }, enterprise: false }, default: 'free' } } })` for per-plan ceilings — `false` = unlimited, unknown/throwing resolvers fall back to `default` then global `max`, and buckets follow the tenant key chain by default. Caveat: the limiter runs before route auth, so `resolve` sees the raw request.
|
|
654
729
|
|
|
730
|
+
## Runtime capability registry (2.34)
|
|
731
|
+
|
|
732
|
+
Under `runtime: 'distributed'`, subsystems holding process-local state must declare it — the end-of-boot audit FAILS the app on undeclared memory state, naming every violator:
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
import { declareRuntimeCapability } from '@classytic/arc/utils';
|
|
736
|
+
|
|
737
|
+
declareRuntimeCapability(fastify, {
|
|
738
|
+
subsystem: 'billing.sequence-cache',
|
|
739
|
+
durability: 'memory', // 'memory' | 'shared'
|
|
740
|
+
accepted: true, // per-process BY DESIGN (logged, never fatal)
|
|
741
|
+
detail: 'short-TTL cache; correctness from TTL, not shared state',
|
|
742
|
+
});
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
`createApp`'s constructor-time guard only sees what it is passed; state a host wires inside `plugins()` was invisible to it and lived on a checklist. This makes the checklist enforcement. Arc's webhooks plugin declares its default in-memory store.
|
|
746
|
+
|
|
655
747
|
## Adapters
|
|
656
748
|
|
|
657
749
|
Cross-framework adapter contract lives in `@classytic/repo-core/adapter`. Every kit-specific adapter ships from its kit's `/adapter` subpath; arc itself has zero kit-bound adapters.
|
|
@@ -126,8 +126,27 @@ import {
|
|
|
126
126
|
import { tracingPlugin } from '@classytic/arc/plugins/tracing';
|
|
127
127
|
import { auditPlugin } from '@classytic/arc/audit';
|
|
128
128
|
import { idempotencyPlugin } from '@classytic/arc/idempotency';
|
|
129
|
+
// Runtime capability registry (2.34) — declare process-local state so a
|
|
130
|
+
// `runtime: 'distributed'` boot fails on undeclared memory state:
|
|
131
|
+
import { declareRuntimeCapability } from '@classytic/arc/utils';
|
|
129
132
|
```
|
|
130
133
|
|
|
134
|
+
**Idempotency store — the one schema mistake that matters.** Backing the store
|
|
135
|
+
with a repository requires the key path to survive the filter. A Mongoose
|
|
136
|
+
schema with NO declared paths under a global `strictQuery: true` has EVERY
|
|
137
|
+
filter key stripped, so `getOne({ _id: key })` becomes `getOne({})`: one row
|
|
138
|
+
absorbs every key and responses replay across keys AND users. Declare the path
|
|
139
|
+
and pin the option on the schema:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
new Schema({ _id: String }, { strict: false, strictQuery: false, timestamps: false })
|
|
143
|
+
// NOT `new Schema({}, { _id: false, strict: false })`
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The plugin's boot self-check (default on, `selfCheck: false` to disable) probes
|
|
147
|
+
for exactly this and refuses to register; the adapter also throws
|
|
148
|
+
`IdempotencyStoreMisconfiguredError` on any cross-key read at runtime.
|
|
149
|
+
|
|
131
150
|
## Integrations
|
|
132
151
|
|
|
133
152
|
```typescript
|
|
@@ -292,7 +292,7 @@ field masking. Client = plain `EventSource` with `?token=`. No replay — reconn
|
|
|
292
292
|
refetches the list. Choosing: per-user resource views → realtime; bidirectional/geo
|
|
293
293
|
→ `integrations/websocket` (its CRUD bridge is org-coarse — never expose
|
|
294
294
|
row-permissioned resources through it); event firehose → sse. Massive fan-out
|
|
295
|
-
(viral feeds, live scores) → publish once to
|
|
295
|
+
(viral feeds, live scores) → publish once to a dedicated edge fan-out layer
|
|
296
296
|
via `app.events.subscribe(...)`; full tiering guide: docs/production-ops/realtime.mdx.
|
|
297
297
|
|
|
298
298
|
```typescript
|
|
@@ -618,7 +618,7 @@ presets: [bulkPreset({ operations: ['createMany', 'updateMany'], maxCreateItems:
|
|
|
618
618
|
## Compensating Transaction
|
|
619
619
|
|
|
620
620
|
In-process rollback primitive — runs steps in order, compensates in reverse on failure.
|
|
621
|
-
For distributed sagas across services, use
|
|
621
|
+
For distributed sagas across services, use `@classytic/streamline` or a dedicated workflow orchestrator.
|
|
622
622
|
|
|
623
623
|
```typescript
|
|
624
624
|
import { withCompensation } from '@classytic/arc/utils';
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { hn as HookSystem, z as ResourceRegistry } from "./index-CpzxqePp.mjs";
|
|
3
|
-
import { t as ExternalOpenApiPaths } from "./externalPaths-CmGOxOtD.mjs";
|
|
4
|
-
import { h as HealthCheck, r as ArcModuleRegistry, t as ArcModule, u as ScheduleDefinition } from "./types-CBgvkp8O.mjs";
|
|
5
|
-
import { s as ModuleState } from "./index-CFThn1K9.mjs";
|
|
6
|
-
import { FastifyPluginAsync } from "fastify";
|
|
7
|
-
//#region src/core/arcCorePlugin.d.ts
|
|
8
|
-
interface ArcCorePluginOptions {
|
|
9
|
-
/** Enable event emission for CRUD operations (requires eventPlugin) */
|
|
10
|
-
emitEvents?: boolean;
|
|
11
|
-
/** Hook system instance (for testing/custom setup) */
|
|
12
|
-
hookSystem?: HookSystem;
|
|
13
|
-
/** Resource registry instance (for testing/custom setup) */
|
|
14
|
-
registry?: ResourceRegistry;
|
|
15
|
-
}
|
|
16
|
-
interface PluginMeta {
|
|
17
|
-
name: string;
|
|
18
|
-
version?: string;
|
|
19
|
-
options?: Record<string, unknown>;
|
|
20
|
-
registeredAt: string;
|
|
21
|
-
}
|
|
22
|
-
interface ArcCore {
|
|
23
|
-
/** Instance-scoped hook system */
|
|
24
|
-
hooks: HookSystem;
|
|
25
|
-
/** Instance-scoped resource registry */
|
|
26
|
-
registry: ResourceRegistry;
|
|
27
|
-
/** Whether event emission is enabled */
|
|
28
|
-
emitEvents: boolean;
|
|
29
|
-
/** External OpenAPI paths contributed by auth adapters or third-party integrations */
|
|
30
|
-
externalOpenApiPaths: ExternalOpenApiPaths[];
|
|
31
|
-
/** Registered plugins for introspection */
|
|
32
|
-
plugins: Map<string, PluginMeta>;
|
|
33
|
-
/**
|
|
34
|
-
* Module public exports — `bootstrap()` return values keyed by module name
|
|
35
|
-
* (see `src/factory/module.ts`). Read via `getModuleExports(fastify, name)`:
|
|
36
|
-
* augment `ArcModuleRegistry` once and the name infers the export type;
|
|
37
|
-
* otherwise pass it inline. `Partial<ArcModuleRegistry>` gives augmented
|
|
38
|
-
* apps typed direct access (`fastify.arc.modules.order`), while the
|
|
39
|
-
* `Record<string, unknown>` intersection keeps un-augmented reads open.
|
|
40
|
-
* Populated lazily by `registerResources` — absent until a module exports.
|
|
41
|
-
*/
|
|
42
|
-
modules?: Partial<ArcModuleRegistry> & Record<string, unknown>;
|
|
43
|
-
/**
|
|
44
|
-
* Lifecycle state per composed module (null-proto map, populated by
|
|
45
|
-
* `registerResources` the moment the graph is validated — so it answers
|
|
46
|
-
* "is this module composed?" even for resource-only modules that never
|
|
47
|
-
* record an export). Read via `hasModule(fastify, name)` /
|
|
48
|
-
* `getModuleState(fastify, name)`; the map itself is internal.
|
|
49
|
-
*/
|
|
50
|
-
moduleStates?: Record<string, ModuleState>;
|
|
51
|
-
/**
|
|
52
|
-
* Module-contributed readiness checks, collected in dependency order and
|
|
53
|
-
* frozen by `createApp`. The API role merges these into its `healthPlugin`;
|
|
54
|
-
* `createWorker` reads this to give its probe listener the identical union
|
|
55
|
-
* (the worker preset disables the main health plugin, so this is how module
|
|
56
|
-
* checks reach the probe without a double registration). Empty until
|
|
57
|
-
* `createApp` populates it.
|
|
58
|
-
*/
|
|
59
|
-
healthChecks?: readonly HealthCheck[];
|
|
60
|
-
/**
|
|
61
|
-
* The resolved, dependency-ordered module definitions (frozen by createApp).
|
|
62
|
-
* Integrations that collect a module arm at their OWN init time — e.g. the
|
|
63
|
-
* streamline integration calling `collectModuleWorkflows(fastify, defs)` after
|
|
64
|
-
* it has created + decorated its container — read the list from here. Empty
|
|
65
|
-
* until createApp populates it.
|
|
66
|
-
*/
|
|
67
|
-
moduleDefinitions?: readonly ArcModule[];
|
|
68
|
-
/**
|
|
69
|
-
* Module-contributed recurring schedules, resolved once in dependency order
|
|
70
|
-
* and frozen after bootstraps. The same definitions are executed by Arc's
|
|
71
|
-
* schedules plugin and exposed here for introspection. Empty until collection.
|
|
72
|
-
*/
|
|
73
|
-
scheduledJobs?: readonly Readonly<ScheduleDefinition>[];
|
|
74
|
-
}
|
|
75
|
-
declare const arcCorePlugin: FastifyPluginAsync<ArcCorePluginOptions>;
|
|
76
|
-
declare const _default: FastifyPluginAsync<ArcCorePluginOptions>;
|
|
77
|
-
//#endregion
|
|
78
|
-
//#region src/types/fastify-augmentation.d.ts
|
|
79
|
-
declare module "fastify" {
|
|
80
|
-
interface FastifyInstance {
|
|
81
|
-
arc?: ArcCore;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
//#endregion
|
|
85
|
-
export { arcCorePlugin as a, _default as i, ArcCorePluginOptions as n, PluginMeta as r, ArcCore as t };
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/*! @classytic/arc v2.31.0 | MIT | https://classytic.com */
|
|
2
|
-
import { c as getOrgId, g as getUserId, x as isMember } from "./types-Cys0ko9Z.mjs";
|
|
3
|
-
//#region src/scope/projection.ts
|
|
4
|
-
/**
|
|
5
|
-
* Compute the request-scope projection. Returns `undefined` when no
|
|
6
|
-
* scope is attached (public / unscoped routes) so hosts can idiomatically
|
|
7
|
-
* write `ctx.scope?.organizationId` without a double-null check.
|
|
8
|
-
*/
|
|
9
|
-
function buildRequestScopeProjection(scope) {
|
|
10
|
-
if (!scope) return void 0;
|
|
11
|
-
return {
|
|
12
|
-
organizationId: getOrgId(scope),
|
|
13
|
-
userId: getUserId(scope),
|
|
14
|
-
orgRoles: isMember(scope) ? scope.orgRoles : void 0
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
//#endregion
|
|
18
|
-
export { buildRequestScopeProjection as t };
|