@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.
Files changed (125) hide show
  1. package/README.md +50 -1
  2. package/dist/adapters/index.d.mts +2 -2
  3. package/dist/audit/index.d.mts +1 -1
  4. package/dist/audit/index.mjs +1 -1
  5. package/dist/audit/mongodb.d.mts +1 -1
  6. package/dist/audit/mongodb.mjs +1 -1
  7. package/dist/auth/index.d.mts +4 -4
  8. package/dist/auth/index.mjs +2 -2
  9. package/dist/auth/redis-session.d.mts +1 -1
  10. package/dist/{betterAuthOpenApi-C5lDyRH2.mjs → betterAuthOpenApi-BuUcUEJq.mjs} +1 -1
  11. package/dist/cache/index.d.mts +73 -3
  12. package/dist/cache/index.mjs +95 -2
  13. package/dist/cli/commands/docs.mjs +2 -2
  14. package/dist/cli/commands/generate.mjs +1 -1
  15. package/dist/cli/commands/introspect.mjs +1 -1
  16. package/dist/core/index.d.mts +2 -2
  17. package/dist/core/index.mjs +3 -3
  18. package/dist/{core-DKSwNSXf.mjs → core-F0QoWBt2.mjs} +1 -1
  19. package/dist/{createActionRouter-Df1BuawX.mjs → createActionRouter-BORM8f17.mjs} +1 -1
  20. package/dist/{createApp-BOYjBgdI.mjs → createApp-B1EY8zxa.mjs} +11 -11
  21. package/dist/{defineResource-Bb_Bdhtw.mjs → defineResource-tcgySDo1.mjs} +2 -2
  22. package/dist/docs/index.d.mts +2 -2
  23. package/dist/docs/index.mjs +1 -1
  24. package/dist/dynamic/index.d.mts +2 -2
  25. package/dist/dynamic/index.mjs +1 -1
  26. package/dist/{elevation-BBGFjzIP.mjs → elevation-DtFxrG0s.mjs} +1 -1
  27. package/dist/{errorHandler-CdZDavNH.d.mts → errorHandler-Bah5JhBd.d.mts} +1 -1
  28. package/dist/{eventPlugin-CVxlE6De.d.mts → eventPlugin-D9DKB2zM.d.mts} +1 -1
  29. package/dist/events/index.d.mts +3 -3
  30. package/dist/events/index.mjs +1 -1
  31. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  32. package/dist/events/transports/redis.d.mts +1 -1
  33. package/dist/factory/index.d.mts +1 -1
  34. package/dist/factory/index.mjs +2 -2
  35. package/dist/filesUpload-C7r7HIeA.mjs +319 -0
  36. package/dist/hooks/index.d.mts +1 -1
  37. package/dist/hooks/index.mjs +1 -1
  38. package/dist/idempotency/index.d.mts +3 -3
  39. package/dist/idempotency/mongodb.d.mts +1 -1
  40. package/dist/idempotency/redis.d.mts +2 -2
  41. package/dist/idempotency/redis.mjs +134 -13
  42. package/dist/{index-CSkeivBx.d.mts → index-BLXBmWud.d.mts} +3 -3
  43. package/dist/{index-BgmMdpm8.d.mts → index-C1meYuDn.d.mts} +1 -1
  44. package/dist/{index-CpTSDqmD.d.mts → index-DtDzOBn8.d.mts} +3 -3
  45. package/dist/index.d.mts +7 -7
  46. package/dist/index.mjs +4 -4
  47. package/dist/integrations/event-gateway.d.mts +1 -1
  48. package/dist/integrations/event-gateway.mjs +1 -1
  49. package/dist/integrations/index.d.mts +1 -1
  50. package/dist/integrations/jobs.d.mts +25 -3
  51. package/dist/integrations/jobs.mjs +63 -4
  52. package/dist/integrations/mcp/index.d.mts +51 -3
  53. package/dist/integrations/mcp/index.mjs +78 -19
  54. package/dist/integrations/mcp/testing.d.mts +1 -1
  55. package/dist/integrations/mcp/testing.mjs +1 -1
  56. package/dist/{interface-BVuMfeVv.d.mts → interface-CMRutPfe.d.mts} +38 -16
  57. package/dist/{mongodb-B8U2xaLj.d.mts → mongodb-BsP-WbhN.d.mts} +1 -1
  58. package/dist/{mongodb-X7LbEjTN.d.mts → mongodb-CTcp0hQZ.d.mts} +1 -1
  59. package/dist/{openapi-CYCuekCn.mjs → openapi-CbKUJY_m.mjs} +3 -3
  60. package/dist/org/index.d.mts +2 -2
  61. package/dist/permissions/index.d.mts +3 -3
  62. package/dist/plugins/index.d.mts +4 -4
  63. package/dist/plugins/index.mjs +8 -8
  64. package/dist/plugins/tracing-entry.d.mts +1 -1
  65. package/dist/plugins/tracing-entry.mjs +1 -1
  66. package/dist/policies/index.d.mts +1 -1
  67. package/dist/presets/filesUpload.d.mts +49 -0
  68. package/dist/presets/filesUpload.mjs +2 -0
  69. package/dist/presets/index.d.mts +3 -2
  70. package/dist/presets/index.mjs +2 -1
  71. package/dist/presets/multiTenant.d.mts +1 -1
  72. package/dist/{queryCachePlugin-CnTZZTC5.d.mts → queryCachePlugin-BJJGBTlu.d.mts} +1 -1
  73. package/dist/redis-BM00zaPB.d.mts +115 -0
  74. package/dist/{redis-stream-D54N5oXs.d.mts → redis-stream-CrsfUmPt.d.mts} +1 -1
  75. package/dist/registry/index.d.mts +1 -1
  76. package/dist/registry/index.mjs +2 -2
  77. package/dist/{resourceToTools-O_HwWXFa.mjs → resourceToTools-8s-EsCCe.mjs} +1 -1
  78. package/dist/rpc/index.d.mts +1 -1
  79. package/dist/{schemaConverter-OxfCshus.mjs → schemaConverter-Y7nCYaLJ.mjs} +24 -8
  80. package/dist/scope/index.d.mts +2 -2
  81. package/dist/scope/index.mjs +1 -1
  82. package/dist/{sse-CJpt7LGI.mjs → sse-Ad7ypl9e.mjs} +1 -1
  83. package/dist/storage-Dfzt4VTl.d.mts +146 -0
  84. package/dist/testing/index.d.mts +4 -3
  85. package/dist/testing/index.mjs +3 -2
  86. package/dist/testing/storageContract.d.mts +26 -0
  87. package/dist/testing/storageContract.mjs +216 -0
  88. package/dist/types/index.d.mts +4 -4
  89. package/dist/types/storage.d.mts +2 -0
  90. package/dist/types/storage.mjs +1 -0
  91. package/dist/{types-CcG4avic.d.mts → types-BsbNMEDR.d.mts} +1 -1
  92. package/dist/{types-Bg2X42_m.d.mts → types-Ch9pTQbf.d.mts} +9 -9
  93. package/dist/{types-CVC4HOKi.d.mts → types-DZi1aYhm.d.mts} +1 -1
  94. package/dist/utils/index.d.mts +26 -8
  95. package/dist/utils/index.mjs +1 -1
  96. package/package.json +16 -1
  97. package/skills/arc/SKILL.md +22 -0
  98. package/skills/arc/references/events.md +29 -0
  99. package/skills/arc/references/mcp.md +37 -0
  100. package/dist/redis-z3sFr1UP.d.mts +0 -49
  101. /package/dist/{EventTransport-CinyO7zQ.d.mts → EventTransport-BXja8NOc.d.mts} +0 -0
  102. /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-HprTmvVY.mjs} +0 -0
  103. /package/dist/{ResourceRegistry-Dq3_zBQP.mjs → ResourceRegistry-C6uXlWe3.mjs} +0 -0
  104. /package/dist/{caching-CjybdRwx.mjs → caching-IMuYVjTL.mjs} +0 -0
  105. /package/dist/{circuitBreaker-CvXkjfrW.d.mts → circuitBreaker-dTtG-UyS.d.mts} +0 -0
  106. /package/dist/{elevation-s5ykdNHr.d.mts → elevation-B6S5csVA.d.mts} +0 -0
  107. /package/dist/{errorHandler-mzqk4cGl.mjs → errorHandler-f869_8PQ.mjs} +0 -0
  108. /package/dist/{errors-Bmn3eZT6.d.mts → errors-Ck2h67pm.d.mts} +0 -0
  109. /package/dist/{eventPlugin-D91S2YF4.mjs → eventPlugin-CDjVTM82.mjs} +0 -0
  110. /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BnkYrNzp.d.mts} +0 -0
  111. /package/dist/{fields-DC4So2M2.d.mts → fields-DpZQa_Q3.d.mts} +0 -0
  112. /package/dist/{interface-DplgQO2e.d.mts → interface-4y979v99.d.mts} +0 -0
  113. /package/dist/{interface-B-pe8fhj.d.mts → interface-DfLGcus7.d.mts} +0 -0
  114. /package/dist/{loadResources-Bksk8ydA.mjs → loadResources-PWd0OCpV.mjs} +0 -0
  115. /package/dist/{logger-CDjpjySd.mjs → logger-D1YrIImS.mjs} +0 -0
  116. /package/dist/{metrics-TuOmguhi.mjs → metrics-B-PU4-Yu.mjs} +0 -0
  117. /package/dist/{mongodb-B5O6xaW1.mjs → mongodb-Utc5k_-0.mjs} +0 -0
  118. /package/dist/{pluralize-A0tWEl1K.mjs → pluralize-CWP6MB39.mjs} +0 -0
  119. /package/dist/{queryCachePlugin-D0iIVhW_.mjs → queryCachePlugin-BH-fidlv.mjs} +0 -0
  120. /package/dist/{registry-B0Wl7uVV.mjs → registry-BiTKT1Dg.mjs} +0 -0
  121. /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-CxkYGT81.mjs} +0 -0
  122. /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-DDCmiNIo.d.mts} +0 -0
  123. /package/dist/{tracing-DxjKk7eW.d.mts → tracing-DdN2-wHJ.d.mts} +0 -0
  124. /package/dist/{types-C72d3NDn.d.mts → types-BD85MlEK.d.mts} +0 -0
  125. /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 };
