@classytic/arc 1.0.0

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +900 -0
  3. package/bin/arc.js +344 -0
  4. package/dist/adapters/index.d.ts +237 -0
  5. package/dist/adapters/index.js +668 -0
  6. package/dist/arcCorePlugin-DTPWXcZN.d.ts +273 -0
  7. package/dist/audit/index.d.ts +195 -0
  8. package/dist/audit/index.js +319 -0
  9. package/dist/auth/index.d.ts +47 -0
  10. package/dist/auth/index.js +174 -0
  11. package/dist/cli/commands/docs.d.ts +11 -0
  12. package/dist/cli/commands/docs.js +474 -0
  13. package/dist/cli/commands/introspect.d.ts +8 -0
  14. package/dist/cli/commands/introspect.js +338 -0
  15. package/dist/cli/index.d.ts +43 -0
  16. package/dist/cli/index.js +520 -0
  17. package/dist/createApp-pzUAkzbz.d.ts +77 -0
  18. package/dist/docs/index.d.ts +166 -0
  19. package/dist/docs/index.js +650 -0
  20. package/dist/errors-8WIxGS_6.d.ts +122 -0
  21. package/dist/events/index.d.ts +117 -0
  22. package/dist/events/index.js +89 -0
  23. package/dist/factory/index.d.ts +38 -0
  24. package/dist/factory/index.js +1664 -0
  25. package/dist/hooks/index.d.ts +4 -0
  26. package/dist/hooks/index.js +199 -0
  27. package/dist/idempotency/index.d.ts +323 -0
  28. package/dist/idempotency/index.js +500 -0
  29. package/dist/index-DkAW8BXh.d.ts +1302 -0
  30. package/dist/index.d.ts +331 -0
  31. package/dist/index.js +4734 -0
  32. package/dist/migrations/index.d.ts +185 -0
  33. package/dist/migrations/index.js +274 -0
  34. package/dist/org/index.d.ts +129 -0
  35. package/dist/org/index.js +220 -0
  36. package/dist/permissions/index.d.ts +144 -0
  37. package/dist/permissions/index.js +100 -0
  38. package/dist/plugins/index.d.ts +46 -0
  39. package/dist/plugins/index.js +1069 -0
  40. package/dist/policies/index.d.ts +398 -0
  41. package/dist/policies/index.js +196 -0
  42. package/dist/presets/index.d.ts +336 -0
  43. package/dist/presets/index.js +382 -0
  44. package/dist/presets/multiTenant.d.ts +39 -0
  45. package/dist/presets/multiTenant.js +112 -0
  46. package/dist/registry/index.d.ts +16 -0
  47. package/dist/registry/index.js +253 -0
  48. package/dist/testing/index.d.ts +618 -0
  49. package/dist/testing/index.js +48032 -0
  50. package/dist/types/index.d.ts +4 -0
  51. package/dist/types/index.js +8 -0
  52. package/dist/types-0IPhH_NR.d.ts +143 -0
  53. package/dist/types-B99TBmFV.d.ts +76 -0
  54. package/dist/utils/index.d.ts +655 -0
  55. package/dist/utils/index.js +905 -0
  56. package/package.json +227 -0
