@classytic/arc 1.1.0 → 2.1.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 +247 -794
- package/bin/arc.js +91 -52
- package/dist/EventTransport-BkUDYZEb.d.mts +99 -0
- package/dist/HookSystem-BsGV-j2l.mjs +404 -0
- package/dist/ResourceRegistry-7Ic20ZMw.mjs +249 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.mjs +3 -0
- package/dist/audit/index.d.mts +81 -0
- package/dist/audit/index.mjs +275 -0
- package/dist/audit/mongodb.d.mts +5 -0
- package/dist/audit/mongodb.mjs +3 -0
- package/dist/audited-CGdLiSlE.mjs +140 -0
- package/dist/auth/index.d.mts +188 -0
- package/dist/auth/index.mjs +1096 -0
- package/dist/auth/redis-session.d.mts +43 -0
- package/dist/auth/redis-session.mjs +75 -0
- package/dist/betterAuthOpenApi-DjWDddNc.mjs +249 -0
- package/dist/cache/index.d.mts +145 -0
- package/dist/cache/index.mjs +91 -0
- package/dist/caching-GSDJcA6-.mjs +93 -0
- package/dist/chunk-C7Uep-_p.mjs +20 -0
- package/dist/circuitBreaker-DYhWBW_D.mjs +1096 -0
- package/dist/cli/commands/describe.d.mts +18 -0
- package/dist/cli/commands/describe.mjs +238 -0
- package/dist/cli/commands/docs.d.mts +13 -0
- package/dist/cli/commands/docs.mjs +52 -0
- package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -2
- package/dist/cli/commands/generate.mjs +357 -0
- package/dist/cli/commands/{init.d.ts → init.d.mts} +11 -8
- package/dist/cli/commands/{init.js → init.mjs} +807 -617
- package/dist/cli/commands/introspect.d.mts +10 -0
- package/dist/cli/commands/introspect.mjs +75 -0
- package/dist/cli/index.d.mts +16 -0
- package/dist/cli/index.mjs +156 -0
- package/dist/constants-DdXFXQtN.mjs +84 -0
- package/dist/core/index.d.mts +5 -0
- package/dist/core/index.mjs +4 -0
- package/dist/createApp-D2D5XXaV.mjs +559 -0
- package/dist/defineResource-PXzSJ15_.mjs +2197 -0
- package/dist/discovery/index.d.mts +46 -0
- package/dist/discovery/index.mjs +109 -0
- package/dist/docs/index.d.mts +162 -0
- package/dist/docs/index.mjs +74 -0
- package/dist/elevation-DGo5shaX.d.mts +87 -0
- package/dist/elevation-DSTbVvYj.mjs +113 -0
- package/dist/errorHandler-C3GY3_ow.mjs +108 -0
- package/dist/errorHandler-CW3OOeYq.d.mts +72 -0
- package/dist/errors-DAWRdiYP.d.mts +124 -0
- package/dist/errors-DBANPbGr.mjs +211 -0
- package/dist/eventPlugin-BEOvaDqo.mjs +229 -0
- package/dist/eventPlugin-H6wDDjGO.d.mts +124 -0
- package/dist/events/index.d.mts +53 -0
- package/dist/events/index.mjs +51 -0
- package/dist/events/transports/redis-stream-entry.d.mts +2 -0
- package/dist/events/transports/redis-stream-entry.mjs +177 -0
- package/dist/events/transports/redis.d.mts +76 -0
- package/dist/events/transports/redis.mjs +124 -0
- package/dist/externalPaths-SyPF2tgK.d.mts +50 -0
- package/dist/factory/index.d.mts +63 -0
- package/dist/factory/index.mjs +3 -0
- package/dist/fastifyAdapter-C8DlE0YH.d.mts +216 -0
- package/dist/fields-Bi_AVKSo.d.mts +109 -0
- package/dist/fields-CTd_CrKr.mjs +114 -0
- package/dist/hooks/index.d.mts +4 -0
- package/dist/hooks/index.mjs +3 -0
- package/dist/idempotency/index.d.mts +96 -0
- package/dist/idempotency/index.mjs +319 -0
- package/dist/idempotency/mongodb.d.mts +2 -0
- package/dist/idempotency/mongodb.mjs +114 -0
- package/dist/idempotency/redis.d.mts +2 -0
- package/dist/idempotency/redis.mjs +103 -0
- package/dist/index.d.mts +260 -0
- package/dist/index.mjs +104 -0
- package/dist/integrations/event-gateway.d.mts +46 -0
- package/dist/integrations/event-gateway.mjs +43 -0
- package/dist/integrations/index.d.mts +5 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs.d.mts +103 -0
- package/dist/integrations/jobs.mjs +123 -0
- package/dist/integrations/streamline.d.mts +60 -0
- package/dist/integrations/streamline.mjs +125 -0
- package/dist/integrations/websocket.d.mts +82 -0
- package/dist/integrations/websocket.mjs +288 -0
- package/dist/interface-CSNjltAc.d.mts +77 -0
- package/dist/interface-DTbsvIWe.d.mts +54 -0
- package/dist/interface-e9XfSsUV.d.mts +1097 -0
- package/dist/introspectionPlugin-B3JkrjwU.mjs +53 -0
- package/dist/keys-DhqDRxv3.mjs +42 -0
- package/dist/logger-ByrvQWZO.mjs +78 -0
- package/dist/memory-B2v7KrCB.mjs +143 -0
- package/dist/migrations/index.d.mts +156 -0
- package/dist/migrations/index.mjs +260 -0
- package/dist/mongodb-ClykrfGo.d.mts +118 -0
- package/dist/mongodb-DNKEExbf.mjs +93 -0
- package/dist/mongodb-Dg8O_gvd.d.mts +71 -0
- package/dist/openapi-9nB_kiuR.mjs +525 -0
- package/dist/org/index.d.mts +68 -0
- package/dist/org/index.mjs +513 -0
- package/dist/org/types.d.mts +82 -0
- package/dist/org/types.mjs +1 -0
- package/dist/permissions/index.d.mts +278 -0
- package/dist/permissions/index.mjs +579 -0
- package/dist/plugins/index.d.mts +172 -0
- package/dist/plugins/index.mjs +522 -0
- package/dist/plugins/response-cache.d.mts +87 -0
- package/dist/plugins/response-cache.mjs +283 -0
- package/dist/plugins/tracing-entry.d.mts +2 -0
- package/dist/plugins/tracing-entry.mjs +185 -0
- package/dist/pluralize-CM-jZg7p.mjs +86 -0
- package/dist/policies/{index.d.ts → index.d.mts} +204 -170
- package/dist/policies/index.mjs +321 -0
- package/dist/presets/{index.d.ts → index.d.mts} +62 -131
- package/dist/presets/index.mjs +143 -0
- package/dist/presets/multiTenant.d.mts +24 -0
- package/dist/presets/multiTenant.mjs +113 -0
- package/dist/presets-BTeYbw7h.d.mts +57 -0
- package/dist/presets-CeFtfDR8.mjs +119 -0
- package/dist/prisma-C3iornoK.d.mts +274 -0
- package/dist/prisma-DJbMt3yf.mjs +627 -0
- package/dist/queryCachePlugin-B6R0d4av.mjs +138 -0
- package/dist/queryCachePlugin-Q6SYuHZ6.d.mts +71 -0
- package/dist/redis-UwjEp8Ea.d.mts +49 -0
- package/dist/redis-stream-CBg0upHI.d.mts +103 -0
- package/dist/registry/index.d.mts +11 -0
- package/dist/registry/index.mjs +4 -0
- package/dist/requestContext-xi6OKBL-.mjs +55 -0
- package/dist/schemaConverter-Dtg0Kt9T.mjs +98 -0
- package/dist/schemas/index.d.mts +63 -0
- package/dist/schemas/index.mjs +82 -0
- package/dist/scope/index.d.mts +21 -0
- package/dist/scope/index.mjs +65 -0
- package/dist/sessionManager-D_iEHjQl.d.mts +186 -0
- package/dist/sse-DkqQ1uxb.mjs +123 -0
- package/dist/testing/index.d.mts +907 -0
- package/dist/testing/index.mjs +1976 -0
- package/dist/tracing-8CEbhF0w.d.mts +70 -0
- package/dist/typeGuards-DwxA1t_L.mjs +9 -0
- package/dist/types/index.d.mts +946 -0
- package/dist/types/index.mjs +14 -0
- package/dist/types-B0dhNrnd.d.mts +445 -0
- package/dist/types-Beqn1Un7.mjs +38 -0
- package/dist/types-DelU6kln.mjs +25 -0
- package/dist/types-RLkFVgaw.d.mts +101 -0
- package/dist/utils/index.d.mts +747 -0
- package/dist/utils/index.mjs +6 -0
- package/package.json +194 -68
- package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
- package/dist/adapters/index.d.ts +0 -237
- package/dist/adapters/index.js +0 -668
- package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
- package/dist/audit/index.d.ts +0 -195
- package/dist/audit/index.js +0 -319
- package/dist/auth/index.d.ts +0 -47
- package/dist/auth/index.js +0 -174
- package/dist/cli/commands/docs.d.ts +0 -11
- package/dist/cli/commands/docs.js +0 -474
- package/dist/cli/commands/generate.js +0 -334
- package/dist/cli/commands/introspect.d.ts +0 -8
- package/dist/cli/commands/introspect.js +0 -338
- package/dist/cli/index.d.ts +0 -4
- package/dist/cli/index.js +0 -3269
- package/dist/core/index.d.ts +0 -220
- package/dist/core/index.js +0 -2786
- package/dist/createApp-Ce9wl8W9.d.ts +0 -77
- package/dist/docs/index.d.ts +0 -166
- package/dist/docs/index.js +0 -658
- package/dist/errors-8WIxGS_6.d.ts +0 -122
- package/dist/events/index.d.ts +0 -117
- package/dist/events/index.js +0 -89
- package/dist/factory/index.d.ts +0 -38
- package/dist/factory/index.js +0 -1652
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -199
- package/dist/idempotency/index.d.ts +0 -323
- package/dist/idempotency/index.js +0 -500
- package/dist/index-B4t03KQ0.d.ts +0 -1366
- package/dist/index.d.ts +0 -135
- package/dist/index.js +0 -4756
- package/dist/migrations/index.d.ts +0 -185
- package/dist/migrations/index.js +0 -274
- package/dist/org/index.d.ts +0 -129
- package/dist/org/index.js +0 -220
- package/dist/permissions/index.d.ts +0 -144
- package/dist/permissions/index.js +0 -103
- package/dist/plugins/index.d.ts +0 -46
- package/dist/plugins/index.js +0 -1069
- package/dist/policies/index.js +0 -196
- package/dist/presets/index.js +0 -384
- package/dist/presets/multiTenant.d.ts +0 -39
- package/dist/presets/multiTenant.js +0 -112
- package/dist/registry/index.d.ts +0 -16
- package/dist/registry/index.js +0 -253
- package/dist/testing/index.d.ts +0 -618
- package/dist/testing/index.js +0 -48020
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.js +0 -8
- package/dist/types-B99TBmFV.d.ts +0 -76
- package/dist/types-BvckRbs2.d.ts +0 -143
- package/dist/utils/index.d.ts +0 -679
- package/dist/utils/index.js +0 -931
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
//#region src/policies/PolicyInterface.ts
|
|
2
|
+
/**
|
|
3
|
+
* Create a PermissionCheck from access control statements.
|
|
4
|
+
*
|
|
5
|
+
* Maps Better Auth's statement-based access control model to Arc's
|
|
6
|
+
* PermissionCheck function, which can be used directly in resource permissions.
|
|
7
|
+
*
|
|
8
|
+
* The returned PermissionCheck:
|
|
9
|
+
* 1. Looks up the resource + action in the statements list
|
|
10
|
+
* 2. If no matching statement exists, denies access
|
|
11
|
+
* 3. If a matching statement exists and `checkPermission` is provided,
|
|
12
|
+
* calls it for dynamic verification (e.g., check org role)
|
|
13
|
+
* 4. If `checkPermission` is not provided, allows access based on static statements
|
|
14
|
+
*
|
|
15
|
+
* @example Static statements only
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { createAccessControlPolicy } from '@classytic/arc/policies';
|
|
18
|
+
*
|
|
19
|
+
* const editorPermissions = createAccessControlPolicy({
|
|
20
|
+
* statements: [
|
|
21
|
+
* { resource: 'product', action: ['create', 'update'] },
|
|
22
|
+
* { resource: 'order', action: ['read'] },
|
|
23
|
+
* ],
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // Use in resource config
|
|
27
|
+
* defineResource({
|
|
28
|
+
* name: 'product',
|
|
29
|
+
* permissions: {
|
|
30
|
+
* create: editorPermissions,
|
|
31
|
+
* update: editorPermissions,
|
|
32
|
+
* },
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @example With dynamic permission check (Better Auth org roles)
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const policy = createAccessControlPolicy({
|
|
39
|
+
* statements: [
|
|
40
|
+
* { resource: 'product', action: ['create', 'update'] },
|
|
41
|
+
* { resource: 'order', action: ['read'] },
|
|
42
|
+
* ],
|
|
43
|
+
* checkPermission: async (userId, resource, action) => {
|
|
44
|
+
* return hasOrgPermission(userId, resource, action);
|
|
45
|
+
* },
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
function createAccessControlPolicy(options) {
|
|
50
|
+
const statementMap = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const statement of options.statements) {
|
|
52
|
+
const existing = statementMap.get(statement.resource);
|
|
53
|
+
if (existing) for (const action of statement.action) existing.add(action);
|
|
54
|
+
else statementMap.set(statement.resource, new Set(statement.action));
|
|
55
|
+
}
|
|
56
|
+
const permissionCheck = async (context) => {
|
|
57
|
+
const { user, resource, action } = context;
|
|
58
|
+
const allowedActions = statementMap.get(resource);
|
|
59
|
+
if (!allowedActions || !allowedActions.has(action)) return {
|
|
60
|
+
granted: false,
|
|
61
|
+
reason: `Action '${action}' is not permitted on resource '${resource}'`
|
|
62
|
+
};
|
|
63
|
+
if (options.checkPermission) {
|
|
64
|
+
const userId = user?.id ?? user?._id;
|
|
65
|
+
if (!userId) return {
|
|
66
|
+
granted: false,
|
|
67
|
+
reason: "Authentication required"
|
|
68
|
+
};
|
|
69
|
+
if (!await options.checkPermission(String(userId), resource, action)) return {
|
|
70
|
+
granted: false,
|
|
71
|
+
reason: `User does not have '${action}' permission on '${resource}'`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return { granted: true };
|
|
75
|
+
};
|
|
76
|
+
return permissionCheck;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/policies/helpers.ts
|
|
81
|
+
/**
|
|
82
|
+
* Helper to create Fastify middleware from any PolicyEngine implementation
|
|
83
|
+
*
|
|
84
|
+
* This is a convenience function that provides a standard middleware pattern.
|
|
85
|
+
* Most policies can use this instead of implementing toMiddleware() manually.
|
|
86
|
+
*
|
|
87
|
+
* @param policy - Policy engine instance
|
|
88
|
+
* @param operation - Operation name (list, get, create, update, delete)
|
|
89
|
+
* @returns Fastify preHandler middleware
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* class SimplePolicy implements PolicyEngine {
|
|
94
|
+
* can(user, operation) {
|
|
95
|
+
* return { allowed: user.isActive };
|
|
96
|
+
* }
|
|
97
|
+
*
|
|
98
|
+
* toMiddleware(operation) {
|
|
99
|
+
* return createPolicyMiddleware(this, operation);
|
|
100
|
+
* }
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
function createPolicyMiddleware(policy, operation) {
|
|
105
|
+
return async function policyMiddleware(request, reply) {
|
|
106
|
+
const context = {
|
|
107
|
+
document: request.document,
|
|
108
|
+
body: request.body,
|
|
109
|
+
params: request.params,
|
|
110
|
+
query: request.query
|
|
111
|
+
};
|
|
112
|
+
const result = await policy.can(request.user, operation, context);
|
|
113
|
+
if (!result.allowed) return reply.code(403).send({
|
|
114
|
+
success: false,
|
|
115
|
+
error: "Access denied",
|
|
116
|
+
message: result.reason || "You do not have permission to perform this action"
|
|
117
|
+
});
|
|
118
|
+
request.policyResult = result;
|
|
119
|
+
if (result.filters && Object.keys(result.filters).length > 0) request._policyFilters = result.filters;
|
|
120
|
+
if (result.fieldMask) request.fieldMask = result.fieldMask;
|
|
121
|
+
if (result.metadata) request.policyMetadata = result.metadata;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Combine multiple policies with AND logic
|
|
126
|
+
*
|
|
127
|
+
* All policies must allow the operation for it to succeed.
|
|
128
|
+
* First denial stops evaluation and returns the denial reason.
|
|
129
|
+
*
|
|
130
|
+
* @param policies - Array of policy engines to combine
|
|
131
|
+
* @returns Combined policy engine
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* const combinedPolicy = combinePolicies(
|
|
136
|
+
* rbacPolicy, // Must have correct role
|
|
137
|
+
* ownershipPolicy, // Must own the resource
|
|
138
|
+
* auditPolicy, // Logs access for compliance
|
|
139
|
+
* );
|
|
140
|
+
*
|
|
141
|
+
* // All three policies must pass for the operation to succeed
|
|
142
|
+
* const result = await combinedPolicy.can(user, 'update', context);
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @example Multi-tenant + RBAC
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const policy = combinePolicies(
|
|
148
|
+
* definePolicy({ tenant: { field: 'organizationId' } }),
|
|
149
|
+
* definePolicy({ roles: { update: ['admin', 'editor'] } }),
|
|
150
|
+
* );
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
function combinePolicies(...policies) {
|
|
154
|
+
if (policies.length === 0) throw new Error("combinePolicies requires at least one policy");
|
|
155
|
+
if (policies.length === 1) return policies[0];
|
|
156
|
+
return {
|
|
157
|
+
async can(user, operation, context) {
|
|
158
|
+
const results = [];
|
|
159
|
+
for (const policy of policies) {
|
|
160
|
+
const result = await policy.can(user, operation, context);
|
|
161
|
+
if (!result.allowed) return result;
|
|
162
|
+
results.push(result);
|
|
163
|
+
}
|
|
164
|
+
const mergedResult = {
|
|
165
|
+
allowed: true,
|
|
166
|
+
filters: {},
|
|
167
|
+
metadata: {}
|
|
168
|
+
};
|
|
169
|
+
for (const result of results) if (result.filters) Object.assign(mergedResult.filters, result.filters);
|
|
170
|
+
const allExcludes = /* @__PURE__ */ new Set();
|
|
171
|
+
const allIncludes = [];
|
|
172
|
+
for (const result of results) {
|
|
173
|
+
if (result.fieldMask?.exclude) result.fieldMask.exclude.forEach((field) => allExcludes.add(field));
|
|
174
|
+
if (result.fieldMask?.include) allIncludes.push(new Set(result.fieldMask.include));
|
|
175
|
+
}
|
|
176
|
+
if (allExcludes.size > 0 || allIncludes.length > 0) {
|
|
177
|
+
mergedResult.fieldMask = {};
|
|
178
|
+
if (allExcludes.size > 0) mergedResult.fieldMask.exclude = Array.from(allExcludes);
|
|
179
|
+
if (allIncludes.length > 0) {
|
|
180
|
+
const intersection = allIncludes.reduce((acc, set) => {
|
|
181
|
+
return new Set([...acc].filter((x) => set.has(x)));
|
|
182
|
+
});
|
|
183
|
+
if (intersection.size > 0) mergedResult.fieldMask.include = Array.from(intersection);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const result of results) if (result.metadata) Object.assign(mergedResult.metadata, result.metadata);
|
|
187
|
+
if (Object.keys(mergedResult.filters).length === 0) delete mergedResult.filters;
|
|
188
|
+
if (Object.keys(mergedResult.metadata).length === 0) delete mergedResult.metadata;
|
|
189
|
+
return mergedResult;
|
|
190
|
+
},
|
|
191
|
+
toMiddleware(operation) {
|
|
192
|
+
const middlewares = policies.map((p) => p.toMiddleware(operation));
|
|
193
|
+
return async (request, reply) => {
|
|
194
|
+
for (const middleware of middlewares) {
|
|
195
|
+
await middleware(request, reply);
|
|
196
|
+
if (reply.sent) return;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Combine multiple policies with OR logic
|
|
204
|
+
*
|
|
205
|
+
* At least one policy must allow the operation for it to succeed.
|
|
206
|
+
* If all policies deny, returns the first denial reason.
|
|
207
|
+
*
|
|
208
|
+
* @param policies - Array of policy engines to combine
|
|
209
|
+
* @returns Combined policy engine
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* const policy = anyPolicy(
|
|
214
|
+
* ownerPolicy, // User owns the resource
|
|
215
|
+
* adminPolicy, // OR user is admin
|
|
216
|
+
* publicPolicy, // OR resource is public
|
|
217
|
+
* );
|
|
218
|
+
*
|
|
219
|
+
* // Any one of these policies passing allows the operation
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
function anyPolicy(...policies) {
|
|
223
|
+
if (policies.length === 0) throw new Error("anyPolicy requires at least one policy");
|
|
224
|
+
if (policies.length === 1) return policies[0];
|
|
225
|
+
return {
|
|
226
|
+
async can(user, operation, context) {
|
|
227
|
+
let firstDenial = null;
|
|
228
|
+
for (const policy of policies) {
|
|
229
|
+
const result = await policy.can(user, operation, context);
|
|
230
|
+
if (result.allowed) return result;
|
|
231
|
+
if (!firstDenial) firstDenial = result;
|
|
232
|
+
}
|
|
233
|
+
return firstDenial;
|
|
234
|
+
},
|
|
235
|
+
toMiddleware(operation) {
|
|
236
|
+
return async (request, reply) => {
|
|
237
|
+
const results = [];
|
|
238
|
+
for (const policy of policies) {
|
|
239
|
+
const result = await policy.can(request.user, operation, {
|
|
240
|
+
document: request.document,
|
|
241
|
+
body: request.body,
|
|
242
|
+
params: request.params,
|
|
243
|
+
query: request.query
|
|
244
|
+
});
|
|
245
|
+
if (result.allowed) {
|
|
246
|
+
request.policyResult = result;
|
|
247
|
+
if (result.filters) request._policyFilters = result.filters;
|
|
248
|
+
if (result.fieldMask) request.fieldMask = result.fieldMask;
|
|
249
|
+
if (result.metadata) request.policyMetadata = result.metadata;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
results.push(result);
|
|
253
|
+
}
|
|
254
|
+
return reply.code(403).send({
|
|
255
|
+
success: false,
|
|
256
|
+
error: "Access denied",
|
|
257
|
+
message: results[0]?.reason || "You do not have permission to perform this action"
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Create a pass-through policy that always allows
|
|
265
|
+
*
|
|
266
|
+
* Useful for testing or for routes that don't need authorization.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```typescript
|
|
270
|
+
* const policy = allowAll();
|
|
271
|
+
* const result = await policy.can(user, 'any-operation');
|
|
272
|
+
* // result.allowed === true
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
function allowAll() {
|
|
276
|
+
return {
|
|
277
|
+
can() {
|
|
278
|
+
return { allowed: true };
|
|
279
|
+
},
|
|
280
|
+
toMiddleware() {
|
|
281
|
+
return async () => {};
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Create a policy that always denies
|
|
287
|
+
*
|
|
288
|
+
* Useful for explicitly blocking operations or for testing.
|
|
289
|
+
*
|
|
290
|
+
* @param reason - Denial reason
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```typescript
|
|
294
|
+
* const policy = denyAll('This resource is deprecated');
|
|
295
|
+
* const result = await policy.can(user, 'any-operation');
|
|
296
|
+
* // result.allowed === false
|
|
297
|
+
* // result.reason === 'This resource is deprecated'
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
300
|
+
function denyAll(reason = "Operation not allowed") {
|
|
301
|
+
return {
|
|
302
|
+
can() {
|
|
303
|
+
return {
|
|
304
|
+
allowed: false,
|
|
305
|
+
reason
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
toMiddleware() {
|
|
309
|
+
return async (request, reply) => {
|
|
310
|
+
return reply.code(403).send({
|
|
311
|
+
success: false,
|
|
312
|
+
error: "Access denied",
|
|
313
|
+
message: reason
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
//#endregion
|
|
321
|
+
export { allowAll, anyPolicy, combinePolicies, createAccessControlPolicy, createPolicyMiddleware, denyAll };
|
|
@@ -1,115 +1,43 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import '../types-B99TBmFV.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Soft Delete Preset
|
|
10
|
-
*
|
|
11
|
-
* Adds routes for listing deleted items and restoring them.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
interface SoftDeleteOptions {
|
|
15
|
-
deletedField?: string;
|
|
16
|
-
}
|
|
17
|
-
declare function softDeletePreset(options?: SoftDeleteOptions): PresetResult;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Slug Lookup Preset
|
|
21
|
-
*
|
|
22
|
-
* Adds a route to get resource by slug.
|
|
23
|
-
*/
|
|
1
|
+
import "../elevation-DGo5shaX.mjs";
|
|
2
|
+
import { b as IControllerResponse, k as PaginatedResult, x as IRequestContext } from "../interface-e9XfSsUV.mjs";
|
|
3
|
+
import "../types-RLkFVgaw.mjs";
|
|
4
|
+
import { AnyRecord, PresetResult, ResourceConfig } from "../types/index.mjs";
|
|
5
|
+
import multiTenantPreset, { MultiTenantOptions } from "./multiTenant.mjs";
|
|
24
6
|
|
|
7
|
+
//#region src/presets/softDelete.d.ts
|
|
8
|
+
declare function softDeletePreset(): PresetResult;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/presets/slugLookup.d.ts
|
|
25
11
|
interface SlugLookupOptions {
|
|
26
|
-
|
|
12
|
+
slugField?: string;
|
|
27
13
|
}
|
|
28
14
|
declare function slugLookupPreset(options?: SlugLookupOptions): PresetResult;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* Owned By User Preset
|
|
32
|
-
*
|
|
33
|
-
* Adds ownership validation for update/delete operations.
|
|
34
|
-
*
|
|
35
|
-
* BEHAVIOR:
|
|
36
|
-
* - On update/remove, sets _ownershipCheck on request
|
|
37
|
-
* - BaseController enforces ownership before mutation
|
|
38
|
-
* - Users can only modify resources where ownerField matches their ID
|
|
39
|
-
*
|
|
40
|
-
* BYPASS:
|
|
41
|
-
* - Users with bypassRoles (default: ['admin', 'superadmin']) skip check
|
|
42
|
-
* - Resources without the ownerField are not checked
|
|
43
|
-
*
|
|
44
|
-
* @example
|
|
45
|
-
* defineResource({
|
|
46
|
-
* name: 'post',
|
|
47
|
-
* presets: [{ name: 'ownedByUser', ownerField: 'authorId' }],
|
|
48
|
-
* });
|
|
49
|
-
*
|
|
50
|
-
* // User A cannot update/delete User B's posts
|
|
51
|
-
* // Admins can modify any post
|
|
52
|
-
*/
|
|
53
|
-
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/presets/ownedByUser.d.ts
|
|
54
17
|
interface OwnedByUserOptions {
|
|
55
|
-
|
|
56
|
-
bypassRoles?: string[];
|
|
18
|
+
ownerField?: string;
|
|
57
19
|
}
|
|
58
20
|
declare function ownedByUserPreset(options?: OwnedByUserOptions): PresetResult;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* Tree Preset
|
|
62
|
-
*
|
|
63
|
-
* Adds routes for hierarchical tree structures.
|
|
64
|
-
*/
|
|
65
|
-
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/presets/tree.d.ts
|
|
66
23
|
interface TreeOptions {
|
|
67
|
-
|
|
24
|
+
parentField?: string;
|
|
68
25
|
}
|
|
69
26
|
declare function treePreset(options?: TreeOptions): PresetResult;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
* Audited Preset
|
|
73
|
-
*
|
|
74
|
-
* Adds createdBy/updatedBy tracking to resources.
|
|
75
|
-
* Works with the audit plugin for full change tracking.
|
|
76
|
-
*
|
|
77
|
-
* @example
|
|
78
|
-
* defineResource({
|
|
79
|
-
* name: 'product',
|
|
80
|
-
* presets: ['audited'],
|
|
81
|
-
* // Fields createdBy, updatedBy auto-populated from user context
|
|
82
|
-
* });
|
|
83
|
-
*/
|
|
84
|
-
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/presets/audited.d.ts
|
|
85
29
|
interface AuditedPresetOptions {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
30
|
+
/** Field name for creator (default: 'createdBy') */
|
|
31
|
+
createdByField?: string;
|
|
32
|
+
/** Field name for updater (default: 'updatedBy') */
|
|
33
|
+
updatedByField?: string;
|
|
90
34
|
}
|
|
91
35
|
/**
|
|
92
36
|
* Audited preset - adds createdBy/updatedBy tracking
|
|
93
37
|
*/
|
|
94
38
|
declare function auditedPreset(options?: AuditedPresetOptions): PresetResult;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
* Preset Type Interfaces
|
|
98
|
-
*
|
|
99
|
-
* TypeScript interfaces that document the controller methods required by each preset.
|
|
100
|
-
* These interfaces help with type safety when using presets.
|
|
101
|
-
*
|
|
102
|
-
* @example Using with custom controllers
|
|
103
|
-
* ```typescript
|
|
104
|
-
* import { BaseController } from '@classytic/arc';
|
|
105
|
-
* import type { ISoftDeleteController } from '@classytic/arc/presets';
|
|
106
|
-
*
|
|
107
|
-
* class ProductController extends BaseController<Product> implements ISoftDeleteController {
|
|
108
|
-
* // TypeScript now ensures you have getDeleted() and restore() methods
|
|
109
|
-
* }
|
|
110
|
-
* ```
|
|
111
|
-
*/
|
|
112
|
-
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/presets/types.d.ts
|
|
113
41
|
/**
|
|
114
42
|
* Soft Delete Preset Interface
|
|
115
43
|
*
|
|
@@ -138,16 +66,16 @@ declare function auditedPreset(options?: AuditedPresetOptions): PresetResult;
|
|
|
138
66
|
* ```
|
|
139
67
|
*/
|
|
140
68
|
interface ISoftDeleteController<TDoc = unknown> {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Get all soft-deleted items
|
|
71
|
+
* Called by: GET /deleted
|
|
72
|
+
*/
|
|
73
|
+
getDeleted(req: IRequestContext): Promise<IControllerResponse<PaginatedResult<TDoc>>>;
|
|
74
|
+
/**
|
|
75
|
+
* Restore a soft-deleted item by ID
|
|
76
|
+
* Called by: POST /:id/restore
|
|
77
|
+
*/
|
|
78
|
+
restore(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
151
79
|
}
|
|
152
80
|
/**
|
|
153
81
|
* Slug Lookup Preset Interface
|
|
@@ -175,11 +103,11 @@ interface ISoftDeleteController<TDoc = unknown> {
|
|
|
175
103
|
* ```
|
|
176
104
|
*/
|
|
177
105
|
interface ISlugLookupController<TDoc = unknown> {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Get a resource by its slug
|
|
108
|
+
* Called by: GET /slug/:slug
|
|
109
|
+
*/
|
|
110
|
+
getBySlug(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
183
111
|
}
|
|
184
112
|
/**
|
|
185
113
|
* Tree Preset Interface
|
|
@@ -209,16 +137,16 @@ interface ISlugLookupController<TDoc = unknown> {
|
|
|
209
137
|
* ```
|
|
210
138
|
*/
|
|
211
139
|
interface ITreeController<TDoc = unknown> {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Get the full hierarchical tree
|
|
142
|
+
* Called by: GET /tree
|
|
143
|
+
*/
|
|
144
|
+
getTree(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
|
|
145
|
+
/**
|
|
146
|
+
* Get direct children of a parent node
|
|
147
|
+
* Called by: GET /:parent/children
|
|
148
|
+
*/
|
|
149
|
+
getChildren(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
|
|
222
150
|
}
|
|
223
151
|
/**
|
|
224
152
|
* Owned By User Preset
|
|
@@ -301,36 +229,39 @@ type IAuditedPreset = never;
|
|
|
301
229
|
* ```
|
|
302
230
|
*/
|
|
303
231
|
type IPresetController<TDoc = unknown, TPresets extends 'softDelete' | 'slugLookup' | 'tree' | never = never> = TPresets extends 'softDelete' ? ISoftDeleteController<TDoc> : TPresets extends 'slugLookup' ? ISlugLookupController<TDoc> : TPresets extends 'tree' ? ITreeController<TDoc> : unknown;
|
|
304
|
-
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/presets/index.d.ts
|
|
305
234
|
/**
|
|
306
235
|
* Convenience alias for multiTenantPreset with public list/get routes
|
|
307
236
|
* Equivalent to: multiTenantPreset({ allowPublic: ['list', 'get'] })
|
|
308
237
|
*/
|
|
309
238
|
declare const flexibleMultiTenantPreset: (options?: Omit<MultiTenantOptions, "allowPublic">) => PresetResult;
|
|
310
|
-
|
|
311
239
|
type PresetFactory = (options?: AnyRecord) => PresetResult;
|
|
312
240
|
/**
|
|
313
241
|
* Get preset by name with options
|
|
314
242
|
*/
|
|
315
243
|
declare function getPreset(nameOrConfig: string | {
|
|
316
|
-
|
|
317
|
-
|
|
244
|
+
name: string;
|
|
245
|
+
[key: string]: unknown;
|
|
318
246
|
}): PresetResult;
|
|
319
247
|
/**
|
|
320
248
|
* Register a custom preset
|
|
321
249
|
*/
|
|
322
|
-
declare function registerPreset(name: string, factory: PresetFactory
|
|
250
|
+
declare function registerPreset(name: string, factory: PresetFactory, options?: {
|
|
251
|
+
override?: boolean;
|
|
252
|
+
}): void;
|
|
323
253
|
/**
|
|
324
254
|
* Get all available preset names
|
|
325
255
|
*/
|
|
326
256
|
declare function getAvailablePresets(): string[];
|
|
327
257
|
type PresetInput = string | PresetResult | {
|
|
328
|
-
|
|
329
|
-
|
|
258
|
+
name: string;
|
|
259
|
+
[key: string]: unknown;
|
|
330
260
|
};
|
|
331
261
|
/**
|
|
332
|
-
* Apply presets to resource config
|
|
262
|
+
* Apply presets to resource config.
|
|
263
|
+
* Validates preset combinations for conflicts before merging.
|
|
333
264
|
*/
|
|
334
265
|
declare function applyPresets<TDoc = AnyRecord>(config: ResourceConfig<TDoc>, presets?: PresetInput[]): ResourceConfig<TDoc>;
|
|
335
|
-
|
|
336
|
-
export { type AuditedPresetOptions, type IAuditedPreset, type IMultiTenantPreset, type IOwnedByUserPreset, type IPresetController, type ISlugLookupController, type ISoftDeleteController, type ITreeController, MultiTenantOptions, type OwnedByUserOptions, type SlugLookupOptions, type
|
|
266
|
+
//#endregion
|
|
267
|
+
export { type AuditedPresetOptions, type IAuditedPreset, type IMultiTenantPreset, type IOwnedByUserPreset, type IPresetController, type ISlugLookupController, type ISoftDeleteController, type ITreeController, type MultiTenantOptions, type OwnedByUserOptions, type SlugLookupOptions, type TreeOptions, applyPresets, auditedPreset, flexibleMultiTenantPreset, getAvailablePresets, getPreset, multiTenantPreset, ownedByUserPreset, registerPreset, slugLookupPreset, softDeletePreset, treePreset };
|