@classytic/arc 2.11.3 → 2.13.1
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 +27 -18
- package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
- package/dist/EventTransport-CT_52aWU.d.mts +34 -0
- package/dist/EventTransport-DLWoUMHy.mjs +103 -0
- package/dist/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
- package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/audit.d.mts +199 -0
- package/dist/auth/audit.mjs +288 -0
- package/dist/auth/index.d.mts +5 -5
- package/dist/auth/index.mjs +117 -191
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
- package/dist/buildHandler-olo-gt94.mjs +610 -0
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/describe.d.mts +89 -13
- package/dist/cli/commands/describe.mjs +56 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +147 -48
- package/dist/cli/commands/init.d.mts +13 -0
- package/dist/cli/commands/init.mjs +237 -112
- package/dist/cli/commands/introspect.mjs +8 -1
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/core-D72ia0EH.mjs +1399 -0
- package/dist/{createActionRouter-u3ql2EDo.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
- package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
- package/dist/{createApp-BFxtdKy6.mjs → createApp-XX2-N0Yd.mjs} +31 -27
- package/dist/defineEvent-D5h7EvAx.mjs +188 -0
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
- package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
- package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
- package/dist/errors-j4aJm1Wg.mjs +184 -0
- package/dist/{eventPlugin-KrFIQ097.mjs → eventPlugin-CaKTYkYM.mjs} +35 -137
- package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-qXpqTebY.d.mts} +57 -7
- package/dist/events/index.d.mts +164 -5
- package/dist/events/index.mjs +133 -209
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +204 -31
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +2 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-C8Y0XLAu.d.mts → fields-COhcH3fk.d.mts} +23 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/index.mjs +1 -20
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-BYCqHCVu.d.mts → index-BTqLEvhu.d.mts} +164 -4
- package/dist/{index-6u4_Gg6G.d.mts → index-BtW7qYwa.d.mts} +661 -281
- package/dist/{index-BdXnTPRj.d.mts → index-Ds61mrJE.d.mts} +50 -4
- package/dist/{index-DdQ3O9Pg.d.mts → index-Dz5IKsrE.d.mts} +360 -219
- package/dist/index.d.mts +6 -7
- package/dist/index.mjs +9 -10
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +60 -11
- package/dist/integrations/streamline.mjs +75 -85
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +2 -8
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +2 -2
- package/dist/migrations/index.d.mts +23 -3
- package/dist/migrations/index.mjs +0 -7
- package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
- package/dist/{openapi-BGUn7Ki1.mjs → openapi-CiOMVW1p.mjs} +143 -13
- package/dist/org/index.d.mts +2 -2
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
- package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +18 -33
- package/dist/plugins/index.mjs +33 -13
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +5 -5
- package/dist/presets/filesUpload.mjs +6 -9
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +2 -2
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +6 -8
- package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
- package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
- package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
- package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
- package/dist/redis-stream-D6HzR1Z_.d.mts +232 -0
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
- package/dist/{resourceToTools-ByZpgjeH.mjs → resourceToTools-C5coh64w.mjs} +224 -71
- package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
- package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
- package/dist/schemas/index.d.mts +100 -30
- package/dist/schemas/index.mjs +86 -29
- package/dist/scim/index.d.mts +264 -0
- package/dist/scim/index.mjs +963 -0
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +4 -4
- package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
- package/dist/{store-helpers-BhrzxvyQ.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -8
- package/dist/testing/index.mjs +16 -24
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-BH7dEGvU.d.mts → types-BvqwCCSx.d.mts} +77 -29
- package/dist/{types-tgR4Pt8F.d.mts → types-CTYvcwHe.d.mts} +195 -1
- package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
- package/dist/{types-9beEMe25.d.mts → types-DQHFc8PM.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +5 -5
- package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
- package/dist/{versioning-M9lNLhO8.d.mts → versioning-DTTvc80y.d.mts} +1 -1
- package/package.json +24 -34
- package/skills/arc/SKILL.md +521 -785
- package/skills/arc/references/agent-auth.md +238 -0
- package/skills/arc/references/api-reference.md +187 -0
- package/skills/arc/references/auth.md +354 -7
- package/skills/arc/references/enterprise-auth.md +94 -0
- package/skills/arc/references/events.md +8 -6
- package/skills/arc/references/mcp.md +2 -2
- package/skills/arc/references/multi-tenancy.md +11 -2
- package/skills/arc/references/production.md +10 -9
- package/skills/arc/references/scim.md +247 -0
- package/skills/arc/references/testing.md +1 -1
- package/skills/arc-code-review/SKILL.md +141 -0
- package/skills/arc-code-review/references/anti-patterns.md +911 -0
- package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
- package/skills/arc-code-review/references/migration-recipes.md +700 -0
- package/skills/arc-code-review/references/mongokit-migration.md +386 -0
- package/skills/arc-code-review/references/scaffolding.md +230 -0
- package/skills/arc-code-review/references/severity.md +127 -0
- package/dist/EventTransport-CfVEGaEl.d.mts +0 -293
- package/dist/adapters/index.d.mts +0 -3
- package/dist/adapters/index.mjs +0 -2
- package/dist/adapters-D0tT2Tyo.mjs +0 -949
- package/dist/auth/mongoose.d.mts +0 -191
- package/dist/auth/mongoose.mjs +0 -73
- package/dist/core-DnUsRpuX.mjs +0 -1049
- package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
- package/dist/errorHandler-Co3lnVmJ.d.mts +0 -114
- package/dist/errors-D5c-5BJL.mjs +0 -232
- package/dist/index-BbMrcvGp.d.mts +0 -362
- package/dist/redis-stream-CM8TXTix.d.mts +0 -110
- /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
- /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
- /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
- /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
- /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
- /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BD5nw6St.d.mts} +0 -0
- /package/dist/{interface-CkkWm5uR.d.mts → interface-DfLGcus7.d.mts} +0 -0
- /package/dist/{interface-Da0r7Lna.d.mts → interface-beEtJyWM.d.mts} +0 -0
- /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
- /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
- /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
- /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
- /package/dist/{pluralize-BneOJkpi.mjs → pluralize-DQgqgifU.mjs} +0 -0
- /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
- /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
- /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
- /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-C4Le_UB3.d.mts} +0 -0
- /package/dist/{storage-BwGQXUpd.d.mts → storage-Dfzt4VTl.d.mts} +0 -0
- /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
- /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
- /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
- /package/dist/{websocket-CyJ1VIFI.d.mts → websocket-ChC2rqe1.d.mts} +0 -0
|
@@ -13,10 +13,34 @@ var RedisStreamTransport = class {
|
|
|
13
13
|
maxLen;
|
|
14
14
|
maxPayloadBytes;
|
|
15
15
|
logger;
|
|
16
|
+
/** Tracks the lifecycle policy — set in constructor, read in close(). */
|
|
17
|
+
externalLifecycle;
|
|
18
|
+
closeTimeoutMs;
|
|
16
19
|
handlers = /* @__PURE__ */ new Map();
|
|
17
20
|
running = false;
|
|
18
21
|
pollPromise = null;
|
|
22
|
+
/**
|
|
23
|
+
* Monotonic counter bumped every time the poll loop should stop —
|
|
24
|
+
* `unsubscribe` (last handler removed) and `close()` increment it. Each
|
|
25
|
+
* `pollLoop` instance captures its generation at start and exits when
|
|
26
|
+
* `this.generation` no longer matches. Prevents the
|
|
27
|
+
* subscribe → unsubscribe → fast-resubscribe race where the old loop
|
|
28
|
+
* would still be in `XREADGROUP BLOCK` while a new loop started, leading
|
|
29
|
+
* to two concurrent poll loops on the same consumer name.
|
|
30
|
+
*/
|
|
31
|
+
generation = 0;
|
|
19
32
|
groupCreated = false;
|
|
33
|
+
/**
|
|
34
|
+
* Last-seen failure context per message id, populated when an in-process
|
|
35
|
+
* handler throws in {@link processEntry}. Consumed (and cleared) by
|
|
36
|
+
* {@link moveToDlq} so the dead-letter envelope carries the actual error
|
|
37
|
+
* message instead of opaque "reclaimed without context". Bounded by
|
|
38
|
+
* `maxRetries × consumer-throughput` — entries are deleted on ack and
|
|
39
|
+
* on DLQ write, so the map naturally drains.
|
|
40
|
+
*/
|
|
41
|
+
failureContext = /* @__PURE__ */ new Map();
|
|
42
|
+
/** One-shot guard so the "client lacks xrange" warning fires once per process. */
|
|
43
|
+
xrangeWarningEmitted = false;
|
|
20
44
|
constructor(redis, options = {}) {
|
|
21
45
|
this.redis = redis;
|
|
22
46
|
this.stream = options.stream ?? "arc:events";
|
|
@@ -29,6 +53,8 @@ var RedisStreamTransport = class {
|
|
|
29
53
|
this.deadLetterStream = options.deadLetterStream ?? "arc:events:dlq";
|
|
30
54
|
this.maxLen = options.maxLen ?? 1e4;
|
|
31
55
|
this.maxPayloadBytes = options.maxPayloadBytes ?? 1e6;
|
|
56
|
+
this.externalLifecycle = options.externalLifecycle ?? false;
|
|
57
|
+
this.closeTimeoutMs = options.closeTimeoutMs ?? 1e3;
|
|
32
58
|
this.logger = options.logger ?? console;
|
|
33
59
|
}
|
|
34
60
|
async publish(event) {
|
|
@@ -55,9 +81,10 @@ var RedisStreamTransport = class {
|
|
|
55
81
|
if (!this.running) {
|
|
56
82
|
await this.ensureGroup();
|
|
57
83
|
this.running = true;
|
|
58
|
-
|
|
84
|
+
const myGen = ++this.generation;
|
|
85
|
+
this.pollPromise = this.pollLoop(myGen).catch((err) => {
|
|
59
86
|
this.logger.error("[RedisStreamTransport] Poll loop crashed:", err);
|
|
60
|
-
this.running = false;
|
|
87
|
+
if (this.generation === myGen) this.running = false;
|
|
61
88
|
});
|
|
62
89
|
}
|
|
63
90
|
return () => {
|
|
@@ -66,16 +93,59 @@ var RedisStreamTransport = class {
|
|
|
66
93
|
set.delete(handler);
|
|
67
94
|
if (set.size === 0) this.handlers.delete(pattern);
|
|
68
95
|
}
|
|
69
|
-
if (this.handlers.size === 0 && this.running)
|
|
96
|
+
if (this.handlers.size === 0 && this.running) {
|
|
97
|
+
this.running = false;
|
|
98
|
+
this.generation++;
|
|
99
|
+
}
|
|
70
100
|
};
|
|
71
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Stop polling and release transport state.
|
|
104
|
+
*
|
|
105
|
+
* **Two close contracts** — pick the one that matches your deployment:
|
|
106
|
+
*
|
|
107
|
+
* 1. **Default (`externalLifecycle: false`) — strict bounded close.**
|
|
108
|
+
* `close()` waits up to `closeTimeoutMs` for the in-flight
|
|
109
|
+
* `XREADGROUP BLOCK` to drain. On timeout it calls `redis.disconnect()`
|
|
110
|
+
* (or `quit()` if the client lacks `disconnect`) to break the BLOCK
|
|
111
|
+
* immediately, then awaits the loop's exit. After `close()` returns
|
|
112
|
+
* the transport is fully closed and the connection is released.
|
|
113
|
+
*
|
|
114
|
+
* 2. **`externalLifecycle: true` — bounded RETURN, background drain.**
|
|
115
|
+
* Arc must NOT touch a connection it doesn't own. `close()` returns
|
|
116
|
+
* within `closeTimeoutMs`, but the poll loop is left to drain on its
|
|
117
|
+
* own when its outstanding `XREADGROUP BLOCK` returns (up to
|
|
118
|
+
* `blockTimeMs`). Arc silently absorbs the loop's eventual completion
|
|
119
|
+
* so the host doesn't see unhandled rejections / log spam against a
|
|
120
|
+
* transport it considers closed. The host's own `redis.quit()` /
|
|
121
|
+
* process exit is what ultimately tears the connection down.
|
|
122
|
+
*
|
|
123
|
+
* Practical implication: under `externalLifecycle: true`, set
|
|
124
|
+
* `blockTimeMs` low (e.g. 500ms) so the background drain window is
|
|
125
|
+
* short. The transport is "closed enough" to stop dispatching to
|
|
126
|
+
* handlers (handlers map is cleared and generation is bumped) but is
|
|
127
|
+
* not "fully closed" in the connection-lifecycle sense until the host
|
|
128
|
+
* closes the underlying client.
|
|
129
|
+
*
|
|
130
|
+
* In both modes the generation counter is bumped, so a follow-up
|
|
131
|
+
* `subscribe()` spawns a fresh poll loop with a new generation — the
|
|
132
|
+
* stale loop exits on its next iteration and never overlaps the new one.
|
|
133
|
+
*/
|
|
72
134
|
async close() {
|
|
73
135
|
this.running = false;
|
|
136
|
+
this.generation++;
|
|
74
137
|
this.handlers.clear();
|
|
75
138
|
if (this.pollPromise) {
|
|
76
|
-
await this.pollPromise
|
|
139
|
+
if (await Promise.race([this.pollPromise.then(() => "drained"), this.sleep(this.closeTimeoutMs).then(() => "timeout")]) === "timeout") if (!this.externalLifecycle) {
|
|
140
|
+
if (typeof this.redis.disconnect === "function") this.redis.disconnect();
|
|
141
|
+
else await this.redis.quit().catch((err) => {
|
|
142
|
+
this.logger.error("[RedisStreamTransport] quit() during close raced:", err);
|
|
143
|
+
});
|
|
144
|
+
await this.pollPromise.catch(() => void 0);
|
|
145
|
+
} else this.pollPromise.catch(() => void 0);
|
|
77
146
|
this.pollPromise = null;
|
|
78
147
|
}
|
|
148
|
+
if (!this.externalLifecycle) await this.redis.quit().catch(() => void 0);
|
|
79
149
|
}
|
|
80
150
|
async ensureGroup() {
|
|
81
151
|
if (this.groupCreated) return;
|
|
@@ -86,12 +156,12 @@ var RedisStreamTransport = class {
|
|
|
86
156
|
}
|
|
87
157
|
this.groupCreated = true;
|
|
88
158
|
}
|
|
89
|
-
async pollLoop() {
|
|
90
|
-
while (this.running) try {
|
|
159
|
+
async pollLoop(myGen) {
|
|
160
|
+
while (this.running && this.generation === myGen) try {
|
|
91
161
|
await this.claimPending();
|
|
92
162
|
await this.readNewMessages();
|
|
93
163
|
} catch (err) {
|
|
94
|
-
if (this.running) {
|
|
164
|
+
if (this.running && this.generation === myGen) {
|
|
95
165
|
this.logger.error("[RedisStreamTransport] Poll error:", err);
|
|
96
166
|
await this.sleep(1e3);
|
|
97
167
|
}
|
|
@@ -123,39 +193,38 @@ var RedisStreamTransport = class {
|
|
|
123
193
|
}
|
|
124
194
|
}
|
|
125
195
|
async processEntry(messageId, fields) {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const rawData = fieldMap.get("data");
|
|
130
|
-
if (!eventType || !rawData) {
|
|
131
|
-
await this.redis.xack(this.stream, this.group, messageId);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
let event;
|
|
135
|
-
try {
|
|
136
|
-
const parsed = JSON.parse(rawData, (key, value) => {
|
|
137
|
-
if (key === "timestamp" && typeof value === "string") return new Date(value);
|
|
138
|
-
return value;
|
|
139
|
-
});
|
|
140
|
-
if (!parsed || typeof parsed !== "object" || typeof parsed.type !== "string" || !parsed.meta?.id) {
|
|
141
|
-
this.logger.warn("[RedisStreamTransport] Malformed event — missing type or meta.id, acking and skipping");
|
|
142
|
-
await this.redis.xack(this.stream, this.group, messageId);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
event = parsed;
|
|
146
|
-
} catch {
|
|
196
|
+
const event = parseStreamFields(fields);
|
|
197
|
+
if (!event) {
|
|
198
|
+
this.logger.warn(`[RedisStreamTransport] Malformed entry ${messageId} — missing type/data or invalid JSON, acking and skipping`);
|
|
147
199
|
await this.redis.xack(this.stream, this.group, messageId);
|
|
148
200
|
return;
|
|
149
201
|
}
|
|
150
202
|
const matchingHandlers = this.getMatchingHandlers(event.type);
|
|
151
203
|
let allSucceeded = true;
|
|
204
|
+
let lastError;
|
|
205
|
+
let lastHandlerName;
|
|
152
206
|
for (const handler of matchingHandlers) try {
|
|
153
207
|
await handler(event);
|
|
154
208
|
} catch (err) {
|
|
155
209
|
allSucceeded = false;
|
|
210
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
211
|
+
lastHandlerName = handler.name || lastHandlerName;
|
|
156
212
|
this.logger.error(`[RedisStreamTransport] Handler error for ${event.type}:`, err);
|
|
157
213
|
}
|
|
158
|
-
if (allSucceeded)
|
|
214
|
+
if (allSucceeded) {
|
|
215
|
+
await this.redis.xack(this.stream, this.group, messageId);
|
|
216
|
+
this.failureContext.delete(messageId);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const now = /* @__PURE__ */ new Date();
|
|
220
|
+
const prior = this.failureContext.get(messageId);
|
|
221
|
+
this.failureContext.set(messageId, {
|
|
222
|
+
error: lastError ? toErrorRecord(lastError) : { message: "handler returned without acking — no error captured" },
|
|
223
|
+
firstFailedAt: prior?.firstFailedAt ?? now,
|
|
224
|
+
lastFailedAt: now,
|
|
225
|
+
attempts: (prior?.attempts ?? 0) + 1,
|
|
226
|
+
handlerName: lastHandlerName ?? prior?.handlerName
|
|
227
|
+
});
|
|
159
228
|
}
|
|
160
229
|
getMatchingHandlers(eventType) {
|
|
161
230
|
const matched = [];
|
|
@@ -173,19 +242,123 @@ var RedisStreamTransport = class {
|
|
|
173
242
|
}
|
|
174
243
|
async moveToDlq(ids) {
|
|
175
244
|
if (this.deadLetterStream === false) {
|
|
176
|
-
for (const id of ids)
|
|
245
|
+
for (const id of ids) {
|
|
246
|
+
await this.redis.xack(this.stream, this.group, id);
|
|
247
|
+
this.failureContext.delete(id);
|
|
248
|
+
}
|
|
177
249
|
return;
|
|
178
250
|
}
|
|
179
251
|
for (const id of ids) try {
|
|
180
|
-
|
|
252
|
+
const envelope = await this.buildDlqEnvelope(id);
|
|
253
|
+
if (!envelope) {
|
|
254
|
+
this.logger.error(`[RedisStreamTransport] DLQ for ${id}: source entry missing AND no failure context — acking to drop`);
|
|
255
|
+
await this.redis.xack(this.stream, this.group, id);
|
|
256
|
+
this.failureContext.delete(id);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
await this.redis.xadd(this.deadLetterStream, "*", "type", envelope.event.type, "originalStream", this.stream, "originalId", id, "group", this.group, "data", JSON.stringify(envelope));
|
|
181
260
|
await this.redis.xack(this.stream, this.group, id);
|
|
261
|
+
this.failureContext.delete(id);
|
|
182
262
|
} catch (err) {
|
|
183
263
|
this.logger.error(`[RedisStreamTransport] DLQ write failed for ${id}:`, err);
|
|
184
264
|
}
|
|
185
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Reconstruct a `DeadLetteredEvent` for a message id. Reads the original
|
|
268
|
+
* entry via `xrange` (when the client supports it) and merges in any
|
|
269
|
+
* in-process failure context. Returns `null` only when BOTH sources are
|
|
270
|
+
* missing — callers ack-and-drop rather than re-queuing a ghost.
|
|
271
|
+
*
|
|
272
|
+
* Graceful degradation paths:
|
|
273
|
+
* - Client lacks `xrange` (older custom wrappers) → log once, build the
|
|
274
|
+
* envelope from `failureContext` alone. Payload is absent but the
|
|
275
|
+
* error reason + attempt accounting still survive.
|
|
276
|
+
* - `xrange` throws (network blip, ACL) → same fallback.
|
|
277
|
+
* - Source entry trimmed before DLQ write → same fallback.
|
|
278
|
+
*/
|
|
279
|
+
async buildDlqEnvelope(id) {
|
|
280
|
+
const ctx = this.failureContext.get(id);
|
|
281
|
+
let event = null;
|
|
282
|
+
if (typeof this.redis.xrange === "function") try {
|
|
283
|
+
const fields = (await this.redis.xrange(this.stream, id, id))[0]?.[1];
|
|
284
|
+
if (fields) {
|
|
285
|
+
const parsed = parseStreamFields(fields);
|
|
286
|
+
if (parsed) event = parsed;
|
|
287
|
+
}
|
|
288
|
+
} catch (err) {
|
|
289
|
+
this.logger.error(`[RedisStreamTransport] xrange for DLQ source ${id} failed:`, err);
|
|
290
|
+
}
|
|
291
|
+
else if (!this.xrangeWarningEmitted) {
|
|
292
|
+
this.xrangeWarningEmitted = true;
|
|
293
|
+
this.logger.warn("[RedisStreamTransport] Redis client lacks xrange() — DLQ envelopes will not include the original event payload. Upgrade your client (ioredis ≥4 supports it) or use a wrapper that proxies xrange to enable replay.");
|
|
294
|
+
}
|
|
295
|
+
if (!event && !ctx) return null;
|
|
296
|
+
const fallbackTime = /* @__PURE__ */ new Date();
|
|
297
|
+
return {
|
|
298
|
+
event: event ?? {
|
|
299
|
+
type: "<unknown>",
|
|
300
|
+
payload: null,
|
|
301
|
+
meta: {
|
|
302
|
+
id,
|
|
303
|
+
timestamp: fallbackTime
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
error: ctx?.error ?? { message: "exhausted retries — failure occurred on a different consumer; error context not preserved across consumer-group failover" },
|
|
307
|
+
attempts: ctx?.attempts ?? this.maxRetries,
|
|
308
|
+
firstFailedAt: ctx?.firstFailedAt ?? fallbackTime,
|
|
309
|
+
lastFailedAt: ctx?.lastFailedAt ?? fallbackTime,
|
|
310
|
+
...ctx?.handlerName ? { handlerName: ctx.handlerName } : {}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
186
313
|
sleep(ms) {
|
|
187
314
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
188
315
|
}
|
|
189
316
|
};
|
|
317
|
+
/**
|
|
318
|
+
* Convert a thrown value into the `DeadLetteredEvent.error` shape — message
|
|
319
|
+
* always present, optional `code` (string only) and `stack`. Centralised so
|
|
320
|
+
* the failure-context tracker and the DLQ envelope writer agree.
|
|
321
|
+
*/
|
|
322
|
+
function toErrorRecord(err) {
|
|
323
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
324
|
+
const code = e.code;
|
|
325
|
+
return {
|
|
326
|
+
message: e.message,
|
|
327
|
+
...typeof code === "string" ? { code } : {},
|
|
328
|
+
...e.stack ? { stack: e.stack } : {}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Parse a Redis Stream entry's flat `[key, value, key, value, ...]` field
|
|
333
|
+
* array into a typed `DomainEvent`, or `null` when the entry is malformed
|
|
334
|
+
* (missing `type` / `data`, unparseable JSON, or missing required event
|
|
335
|
+
* structure).
|
|
336
|
+
*
|
|
337
|
+
* Pure on purpose — used by both `processEntry` (the live consumer path)
|
|
338
|
+
* and `buildDlqEnvelope` (the dead-letter writer). Keeping the parse logic
|
|
339
|
+
* in one place avoids the silent drift class that produced the original
|
|
340
|
+
* "DLQ has no payload" bug.
|
|
341
|
+
*/
|
|
342
|
+
function parseStreamFields(fields) {
|
|
343
|
+
let eventType;
|
|
344
|
+
let rawData;
|
|
345
|
+
for (let i = 0; i < fields.length; i += 2) {
|
|
346
|
+
const key = fields[i];
|
|
347
|
+
const value = fields[i + 1];
|
|
348
|
+
if (key === "type") eventType = value;
|
|
349
|
+
else if (key === "data") rawData = value;
|
|
350
|
+
}
|
|
351
|
+
if (!eventType || !rawData) return null;
|
|
352
|
+
try {
|
|
353
|
+
const parsed = JSON.parse(rawData, (key, value) => {
|
|
354
|
+
if (key === "timestamp" && typeof value === "string") return new Date(value);
|
|
355
|
+
return value;
|
|
356
|
+
});
|
|
357
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.type !== "string" || !parsed.meta?.id) return null;
|
|
358
|
+
return parsed;
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
190
363
|
//#endregion
|
|
191
364
|
export { RedisStreamTransport };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "../../EventTransport-CT_52aWU.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/events/transports/redis.d.ts
|
|
4
4
|
interface RedisLike {
|
package/dist/factory/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as ResourceModule, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, p as loadResources, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-
|
|
1
|
+
import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as ResourceModule, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, p as loadResources, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-BvqwCCSx.mjs";
|
|
2
2
|
import { FastifyInstance } from "fastify";
|
|
3
3
|
|
|
4
4
|
//#region src/factory/createApp.d.ts
|
|
@@ -14,7 +14,7 @@ import { FastifyInstance } from "fastify";
|
|
|
14
14
|
* 4. Arc core (fastify.arc, events)
|
|
15
15
|
* 5. Arc plugins (requestId, health, caching, SSE, metrics, versioning)
|
|
16
16
|
* 6. Auth (scope decoration, auth strategy, elevation, error handler)
|
|
17
|
-
* 7. plugins() — user infra (DB,
|
|
17
|
+
* 7. plugins() — user infra (DB, data, webhooks)
|
|
18
18
|
* 8. bootstrap[] — domain init (singletons, event handlers)
|
|
19
19
|
* 9. resources[] — auto-discovered routes (prefix + skipGlobalPrefix)
|
|
20
20
|
* 10. afterResources() — post-registration wiring
|
package/dist/factory/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-
|
|
2
|
-
import { t as loadResources } from "../loadResources-
|
|
1
|
+
import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-XX2-N0Yd.mjs";
|
|
2
|
+
import { t as loadResources } from "../loadResources-DBMQg_Aj.mjs";
|
|
3
3
|
//#region src/factory/edge.ts
|
|
4
4
|
/**
|
|
5
5
|
* Convert a Fastify app into a Web Standards fetch handler.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as RequestScope } from "./types-CTYvcwHe.mjs";
|
|
2
2
|
import { FastifyRequest } from "fastify";
|
|
3
3
|
|
|
4
4
|
//#region src/permissions/types.d.ts
|
|
@@ -134,7 +134,7 @@ interface PermissionResult {
|
|
|
134
134
|
type PermissionCheck<TDoc = Record<string, unknown>> = ((context: PermissionContext<TDoc>) => boolean | PermissionResult | Promise<boolean | PermissionResult>) & PermissionCheckMeta;
|
|
135
135
|
/**
|
|
136
136
|
* Optional metadata attached to permission check functions.
|
|
137
|
-
* Used for OpenAPI
|
|
137
|
+
* Used for OpenAPI data, introspection, and route-level auth decisions.
|
|
138
138
|
*
|
|
139
139
|
* Each helper from `permissions/index.ts` writes its own discriminating tag
|
|
140
140
|
* so downstream tooling (OpenAPI generator, MCP resource builder, route
|
|
@@ -173,6 +173,27 @@ interface PermissionCheckMeta {
|
|
|
173
173
|
* (e.g. from route params).
|
|
174
174
|
*/
|
|
175
175
|
_orgInScopeTarget?: string | ((ctx: PermissionContext) => string | undefined);
|
|
176
|
+
/**
|
|
177
|
+
* Set by requireDPoP() — the inbound credential must be sender-constrained
|
|
178
|
+
* via DPoP (RFC 9449), with `scope.dpopJkt` set by the authenticate
|
|
179
|
+
* function after a successful proof verification.
|
|
180
|
+
*/
|
|
181
|
+
_dpopRequired?: boolean;
|
|
182
|
+
/**
|
|
183
|
+
* Set by requireMandate() — the capability string the mandate on
|
|
184
|
+
* `scope.mandate` must authorize (e.g. `payment.charge`, `data.export`).
|
|
185
|
+
*/
|
|
186
|
+
_mandateCapability?: string;
|
|
187
|
+
/**
|
|
188
|
+
* Set by requireAgentScope() — composite descriptor for AI-agent flows.
|
|
189
|
+
* Tools (audit, OpenAPI, MCP) can render the full agent-auth requirement
|
|
190
|
+
* in one read instead of unpacking three separate metadata fields.
|
|
191
|
+
*/
|
|
192
|
+
_agentScope?: {
|
|
193
|
+
capability: string;
|
|
194
|
+
scopes?: readonly string[];
|
|
195
|
+
dpop: boolean;
|
|
196
|
+
};
|
|
176
197
|
}
|
|
177
198
|
//#endregion
|
|
178
199
|
//#region src/permissions/fields.d.ts
|
package/dist/hooks/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { An as afterUpdate, Cn as HookOperation, Dn as HookSystemOptions, En as HookSystem, Fn as defineHook, Mn as beforeDelete, Nn as beforeUpdate, On as afterCreate, Pn as createHookSystem, Sn as HookHandler, Tn as HookRegistration, bn as DefineHookOptions, jn as beforeCreate, kn as afterDelete, wn as HookPhase, xn as HookContext } from "../index-BtW7qYwa.mjs";
|
|
2
2
|
export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
|
package/dist/hooks/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as beforeCreate, c as createHookSystem, i as afterUpdate, l as defineHook, n as afterCreate, o as beforeDelete, r as afterDelete, s as beforeUpdate, t as HookSystem } from "../HookSystem-
|
|
1
|
+
import { a as beforeCreate, c as createHookSystem, i as afterUpdate, l as defineHook, n as afterCreate, o as beforeDelete, r as afterDelete, s as beforeUpdate, t as HookSystem } from "../HookSystem-Iiebom92.mjs";
|
|
2
2
|
export { HookSystem, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as
|
|
3
|
-
import { i as RedisIdempotencyStoreOptions, n as RedisClient } from "../redis-Cm1gnRDf.mjs";
|
|
1
|
+
import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-DfLGcus7.mjs";
|
|
2
|
+
import { i as RedisIdempotencyStoreOptions, n as RedisClient } from "../redis-DiMkdHEl.mjs";
|
|
4
3
|
import { FastifyPluginAsync } from "fastify";
|
|
4
|
+
import { RepositoryLike } from "@classytic/repo-core/adapter";
|
|
5
5
|
|
|
6
6
|
//#region src/idempotency/idempotencyPlugin.d.ts
|
|
7
7
|
interface IdempotencyPluginOptions {
|
|
@@ -1,28 +1,9 @@
|
|
|
1
|
-
import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-
|
|
1
|
+
import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-BkIN9-vu.mjs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import fp from "fastify-plugin";
|
|
4
4
|
import { and, eq, exists, gt, lt, or, startsWith } from "@classytic/repo-core/filter";
|
|
5
5
|
import { update } from "@classytic/repo-core/update";
|
|
6
6
|
//#region src/idempotency/repository-idempotency-adapter.ts
|
|
7
|
-
/**
|
|
8
|
-
* RepositoryLike → IdempotencyStore adapter.
|
|
9
|
-
*
|
|
10
|
-
* Maps the idempotency store's verbs (get / set / tryLock / unlock / delete /
|
|
11
|
-
* deleteByPrefix / findByPrefix) onto arc's canonical repository primitives
|
|
12
|
-
* (`getOne` / `deleteMany` / `findOneAndUpdate`). `idempotencyPlugin` wraps
|
|
13
|
-
* a passed repository with this helper when you use the `{ repository }`
|
|
14
|
-
* option; the function is also re-exported from `@classytic/arc/idempotency`
|
|
15
|
-
* so consumers can build and decorate the store (metrics, tracing, key
|
|
16
|
-
* namespacing) before passing it via `store:`.
|
|
17
|
-
*
|
|
18
|
-
* Portability: filters compose via `@classytic/repo-core/filter` builders
|
|
19
|
-
* (`and` / `or` / `eq` / `gt` / `lt` / `exists` / `startsWith`) and updates
|
|
20
|
-
* via `@classytic/repo-core/update` (`update({ set, unset, setOnInsert })`).
|
|
21
|
-
* Both IRs compile to Mongo operators on mongokit, SQL predicates on
|
|
22
|
-
* sqlitekit / pgkit, and `WhereInput` / `update` on prismakit. The store
|
|
23
|
-
* therefore runs identically on every backend that implements the
|
|
24
|
-
* `StandardRepo.findOneAndUpdate` + `getOne` + `deleteMany` surface.
|
|
25
|
-
*/
|
|
26
7
|
function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
27
8
|
const missing = [];
|
|
28
9
|
if (typeof repository.getOne !== "function") missing.push("getOne");
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as UpstashRedisLike, i as RedisIdempotencyStoreOptions, n as RedisClient, o as ioredisAsIdempotencyClient, r as RedisIdempotencyStore, s as upstashAsIdempotencyClient, t as IoredisLike } from "../redis-
|
|
1
|
+
import { a as UpstashRedisLike, i as RedisIdempotencyStoreOptions, n as RedisClient, o as ioredisAsIdempotencyClient, r as RedisIdempotencyStore, s as upstashAsIdempotencyClient, t as IoredisLike } from "../redis-DiMkdHEl.mjs";
|
|
2
2
|
export { type IoredisLike, type RedisClient, RedisIdempotencyStore, type RedisIdempotencyStoreOptions, type UpstashRedisLike, ioredisAsIdempotencyClient, upstashAsIdempotencyClient };
|
|
@@ -218,7 +218,7 @@ function upstashAsIdempotencyClient(client) {
|
|
|
218
218
|
const keyCount = _numKeys;
|
|
219
219
|
const keys = args.slice(0, keyCount).map(String);
|
|
220
220
|
const rest = args.slice(keyCount);
|
|
221
|
-
return client.eval(script, keys, rest);
|
|
221
|
+
return client.eval?.(script, keys, rest);
|
|
222
222
|
} : void 0,
|
|
223
223
|
async quit() {
|
|
224
224
|
return "OK";
|
|
@@ -1,8 +1,168 @@
|
|
|
1
|
-
import { r as CacheStore, t as CacheLogger } from "./interface-
|
|
2
|
-
import {
|
|
3
|
-
import { c as PermissionCheck, l as PermissionContext, u as PermissionResult } from "./fields-
|
|
1
|
+
import { r as CacheStore, t as CacheLogger } from "./interface-beEtJyWM.mjs";
|
|
2
|
+
import { i as RequestScope, n as Mandate } from "./types-CTYvcwHe.mjs";
|
|
3
|
+
import { c as PermissionCheck, l as PermissionContext, u as PermissionResult } from "./fields-COhcH3fk.mjs";
|
|
4
4
|
import { FastifyReply, FastifyRequest } from "fastify";
|
|
5
5
|
|
|
6
|
+
//#region src/permissions/agent.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Require a sender-constrained credential — the inbound token MUST carry a
|
|
9
|
+
* DPoP proof (RFC 9449) bound to a known key. Arc reads `scope.dpopJkt` (the
|
|
10
|
+
* JWK SHA-256 thumbprint per RFC 7638); your `authenticate` function performs
|
|
11
|
+
* the cryptographic `jose.dpop.verify(...)` and sets the field on success.
|
|
12
|
+
*
|
|
13
|
+
* **Pass behavior:**
|
|
14
|
+
* - `service` scope where `dpopJkt` is set → grant
|
|
15
|
+
* - `elevated` scope → grant (platform admin bypass)
|
|
16
|
+
* - Anything else → deny with a clear reason
|
|
17
|
+
*
|
|
18
|
+
* Use for high-value endpoints where bearer-token replay must be impossible:
|
|
19
|
+
* payment charges, data exports, account-takeover-class admin actions.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* permissions: { charge: allOf(requireServiceScope('payment.write'), requireDPoP()) }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
declare function requireDPoP<TDoc = Record<string, unknown>>(): PermissionCheck<TDoc>;
|
|
27
|
+
/**
|
|
28
|
+
* Options for `requireMandate(capability, opts)`.
|
|
29
|
+
*/
|
|
30
|
+
interface RequireMandateOptions<TDoc = Record<string, unknown>> {
|
|
31
|
+
/**
|
|
32
|
+
* Custom validator for the mandate's numeric ceiling against the inbound
|
|
33
|
+
* request. Arc passes the request body / params; you decide whether the
|
|
34
|
+
* action stays within the mandate's `cap`.
|
|
35
|
+
*
|
|
36
|
+
* Return `true` to accept, `false` (or a string reason) to deny. When
|
|
37
|
+
* omitted, arc skips amount validation — useful for boolean-capability
|
|
38
|
+
* mandates where presence of the mandate IS the authorization (no cap).
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* validateAmount: (ctx, mandate) => {
|
|
43
|
+
* const amount = (ctx.data as { amount?: number })?.amount ?? 0;
|
|
44
|
+
* if (amount <= (mandate.cap ?? 0)) return true;
|
|
45
|
+
* return `Amount ${amount} exceeds mandate cap ${mandate.cap}`;
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
validateAmount?: (ctx: PermissionContext<TDoc>, mandate: Readonly<Mandate>) => boolean | string;
|
|
50
|
+
/**
|
|
51
|
+
* Resource the mandate must be bound to (`Mandate.audience`). Pass a
|
|
52
|
+
* static value or a function that derives it from the request (typically
|
|
53
|
+
* `ctx.params.id`). When set and the mandate's `audience` doesn't match,
|
|
54
|
+
* the request is denied — prevents a payment-mandate for invoice A being
|
|
55
|
+
* replayed against invoice B.
|
|
56
|
+
*/
|
|
57
|
+
audience?: string | ((ctx: PermissionContext<TDoc>) => string | undefined);
|
|
58
|
+
/**
|
|
59
|
+
* Clock-skew tolerance for `Mandate.expiresAt`, in milliseconds.
|
|
60
|
+
* Default `30_000` (30s).
|
|
61
|
+
*/
|
|
62
|
+
ttlGraceMs?: number;
|
|
63
|
+
/**
|
|
64
|
+
* When `true`, `elevated` scope is NOT allowed to bypass the mandate check.
|
|
65
|
+
* Defaults to `false` — platform admins normally bypass everything.
|
|
66
|
+
* Set when you genuinely want "even an admin needs a mandate" semantics
|
|
67
|
+
* (audited break-glass actions, regulated payment flows).
|
|
68
|
+
*/
|
|
69
|
+
noElevatedBypass?: boolean;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Require a capability mandate (AP2 / x402 / MCP authorization) that
|
|
73
|
+
* authorizes the action being attempted.
|
|
74
|
+
*
|
|
75
|
+
* The mandate is set on `request.scope.mandate` by your authenticate function
|
|
76
|
+
* after verifying the inbound mandate JWT/VC. This check validates that the
|
|
77
|
+
* presented mandate covers the requested capability, hasn't expired, is bound
|
|
78
|
+
* to the right resource (when `audience` opt is set), and respects the
|
|
79
|
+
* mandate's numeric ceiling (when `validateAmount` opt is set).
|
|
80
|
+
*
|
|
81
|
+
* **Pass behavior:**
|
|
82
|
+
* - `elevated` scope → grant unless `noElevatedBypass: true`
|
|
83
|
+
* - `service` scope with mandate matching `capability`, not expired, and
|
|
84
|
+
* passing `validateAmount` + `audience` checks → grant
|
|
85
|
+
* - Anything else → deny with a precise reason
|
|
86
|
+
*
|
|
87
|
+
* Pair with `requireDPoP()` for replay-resistance, or use the bundled
|
|
88
|
+
* `requireAgentScope(...)` to declare both at once.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* // Single payment charge — amount must fit the mandate's cap
|
|
93
|
+
* permissions: {
|
|
94
|
+
* pay: requireMandate('payment.charge', {
|
|
95
|
+
* validateAmount: (ctx, m) => (ctx.data as { amount: number }).amount <= (m.cap ?? 0),
|
|
96
|
+
* audience: (ctx) => `invoice:${ctx.params?.id}`,
|
|
97
|
+
* }),
|
|
98
|
+
* }
|
|
99
|
+
*
|
|
100
|
+
* // Boolean capability — presence of mandate is the gate
|
|
101
|
+
* permissions: {
|
|
102
|
+
* exportData: requireMandate('data.export'),
|
|
103
|
+
* }
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
declare function requireMandate<TDoc = Record<string, unknown>>(capability: string, opts?: RequireMandateOptions<TDoc>): PermissionCheck<TDoc>;
|
|
107
|
+
/**
|
|
108
|
+
* Options for `requireAgentScope(opts)`.
|
|
109
|
+
*/
|
|
110
|
+
interface RequireAgentScopeOptions<TDoc = Record<string, unknown>> extends RequireMandateOptions<TDoc> {
|
|
111
|
+
/**
|
|
112
|
+
* Capability the mandate must authorize (e.g., `payment.charge`,
|
|
113
|
+
* `inbox.send`). Required.
|
|
114
|
+
*/
|
|
115
|
+
capability: string;
|
|
116
|
+
/**
|
|
117
|
+
* When `true`, the inbound credential must also be DPoP-bound (RFC 9449).
|
|
118
|
+
* Defaults to `true` — sender-constrained credentials are the standard
|
|
119
|
+
* for high-value agent flows. Set `false` only when you intentionally
|
|
120
|
+
* accept bearer tokens (rare; usually a regression).
|
|
121
|
+
*/
|
|
122
|
+
requireDPoP?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Optional OAuth-style scope strings the service identity must hold in
|
|
125
|
+
* addition to the mandate (e.g., `['payment.write']`). Pairs with the
|
|
126
|
+
* mandate's narrower per-request authorization — scopes answer "ever
|
|
127
|
+
* allowed?", mandate answers "right now?".
|
|
128
|
+
*/
|
|
129
|
+
scopes?: readonly string[];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Composite gate for AI-agent / M2M flows on protected resources.
|
|
133
|
+
*
|
|
134
|
+
* Bundles the three things every high-value agent endpoint needs:
|
|
135
|
+
* 1. **Service identity** — `scope.kind === 'service'` with `clientId`
|
|
136
|
+
* 2. **Capability mandate** — narrows what *this request* may do
|
|
137
|
+
* 3. **DPoP binding** — credential cannot be replayed from a different key
|
|
138
|
+
*
|
|
139
|
+
* Use this instead of hand-composing `allOf(requireServiceScope(...),
|
|
140
|
+
* requireMandate(...), requireDPoP())` — fewer ways to misconfigure, one
|
|
141
|
+
* meta-tag downstream tools (audit, MCP, OpenAPI) can read.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```typescript
|
|
145
|
+
* import { requireAgentScope } from '@classytic/arc/permissions';
|
|
146
|
+
*
|
|
147
|
+
* defineResource({
|
|
148
|
+
* name: 'invoice',
|
|
149
|
+
* actions: {
|
|
150
|
+
* pay: {
|
|
151
|
+
* handler: payInvoice,
|
|
152
|
+
* permissions: requireAgentScope({
|
|
153
|
+
* capability: 'payment.charge',
|
|
154
|
+
* scopes: ['payment.write'],
|
|
155
|
+
* requireDPoP: true,
|
|
156
|
+
* audience: (ctx) => `invoice:${ctx.params?.id}`,
|
|
157
|
+
* validateAmount: (ctx, m) => (ctx.data as { amount: number }).amount <= (m.cap ?? 0),
|
|
158
|
+
* }),
|
|
159
|
+
* },
|
|
160
|
+
* },
|
|
161
|
+
* });
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
declare function requireAgentScope<TDoc = Record<string, unknown>>(opts: RequireAgentScopeOptions<TDoc>): PermissionCheck<TDoc>;
|
|
165
|
+
//#endregion
|
|
6
166
|
//#region src/permissions/applyPermissionResult.d.ts
|
|
7
167
|
/**
|
|
8
168
|
* Normalize a permission check return value (`boolean | PermissionResult`)
|
|
@@ -512,4 +672,4 @@ declare function fullPublic<TDoc = any>(overrides?: PermissionOverrides<TDoc>):
|
|
|
512
672
|
*/
|
|
513
673
|
declare function readOnly<TDoc = any>(overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
|
|
514
674
|
//#endregion
|
|
515
|
-
export { requireRoles as A, allOf as C, not as D, denyAll as E, when as M, applyPermissionResult as N, requireAuth as O, normalizePermissionResult as P, createOrgPermissions as S, anyOf as T, ConnectEventsOptions as _, presets_d_exports as a, PermissionEventBus as b, readOnly as c, requireOrgRole as d, requireScopeContext as f, createRoleHierarchy as g, RoleHierarchy as h, ownerWithAdminBypass as i, roles as j, requireOwnership as k, requireOrgInScope as l, requireTeamMembership as m, authenticated as n, publicRead as o, requireServiceScope as p, fullPublic as r, publicReadAdminWrite as s, adminOnly as t, requireOrgMembership as u, DynamicPermissionMatrix as v, allowPublic as w, createDynamicPermissionMatrix as x, DynamicPermissionMatrixConfig as y };
|
|
675
|
+
export { requireRoles as A, allOf as C, not as D, denyAll as E, RequireAgentScopeOptions as F, RequireMandateOptions as I, requireAgentScope as L, when as M, applyPermissionResult as N, requireAuth as O, normalizePermissionResult as P, requireDPoP as R, createOrgPermissions as S, anyOf as T, ConnectEventsOptions as _, presets_d_exports as a, PermissionEventBus as b, readOnly as c, requireOrgRole as d, requireScopeContext as f, createRoleHierarchy as g, RoleHierarchy as h, ownerWithAdminBypass as i, roles as j, requireOwnership as k, requireOrgInScope as l, requireTeamMembership as m, authenticated as n, publicRead as o, requireServiceScope as p, fullPublic as r, publicReadAdminWrite as s, adminOnly as t, requireOrgMembership as u, DynamicPermissionMatrix as v, allowPublic as w, createDynamicPermissionMatrix as x, DynamicPermissionMatrixConfig as y, requireMandate as z };
|