@@ -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-C72d3NDn.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-BVuMfeVv.mjs";
3
- import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "../types-CVC4HOKi.mjs";
4
- import { n as ElevationOptions, t as ElevationEvent } from "../elevation-s5ykdNHr.mjs";
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,2 @@
1
+ import { a as StorageReadResult, i as StorageReadRange, n as StorageContext, o as StorageUploadInput, r as StorageFile, t as Storage } from "../storage-Dfzt4VTl.mjs";
2
+ export { Storage, StorageContext, StorageFile, StorageReadRange, StorageReadResult, StorageUploadInput };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import { qt as ResourceDefinition } from "./interface-BVuMfeVv.mjs";
1
+ import { qt as ResourceDefinition } from "./interface-CMRutPfe.mjs";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/integrations/mcp/types.d.ts
@@ -1,12 +1,12 @@
1
- import { x as Authenticator } from "./interface-BVuMfeVv.mjs";
2
- import { n as ElevationOptions } from "./elevation-s5ykdNHr.mjs";
3
- import { t as ExternalOpenApiPaths } from "./externalPaths-Bapitwvd.mjs";
4
- import { i as CacheStore } from "./interface-DplgQO2e.mjs";
5
- import { r as QueryCachePluginOptions } from "./queryCachePlugin-CnTZZTC5.mjs";
6
- import { i as EventTransport } from "./EventTransport-CinyO7zQ.mjs";
7
- import { t as EventPluginOptions } from "./eventPlugin-CVxlE6De.mjs";
8
- import { f as SSEOptions, h as CachingOptions, i as VersioningOptions, l as MetricsOptions, t as ErrorHandlerOptions } from "./errorHandler-CdZDavNH.mjs";
9
- import { r as IdempotencyStore } from "./interface-B-pe8fhj.mjs";
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
@@ -1,4 +1,4 @@
1
- import { r as RequestScope } from "./types-C72d3NDn.mjs";
1
+ import { r as RequestScope } from "./types-BD85MlEK.mjs";
2
2
  import { FastifyRequest } from "fastify";
3
3
 
4
4
  //#region src/permissions/types.d.ts
@@ -1,6 +1,6 @@
1
- import { Y as OpenApiSchemas, Z as ParsedQuery, m as AnyRecord, nt as QueryParserInterface } from "../interface-BVuMfeVv.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-Bmn3eZT6.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-CvXkjfrW.mjs";
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: 'openapi-3.0' })`
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>): 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 };
@@ -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-OxfCshus.mjs";
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",
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",
@@ -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 };