@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.
Files changed (200) hide show
  1. package/README.md +247 -794
  2. package/bin/arc.js +91 -52
  3. package/dist/EventTransport-BkUDYZEb.d.mts +99 -0
  4. package/dist/HookSystem-BsGV-j2l.mjs +404 -0
  5. package/dist/ResourceRegistry-7Ic20ZMw.mjs +249 -0
  6. package/dist/adapters/index.d.mts +5 -0
  7. package/dist/adapters/index.mjs +3 -0
  8. package/dist/audit/index.d.mts +81 -0
  9. package/dist/audit/index.mjs +275 -0
  10. package/dist/audit/mongodb.d.mts +5 -0
  11. package/dist/audit/mongodb.mjs +3 -0
  12. package/dist/audited-CGdLiSlE.mjs +140 -0
  13. package/dist/auth/index.d.mts +188 -0
  14. package/dist/auth/index.mjs +1096 -0
  15. package/dist/auth/redis-session.d.mts +43 -0
  16. package/dist/auth/redis-session.mjs +75 -0
  17. package/dist/betterAuthOpenApi-DjWDddNc.mjs +249 -0
  18. package/dist/cache/index.d.mts +145 -0
  19. package/dist/cache/index.mjs +91 -0
  20. package/dist/caching-GSDJcA6-.mjs +93 -0
  21. package/dist/chunk-C7Uep-_p.mjs +20 -0
  22. package/dist/circuitBreaker-DYhWBW_D.mjs +1096 -0
  23. package/dist/cli/commands/describe.d.mts +18 -0
  24. package/dist/cli/commands/describe.mjs +238 -0
  25. package/dist/cli/commands/docs.d.mts +13 -0
  26. package/dist/cli/commands/docs.mjs +52 -0
  27. package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -2
  28. package/dist/cli/commands/generate.mjs +357 -0
  29. package/dist/cli/commands/{init.d.ts → init.d.mts} +11 -8
  30. package/dist/cli/commands/{init.js → init.mjs} +807 -617
  31. package/dist/cli/commands/introspect.d.mts +10 -0
  32. package/dist/cli/commands/introspect.mjs +75 -0
  33. package/dist/cli/index.d.mts +16 -0
  34. package/dist/cli/index.mjs +156 -0
  35. package/dist/constants-DdXFXQtN.mjs +84 -0
  36. package/dist/core/index.d.mts +5 -0
  37. package/dist/core/index.mjs +4 -0
  38. package/dist/createApp-D2D5XXaV.mjs +559 -0
  39. package/dist/defineResource-PXzSJ15_.mjs +2197 -0
  40. package/dist/discovery/index.d.mts +46 -0
  41. package/dist/discovery/index.mjs +109 -0
  42. package/dist/docs/index.d.mts +162 -0
  43. package/dist/docs/index.mjs +74 -0
  44. package/dist/elevation-DGo5shaX.d.mts +87 -0
  45. package/dist/elevation-DSTbVvYj.mjs +113 -0
  46. package/dist/errorHandler-C3GY3_ow.mjs +108 -0
  47. package/dist/errorHandler-CW3OOeYq.d.mts +72 -0
  48. package/dist/errors-DAWRdiYP.d.mts +124 -0
  49. package/dist/errors-DBANPbGr.mjs +211 -0
  50. package/dist/eventPlugin-BEOvaDqo.mjs +229 -0
  51. package/dist/eventPlugin-H6wDDjGO.d.mts +124 -0
  52. package/dist/events/index.d.mts +53 -0
  53. package/dist/events/index.mjs +51 -0
  54. package/dist/events/transports/redis-stream-entry.d.mts +2 -0
  55. package/dist/events/transports/redis-stream-entry.mjs +177 -0
  56. package/dist/events/transports/redis.d.mts +76 -0
  57. package/dist/events/transports/redis.mjs +124 -0
  58. package/dist/externalPaths-SyPF2tgK.d.mts +50 -0
  59. package/dist/factory/index.d.mts +63 -0
  60. package/dist/factory/index.mjs +3 -0
  61. package/dist/fastifyAdapter-C8DlE0YH.d.mts +216 -0
  62. package/dist/fields-Bi_AVKSo.d.mts +109 -0
  63. package/dist/fields-CTd_CrKr.mjs +114 -0
  64. package/dist/hooks/index.d.mts +4 -0
  65. package/dist/hooks/index.mjs +3 -0
  66. package/dist/idempotency/index.d.mts +96 -0
  67. package/dist/idempotency/index.mjs +319 -0
  68. package/dist/idempotency/mongodb.d.mts +2 -0
  69. package/dist/idempotency/mongodb.mjs +114 -0
  70. package/dist/idempotency/redis.d.mts +2 -0
  71. package/dist/idempotency/redis.mjs +103 -0
  72. package/dist/index.d.mts +260 -0
  73. package/dist/index.mjs +104 -0
  74. package/dist/integrations/event-gateway.d.mts +46 -0
  75. package/dist/integrations/event-gateway.mjs +43 -0
  76. package/dist/integrations/index.d.mts +5 -0
  77. package/dist/integrations/index.mjs +1 -0
  78. package/dist/integrations/jobs.d.mts +103 -0
  79. package/dist/integrations/jobs.mjs +123 -0
  80. package/dist/integrations/streamline.d.mts +60 -0
  81. package/dist/integrations/streamline.mjs +125 -0
  82. package/dist/integrations/websocket.d.mts +82 -0
  83. package/dist/integrations/websocket.mjs +288 -0
  84. package/dist/interface-CSNjltAc.d.mts +77 -0
  85. package/dist/interface-DTbsvIWe.d.mts +54 -0
  86. package/dist/interface-e9XfSsUV.d.mts +1097 -0
  87. package/dist/introspectionPlugin-B3JkrjwU.mjs +53 -0
  88. package/dist/keys-DhqDRxv3.mjs +42 -0
  89. package/dist/logger-ByrvQWZO.mjs +78 -0
  90. package/dist/memory-B2v7KrCB.mjs +143 -0
  91. package/dist/migrations/index.d.mts +156 -0
  92. package/dist/migrations/index.mjs +260 -0
  93. package/dist/mongodb-ClykrfGo.d.mts +118 -0
  94. package/dist/mongodb-DNKEExbf.mjs +93 -0
  95. package/dist/mongodb-Dg8O_gvd.d.mts +71 -0
  96. package/dist/openapi-9nB_kiuR.mjs +525 -0
  97. package/dist/org/index.d.mts +68 -0
  98. package/dist/org/index.mjs +513 -0
  99. package/dist/org/types.d.mts +82 -0
  100. package/dist/org/types.mjs +1 -0
  101. package/dist/permissions/index.d.mts +278 -0
  102. package/dist/permissions/index.mjs +579 -0
  103. package/dist/plugins/index.d.mts +172 -0
  104. package/dist/plugins/index.mjs +522 -0
  105. package/dist/plugins/response-cache.d.mts +87 -0
  106. package/dist/plugins/response-cache.mjs +283 -0
  107. package/dist/plugins/tracing-entry.d.mts +2 -0
  108. package/dist/plugins/tracing-entry.mjs +185 -0
  109. package/dist/pluralize-CM-jZg7p.mjs +86 -0
  110. package/dist/policies/{index.d.ts → index.d.mts} +204 -170
  111. package/dist/policies/index.mjs +321 -0
  112. package/dist/presets/{index.d.ts → index.d.mts} +62 -131
  113. package/dist/presets/index.mjs +143 -0
  114. package/dist/presets/multiTenant.d.mts +24 -0
  115. package/dist/presets/multiTenant.mjs +113 -0
  116. package/dist/presets-BTeYbw7h.d.mts +57 -0
  117. package/dist/presets-CeFtfDR8.mjs +119 -0
  118. package/dist/prisma-C3iornoK.d.mts +274 -0
  119. package/dist/prisma-DJbMt3yf.mjs +627 -0
  120. package/dist/queryCachePlugin-B6R0d4av.mjs +138 -0
  121. package/dist/queryCachePlugin-Q6SYuHZ6.d.mts +71 -0
  122. package/dist/redis-UwjEp8Ea.d.mts +49 -0
  123. package/dist/redis-stream-CBg0upHI.d.mts +103 -0
  124. package/dist/registry/index.d.mts +11 -0
  125. package/dist/registry/index.mjs +4 -0
  126. package/dist/requestContext-xi6OKBL-.mjs +55 -0
  127. package/dist/schemaConverter-Dtg0Kt9T.mjs +98 -0
  128. package/dist/schemas/index.d.mts +63 -0
  129. package/dist/schemas/index.mjs +82 -0
  130. package/dist/scope/index.d.mts +21 -0
  131. package/dist/scope/index.mjs +65 -0
  132. package/dist/sessionManager-D_iEHjQl.d.mts +186 -0
  133. package/dist/sse-DkqQ1uxb.mjs +123 -0
  134. package/dist/testing/index.d.mts +907 -0
  135. package/dist/testing/index.mjs +1976 -0
  136. package/dist/tracing-8CEbhF0w.d.mts +70 -0
  137. package/dist/typeGuards-DwxA1t_L.mjs +9 -0
  138. package/dist/types/index.d.mts +946 -0
  139. package/dist/types/index.mjs +14 -0
  140. package/dist/types-B0dhNrnd.d.mts +445 -0
  141. package/dist/types-Beqn1Un7.mjs +38 -0
  142. package/dist/types-DelU6kln.mjs +25 -0
  143. package/dist/types-RLkFVgaw.d.mts +101 -0
  144. package/dist/utils/index.d.mts +747 -0
  145. package/dist/utils/index.mjs +6 -0
  146. package/package.json +194 -68
  147. package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
  148. package/dist/adapters/index.d.ts +0 -237
  149. package/dist/adapters/index.js +0 -668
  150. package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
  151. package/dist/audit/index.d.ts +0 -195
  152. package/dist/audit/index.js +0 -319
  153. package/dist/auth/index.d.ts +0 -47
  154. package/dist/auth/index.js +0 -174
  155. package/dist/cli/commands/docs.d.ts +0 -11
  156. package/dist/cli/commands/docs.js +0 -474
  157. package/dist/cli/commands/generate.js +0 -334
  158. package/dist/cli/commands/introspect.d.ts +0 -8
  159. package/dist/cli/commands/introspect.js +0 -338
  160. package/dist/cli/index.d.ts +0 -4
  161. package/dist/cli/index.js +0 -3269
  162. package/dist/core/index.d.ts +0 -220
  163. package/dist/core/index.js +0 -2786
  164. package/dist/createApp-Ce9wl8W9.d.ts +0 -77
  165. package/dist/docs/index.d.ts +0 -166
  166. package/dist/docs/index.js +0 -658
  167. package/dist/errors-8WIxGS_6.d.ts +0 -122
  168. package/dist/events/index.d.ts +0 -117
  169. package/dist/events/index.js +0 -89
  170. package/dist/factory/index.d.ts +0 -38
  171. package/dist/factory/index.js +0 -1652
  172. package/dist/hooks/index.d.ts +0 -4
  173. package/dist/hooks/index.js +0 -199
  174. package/dist/idempotency/index.d.ts +0 -323
  175. package/dist/idempotency/index.js +0 -500
  176. package/dist/index-B4t03KQ0.d.ts +0 -1366
  177. package/dist/index.d.ts +0 -135
  178. package/dist/index.js +0 -4756
  179. package/dist/migrations/index.d.ts +0 -185
  180. package/dist/migrations/index.js +0 -274
  181. package/dist/org/index.d.ts +0 -129
  182. package/dist/org/index.js +0 -220
  183. package/dist/permissions/index.d.ts +0 -144
  184. package/dist/permissions/index.js +0 -103
  185. package/dist/plugins/index.d.ts +0 -46
  186. package/dist/plugins/index.js +0 -1069
  187. package/dist/policies/index.js +0 -196
  188. package/dist/presets/index.js +0 -384
  189. package/dist/presets/multiTenant.d.ts +0 -39
  190. package/dist/presets/multiTenant.js +0 -112
  191. package/dist/registry/index.d.ts +0 -16
  192. package/dist/registry/index.js +0 -253
  193. package/dist/testing/index.d.ts +0 -618
  194. package/dist/testing/index.js +0 -48020
  195. package/dist/types/index.d.ts +0 -4
  196. package/dist/types/index.js +0 -8
  197. package/dist/types-B99TBmFV.d.ts +0 -76
  198. package/dist/types-BvckRbs2.d.ts +0 -143
  199. package/dist/utils/index.d.ts +0 -679
  200. package/dist/utils/index.js +0 -931
