@classytic/arc 2.9.1 → 2.10.3

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 (139) hide show
  1. package/README.md +19 -90
  2. package/dist/{BaseController-Vu2yc56T.mjs → BaseController-CbKKIflT.mjs} +8 -44
  3. package/dist/{ResourceRegistry-Dq3_zBQP.mjs → ResourceRegistry-BPd6NQDm.mjs} +1 -1
  4. package/dist/adapters/index.d.mts +3 -3
  5. package/dist/adapters/index.mjs +2 -2
  6. package/dist/{adapters-BBqAVvPK.mjs → adapters-BXY4i-hw.mjs} +210 -41
  7. package/dist/audit/index.d.mts +38 -3
  8. package/dist/audit/index.mjs +41 -7
  9. package/dist/auth/index.d.mts +4 -4
  10. package/dist/auth/index.mjs +5 -5
  11. package/dist/auth/redis-session.d.mts +1 -1
  12. package/dist/cache/index.d.mts +17 -15
  13. package/dist/cache/index.mjs +15 -14
  14. package/dist/{caching-CjybdRwx.mjs → caching-CBpK_SCM.mjs} +8 -3
  15. package/dist/cli/commands/describe.mjs +1 -1
  16. package/dist/cli/commands/docs.mjs +2 -2
  17. package/dist/cli/commands/generate.mjs +1 -1
  18. package/dist/cli/commands/init.mjs +1 -1
  19. package/dist/cli/commands/introspect.mjs +1 -1
  20. package/dist/core/index.d.mts +2 -2
  21. package/dist/core/index.mjs +3 -4
  22. package/dist/{defineResource-C__jkwvs.mjs → core-CcR01lup.mjs} +44 -12
  23. package/dist/{createActionRouter-DH1YFL9m.mjs → createActionRouter-Bp_5c_2b.mjs} +1 -1
  24. package/dist/{createApp-CBJUJKGP.mjs → createApp-BuvPma24.mjs} +14 -14
  25. package/dist/docs/index.d.mts +2 -2
  26. package/dist/docs/index.mjs +2 -2
  27. package/dist/{elevation-DxQ6ACbt.mjs → elevation-C7hgL_aI.mjs} +2 -2
  28. package/dist/{errorHandler-CZDW4EXS.mjs → errorHandler-Bb49BvPD.mjs} +1 -1
  29. package/dist/{errorHandler-DixGcttC.d.mts → errorHandler-DRQ3EqfL.d.mts} +1 -1
  30. package/dist/{eventPlugin-BxvaCIZF.d.mts → eventPlugin-CxWgpd6K.d.mts} +1 -1
  31. package/dist/{eventPlugin-Dl7MoVWH.mjs → eventPlugin-DCUjuiQT.mjs} +1 -1
  32. package/dist/events/index.d.mts +8 -5
  33. package/dist/events/index.mjs +34 -17
  34. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  35. package/dist/events/transports/redis.d.mts +1 -1
  36. package/dist/factory/index.d.mts +1 -1
  37. package/dist/factory/index.mjs +2 -2
  38. package/dist/{types-DZi1aYhm.d.mts → fields-Lo1VUDpt.d.mts} +121 -1
  39. package/dist/{filesUpload-q8oHt--L.mjs → filesUpload-t21LS-py.mjs} +2 -2
  40. package/dist/hooks/index.d.mts +1 -1
  41. package/dist/hooks/index.mjs +1 -1
  42. package/dist/idempotency/index.d.mts +7 -4
  43. package/dist/idempotency/index.mjs +9 -11
  44. package/dist/idempotency/redis.d.mts +1 -1
  45. package/dist/{index-Cibkchnx.d.mts → index-8qw4y6ff.d.mts} +2 -2
  46. package/dist/{index-C-xjcA6F.d.mts → index-ChIw3776.d.mts} +283 -408
  47. package/dist/{interface-YrWsmKqE.d.mts → index-Cl0uoKd5.d.mts} +1885 -2741
  48. package/dist/{index-CtGKT0lf.d.mts → index-DStwgFUK.d.mts} +81 -7
  49. package/dist/index.d.mts +7 -8
  50. package/dist/index.mjs +11 -12
  51. package/dist/integrations/event-gateway.d.mts +1 -1
  52. package/dist/integrations/event-gateway.mjs +1 -1
  53. package/dist/integrations/index.d.mts +1 -1
  54. package/dist/integrations/mcp/index.d.mts +2 -2
  55. package/dist/integrations/mcp/index.mjs +1 -1
  56. package/dist/integrations/mcp/testing.d.mts +1 -1
  57. package/dist/integrations/mcp/testing.mjs +1 -1
  58. package/dist/interface-D218ikEo.d.mts +77 -0
  59. package/dist/{memory-BFAYkf8H.mjs → memory-B5Amv9A1.mjs} +23 -8
  60. package/dist/{openapi-CXuTG1M9.mjs → openapi-B5F8AddX.mjs} +2 -2
  61. package/dist/org/index.d.mts +2 -2
  62. package/dist/permissions/index.d.mts +3 -4
  63. package/dist/permissions/index.mjs +5 -5
  64. package/dist/{permissions-oNZawnkR.mjs → permissions-Dk6mshja.mjs} +315 -397
  65. package/dist/plugins/index.d.mts +4 -4
  66. package/dist/plugins/index.mjs +12 -14
  67. package/dist/plugins/response-cache.mjs +1 -1
  68. package/dist/plugins/tracing-entry.d.mts +1 -1
  69. package/dist/plugins/tracing-entry.mjs +1 -1
  70. package/dist/presets/filesUpload.d.mts +3 -3
  71. package/dist/presets/filesUpload.mjs +1 -1
  72. package/dist/presets/index.d.mts +1 -1
  73. package/dist/presets/index.mjs +2 -2
  74. package/dist/presets/multiTenant.d.mts +1 -1
  75. package/dist/presets/multiTenant.mjs +1 -1
  76. package/dist/presets/search.d.mts +91 -4
  77. package/dist/presets/search.mjs +1 -1
  78. package/dist/{presets-hM4WhNWY.mjs → presets-fLJVXdVn.mjs} +1 -1
  79. package/dist/{queryCachePlugin-CnTZZTC5.d.mts → queryCachePlugin-BKbWjgDG.d.mts} +1 -1
  80. package/dist/{queryCachePlugin-DbUVroUG.mjs → queryCachePlugin-DQCEfJis.mjs} +8 -8
  81. package/dist/{queryParser-Cs-6SHQK.mjs → queryParser-DBqBB6AC.mjs} +1 -1
  82. package/dist/{redis-MXLp1oOf.d.mts → redis-DqyeggCa.d.mts} +1 -1
  83. package/dist/{redis-stream-Bz-4q96t.d.mts → redis-stream-CakIQmwR.d.mts} +1 -1
  84. package/dist/registry/index.d.mts +1 -1
  85. package/dist/registry/index.mjs +2 -2
  86. package/dist/{resourceToTools-C3cWymnW.mjs → resourceToTools-BElv3xPT.mjs} +3 -3
  87. package/dist/scope/index.d.mts +1 -1
  88. package/dist/scope/index.mjs +2 -2
  89. package/dist/{sse-CJpt7LGI.mjs → sse-yBCgOLGu.mjs} +1 -1
  90. package/dist/testing/index.d.mts +6 -5
  91. package/dist/testing/index.mjs +8 -10
  92. package/dist/testing/storageContract.d.mts +1 -1
  93. package/dist/types/index.d.mts +4 -4
  94. package/dist/types/index.mjs +1 -31
  95. package/dist/types/storage.d.mts +1 -1
  96. package/dist/{types-CoSzA-s-.d.mts → types-Btdda02s.d.mts} +1 -1
  97. package/dist/{types-CunEX4UX.d.mts → types-Co8k3NyS.d.mts} +9 -9
  98. package/dist/types-Csi3FLfq.mjs +27 -0
  99. package/dist/utils/index.d.mts +207 -3
  100. package/dist/utils/index.mjs +3 -4
  101. package/dist/{utils-B7FuRr9w.mjs → utils-B2fNOD_i.mjs} +285 -2
  102. package/dist/{versioning-Cm8qoFDg.mjs → versioning-C2U_bLY0.mjs} +3 -5
  103. package/package.json +15 -18
  104. package/skills/arc/SKILL.md +7 -11
  105. package/skills/arc/references/production.md +0 -41
  106. package/dist/circuitBreaker-CvXkjfrW.d.mts +0 -206
  107. package/dist/circuitBreaker-l18oRgL5.mjs +0 -284
  108. package/dist/core-DNncu0xF.mjs +0 -34
  109. package/dist/dynamic/index.d.mts +0 -93
  110. package/dist/dynamic/index.mjs +0 -122
  111. package/dist/fields-BC7zcmI9.d.mts +0 -121
  112. package/dist/interface-DplgQO2e.d.mts +0 -54
  113. package/dist/policies/index.d.mts +0 -425
  114. package/dist/policies/index.mjs +0 -318
  115. package/dist/rpc/index.d.mts +0 -90
  116. package/dist/rpc/index.mjs +0 -248
  117. /package/dist/{EventTransport-CqZ8FyM_.d.mts → EventTransport-CUw5NNWe.d.mts} +0 -0
  118. /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-BNYKnrXF.mjs} +0 -0
  119. /package/dist/{applyPermissionResult-bqGpo9ML.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
  120. /package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-BBRVhjQN.mjs} +0 -0
  121. /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
  122. /package/dist/{elevation-B6S5csVA.d.mts → elevation-C5SwtkAn.d.mts} +0 -0
  123. /package/dist/{errors-BI8kEKsO.d.mts → errors-CCSsMpXE.d.mts} +0 -0
  124. /package/dist/{errors-CqWnSqM-.mjs → errors-D5c-5BJL.mjs} +0 -0
  125. /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BQ8QijNH.d.mts} +0 -0
  126. /package/dist/{fields-CU6FlaDV.mjs → fields-bxkeltzz.mjs} +0 -0
  127. /package/dist/{interface-B-pe8fhj.d.mts → interface-CSbZdv_3.d.mts} +0 -0
  128. /package/dist/{loadResources-Bksk8ydA.mjs → loadResources-BAzJItAJ.mjs} +0 -0
  129. /package/dist/{logger-CDjpjySd.mjs → logger-DLg8-Ueg.mjs} +0 -0
  130. /package/dist/{metrics-TuOmguhi.mjs → metrics-DuhiSEZI.mjs} +0 -0
  131. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-A0tWEl1K.mjs} +0 -0
  132. /package/dist/{registry-B0Wl7uVV.mjs → registry-B3lRFBWo.mjs} +0 -0
  133. /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-CXtJDAZ0.mjs} +0 -0
  134. /package/dist/{requestContext-DYtmNpm5.mjs → requestContext-xHIKedG6.mjs} +0 -0
  135. /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-BkzVU8h2.d.mts} +0 -0
  136. /package/dist/{storage-BwGQXUpd.d.mts → storage-CVk_SEn2.d.mts} +0 -0
  137. /package/dist/{store-helpers-DFiZl5TL.mjs → store-helpers-ZCSMJJAX.mjs} +0 -0
  138. /package/dist/{tracing-xqXzWeaf.d.mts → tracing-65B51Dw3.d.mts} +0 -0
  139. /package/dist/{types-ZUu_h0jp.mjs → types-DV9WDfeg.mjs} +0 -0