@@ -0,0 +1,398 @@
1
+ import { FastifyRequest, FastifyReply } from 'fastify';
2
+
3
+ /**
4
+ * Policy Interface
5
+ *
6
+ * Pluggable authorization interface for Arc.
7
+ * Apps implement this interface to define custom authorization strategies.
8
+ *
9
+ * @example RBAC Policy
10
+ * ```typescript
11
+ * class RBACPolicy implements PolicyEngine {
12
+ * can(user, operation, context) {
13
+ * return {
14
+ * allowed: user.roles.includes('admin'),
15
+ * reason: 'Admin role required',
16
+ * };
17
+ * }
18
+ * toMiddleware(operation) {
19
+ * return async (request, reply) => {
20
+ * const result = await this.can(request.user, operation);
21
+ * if (!result.allowed) {
22
+ * reply.code(403).send({ error: result.reason });
23
+ * }
24
+ * };
25
+ * }
26
+ * }
27
+ * ```
28
+ *
29
+ * @example ABAC (Attribute-Based) Policy
30
+ * ```typescript
31
+ * class ABACPolicy implements PolicyEngine {
32
+ * can(user, operation, context) {
33
+ * return {
34
+ * allowed: this.evaluateAttributes(user, operation, context),
35
+ * filters: { department: user.department },
36
+ * fieldMask: { exclude: ['salary', 'ssn'] },
37
+ * };
38
+ * }
39
+ * // ...
40
+ * }
41
+ * ```
42
+ */
43
+
44
+ /**
45
+ * Policy result returned by can() method
46
+ */
47
+ interface PolicyResult {
48
+ /**
49
+ * Whether the operation is allowed
50
+ */
51
+ allowed: boolean;
52
+ /**
53
+ * Human-readable reason if denied
54
+ * Returned in 403 error responses
55
+ */
56
+ reason?: string;
57
+ /**
58
+ * Query filters to apply (for list operations)
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * // Multi-tenant filter
63
+ * { organizationId: user.organizationId }
64
+ *
65
+ * // Ownership filter
66
+ * { userId: user.id }
67
+ *
68
+ * // Complex filter
69
+ * { $or: [{ public: true }, { createdBy: user.id }] }
70
+ * ```
71
+ */
72
+ filters?: Record<string, any>;
73
+ /**
74
+ * Fields to include/exclude in response
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * // Hide sensitive fields from non-admins
79
+ * { exclude: ['password', 'ssn', 'salary'] }
80
+ *
81
+ * // Only show specific fields
82
+ * { include: ['name', 'email', 'role'] }
83
+ * ```
84
+ */
85
+ fieldMask?: {
86
+ include?: string[];
87
+ exclude?: string[];
88
+ };
89
+ /**
90
+ * Additional context for downstream middleware
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * {
95
+ * auditLog: { action: 'read', resource: 'patient', userId: user.id },
96
+ * rateLimit: { tier: user.subscriptionTier },
97
+ * }
98
+ * ```
99
+ */
100
+ metadata?: Record<string, any>;
101
+ }
102
+ /**
103
+ * Policy context provided to can() method
104
+ */
105
+ interface PolicyContext {
106
+ /**
107
+ * The document being accessed (for update/delete/get)
108
+ * Populated by fetchDocument middleware
109
+ */
110
+ document?: any;
111
+ /**
112
+ * Request body (for create/update)
113
+ */
114
+ body?: any;
115
+ /**
116
+ * Request params (e.g., :id from route)
117
+ */
118
+ params?: any;
119
+ /**
120
+ * Request query parameters
121
+ */
122
+ query?: any;
123
+ /**
124
+ * Additional app-specific context
125
+ * Can include anything your policy needs to make decisions
126
+ */
127
+ [key: string]: any;
128
+ }
129
+ /**
130
+ * Policy Engine Interface
131
+ *
132
+ * Implement this interface to create your own authorization strategy.
133
+ *
134
+ * Arc provides the interface, apps provide the implementation.
135
+ * This follows the same pattern as:
136
+ * - Database drivers (interface: query(), implementation: PostgreSQL, MySQL)
137
+ * - Storage providers (interface: upload(), implementation: S3, Azure)
138
+ * - Authentication strategies (interface: verify(), implementation: JWT, OAuth)
139
+ *
140
+ * @example E-commerce RBAC + Ownership
141
+ * ```typescript
142
+ * class EcommercePolicyEngine implements PolicyEngine {
143
+ * constructor(private config: { roles: Record<string, string[]> }) {}
144
+ *
145
+ * can(user, operation, context) {
146
+ * // Check RBAC
147
+ * const allowedRoles = this.config.roles[operation] || [];
148
+ * if (!user.roles.some(r => allowedRoles.includes(r))) {
149
+ * return { allowed: false, reason: 'Insufficient permissions' };
150
+ * }
151
+ *
152
+ * // Check ownership for update/delete
153
+ * if (['update', 'delete'].includes(operation)) {
154
+ * if (context.document.userId !== user.id) {
155
+ * return { allowed: false, reason: 'Not the owner' };
156
+ * }
157
+ * }
158
+ *
159
+ * // Multi-tenant filter for list
160
+ * if (operation === 'list') {
161
+ * return {
162
+ * allowed: true,
163
+ * filters: { organizationId: user.organizationId },
164
+ * };
165
+ * }
166
+ *
167
+ * return { allowed: true };
168
+ * }
169
+ *
170
+ * toMiddleware(operation) {
171
+ * return async (request, reply) => {
172
+ * const result = await this.can(request.user, operation, {
173
+ * document: request.document,
174
+ * body: request.body,
175
+ * params: request.params,
176
+ * query: request.query,
177
+ * });
178
+ *
179
+ * if (!result.allowed) {
180
+ * reply.code(403).send({ error: result.reason });
181
+ * }
182
+ *
183
+ * // Attach filters/fieldMask to request
184
+ * request.policyResult = result;
185
+ * };
186
+ * }
187
+ * }
188
+ * ```
189
+ *
190
+ * @example HIPAA Compliance
191
+ * ```typescript
192
+ * class HIPAAPolicyEngine implements PolicyEngine {
193
+ * can(user, operation, context) {
194
+ * // Check patient consent
195
+ * // Verify user certifications
196
+ * // Check data sensitivity level
197
+ * // Create audit log entry
198
+ *
199
+ * return {
200
+ * allowed: this.checkHIPAACompliance(user, operation, context),
201
+ * reason: 'HIPAA compliance check failed',
202
+ * metadata: {
203
+ * auditLog: this.createAuditEntry(user, operation),
204
+ * },
205
+ * };
206
+ * }
207
+ *
208
+ * toMiddleware(operation) {
209
+ * // HIPAA-specific middleware with audit logging
210
+ * }
211
+ * }
212
+ * ```
213
+ */
214
+ interface PolicyEngine {
215
+ /**
216
+ * Check if user can perform operation
217
+ *
218
+ * @param user - User object from request (request.user)
219
+ * @param operation - Operation name (list, get, create, update, delete, custom)
220
+ * @param context - Additional context (document, body, params, query, etc.)
221
+ * @returns Policy result with allowed/denied and optional filters/fieldMask
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * const result = await policy.can(request.user, 'update', {
226
+ * document: existingDocument,
227
+ * body: request.body,
228
+ * });
229
+ *
230
+ * if (!result.allowed) {
231
+ * throw new Error(result.reason);
232
+ * }
233
+ * ```
234
+ */
235
+ can(user: any, operation: string, context?: PolicyContext): PolicyResult | Promise<PolicyResult>;
236
+ /**
237
+ * Generate Fastify middleware for this policy
238
+ *
239
+ * Called during route registration to create preHandler middleware.
240
+ * Middleware should:
241
+ * 1. Call can() with request context
242
+ * 2. Return 403 if denied
243
+ * 3. Attach result to request for downstream use
244
+ *
245
+ * @param operation - Operation name (list, get, create, update, delete)
246
+ * @returns Fastify preHandler middleware
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * const middleware = policy.toMiddleware('update');
251
+ * fastify.put('/products/:id', {
252
+ * preHandler: [authenticate, middleware],
253
+ * }, handler);
254
+ * ```
255
+ */
256
+ toMiddleware(operation: string): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
257
+ }
258
+ /**
259
+ * Policy factory function signature
260
+ *
261
+ * Policies are typically created via factory functions that accept configuration.
262
+ *
263
+ * @example
264
+ * ```typescript
265
+ * export function definePolicy(config: PolicyConfig): PolicyEngine {
266
+ * return new MyPolicyEngine(config);
267
+ * }
268
+ *
269
+ * // Usage
270
+ * const productPolicy = definePolicy({
271
+ * resource: 'product',
272
+ * roles: { list: ['user'], create: ['admin'] },
273
+ * ownership: { field: 'userId', operations: ['update', 'delete'] },
274
+ * });
275
+ * ```
276
+ */
277
+ type PolicyFactory<TConfig = any> = (config: TConfig) => PolicyEngine;
278
+ /**
279
+ * Extended Fastify request with policy result
280
+ */
281
+ declare module 'fastify' {
282
+ interface FastifyRequest {
283
+ policyResult?: PolicyResult;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Policy Helper Utilities
289
+ *
290
+ * Common operations for working with PolicyEngine implementations.
291
+ */
292
+
293
+ /**
294
+ * Helper to create Fastify middleware from any PolicyEngine implementation
295
+ *
296
+ * This is a convenience function that provides a standard middleware pattern.
297
+ * Most policies can use this instead of implementing toMiddleware() manually.
298
+ *
299
+ * @param policy - Policy engine instance
300
+ * @param operation - Operation name (list, get, create, update, delete)
301
+ * @returns Fastify preHandler middleware
302
+ *
303
+ * @example
304
+ * ```typescript
305
+ * class SimplePolicy implements PolicyEngine {
306
+ * can(user, operation) {
307
+ * return { allowed: user.isActive };
308
+ * }
309
+ *
310
+ * toMiddleware(operation) {
311
+ * return createPolicyMiddleware(this, operation);
312
+ * }
313
+ * }
314
+ * ```
315
+ */
316
+ declare function createPolicyMiddleware(policy: PolicyEngine, operation: string): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
317
+ /**
318
+ * Combine multiple policies with AND logic
319
+ *
320
+ * All policies must allow the operation for it to succeed.
321
+ * First denial stops evaluation and returns the denial reason.
322
+ *
323
+ * @param policies - Array of policy engines to combine
324
+ * @returns Combined policy engine
325
+ *
326
+ * @example
327
+ * ```typescript
328
+ * const combinedPolicy = combinePolicies(
329
+ * rbacPolicy, // Must have correct role
330
+ * ownershipPolicy, // Must own the resource
331
+ * auditPolicy, // Logs access for compliance
332
+ * );
333
+ *
334
+ * // All three policies must pass for the operation to succeed
335
+ * const result = await combinedPolicy.can(user, 'update', context);
336
+ * ```
337
+ *
338
+ * @example Multi-tenant + RBAC
339
+ * ```typescript
340
+ * const policy = combinePolicies(
341
+ * definePolicy({ tenant: { field: 'organizationId' } }),
342
+ * definePolicy({ roles: { update: ['admin', 'editor'] } }),
343
+ * );
344
+ * ```
345
+ */
346
+ declare function combinePolicies(...policies: PolicyEngine[]): PolicyEngine;
347
+ /**
348
+ * Combine multiple policies with OR logic
349
+ *
350
+ * At least one policy must allow the operation for it to succeed.
351
+ * If all policies deny, returns the first denial reason.
352
+ *
353
+ * @param policies - Array of policy engines to combine
354
+ * @returns Combined policy engine
355
+ *
356
+ * @example
357
+ * ```typescript
358
+ * const policy = anyPolicy(
359
+ * ownerPolicy, // User owns the resource
360
+ * adminPolicy, // OR user is admin
361
+ * publicPolicy, // OR resource is public
362
+ * );
363
+ *
364
+ * // Any one of these policies passing allows the operation
365
+ * ```
366
+ */
367
+ declare function anyPolicy(...policies: PolicyEngine[]): PolicyEngine;
368
+ /**
369
+ * Create a pass-through policy that always allows
370
+ *
371
+ * Useful for testing or for routes that don't need authorization.
372
+ *
373
+ * @example
374
+ * ```typescript
375
+ * const policy = allowAll();
376
+ * const result = await policy.can(user, 'any-operation');
377
+ * // result.allowed === true
378
+ * ```
379
+ */
380
+ declare function allowAll(): PolicyEngine;
381
+ /**
382
+ * Create a policy that always denies
383
+ *
384
+ * Useful for explicitly blocking operations or for testing.
385
+ *
386
+ * @param reason - Denial reason
387
+ *
388
+ * @example
389
+ * ```typescript
390
+ * const policy = denyAll('This resource is deprecated');
391
+ * const result = await policy.can(user, 'any-operation');
392
+ * // result.allowed === false
393
+ * // result.reason === 'This resource is deprecated'
394
+ * ```
395
+ */
396
+ declare function denyAll(reason?: string): PolicyEngine;
397
+
398
+ export { type PolicyContext, type PolicyEngine, type PolicyFactory, type PolicyResult, allowAll, anyPolicy, combinePolicies, createPolicyMiddleware, denyAll };
@@ -0,0 +1,196 @@
1
+ // src/policies/helpers.ts
2
+ function createPolicyMiddleware(policy, operation) {
3
+ return async function policyMiddleware(request, reply) {
4
+ const context = {
5
+ document: request.document,
6
+ body: request.body,
7
+ params: request.params,
8
+ query: request.query
9
+ };
10
+ const result = await policy.can(request.user, operation, context);
11
+ if (!result.allowed) {
12
+ return reply.code(403).send({
13
+ success: false,
14
+ error: "Access denied",
15
+ message: result.reason || "You do not have permission to perform this action"
16
+ });
17
+ }
18
+ request.policyResult = result;
19
+ if (result.filters && Object.keys(result.filters).length > 0) {
20
+ request.query = request.query || {};
21
+ request.query._policyFilters = result.filters;
22
+ }
23
+ if (result.fieldMask) {
24
+ request.fieldMask = result.fieldMask;
25
+ }
26
+ if (result.metadata) {
27
+ request.policyMetadata = result.metadata;
28
+ }
29
+ };
30
+ }
31
+ function combinePolicies(...policies) {
32
+ if (policies.length === 0) {
33
+ throw new Error("combinePolicies requires at least one policy");
34
+ }
35
+ if (policies.length === 1) {
36
+ return policies[0];
37
+ }
38
+ return {
39
+ async can(user, operation, context) {
40
+ const results = [];
41
+ for (const policy of policies) {
42
+ const result = await policy.can(user, operation, context);
43
+ if (!result.allowed) {
44
+ return result;
45
+ }
46
+ results.push(result);
47
+ }
48
+ const mergedResult = {
49
+ allowed: true,
50
+ filters: {},
51
+ metadata: {}
52
+ };
53
+ for (const result of results) {
54
+ if (result.filters) {
55
+ Object.assign(mergedResult.filters, result.filters);
56
+ }
57
+ }
58
+ const allExcludes = /* @__PURE__ */ new Set();
59
+ const allIncludes = [];
60
+ for (const result of results) {
61
+ if (result.fieldMask?.exclude) {
62
+ result.fieldMask.exclude.forEach((field) => allExcludes.add(field));
63
+ }
64
+ if (result.fieldMask?.include) {
65
+ allIncludes.push(new Set(result.fieldMask.include));
66
+ }
67
+ }
68
+ if (allExcludes.size > 0 || allIncludes.length > 0) {
69
+ mergedResult.fieldMask = {};
70
+ if (allExcludes.size > 0) {
71
+ mergedResult.fieldMask.exclude = Array.from(allExcludes);
72
+ }
73
+ if (allIncludes.length > 0) {
74
+ const intersection = allIncludes.reduce((acc, set) => {
75
+ return new Set([...acc].filter((x) => set.has(x)));
76
+ });
77
+ if (intersection.size > 0) {
78
+ mergedResult.fieldMask.include = Array.from(intersection);
79
+ }
80
+ }
81
+ }
82
+ for (const result of results) {
83
+ if (result.metadata) {
84
+ Object.assign(mergedResult.metadata, result.metadata);
85
+ }
86
+ }
87
+ if (Object.keys(mergedResult.filters).length === 0) {
88
+ delete mergedResult.filters;
89
+ }
90
+ if (Object.keys(mergedResult.metadata).length === 0) {
91
+ delete mergedResult.metadata;
92
+ }
93
+ return mergedResult;
94
+ },
95
+ toMiddleware(operation) {
96
+ const middlewares = policies.map((p) => p.toMiddleware(operation));
97
+ return async (request, reply) => {
98
+ for (const middleware of middlewares) {
99
+ await middleware(request, reply);
100
+ if (reply.sent) {
101
+ return;
102
+ }
103
+ }
104
+ };
105
+ }
106
+ };
107
+ }
108
+ function anyPolicy(...policies) {
109
+ if (policies.length === 0) {
110
+ throw new Error("anyPolicy requires at least one policy");
111
+ }
112
+ if (policies.length === 1) {
113
+ return policies[0];
114
+ }
115
+ return {
116
+ async can(user, operation, context) {
117
+ let firstDenial = null;
118
+ for (const policy of policies) {
119
+ const result = await policy.can(user, operation, context);
120
+ if (result.allowed) {
121
+ return result;
122
+ }
123
+ if (!firstDenial) {
124
+ firstDenial = result;
125
+ }
126
+ }
127
+ return firstDenial;
128
+ },
129
+ toMiddleware(operation) {
130
+ return async (request, reply) => {
131
+ const results = [];
132
+ for (const policy of policies) {
133
+ const result = await policy.can(
134
+ request.user,
135
+ operation,
136
+ {
137
+ document: request.document,
138
+ body: request.body,
139
+ params: request.params,
140
+ query: request.query
141
+ }
142
+ );
143
+ if (result.allowed) {
144
+ request.policyResult = result;
145
+ if (result.filters) {
146
+ request.query = request.query || {};
147
+ request.query._policyFilters = result.filters;
148
+ }
149
+ if (result.fieldMask) {
150
+ request.fieldMask = result.fieldMask;
151
+ }
152
+ if (result.metadata) {
153
+ request.policyMetadata = result.metadata;
154
+ }
155
+ return;
156
+ }
157
+ results.push(result);
158
+ }
159
+ return reply.code(403).send({
160
+ success: false,
161
+ error: "Access denied",
162
+ message: results[0]?.reason || "You do not have permission to perform this action"
163
+ });
164
+ };
165
+ }
166
+ };
167
+ }
168
+ function allowAll() {
169
+ return {
170
+ can() {
171
+ return { allowed: true };
172
+ },
173
+ toMiddleware() {
174
+ return async () => {
175
+ };
176
+ }
177
+ };
178
+ }
179
+ function denyAll(reason = "Operation not allowed") {
180
+ return {
181
+ can() {
182
+ return { allowed: false, reason };
183
+ },
184
+ toMiddleware() {
185
+ return async (request, reply) => {
186
+ return reply.code(403).send({
187
+ success: false,
188
+ error: "Access denied",
189
+ message: reason
190
+ });
191
+ };
192
+ }
193
+ };
194
+ }
195
+
196
+ export { allowAll, anyPolicy, combinePolicies, createPolicyMiddleware, denyAll };