@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,12 +1,43 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { n as requireSingleHeaderValue } from "../headers-
|
|
3
|
-
import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-
|
|
4
|
-
import { t as createIdempotencyResult } from "../interface-
|
|
1
|
+
/*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
|
|
2
|
+
import { n as requireSingleHeaderValue } from "../headers-CveNO9gR.mjs";
|
|
3
|
+
import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-B88p99Z2.mjs";
|
|
4
|
+
import { t as createIdempotencyResult } from "../interface-DznJrm9R.mjs";
|
|
5
5
|
import { and, eq, exists, gt, lt, or, startsWith } from "@classytic/repo-core/filter";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import fp from "fastify-plugin";
|
|
8
8
|
import { update } from "@classytic/repo-core/update";
|
|
9
9
|
//#region src/idempotency/repository-idempotency-adapter.ts
|
|
10
|
+
/**
|
|
11
|
+
* The backing repository returned a document whose key does not match the
|
|
12
|
+
* key that was asked for — which means the repository is NOT applying the
|
|
13
|
+
* key filter. Concretely observed (2026-08-14, be-prod): a Mongoose schema
|
|
14
|
+
* with no declared paths (`new Schema({}, { _id: false })`) under a global
|
|
15
|
+
* `mongoose.set('strictQuery', true)` strips EVERY filter key as an
|
|
16
|
+
* "unknown path", so `getOne({ _id: key })` becomes `getOne({})` — one
|
|
17
|
+
* shared document then absorbs every idempotency key, and every request
|
|
18
|
+
* replays the most recent cached response ACROSS KEYS AND ACROSS USERS
|
|
19
|
+
* (a cancel response for order A was served as "success" for order B,
|
|
20
|
+
* which was never actually canceled).
|
|
21
|
+
*
|
|
22
|
+
* That failure mode is silent by construction — every read "succeeds" with
|
|
23
|
+
* a plausible document — so the adapter verifies the identity of every
|
|
24
|
+
* document it gets back and THROWS this instead of trusting it. Returning
|
|
25
|
+
* `undefined` (treat as miss) would be worse, not safer: the very next
|
|
26
|
+
* `tryLock`/`set` on the same broken repository would corrupt the shared
|
|
27
|
+
* document again.
|
|
28
|
+
*
|
|
29
|
+
* Fix on the host side: declare the key path on the schema (e.g.
|
|
30
|
+
* `_id: String`) and/or set `strictQuery: false` in the SCHEMA options so
|
|
31
|
+
* filters on it are never stripped — see `idempotencyPlugin`'s
|
|
32
|
+
* `repository` option docs.
|
|
33
|
+
*/
|
|
34
|
+
var IdempotencyStoreMisconfiguredError = class extends Error {
|
|
35
|
+
code = "IDEMPOTENCY_STORE_MISCONFIGURED";
|
|
36
|
+
constructor(operation, expectedKey, actualKey) {
|
|
37
|
+
super(`idempotency repository store: ${operation} for key '${expectedKey}' returned a document keyed '${String(actualKey)}' — the repository is not applying the key filter (commonly: a pathless Mongoose schema under strictQuery:true strips the filter to {}). Declare the key path on the schema (e.g. _id: String) and set strictQuery: false in the schema options. Refusing to serve cross-key results.`);
|
|
38
|
+
this.name = "IdempotencyStoreMisconfiguredError";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
10
41
|
function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
11
42
|
const missing = [];
|
|
12
43
|
if (typeof repository.getOne !== "function") missing.push("getOne");
|
|
@@ -17,11 +48,23 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
|
17
48
|
const idField = repository.idField ?? "_id";
|
|
18
49
|
const isDuplicateKeyError = createIsDuplicateKeyError(repository);
|
|
19
50
|
const safeGetOne = createSafeGetOne(repository);
|
|
51
|
+
/**
|
|
52
|
+
* Every document read back MUST carry the key it was fetched by. A
|
|
53
|
+
* mismatch means the repository dropped the filter (see
|
|
54
|
+
* {@link IdempotencyStoreMisconfiguredError}) — fail loud, never serve
|
|
55
|
+
* or mutate a cross-key document.
|
|
56
|
+
*/
|
|
57
|
+
function assertKeyIdentity(operation, doc, expectedKey) {
|
|
58
|
+
const actual = doc[idField];
|
|
59
|
+
if (String(actual) !== expectedKey) throw new IdempotencyStoreMisconfiguredError(operation, expectedKey, actual);
|
|
60
|
+
}
|
|
20
61
|
return {
|
|
21
62
|
name: "repository",
|
|
22
63
|
async get(key) {
|
|
23
64
|
const doc = await safeGetOne(eq(idField, key));
|
|
24
|
-
if (!doc
|
|
65
|
+
if (!doc) return void 0;
|
|
66
|
+
assertKeyIdentity("get", doc, key);
|
|
67
|
+
if (!doc.result) return void 0;
|
|
25
68
|
if (new Date(doc.expiresAt) < /* @__PURE__ */ new Date()) return void 0;
|
|
26
69
|
return {
|
|
27
70
|
key,
|
|
@@ -33,7 +76,7 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
|
33
76
|
};
|
|
34
77
|
},
|
|
35
78
|
async set(key, result) {
|
|
36
|
-
await r.findOneAndUpdate(eq(idField, key), update({
|
|
79
|
+
const doc = await r.findOneAndUpdate(eq(idField, key), update({
|
|
37
80
|
set: {
|
|
38
81
|
result: {
|
|
39
82
|
statusCode: result.statusCode,
|
|
@@ -43,11 +86,13 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
|
43
86
|
createdAt: result.createdAt,
|
|
44
87
|
expiresAt: result.expiresAt
|
|
45
88
|
},
|
|
89
|
+
setOnInsert: { [idField]: key },
|
|
46
90
|
unset: ["lock"]
|
|
47
91
|
}), {
|
|
48
92
|
upsert: true,
|
|
49
93
|
returnDocument: "after"
|
|
50
94
|
});
|
|
95
|
+
if (doc) assertKeyIdentity("set", doc, key);
|
|
51
96
|
},
|
|
52
97
|
async tryLock(key, requestId, ttlMs) {
|
|
53
98
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -60,6 +105,7 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
|
60
105
|
expiresAt: lockExpiresAt
|
|
61
106
|
} },
|
|
62
107
|
setOnInsert: {
|
|
108
|
+
[idField]: key,
|
|
63
109
|
createdAt: now,
|
|
64
110
|
expiresAt: docExpiresAt
|
|
65
111
|
}
|
|
@@ -67,6 +113,7 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
|
67
113
|
upsert: true,
|
|
68
114
|
returnDocument: "after"
|
|
69
115
|
});
|
|
116
|
+
if (doc) assertKeyIdentity("tryLock", doc, key);
|
|
70
117
|
return doc !== null && doc !== void 0;
|
|
71
118
|
} catch (err) {
|
|
72
119
|
if (isDuplicateKeyError(err)) return false;
|
|
@@ -90,6 +137,7 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
|
90
137
|
async findByPrefix(prefix) {
|
|
91
138
|
const doc = await safeGetOne(and(startsWith(idField, prefix, "sensitive"), exists("result", true), gt("expiresAt", /* @__PURE__ */ new Date())));
|
|
92
139
|
if (!doc?.result) return void 0;
|
|
140
|
+
if (!String(doc[idField] ?? "").startsWith(prefix)) throw new IdempotencyStoreMisconfiguredError("findByPrefix", prefix, doc[idField]);
|
|
93
141
|
return {
|
|
94
142
|
key: String(doc[idField] ?? prefix),
|
|
95
143
|
statusCode: doc.result.statusCode,
|
|
@@ -138,8 +186,10 @@ var MemoryIdempotencyStore = class {
|
|
|
138
186
|
}
|
|
139
187
|
async tryLock(key, requestId, ttlMs) {
|
|
140
188
|
const existing = this.locks.get(key);
|
|
141
|
-
if (existing)
|
|
142
|
-
|
|
189
|
+
if (existing) {
|
|
190
|
+
if (/* @__PURE__ */ new Date() > existing.expiresAt) this.locks.delete(key);
|
|
191
|
+
else return false;
|
|
192
|
+
}
|
|
143
193
|
this.locks.set(key, {
|
|
144
194
|
key,
|
|
145
195
|
requestId,
|
|
@@ -285,8 +335,42 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
|
|
|
285
335
|
"POST",
|
|
286
336
|
"PUT",
|
|
287
337
|
"PATCH"
|
|
288
|
-
], include, exclude, repository, store: explicitStore, retryAfterSeconds = 1, namespace } = opts;
|
|
338
|
+
], include, exclude, repository, store: explicitStore, retryAfterSeconds = 1, namespace, selfCheck = true } = opts;
|
|
289
339
|
const store = repository ? repositoryAsIdempotencyStore(repository, ttlMs) : explicitStore ?? new MemoryIdempotencyStore({ ttlMs });
|
|
340
|
+
/**
|
|
341
|
+
* Who owns the store's lifecycle — arc closes ONLY what arc built.
|
|
342
|
+
*
|
|
343
|
+
* A host-supplied `store` may outlive this app: two arc apps in one process
|
|
344
|
+
* (a documented topology) can share one, and `MemoryIdempotencyStore.close()`
|
|
345
|
+
* CLEARS its maps. Closing on the first app's shutdown therefore wiped the
|
|
346
|
+
* second app's live idempotency records — replayable requests silently
|
|
347
|
+
* became re-executable. A store handed in from outside is the host's to
|
|
348
|
+
* close. The same rule now applies to the events transport and the
|
|
349
|
+
* query-cache store, which had the identical defect.
|
|
350
|
+
*
|
|
351
|
+
* The `repository` path builds its own adapter here, so arc owns that
|
|
352
|
+
* wrapper — but the adapter has no `close`, and the underlying repository
|
|
353
|
+
* (host-owned) is never touched.
|
|
354
|
+
*/
|
|
355
|
+
const ownsStore = !explicitStore;
|
|
356
|
+
if (enabled && selfCheck) {
|
|
357
|
+
const probeSuffix = createHash("sha256").update(`${process.pid}:${Date.now()}:${Math.random()}`).digest("hex").slice(0, 12);
|
|
358
|
+
const probeKeyA = `__arc_idempotency_selfcheck__:${probeSuffix}:a`;
|
|
359
|
+
const probeKeyB = `__arc_idempotency_selfcheck__:${probeSuffix}:b`;
|
|
360
|
+
/** Correctness verdicts rethrow; everything else degrades to a warning. */
|
|
361
|
+
class SelfCheckViolation extends Error {}
|
|
362
|
+
try {
|
|
363
|
+
await store.set(probeKeyA, createIdempotencyResult(299, { probe: true }, {}, 6e4));
|
|
364
|
+
const own = await store.get(probeKeyA);
|
|
365
|
+
if (!own || own.statusCode !== 299) fastify.log?.warn?.({ probe: "own-key" }, "idempotencyPlugin self-check: probe entry did not read back under its own key. This is usually the store being unreachable at boot (connection not ready), not a misconfiguration — continuing, with the adapter's per-read key-identity checks as the runtime backstop. If keyed requests later fail with IDEMPOTENCY_STORE_MISCONFIGURED, the store's key filter is being stripped (see IdempotencyStoreMisconfiguredError).");
|
|
366
|
+
else if (await store.get(probeKeyB)) throw new SelfCheckViolation("idempotencyPlugin self-check: a probe read under key B returned key A's entry — the store is ignoring the key filter, which in production replays cached responses across keys and users (observed cause: a pathless Mongoose schema under strictQuery:true strips every filter to {}). Declare the key path on the schema (e.g. _id: String) and set strictQuery: false in the schema options.");
|
|
367
|
+
} catch (err) {
|
|
368
|
+
if (err instanceof SelfCheckViolation || err?.code === "IDEMPOTENCY_STORE_MISCONFIGURED") throw err;
|
|
369
|
+
fastify.log?.warn?.({ err }, "idempotencyPlugin self-check could not complete (store unreachable at boot?) — continuing. The adapter's per-read key-identity checks remain the runtime backstop.");
|
|
370
|
+
} finally {
|
|
371
|
+
await store.deleteByPrefix(`__arc_idempotency_selfcheck__:${probeSuffix}:`).catch(() => {});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
290
374
|
if (!enabled) {
|
|
291
375
|
fastify.decorate("idempotency", {
|
|
292
376
|
invalidate: async () => {},
|
|
@@ -429,7 +513,7 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
|
|
|
429
513
|
await store.unlock(fullKey, request.id);
|
|
430
514
|
});
|
|
431
515
|
fastify.addHook("onClose", async () => {
|
|
432
|
-
await store.close?.();
|
|
516
|
+
if (ownsStore) await store.close?.();
|
|
433
517
|
});
|
|
434
518
|
fastify.log?.debug?.({
|
|
435
519
|
headerName,
|
|
@@ -442,4 +526,4 @@ var idempotencyPlugin_default = fp(idempotencyPlugin, {
|
|
|
442
526
|
fastify: "5.x"
|
|
443
527
|
});
|
|
444
528
|
//#endregion
|
|
445
|
-
export { MemoryIdempotencyStore, createIdempotencyResult, idempotencyPlugin_default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn, repositoryAsIdempotencyStore };
|
|
529
|
+
export { IdempotencyStoreMisconfiguredError, MemoryIdempotencyStore, createIdempotencyResult, idempotencyPlugin_default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn, repositoryAsIdempotencyStore };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
|
-
import { r as CacheStore, t as CacheLogger } from "./interface-BOk3YnTz.mjs";
|
|
3
2
|
import { i as RequestScope, n as Mandate } from "./types-ChuShpCP.mjs";
|
|
4
|
-
import {
|
|
3
|
+
import { l as PermissionCheck, s as AuthorizationDecision, u as PermissionContext } from "./base-Csjc2oF-.mjs";
|
|
4
|
+
import { r as CacheStore, t as CacheLogger } from "./interface-BOk3YnTz.mjs";
|
|
5
5
|
import { FastifyReply, FastifyRequest } from "fastify";
|
|
6
6
|
//#region src/permissions/agent.d.ts
|
|
7
7
|
/**
|
|
@@ -990,4 +990,83 @@ declare function fullPublic<TDoc = Record<string, unknown>>(overrides?: Permissi
|
|
|
990
990
|
*/
|
|
991
991
|
declare function readOnly<TDoc = Record<string, unknown>>(overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
992
992
|
//#endregion
|
|
993
|
-
|
|
993
|
+
//#region src/permissions/matrix.d.ts
|
|
994
|
+
type PermissionEntryType = "public" | "authenticated" | "roles" | "scoped";
|
|
995
|
+
interface PermissionEntry {
|
|
996
|
+
type: PermissionEntryType;
|
|
997
|
+
roles: string[];
|
|
998
|
+
/**
|
|
999
|
+
* Present only for `scoped` — the caller-scope dimensions the gate requires
|
|
1000
|
+
* (e.g. `{ branchRole: "head_office" }`), for the client to resolve against
|
|
1001
|
+
* the viewer's active context.
|
|
1002
|
+
*/
|
|
1003
|
+
scope?: Record<string, unknown>;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* The slice of arc's `ResourceRegistry` this reads.
|
|
1007
|
+
*
|
|
1008
|
+
* Structural on purpose: coupling to the full registry type would drag the
|
|
1009
|
+
* resource layer into the permissions layer, and this only ever reads names
|
|
1010
|
+
* and gates.
|
|
1011
|
+
*/
|
|
1012
|
+
interface PermissionMatrixRegistry {
|
|
1013
|
+
getAll(): ReadonlyArray<{
|
|
1014
|
+
name: string;
|
|
1015
|
+
permissions?: Record<string, unknown>;
|
|
1016
|
+
actions?: ReadonlyArray<{
|
|
1017
|
+
name: string;
|
|
1018
|
+
permissions?: unknown;
|
|
1019
|
+
}>;
|
|
1020
|
+
aggregations?: ReadonlyArray<{
|
|
1021
|
+
name: string;
|
|
1022
|
+
permissions?: unknown;
|
|
1023
|
+
}>;
|
|
1024
|
+
/**
|
|
1025
|
+
* Custom routes. Only those declaring `capability` are published — see the
|
|
1026
|
+
* loop in `introspectRegistry` for why a route needs an explicit name.
|
|
1027
|
+
*/
|
|
1028
|
+
customRoutes?: ReadonlyArray<{
|
|
1029
|
+
method: string;
|
|
1030
|
+
path: string;
|
|
1031
|
+
capability?: string;
|
|
1032
|
+
permissions?: unknown;
|
|
1033
|
+
}>;
|
|
1034
|
+
}>;
|
|
1035
|
+
}
|
|
1036
|
+
interface PermissionMatrixOptions {
|
|
1037
|
+
/** The live registry. Omit and the matrix carries only curated modules. */
|
|
1038
|
+
registry?: PermissionMatrixRegistry;
|
|
1039
|
+
/**
|
|
1040
|
+
* Curated, human-named groupings the client already organises around
|
|
1041
|
+
* (`products`, `finance`, …). Overlaid FIRST — the live registry wins on an
|
|
1042
|
+
* exact name collision, because the enforced gate beats an approximation.
|
|
1043
|
+
*/
|
|
1044
|
+
curatedModules?: Record<string, unknown>;
|
|
1045
|
+
/**
|
|
1046
|
+
* Second curated layer, applied only where `curatedModules` has no entry —
|
|
1047
|
+
* typically per-resource policies behind the domain groupings.
|
|
1048
|
+
*/
|
|
1049
|
+
fallbackModules?: Record<string, unknown>;
|
|
1050
|
+
/** Platform-level role names. */
|
|
1051
|
+
platformRoles?: readonly string[];
|
|
1052
|
+
/** Organization/branch-level role names. */
|
|
1053
|
+
orgRoles?: readonly string[];
|
|
1054
|
+
}
|
|
1055
|
+
interface PermissionMatrix {
|
|
1056
|
+
/** Every role name, both namespaces — so a role picker has one source. */
|
|
1057
|
+
roles: string[];
|
|
1058
|
+
roleGroups: {
|
|
1059
|
+
platform: string[];
|
|
1060
|
+
branch: string[];
|
|
1061
|
+
};
|
|
1062
|
+
modules: Record<string, Record<string, PermissionEntry>>;
|
|
1063
|
+
}
|
|
1064
|
+
/** Re-shape arc's typed requirement into the wire entry. */
|
|
1065
|
+
declare function introspectCheck(check: PermissionCheck): PermissionEntry;
|
|
1066
|
+
/** Introspect a permission module, flattening nested groups to `a.b`. */
|
|
1067
|
+
declare function introspectModule(mod: Record<string, unknown>): Record<string, PermissionEntry>;
|
|
1068
|
+
/** Every live resource's ACTUAL gates, keyed by arc resource name. */
|
|
1069
|
+
declare function introspectRegistry(registry: PermissionMatrixRegistry): Record<string, Record<string, PermissionEntry>>;
|
|
1070
|
+
declare function buildPermissionMatrix(options?: PermissionMatrixOptions): PermissionMatrix;
|
|
1071
|
+
//#endregion
|
|
1072
|
+
export { requireAuth as $, RequireGrantOptions as A, ConnectEventsOptions as B, requireServiceScope as C, GRANT_MODES as D, createRoleHierarchy as E, PermissionRequirement as F, createOrgPermissions as G, DynamicPermissionMatrixConfig as H, collectPublicSurface as I, allowPublic as J, allOf as K, describePermission as L, requireGrant as M, conjoinPolicyFilters as N, GrantMode as O, AccessExplanation as P, not as Q, describePermissionMap as R, requireScopeContext as S, RoleHierarchy as T, PermissionEventBus as U, DynamicPermissionMatrix as V, createDynamicPermissionMatrix as W, deny as X, anyOf as Y, denyAll as Z, publicReadAdminWrite as _, PermissionMatrixRegistry as a, scopeOf as at, requireOrgMembership as b, introspectModule as c, evaluatePermissionDecision as ct, authenticated as d, RequireMandateOptions as dt, requireOwnership as et, fullPublic as f, requireAgentScope as ft, publicRead as g, presets_d_exports as h, PermissionMatrixOptions as i, when as it, modeSatisfies as j, GrantResolution as k, introspectRegistry as l, normalizeToDecision as lt, permissionMatrix as m, requireMandate as mt, PermissionEntryType as n, requireRoles as nt, buildPermissionMatrix as o, PermissionCheckReturn as ot, ownerWithAdminBypass as p, requireDPoP as pt, allow as q, PermissionMatrix as r, roles as rt, introspectCheck as s, applyAuthorizationDecision as st, PermissionEntry as t, requirePlatformRole as tt, adminOnly as u, RequireAgentScopeOptions as ut, readOnly as v, requireTeamMembership as w, requireOrgRole as x, requireOrgInScope as y, explainAccess as z };
|