@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,220 @@
1
+ import fp from 'fastify-plugin';
2
+
3
+ // src/org/orgScopePlugin.ts
4
+ function createOrgContext(orgId, options = {}) {
5
+ return {
6
+ organizationId: orgId,
7
+ orgScope: options.orgScope ?? "explicit",
8
+ orgRoles: options.orgRoles ?? [],
9
+ ...options
10
+ };
11
+ }
12
+ var orgScopePlugin = async (fastify, opts = {}) => {
13
+ const {
14
+ header = "x-organization-id",
15
+ bypassRoles = ["superadmin"],
16
+ userOrgsPath = "organizations",
17
+ validateMembership
18
+ } = opts;
19
+ if (!fastify.hasRequestDecorator("context")) {
20
+ fastify.decorateRequest("context", void 0);
21
+ }
22
+ if (!fastify.hasRequestDecorator("organizationId")) {
23
+ fastify.decorateRequest("organizationId", void 0);
24
+ }
25
+ fastify.decorate("organizationScoped", function organizationScoped(options = {}) {
26
+ const { required = true } = options;
27
+ return async function organizationScopePreHandler(request, reply) {
28
+ const user = request.user;
29
+ const userWithRoles = user;
30
+ const roles = Array.isArray(userWithRoles?.roles) ? userWithRoles.roles : userWithRoles?.roles ? [String(userWithRoles.roles)] : [];
31
+ const orgIdFromHeader = request.headers[header]?.toString().trim();
32
+ const isAuthenticated = !!user;
33
+ const isSuperadmin = bypassRoles.some((role) => roles.includes(role));
34
+ const req = request;
35
+ req.context = req.context ?? {};
36
+ if (!orgIdFromHeader) {
37
+ if (isSuperadmin) {
38
+ request.log?.debug?.({ msg: "Superadmin - no org filter required" });
39
+ req.context.orgScope = "global";
40
+ return;
41
+ }
42
+ if (required) {
43
+ reply.code(403).send({
44
+ success: false,
45
+ error: "Organization context required",
46
+ code: "ORG_HEADER_REQUIRED",
47
+ message: "x-organization-id header required"
48
+ });
49
+ return;
50
+ }
51
+ request.log?.debug?.({ msg: "No org filter - showing all public data" });
52
+ req.context.orgScope = "public";
53
+ return;
54
+ }
55
+ if (!isAuthenticated) {
56
+ request.log?.warn?.({
57
+ msg: "Organization filtering requires authentication",
58
+ headerOrgId: orgIdFromHeader
59
+ });
60
+ reply.code(401).send({
61
+ success: false,
62
+ error: "Authentication required to filter by organization",
63
+ code: "AUTH_REQUIRED_FOR_ORG"
64
+ });
65
+ return;
66
+ }
67
+ if (isSuperadmin) {
68
+ request.log?.debug?.({
69
+ msg: "Superadmin accessing organization",
70
+ orgId: orgIdFromHeader
71
+ });
72
+ req.organizationId = orgIdFromHeader;
73
+ req.context.organizationId = orgIdFromHeader;
74
+ req.context.orgScope = "bypass";
75
+ req.context.bypassReason = "superadmin";
76
+ return;
77
+ }
78
+ const userOrgs = user?.[userOrgsPath] ?? [];
79
+ let hasAccess = false;
80
+ if (validateMembership) {
81
+ hasAccess = await validateMembership(user, orgIdFromHeader);
82
+ } else {
83
+ hasAccess = userOrgs.some((org) => {
84
+ const memberOrgId = org.organizationId?.toString() ?? String(org);
85
+ return memberOrgId === orgIdFromHeader;
86
+ });
87
+ }
88
+ if (!hasAccess) {
89
+ request.log?.warn?.({
90
+ msg: "Access denied - user not member of organization",
91
+ userId: user._id ?? user.id,
92
+ requestedOrgId: orgIdFromHeader,
93
+ userOrgIds: userOrgs.map((o) => o.organizationId?.toString() ?? String(o))
94
+ });
95
+ reply.code(403).send({
96
+ success: false,
97
+ error: "No access to this organization",
98
+ code: "ORG_ACCESS_DENIED"
99
+ });
100
+ return;
101
+ }
102
+ req.organizationId = orgIdFromHeader;
103
+ req.context.organizationId = orgIdFromHeader;
104
+ req.context.orgScope = "member";
105
+ const orgMembership = userOrgs.find((o) => {
106
+ const memberOrgId = o.organizationId?.toString() ?? String(o);
107
+ return memberOrgId === orgIdFromHeader;
108
+ });
109
+ req.context.orgRoles = orgMembership?.roles ?? [];
110
+ request.log?.debug?.({
111
+ msg: "Organization context set",
112
+ orgId: orgIdFromHeader,
113
+ userId: user._id ?? user.id,
114
+ orgRoles: req.context.orgRoles
115
+ });
116
+ };
117
+ });
118
+ fastify.decorate("createOrgContext", createOrgContext);
119
+ fastify.log?.info?.("Organization scope plugin registered");
120
+ };
121
+ var orgScopePlugin_default = fp(orgScopePlugin, {
122
+ name: "arc-org-scope",
123
+ fastify: "5.x"
124
+ });
125
+
126
+ // src/org/orgGuard.ts
127
+ function orgGuard(options = {}) {
128
+ const {
129
+ requireOrgContext = true,
130
+ roles = [],
131
+ allowGlobal = false
132
+ } = options;
133
+ return async function orgGuardMiddleware(request, reply) {
134
+ const context = request.context ?? {};
135
+ const user = request.user;
136
+ const userWithRoles = user;
137
+ if (allowGlobal && userWithRoles?.roles?.includes("superadmin")) {
138
+ return;
139
+ }
140
+ if (requireOrgContext && !context.organizationId) {
141
+ reply.code(403).send({
142
+ success: false,
143
+ error: "Organization context required",
144
+ code: "ORG_CONTEXT_REQUIRED",
145
+ message: "This endpoint requires an organization context. Please specify organization via x-organization-id header."
146
+ });
147
+ return;
148
+ }
149
+ if (roles.length > 0 && context.organizationId) {
150
+ const contextWithRoles = context;
151
+ const userOrgRoles = contextWithRoles.orgRoles ?? [];
152
+ const hasRequiredRole = roles.some((role) => userOrgRoles.includes(role));
153
+ if (!hasRequiredRole && !userWithRoles?.roles?.includes("superadmin")) {
154
+ reply.code(403).send({
155
+ success: false,
156
+ error: "Insufficient organization permissions",
157
+ code: "ORG_ROLE_REQUIRED",
158
+ message: `This action requires one of these organization roles: ${roles.join(", ")}`,
159
+ required: roles,
160
+ current: userOrgRoles
161
+ });
162
+ return;
163
+ }
164
+ }
165
+ };
166
+ }
167
+ function requireOrg() {
168
+ return orgGuard({ requireOrgContext: true });
169
+ }
170
+ function requireOrgRole(...roles) {
171
+ return orgGuard({ requireOrgContext: true, roles });
172
+ }
173
+
174
+ // src/org/orgMembership.ts
175
+ async function orgMembershipCheck(user, orgId, options = {}) {
176
+ const {
177
+ userOrgsPath = "organizations",
178
+ bypassRoles = ["superadmin"],
179
+ validateFromDb
180
+ } = options;
181
+ if (!user || !orgId) return false;
182
+ const userWithRoles = user;
183
+ const userRoles = userWithRoles.roles ?? [];
184
+ if (bypassRoles.some((role) => userRoles.includes(role))) {
185
+ return true;
186
+ }
187
+ const userOrgs = user[userOrgsPath] ?? [];
188
+ const isMemberFromUser = userOrgs.some((o) => {
189
+ const memberOrgId = o.organizationId?.toString() ?? String(o);
190
+ return memberOrgId === orgId.toString();
191
+ });
192
+ if (isMemberFromUser) return true;
193
+ if (validateFromDb) {
194
+ const userId = (user._id ?? user.id)?.toString();
195
+ if (userId) {
196
+ return validateFromDb(userId, orgId);
197
+ }
198
+ }
199
+ return false;
200
+ }
201
+ function getOrgRoles(user, orgId, options = {}) {
202
+ const { userOrgsPath = "organizations" } = options;
203
+ if (!user || !orgId) return [];
204
+ const userOrgs = user[userOrgsPath] ?? [];
205
+ const membership = userOrgs.find((o) => {
206
+ const memberOrgId = o.organizationId?.toString() ?? String(o);
207
+ return memberOrgId === orgId.toString();
208
+ });
209
+ const membershipRoles = membership;
210
+ return membershipRoles?.roles ?? [];
211
+ }
212
+ function hasOrgRole(user, orgId, roles, options = {}) {
213
+ const userOrgRoles = getOrgRoles(user, orgId, options);
214
+ const requiredRoles = Array.isArray(roles) ? roles : [roles];
215
+ const userWithRoles = user;
216
+ if (userWithRoles?.roles?.includes("superadmin")) return true;
217
+ return requiredRoles.some((role) => userOrgRoles.includes(role));
218
+ }
219
+
220
+ export { createOrgContext, getOrgRoles, hasOrgRole, orgGuard, orgMembershipCheck, orgScopePlugin_default as orgScopePlugin, orgScopePlugin as orgScopePluginFn, requireOrg, requireOrgRole };
@@ -0,0 +1,144 @@
1
+ import { P as PermissionCheck, a as PermissionContext } from '../types-B99TBmFV.js';
2
+ export { b as PermissionResult, U as UserBase } from '../types-B99TBmFV.js';
3
+ import 'fastify';
4
+
5
+ /**
6
+ * Permission System
7
+ *
8
+ * Clean, function-based permission system.
9
+ * PermissionCheck is THE ONLY way to define permissions.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { allowPublic, requireAuth, requireRoles } from '@classytic/arc/permissions';
14
+ *
15
+ * defineResource({
16
+ * permissions: {
17
+ * list: allowPublic(),
18
+ * get: allowPublic(),
19
+ * create: requireAuth(),
20
+ * update: requireRoles(['admin', 'editor']),
21
+ * delete: requireRoles(['admin']),
22
+ * }
23
+ * });
24
+ * ```
25
+ */
26
+
27
+ /**
28
+ * Allow public access (no authentication required)
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * permissions: {
33
+ * list: allowPublic(),
34
+ * get: allowPublic(),
35
+ * }
36
+ * ```
37
+ */
38
+ declare function allowPublic(): PermissionCheck;
39
+ /**
40
+ * Require authentication (any authenticated user)
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * permissions: {
45
+ * create: requireAuth(),
46
+ * update: requireAuth(),
47
+ * }
48
+ * ```
49
+ */
50
+ declare function requireAuth(): PermissionCheck;
51
+ /**
52
+ * Require specific roles
53
+ *
54
+ * @param roles - Required roles (user needs at least one)
55
+ * @param options - Optional bypass roles
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * permissions: {
60
+ * create: requireRoles(['admin', 'editor']),
61
+ * delete: requireRoles(['admin']),
62
+ * }
63
+ *
64
+ * // With bypass roles
65
+ * permissions: {
66
+ * update: requireRoles(['owner'], { bypassRoles: ['admin', 'superadmin'] }),
67
+ * }
68
+ * ```
69
+ */
70
+ declare function requireRoles(roles: readonly string[], options?: {
71
+ bypassRoles?: readonly string[];
72
+ }): PermissionCheck;
73
+ /**
74
+ * Require resource ownership
75
+ *
76
+ * Returns filters to scope queries to user's owned resources.
77
+ *
78
+ * @param ownerField - Field containing owner ID (default: 'userId')
79
+ * @param options - Optional bypass roles
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * permissions: {
84
+ * update: requireOwnership('userId'),
85
+ * delete: requireOwnership('createdBy', { bypassRoles: ['admin'] }),
86
+ * }
87
+ * ```
88
+ */
89
+ declare function requireOwnership(ownerField?: string, options?: {
90
+ bypassRoles?: readonly string[];
91
+ }): PermissionCheck;
92
+ /**
93
+ * Combine multiple checks - ALL must pass (AND logic)
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * permissions: {
98
+ * update: allOf(
99
+ * requireAuth(),
100
+ * requireRoles(['editor']),
101
+ * requireOwnership('createdBy')
102
+ * ),
103
+ * }
104
+ * ```
105
+ */
106
+ declare function allOf(...checks: PermissionCheck[]): PermissionCheck;
107
+ /**
108
+ * Combine multiple checks - ANY must pass (OR logic)
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * permissions: {
113
+ * update: anyOf(
114
+ * requireRoles(['admin']),
115
+ * requireOwnership('createdBy')
116
+ * ),
117
+ * }
118
+ * ```
119
+ */
120
+ declare function anyOf(...checks: PermissionCheck[]): PermissionCheck;
121
+ /**
122
+ * Deny all access
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * permissions: {
127
+ * delete: denyAll('Deletion not allowed'),
128
+ * }
129
+ * ```
130
+ */
131
+ declare function denyAll(reason?: string): PermissionCheck;
132
+ /**
133
+ * Dynamic permission based on context
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * permissions: {
138
+ * update: when((ctx) => ctx.data?.status === 'draft'),
139
+ * }
140
+ * ```
141
+ */
142
+ declare function when(condition: (ctx: PermissionContext) => boolean | Promise<boolean>): PermissionCheck;
143
+
144
+ export { PermissionCheck, PermissionContext, allOf, allowPublic, anyOf, denyAll, requireAuth, requireOwnership, requireRoles, when };
@@ -0,0 +1,100 @@
1
+ // src/permissions/index.ts
2
+ function allowPublic() {
3
+ const check = () => true;
4
+ check._isPublic = true;
5
+ return check;
6
+ }
7
+ function requireAuth() {
8
+ return (ctx) => {
9
+ if (!ctx.user) {
10
+ return { granted: false, reason: "Authentication required" };
11
+ }
12
+ return true;
13
+ };
14
+ }
15
+ function requireRoles(roles, options) {
16
+ return (ctx) => {
17
+ if (!ctx.user) {
18
+ return { granted: false, reason: "Authentication required" };
19
+ }
20
+ const userRoles = ctx.user.roles ?? [];
21
+ if (options?.bypassRoles?.some((r) => userRoles.includes(r))) {
22
+ return true;
23
+ }
24
+ if (roles.some((r) => userRoles.includes(r))) {
25
+ return true;
26
+ }
27
+ return {
28
+ granted: false,
29
+ reason: `Required roles: ${roles.join(", ")}`
30
+ };
31
+ };
32
+ }
33
+ function requireOwnership(ownerField = "userId", options) {
34
+ return (ctx) => {
35
+ if (!ctx.user) {
36
+ return { granted: false, reason: "Authentication required" };
37
+ }
38
+ const userRoles = ctx.user.roles ?? [];
39
+ if (options?.bypassRoles?.some((r) => userRoles.includes(r))) {
40
+ return true;
41
+ }
42
+ const userId = ctx.user.id ?? ctx.user._id;
43
+ return {
44
+ granted: true,
45
+ filters: { [ownerField]: userId }
46
+ };
47
+ };
48
+ }
49
+ function allOf(...checks) {
50
+ return async (ctx) => {
51
+ let mergedFilters = {};
52
+ for (const check of checks) {
53
+ const result = await check(ctx);
54
+ const normalized = typeof result === "boolean" ? { granted: result } : result;
55
+ if (!normalized.granted) {
56
+ return normalized;
57
+ }
58
+ if (normalized.filters) {
59
+ mergedFilters = { ...mergedFilters, ...normalized.filters };
60
+ }
61
+ }
62
+ return {
63
+ granted: true,
64
+ filters: Object.keys(mergedFilters).length > 0 ? mergedFilters : void 0
65
+ };
66
+ };
67
+ }
68
+ function anyOf(...checks) {
69
+ return async (ctx) => {
70
+ const reasons = [];
71
+ for (const check of checks) {
72
+ const result = await check(ctx);
73
+ const normalized = typeof result === "boolean" ? { granted: result } : result;
74
+ if (normalized.granted) {
75
+ return normalized;
76
+ }
77
+ if (normalized.reason) {
78
+ reasons.push(normalized.reason);
79
+ }
80
+ }
81
+ return {
82
+ granted: false,
83
+ reason: reasons.join("; ")
84
+ };
85
+ };
86
+ }
87
+ function denyAll(reason = "Access denied") {
88
+ return () => ({ granted: false, reason });
89
+ }
90
+ function when(condition) {
91
+ return async (ctx) => {
92
+ const result = await condition(ctx);
93
+ return {
94
+ granted: result,
95
+ reason: result ? void 0 : "Condition not met"
96
+ };
97
+ };
98
+ }
99
+
100
+ export { allOf, allowPublic, anyOf, denyAll, requireAuth, requireOwnership, requireRoles, when };
@@ -0,0 +1,46 @@
1
+ export { k as ArcCore, A as ArcCorePluginOptions, G as GracefulShutdownOptions, b as HealthCheck, H as HealthOptions, R as RequestIdOptions, T as TracingOptions, f as arcCorePlugin, j as arcCorePluginFn, d as createSpan, e as gracefulShutdownPlugin, g as gracefulShutdownPluginFn, a as healthPlugin, h as healthPluginFn, i as isTracingAvailable, _ as requestIdPlugin, r as requestIdPluginFn, t as traced, c as tracingPlugin } from '../arcCorePlugin-DTPWXcZN.js';
2
+ import { FastifyInstance, FastifyRequest } from 'fastify';
3
+ import '../index-DkAW8BXh.js';
4
+ import 'mongoose';
5
+ import '../types-B99TBmFV.js';
6
+
7
+ /**
8
+ * Error Handler Plugin
9
+ *
10
+ * Global error handling for Arc applications.
11
+ * Catches all errors and returns a consistent JSON response.
12
+ *
13
+ * @example
14
+ * import { errorHandlerPlugin } from '@classytic/arc/plugins';
15
+ *
16
+ * await fastify.register(errorHandlerPlugin, {
17
+ * includeStack: process.env.NODE_ENV !== 'production',
18
+ * onError: (error, request) => {
19
+ * // Log to external service (Sentry, etc.)
20
+ * Sentry.captureException(error);
21
+ * }
22
+ * });
23
+ */
24
+
25
+ interface ErrorHandlerOptions {
26
+ /**
27
+ * Include stack trace in error responses (default: false in production)
28
+ */
29
+ includeStack?: boolean;
30
+ /**
31
+ * Custom error callback for logging to external services
32
+ */
33
+ onError?: (error: Error, request: FastifyRequest) => void | Promise<void>;
34
+ /**
35
+ * Map specific error types to custom responses
36
+ */
37
+ errorMap?: Record<string, {
38
+ statusCode: number;
39
+ code: string;
40
+ message?: string;
41
+ }>;
42
+ }
43
+ declare function errorHandlerPluginFn(fastify: FastifyInstance, options?: ErrorHandlerOptions): Promise<void>;
44
+ declare const errorHandlerPlugin: typeof errorHandlerPluginFn;
45
+
46
+ export { type ErrorHandlerOptions, errorHandlerPlugin, errorHandlerPlugin as errorHandlerPluginFn };