@@ -0,0 +1,525 @@
1
+ import { t as getUserRoles } from "./types-DelU6kln.mjs";
2
+ import { n as convertRouteSchema } from "./schemaConverter-Dtg0Kt9T.mjs";
3
+ import fp from "fastify-plugin";
4
+
5
+ //#region src/docs/openapi.ts
6
+ /**
7
+ * OpenAPI Spec Generator
8
+ *
9
+ * Auto-generates OpenAPI 3.0 specification from Arc resource registry.
10
+ *
11
+ * @example
12
+ * import { openApiPlugin } from '@classytic/arc/docs';
13
+ *
14
+ * await fastify.register(openApiPlugin, {
15
+ * title: 'My API',
16
+ * version: '1.0.0',
17
+ * });
18
+ *
19
+ * // Spec available at /_docs/openapi.json
20
+ */
21
+ const openApiPlugin = async (fastify, opts = {}) => {
22
+ const { title = "Arc API", version = "1.0.0", description, serverUrl, prefix = "/_docs", apiPrefix = "", authRoles = [] } = opts;
23
+ const buildSpec = () => {
24
+ const arc = fastify.arc;
25
+ const resources = arc?.registry?.getAll() ?? [];
26
+ const externalPaths = arc?.externalOpenApiPaths ?? [];
27
+ return buildOpenApiSpec(resources, {
28
+ title,
29
+ version,
30
+ description,
31
+ serverUrl,
32
+ apiPrefix
33
+ }, externalPaths.length > 0 ? externalPaths : void 0);
34
+ };
35
+ fastify.get(`${prefix}/openapi.json`, async (request, reply) => {
36
+ if (authRoles.length > 0) {
37
+ const user = request.user;
38
+ const roles = getUserRoles(user);
39
+ if (!authRoles.some((r) => roles.includes(r)) && !roles.includes("superadmin")) {
40
+ reply.code(403).send({ error: "Access denied" });
41
+ return;
42
+ }
43
+ }
44
+ return buildSpec();
45
+ });
46
+ fastify.log?.debug?.(`OpenAPI spec available at ${prefix}/openapi.json`);
47
+ };
48
+ /**
49
+ * Build OpenAPI spec from registry resources.
50
+ * Shared by HTTP docs endpoint and CLI export command.
51
+ */
52
+ function buildOpenApiSpec(resources, options = {}, externalPaths) {
53
+ const { title = "Arc API", version = "1.0.0", description, serverUrl, apiPrefix = "" } = options;
54
+ const paths = {};
55
+ const tags = [];
56
+ const additionalSecurity = externalPaths?.flatMap((ext) => ext.resourceSecurity ?? []) ?? [];
57
+ for (const resource of resources) {
58
+ const tagDescParts = [`${resource.displayName || resource.name} operations`];
59
+ if (resource.presets && resource.presets.length > 0) tagDescParts.push(`Presets: ${resource.presets.join(", ")}`);
60
+ if (resource.pipelineSteps && resource.pipelineSteps.length > 0) {
61
+ const stepNames = resource.pipelineSteps.map((s) => `${s.type}(${s.name})`);
62
+ tagDescParts.push(`Pipeline: ${stepNames.join(" → ")}`);
63
+ }
64
+ if (resource.events && resource.events.length > 0) tagDescParts.push(`Events: ${resource.events.join(", ")}`);
65
+ tags.push({
66
+ name: resource.tag || resource.name,
67
+ description: tagDescParts.join(". ")
68
+ });
69
+ const resourcePaths = generateResourcePaths(resource, apiPrefix, additionalSecurity);
70
+ Object.assign(paths, resourcePaths);
71
+ }
72
+ if (externalPaths) for (const ext of externalPaths) {
73
+ for (const [path, methods] of Object.entries(ext.paths)) paths[path] = paths[path] ? {
74
+ ...paths[path],
75
+ ...methods
76
+ } : methods;
77
+ if (ext.tags) {
78
+ for (const tag of ext.tags) if (!tags.find((t) => t.name === tag.name)) tags.push(tag);
79
+ }
80
+ }
81
+ const externalSecuritySchemes = externalPaths?.reduce((acc, ext) => ({
82
+ ...acc,
83
+ ...ext.securitySchemes
84
+ }), {}) ?? {};
85
+ const externalSchemas = externalPaths?.reduce((acc, ext) => ({
86
+ ...acc,
87
+ ...ext.schemas
88
+ }), {}) ?? {};
89
+ return {
90
+ openapi: "3.0.3",
91
+ info: {
92
+ title,
93
+ version,
94
+ ...description && { description }
95
+ },
96
+ ...serverUrl && { servers: [{ url: serverUrl }] },
97
+ paths,
98
+ components: {
99
+ schemas: {
100
+ ...generateSchemas(resources),
101
+ ...externalSchemas
102
+ },
103
+ securitySchemes: {
104
+ bearerAuth: {
105
+ type: "http",
106
+ scheme: "bearer",
107
+ bearerFormat: "JWT"
108
+ },
109
+ orgHeader: {
110
+ type: "apiKey",
111
+ in: "header",
112
+ name: "x-organization-id"
113
+ },
114
+ ...externalSecuritySchemes
115
+ }
116
+ },
117
+ tags
118
+ };
119
+ }
120
+ /**
121
+ * Convert Fastify-style params (/:id) to OpenAPI-style params (/{id})
122
+ */
123
+ function toOpenApiPath(path) {
124
+ return path.replace(/:([^/]+)/g, "{$1}");
125
+ }
126
+ /**
127
+ * Convert OpenAPI schema to query parameters array
128
+ * Transforms { properties: { page: { type: 'integer' } } } to [{ name: 'page', in: 'query', schema: { type: 'integer' } }]
129
+ */
130
+ function convertSchemaToParameters(schema) {
131
+ const params = [];
132
+ const properties = schema.properties || {};
133
+ const required = schema.required || [];
134
+ for (const [name, prop] of Object.entries(properties)) {
135
+ const description = prop.description;
136
+ const { description: _, ...schemaProps } = prop;
137
+ const param = {
138
+ name,
139
+ in: "query",
140
+ required: required.includes(name),
141
+ schema: schemaProps
142
+ };
143
+ if (description) param.description = description;
144
+ params.push(param);
145
+ }
146
+ return params;
147
+ }
148
+ /**
149
+ * Default query parameters when no listQuery schema is provided
150
+ */
151
+ const DEFAULT_LIST_PARAMS = [
152
+ {
153
+ name: "page",
154
+ in: "query",
155
+ schema: { type: "integer" },
156
+ description: "Page number"
157
+ },
158
+ {
159
+ name: "limit",
160
+ in: "query",
161
+ schema: { type: "integer" },
162
+ description: "Items per page"
163
+ },
164
+ {
165
+ name: "sort",
166
+ in: "query",
167
+ schema: { type: "string" },
168
+ description: "Sort field (prefix with - for descending)"
169
+ }
170
+ ];
171
+ /**
172
+ * Generate paths for a resource
173
+ */
174
+ function generateResourcePaths(resource, apiPrefix = "", additionalSecurity = []) {
175
+ const paths = {};
176
+ const basePath = `${apiPrefix}${resource.prefix}`;
177
+ if (resource.disableDefaultRoutes && (!resource.additionalRoutes || resource.additionalRoutes.length === 0)) return paths;
178
+ if (!resource.disableDefaultRoutes) {
179
+ const disabledSet = new Set(resource.disabledRoutes ?? []);
180
+ const updateMethod = resource.updateMethod ?? "PATCH";
181
+ const collectionPath = {};
182
+ if (!disabledSet.has("list")) collectionPath.get = createOperation(resource, "list", "List all", {
183
+ parameters: resource.openApiSchemas?.listQuery ? convertSchemaToParameters(resource.openApiSchemas.listQuery) : DEFAULT_LIST_PARAMS,
184
+ responses: { "200": {
185
+ description: "List of items",
186
+ content: { "application/json": { schema: {
187
+ type: "object",
188
+ properties: {
189
+ success: { type: "boolean" },
190
+ docs: {
191
+ type: "array",
192
+ items: { $ref: `#/components/schemas/${resource.name}` }
193
+ },
194
+ page: { type: "integer" },
195
+ limit: { type: "integer" },
196
+ total: { type: "integer" },
197
+ pages: { type: "integer" },
198
+ hasNext: { type: "boolean" },
199
+ hasPrev: { type: "boolean" }
200
+ }
201
+ } } }
202
+ } }
203
+ }, void 0, additionalSecurity);
204
+ if (!disabledSet.has("create")) collectionPath.post = createOperation(resource, "create", "Create new", {
205
+ requestBody: {
206
+ required: true,
207
+ content: { "application/json": { schema: { $ref: `#/components/schemas/${resource.name}Input` } } }
208
+ },
209
+ responses: { "201": {
210
+ description: "Created successfully",
211
+ content: { "application/json": { schema: {
212
+ type: "object",
213
+ properties: {
214
+ success: { type: "boolean" },
215
+ data: { $ref: `#/components/schemas/${resource.name}` },
216
+ message: { type: "string" }
217
+ }
218
+ } } }
219
+ } }
220
+ }, void 0, additionalSecurity);
221
+ if (Object.keys(collectionPath).length > 0) paths[basePath] = collectionPath;
222
+ const itemPath = {};
223
+ if (!disabledSet.has("get")) itemPath.get = createOperation(resource, "get", "Get by ID", {
224
+ parameters: [{
225
+ name: "id",
226
+ in: "path",
227
+ required: true,
228
+ schema: { type: "string" }
229
+ }],
230
+ responses: {
231
+ "200": {
232
+ description: "Item found",
233
+ content: { "application/json": { schema: {
234
+ type: "object",
235
+ properties: {
236
+ success: { type: "boolean" },
237
+ data: { $ref: `#/components/schemas/${resource.name}` }
238
+ }
239
+ } } }
240
+ },
241
+ "404": { description: "Not found" }
242
+ }
243
+ }, void 0, additionalSecurity);
244
+ if (!disabledSet.has("update")) {
245
+ const updateOp = createOperation(resource, "update", "Update", {
246
+ parameters: [{
247
+ name: "id",
248
+ in: "path",
249
+ required: true,
250
+ schema: { type: "string" }
251
+ }],
252
+ requestBody: {
253
+ required: true,
254
+ content: { "application/json": { schema: { $ref: `#/components/schemas/${resource.name}Input` } } }
255
+ },
256
+ responses: { "200": {
257
+ description: "Updated successfully",
258
+ content: { "application/json": { schema: {
259
+ type: "object",
260
+ properties: {
261
+ success: { type: "boolean" },
262
+ data: { $ref: `#/components/schemas/${resource.name}` },
263
+ message: { type: "string" }
264
+ }
265
+ } } }
266
+ } }
267
+ }, void 0, additionalSecurity);
268
+ if (updateMethod === "both") {
269
+ itemPath.put = updateOp;
270
+ itemPath.patch = updateOp;
271
+ } else if (updateMethod === "PUT") itemPath.put = updateOp;
272
+ else itemPath.patch = updateOp;
273
+ }
274
+ if (!disabledSet.has("delete")) itemPath.delete = createOperation(resource, "delete", "Delete", {
275
+ parameters: [{
276
+ name: "id",
277
+ in: "path",
278
+ required: true,
279
+ schema: { type: "string" }
280
+ }],
281
+ responses: { "200": {
282
+ description: "Deleted successfully",
283
+ content: { "application/json": { schema: {
284
+ type: "object",
285
+ properties: {
286
+ success: { type: "boolean" },
287
+ message: { type: "string" }
288
+ }
289
+ } } }
290
+ } }
291
+ }, void 0, additionalSecurity);
292
+ if (Object.keys(itemPath).length > 0) paths[toOpenApiPath(`${basePath}/:id`)] = itemPath;
293
+ }
294
+ for (const route of resource.additionalRoutes || []) {
295
+ const fullPath = toOpenApiPath(`${basePath}${route.path}`);
296
+ const method = route.method.toLowerCase();
297
+ if (!paths[fullPath]) paths[fullPath] = {};
298
+ const handlerName = route.operation ?? (typeof route.handler === "string" ? route.handler : "handler");
299
+ const isPublicRoute = route.permissions?._isPublic === true;
300
+ const requiresAuthForRoute = !!route.permissions && !isPublicRoute;
301
+ const extras = {
302
+ parameters: extractPathParams(route.path),
303
+ responses: { "200": { description: route.description || "Success" } }
304
+ };
305
+ const rawSchema = route.schema;
306
+ const routeSchema = rawSchema ? convertRouteSchema(rawSchema) : void 0;
307
+ if (routeSchema?.body && [
308
+ "post",
309
+ "put",
310
+ "patch"
311
+ ].includes(method)) extras.requestBody = {
312
+ required: true,
313
+ content: { "application/json": { schema: routeSchema.body } }
314
+ };
315
+ if (routeSchema?.querystring) {
316
+ const queryParams = convertSchemaToParameters(routeSchema.querystring);
317
+ extras.parameters = [...extras.parameters || [], ...queryParams];
318
+ }
319
+ if (routeSchema?.response) {
320
+ const responseSchemas = routeSchema.response;
321
+ for (const [statusCode, schema] of Object.entries(responseSchemas)) extras.responses[statusCode] = {
322
+ description: schema.description || `Response ${statusCode}`,
323
+ content: { "application/json": { schema } }
324
+ };
325
+ }
326
+ paths[fullPath][method] = createOperation(resource, handlerName, route.summary ?? handlerName, extras, requiresAuthForRoute, additionalSecurity);
327
+ }
328
+ return paths;
329
+ }
330
+ /**
331
+ * Create an operation object
332
+ * @param requiresAuthOverride - Override for whether auth is required (for additional routes)
333
+ * @param additionalSecurity - Extra security alternatives from external integrations (OR'd with bearerAuth)
334
+ */
335
+ function createOperation(resource, operation, summary, extras, requiresAuthOverride, additionalSecurity = []) {
336
+ const operationPermission = (resource.permissions || {})[operation];
337
+ const isPublic = operationPermission?._isPublic === true;
338
+ operationPermission?._roles;
339
+ const requiresAuth = requiresAuthOverride !== void 0 ? requiresAuthOverride : typeof operationPermission === "function" && !isPublic;
340
+ const permAnnotation = describePermissionForOpenApi(operationPermission);
341
+ const descParts = [];
342
+ if (permAnnotation) descParts.push(`**Permission**: ${permAnnotation.type === "public" ? "Public" : permAnnotation.type === "requireRoles" ? `Requires roles: ${(permAnnotation.roles ?? []).join(", ")}` : "Requires authentication"}`);
343
+ if (resource.presets && resource.presets.length > 0) descParts.push(`**Presets**: ${resource.presets.join(", ")}`);
344
+ const applicableSteps = (resource.pipelineSteps ?? []).filter((s) => {
345
+ if (!s.operations) return true;
346
+ return s.operations.includes(operation);
347
+ });
348
+ return {
349
+ tags: [resource.tag || "Resource"],
350
+ summary: `${summary} ${(resource.displayName || resource.name).toLowerCase()}`,
351
+ operationId: `${resource.name}_${operation}`,
352
+ ...descParts.length > 0 && { description: descParts.join("\n\n") },
353
+ ...requiresAuth && { security: [{ bearerAuth: [] }, ...additionalSecurity] },
354
+ ...permAnnotation && { "x-arc-permission": permAnnotation },
355
+ ...applicableSteps.length > 0 && { "x-arc-pipeline": applicableSteps.map((s) => ({
356
+ type: s.type,
357
+ name: s.name
358
+ })) },
359
+ responses: {
360
+ ...requiresAuth && {
361
+ "401": {
362
+ description: "Authentication required — no valid Bearer token provided",
363
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
364
+ },
365
+ "403": {
366
+ description: permAnnotation?.roles ? `Forbidden — requires one of: ${permAnnotation.roles.join(", ")}` : "Forbidden — insufficient permissions",
367
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
368
+ }
369
+ },
370
+ "500": { description: "Internal server error" }
371
+ },
372
+ ...extras
373
+ };
374
+ }
375
+ /**
376
+ * Describe a permission check function for OpenAPI.
377
+ * Extracts role, org role, and team permission metadata from permission functions.
378
+ */
379
+ function describePermissionForOpenApi(check) {
380
+ if (!check || typeof check !== "function") return void 0;
381
+ const fn = check;
382
+ if (fn._isPublic === true) return { type: "public" };
383
+ const result = { type: "requireAuth" };
384
+ if (Array.isArray(fn._roles) && fn._roles.length > 0) {
385
+ result.type = "requireRoles";
386
+ result.roles = fn._roles;
387
+ }
388
+ if (Array.isArray(fn._orgRoles) && fn._orgRoles.length > 0) result.orgRoles = fn._orgRoles;
389
+ return result;
390
+ }
391
+ /**
392
+ * Extract path parameters from route path
393
+ */
394
+ function extractPathParams(path) {
395
+ const params = [];
396
+ const matches = path.matchAll(/:([^/]+)/g);
397
+ for (const match of matches) {
398
+ const paramName = match[1];
399
+ if (paramName) params.push({
400
+ name: paramName,
401
+ in: "path",
402
+ required: true,
403
+ schema: { type: "string" }
404
+ });
405
+ }
406
+ return params;
407
+ }
408
+ /**
409
+ * Generate schema definitions from pre-stored registry schemas.
410
+ * Schemas are generated at resource definition time and stored in the registry.
411
+ *
412
+ * Response schema priority:
413
+ * 1. If resource provides explicit `openApiSchemas.response`, use it as-is
414
+ * 2. Otherwise, auto-generate from `createBody` + _id + timestamps
415
+ *
416
+ * Note: This is for OpenAPI documentation only - does NOT affect Fastify serialization.
417
+ */
418
+ function generateSchemas(resources) {
419
+ const schemas = { Error: {
420
+ type: "object",
421
+ properties: {
422
+ success: {
423
+ type: "boolean",
424
+ example: false
425
+ },
426
+ error: { type: "string" },
427
+ code: { type: "string" },
428
+ requestId: { type: "string" },
429
+ timestamp: { type: "string" }
430
+ }
431
+ } };
432
+ for (const resource of resources) {
433
+ const storedSchemas = resource.openApiSchemas;
434
+ const fieldPerms = resource.fieldPermissions;
435
+ if (storedSchemas?.response) schemas[resource.name] = {
436
+ type: "object",
437
+ description: resource.displayName,
438
+ ...storedSchemas.response
439
+ };
440
+ else if (storedSchemas?.createBody) schemas[resource.name] = {
441
+ type: "object",
442
+ description: resource.displayName,
443
+ properties: {
444
+ _id: {
445
+ type: "string",
446
+ description: "Unique identifier"
447
+ },
448
+ ...storedSchemas.createBody.properties ?? {},
449
+ createdAt: {
450
+ type: "string",
451
+ format: "date-time",
452
+ description: "Creation timestamp"
453
+ },
454
+ updatedAt: {
455
+ type: "string",
456
+ format: "date-time",
457
+ description: "Last update timestamp"
458
+ }
459
+ }
460
+ };
461
+ else schemas[resource.name] = {
462
+ type: "object",
463
+ description: resource.displayName,
464
+ properties: {
465
+ _id: {
466
+ type: "string",
467
+ description: "Unique identifier"
468
+ },
469
+ createdAt: {
470
+ type: "string",
471
+ format: "date-time",
472
+ description: "Creation timestamp"
473
+ },
474
+ updatedAt: {
475
+ type: "string",
476
+ format: "date-time",
477
+ description: "Last update timestamp"
478
+ }
479
+ }
480
+ };
481
+ if (fieldPerms && schemas[resource.name]?.properties) {
482
+ const props = schemas[resource.name].properties;
483
+ for (const [field, perm] of Object.entries(fieldPerms)) if (props[field]) {
484
+ const desc = props[field].description ?? "";
485
+ const permDesc = formatFieldPermDescription(perm);
486
+ props[field].description = desc ? `${desc} (${permDesc})` : permDesc;
487
+ } else if (perm.type === "hidden") {}
488
+ }
489
+ if (storedSchemas?.createBody) {
490
+ schemas[`${resource.name}Input`] = {
491
+ type: "object",
492
+ description: `${resource.displayName} create input`,
493
+ ...storedSchemas.createBody
494
+ };
495
+ if (storedSchemas.updateBody) schemas[`${resource.name}Update`] = {
496
+ type: "object",
497
+ description: `${resource.displayName} update input`,
498
+ ...storedSchemas.updateBody
499
+ };
500
+ } else schemas[`${resource.name}Input`] = {
501
+ type: "object",
502
+ description: `${resource.displayName} input`
503
+ };
504
+ }
505
+ return schemas;
506
+ }
507
+ /**
508
+ * Format a field permission description for OpenAPI
509
+ */
510
+ function formatFieldPermDescription(perm) {
511
+ switch (perm.type) {
512
+ case "hidden": return "Hidden — never returned in responses";
513
+ case "visibleTo": return `Visible to: ${(perm.roles ?? []).join(", ")}`;
514
+ case "writableBy": return `Writable by: ${(perm.roles ?? []).join(", ")}`;
515
+ case "redactFor": return `Redacted for: ${(perm.roles ?? []).join(", ")}`;
516
+ default: return perm.type;
517
+ }
518
+ }
519
+ var openapi_default = fp(openApiPlugin, {
520
+ name: "arc-openapi",
521
+ fastify: "5.x"
522
+ });
523
+
524
+ //#endregion
525
+ export { openApiPlugin as n, openapi_default as r, buildOpenApiSpec as t };
@@ -0,0 +1,68 @@
1
+ import "../elevation-DGo5shaX.mjs";
2
+ import { S as RouteHandler } from "../interface-e9XfSsUV.mjs";
3
+ import { i as UserBase } from "../types-RLkFVgaw.mjs";
4
+ import "../types/index.mjs";
5
+ import { InvitationAdapter, InvitationDoc, MemberDoc, OrgAdapter, OrgDoc, OrgPermissionStatement, OrgRole, OrganizationPluginOptions } from "./types.mjs";
6
+ import { FastifyPluginAsync, RouteHandlerMethod } from "fastify";
7
+
8
+ //#region src/org/orgGuard.d.ts
9
+ interface OrgGuardOptions {
10
+ /** Require organization context (default: true) */
11
+ requireOrgContext?: boolean;
12
+ /** Required org-level roles */
13
+ roles?: string[];
14
+ }
15
+ /**
16
+ * Create org guard middleware.
17
+ * Reads `request.scope` for org context and roles.
18
+ * Elevated scope always passes.
19
+ */
20
+ declare function orgGuard(options?: OrgGuardOptions): RouteHandler;
21
+ /**
22
+ * Shorthand for requiring org context
23
+ */
24
+ declare function requireOrg(): RouteHandler;
25
+ /**
26
+ * Require org context with specific roles
27
+ */
28
+ declare function requireOrgRole(...roles: string[]): RouteHandler;
29
+ //#endregion
30
+ //#region src/org/orgMembership.d.ts
31
+ interface OrgMembershipOptions {
32
+ /** Path to user's organizations array */
33
+ userOrgsPath?: string;
34
+ /** Optional DB lookup function */
35
+ validateFromDb?: (userId: string, orgId: string) => Promise<boolean>;
36
+ }
37
+ interface OrgRolesOptions {
38
+ /** Path to user's organizations array */
39
+ userOrgsPath?: string;
40
+ }
41
+ /**
42
+ * Check if user is member of organization.
43
+ * This is a low-level utility for checking membership from user object data.
44
+ * For request-level checks, use `request.scope` (isMember/isElevated guards).
45
+ */
46
+ declare function orgMembershipCheck(user: UserBase | undefined | null, orgId: string | undefined | null, options?: OrgMembershipOptions): Promise<boolean>;
47
+ /**
48
+ * Get user's role in organization from user object data.
49
+ * For request-level role checks, use `request.scope.orgRoles` (when scope is 'member').
50
+ */
51
+ declare function getUserOrgRoles(user: UserBase | undefined | null, orgId: string | undefined | null, options?: OrgRolesOptions): string[];
52
+ /**
53
+ * Check if user has specific role in organization from user object data.
54
+ * For request-level role checks, use `requireOrgRole()` permission or `request.scope`.
55
+ */
56
+ declare function hasOrgRole(user: UserBase | undefined | null, orgId: string | undefined | null, roles: string | string[], options?: OrgRolesOptions): boolean;
57
+ //#endregion
58
+ //#region src/org/organizationPlugin.d.ts
59
+ declare module 'fastify' {
60
+ interface FastifyInstance {
61
+ /** Middleware: require the caller to hold one of the listed org roles */
62
+ requireOrgRole: (roles: string[]) => RouteHandlerMethod;
63
+ }
64
+ }
65
+ declare const organizationPlugin: FastifyPluginAsync<OrganizationPluginOptions>;
66
+ declare const _default: FastifyPluginAsync<OrganizationPluginOptions>;
67
+ //#endregion
68
+ export { type InvitationAdapter, type InvitationDoc, type MemberDoc, type OrgAdapter, type OrgDoc, type OrgGuardOptions, type OrgMembershipOptions, type OrgPermissionStatement, type OrgRole, type OrgRolesOptions, type OrganizationPluginOptions, getUserOrgRoles, hasOrgRole, orgGuard, orgMembershipCheck, _default as organizationPlugin, organizationPlugin as organizationPluginFn, requireOrg, requireOrgRole };