@@ -1,318 +0,0 @@
1
- //#region src/policies/helpers.ts
2
- /**
3
- * Helper to create Fastify middleware from any PolicyEngine implementation
4
- *
5
- * This is a convenience function that provides a standard middleware pattern.
6
- * Most policies can use this instead of implementing toMiddleware() manually.
7
- *
8
- * @param policy - Policy engine instance
9
- * @param operation - Operation name (list, get, create, update, delete)
10
- * @returns Fastify preHandler middleware
11
- *
12
- * @example
13
- * ```typescript
14
- * class SimplePolicy implements PolicyEngine {
15
- * can(user, operation) {
16
- * return { allowed: user.isActive };
17
- * }
18
- *
19
- * toMiddleware(operation) {
20
- * return createPolicyMiddleware(this, operation);
21
- * }
22
- * }
23
- * ```
24
- */
25
- function createPolicyMiddleware(policy, operation) {
26
- return async function policyMiddleware(request, reply) {
27
- const context = {
28
- document: request.document,
29
- body: request.body,
30
- params: request.params,
31
- query: request.query
32
- };
33
- const result = await policy.can(request.user, operation, context);
34
- if (!result.allowed) return reply.code(403).send({
35
- success: false,
36
- error: "Access denied",
37
- message: result.reason || "You do not have permission to perform this action"
38
- });
39
- request.policyResult = result;
40
- if (result.filters && Object.keys(result.filters).length > 0) request._policyFilters = result.filters;
41
- if (result.fieldMask) request.fieldMask = result.fieldMask;
42
- if (result.metadata) request.policyMetadata = result.metadata;
43
- };
44
- }
45
- /**
46
- * Combine multiple policies with AND logic
47
- *
48
- * All policies must allow the operation for it to succeed.
49
- * First denial stops evaluation and returns the denial reason.
50
- *
51
- * @param policies - Array of policy engines to combine
52
- * @returns Combined policy engine
53
- *
54
- * @example
55
- * ```typescript
56
- * const combinedPolicy = combinePolicies(
57
- * rbacPolicy, // Must have correct role
58
- * ownershipPolicy, // Must own the resource
59
- * auditPolicy, // Logs access for compliance
60
- * );
61
- *
62
- * // All three policies must pass for the operation to succeed
63
- * const result = await combinedPolicy.can(user, 'update', context);
64
- * ```
65
- *
66
- * @example Multi-tenant + RBAC
67
- * ```typescript
68
- * const policy = combinePolicies(
69
- * definePolicy({ tenant: { field: 'organizationId' } }),
70
- * definePolicy({ roles: { update: ['admin', 'editor'] } }),
71
- * );
72
- * ```
73
- */
74
- function combinePolicies(...policies) {
75
- if (policies.length === 0) throw new Error("combinePolicies requires at least one policy");
76
- if (policies.length === 1) return policies[0];
77
- return {
78
- async can(user, operation, context) {
79
- const results = [];
80
- for (const policy of policies) {
81
- const result = await policy.can(user, operation, context);
82
- if (!result.allowed) return result;
83
- results.push(result);
84
- }
85
- const mergedResult = {
86
- allowed: true,
87
- filters: {},
88
- metadata: {}
89
- };
90
- for (const result of results) if (result.filters) Object.assign(mergedResult.filters, result.filters);
91
- const allExcludes = /* @__PURE__ */ new Set();
92
- const allIncludes = [];
93
- for (const result of results) {
94
- if (result.fieldMask?.exclude) for (const field of result.fieldMask.exclude) allExcludes.add(field);
95
- if (result.fieldMask?.include) allIncludes.push(new Set(result.fieldMask.include));
96
- }
97
- if (allExcludes.size > 0 || allIncludes.length > 0) {
98
- mergedResult.fieldMask = {};
99
- if (allExcludes.size > 0) mergedResult.fieldMask.exclude = Array.from(allExcludes);
100
- if (allIncludes.length > 0) {
101
- const intersection = allIncludes.reduce((acc, set) => {
102
- return new Set([...acc].filter((x) => set.has(x)));
103
- });
104
- if (intersection.size > 0) mergedResult.fieldMask.include = Array.from(intersection);
105
- }
106
- }
107
- for (const result of results) if (result.metadata) Object.assign(mergedResult.metadata, result.metadata);
108
- if (Object.keys(mergedResult.filters).length === 0) delete mergedResult.filters;
109
- if (Object.keys(mergedResult.metadata).length === 0) delete mergedResult.metadata;
110
- return mergedResult;
111
- },
112
- toMiddleware(operation) {
113
- const middlewares = policies.map((p) => p.toMiddleware(operation));
114
- return async (request, reply) => {
115
- for (const middleware of middlewares) {
116
- await middleware(request, reply);
117
- if (reply.sent) return;
118
- }
119
- };
120
- }
121
- };
122
- }
123
- /**
124
- * Combine multiple policies with OR logic
125
- *
126
- * At least one policy must allow the operation for it to succeed.
127
- * If all policies deny, returns the first denial reason.
128
- *
129
- * @param policies - Array of policy engines to combine
130
- * @returns Combined policy engine
131
- *
132
- * @example
133
- * ```typescript
134
- * const policy = anyPolicy(
135
- * ownerPolicy, // User owns the resource
136
- * adminPolicy, // OR user is admin
137
- * publicPolicy, // OR resource is public
138
- * );
139
- *
140
- * // Any one of these policies passing allows the operation
141
- * ```
142
- */
143
- function anyPolicy(...policies) {
144
- if (policies.length === 0) throw new Error("anyPolicy requires at least one policy");
145
- if (policies.length === 1) return policies[0];
146
- return {
147
- async can(user, operation, context) {
148
- let firstDenial = null;
149
- for (const policy of policies) {
150
- const result = await policy.can(user, operation, context);
151
- if (result.allowed) return result;
152
- if (!firstDenial) firstDenial = result;
153
- }
154
- return firstDenial;
155
- },
156
- toMiddleware(operation) {
157
- return async (request, reply) => {
158
- const results = [];
159
- for (const policy of policies) {
160
- const result = await policy.can(request.user, operation, {
161
- document: request.document,
162
- body: request.body,
163
- params: request.params,
164
- query: request.query
165
- });
166
- if (result.allowed) {
167
- request.policyResult = result;
168
- if (result.filters) request._policyFilters = result.filters;
169
- if (result.fieldMask) request.fieldMask = result.fieldMask;
170
- if (result.metadata) request.policyMetadata = result.metadata;
171
- return;
172
- }
173
- results.push(result);
174
- }
175
- return reply.code(403).send({
176
- success: false,
177
- error: "Access denied",
178
- message: results[0]?.reason || "You do not have permission to perform this action"
179
- });
180
- };
181
- }
182
- };
183
- }
184
- /**
185
- * Create a pass-through policy that always allows
186
- *
187
- * Useful for testing or for routes that don't need authorization.
188
- *
189
- * @example
190
- * ```typescript
191
- * const policy = allowAll();
192
- * const result = await policy.can(user, 'any-operation');
193
- * // result.allowed === true
194
- * ```
195
- */
196
- function allowAll() {
197
- return {
198
- can() {
199
- return { allowed: true };
200
- },
201
- toMiddleware() {
202
- return async () => {};
203
- }
204
- };
205
- }
206
- /**
207
- * Create a policy that always denies
208
- *
209
- * Useful for explicitly blocking operations or for testing.
210
- *
211
- * @param reason - Denial reason
212
- *
213
- * @example
214
- * ```typescript
215
- * const policy = denyAll('This resource is deprecated');
216
- * const result = await policy.can(user, 'any-operation');
217
- * // result.allowed === false
218
- * // result.reason === 'This resource is deprecated'
219
- * ```
220
- */
221
- function denyAll(reason = "Operation not allowed") {
222
- return {
223
- can() {
224
- return {
225
- allowed: false,
226
- reason
227
- };
228
- },
229
- toMiddleware() {
230
- return async (_request, reply) => {
231
- return reply.code(403).send({
232
- success: false,
233
- error: "Access denied",
234
- message: reason
235
- });
236
- };
237
- }
238
- };
239
- }
240
- //#endregion
241
- //#region src/policies/PolicyInterface.ts
242
- /**
243
- * Create a PermissionCheck from access control statements.
244
- *
245
- * Maps Better Auth's statement-based access control model to Arc's
246
- * PermissionCheck function, which can be used directly in resource permissions.
247
- *
248
- * The returned PermissionCheck:
249
- * 1. Looks up the resource + action in the statements list
250
- * 2. If no matching statement exists, denies access
251
- * 3. If a matching statement exists and `checkPermission` is provided,
252
- * calls it for dynamic verification (e.g., check org role)
253
- * 4. If `checkPermission` is not provided, allows access based on static statements
254
- *
255
- * @example Static statements only
256
- * ```typescript
257
- * import { createAccessControlPolicy } from '@classytic/arc/policies';
258
- *
259
- * const editorPermissions = createAccessControlPolicy({
260
- * statements: [
261
- * { resource: 'product', action: ['create', 'update'] },
262
- * { resource: 'order', action: ['read'] },
263
- * ],
264
- * });
265
- *
266
- * // Use in resource config
267
- * defineResource({
268
- * name: 'product',
269
- * permissions: {
270
- * create: editorPermissions,
271
- * update: editorPermissions,
272
- * },
273
- * });
274
- * ```
275
- *
276
- * @example With dynamic permission check (Better Auth org roles)
277
- * ```typescript
278
- * const policy = createAccessControlPolicy({
279
- * statements: [
280
- * { resource: 'product', action: ['create', 'update'] },
281
- * { resource: 'order', action: ['read'] },
282
- * ],
283
- * checkPermission: async (userId, resource, action) => {
284
- * return hasOrgPermission(userId, resource, action);
285
- * },
286
- * });
287
- * ```
288
- */
289
- function createAccessControlPolicy(options) {
290
- const statementMap = /* @__PURE__ */ new Map();
291
- for (const statement of options.statements) {
292
- const existing = statementMap.get(statement.resource);
293
- if (existing) for (const action of statement.action) existing.add(action);
294
- else statementMap.set(statement.resource, new Set(statement.action));
295
- }
296
- const permissionCheck = async (context) => {
297
- const { user, resource, action } = context;
298
- if (!statementMap.get(resource)?.has(action)) return {
299
- granted: false,
300
- reason: `Action '${action}' is not permitted on resource '${resource}'`
301
- };
302
- if (options.checkPermission) {
303
- const userId = user?.id ?? user?._id;
304
- if (!userId) return {
305
- granted: false,
306
- reason: "Authentication required"
307
- };
308
- if (!await options.checkPermission(String(userId), resource, action)) return {
309
- granted: false,
310
- reason: `User does not have '${action}' permission on '${resource}'`
311
- };
312
- }
313
- return { granted: true };
314
- };
315
- return permissionCheck;
316
- }
317
- //#endregion
318
- export { allowAll, anyPolicy, combinePolicies, createAccessControlPolicy, createPolicyMiddleware, denyAll };
@@ -1,90 +0,0 @@
1
- import { r as CircuitBreakerOptions } from "../circuitBreaker-CvXkjfrW.mjs";
2
-
3
- //#region src/rpc/serviceClient.d.ts
4
- interface RetryConfig {
5
- /** Max retry attempts (not counting initial attempt). Default: 2 */
6
- maxRetries?: number;
7
- /** Initial backoff delay in ms. Doubles on each retry. Default: 200 */
8
- backoffMs?: number;
9
- /** Max backoff cap in ms. Default: 5000 */
10
- maxBackoffMs?: number;
11
- /**
12
- * HTTP status codes to retry on. Default: [502, 503, 504, 408, 429]
13
- * 4xx errors (except 408, 429) are NOT retried — they are client errors.
14
- */
15
- retryableStatuses?: number[];
16
- }
17
- interface RequestInfo {
18
- method: string;
19
- url: string;
20
- headers?: Record<string, string>;
21
- }
22
- interface ResponseInfo {
23
- method: string;
24
- url: string;
25
- status: number;
26
- durationMs: number;
27
- retries: number;
28
- }
29
- interface ServiceClientOptions {
30
- /** Base URL of the remote Arc service (e.g., 'http://catalog-service:3000') */
31
- baseUrl: string;
32
- /** Static bearer token, or function that returns one (for rotation) */
33
- token?: string | (() => string);
34
- /** Organization ID — sent as x-organization-id header */
35
- organizationId?: string;
36
- /**
37
- * Correlation ID for distributed tracing — sent as x-request-id header.
38
- * Static string or function (e.g., () => request.id from current request context).
39
- */
40
- correlationId?: string | (() => string);
41
- /** Schema version — sent as x-arc-schema-version header for contract compatibility */
42
- schemaVersion?: string;
43
- /** Additional headers sent with every request */
44
- headers?: Record<string, string>;
45
- /** Request timeout in ms (default: 10000) */
46
- timeout?: number;
47
- /** Retry config for transient failures (default: disabled) */
48
- retry?: RetryConfig;
49
- /** Circuit breaker config (default: disabled) */
50
- circuitBreaker?: Pick<CircuitBreakerOptions, "failureThreshold" | "resetTimeout" | "timeout" | "successThreshold">;
51
- /** Health check path (default: '/_health/live' — matches Arc's health plugin) */
52
- healthPath?: string;
53
- /** Called before each request (for logging, metrics, tracing) */
54
- onRequest?: (info: RequestInfo) => void;
55
- /** Called after each response (for logging, metrics, tracing) */
56
- onResponse?: (info: ResponseInfo) => void;
57
- }
58
- interface ResourceClient {
59
- /** GET /{resource}s?...query */
60
- list(query?: Record<string, unknown>): Promise<ServiceResponse>;
61
- /** GET /{resource}s/:id */
62
- get(id: string): Promise<ServiceResponse>;
63
- /** POST /{resource}s */
64
- create(data: Record<string, unknown>): Promise<ServiceResponse>;
65
- /** PATCH /{resource}s/:id */
66
- update(id: string, data: Record<string, unknown>): Promise<ServiceResponse>;
67
- /** DELETE /{resource}s/:id */
68
- delete(id: string): Promise<ServiceResponse>;
69
- /** POST /{resource}s/:id/action */
70
- action(id: string, actionName: string, data?: Record<string, unknown>): Promise<ServiceResponse>;
71
- }
72
- interface ServiceResponse<T = any> {
73
- success: boolean;
74
- data?: T;
75
- error?: string;
76
- message?: string;
77
- status?: number;
78
- meta?: Record<string, unknown>;
79
- }
80
- interface ServiceClient {
81
- /** Get a typed resource client for CRUD + actions */
82
- resource(name: string): ResourceClient;
83
- /** Raw call to any path (for non-resource endpoints) */
84
- call(method: string, path: string, body?: unknown): Promise<ServiceResponse>;
85
- /** Health check — returns true if service is reachable */
86
- health(): Promise<boolean>;
87
- }
88
- declare function createServiceClient(options: ServiceClientOptions): ServiceClient;
89
- //#endregion
90
- export { type RequestInfo, type ResourceClient, type ResponseInfo, type RetryConfig, type ServiceClient, type ServiceClientOptions, type ServiceResponse, createServiceClient };
@@ -1,248 +0,0 @@
1
- import { t as CircuitBreaker } from "../circuitBreaker-l18oRgL5.mjs";
2
- //#region src/rpc/serviceClient.ts
3
- /**
4
- * Service Client — Resource-Oriented RPC
5
- *
6
- * Typed HTTP client that speaks Arc's resource protocol.
7
- * Built for microservice-to-microservice communication with:
8
- * - correlationId propagation (distributed tracing)
9
- * - Retry with exponential backoff (transient failure recovery)
10
- * - Circuit breaker integration (cascading failure prevention)
11
- * - Error normalization (consistent error handling)
12
- * - Lifecycle hooks (observability)
13
- *
14
- * Zero external dependencies — uses native fetch + Arc's CircuitBreaker.
15
- *
16
- * @example
17
- * ```typescript
18
- * import { createServiceClient } from '@classytic/arc/rpc';
19
- *
20
- * const catalog = createServiceClient({
21
- * baseUrl: 'http://catalog-service:3000',
22
- * token: () => getServiceToken(),
23
- * correlationId: () => request.id, // propagate trace context
24
- * organizationId: req.scope.organizationId,
25
- * retry: { maxRetries: 2, backoffMs: 200 },
26
- * circuitBreaker: { failureThreshold: 5, resetTimeout: 30000 },
27
- * onResponse: ({ method, url, status, durationMs }) => {
28
- * metrics.histogram('rpc_duration', durationMs, { method, url, status });
29
- * },
30
- * });
31
- *
32
- * const products = await catalog.resource('product').list({ filters: { active: true } });
33
- * ```
34
- */
35
- const DEFAULT_RETRYABLE_STATUSES = [
36
- 502,
37
- 503,
38
- 504,
39
- 408,
40
- 429
41
- ];
42
- function createServiceClient(options) {
43
- const { baseUrl, token, organizationId, correlationId, schemaVersion, headers: extraHeaders = {}, timeout = 1e4, retry: retryConfig, circuitBreaker: cbOpts, healthPath = "/_health/live", onRequest, onResponse } = options;
44
- const base = baseUrl.replace(/\/+$/, "");
45
- let breaker;
46
- if (cbOpts) breaker = new CircuitBreaker(singleFetch, {
47
- name: `service-client:${base}`,
48
- failureThreshold: cbOpts.failureThreshold ?? 5,
49
- resetTimeout: cbOpts.resetTimeout ?? 6e4,
50
- timeout: cbOpts.timeout ?? timeout,
51
- successThreshold: cbOpts.successThreshold ?? 1
52
- });
53
- function buildHeaders(hasBody = false) {
54
- const h = {
55
- accept: "application/json",
56
- ...extraHeaders
57
- };
58
- if (hasBody) h["content-type"] = "application/json";
59
- const resolvedToken = typeof token === "function" ? token() : token;
60
- if (resolvedToken) h.authorization = `Bearer ${resolvedToken}`;
61
- if (organizationId) h["x-organization-id"] = organizationId;
62
- if (schemaVersion) h["x-arc-schema-version"] = schemaVersion;
63
- const resolvedCorrelationId = typeof correlationId === "function" ? correlationId() : correlationId;
64
- if (resolvedCorrelationId) h["x-request-id"] = resolvedCorrelationId;
65
- return h;
66
- }
67
- async function singleFetch(url, init) {
68
- const controller = new AbortController();
69
- const timer = setTimeout(() => controller.abort(), timeout);
70
- try {
71
- const hasBody = !!init.body;
72
- const response = await fetch(url, {
73
- ...init,
74
- signal: controller.signal,
75
- headers: {
76
- ...buildHeaders(hasBody),
77
- ...init.headers ?? {}
78
- }
79
- });
80
- let body;
81
- if ((response.headers.get("content-type") ?? "").includes("application/json")) body = await response.json();
82
- else {
83
- const text = await response.text();
84
- body = {
85
- success: false,
86
- error: response.statusText || "Unknown error",
87
- message: text.slice(0, 200),
88
- status: response.status
89
- };
90
- }
91
- if (body.status === void 0) body.status = response.status;
92
- if (body.success === void 0) body.success = response.ok;
93
- return {
94
- response,
95
- body
96
- };
97
- } finally {
98
- clearTimeout(timer);
99
- }
100
- }
101
- async function execute(method, url, init) {
102
- const startTime = performance.now();
103
- let lastResponse;
104
- let retries = 0;
105
- const maxRetries = retryConfig?.maxRetries ?? 0;
106
- const backoffMs = retryConfig?.backoffMs ?? 200;
107
- const maxBackoffMs = retryConfig?.maxBackoffMs ?? 5e3;
108
- const retryableStatuses = retryConfig?.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES;
109
- onRequest?.({
110
- method,
111
- url
112
- });
113
- for (let attempt = 0; attempt <= maxRetries; attempt++) try {
114
- let result;
115
- if (breaker) result = await breaker.call(url, init);
116
- else result = await singleFetch(url, init);
117
- lastResponse = result.body;
118
- if (result.response.ok || !retryableStatuses.includes(result.response.status)) {
119
- onResponse?.({
120
- method,
121
- url,
122
- status: result.response.status,
123
- durationMs: performance.now() - startTime,
124
- retries
125
- });
126
- return result.body;
127
- }
128
- if (attempt < maxRetries) {
129
- retries++;
130
- await sleep(Math.min(backoffMs * 2 ** attempt, maxBackoffMs));
131
- continue;
132
- }
133
- onResponse?.({
134
- method,
135
- url,
136
- status: result.response.status,
137
- durationMs: performance.now() - startTime,
138
- retries
139
- });
140
- return result.body;
141
- } catch (err) {
142
- if (attempt < maxRetries) {
143
- retries++;
144
- await sleep(Math.min(backoffMs * 2 ** attempt, maxBackoffMs));
145
- continue;
146
- }
147
- const error = err instanceof Error ? err : new Error(String(err));
148
- lastResponse = {
149
- success: false,
150
- error: error.message,
151
- status: 0
152
- };
153
- onResponse?.({
154
- method,
155
- url,
156
- status: 0,
157
- durationMs: performance.now() - startTime,
158
- retries
159
- });
160
- if (maxRetries === 0) throw error;
161
- return lastResponse;
162
- }
163
- return lastResponse ?? {
164
- success: false,
165
- error: "Unknown error",
166
- status: 0
167
- };
168
- }
169
- function toQueryString(query) {
170
- if (!query || Object.keys(query).length === 0) return "";
171
- const params = new URLSearchParams();
172
- for (const [key, value] of Object.entries(query)) if (value !== void 0 && value !== null) if (typeof value === "object") {
173
- for (const [k, v] of Object.entries(value)) if (v !== void 0 && v !== null) params.set(k, String(v));
174
- } else params.set(key, String(value));
175
- const qs = params.toString();
176
- return qs ? `?${qs}` : "";
177
- }
178
- function plural(name) {
179
- if (name.endsWith("s")) return name;
180
- if (name.endsWith("y") && !name.endsWith("ay") && !name.endsWith("ey") && !name.endsWith("oy") && !name.endsWith("uy")) return `${name.slice(0, -1)}ies`;
181
- return `${name}s`;
182
- }
183
- return {
184
- resource(name) {
185
- const prefix = `${base}/${plural(name)}`;
186
- return {
187
- async list(query) {
188
- return execute("GET", `${prefix}${toQueryString(query?.filters ? query.filters : query)}`, { method: "GET" });
189
- },
190
- async get(id) {
191
- return execute("GET", `${prefix}/${id}`, { method: "GET" });
192
- },
193
- async create(data) {
194
- return execute("POST", prefix, {
195
- method: "POST",
196
- body: JSON.stringify(data)
197
- });
198
- },
199
- async update(id, data) {
200
- return execute("PATCH", `${prefix}/${id}`, {
201
- method: "PATCH",
202
- body: JSON.stringify(data)
203
- });
204
- },
205
- async delete(id) {
206
- return execute("DELETE", `${prefix}/${id}`, { method: "DELETE" });
207
- },
208
- async action(id, actionName, data) {
209
- return execute("POST", `${prefix}/${id}/action`, {
210
- method: "POST",
211
- body: JSON.stringify({
212
- action: actionName,
213
- ...data
214
- })
215
- });
216
- }
217
- };
218
- },
219
- async call(method, path, body) {
220
- const url = `${base}${path}`;
221
- const init = { method };
222
- if (body !== void 0) init.body = JSON.stringify(body);
223
- return execute(method, url, init);
224
- },
225
- async health() {
226
- try {
227
- const controller = new AbortController();
228
- const timer = setTimeout(() => controller.abort(), timeout);
229
- try {
230
- return (await fetch(`${base}${healthPath}`, {
231
- method: "GET",
232
- signal: controller.signal,
233
- headers: buildHeaders()
234
- })).ok;
235
- } finally {
236
- clearTimeout(timer);
237
- }
238
- } catch {
239
- return false;
240
- }
241
- }
242
- };
243
- }
244
- function sleep(ms) {
245
- return new Promise((resolve) => setTimeout(resolve, ms));
246
- }
247
- //#endregion
248
- export { createServiceClient };