@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.
- package/README.md +19 -90
- package/dist/{BaseController-Vu2yc56T.mjs → BaseController-CbKKIflT.mjs} +8 -44
- package/dist/{ResourceRegistry-Dq3_zBQP.mjs → ResourceRegistry-BPd6NQDm.mjs} +1 -1
- package/dist/adapters/index.d.mts +3 -3
- package/dist/adapters/index.mjs +2 -2
- package/dist/{adapters-BBqAVvPK.mjs → adapters-BXY4i-hw.mjs} +210 -41
- package/dist/audit/index.d.mts +38 -3
- package/dist/audit/index.mjs +41 -7
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +5 -5
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/cache/index.d.mts +17 -15
- package/dist/cache/index.mjs +15 -14
- package/dist/{caching-CjybdRwx.mjs → caching-CBpK_SCM.mjs} +8 -3
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -4
- package/dist/{defineResource-C__jkwvs.mjs → core-CcR01lup.mjs} +44 -12
- package/dist/{createActionRouter-DH1YFL9m.mjs → createActionRouter-Bp_5c_2b.mjs} +1 -1
- package/dist/{createApp-CBJUJKGP.mjs → createApp-BuvPma24.mjs} +14 -14
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DxQ6ACbt.mjs → elevation-C7hgL_aI.mjs} +2 -2
- package/dist/{errorHandler-CZDW4EXS.mjs → errorHandler-Bb49BvPD.mjs} +1 -1
- package/dist/{errorHandler-DixGcttC.d.mts → errorHandler-DRQ3EqfL.d.mts} +1 -1
- package/dist/{eventPlugin-BxvaCIZF.d.mts → eventPlugin-CxWgpd6K.d.mts} +1 -1
- package/dist/{eventPlugin-Dl7MoVWH.mjs → eventPlugin-DCUjuiQT.mjs} +1 -1
- package/dist/events/index.d.mts +8 -5
- package/dist/events/index.mjs +34 -17
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +2 -2
- package/dist/{types-DZi1aYhm.d.mts → fields-Lo1VUDpt.d.mts} +121 -1
- package/dist/{filesUpload-q8oHt--L.mjs → filesUpload-t21LS-py.mjs} +2 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +7 -4
- package/dist/idempotency/index.mjs +9 -11
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-Cibkchnx.d.mts → index-8qw4y6ff.d.mts} +2 -2
- package/dist/{index-C-xjcA6F.d.mts → index-ChIw3776.d.mts} +283 -408
- package/dist/{interface-YrWsmKqE.d.mts → index-Cl0uoKd5.d.mts} +1885 -2741
- package/dist/{index-CtGKT0lf.d.mts → index-DStwgFUK.d.mts} +81 -7
- package/dist/index.d.mts +7 -8
- package/dist/index.mjs +11 -12
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/interface-D218ikEo.d.mts +77 -0
- package/dist/{memory-BFAYkf8H.mjs → memory-B5Amv9A1.mjs} +23 -8
- package/dist/{openapi-CXuTG1M9.mjs → openapi-B5F8AddX.mjs} +2 -2
- package/dist/org/index.d.mts +2 -2
- package/dist/permissions/index.d.mts +3 -4
- package/dist/permissions/index.mjs +5 -5
- package/dist/{permissions-oNZawnkR.mjs → permissions-Dk6mshja.mjs} +315 -397
- package/dist/plugins/index.d.mts +4 -4
- package/dist/plugins/index.mjs +12 -14
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +2 -2
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +91 -4
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-hM4WhNWY.mjs → presets-fLJVXdVn.mjs} +1 -1
- package/dist/{queryCachePlugin-CnTZZTC5.d.mts → queryCachePlugin-BKbWjgDG.d.mts} +1 -1
- package/dist/{queryCachePlugin-DbUVroUG.mjs → queryCachePlugin-DQCEfJis.mjs} +8 -8
- package/dist/{queryParser-Cs-6SHQK.mjs → queryParser-DBqBB6AC.mjs} +1 -1
- package/dist/{redis-MXLp1oOf.d.mts → redis-DqyeggCa.d.mts} +1 -1
- package/dist/{redis-stream-Bz-4q96t.d.mts → redis-stream-CakIQmwR.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{resourceToTools-C3cWymnW.mjs → resourceToTools-BElv3xPT.mjs} +3 -3
- package/dist/scope/index.d.mts +1 -1
- package/dist/scope/index.mjs +2 -2
- package/dist/{sse-CJpt7LGI.mjs → sse-yBCgOLGu.mjs} +1 -1
- package/dist/testing/index.d.mts +6 -5
- package/dist/testing/index.mjs +8 -10
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/index.mjs +1 -31
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-CoSzA-s-.d.mts → types-Btdda02s.d.mts} +1 -1
- package/dist/{types-CunEX4UX.d.mts → types-Co8k3NyS.d.mts} +9 -9
- package/dist/types-Csi3FLfq.mjs +27 -0
- package/dist/utils/index.d.mts +207 -3
- package/dist/utils/index.mjs +3 -4
- package/dist/{utils-B7FuRr9w.mjs → utils-B2fNOD_i.mjs} +285 -2
- package/dist/{versioning-Cm8qoFDg.mjs → versioning-C2U_bLY0.mjs} +3 -5
- package/package.json +15 -18
- package/skills/arc/SKILL.md +7 -11
- package/skills/arc/references/production.md +0 -41
- package/dist/circuitBreaker-CvXkjfrW.d.mts +0 -206
- package/dist/circuitBreaker-l18oRgL5.mjs +0 -284
- package/dist/core-DNncu0xF.mjs +0 -34
- package/dist/dynamic/index.d.mts +0 -93
- package/dist/dynamic/index.mjs +0 -122
- package/dist/fields-BC7zcmI9.d.mts +0 -121
- package/dist/interface-DplgQO2e.d.mts +0 -54
- package/dist/policies/index.d.mts +0 -425
- package/dist/policies/index.mjs +0 -318
- package/dist/rpc/index.d.mts +0 -90
- package/dist/rpc/index.mjs +0 -248
- /package/dist/{EventTransport-CqZ8FyM_.d.mts → EventTransport-CUw5NNWe.d.mts} +0 -0
- /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-BNYKnrXF.mjs} +0 -0
- /package/dist/{applyPermissionResult-bqGpo9ML.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
- /package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-BBRVhjQN.mjs} +0 -0
- /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
- /package/dist/{elevation-B6S5csVA.d.mts → elevation-C5SwtkAn.d.mts} +0 -0
- /package/dist/{errors-BI8kEKsO.d.mts → errors-CCSsMpXE.d.mts} +0 -0
- /package/dist/{errors-CqWnSqM-.mjs → errors-D5c-5BJL.mjs} +0 -0
- /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BQ8QijNH.d.mts} +0 -0
- /package/dist/{fields-CU6FlaDV.mjs → fields-bxkeltzz.mjs} +0 -0
- /package/dist/{interface-B-pe8fhj.d.mts → interface-CSbZdv_3.d.mts} +0 -0
- /package/dist/{loadResources-Bksk8ydA.mjs → loadResources-BAzJItAJ.mjs} +0 -0
- /package/dist/{logger-CDjpjySd.mjs → logger-DLg8-Ueg.mjs} +0 -0
- /package/dist/{metrics-TuOmguhi.mjs → metrics-DuhiSEZI.mjs} +0 -0
- /package/dist/{pluralize-CWP6MB39.mjs → pluralize-A0tWEl1K.mjs} +0 -0
- /package/dist/{registry-B0Wl7uVV.mjs → registry-B3lRFBWo.mjs} +0 -0
- /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-CXtJDAZ0.mjs} +0 -0
- /package/dist/{requestContext-DYtmNpm5.mjs → requestContext-xHIKedG6.mjs} +0 -0
- /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-BkzVU8h2.d.mts} +0 -0
- /package/dist/{storage-BwGQXUpd.d.mts → storage-CVk_SEn2.d.mts} +0 -0
- /package/dist/{store-helpers-DFiZl5TL.mjs → store-helpers-ZCSMJJAX.mjs} +0 -0
- /package/dist/{tracing-xqXzWeaf.d.mts → tracing-65B51Dw3.d.mts} +0 -0
- /package/dist/{types-ZUu_h0jp.mjs → types-DV9WDfeg.mjs} +0 -0
package/dist/policies/index.mjs
DELETED
|
@@ -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 };
|
package/dist/rpc/index.d.mts
DELETED
|
@@ -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 };
|
package/dist/rpc/index.mjs
DELETED
|
@@ -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 };
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|