@classytic/arc 2.10.8 → 2.11.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.
Files changed (136) hide show
  1. package/dist/{BaseController-DVNKvoX4.mjs → BaseController-JNV08qOT.mjs} +480 -442
  2. package/dist/{queryCachePlugin-Dumka73q.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/adapters/index.mjs +1 -1
  5. package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
  6. package/dist/audit/index.d.mts +1 -1
  7. package/dist/auth/index.d.mts +1 -1
  8. package/dist/auth/index.mjs +5 -5
  9. package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
  10. package/dist/cache/index.d.mts +3 -2
  11. package/dist/cache/index.mjs +3 -3
  12. package/dist/cli/commands/docs.mjs +2 -2
  13. package/dist/cli/commands/generate.mjs +37 -27
  14. package/dist/cli/commands/init.mjs +46 -33
  15. package/dist/cli/commands/introspect.mjs +1 -1
  16. package/dist/context/index.mjs +1 -1
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -3
  19. package/dist/core-DXdSSFW-.mjs +1037 -0
  20. package/dist/createActionRouter-BwaSM0No.mjs +166 -0
  21. package/dist/{createApp-BwnEAO2h.mjs → createApp-P1d6rjPy.mjs} +75 -27
  22. package/dist/docs/index.d.mts +1 -1
  23. package/dist/docs/index.mjs +2 -2
  24. package/dist/{elevation-Dci0AYLT.mjs → elevation-DOFoxoDs.mjs} +1 -1
  25. package/dist/{errorHandler-CSxe7KIM.mjs → errorHandler-BQm8ZxTK.mjs} +1 -1
  26. package/dist/{eventPlugin-ByU4Cv0e.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
  27. package/dist/events/index.d.mts +3 -3
  28. package/dist/events/index.mjs +2 -2
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/factory/index.d.mts +2 -2
  31. package/dist/factory/index.mjs +2 -2
  32. package/dist/hooks/index.d.mts +1 -1
  33. package/dist/hooks/index.mjs +1 -1
  34. package/dist/idempotency/index.d.mts +3 -3
  35. package/dist/idempotency/index.mjs +1 -1
  36. package/dist/idempotency/redis.d.mts +1 -1
  37. package/dist/{index-C_Noptz-.d.mts → index-BYCqHCVu.d.mts} +2 -2
  38. package/dist/{index-BGbpGVyM.d.mts → index-C_bgx9o4.d.mts} +712 -500
  39. package/dist/{index-BziRPS4H.d.mts → index-CvM1e09j.d.mts} +29 -10
  40. package/dist/{index-EqQN6p0W.d.mts → index-pUczGjO0.d.mts} +11 -8
  41. package/dist/index-smCAoA5W.d.mts +1179 -0
  42. package/dist/index.d.mts +6 -38
  43. package/dist/index.mjs +9 -9
  44. package/dist/integrations/event-gateway.d.mts +1 -1
  45. package/dist/integrations/event-gateway.mjs +1 -1
  46. package/dist/integrations/index.d.mts +2 -2
  47. package/dist/integrations/mcp/index.d.mts +2 -2
  48. package/dist/integrations/mcp/index.mjs +1 -1
  49. package/dist/integrations/mcp/testing.d.mts +1 -1
  50. package/dist/integrations/mcp/testing.mjs +1 -1
  51. package/dist/integrations/streamline.d.mts +46 -5
  52. package/dist/integrations/streamline.mjs +50 -21
  53. package/dist/integrations/websocket-redis.d.mts +1 -1
  54. package/dist/integrations/websocket.d.mts +2 -154
  55. package/dist/integrations/websocket.mjs +292 -224
  56. package/dist/{keys-nWQGUTu1.mjs → keys-CARyUjiR.mjs} +2 -0
  57. package/dist/{loadResources-Bksk8ydA.mjs → loadResources-CPpkyKfM.mjs} +32 -8
  58. package/dist/middleware/index.d.mts +1 -1
  59. package/dist/middleware/index.mjs +1 -1
  60. package/dist/{openapi-DpNpqBmo.mjs → openapi-C0L9ar7m.mjs} +4 -4
  61. package/dist/org/index.d.mts +1 -1
  62. package/dist/permissions/index.d.mts +1 -1
  63. package/dist/permissions/index.mjs +2 -4
  64. package/dist/{permissions-wkqRwicB.mjs → permissions-B4vU9L0Q.mjs} +221 -3
  65. package/dist/{pipe-CGJxqDGx.mjs → pipe-DVoIheVC.mjs} +1 -1
  66. package/dist/pipeline/index.d.mts +1 -1
  67. package/dist/pipeline/index.mjs +1 -1
  68. package/dist/plugins/index.d.mts +4 -4
  69. package/dist/plugins/index.mjs +10 -10
  70. package/dist/plugins/response-cache.mjs +1 -1
  71. package/dist/plugins/tracing-entry.d.mts +1 -1
  72. package/dist/plugins/tracing-entry.mjs +42 -24
  73. package/dist/presets/filesUpload.d.mts +1 -1
  74. package/dist/presets/filesUpload.mjs +3 -3
  75. package/dist/presets/index.d.mts +1 -1
  76. package/dist/presets/index.mjs +1 -1
  77. package/dist/presets/multiTenant.d.mts +1 -1
  78. package/dist/presets/multiTenant.mjs +6 -0
  79. package/dist/presets/search.d.mts +1 -1
  80. package/dist/presets/search.mjs +1 -1
  81. package/dist/{presets-CrwOvuXI.mjs → presets-k604Lj99.mjs} +1 -1
  82. package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
  83. package/dist/{queryCachePlugin-ChLNZvFT.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
  84. package/dist/{redis-MXLp1oOf.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
  85. package/dist/registry/index.d.mts +1 -1
  86. package/dist/registry/index.mjs +2 -2
  87. package/dist/{resourceToTools-BhF3JV5p.mjs → resourceToTools--okX6QBr.mjs} +534 -420
  88. package/dist/routerShared-DeESFp4a.mjs +515 -0
  89. package/dist/schemaIR-BlG9bY7v.mjs +137 -0
  90. package/dist/scope/index.mjs +2 -2
  91. package/dist/testing/index.d.mts +367 -711
  92. package/dist/testing/index.mjs +637 -1434
  93. package/dist/{tracing-xqXzWeaf.d.mts → tracing-DokiEsuz.d.mts} +9 -4
  94. package/dist/types/index.d.mts +3 -3
  95. package/dist/types/index.mjs +1 -3
  96. package/dist/{types-CVdgPXBW.d.mts → types-BdA4uMBV.d.mts} +191 -28
  97. package/dist/{types-CVKBssX5.d.mts → types-Bh_gEJBi.d.mts} +1 -1
  98. package/dist/utils/index.d.mts +2 -968
  99. package/dist/utils/index.mjs +5 -6
  100. package/dist/utils-D3Yxnrwr.mjs +1639 -0
  101. package/dist/websocket-CyJ1VIFI.d.mts +186 -0
  102. package/package.json +7 -5
  103. package/skills/arc/SKILL.md +124 -39
  104. package/skills/arc/references/testing.md +212 -183
  105. package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
  106. package/dist/core-3MWJosCH.mjs +0 -1459
  107. package/dist/createActionRouter-C8UUB3Px.mjs +0 -249
  108. package/dist/errors-BI8kEKsO.d.mts +0 -140
  109. package/dist/fields-CTMWOUDt.mjs +0 -126
  110. package/dist/queryParser-NR__Qiju.mjs +0 -419
  111. package/dist/types-CDnTEpga.mjs +0 -27
  112. package/dist/utils-LMwVidKy.mjs +0 -947
  113. /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
  114. /package/dist/{ResourceRegistry-CcN2LVrc.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
  115. /package/dist/{actionPermissions-TUVR3uiZ.mjs → actionPermissions-C8YYU92K.mjs} +0 -0
  116. /package/dist/{caching-3h93rkJM.mjs → caching-CheW3m-S.mjs} +0 -0
  117. /package/dist/{errorHandler-2ii4RIYr.d.mts → errorHandler-Co3lnVmJ.d.mts} +0 -0
  118. /package/dist/{errors-BqdUDja_.mjs → errors-D5c-5BJL.mjs} +0 -0
  119. /package/dist/{eventPlugin-D1ThQ1Pp.d.mts → eventPlugin-CUNjYYRY.d.mts} +0 -0
  120. /package/dist/{interface-B-pe8fhj.d.mts → interface-CkkWm5uR.d.mts} +0 -0
  121. /package/dist/{interface-yhyb_pLY.d.mts → interface-Da0r7Lna.d.mts} +0 -0
  122. /package/dist/{memory-DqI-449b.mjs → memory-DikHSvWa.mjs} +0 -0
  123. /package/dist/{metrics-TuOmguhi.mjs → metrics-Csh4nsvv.mjs} +0 -0
  124. /package/dist/{multipartBody-CUQGVlM_.mjs → multipartBody-CvTR1Un6.mjs} +0 -0
  125. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-BneOJkpi.mjs} +0 -0
  126. /package/dist/{redis-stream-bkO88VHx.d.mts → redis-stream-CM8TXTix.d.mts} +0 -0
  127. /package/dist/{registry-B0Wl7uVV.mjs → registry-D63ee7fl.mjs} +0 -0
  128. /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
  129. /package/dist/{requestContext-C38GskNt.mjs → requestContext-CfRkaxwf.mjs} +0 -0
  130. /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
  131. /package/dist/{sse-D8UeDwis.mjs → sse-V7aXc3bW.mjs} +0 -0
  132. /package/dist/{store-helpers-DYYUQbQN.mjs → store-helpers-BhrzxvyQ.mjs} +0 -0
  133. /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
  134. /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
  135. /package/dist/{versioning-B6mimogM.mjs → versioning-CGPjkqAg.mjs} +0 -0
  136. /package/dist/{versioning-CeUXHfjw.d.mts → versioning-M9lNLhO8.d.mts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import fp from "fastify-plugin";
2
- //#region src/integrations/websocket.ts
2
+ //#region src/integrations/websocket/adapter.ts
3
3
  /**
4
4
  * Default adapter — no cross-instance broadcast (single-instance only).
5
5
  * All methods are no-ops. Used when no adapter is configured.
@@ -10,6 +10,271 @@ var LocalWebSocketAdapter = class {
10
10
  async subscribe() {}
11
11
  async close() {}
12
12
  };
13
+ //#endregion
14
+ //#region src/integrations/websocket/auth.ts
15
+ function createCaptureReply() {
16
+ let rejected = false;
17
+ let capturedStatus;
18
+ const reply = {
19
+ code(statusCode) {
20
+ rejected = true;
21
+ capturedStatus = statusCode;
22
+ return reply;
23
+ },
24
+ send() {
25
+ return reply;
26
+ },
27
+ get rejected() {
28
+ return rejected;
29
+ },
30
+ get statusCode() {
31
+ return capturedStatus;
32
+ },
33
+ sent: false
34
+ };
35
+ return reply;
36
+ }
37
+ /**
38
+ * Run authentication against a request and return a uniform `AuthResult`
39
+ * (or `null` if denied). Handles both modes:
40
+ *
41
+ * - `customAuth` provided → call it directly; trust its return shape.
42
+ * - Otherwise → invoke `fastify.authenticate(request, captureReply)` to
43
+ * populate `request.user` / `request.scope`, then read IDs off those.
44
+ *
45
+ * This is the single source of truth for "did this request authenticate
46
+ * successfully, and what identity did it establish". The handshake path
47
+ * calls it once; the re-auth loop calls it on every interval.
48
+ */
49
+ async function authenticateWebSocket(fastify, request, customAuth) {
50
+ if (customAuth) try {
51
+ return await customAuth(request);
52
+ } catch {
53
+ return null;
54
+ }
55
+ const authenticate = fastify.authenticate;
56
+ if (!authenticate) return null;
57
+ const reply = createCaptureReply();
58
+ try {
59
+ await authenticate(request, reply);
60
+ } catch {
61
+ return null;
62
+ }
63
+ if (reply.rejected) return null;
64
+ const shape = request;
65
+ const user = shape.user;
66
+ const scope = shape.scope;
67
+ if (!user) return null;
68
+ const userId = readIdField(user, "id") ?? readIdField(user, "sub");
69
+ const organizationId = scope ? readIdField(scope, "organizationId") : void 0;
70
+ return {
71
+ ...userId !== void 0 ? { userId } : {},
72
+ ...organizationId !== void 0 ? { organizationId } : {}
73
+ };
74
+ }
75
+ function readIdField(obj, key) {
76
+ const value = obj[key];
77
+ return typeof value === "string" ? value : void 0;
78
+ }
79
+ //#endregion
80
+ //#region src/integrations/websocket/connection.ts
81
+ async function handleConnection(ctx, socket, request) {
82
+ const { fastify, rooms, options } = ctx;
83
+ const clientId = ctx.nextClientId();
84
+ let userId;
85
+ let organizationId;
86
+ let serviceClientId;
87
+ let serviceScopes;
88
+ if (options.auth) {
89
+ const result = await authenticateWebSocket(fastify, request, options.authenticate);
90
+ if (!result) {
91
+ socket.close(4001, "Unauthorized");
92
+ return;
93
+ }
94
+ userId = result.userId;
95
+ organizationId = result.organizationId;
96
+ serviceClientId = result.clientId;
97
+ serviceScopes = result.scopes;
98
+ }
99
+ const client = {
100
+ id: clientId,
101
+ socket,
102
+ subscriptions: /* @__PURE__ */ new Set(),
103
+ userId,
104
+ organizationId,
105
+ ...serviceClientId ? { clientId: serviceClientId } : {},
106
+ ...serviceScopes ? { scopes: serviceScopes } : {}
107
+ };
108
+ rooms.addClient(client);
109
+ await options.onConnect?.(client);
110
+ socket.send(JSON.stringify({
111
+ type: "connected",
112
+ clientId,
113
+ resources: options.resources
114
+ }));
115
+ let heartbeatTimer;
116
+ if (options.heartbeatInterval > 0) heartbeatTimer = setInterval(() => {
117
+ if (socket.readyState === 1) socket.send(JSON.stringify({
118
+ type: "ping",
119
+ timestamp: Date.now()
120
+ }));
121
+ }, options.heartbeatInterval);
122
+ let reauthTimer;
123
+ if (options.reauthInterval > 0 && options.auth) reauthTimer = setInterval(async () => {
124
+ if (socket.readyState !== 1) return;
125
+ if (!await authenticateWebSocket(fastify, request, options.authenticate)) {
126
+ socket.send(JSON.stringify({
127
+ type: "error",
128
+ error: "Session expired"
129
+ }));
130
+ socket.close(4003, "Session expired");
131
+ }
132
+ }, options.reauthInterval);
133
+ socket.on("message", async (raw) => {
134
+ if ((typeof raw === "string" ? Buffer.byteLength(raw) : raw.length) > options.maxMessageBytes) {
135
+ socket.send(JSON.stringify({
136
+ type: "error",
137
+ error: "Message too large"
138
+ }));
139
+ return;
140
+ }
141
+ let msg;
142
+ try {
143
+ msg = JSON.parse(typeof raw === "string" ? raw : raw.toString());
144
+ } catch {
145
+ socket.send(JSON.stringify({
146
+ type: "error",
147
+ error: "Invalid message format"
148
+ }));
149
+ return;
150
+ }
151
+ switch (msg.type) {
152
+ case "subscribe": {
153
+ const room = msg.resource ?? msg.channel;
154
+ if (!room) break;
155
+ if (client.subscriptions.size >= options.maxSubscriptionsPerClient) {
156
+ socket.send(JSON.stringify({
157
+ type: "error",
158
+ channel: room,
159
+ error: "Subscription limit reached"
160
+ }));
161
+ break;
162
+ }
163
+ if (options.roomPolicy) {
164
+ if (!await options.roomPolicy(client, room)) {
165
+ socket.send(JSON.stringify({
166
+ type: "error",
167
+ channel: room,
168
+ error: "Subscription denied"
169
+ }));
170
+ break;
171
+ }
172
+ }
173
+ const ok = rooms.subscribe(clientId, room);
174
+ socket.send(JSON.stringify({
175
+ type: ok ? "subscribed" : "error",
176
+ channel: room,
177
+ ...ok ? {} : { error: "Room at capacity" }
178
+ }));
179
+ break;
180
+ }
181
+ case "unsubscribe": {
182
+ const room = msg.resource ?? msg.channel;
183
+ if (room) {
184
+ rooms.unsubscribe(clientId, room);
185
+ socket.send(JSON.stringify({
186
+ type: "unsubscribed",
187
+ channel: room
188
+ }));
189
+ }
190
+ break;
191
+ }
192
+ case "pong": break;
193
+ default:
194
+ await options.onMessage?.(client, msg);
195
+ break;
196
+ }
197
+ });
198
+ socket.on("close", async () => {
199
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
200
+ if (reauthTimer) clearInterval(reauthTimer);
201
+ await options.onDisconnect?.(client);
202
+ rooms.removeClient(clientId);
203
+ });
204
+ socket.on("error", () => {
205
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
206
+ if (reauthTimer) clearInterval(reauthTimer);
207
+ rooms.removeClient(clientId);
208
+ });
209
+ }
210
+ //#endregion
211
+ //#region src/integrations/websocket/event-bridge.ts
212
+ /**
213
+ * Subscribe to `<resource>.created|updated|deleted` events for every
214
+ * resource in `resources` and fan them out to the matching room (same
215
+ * name as the resource). Returns the unsubscriber list so the caller can
216
+ * clean up on `onClose`.
217
+ *
218
+ * Org-scoped events (events carrying `meta.organizationId`) broadcast
219
+ * only to clients in that org; unscoped events broadcast to every
220
+ * subscriber of the room.
221
+ */
222
+ async function wireResourceEvents(fastify, rooms, resources) {
223
+ const unsubscribers = [];
224
+ const events = fastify.events;
225
+ if (!events?.subscribe) return unsubscribers;
226
+ if (resources.length === 0) return unsubscribers;
227
+ const subscribe = events.subscribe;
228
+ for (const resourceName of resources) for (const op of [
229
+ "created",
230
+ "updated",
231
+ "deleted"
232
+ ]) {
233
+ const unsub = await subscribe(`${resourceName}.${op}`, (event) => {
234
+ const room = resourceName;
235
+ const payload = JSON.stringify({
236
+ type: `${resourceName}.${op}`,
237
+ data: event.payload,
238
+ meta: {
239
+ timestamp: event.meta?.timestamp,
240
+ userId: event.meta?.userId,
241
+ organizationId: event.meta?.organizationId
242
+ }
243
+ });
244
+ if (event.meta?.organizationId) rooms.broadcastToOrgWithAdapter(event.meta.organizationId, room, payload);
245
+ else rooms.broadcastWithAdapter(room, payload);
246
+ });
247
+ unsubscribers.push(unsub);
248
+ }
249
+ return unsubscribers;
250
+ }
251
+ /**
252
+ * Register the optional `{path}/stats` endpoint.
253
+ *
254
+ * - `false` (default): no registration
255
+ * - `true`: open endpoint
256
+ * - `'authenticated'`: guarded by `fastify.authenticate` if registered;
257
+ * silently skipped with a warn when it isn't (doesn't fail boot — the
258
+ * stats endpoint is diagnostic, not load-bearing)
259
+ */
260
+ function registerStatsRoute(fastify, rooms, path, expose) {
261
+ if (expose === true) {
262
+ fastify.get(`${path}/stats`, async () => ({
263
+ success: true,
264
+ data: rooms.getStats()
265
+ }));
266
+ return;
267
+ }
268
+ if (expose === "authenticated") if (fastify.hasDecorator("authenticate")) {
269
+ const authenticate = fastify.authenticate;
270
+ fastify.get(`${path}/stats`, { preHandler: authenticate }, async () => ({
271
+ success: true,
272
+ data: rooms.getStats()
273
+ }));
274
+ } else fastify.log.warn("arc-websocket: exposeStats is \"authenticated\" but fastify.authenticate is not registered — stats endpoint skipped");
275
+ }
276
+ //#endregion
277
+ //#region src/integrations/websocket/room-manager.ts
13
278
  var RoomManager = class {
14
279
  rooms = /* @__PURE__ */ new Map();
15
280
  clients = /* @__PURE__ */ new Map();
@@ -106,11 +371,12 @@ var RoomManager = class {
106
371
  };
107
372
  }
108
373
  };
374
+ //#endregion
375
+ //#region src/integrations/websocket/plugin.ts
109
376
  const websocketPluginImpl = async (fastify, options) => {
110
- let clientCounter = 0;
111
377
  const { path = "/ws", auth = true, resources = [], heartbeatInterval = 3e4, authenticate: customAuth, maxClientsPerRoom = 1e4, roomPolicy, maxMessageBytes = 16384, maxSubscriptionsPerClient = 100, reauthInterval = 0, adapter, exposeStats = false, onMessage, onConnect, onDisconnect } = options;
112
378
  if (auth && !customAuth && !fastify.hasDecorator("authenticate")) throw new Error("[arc-websocket] auth is true but fastify.authenticate is not registered. Register an auth plugin before WebSocket, provide a custom authenticate function, or set auth: false.");
113
- const rooms = new RoomManager(maxClientsPerRoom, adapter);
379
+ const rooms = new RoomManager(maxClientsPerRoom, adapter ?? new LocalWebSocketAdapter());
114
380
  if (adapter) await adapter.subscribe((room, message) => {
115
381
  if (room.startsWith("org:")) {
116
382
  const parts = room.split(":");
@@ -139,229 +405,31 @@ const websocketPluginImpl = async (fastify, options) => {
139
405
  },
140
406
  getStats: () => rooms.getStats()
141
407
  });
142
- const eventUnsubscribers = [];
143
- if (resources.length > 0 && fastify.events?.subscribe) for (const resourceName of resources) for (const op of [
144
- "created",
145
- "updated",
146
- "deleted"
147
- ]) {
148
- const unsub = await fastify.events.subscribe(`${resourceName}.${op}`, async (event) => {
149
- const room = resourceName;
150
- const payload = JSON.stringify({
151
- type: `${resourceName}.${op}`,
152
- data: event.payload,
153
- meta: {
154
- timestamp: event.meta?.timestamp,
155
- userId: event.meta?.userId,
156
- organizationId: event.meta?.organizationId
157
- }
158
- });
159
- if (event.meta?.organizationId) rooms.broadcastToOrgWithAdapter(event.meta.organizationId, room, payload);
160
- else rooms.broadcastWithAdapter(room, payload);
161
- });
162
- eventUnsubscribers.push(unsub);
163
- }
164
- fastify.get(path, { websocket: true }, async (socket, request) => {
165
- const clientId = `ws_${++clientCounter}_${Date.now()}`;
166
- let userId;
167
- let organizationId;
168
- let serviceClientId;
169
- let serviceScopes;
170
- if (auth) if (customAuth) {
171
- const result = await customAuth(request);
172
- if (!result) {
173
- socket.close(4001, "Unauthorized");
174
- return;
175
- }
176
- userId = result.userId;
177
- organizationId = result.organizationId;
178
- serviceClientId = result.clientId;
179
- serviceScopes = result.scopes;
180
- } else {
181
- if (fastify.authenticate) try {
182
- let rejected = false;
183
- const fakeReply = {
184
- code(_statusCode) {
185
- rejected = true;
186
- return fakeReply;
187
- },
188
- send() {
189
- return fakeReply;
190
- },
191
- sent: false
192
- };
193
- await fastify.authenticate(request, fakeReply);
194
- if (rejected) {
195
- socket.close(4001, "Unauthorized");
196
- return;
197
- }
198
- } catch {
199
- socket.close(4001, "Unauthorized");
200
- return;
201
- }
202
- if (request.user) {
203
- userId = request.user.id ?? request.user.sub;
204
- organizationId = request.scope?.organizationId;
205
- } else {
206
- socket.close(4001, "Unauthorized");
207
- return;
208
- }
408
+ const eventUnsubscribers = await wireResourceEvents(fastify, rooms, resources);
409
+ let clientCounter = 0;
410
+ const ctx = {
411
+ fastify,
412
+ rooms,
413
+ nextClientId: () => `ws_${++clientCounter}_${Date.now()}`,
414
+ options: {
415
+ auth,
416
+ resources,
417
+ heartbeatInterval,
418
+ maxClientsPerRoom,
419
+ maxMessageBytes,
420
+ maxSubscriptionsPerClient,
421
+ reauthInterval,
422
+ authenticate: customAuth,
423
+ roomPolicy,
424
+ onConnect,
425
+ onDisconnect,
426
+ onMessage
209
427
  }
210
- const client = {
211
- id: clientId,
212
- socket,
213
- subscriptions: /* @__PURE__ */ new Set(),
214
- userId,
215
- organizationId,
216
- ...serviceClientId ? { clientId: serviceClientId } : {},
217
- ...serviceScopes ? { scopes: serviceScopes } : {}
218
- };
219
- rooms.addClient(client);
220
- await onConnect?.(client);
221
- socket.send(JSON.stringify({
222
- type: "connected",
223
- clientId,
224
- resources
225
- }));
226
- let heartbeatTimer;
227
- if (heartbeatInterval > 0) heartbeatTimer = setInterval(() => {
228
- if (socket.readyState === 1) socket.send(JSON.stringify({
229
- type: "ping",
230
- timestamp: Date.now()
231
- }));
232
- }, heartbeatInterval);
233
- let reauthTimer;
234
- if (reauthInterval > 0 && auth) reauthTimer = setInterval(async () => {
235
- if (socket.readyState !== 1) return;
236
- try {
237
- if (customAuth) {
238
- if (!await customAuth(request)) {
239
- socket.send(JSON.stringify({
240
- type: "error",
241
- error: "Session expired"
242
- }));
243
- socket.close(4003, "Session expired");
244
- return;
245
- }
246
- } else if (fastify.authenticate) {
247
- let rejected = false;
248
- const fakeReply = {
249
- code() {
250
- rejected = true;
251
- return fakeReply;
252
- },
253
- send() {
254
- return fakeReply;
255
- },
256
- sent: false
257
- };
258
- await fastify.authenticate(request, fakeReply);
259
- if (rejected) {
260
- socket.send(JSON.stringify({
261
- type: "error",
262
- error: "Session expired"
263
- }));
264
- socket.close(4003, "Session expired");
265
- return;
266
- }
267
- }
268
- } catch {
269
- socket.send(JSON.stringify({
270
- type: "error",
271
- error: "Session expired"
272
- }));
273
- socket.close(4003, "Session expired");
274
- }
275
- }, reauthInterval);
276
- socket.on("message", async (raw) => {
277
- if ((typeof raw === "string" ? Buffer.byteLength(raw) : raw.length) > maxMessageBytes) {
278
- socket.send(JSON.stringify({
279
- type: "error",
280
- error: "Message too large"
281
- }));
282
- return;
283
- }
284
- try {
285
- const msg = JSON.parse(typeof raw === "string" ? raw : raw.toString());
286
- switch (msg.type) {
287
- case "subscribe": {
288
- const room = msg.resource ?? msg.channel;
289
- if (room) {
290
- if (client.subscriptions.size >= maxSubscriptionsPerClient) {
291
- socket.send(JSON.stringify({
292
- type: "error",
293
- channel: room,
294
- error: "Subscription limit reached"
295
- }));
296
- break;
297
- }
298
- if (roomPolicy) {
299
- if (!await roomPolicy(client, room)) {
300
- socket.send(JSON.stringify({
301
- type: "error",
302
- channel: room,
303
- error: "Subscription denied"
304
- }));
305
- break;
306
- }
307
- }
308
- const ok = rooms.subscribe(clientId, room);
309
- socket.send(JSON.stringify({
310
- type: ok ? "subscribed" : "error",
311
- channel: room,
312
- ...ok ? {} : { error: "Room at capacity" }
313
- }));
314
- }
315
- break;
316
- }
317
- case "unsubscribe": {
318
- const room = msg.resource ?? msg.channel;
319
- if (room) {
320
- rooms.unsubscribe(clientId, room);
321
- socket.send(JSON.stringify({
322
- type: "unsubscribed",
323
- channel: room
324
- }));
325
- }
326
- break;
327
- }
328
- case "pong": break;
329
- default:
330
- await onMessage?.(client, msg);
331
- break;
332
- }
333
- } catch {
334
- socket.send(JSON.stringify({
335
- type: "error",
336
- error: "Invalid message format"
337
- }));
338
- }
339
- });
340
- socket.on("close", async () => {
341
- if (heartbeatTimer) clearInterval(heartbeatTimer);
342
- if (reauthTimer) clearInterval(reauthTimer);
343
- await onDisconnect?.(client);
344
- rooms.removeClient(clientId);
345
- });
346
- socket.on("error", () => {
347
- if (heartbeatTimer) clearInterval(heartbeatTimer);
348
- if (reauthTimer) clearInterval(reauthTimer);
349
- rooms.removeClient(clientId);
350
- });
351
- });
352
- if (exposeStats === true) fastify.get(`${path}/stats`, async () => {
353
- return {
354
- success: true,
355
- data: rooms.getStats()
356
- };
357
- });
358
- else if (exposeStats === "authenticated") if (fastify.hasDecorator("authenticate")) fastify.get(`${path}/stats`, { preHandler: fastify.authenticate }, async () => {
359
- return {
360
- success: true,
361
- data: rooms.getStats()
362
- };
428
+ };
429
+ fastify.get(path, { websocket: true }, async (socket, request) => {
430
+ await handleConnection(ctx, socket, request);
363
431
  });
364
- else fastify.log.warn("arc-websocket: exposeStats is \"authenticated\" but fastify.authenticate is not registered — stats endpoint skipped");
432
+ registerStatsRoute(fastify, rooms, path, exposeStats);
365
433
  fastify.addHook("onClose", async () => {
366
434
  for (const unsub of eventUnsubscribers) unsub();
367
435
  eventUnsubscribers.length = 0;
@@ -30,6 +30,8 @@ function stableStringify(value) {
30
30
  if (value === null || value === void 0) return "";
31
31
  if (typeof value !== "object") return String(value);
32
32
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
33
+ if (value instanceof RegExp) return `/${value.source}/${value.flags}`;
34
+ if (value instanceof Date) return `d${value.getTime()}`;
33
35
  return "{" + Object.keys(value).sort().map((k) => `${k}:${stableStringify(value[k])}`).join(",") + "}";
34
36
  }
35
37
  function djb2(str) {
@@ -1,4 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
+ import { arcLog } from "./logger/index.mjs";
2
3
  import { readdir } from "node:fs/promises";
3
4
  import { dirname, join, resolve } from "node:path";
4
5
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -58,8 +59,9 @@ var loadResources_exports = /* @__PURE__ */ __exportAll({ loadResources: () => l
58
59
  * ```
59
60
  */
60
61
  async function loadResources(dir, options = {}) {
61
- const { suffix = ".resource", recursive = true, exclude, include, silent = false } = options;
62
- const files = await collectFiles(resolve(dir.startsWith("file://") ? dirname(fileURLToPath(dir)) : dir), new RegExp(`${escapeRegex(suffix)}\\.(ts|js|mts|mjs)$`), recursive);
62
+ const { suffix = ".resource", recursive = true, exclude, include } = options;
63
+ const absDir = resolve(dir.startsWith("file://") ? dirname(fileURLToPath(dir)) : dir);
64
+ const files = await collectFiles(absDir, new RegExp(`${escapeRegex(suffix)}\\.(ts|js|mts|mjs)$`), recursive);
63
65
  files.sort();
64
66
  const includeSet = include ? new Set(include) : null;
65
67
  const excludeSet = exclude ? new Set(exclude) : null;
@@ -98,13 +100,30 @@ async function loadResources(dir, options = {}) {
98
100
  return null;
99
101
  }));
100
102
  const resources = [];
103
+ const factoryFailed = [];
101
104
  for (const result of results) {
102
105
  if (!result) continue;
103
- let resource = result.mod.default ?? result.mod.resource;
104
- if (!resource || typeof resource.toPlugin !== "function") {
105
- for (const value of Object.values(result.mod)) if (value && typeof value === "object" && typeof value.toPlugin === "function") {
106
- resource = value;
107
- break;
106
+ let resource;
107
+ const rawDefault = result.mod.default;
108
+ if (typeof rawDefault === "function" && !("toPlugin" in rawDefault)) try {
109
+ const built = await rawDefault(options.context);
110
+ if (built && typeof built === "object" && typeof built.toPlugin === "function") resource = built;
111
+ else {
112
+ factoryFailed.push(`${result.file}: factory returned non-resource value`);
113
+ continue;
114
+ }
115
+ } catch (err) {
116
+ const msg = err instanceof Error ? err.message : String(err);
117
+ factoryFailed.push(`${result.file}: factory threw: ${msg}`);
118
+ continue;
119
+ }
120
+ else {
121
+ resource = rawDefault ?? result.mod.resource;
122
+ if (!resource || typeof resource.toPlugin !== "function") {
123
+ for (const value of Object.values(result.mod)) if (value && typeof value === "object" && typeof value.toPlugin === "function") {
124
+ resource = value;
125
+ break;
126
+ }
108
127
  }
109
128
  }
110
129
  if (!resource || typeof resource.toPlugin !== "function") {
@@ -118,16 +137,21 @@ async function loadResources(dir, options = {}) {
118
137
  }
119
138
  resources.push(resource);
120
139
  }
121
- const log = silent ? void 0 : options?.logger;
140
+ const log = options?.logger ?? arcLog("loadResources");
122
141
  if (log) {
123
142
  if (failed.length) {
124
143
  log.warn(`[arc] loadResources: ${failed.length} file(s) failed to import:`);
125
144
  for (const f of failed) log.warn(` - ${f}`);
126
145
  }
146
+ if (factoryFailed.length) {
147
+ log.warn(`[arc] loadResources: ${factoryFailed.length} factory export(s) failed (function default that returned non-resource or threw):`);
148
+ for (const f of factoryFailed) log.warn(` - ${f}`);
149
+ }
127
150
  if (skipped.length) {
128
151
  log.warn(`[arc] loadResources: ${skipped.length} file(s) skipped (no default export with toPlugin):`);
129
152
  for (const f of skipped) log.warn(` - ${f}`);
130
153
  }
154
+ if (resources.length === 0 && files.length === 0) log.warn(`[arc] loadResources: 0 matching files found at "${absDir}" (pattern: *${suffix}.{ts,js,mts,mjs}). Check the path and runtime layout (src/ vs dist/).`);
131
155
  }
132
156
  return resources;
133
157
  }
@@ -1,4 +1,4 @@
1
- import { I as MiddlewareHandler, L as RequestWithExtras, pt as MiddlewareConfig } from "../index-BGbpGVyM.mjs";
1
+ import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-C_bgx9o4.mjs";
2
2
  import { RouteHandlerMethod } from "fastify";
3
3
 
4
4
  //#region src/middleware/middleware.d.ts
@@ -1,5 +1,5 @@
1
1
  import { t as CRUD_OPERATIONS } from "../constants-BhY1OHoH.mjs";
2
- import { t as multipartBody } from "../multipartBody-CUQGVlM_.mjs";
2
+ import { t as multipartBody } from "../multipartBody-CvTR1Un6.mjs";
3
3
  //#region src/middleware/middleware.ts
4
4
  /**
5
5
  * Named Middleware — Priority-based, conditional middleware execution.
@@ -1,7 +1,7 @@
1
- import { t as getUserRoles } from "./types-D57iXYb8.mjs";
2
- import { n as convertRouteSchema } from "./schemaConverter-BxFDdtXu.mjs";
3
- import { t as resolveActionPermission } from "./actionPermissions-TUVR3uiZ.mjs";
4
- import { t as buildActionBodySchema } from "./createActionRouter-C8UUB3Px.mjs";
1
+ import { t as getUserRoles } from "./types-DV9WDfeg.mjs";
2
+ import { n as convertRouteSchema } from "./schemaConverter-B0oKLuqI.mjs";
3
+ import { t as resolveActionPermission } from "./actionPermissions-C8YYU92K.mjs";
4
+ import { t as buildActionBodySchema } from "./createActionRouter-BwaSM0No.mjs";
5
5
  import fp from "fastify-plugin";
6
6
  //#region src/docs/openapi.ts
7
7
  const openApiPlugin = async (fastify, opts = {}) => {
@@ -1,4 +1,4 @@
1
- import { Pt as RouteHandler } from "../index-BGbpGVyM.mjs";
1
+ import { yt as RouteHandler } from "../index-C_bgx9o4.mjs";
2
2
  import { d as UserBase } from "../fields-C8Y0XLAu.mjs";
3
3
  import { InvitationAdapter, InvitationDoc, MemberDoc, OrgAdapter, OrgDoc, OrgPermissionStatement, OrgRole, OrganizationPluginOptions } from "./types.mjs";
4
4
  import { FastifyPluginAsync, RouteHandlerMethod } from "fastify";
@@ -1,3 +1,3 @@
1
1
  import { a as applyFieldWritePermissions, c as PermissionCheck, d as UserBase, f as getUserRoles, i as applyFieldReadPermissions, l as PermissionContext, n as FieldPermissionMap, o as fields, p as normalizeRoles, r as FieldPermissionType, s as resolveEffectiveRoles, t as FieldPermission, u as PermissionResult } from "../fields-C8Y0XLAu.mjs";
2
- import { A as requireRoles, C as allOf, D as not, E as denyAll, M as when, N as applyPermissionResult, O as requireAuth, P as normalizePermissionResult, S as createOrgPermissions, T as anyOf, _ as ConnectEventsOptions, a as presets_d_exports, b as PermissionEventBus, c as readOnly, d as requireOrgRole, f as requireScopeContext, g as createRoleHierarchy, h as RoleHierarchy, i as ownerWithAdminBypass, j as roles, k as requireOwnership, l as requireOrgInScope, m as requireTeamMembership, n as authenticated, o as publicRead, p as requireServiceScope, r as fullPublic, s as publicReadAdminWrite, t as adminOnly, u as requireOrgMembership, v as DynamicPermissionMatrix, w as allowPublic, x as createDynamicPermissionMatrix, y as DynamicPermissionMatrixConfig } from "../index-C_Noptz-.mjs";
2
+ import { A as requireRoles, C as allOf, D as not, E as denyAll, M as when, N as applyPermissionResult, O as requireAuth, P as normalizePermissionResult, S as createOrgPermissions, T as anyOf, _ as ConnectEventsOptions, a as presets_d_exports, b as PermissionEventBus, c as readOnly, d as requireOrgRole, f as requireScopeContext, g as createRoleHierarchy, h as RoleHierarchy, i as ownerWithAdminBypass, j as roles, k as requireOwnership, l as requireOrgInScope, m as requireTeamMembership, n as authenticated, o as publicRead, p as requireServiceScope, r as fullPublic, s as publicReadAdminWrite, t as adminOnly, u as requireOrgMembership, v as DynamicPermissionMatrix, w as allowPublic, x as createDynamicPermissionMatrix, y as DynamicPermissionMatrixConfig } from "../index-BYCqHCVu.mjs";
3
3
  export { ConnectEventsOptions, DynamicPermissionMatrix, DynamicPermissionMatrixConfig, FieldPermission, FieldPermissionMap, FieldPermissionType, PermissionCheck, PermissionContext, PermissionEventBus, PermissionResult, RoleHierarchy, UserBase, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, presets_d_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAuth, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };
@@ -1,5 +1,3 @@
1
- import { i as resolveEffectiveRoles, n as applyFieldWritePermissions, r as fields, t as applyFieldReadPermissions } from "../fields-CTMWOUDt.mjs";
2
- import { n as normalizeRoles, t as getUserRoles } from "../types-D57iXYb8.mjs";
3
- import { n as normalizePermissionResult, t as applyPermissionResult } from "../applyPermissionResult-QhV1Pa-g.mjs";
4
- import { C as requireAuth, D as when, E as roles, S as not, T as requireRoles, _ as requireTeamMembership, a as presets_exports, b as anyOf, c as readOnly, d as createOrgPermissions, f as requireOrgInScope, g as requireServiceScope, h as requireScopeContext, i as ownerWithAdminBypass, l as createRoleHierarchy, m as requireOrgRole, n as authenticated, o as publicRead, p as requireOrgMembership, r as fullPublic, s as publicReadAdminWrite, t as adminOnly, u as createDynamicPermissionMatrix, v as allOf, w as requireOwnership, x as denyAll, y as allowPublic } from "../permissions-wkqRwicB.mjs";
1
+ import { A as normalizePermissionResult, C as requireAuth, D as when, E as roles, M as applyFieldWritePermissions, N as fields, O as applyPermissionResult, P as resolveEffectiveRoles, S as not, T as requireRoles, _ as requireTeamMembership, a as presets_exports, b as anyOf, c as readOnly, d as createOrgPermissions, f as requireOrgInScope, g as requireServiceScope, h as requireScopeContext, i as ownerWithAdminBypass, j as applyFieldReadPermissions, l as createRoleHierarchy, m as requireOrgRole, n as authenticated, o as publicRead, p as requireOrgMembership, r as fullPublic, s as publicReadAdminWrite, t as adminOnly, u as createDynamicPermissionMatrix, v as allOf, w as requireOwnership, x as denyAll, y as allowPublic } from "../permissions-B4vU9L0Q.mjs";
2
+ import { n as normalizeRoles, t as getUserRoles } from "../types-DV9WDfeg.mjs";
5
3
  export { adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, presets_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAuth, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };