@classytic/arc 2.10.8 → 2.11.1

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 (136) hide show
  1. package/dist/{BaseController-DVNKvoX4.mjs → BaseController-JNV08qOT.mjs} +480 -442
  2. package/dist/{queryCachePlugin-Dumka73q.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/adapters/index.mjs +1 -1
  5. package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
  6. package/dist/audit/index.d.mts +1 -1
  7. package/dist/auth/index.d.mts +1 -1
  8. package/dist/auth/index.mjs +5 -5
  9. package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
  10. package/dist/cache/index.d.mts +3 -2
  11. package/dist/cache/index.mjs +3 -3
  12. package/dist/cli/commands/docs.mjs +2 -2
  13. package/dist/cli/commands/generate.mjs +37 -27
  14. package/dist/cli/commands/init.mjs +46 -33
  15. package/dist/cli/commands/introspect.mjs +1 -1
  16. package/dist/context/index.mjs +1 -1
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -3
  19. package/dist/core-DXdSSFW-.mjs +1037 -0
  20. package/dist/createActionRouter-BwaSM0No.mjs +166 -0
  21. package/dist/{createApp-BwnEAO2h.mjs → createApp-P1d6rjPy.mjs} +75 -27
  22. package/dist/docs/index.d.mts +1 -1
  23. package/dist/docs/index.mjs +2 -2
  24. package/dist/{elevation-Dci0AYLT.mjs → elevation-DOFoxoDs.mjs} +1 -1
  25. package/dist/{errorHandler-CSxe7KIM.mjs → errorHandler-BQm8ZxTK.mjs} +1 -1
  26. package/dist/{eventPlugin-ByU4Cv0e.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
  27. package/dist/events/index.d.mts +3 -3
  28. package/dist/events/index.mjs +2 -2
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/factory/index.d.mts +2 -2
  31. package/dist/factory/index.mjs +2 -2
  32. package/dist/hooks/index.d.mts +1 -1
  33. package/dist/hooks/index.mjs +1 -1
  34. package/dist/idempotency/index.d.mts +3 -3
  35. package/dist/idempotency/index.mjs +1 -1
  36. package/dist/idempotency/redis.d.mts +1 -1
  37. package/dist/{index-C_Noptz-.d.mts → index-BYCqHCVu.d.mts} +2 -2
  38. package/dist/{index-BGbpGVyM.d.mts → index-C_bgx9o4.d.mts} +712 -500
  39. package/dist/{index-BziRPS4H.d.mts → index-CvM1e09j.d.mts} +29 -10
  40. package/dist/{index-EqQN6p0W.d.mts → index-pUczGjO0.d.mts} +11 -8
  41. package/dist/index-smCAoA5W.d.mts +1179 -0
  42. package/dist/index.d.mts +6 -38
  43. package/dist/index.mjs +9 -9
  44. package/dist/integrations/event-gateway.d.mts +1 -1
  45. package/dist/integrations/event-gateway.mjs +1 -1
  46. package/dist/integrations/index.d.mts +2 -2
  47. package/dist/integrations/mcp/index.d.mts +2 -2
  48. package/dist/integrations/mcp/index.mjs +1 -1
  49. package/dist/integrations/mcp/testing.d.mts +1 -1
  50. package/dist/integrations/mcp/testing.mjs +1 -1
  51. package/dist/integrations/streamline.d.mts +46 -5
  52. package/dist/integrations/streamline.mjs +50 -21
  53. package/dist/integrations/websocket-redis.d.mts +1 -1
  54. package/dist/integrations/websocket.d.mts +2 -154
  55. package/dist/integrations/websocket.mjs +292 -224
  56. package/dist/{keys-nWQGUTu1.mjs → keys-CARyUjiR.mjs} +2 -0
  57. package/dist/{loadResources-Bksk8ydA.mjs → loadResources-CPpkyKfM.mjs} +32 -8
  58. package/dist/middleware/index.d.mts +1 -1
  59. package/dist/middleware/index.mjs +1 -1
  60. package/dist/{openapi-DpNpqBmo.mjs → openapi-C0L9ar7m.mjs} +4 -4
  61. package/dist/org/index.d.mts +1 -1
  62. package/dist/permissions/index.d.mts +1 -1
  63. package/dist/permissions/index.mjs +2 -4
  64. package/dist/{permissions-wkqRwicB.mjs → permissions-B4vU9L0Q.mjs} +221 -3
  65. package/dist/{pipe-CGJxqDGx.mjs → pipe-DVoIheVC.mjs} +1 -1
  66. package/dist/pipeline/index.d.mts +1 -1
  67. package/dist/pipeline/index.mjs +1 -1
  68. package/dist/plugins/index.d.mts +4 -4
  69. package/dist/plugins/index.mjs +10 -10
  70. package/dist/plugins/response-cache.mjs +1 -1
  71. package/dist/plugins/tracing-entry.d.mts +1 -1
  72. package/dist/plugins/tracing-entry.mjs +42 -24
  73. package/dist/presets/filesUpload.d.mts +1 -1
  74. package/dist/presets/filesUpload.mjs +3 -3
  75. package/dist/presets/index.d.mts +1 -1
  76. package/dist/presets/index.mjs +1 -1
  77. package/dist/presets/multiTenant.d.mts +1 -1
  78. package/dist/presets/multiTenant.mjs +6 -0
  79. package/dist/presets/search.d.mts +1 -1
  80. package/dist/presets/search.mjs +1 -1
  81. package/dist/{presets-CrwOvuXI.mjs → presets-k604Lj99.mjs} +1 -1
  82. package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
  83. package/dist/{queryCachePlugin-ChLNZvFT.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
  84. package/dist/{redis-MXLp1oOf.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
  85. package/dist/registry/index.d.mts +1 -1
  86. package/dist/registry/index.mjs +2 -2
  87. package/dist/{resourceToTools-BhF3JV5p.mjs → resourceToTools--okX6QBr.mjs} +534 -420
  88. package/dist/routerShared-DeESFp4a.mjs +515 -0
  89. package/dist/schemaIR-BlG9bY7v.mjs +137 -0
  90. package/dist/scope/index.mjs +2 -2
  91. package/dist/testing/index.d.mts +367 -711
  92. package/dist/testing/index.mjs +637 -1434
  93. package/dist/{tracing-xqXzWeaf.d.mts → tracing-DokiEsuz.d.mts} +9 -4
  94. package/dist/types/index.d.mts +3 -3
  95. package/dist/types/index.mjs +1 -3
  96. package/dist/{types-CVdgPXBW.d.mts → types-BdA4uMBV.d.mts} +191 -28
  97. package/dist/{types-CVKBssX5.d.mts → types-Bh_gEJBi.d.mts} +1 -1
  98. package/dist/utils/index.d.mts +2 -968
  99. package/dist/utils/index.mjs +5 -6
  100. package/dist/utils-D3Yxnrwr.mjs +1639 -0
  101. package/dist/websocket-CyJ1VIFI.d.mts +186 -0
  102. package/package.json +7 -5
  103. package/skills/arc/SKILL.md +124 -39
  104. package/skills/arc/references/testing.md +212 -183
  105. package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
  106. package/dist/core-3MWJosCH.mjs +0 -1459
  107. package/dist/createActionRouter-C8UUB3Px.mjs +0 -249
  108. package/dist/errors-BI8kEKsO.d.mts +0 -140
  109. package/dist/fields-CTMWOUDt.mjs +0 -126
  110. package/dist/queryParser-NR__Qiju.mjs +0 -419
  111. package/dist/types-CDnTEpga.mjs +0 -27
  112. package/dist/utils-LMwVidKy.mjs +0 -947
  113. /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
  114. /package/dist/{ResourceRegistry-CcN2LVrc.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
  115. /package/dist/{actionPermissions-TUVR3uiZ.mjs → actionPermissions-C8YYU92K.mjs} +0 -0
  116. /package/dist/{caching-3h93rkJM.mjs → caching-CheW3m-S.mjs} +0 -0
  117. /package/dist/{errorHandler-2ii4RIYr.d.mts → errorHandler-Co3lnVmJ.d.mts} +0 -0
  118. /package/dist/{errors-BqdUDja_.mjs → errors-D5c-5BJL.mjs} +0 -0
  119. /package/dist/{eventPlugin-D1ThQ1Pp.d.mts → eventPlugin-CUNjYYRY.d.mts} +0 -0
  120. /package/dist/{interface-B-pe8fhj.d.mts → interface-CkkWm5uR.d.mts} +0 -0
  121. /package/dist/{interface-yhyb_pLY.d.mts → interface-Da0r7Lna.d.mts} +0 -0
  122. /package/dist/{memory-DqI-449b.mjs → memory-DikHSvWa.mjs} +0 -0
  123. /package/dist/{metrics-TuOmguhi.mjs → metrics-Csh4nsvv.mjs} +0 -0
  124. /package/dist/{multipartBody-CUQGVlM_.mjs → multipartBody-CvTR1Un6.mjs} +0 -0
  125. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-BneOJkpi.mjs} +0 -0
  126. /package/dist/{redis-stream-bkO88VHx.d.mts → redis-stream-CM8TXTix.d.mts} +0 -0
  127. /package/dist/{registry-B0Wl7uVV.mjs → registry-D63ee7fl.mjs} +0 -0
  128. /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
  129. /package/dist/{requestContext-C38GskNt.mjs → requestContext-CfRkaxwf.mjs} +0 -0
  130. /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
  131. /package/dist/{sse-D8UeDwis.mjs → sse-V7aXc3bW.mjs} +0 -0
  132. /package/dist/{store-helpers-DYYUQbQN.mjs → store-helpers-BhrzxvyQ.mjs} +0 -0
  133. /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
  134. /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
  135. /package/dist/{versioning-B6mimogM.mjs → versioning-CGPjkqAg.mjs} +0 -0
  136. /package/dist/{versioning-CeUXHfjw.d.mts → versioning-M9lNLhO8.d.mts} +0 -0
@@ -1,249 +0,0 @@
1
- import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { n as normalizePermissionResult, t as applyPermissionResult } from "./applyPermissionResult-QhV1Pa-g.mjs";
3
- import { a as toJsonSchema } from "./schemaConverter-BxFDdtXu.mjs";
4
- //#region src/core/createActionRouter.ts
5
- var createActionRouter_exports = /* @__PURE__ */ __exportAll({
6
- buildActionBodySchema: () => buildActionBodySchema,
7
- createActionRouter: () => createActionRouter
8
- });
9
- /**
10
- * Create action-based state transition endpoint
11
- *
12
- * Registers: POST /:id/action
13
- * Body: { action: string, ...actionData }
14
- *
15
- * @param fastify - Fastify instance
16
- * @param config - Action router configuration
17
- */
18
- function createActionRouter(fastify, config) {
19
- const { tag, actions, actionPermissions = {}, actionSchemas = {}, globalAuth, idempotencyService, onError } = config;
20
- const actionEnum = Object.keys(actions);
21
- if (actionEnum.length === 0) {
22
- fastify.log.warn("[createActionRouter] No actions defined, skipping route creation");
23
- return;
24
- }
25
- const bodySchema = buildActionBodySchema(actionEnum, actionSchemas);
26
- const routeSchema = {
27
- tags: tag ? [tag] : void 0,
28
- summary: `Perform action (${actionEnum.join("/")})`,
29
- description: buildActionDescription(actions, actionPermissions),
30
- params: {
31
- type: "object",
32
- properties: { id: {
33
- type: "string",
34
- description: "Resource ID"
35
- } },
36
- required: ["id"]
37
- },
38
- body: bodySchema
39
- };
40
- const preHandler = [];
41
- const hasPublicActions = Object.entries(actionPermissions).some(([, p]) => p?._isPublic) || globalAuth && globalAuth?._isPublic;
42
- const hasProtectedActions = Object.entries(actionPermissions).some(([, p]) => !p?._isPublic) || globalAuth && !globalAuth?._isPublic;
43
- if (hasProtectedActions && !hasPublicActions && fastify.authenticate) preHandler.push(fastify.authenticate);
44
- fastify.post("/:id/action", {
45
- schema: routeSchema,
46
- preHandler: preHandler.length ? preHandler : void 0
47
- }, async (req, reply) => {
48
- const { action, ...data } = req.body;
49
- const { id } = req.params;
50
- const rawIdempotencyKey = req.headers["idempotency-key"];
51
- const idempotencyKey = Array.isArray(rawIdempotencyKey) ? rawIdempotencyKey[0] : rawIdempotencyKey;
52
- const handler = actions[action];
53
- if (!handler) return reply.code(400).send({
54
- success: false,
55
- error: `Invalid action '${action}'. Valid actions: ${actionEnum.join(", ")}`,
56
- validActions: actionEnum
57
- });
58
- const permissionCheck = actionPermissions[action] ?? globalAuth;
59
- if (hasPublicActions && hasProtectedActions && permissionCheck) {
60
- if (!permissionCheck?._isPublic && fastify.authenticate) {
61
- try {
62
- await fastify.authenticate(req, reply);
63
- } catch {
64
- if (!reply.sent) return reply.code(401).send({
65
- success: false,
66
- error: "Authentication required"
67
- });
68
- return;
69
- }
70
- if (reply.sent) return;
71
- }
72
- }
73
- if (permissionCheck) {
74
- const context = {
75
- user: req.user ?? null,
76
- request: req,
77
- resource: tag ?? "action",
78
- action,
79
- resourceId: id,
80
- params: req.params,
81
- data
82
- };
83
- let result;
84
- try {
85
- result = await permissionCheck(context);
86
- } catch (err) {
87
- req.log?.warn?.({
88
- err,
89
- resource: tag ?? "action",
90
- action
91
- }, "Permission check threw");
92
- return reply.code(403).send({
93
- success: false,
94
- error: "Permission denied"
95
- });
96
- }
97
- const permResult = normalizePermissionResult(result);
98
- if (!permResult.granted) return reply.code(context.user ? 403 : 401).send({
99
- success: false,
100
- error: permResult.reason ?? (context.user ? `Permission denied for '${action}'` : "Authentication required")
101
- });
102
- applyPermissionResult(permResult, req);
103
- }
104
- try {
105
- if (idempotencyKey && idempotencyService) {
106
- const user = req.user;
107
- const payloadForHash = {
108
- action,
109
- id,
110
- data,
111
- userId: (user?._id)?.toString?.() || user?.id || null
112
- };
113
- const idempotencyResult = await idempotencyService.check(idempotencyKey, payloadForHash);
114
- if (!idempotencyResult.isNew && "existingResult" in idempotencyResult) return reply.send({
115
- success: true,
116
- data: idempotencyResult.existingResult,
117
- cached: true
118
- });
119
- }
120
- const result = await handler(id, data, req);
121
- if (idempotencyService) await idempotencyService.complete(idempotencyKey, result);
122
- return reply.send({
123
- success: true,
124
- data: result
125
- });
126
- } catch (error) {
127
- if (idempotencyService) await idempotencyService.fail(idempotencyKey, error);
128
- if (onError) {
129
- const { statusCode, error: errorMsg, code } = onError(error, action, id);
130
- return reply.code(statusCode).send({
131
- success: false,
132
- error: errorMsg,
133
- code
134
- });
135
- }
136
- const err = error;
137
- const statusCode = err.statusCode || err.status || 500;
138
- const errorCode = err.code || "ACTION_FAILED";
139
- if (statusCode >= 500) req.log.error({
140
- err: error,
141
- action,
142
- id
143
- }, "Action handler error");
144
- return reply.code(statusCode).send({
145
- success: false,
146
- error: err.message || `Failed to execute '${action}' action`,
147
- code: errorCode
148
- });
149
- }
150
- });
151
- fastify.log.debug({
152
- actions: actionEnum,
153
- tag
154
- }, "[createActionRouter] Registered action endpoint: POST /:id/action");
155
- }
156
- /**
157
- * Build a discriminated body schema for the unified action endpoint.
158
- *
159
- * Produces a schema of the form:
160
- * ```json
161
- * {
162
- * "type": "object",
163
- * "required": ["action"],
164
- * "oneOf": [
165
- * { "properties": { "action": { "const": "dispatch" }, "carrier": {...} }, "required": ["action", "carrier"] },
166
- * { "properties": { "action": { "const": "approve" } }, "required": ["action"] }
167
- * ]
168
- * }
169
- * ```
170
- *
171
- * AJV validates this natively, so an action call missing required fields is
172
- * rejected with HTTP 400 before the handler ever runs.
173
- *
174
- * Exported so OpenAPI generation and MCP tool generation can reuse the same
175
- * schema shape (single source of truth).
176
- */
177
- function buildActionBodySchema(actionEnum, actionSchemas = {}) {
178
- const branches = [];
179
- for (const actionName of actionEnum) {
180
- const raw = actionSchemas[actionName];
181
- const { properties, required } = normalizeActionSchema(raw);
182
- const branchProperties = {
183
- action: {
184
- type: "string",
185
- const: actionName
186
- },
187
- ...properties
188
- };
189
- const branchRequired = ["action", ...required.filter((r) => r !== "action")];
190
- branches.push({
191
- type: "object",
192
- properties: branchProperties,
193
- required: branchRequired
194
- });
195
- }
196
- return {
197
- type: "object",
198
- required: ["action"],
199
- oneOf: branches
200
- };
201
- }
202
- /**
203
- * Normalize the accepted schema shapes into `{ properties, required }`.
204
- *
205
- * Handles:
206
- * 1. Full JSON Schema object (has `type: 'object'` + `properties`)
207
- * 2. Zod v4 schema (has `_zod` marker) — converted via `toJsonSchema`
208
- * 3. Legacy field map (`{ fieldName: { type: 'string' } }`) — every field required
209
- * unless its schema has `nullable: true` or sentinel `required: false`
210
- */
211
- function normalizeActionSchema(raw) {
212
- if (!raw || typeof raw !== "object") return {
213
- properties: {},
214
- required: []
215
- };
216
- const converted = toJsonSchema(raw);
217
- if (converted && typeof converted === "object" && (converted.type === "object" || "properties" in converted)) return {
218
- properties: converted.properties ?? {},
219
- required: Array.isArray(converted.required) ? converted.required : []
220
- };
221
- const properties = {};
222
- const required = [];
223
- for (const [fieldName, fieldSchema] of Object.entries(raw)) {
224
- if (fieldName === "type" || fieldName === "properties" || fieldName === "required") continue;
225
- if (!fieldSchema || typeof fieldSchema !== "object") continue;
226
- const fs = fieldSchema;
227
- properties[fieldName] = fs;
228
- if (fs.required !== false) required.push(fieldName);
229
- }
230
- return {
231
- properties,
232
- required
233
- };
234
- }
235
- /**
236
- * Build description with action details
237
- * Uses _roles metadata from PermissionCheck functions for OpenAPI docs
238
- */
239
- function buildActionDescription(actions, actionPermissions) {
240
- const lines = ["Unified action endpoint for state transitions.\n\n**Available actions:**"];
241
- Object.keys(actions).forEach((action) => {
242
- const roles = actionPermissions[action]?._roles;
243
- const roleStr = roles?.length ? ` (requires: ${roles.join(" or ")})` : "";
244
- lines.push(`- \`${action}\`${roleStr}`);
245
- });
246
- return lines.join("\n");
247
- }
248
- //#endregion
249
- export { createActionRouter_exports as n, buildActionBodySchema as t };
@@ -1,140 +0,0 @@
1
- //#region src/utils/errors.d.ts
2
- /**
3
- * Error Classes
4
- *
5
- * Standard error types for the Arc framework.
6
- */
7
- interface ErrorDetails {
8
- code?: string;
9
- statusCode?: number;
10
- details?: Record<string, unknown>;
11
- cause?: Error;
12
- requestId?: string;
13
- }
14
- /**
15
- * Base Arc Error
16
- *
17
- * All Arc errors extend this class and produce a consistent error envelope:
18
- * {
19
- * success: false,
20
- * error: "Human-readable message",
21
- * code: "MACHINE_CODE",
22
- * requestId: "uuid", // For tracing
23
- * timestamp: "ISO date", // When error occurred
24
- * details: { ... } // Additional context
25
- * }
26
- */
27
- declare class ArcError extends Error {
28
- name: string;
29
- readonly code: string;
30
- readonly statusCode: number;
31
- readonly details?: Record<string, unknown>;
32
- readonly cause?: Error;
33
- readonly timestamp: string;
34
- requestId?: string;
35
- constructor(message: string, options?: ErrorDetails);
36
- /**
37
- * Set request ID (typically from request context)
38
- */
39
- withRequestId(requestId: string): this;
40
- /**
41
- * Convert to JSON response.
42
- * Includes cause chain when present for debugging visibility.
43
- */
44
- toJSON(): Record<string, unknown>;
45
- }
46
- /**
47
- * Not Found Error - 404
48
- */
49
- declare class NotFoundError extends ArcError {
50
- constructor(resource: string, identifier?: string);
51
- }
52
- /**
53
- * Validation Error - 400
54
- */
55
- declare class ValidationError extends ArcError {
56
- readonly errors: Array<{
57
- field: string;
58
- message: string;
59
- }>;
60
- constructor(message: string, errors?: Array<{
61
- field: string;
62
- message: string;
63
- }>);
64
- }
65
- /**
66
- * Unauthorized Error - 401
67
- */
68
- declare class UnauthorizedError extends ArcError {
69
- constructor(message?: string);
70
- }
71
- /**
72
- * Forbidden Error - 403
73
- */
74
- declare class ForbiddenError extends ArcError {
75
- constructor(message?: string);
76
- }
77
- /**
78
- * Conflict Error - 409
79
- */
80
- declare class ConflictError extends ArcError {
81
- constructor(message: string, field?: string);
82
- }
83
- /**
84
- * Organization Required Error - 403
85
- */
86
- declare class OrgRequiredError extends ArcError {
87
- readonly organizations?: Array<{
88
- id: string;
89
- roles?: string[];
90
- }>;
91
- constructor(message: string, organizations?: Array<{
92
- id: string;
93
- roles?: string[];
94
- }>);
95
- }
96
- /**
97
- * Organization Access Denied Error - 403
98
- */
99
- declare class OrgAccessDeniedError extends ArcError {
100
- constructor(orgId?: string);
101
- }
102
- /**
103
- * Rate Limit Error - 429
104
- */
105
- declare class RateLimitError extends ArcError {
106
- readonly retryAfter?: number;
107
- constructor(message?: string, retryAfter?: number);
108
- }
109
- /**
110
- * Service Unavailable Error - 503
111
- */
112
- declare class ServiceUnavailableError extends ArcError {
113
- constructor(message?: string);
114
- }
115
- /**
116
- * Create error from status code
117
- */
118
- declare function createError(statusCode: number, message: string, details?: Record<string, unknown>): ArcError;
119
- /**
120
- * Create a domain-specific error with automatic HTTP status mapping.
121
- *
122
- * Eliminates manual `if (err.code === 'X') return status` boilerplate.
123
- * Arc's error handler automatically maps `statusCode` to HTTP response.
124
- *
125
- * @example
126
- * ```typescript
127
- * import { createDomainError } from '@classytic/arc';
128
- *
129
- * throw createDomainError('MEMBER_NOT_FOUND', 'Member does not exist', 404);
130
- * throw createDomainError('SELF_REFERRAL', 'Cannot refer yourself', 422);
131
- * throw createDomainError('INSUFFICIENT_BALANCE', 'Not enough credits', 402, { balance: 0 });
132
- * ```
133
- */
134
- declare function createDomainError(code: string, message: string, statusCode?: number, details?: Record<string, unknown>): ArcError;
135
- /**
136
- * Check if error is an Arc error
137
- */
138
- declare function isArcError(error: unknown): error is ArcError;
139
- //#endregion
140
- export { NotFoundError as a, RateLimitError as c, ValidationError as d, createDomainError as f, ForbiddenError as i, ServiceUnavailableError as l, isArcError as m, ConflictError as n, OrgAccessDeniedError as o, createError as p, ErrorDetails as r, OrgRequiredError as s, ArcError as t, UnauthorizedError as u };
@@ -1,126 +0,0 @@
1
- //#region src/permissions/fields.ts
2
- /**
3
- * Field-Level Permissions
4
- *
5
- * Control field visibility and writability per role.
6
- * Integrated into the response path (read) and sanitization path (write).
7
- *
8
- * @example
9
- * ```typescript
10
- * import { fields, defineResource } from '@classytic/arc';
11
- *
12
- * const userResource = defineResource({
13
- * name: 'user',
14
- * adapter: userAdapter,
15
- * fields: {
16
- * salary: fields.visibleTo(['admin', 'hr']),
17
- * internalNotes: fields.writableBy(['admin']),
18
- * email: fields.redactFor(['viewer']),
19
- * password: fields.hidden(),
20
- * },
21
- * });
22
- * ```
23
- */
24
- /** Type guard for Mongoose-like documents with toObject() */
25
- function isMongooseDoc(obj) {
26
- return !!obj && typeof obj === "object" && "toObject" in obj && typeof obj.toObject === "function";
27
- }
28
- const fields = {
29
- hidden() {
30
- return { _type: "hidden" };
31
- },
32
- visibleTo(roles) {
33
- return {
34
- _type: "visibleTo",
35
- roles
36
- };
37
- },
38
- writableBy(roles) {
39
- return {
40
- _type: "writableBy",
41
- roles
42
- };
43
- },
44
- redactFor(roles, redactValue = "***") {
45
- return {
46
- _type: "redactFor",
47
- roles,
48
- redactValue
49
- };
50
- }
51
- };
52
- /**
53
- * Apply field-level READ permissions to a response object.
54
- * Strips hidden fields, enforces visibility, and applies redaction.
55
- *
56
- * @param data - The response object (mutated in place for performance)
57
- * @param fieldPermissions - Field permission map from resource config
58
- * @param userRoles - Current user's roles (empty array for unauthenticated)
59
- * @returns The filtered object
60
- */
61
- function applyFieldReadPermissions(data, fieldPermissions, userRoles) {
62
- if (!data || typeof data !== "object") return data;
63
- const result = { ...isMongooseDoc(data) ? data.toObject() : data };
64
- for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
65
- case "hidden":
66
- delete result[field];
67
- break;
68
- case "visibleTo":
69
- if (!perm.roles?.some((r) => userRoles.includes(r))) delete result[field];
70
- break;
71
- case "redactFor":
72
- if (perm.roles?.some((r) => userRoles.includes(r))) result[field] = perm.redactValue ?? "***";
73
- break;
74
- case "writableBy": break;
75
- }
76
- return result;
77
- }
78
- /**
79
- * Apply field-level WRITE permissions to request body.
80
- *
81
- * Returns both the filtered body and the list of denied fields. Callers are
82
- * expected to reject the request when `deniedFields.length > 0` — silently
83
- * stripping fields hides misconfigurations and real attacks. See
84
- * `BodySanitizer` for the default policy.
85
- *
86
- * @param body - The request body (returns a new filtered copy)
87
- * @param fieldPermissions - Field permission map from resource config
88
- * @param userRoles - Current user's roles
89
- */
90
- function applyFieldWritePermissions(body, fieldPermissions, userRoles) {
91
- const result = { ...body };
92
- const deniedFields = [];
93
- for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
94
- case "hidden":
95
- if (field in result) {
96
- deniedFields.push(field);
97
- delete result[field];
98
- }
99
- break;
100
- case "writableBy":
101
- if (field in result && !perm.roles?.some((r) => userRoles.includes(r))) {
102
- deniedFields.push(field);
103
- delete result[field];
104
- }
105
- break;
106
- }
107
- return {
108
- body: result,
109
- deniedFields
110
- };
111
- }
112
- /**
113
- * Resolve effective roles by merging global user roles with org-level roles.
114
- *
115
- * Global roles come from `req.user.role` (normalized via getUserRoles()).
116
- * Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
117
- *
118
- * When no org context exists, returns global roles only — backward compatible.
119
- */
120
- function resolveEffectiveRoles(userRoles, orgRoles) {
121
- if (orgRoles.length === 0) return [...userRoles];
122
- if (userRoles.length === 0) return [...orgRoles];
123
- return [...new Set([...userRoles, ...orgRoles])];
124
- }
125
- //#endregion
126
- export { resolveEffectiveRoles as i, applyFieldWritePermissions as n, fields as r, applyFieldReadPermissions as t };