@classytic/arc 2.8.3 → 2.8.5
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 +50 -1
- package/dist/adapters/index.d.mts +2 -2
- package/dist/audit/index.d.mts +1 -1
- package/dist/audit/index.mjs +1 -1
- package/dist/audit/mongodb.d.mts +1 -1
- package/dist/audit/mongodb.mjs +1 -1
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +2 -2
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-C5lDyRH2.mjs → betterAuthOpenApi-BuUcUEJq.mjs} +1 -1
- package/dist/cache/index.d.mts +73 -3
- package/dist/cache/index.mjs +95 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -3
- package/dist/{core-DKSwNSXf.mjs → core-F0QoWBt2.mjs} +1 -1
- package/dist/{createActionRouter-Df1BuawX.mjs → createActionRouter-BORM8f17.mjs} +1 -1
- package/dist/{createApp-BOYjBgdI.mjs → createApp-B1EY8zxa.mjs} +11 -11
- package/dist/{defineResource-Bb_Bdhtw.mjs → defineResource-tcgySDo1.mjs} +2 -2
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/dynamic/index.d.mts +2 -2
- package/dist/dynamic/index.mjs +1 -1
- package/dist/{elevation-BBGFjzIP.mjs → elevation-DtFxrG0s.mjs} +1 -1
- package/dist/{errorHandler-CdZDavNH.d.mts → errorHandler-Bah5JhBd.d.mts} +1 -1
- package/dist/{eventPlugin-CVxlE6De.d.mts → eventPlugin-D9DKB2zM.d.mts} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +1 -1
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +2 -2
- package/dist/filesUpload-C7r7HIeA.mjs +319 -0
- 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/mongodb.d.mts +1 -1
- package/dist/idempotency/redis.d.mts +2 -2
- package/dist/idempotency/redis.mjs +134 -13
- package/dist/{index-CSkeivBx.d.mts → index-BLXBmWud.d.mts} +3 -3
- package/dist/{index-BgmMdpm8.d.mts → index-C1meYuDn.d.mts} +1 -1
- package/dist/{index-CpTSDqmD.d.mts → index-DtDzOBn8.d.mts} +3 -3
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +4 -4
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/jobs.d.mts +25 -3
- package/dist/integrations/jobs.mjs +63 -4
- package/dist/integrations/mcp/index.d.mts +51 -3
- package/dist/integrations/mcp/index.mjs +78 -19
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/{interface-BVuMfeVv.d.mts → interface-CMRutPfe.d.mts} +38 -16
- package/dist/{mongodb-B8U2xaLj.d.mts → mongodb-BsP-WbhN.d.mts} +1 -1
- package/dist/{mongodb-X7LbEjTN.d.mts → mongodb-CTcp0hQZ.d.mts} +1 -1
- package/dist/{openapi-CYCuekCn.mjs → openapi-CbKUJY_m.mjs} +3 -3
- package/dist/org/index.d.mts +2 -2
- package/dist/permissions/index.d.mts +3 -3
- package/dist/plugins/index.d.mts +4 -4
- package/dist/plugins/index.mjs +8 -8
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/policies/index.d.mts +1 -1
- package/dist/presets/filesUpload.d.mts +49 -0
- package/dist/presets/filesUpload.mjs +2 -0
- package/dist/presets/index.d.mts +3 -2
- package/dist/presets/index.mjs +2 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/{queryCachePlugin-CnTZZTC5.d.mts → queryCachePlugin-BJJGBTlu.d.mts} +1 -1
- package/dist/redis-BM00zaPB.d.mts +115 -0
- package/dist/{redis-stream-D54N5oXs.d.mts → redis-stream-CrsfUmPt.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{resourceToTools-O_HwWXFa.mjs → resourceToTools-8s-EsCCe.mjs} +1 -1
- package/dist/rpc/index.d.mts +1 -1
- package/dist/{schemaConverter-OxfCshus.mjs → schemaConverter-Y7nCYaLJ.mjs} +24 -8
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +1 -1
- package/dist/{sse-CJpt7LGI.mjs → sse-Ad7ypl9e.mjs} +1 -1
- package/dist/storage-Dfzt4VTl.d.mts +146 -0
- package/dist/testing/index.d.mts +4 -3
- package/dist/testing/index.mjs +3 -2
- package/dist/testing/storageContract.d.mts +26 -0
- package/dist/testing/storageContract.mjs +216 -0
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +2 -0
- package/dist/types/storage.mjs +1 -0
- package/dist/{types-CcG4avic.d.mts → types-BsbNMEDR.d.mts} +1 -1
- package/dist/{types-Bg2X42_m.d.mts → types-Ch9pTQbf.d.mts} +9 -9
- package/dist/{types-CVC4HOKi.d.mts → types-DZi1aYhm.d.mts} +1 -1
- package/dist/utils/index.d.mts +26 -8
- package/dist/utils/index.mjs +1 -1
- package/package.json +16 -1
- package/skills/arc/SKILL.md +22 -0
- package/skills/arc/references/events.md +29 -0
- package/skills/arc/references/mcp.md +37 -0
- package/dist/redis-z3sFr1UP.d.mts +0 -49
- /package/dist/{EventTransport-CinyO7zQ.d.mts → EventTransport-BXja8NOc.d.mts} +0 -0
- /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-HprTmvVY.mjs} +0 -0
- /package/dist/{ResourceRegistry-Dq3_zBQP.mjs → ResourceRegistry-C6uXlWe3.mjs} +0 -0
- /package/dist/{caching-CjybdRwx.mjs → caching-IMuYVjTL.mjs} +0 -0
- /package/dist/{circuitBreaker-CvXkjfrW.d.mts → circuitBreaker-dTtG-UyS.d.mts} +0 -0
- /package/dist/{elevation-s5ykdNHr.d.mts → elevation-B6S5csVA.d.mts} +0 -0
- /package/dist/{errorHandler-mzqk4cGl.mjs → errorHandler-f869_8PQ.mjs} +0 -0
- /package/dist/{errors-Bmn3eZT6.d.mts → errors-Ck2h67pm.d.mts} +0 -0
- /package/dist/{eventPlugin-D91S2YF4.mjs → eventPlugin-CDjVTM82.mjs} +0 -0
- /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BnkYrNzp.d.mts} +0 -0
- /package/dist/{fields-DC4So2M2.d.mts → fields-DpZQa_Q3.d.mts} +0 -0
- /package/dist/{interface-DplgQO2e.d.mts → interface-4y979v99.d.mts} +0 -0
- /package/dist/{interface-B-pe8fhj.d.mts → interface-DfLGcus7.d.mts} +0 -0
- /package/dist/{loadResources-Bksk8ydA.mjs → loadResources-PWd0OCpV.mjs} +0 -0
- /package/dist/{logger-CDjpjySd.mjs → logger-D1YrIImS.mjs} +0 -0
- /package/dist/{metrics-TuOmguhi.mjs → metrics-B-PU4-Yu.mjs} +0 -0
- /package/dist/{mongodb-B5O6xaW1.mjs → mongodb-Utc5k_-0.mjs} +0 -0
- /package/dist/{pluralize-A0tWEl1K.mjs → pluralize-CWP6MB39.mjs} +0 -0
- /package/dist/{queryCachePlugin-D0iIVhW_.mjs → queryCachePlugin-BH-fidlv.mjs} +0 -0
- /package/dist/{registry-B0Wl7uVV.mjs → registry-BiTKT1Dg.mjs} +0 -0
- /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-CxkYGT81.mjs} +0 -0
- /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-DDCmiNIo.d.mts} +0 -0
- /package/dist/{tracing-DxjKk7eW.d.mts → tracing-DdN2-wHJ.d.mts} +0 -0
- /package/dist/{types-C72d3NDn.d.mts → types-BD85MlEK.d.mts} +0 -0
- /package/dist/{versioning-Cm8qoFDg.mjs → versioning-CDugduqI.mjs} +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
//#region src/testing/storageContract.ts
|
|
3
|
+
/**
|
|
4
|
+
* Storage Contract Suite
|
|
5
|
+
*
|
|
6
|
+
* Any implementation of `@classytic/arc/types/storage`'s `Storage` interface
|
|
7
|
+
* can import this and run it against a live instance to guarantee preset
|
|
8
|
+
* compatibility. Passing this suite is the contract.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { runStorageContract } from '@classytic/arc/testing/storage';
|
|
13
|
+
* import { s3Storage } from '../src/storage/s3-storage.js';
|
|
14
|
+
*
|
|
15
|
+
* runStorageContract('s3Storage', async () => {
|
|
16
|
+
* const storage = s3Storage({ bucket: 'test-bucket' });
|
|
17
|
+
* return { storage, teardown: async () => {} };
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* This module statically imports `vitest`. Only load it from test code — arc's
|
|
22
|
+
* production bundle never references this subpath, so the import tree stays
|
|
23
|
+
* clean under tree-shaking.
|
|
24
|
+
*/
|
|
25
|
+
function makeBytes(size, seed = 0) {
|
|
26
|
+
const buf = Buffer.allocUnsafe(size);
|
|
27
|
+
for (let i = 0; i < size; i++) buf[i] = i + seed & 255;
|
|
28
|
+
return buf;
|
|
29
|
+
}
|
|
30
|
+
async function readAll(result) {
|
|
31
|
+
if (result.kind === "buffer") return result.buffer;
|
|
32
|
+
const chunks = [];
|
|
33
|
+
for await (const chunk of result.stream) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
34
|
+
return Buffer.concat(chunks);
|
|
35
|
+
}
|
|
36
|
+
const EMPTY_CTX = { scope: {} };
|
|
37
|
+
function ctxFor(scope) {
|
|
38
|
+
return { scope };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Register the storage contract suite under the caller's name.
|
|
42
|
+
*
|
|
43
|
+
* Assertions covered:
|
|
44
|
+
* 1. upload() returns a StorageFile with every required field populated
|
|
45
|
+
* 2. read(upload.id) round-trips the exact bytes
|
|
46
|
+
* 3. delete() returns true on first call
|
|
47
|
+
* 4. delete() returns false (or throws) on a missing id
|
|
48
|
+
* 5. exists() (if implemented) agrees with upload/delete state
|
|
49
|
+
* 6. resolveUrl() (if implemented) returns a non-empty URL for an existing id
|
|
50
|
+
* 7. Two isolated scopes don't collide (scope threading)
|
|
51
|
+
* 8. Full lifecycle: upload → read → delete → read rejects
|
|
52
|
+
* 9. Both `kind: "stream"` and `kind: "buffer"` read results deliver correct bytes
|
|
53
|
+
* 10. Ranged reads (if adapter supports them) slice correctly
|
|
54
|
+
*/
|
|
55
|
+
function runStorageContract(name, setup) {
|
|
56
|
+
describe(`Storage contract — ${name}`, () => {
|
|
57
|
+
let storage;
|
|
58
|
+
let teardown;
|
|
59
|
+
beforeAll(async () => {
|
|
60
|
+
const result = await setup();
|
|
61
|
+
storage = result.storage;
|
|
62
|
+
teardown = result.teardown;
|
|
63
|
+
});
|
|
64
|
+
afterAll(async () => {
|
|
65
|
+
if (teardown) await teardown();
|
|
66
|
+
});
|
|
67
|
+
it("upload() returns a populated StorageFile", async () => {
|
|
68
|
+
const bytes = makeBytes(64);
|
|
69
|
+
const file = await storage.upload({
|
|
70
|
+
buffer: bytes,
|
|
71
|
+
filename: "contract-1.bin",
|
|
72
|
+
mimeType: "application/octet-stream",
|
|
73
|
+
size: bytes.length
|
|
74
|
+
}, EMPTY_CTX);
|
|
75
|
+
expect(file.id).toBeTruthy();
|
|
76
|
+
expect(file.url).toBeTruthy();
|
|
77
|
+
expect(file.pathname).toBeTruthy();
|
|
78
|
+
expect(file.contentType).toBe("application/octet-stream");
|
|
79
|
+
expect(file.bytes).toBe(bytes.length);
|
|
80
|
+
await storage.delete(file.id, EMPTY_CTX);
|
|
81
|
+
});
|
|
82
|
+
it("read() round-trips the exact bytes uploaded", async () => {
|
|
83
|
+
const bytes = makeBytes(1024, 7);
|
|
84
|
+
const file = await storage.upload({
|
|
85
|
+
buffer: bytes,
|
|
86
|
+
filename: "contract-2.bin",
|
|
87
|
+
mimeType: "application/octet-stream",
|
|
88
|
+
size: bytes.length
|
|
89
|
+
}, EMPTY_CTX);
|
|
90
|
+
const read = await storage.read(file.id, EMPTY_CTX);
|
|
91
|
+
expect((await readAll(read)).equals(bytes)).toBe(true);
|
|
92
|
+
expect(read.contentType).toBe("application/octet-stream");
|
|
93
|
+
await storage.delete(file.id, EMPTY_CTX);
|
|
94
|
+
});
|
|
95
|
+
it("delete() returns true the first time, false (or throws) the second time", async () => {
|
|
96
|
+
const bytes = makeBytes(32);
|
|
97
|
+
const file = await storage.upload({
|
|
98
|
+
buffer: bytes,
|
|
99
|
+
filename: "contract-3.bin",
|
|
100
|
+
mimeType: "application/octet-stream",
|
|
101
|
+
size: bytes.length
|
|
102
|
+
}, EMPTY_CTX);
|
|
103
|
+
expect(await storage.delete(file.id, EMPTY_CTX)).toBe(true);
|
|
104
|
+
let second = "threw";
|
|
105
|
+
try {
|
|
106
|
+
second = await storage.delete(file.id, EMPTY_CTX);
|
|
107
|
+
} catch {
|
|
108
|
+
second = "threw";
|
|
109
|
+
}
|
|
110
|
+
expect(second === false || second === "threw").toBe(true);
|
|
111
|
+
});
|
|
112
|
+
it("exists() agrees with upload/delete state (if implemented)", async () => {
|
|
113
|
+
if (!storage.exists) return;
|
|
114
|
+
const bytes = makeBytes(16);
|
|
115
|
+
const file = await storage.upload({
|
|
116
|
+
buffer: bytes,
|
|
117
|
+
filename: "contract-4.bin",
|
|
118
|
+
mimeType: "application/octet-stream",
|
|
119
|
+
size: bytes.length
|
|
120
|
+
}, EMPTY_CTX);
|
|
121
|
+
expect(await storage.exists(file.id, EMPTY_CTX)).toBe(true);
|
|
122
|
+
await storage.delete(file.id, EMPTY_CTX);
|
|
123
|
+
expect(await storage.exists(file.id, EMPTY_CTX)).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
it("resolveUrl() returns a non-empty URL for an existing id (if implemented)", async () => {
|
|
126
|
+
if (!storage.resolveUrl) return;
|
|
127
|
+
const bytes = makeBytes(8);
|
|
128
|
+
const file = await storage.upload({
|
|
129
|
+
buffer: bytes,
|
|
130
|
+
filename: "contract-5.bin",
|
|
131
|
+
mimeType: "application/octet-stream",
|
|
132
|
+
size: bytes.length
|
|
133
|
+
}, EMPTY_CTX);
|
|
134
|
+
const url = await storage.resolveUrl(file.id, EMPTY_CTX);
|
|
135
|
+
expect(typeof url).toBe("string");
|
|
136
|
+
expect(url.length).toBeGreaterThan(0);
|
|
137
|
+
await storage.delete(file.id, EMPTY_CTX);
|
|
138
|
+
});
|
|
139
|
+
it("two different scopes get distinct ids (scope threading)", async () => {
|
|
140
|
+
const bytes = makeBytes(24, 42);
|
|
141
|
+
const scopeA = ctxFor({ organizationId: "org-a" });
|
|
142
|
+
const scopeB = ctxFor({ organizationId: "org-b" });
|
|
143
|
+
const a = await storage.upload({
|
|
144
|
+
buffer: bytes,
|
|
145
|
+
filename: "scoped.bin",
|
|
146
|
+
mimeType: "application/octet-stream",
|
|
147
|
+
size: bytes.length
|
|
148
|
+
}, scopeA);
|
|
149
|
+
const b = await storage.upload({
|
|
150
|
+
buffer: bytes,
|
|
151
|
+
filename: "scoped.bin",
|
|
152
|
+
mimeType: "application/octet-stream",
|
|
153
|
+
size: bytes.length
|
|
154
|
+
}, scopeB);
|
|
155
|
+
expect(a.id).not.toBe(b.id);
|
|
156
|
+
const readA = await readAll(await storage.read(a.id, scopeA));
|
|
157
|
+
const readB = await readAll(await storage.read(b.id, scopeB));
|
|
158
|
+
expect(readA.equals(bytes)).toBe(true);
|
|
159
|
+
expect(readB.equals(bytes)).toBe(true);
|
|
160
|
+
await storage.delete(a.id, scopeA);
|
|
161
|
+
await storage.delete(b.id, scopeB);
|
|
162
|
+
});
|
|
163
|
+
it("full lifecycle: upload → read → delete → read rejects", async () => {
|
|
164
|
+
const bytes = makeBytes(128);
|
|
165
|
+
const file = await storage.upload({
|
|
166
|
+
buffer: bytes,
|
|
167
|
+
filename: "lifecycle.bin",
|
|
168
|
+
mimeType: "application/octet-stream",
|
|
169
|
+
size: bytes.length
|
|
170
|
+
}, EMPTY_CTX);
|
|
171
|
+
expect((await readAll(await storage.read(file.id, EMPTY_CTX))).equals(bytes)).toBe(true);
|
|
172
|
+
expect(await storage.delete(file.id, EMPTY_CTX)).toBe(true);
|
|
173
|
+
let rejected = false;
|
|
174
|
+
try {
|
|
175
|
+
if (!(await readAll(await storage.read(file.id, EMPTY_CTX))).equals(bytes)) rejected = true;
|
|
176
|
+
} catch {
|
|
177
|
+
rejected = true;
|
|
178
|
+
}
|
|
179
|
+
expect(rejected).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
it("read() handles both stream and buffer kinds", async () => {
|
|
182
|
+
const bytes = makeBytes(256, 9);
|
|
183
|
+
const file = await storage.upload({
|
|
184
|
+
buffer: bytes,
|
|
185
|
+
filename: "kind.bin",
|
|
186
|
+
mimeType: "application/octet-stream",
|
|
187
|
+
size: bytes.length
|
|
188
|
+
}, EMPTY_CTX);
|
|
189
|
+
const result = await storage.read(file.id, EMPTY_CTX);
|
|
190
|
+
expect(result.kind === "stream" || result.kind === "buffer").toBe(true);
|
|
191
|
+
expect((await readAll(result)).equals(bytes)).toBe(true);
|
|
192
|
+
await storage.delete(file.id, EMPTY_CTX);
|
|
193
|
+
});
|
|
194
|
+
it("read() with a mid-object range slices correctly (when adapter supports ranges)", async () => {
|
|
195
|
+
const bytes = makeBytes(1024, 13);
|
|
196
|
+
const file = await storage.upload({
|
|
197
|
+
buffer: bytes,
|
|
198
|
+
filename: "range.bin",
|
|
199
|
+
mimeType: "application/octet-stream",
|
|
200
|
+
size: bytes.length
|
|
201
|
+
}, EMPTY_CTX);
|
|
202
|
+
const result = await storage.read(file.id, EMPTY_CTX, {
|
|
203
|
+
start: 100,
|
|
204
|
+
end: 199
|
|
205
|
+
});
|
|
206
|
+
const actual = await readAll(result);
|
|
207
|
+
if (result.range) {
|
|
208
|
+
expect(actual.length).toBe(100);
|
|
209
|
+
expect(actual.equals(bytes.subarray(100, 200))).toBe(true);
|
|
210
|
+
} else expect(actual.length).toBe(bytes.length);
|
|
211
|
+
await storage.delete(file.id, EMPTY_CTX);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
export { runStorageContract };
|
package/dist/types/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { _ as isAuthenticated, c as getOrgRoles, g as hasOrgAccess, n as PUBLIC_SCOPE, p as getTeamId, r as RequestScope, s as getOrgId, t as AUTHENTICATED_SCOPE, v as isElevated, y as isMember } from "../types-
|
|
2
|
-
import { $ as PresetFunction, $t as DeleteOptions, A as EventsDecorator, B as InferResourceDoc, Bt as FastifyHandler, C as ConfigError, Ct as TypedResourceConfig, D as CrudRouterOptions, Dt as ValidationResult, E as CrudRouteKey, Et as ValidateOptions, F as GracefulShutdownOptions, G as LookupOption, H as IntrospectionPluginOptions, Ht as IControllerResponse, I as HealthCheck, J as ObjectId, K as MiddlewareConfig, L as HealthOptions, M as FastifyWithAuth, N as FastifyWithDecorators, O as CrudSchemas, Ot as envelope, P as FieldRule, Q as PopulateOption, Qt as DeleteManyResult, R as InferAdapterDoc, Rt as ControllerHandler, S as AuthenticatorContext, St as TypedRepository, T as CrudController, Tt as UserOrganization, U as JWTPayload, Ut as IRequestContext, V as IntrospectionData, Vt as IController, W as JwtContext, Wt as RouteHandler, X as OwnershipCheck, Xt as BulkWriteResult, Y as OpenApiSchemas, Yt as BulkWriteOperation, Z as ParsedQuery, Zt as CrudRepository, _ as ArcInternalMetadata, _t as RouteMcpConfig, an as PaginationParams, at as RegistryStats, b as AuthPluginOptions, bt as TokenPair, cn as RepositorySession, ct as RequestWithExtras, d as ActionHandlerFn, dt as ResourceHookContext, en as DeleteResult, et as PresetHook, f as ActionsMap, ft as ResourceHooks, g as ArcDecorator, gt as RouteHandlerMethod, h as ApiResponse, ht as RouteDefinition, in as PaginatedResult, it as RegistryEntry, j as FastifyRequestExtras, jt as BaseControllerOptions, k as EventDefinition, kt as getUserId, l as ActionDefinition, ln as UpdateManyResult, lt as ResourceCacheConfig, m as AnyRecord, mt as ResourcePermissions, nn as KeysetPaginatedResult, nt as QueryParserInterface, on as PaginationResult, ot as RequestContext, p as AdditionalRoute, pt as ResourceMetadata, q as MiddlewareHandler, rn as OffsetPaginatedResult, rt as RateLimitConfig, sn as QueryOptions, st as RequestIdOptions, tn as InferDoc, tt as PresetResult, u as ActionEntry, un as WriteOptions, ut as ResourceConfig, v as ArcRequest, vt as RouteSchemaOptions, w as ControllerQueryOptions, wt as UserLike, x as Authenticator, xt as TypedController, y as AuthHelpers, yt as ServiceContext, z as InferDocType, zt as ControllerLike } from "../interface-
|
|
3
|
-
import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "../types-
|
|
4
|
-
import { n as ElevationOptions, t as ElevationEvent } from "../elevation-
|
|
1
|
+
import { _ as isAuthenticated, c as getOrgRoles, g as hasOrgAccess, n as PUBLIC_SCOPE, p as getTeamId, r as RequestScope, s as getOrgId, t as AUTHENTICATED_SCOPE, v as isElevated, y as isMember } from "../types-BD85MlEK.mjs";
|
|
2
|
+
import { $ as PresetFunction, $t as DeleteOptions, A as EventsDecorator, B as InferResourceDoc, Bt as FastifyHandler, C as ConfigError, Ct as TypedResourceConfig, D as CrudRouterOptions, Dt as ValidationResult, E as CrudRouteKey, Et as ValidateOptions, F as GracefulShutdownOptions, G as LookupOption, H as IntrospectionPluginOptions, Ht as IControllerResponse, I as HealthCheck, J as ObjectId, K as MiddlewareConfig, L as HealthOptions, M as FastifyWithAuth, N as FastifyWithDecorators, O as CrudSchemas, Ot as envelope, P as FieldRule, Q as PopulateOption, Qt as DeleteManyResult, R as InferAdapterDoc, Rt as ControllerHandler, S as AuthenticatorContext, St as TypedRepository, T as CrudController, Tt as UserOrganization, U as JWTPayload, Ut as IRequestContext, V as IntrospectionData, Vt as IController, W as JwtContext, Wt as RouteHandler, X as OwnershipCheck, Xt as BulkWriteResult, Y as OpenApiSchemas, Yt as BulkWriteOperation, Z as ParsedQuery, Zt as CrudRepository, _ as ArcInternalMetadata, _t as RouteMcpConfig, an as PaginationParams, at as RegistryStats, b as AuthPluginOptions, bt as TokenPair, cn as RepositorySession, ct as RequestWithExtras, d as ActionHandlerFn, dt as ResourceHookContext, en as DeleteResult, et as PresetHook, f as ActionsMap, ft as ResourceHooks, g as ArcDecorator, gt as RouteHandlerMethod, h as ApiResponse, ht as RouteDefinition, in as PaginatedResult, it as RegistryEntry, j as FastifyRequestExtras, jt as BaseControllerOptions, k as EventDefinition, kt as getUserId, l as ActionDefinition, ln as UpdateManyResult, lt as ResourceCacheConfig, m as AnyRecord, mt as ResourcePermissions, nn as KeysetPaginatedResult, nt as QueryParserInterface, on as PaginationResult, ot as RequestContext, p as AdditionalRoute, pt as ResourceMetadata, q as MiddlewareHandler, rn as OffsetPaginatedResult, rt as RateLimitConfig, sn as QueryOptions, st as RequestIdOptions, tn as InferDoc, tt as PresetResult, u as ActionEntry, un as WriteOptions, ut as ResourceConfig, v as ArcRequest, vt as RouteSchemaOptions, w as ControllerQueryOptions, wt as UserLike, x as Authenticator, xt as TypedController, y as AuthHelpers, yt as ServiceContext, z as InferDocType, zt as ControllerLike } from "../interface-CMRutPfe.mjs";
|
|
3
|
+
import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "../types-DZi1aYhm.mjs";
|
|
4
|
+
import { n as ElevationOptions, t as ElevationEvent } from "../elevation-B6S5csVA.mjs";
|
|
5
5
|
export { AUTHENTICATED_SCOPE, ActionDefinition, ActionEntry, ActionHandlerFn, ActionsMap, AdditionalRoute, AnyRecord, ApiResponse, ArcDecorator, ArcInternalMetadata, ArcRequest, AuthHelpers, AuthPluginOptions, Authenticator, AuthenticatorContext, BaseControllerOptions, BulkWriteOperation, BulkWriteResult, ConfigError, ControllerHandler, ControllerLike, ControllerQueryOptions, CrudController, CrudRepository, CrudRouteKey, CrudRouterOptions, CrudSchemas, DeleteManyResult, DeleteOptions, DeleteResult, ElevationEvent, ElevationOptions, EventDefinition, EventsDecorator, FastifyHandler, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, IController, IControllerResponse, IRequestContext, InferAdapterDoc, InferDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, JwtContext, KeysetPaginatedResult, LookupOption, MiddlewareConfig, MiddlewareHandler, ObjectId, OffsetPaginatedResult, OpenApiSchemas, OwnershipCheck, PUBLIC_SCOPE, PaginatedResult, PaginationParams, PaginationResult, ParsedQuery, PermissionCheck, PermissionContext, PermissionResult, PopulateOption, PresetFunction, PresetHook, PresetResult, QueryOptions, QueryParserInterface, RateLimitConfig, RegistryEntry, RegistryStats, RepositorySession, RequestContext, RequestIdOptions, RequestScope, RequestWithExtras, ResourceCacheConfig, ResourceConfig, ResourceHookContext, ResourceHooks, ResourceMetadata, ResourcePermissions, RouteDefinition, RouteHandler, RouteHandlerMethod, RouteMcpConfig, RouteSchemaOptions, ServiceContext, TokenPair, TypedController, TypedRepository, TypedResourceConfig, UpdateManyResult, UserBase, UserLike, UserOrganization, ValidateOptions, ValidationResult, WriteOptions, envelope, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { x as Authenticator } from "./interface-
|
|
2
|
-
import { n as ElevationOptions } from "./elevation-
|
|
3
|
-
import { t as ExternalOpenApiPaths } from "./externalPaths-
|
|
4
|
-
import { i as CacheStore } from "./interface-
|
|
5
|
-
import { r as QueryCachePluginOptions } from "./queryCachePlugin-
|
|
6
|
-
import { i as EventTransport } from "./EventTransport-
|
|
7
|
-
import { t as EventPluginOptions } from "./eventPlugin-
|
|
8
|
-
import { f as SSEOptions, h as CachingOptions, i as VersioningOptions, l as MetricsOptions, t as ErrorHandlerOptions } from "./errorHandler-
|
|
9
|
-
import { r as IdempotencyStore } from "./interface-
|
|
1
|
+
import { x as Authenticator } from "./interface-CMRutPfe.mjs";
|
|
2
|
+
import { n as ElevationOptions } from "./elevation-B6S5csVA.mjs";
|
|
3
|
+
import { t as ExternalOpenApiPaths } from "./externalPaths-BnkYrNzp.mjs";
|
|
4
|
+
import { i as CacheStore } from "./interface-4y979v99.mjs";
|
|
5
|
+
import { r as QueryCachePluginOptions } from "./queryCachePlugin-BJJGBTlu.mjs";
|
|
6
|
+
import { i as EventTransport } from "./EventTransport-BXja8NOc.mjs";
|
|
7
|
+
import { t as EventPluginOptions } from "./eventPlugin-D9DKB2zM.mjs";
|
|
8
|
+
import { f as SSEOptions, h as CachingOptions, i as VersioningOptions, l as MetricsOptions, t as ErrorHandlerOptions } from "./errorHandler-Bah5JhBd.mjs";
|
|
9
|
+
import { r as IdempotencyStore } from "./interface-DfLGcus7.mjs";
|
|
10
10
|
import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, FastifyServerOptions } from "fastify";
|
|
11
11
|
|
|
12
12
|
//#region src/factory/loadResources.d.ts
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Y as OpenApiSchemas, Z as ParsedQuery, m as AnyRecord, nt as QueryParserInterface } from "../interface-
|
|
2
|
-
import { a as NotFoundError, c as RateLimitError, d as ValidationError, f as createDomainError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-
|
|
3
|
-
import { a as CircuitBreakerStats, c as createCircuitBreakerRegistry, i as CircuitBreakerRegistry, n as CircuitBreakerError, o as CircuitState, r as CircuitBreakerOptions, s as createCircuitBreaker, t as CircuitBreaker } from "../circuitBreaker-
|
|
1
|
+
import { Y as OpenApiSchemas, Z as ParsedQuery, m as AnyRecord, nt as QueryParserInterface } from "../interface-CMRutPfe.mjs";
|
|
2
|
+
import { a as NotFoundError, c as RateLimitError, d as ValidationError, f as createDomainError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-Ck2h67pm.mjs";
|
|
3
|
+
import { a as CircuitBreakerStats, c as createCircuitBreakerRegistry, i as CircuitBreakerRegistry, n as CircuitBreakerError, o as CircuitState, r as CircuitBreakerOptions, s as createCircuitBreaker, t as CircuitBreaker } from "../circuitBreaker-dTtG-UyS.mjs";
|
|
4
4
|
import { FastifyInstance, FastifyReply, FastifyRequest, RouteHandlerMethod } from "fastify";
|
|
5
5
|
|
|
6
6
|
//#region src/utils/compensation.d.ts
|
|
@@ -506,6 +506,14 @@ declare function getListQueryParams(): AnyRecord;
|
|
|
506
506
|
declare function getDefaultCrudSchemas(): Record<string, Record<string, unknown>>;
|
|
507
507
|
//#endregion
|
|
508
508
|
//#region src/utils/schemaConverter.d.ts
|
|
509
|
+
/**
|
|
510
|
+
* Supported JSON Schema output targets for Zod v4's `toJSONSchema()`.
|
|
511
|
+
* - `draft-7`: Fastify/AJV validation (default)
|
|
512
|
+
* - `draft-2020-12`: AJV 2020 (opt-in, requires ajv/dist/2020)
|
|
513
|
+
* - `openapi-3.0`: OpenAPI 3.0 document generation
|
|
514
|
+
* - `openapi-3.1`: OpenAPI 3.1 document generation
|
|
515
|
+
*/
|
|
516
|
+
type JsonSchemaTarget = "draft-7" | "draft-2020-12" | "openapi-3.0" | "openapi-3.1";
|
|
509
517
|
/**
|
|
510
518
|
* Check if an object is already a plain JSON Schema.
|
|
511
519
|
* Returns true if it has JSON Schema markers (`type`, `properties`, `$ref`,
|
|
@@ -522,15 +530,22 @@ declare function isZodSchema(input: unknown): boolean;
|
|
|
522
530
|
* Detection order:
|
|
523
531
|
* 1. `null`/`undefined` → `undefined`
|
|
524
532
|
* 2. Already JSON Schema → pass through as-is (zero overhead)
|
|
525
|
-
* 3. Zod v4 schema → `z.toJSONSchema(schema, { target
|
|
533
|
+
* 3. Zod v4 schema → `z.toJSONSchema(schema, { target })`
|
|
526
534
|
* 4. Unrecognized object → return as-is (treat as opaque schema)
|
|
535
|
+
*
|
|
536
|
+
* @param input Schema (Zod, plain JSON Schema, or opaque object)
|
|
537
|
+
* @param target Output target — defaults to `draft-7` for Fastify compatibility.
|
|
538
|
+
* Pass `openapi-3.0`/`openapi-3.1` for OpenAPI document generation.
|
|
527
539
|
*/
|
|
528
|
-
declare function toJsonSchema(input: unknown): Record<string, unknown> | undefined;
|
|
540
|
+
declare function toJsonSchema(input: unknown, target?: JsonSchemaTarget): Record<string, unknown> | undefined;
|
|
529
541
|
/**
|
|
530
542
|
* Convert all schema fields in an OpenApiSchemas object.
|
|
531
543
|
* JSON Schema values pass through unchanged. Only Zod schemas are converted.
|
|
544
|
+
*
|
|
545
|
+
* Defaults to the `openapi-3.0` target since this function feeds OpenAPI doc
|
|
546
|
+
* generation, not Fastify route validation.
|
|
532
547
|
*/
|
|
533
|
-
declare function convertOpenApiSchemas(schemas: OpenApiSchemas): OpenApiSchemas;
|
|
548
|
+
declare function convertOpenApiSchemas(schemas: OpenApiSchemas, target?: JsonSchemaTarget): OpenApiSchemas;
|
|
534
549
|
/**
|
|
535
550
|
* Convert schema values in a Fastify route schema record.
|
|
536
551
|
*
|
|
@@ -540,8 +555,11 @@ declare function convertOpenApiSchemas(schemas: OpenApiSchemas): OpenApiSchemas;
|
|
|
540
555
|
* JSON Schema values pass through unchanged. Only Zod schemas are converted.
|
|
541
556
|
*
|
|
542
557
|
* Used for both additionalRoutes and customSchemas (CRUD overrides).
|
|
558
|
+
*
|
|
559
|
+
* Defaults to `draft-7` so Fastify v5's bundled AJV 8 accepts the output.
|
|
560
|
+
* Pass `openapi-3.0` (or `openapi-3.1`) when generating OpenAPI documents.
|
|
543
561
|
*/
|
|
544
|
-
declare function convertRouteSchema(schema: Record<string, unknown
|
|
562
|
+
declare function convertRouteSchema(schema: Record<string, unknown>, target?: JsonSchemaTarget): Record<string, unknown>;
|
|
545
563
|
//#endregion
|
|
546
564
|
//#region src/utils/stateMachine.d.ts
|
|
547
565
|
/**
|
|
@@ -673,4 +691,4 @@ declare function hasEvents(instance: FastifyInstance): instance is FastifyInstan
|
|
|
673
691
|
events: EventsDecorator;
|
|
674
692
|
};
|
|
675
693
|
//#endregion
|
|
676
|
-
export { ArcError, ArcQueryParser, type ArcQueryParserOptions, CircuitBreaker, CircuitBreakerError, type CircuitBreakerOptions, CircuitBreakerRegistry, type CircuitBreakerStats, CircuitState, type CompensationDefinition, type CompensationError, type CompensationHooks, type CompensationResult, type CompensationStep, ConflictError, type ErrorDetails, type EventsDecorator, ForbiddenError, type Guard, type GuardConfig, type JsonSchema, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, type StateMachine, type TransitionConfig, UnauthorizedError, ValidationError, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, defineCompensation, defineGuard, deleteResponse, errorResponseSchema, getDefaultCrudSchemas, getListQueryParams, handleRaw, hasEvents, isArcError, isJsonSchema, isZodSchema, itemResponse, listResponse, mutationResponse, paginationSchema, queryParams, responses, successResponseSchema, toJsonSchema, withCompensation, wrapResponse };
|
|
694
|
+
export { ArcError, ArcQueryParser, type ArcQueryParserOptions, CircuitBreaker, CircuitBreakerError, type CircuitBreakerOptions, CircuitBreakerRegistry, type CircuitBreakerStats, CircuitState, type CompensationDefinition, type CompensationError, type CompensationHooks, type CompensationResult, type CompensationStep, ConflictError, type ErrorDetails, type EventsDecorator, ForbiddenError, type Guard, type GuardConfig, type JsonSchema, type JsonSchemaTarget, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, type StateMachine, type TransitionConfig, UnauthorizedError, ValidationError, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createDomainError, createError, createQueryParser, createStateMachine, defineCompensation, defineGuard, deleteResponse, errorResponseSchema, getDefaultCrudSchemas, getListQueryParams, handleRaw, hasEvents, isArcError, isJsonSchema, isZodSchema, itemResponse, listResponse, mutationResponse, paginationSchema, queryParams, responses, successResponseSchema, toJsonSchema, withCompensation, wrapResponse };
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as createQueryParser, t as ArcQueryParser } from "../queryParser-CgCtsjti.mjs";
|
|
2
|
-
import { a as toJsonSchema, i as isZodSchema, n as convertRouteSchema, r as isJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-
|
|
2
|
+
import { a as toJsonSchema, i as isZodSchema, n as convertRouteSchema, r as isJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-Y7nCYaLJ.mjs";
|
|
3
3
|
import { a as createCircuitBreaker, i as CircuitState, n as CircuitBreakerError, o as createCircuitBreakerRegistry, r as CircuitBreakerRegistry, t as CircuitBreaker } from "../circuitBreaker-cmi5XDv5.mjs";
|
|
4
4
|
import { _ as withCompensation, a as getListQueryParams, c as mutationResponse, d as responses, f as successResponseSchema, g as defineCompensation, h as defineGuard, i as getDefaultCrudSchemas, l as paginationSchema, m as handleRaw, n as deleteResponse, o as itemResponse, p as wrapResponse, r as errorResponseSchema, s as listResponse, t as createStateMachine, u as queryParams } from "../utils-yYT3HDXt.mjs";
|
|
5
5
|
import { a as OrgAccessDeniedError, c as ServiceUnavailableError, d as createDomainError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-BF2bIOIS.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/arc",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.5",
|
|
4
4
|
"description": "Resource-oriented backend framework for Fastify — clean, minimal, powerful, tree-shakable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"types": "./dist/types/index.d.mts",
|
|
21
21
|
"default": "./dist/types/index.mjs"
|
|
22
22
|
},
|
|
23
|
+
"./types/storage": {
|
|
24
|
+
"types": "./dist/types/storage.d.mts",
|
|
25
|
+
"default": "./dist/types/storage.mjs"
|
|
26
|
+
},
|
|
23
27
|
"./adapters": {
|
|
24
28
|
"types": "./dist/adapters/index.d.mts",
|
|
25
29
|
"default": "./dist/adapters/index.mjs"
|
|
@@ -36,6 +40,10 @@
|
|
|
36
40
|
"types": "./dist/presets/multiTenant.d.mts",
|
|
37
41
|
"default": "./dist/presets/multiTenant.mjs"
|
|
38
42
|
},
|
|
43
|
+
"./presets/files-upload": {
|
|
44
|
+
"types": "./dist/presets/filesUpload.d.mts",
|
|
45
|
+
"default": "./dist/presets/filesUpload.mjs"
|
|
46
|
+
},
|
|
39
47
|
"./auth": {
|
|
40
48
|
"types": "./dist/auth/index.d.mts",
|
|
41
49
|
"default": "./dist/auth/index.mjs"
|
|
@@ -116,6 +124,10 @@
|
|
|
116
124
|
"types": "./dist/testing/index.d.mts",
|
|
117
125
|
"default": "./dist/testing/index.mjs"
|
|
118
126
|
},
|
|
127
|
+
"./testing/storage": {
|
|
128
|
+
"types": "./dist/testing/storageContract.d.mts",
|
|
129
|
+
"default": "./dist/testing/storageContract.mjs"
|
|
130
|
+
},
|
|
119
131
|
"./policies": {
|
|
120
132
|
"types": "./dist/policies/index.d.mts",
|
|
121
133
|
"default": "./dist/policies/index.mjs"
|
|
@@ -364,7 +376,10 @@
|
|
|
364
376
|
"@vitest/coverage-v8": "^3.2.4",
|
|
365
377
|
"ajv": "^8.18.0",
|
|
366
378
|
"better-auth": "^1.6.2",
|
|
379
|
+
"bullmq": "^5.73.5",
|
|
380
|
+
"dotenv": "^17.4.2",
|
|
367
381
|
"fastify-raw-body": "^5.0.0",
|
|
382
|
+
"ioredis": "^5.10.1",
|
|
368
383
|
"jsonwebtoken": "^9.0.0",
|
|
369
384
|
"knip": "^6.4.1",
|
|
370
385
|
"mongodb": "^7.1.0",
|
package/skills/arc/SKILL.md
CHANGED
|
@@ -856,6 +856,28 @@ auth: async (headers) => ({
|
|
|
856
856
|
|
|
857
857
|
**Guards** for custom tools: `guard(requireAuth, requireOrg, requireRole('admin'), handler)`
|
|
858
858
|
|
|
859
|
+
**AI SDK bridge** (v2.8.4+) — expose AI SDK `tool()` definitions over MCP without duplicating glue. Handles auth, guards, `{ error } → isError` translation, and thrown-error mapping:
|
|
860
|
+
|
|
861
|
+
```typescript
|
|
862
|
+
import { bridgeToMcp, buildMcpToolsFromBridges, getUserId, hasOrg, type McpBridge } from '@classytic/arc/mcp';
|
|
863
|
+
|
|
864
|
+
export const triggerJobBridge: McpBridge = {
|
|
865
|
+
name: 'trigger_job',
|
|
866
|
+
description: 'Start a job.',
|
|
867
|
+
inputSchema: { phase: z.enum(['investigate', 'fix']) },
|
|
868
|
+
annotations: { destructiveHint: true },
|
|
869
|
+
buildTool: (ctx) => buildTriggerJobTool(getUserId(ctx) ?? ''),
|
|
870
|
+
guard: (ctx) => (hasOrg(ctx) ? null : 'Organization scope required'),
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
await app.register(mcpPlugin, {
|
|
874
|
+
resources,
|
|
875
|
+
extraTools: buildMcpToolsFromBridges([triggerJobBridge], {
|
|
876
|
+
exclude: process.env.DEPLOYMENT === 'readonly' ? ['trigger_job'] : [],
|
|
877
|
+
}),
|
|
878
|
+
});
|
|
879
|
+
```
|
|
880
|
+
|
|
859
881
|
**Service scope**: When `clientId` is set in auth result, MCP produces `kind: "service"` RequestScope — works with `requireServiceScope()`, `getClientId()`, `getServiceScopes()`. No synthetic userId needed for machine principals.
|
|
860
882
|
|
|
861
883
|
**Multi-tenancy**: `organizationId` from auth flows into BaseController org-scoping automatically.
|
|
@@ -168,6 +168,35 @@ class KafkaTransport implements EventTransport {
|
|
|
168
168
|
| Redis Pub/Sub | `@classytic/arc/events/redis` | Multi-instance, real-time |
|
|
169
169
|
| Redis Streams | `@classytic/arc/events/redis-stream` | Ordered, persistent, consumer groups |
|
|
170
170
|
|
|
171
|
+
### Streams vs Pub/Sub — pick the right one
|
|
172
|
+
|
|
173
|
+
Choosing wrong loses messages silently. Default to **Streams** for anything business-critical.
|
|
174
|
+
|
|
175
|
+
| Requirement | Use |
|
|
176
|
+
|---|---|
|
|
177
|
+
| Message MUST NOT be lost (billing, payments, audit) | **Streams** |
|
|
178
|
+
| Real-time notifications, OK to miss when no subscriber is up | Pub/Sub |
|
|
179
|
+
| Need to replay/reprocess past events | **Streams** |
|
|
180
|
+
| Multiple workers processing the same queue | **Streams** (consumer groups) |
|
|
181
|
+
| Simple broadcast to live WebSocket clients | Pub/Sub |
|
|
182
|
+
| Event sourcing or audit trail | **Streams** |
|
|
183
|
+
| Single-instance dev | Memory |
|
|
184
|
+
| At-least-once delivery with durable WAL | **Streams** + outbox pattern |
|
|
185
|
+
|
|
186
|
+
**Why it matters:** Pub/Sub is fire-and-forget. If no subscriber is connected when you publish, the message is gone. Streams persist until every consumer group acknowledges them — crashes, restarts, and network blips are survivable.
|
|
187
|
+
|
|
188
|
+
**Defense-in-depth:** pair `eventPlugin` with the transactional outbox (`EventOutbox` + `MemoryOutboxStore` or your own persistent store) for guaranteed delivery even if Redis is unreachable at publish time.
|
|
189
|
+
|
|
190
|
+
### Redis eviction policy — required for queues and idempotency
|
|
191
|
+
|
|
192
|
+
When you back events (Streams), jobs (BullMQ), idempotency, or cache with Redis, your Redis instance **must** be configured with `maxmemory-policy: noeviction`. Any other policy can silently evict in-flight stream entries or pending jobs.
|
|
193
|
+
|
|
194
|
+
- **Self-hosted Redis:** `redis-cli CONFIG SET maxmemory-policy noeviction` (or set in `redis.conf`).
|
|
195
|
+
- **Upstash:** free/paid DBs default to `optimistic-volatile`. You'll see `IMPORTANT! Eviction policy is optimistic-volatile. It should be "noeviction"` in BullMQ logs. **Do one of:** open a support ticket to request `noeviction`, use a dedicated DB for queues, or accept that long-idle jobs may be evicted.
|
|
196
|
+
- **ElastiCache / Redis Cloud:** set the parameter group's `maxmemory-policy` to `noeviction` before pointing arc at it.
|
|
197
|
+
|
|
198
|
+
For a pure cache DB (no queues, no idempotency), `allkeys-lru` is correct and what you want.
|
|
199
|
+
|
|
171
200
|
## Injectable Logger
|
|
172
201
|
|
|
173
202
|
All transports and retry accept a `logger` option — defaults to `console`, compatible with pino/fastify.log:
|
|
@@ -430,6 +430,43 @@ const createShape = fieldRulesToZod(resource.schemaOptions.fieldRules, {
|
|
|
430
430
|
// → { name: z.string(), price: z.number(), category: z.enum([...]) }
|
|
431
431
|
```
|
|
432
432
|
|
|
433
|
+
## AI SDK Bridge (v2.8.4+)
|
|
434
|
+
|
|
435
|
+
Expose AI SDK `tool()` definitions over MCP without duplicating glue. The bridge handles `isAuthenticated` rejection, optional custom guards, `{ error }` → `isError: true` envelope translation, and thrown-error mapping.
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
import { bridgeToMcp, buildMcpToolsFromBridges, getUserId, hasOrg, type McpBridge } from '@classytic/arc/mcp';
|
|
439
|
+
import { tool } from 'ai';
|
|
440
|
+
import { z } from 'zod';
|
|
441
|
+
|
|
442
|
+
function buildTriggerJobTool(companyId: string) {
|
|
443
|
+
return tool({
|
|
444
|
+
description: 'Start a job.',
|
|
445
|
+
inputSchema: z.object({ phase: z.enum(['investigate', 'fix']) }),
|
|
446
|
+
execute: async ({ phase }) => ({ jobId: `${companyId}-${phase}-${Date.now()}` }),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export const triggerJobBridge: McpBridge = {
|
|
451
|
+
name: 'trigger_job',
|
|
452
|
+
description: 'Start a job.',
|
|
453
|
+
inputSchema: { phase: z.enum(['investigate', 'fix']) },
|
|
454
|
+
annotations: { destructiveHint: true },
|
|
455
|
+
buildTool: (ctx) => buildTriggerJobTool(getUserId(ctx) ?? ''),
|
|
456
|
+
guard: (ctx) => (hasOrg(ctx) ? null : 'Organization scope required'),
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
await app.register(mcpPlugin, {
|
|
460
|
+
resources,
|
|
461
|
+
extraTools: buildMcpToolsFromBridges([triggerJobBridge], {
|
|
462
|
+
// Per-environment filtering — read-only deployments hide destructive tools
|
|
463
|
+
exclude: process.env.DEPLOYMENT === 'readonly' ? ['trigger_job'] : [],
|
|
464
|
+
}),
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
`buildMcpToolsFromBridges` also accepts `{ include: [...] }` for strict allowlists. `buildTool` is called fresh per request — safe for per-tenant dep resolution. `McpBridge.annotations` is the same `ToolAnnotations` shape as `defineTool`.
|
|
469
|
+
|
|
433
470
|
## Schema Discovery — MCP Resources
|
|
434
471
|
|
|
435
472
|
Auto-registered for agent discovery:
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { n as IdempotencyResult, r as IdempotencyStore } from "./interface-B-pe8fhj.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/idempotency/stores/redis.d.ts
|
|
4
|
-
interface RedisClient {
|
|
5
|
-
get(key: string): Promise<string | null>;
|
|
6
|
-
set(key: string, value: string, options?: {
|
|
7
|
-
EX?: number;
|
|
8
|
-
NX?: boolean;
|
|
9
|
-
}): Promise<string | null>;
|
|
10
|
-
del(key: string | string[]): Promise<number>;
|
|
11
|
-
exists(key: string | string[]): Promise<number>;
|
|
12
|
-
/** SCAN command — compatible with node-redis and ioredis varargs signatures. */
|
|
13
|
-
scan?(cursor: string | number, ...args: (string | number)[]): Promise<[string | number, string[]]>;
|
|
14
|
-
quit?(): Promise<string>;
|
|
15
|
-
disconnect?(): Promise<void>;
|
|
16
|
-
}
|
|
17
|
-
interface RedisIdempotencyStoreOptions {
|
|
18
|
-
/** Redis client instance */
|
|
19
|
-
client: RedisClient;
|
|
20
|
-
/** Key prefix (default: 'idem:') */
|
|
21
|
-
prefix?: string;
|
|
22
|
-
/** Lock key prefix (default: 'idem:lock:') */
|
|
23
|
-
lockPrefix?: string;
|
|
24
|
-
/** Default TTL in ms (default: 86400000 = 24 hours) */
|
|
25
|
-
ttlMs?: number;
|
|
26
|
-
}
|
|
27
|
-
declare class RedisIdempotencyStore implements IdempotencyStore {
|
|
28
|
-
readonly name = "redis";
|
|
29
|
-
private client;
|
|
30
|
-
private prefix;
|
|
31
|
-
private lockPrefix;
|
|
32
|
-
private ttlMs;
|
|
33
|
-
constructor(options: RedisIdempotencyStoreOptions);
|
|
34
|
-
private resultKey;
|
|
35
|
-
private lockKey;
|
|
36
|
-
get(key: string): Promise<IdempotencyResult | undefined>;
|
|
37
|
-
set(key: string, result: Omit<IdempotencyResult, "key">): Promise<void>;
|
|
38
|
-
tryLock(key: string, requestId: string, ttlMs: number): Promise<boolean>;
|
|
39
|
-
unlock(key: string, requestId: string): Promise<void>;
|
|
40
|
-
isLocked(key: string): Promise<boolean>;
|
|
41
|
-
delete(key: string): Promise<void>;
|
|
42
|
-
deleteByPrefix(prefix: string): Promise<number>;
|
|
43
|
-
findByPrefix(prefix: string): Promise<IdempotencyResult | undefined>;
|
|
44
|
-
/** Scan Redis keys matching a prefix pattern. Falls back to empty if SCAN unavailable. */
|
|
45
|
-
private scanByPrefix;
|
|
46
|
-
close(): Promise<void>;
|
|
47
|
-
}
|
|
48
|
-
//#endregion
|
|
49
|
-
export { RedisIdempotencyStore as n, RedisIdempotencyStoreOptions as r, RedisClient as t };
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|