@classytic/arc 1.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -794
- package/bin/arc.js +91 -52
- package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
- package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
- package/dist/HookSystem-BsGV-j2l.mjs +405 -0
- package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.mjs +3 -0
- package/dist/audit/index.d.mts +82 -0
- package/dist/audit/index.d.mts.map +1 -0
- package/dist/audit/index.mjs +276 -0
- package/dist/audit/index.mjs.map +1 -0
- package/dist/audit/mongodb.d.mts +5 -0
- package/dist/audit/mongodb.mjs +3 -0
- package/dist/audited-C3T5DTUx.mjs +141 -0
- package/dist/audited-C3T5DTUx.mjs.map +1 -0
- package/dist/auth/index.d.mts +189 -0
- package/dist/auth/index.d.mts.map +1 -0
- package/dist/auth/index.mjs +1102 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth/redis-session.d.mts +44 -0
- package/dist/auth/redis-session.d.mts.map +1 -0
- package/dist/auth/redis-session.mjs +76 -0
- package/dist/auth/redis-session.mjs.map +1 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
- package/dist/cache/index.d.mts +146 -0
- package/dist/cache/index.d.mts.map +1 -0
- package/dist/cache/index.mjs +92 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/caching-Bl28lYsR.mjs +94 -0
- package/dist/caching-Bl28lYsR.mjs.map +1 -0
- package/dist/chunk-C7Uep-_p.mjs +20 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
- package/dist/cli/commands/describe.d.mts +19 -0
- package/dist/cli/commands/describe.d.mts.map +1 -0
- package/dist/cli/commands/describe.mjs +239 -0
- package/dist/cli/commands/describe.mjs.map +1 -0
- package/dist/cli/commands/docs.d.mts +14 -0
- package/dist/cli/commands/docs.d.mts.map +1 -0
- package/dist/cli/commands/docs.mjs +53 -0
- package/dist/cli/commands/docs.mjs.map +1 -0
- package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
- package/dist/cli/commands/generate.d.mts.map +1 -0
- package/dist/cli/commands/generate.mjs +358 -0
- package/dist/cli/commands/generate.mjs.map +1 -0
- package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
- package/dist/cli/commands/init.d.mts.map +1 -0
- package/dist/cli/commands/{init.js → init.mjs} +807 -616
- package/dist/cli/commands/init.mjs.map +1 -0
- package/dist/cli/commands/introspect.d.mts +11 -0
- package/dist/cli/commands/introspect.d.mts.map +1 -0
- package/dist/cli/commands/introspect.mjs +76 -0
- package/dist/cli/commands/introspect.mjs.map +1 -0
- package/dist/cli/index.d.mts +17 -0
- package/dist/cli/index.d.mts.map +1 -0
- package/dist/cli/index.mjs +157 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/constants-DdXFXQtN.mjs +85 -0
- package/dist/constants-DdXFXQtN.mjs.map +1 -0
- package/dist/core/index.d.mts +5 -0
- package/dist/core/index.mjs +4 -0
- package/dist/createApp-CUgNqegw.mjs +560 -0
- package/dist/createApp-CUgNqegw.mjs.map +1 -0
- package/dist/defineResource-k0_BDn8v.mjs +2197 -0
- package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
- package/dist/discovery/index.d.mts +47 -0
- package/dist/discovery/index.d.mts.map +1 -0
- package/dist/discovery/index.mjs +110 -0
- package/dist/discovery/index.mjs.map +1 -0
- package/dist/docs/index.d.mts +163 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +73 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/elevation-BRy3yFWT.mjs +113 -0
- package/dist/elevation-BRy3yFWT.mjs.map +1 -0
- package/dist/elevation-B_2dRLVP.d.mts +88 -0
- package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
- package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
- package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
- package/dist/errorHandler-C1okiriz.mjs +109 -0
- package/dist/errorHandler-C1okiriz.mjs.map +1 -0
- package/dist/errors-B9bZok84.mjs +212 -0
- package/dist/errors-B9bZok84.mjs.map +1 -0
- package/dist/errors-ChKiFz62.d.mts +125 -0
- package/dist/errors-ChKiFz62.d.mts.map +1 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
- package/dist/eventPlugin-DGR_B2on.mjs +230 -0
- package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
- package/dist/events/index.d.mts +54 -0
- package/dist/events/index.d.mts.map +1 -0
- package/dist/events/index.mjs +52 -0
- package/dist/events/index.mjs.map +1 -0
- package/dist/events/transports/redis-stream-entry.d.mts +2 -0
- package/dist/events/transports/redis-stream-entry.mjs +178 -0
- package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
- package/dist/events/transports/redis.d.mts +77 -0
- package/dist/events/transports/redis.d.mts.map +1 -0
- package/dist/events/transports/redis.mjs +125 -0
- package/dist/events/transports/redis.mjs.map +1 -0
- package/dist/externalPaths-DlINfKbP.d.mts +51 -0
- package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
- package/dist/factory/index.d.mts +64 -0
- package/dist/factory/index.d.mts.map +1 -0
- package/dist/factory/index.mjs +3 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
- package/dist/fields-DyaDVX4J.d.mts +110 -0
- package/dist/fields-DyaDVX4J.d.mts.map +1 -0
- package/dist/fields-iagOozy0.mjs +115 -0
- package/dist/fields-iagOozy0.mjs.map +1 -0
- package/dist/hooks/index.d.mts +4 -0
- package/dist/hooks/index.mjs +3 -0
- package/dist/idempotency/index.d.mts +97 -0
- package/dist/idempotency/index.d.mts.map +1 -0
- package/dist/idempotency/index.mjs +320 -0
- package/dist/idempotency/index.mjs.map +1 -0
- package/dist/idempotency/mongodb.d.mts +2 -0
- package/dist/idempotency/mongodb.mjs +115 -0
- package/dist/idempotency/mongodb.mjs.map +1 -0
- package/dist/idempotency/redis.d.mts +2 -0
- package/dist/idempotency/redis.mjs +104 -0
- package/dist/idempotency/redis.mjs.map +1 -0
- package/dist/index.d.mts +261 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +105 -0
- package/dist/index.mjs.map +1 -0
- package/dist/integrations/event-gateway.d.mts +47 -0
- package/dist/integrations/event-gateway.d.mts.map +1 -0
- package/dist/integrations/event-gateway.mjs +44 -0
- package/dist/integrations/event-gateway.mjs.map +1 -0
- package/dist/integrations/index.d.mts +5 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs.d.mts +104 -0
- package/dist/integrations/jobs.d.mts.map +1 -0
- package/dist/integrations/jobs.mjs +124 -0
- package/dist/integrations/jobs.mjs.map +1 -0
- package/dist/integrations/streamline.d.mts +61 -0
- package/dist/integrations/streamline.d.mts.map +1 -0
- package/dist/integrations/streamline.mjs +126 -0
- package/dist/integrations/streamline.mjs.map +1 -0
- package/dist/integrations/websocket.d.mts +83 -0
- package/dist/integrations/websocket.d.mts.map +1 -0
- package/dist/integrations/websocket.mjs +289 -0
- package/dist/integrations/websocket.mjs.map +1 -0
- package/dist/interface-B01JvPVc.d.mts +78 -0
- package/dist/interface-B01JvPVc.d.mts.map +1 -0
- package/dist/interface-CZe8IkMf.d.mts +55 -0
- package/dist/interface-CZe8IkMf.d.mts.map +1 -0
- package/dist/interface-Ch8HU9uM.d.mts +1098 -0
- package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
- package/dist/keys-BqNejWup.mjs +43 -0
- package/dist/keys-BqNejWup.mjs.map +1 -0
- package/dist/logger-Df2O2WsW.mjs +79 -0
- package/dist/logger-Df2O2WsW.mjs.map +1 -0
- package/dist/memory-cQgelFOj.mjs +144 -0
- package/dist/memory-cQgelFOj.mjs.map +1 -0
- package/dist/migrations/index.d.mts +157 -0
- package/dist/migrations/index.d.mts.map +1 -0
- package/dist/migrations/index.mjs +261 -0
- package/dist/migrations/index.mjs.map +1 -0
- package/dist/mongodb-BfJVlUJH.mjs +94 -0
- package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
- package/dist/mongodb-CGzRbfAK.d.mts +119 -0
- package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
- package/dist/mongodb-JN-9JA7K.d.mts +72 -0
- package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
- package/dist/openapi-G3Cw7XuM.mjs +524 -0
- package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
- package/dist/org/index.d.mts +69 -0
- package/dist/org/index.d.mts.map +1 -0
- package/dist/org/index.mjs +514 -0
- package/dist/org/index.mjs.map +1 -0
- package/dist/org/types.d.mts +83 -0
- package/dist/org/types.d.mts.map +1 -0
- package/dist/org/types.mjs +1 -0
- package/dist/permissions/index.d.mts +279 -0
- package/dist/permissions/index.d.mts.map +1 -0
- package/dist/permissions/index.mjs +579 -0
- package/dist/permissions/index.mjs.map +1 -0
- package/dist/plugins/index.d.mts +173 -0
- package/dist/plugins/index.d.mts.map +1 -0
- package/dist/plugins/index.mjs +523 -0
- package/dist/plugins/index.mjs.map +1 -0
- package/dist/plugins/response-cache.d.mts +88 -0
- package/dist/plugins/response-cache.d.mts.map +1 -0
- package/dist/plugins/response-cache.mjs +284 -0
- package/dist/plugins/response-cache.mjs.map +1 -0
- package/dist/plugins/tracing-entry.d.mts +2 -0
- package/dist/plugins/tracing-entry.mjs +186 -0
- package/dist/plugins/tracing-entry.mjs.map +1 -0
- package/dist/pluralize-CEweyOEm.mjs +87 -0
- package/dist/pluralize-CEweyOEm.mjs.map +1 -0
- package/dist/policies/{index.d.ts → index.d.mts} +204 -169
- package/dist/policies/index.d.mts.map +1 -0
- package/dist/policies/index.mjs +322 -0
- package/dist/policies/index.mjs.map +1 -0
- package/dist/presets/{index.d.ts → index.d.mts} +63 -131
- package/dist/presets/index.d.mts.map +1 -0
- package/dist/presets/index.mjs +144 -0
- package/dist/presets/index.mjs.map +1 -0
- package/dist/presets/multiTenant.d.mts +25 -0
- package/dist/presets/multiTenant.d.mts.map +1 -0
- package/dist/presets/multiTenant.mjs +114 -0
- package/dist/presets/multiTenant.mjs.map +1 -0
- package/dist/presets-BITljm96.mjs +120 -0
- package/dist/presets-BITljm96.mjs.map +1 -0
- package/dist/presets-DzSMwlKj.d.mts +58 -0
- package/dist/presets-DzSMwlKj.d.mts.map +1 -0
- package/dist/prisma-DJbMt3yf.mjs +628 -0
- package/dist/prisma-DJbMt3yf.mjs.map +1 -0
- package/dist/prisma-Dg9GoVdj.d.mts +275 -0
- package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
- package/dist/redis-D-JAeLtm.d.mts +50 -0
- package/dist/redis-D-JAeLtm.d.mts.map +1 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
- package/dist/registry/index.d.mts +12 -0
- package/dist/registry/index.d.mts.map +1 -0
- package/dist/registry/index.mjs +4 -0
- package/dist/requestContext-QQD6ROJc.mjs +56 -0
- package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
- package/dist/schemaConverter-BwrmWroW.mjs +99 -0
- package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
- package/dist/schemas/index.d.mts +64 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +83 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/scope/index.d.mts +22 -0
- package/dist/scope/index.d.mts.map +1 -0
- package/dist/scope/index.mjs +66 -0
- package/dist/scope/index.mjs.map +1 -0
- package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
- package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
- package/dist/sse-B3c3_yZp.mjs +124 -0
- package/dist/sse-B3c3_yZp.mjs.map +1 -0
- package/dist/testing/index.d.mts +908 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +1977 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/tracing-Cc7vVQPp.d.mts +71 -0
- package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
- package/dist/typeGuards-DhMNLuvU.mjs +10 -0
- package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
- package/dist/types/index.d.mts +947 -0
- package/dist/types/index.d.mts.map +1 -0
- package/dist/types/index.mjs +15 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/types-Beqn1Un7.mjs +39 -0
- package/dist/types-Beqn1Un7.mjs.map +1 -0
- package/dist/types-CIgB7UUl.d.mts +446 -0
- package/dist/types-CIgB7UUl.d.mts.map +1 -0
- package/dist/types-aYB4V7uN.d.mts +87 -0
- package/dist/types-aYB4V7uN.d.mts.map +1 -0
- package/dist/utils/index.d.mts +748 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +6 -0
- package/package.json +194 -68
- package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
- package/dist/adapters/index.d.ts +0 -237
- package/dist/adapters/index.js +0 -668
- package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
- package/dist/audit/index.d.ts +0 -195
- package/dist/audit/index.js +0 -319
- package/dist/auth/index.d.ts +0 -47
- package/dist/auth/index.js +0 -174
- package/dist/cli/commands/docs.d.ts +0 -11
- package/dist/cli/commands/docs.js +0 -474
- package/dist/cli/commands/generate.js +0 -334
- package/dist/cli/commands/introspect.d.ts +0 -8
- package/dist/cli/commands/introspect.js +0 -338
- package/dist/cli/index.d.ts +0 -4
- package/dist/cli/index.js +0 -3269
- package/dist/core/index.d.ts +0 -220
- package/dist/core/index.js +0 -2786
- package/dist/createApp-Ce9wl8W9.d.ts +0 -77
- package/dist/docs/index.d.ts +0 -166
- package/dist/docs/index.js +0 -658
- package/dist/errors-8WIxGS_6.d.ts +0 -122
- package/dist/events/index.d.ts +0 -117
- package/dist/events/index.js +0 -89
- package/dist/factory/index.d.ts +0 -38
- package/dist/factory/index.js +0 -1652
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -199
- package/dist/idempotency/index.d.ts +0 -323
- package/dist/idempotency/index.js +0 -500
- package/dist/index-B4t03KQ0.d.ts +0 -1366
- package/dist/index.d.ts +0 -135
- package/dist/index.js +0 -4756
- package/dist/migrations/index.d.ts +0 -185
- package/dist/migrations/index.js +0 -274
- package/dist/org/index.d.ts +0 -129
- package/dist/org/index.js +0 -220
- package/dist/permissions/index.d.ts +0 -144
- package/dist/permissions/index.js +0 -103
- package/dist/plugins/index.d.ts +0 -46
- package/dist/plugins/index.js +0 -1069
- package/dist/policies/index.js +0 -196
- package/dist/presets/index.js +0 -384
- package/dist/presets/multiTenant.d.ts +0 -39
- package/dist/presets/multiTenant.js +0 -112
- package/dist/registry/index.d.ts +0 -16
- package/dist/registry/index.js +0 -253
- package/dist/testing/index.d.ts +0 -618
- package/dist/testing/index.js +0 -48020
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.js +0 -8
- package/dist/types-B99TBmFV.d.ts +0 -76
- package/dist/types-BvckRbs2.d.ts +0 -143
- package/dist/utils/index.d.ts +0 -679
- package/dist/utils/index.js +0 -931
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { n as convertRouteSchema } from "./schemaConverter-BwrmWroW.mjs";
|
|
2
|
+
import fp from "fastify-plugin";
|
|
3
|
+
|
|
4
|
+
//#region src/docs/openapi.ts
|
|
5
|
+
/**
|
|
6
|
+
* OpenAPI Spec Generator
|
|
7
|
+
*
|
|
8
|
+
* Auto-generates OpenAPI 3.0 specification from Arc resource registry.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { openApiPlugin } from '@classytic/arc/docs';
|
|
12
|
+
*
|
|
13
|
+
* await fastify.register(openApiPlugin, {
|
|
14
|
+
* title: 'My API',
|
|
15
|
+
* version: '1.0.0',
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // Spec available at /_docs/openapi.json
|
|
19
|
+
*/
|
|
20
|
+
const openApiPlugin = async (fastify, opts = {}) => {
|
|
21
|
+
const { title = "Arc API", version = "1.0.0", description, serverUrl, prefix = "/_docs", apiPrefix = "", authRoles = [] } = opts;
|
|
22
|
+
const buildSpec = () => {
|
|
23
|
+
const arc = fastify.arc;
|
|
24
|
+
const resources = arc?.registry?.getAll() ?? [];
|
|
25
|
+
const externalPaths = arc?.externalOpenApiPaths ?? [];
|
|
26
|
+
return buildOpenApiSpec(resources, {
|
|
27
|
+
title,
|
|
28
|
+
version,
|
|
29
|
+
description,
|
|
30
|
+
serverUrl,
|
|
31
|
+
apiPrefix
|
|
32
|
+
}, externalPaths.length > 0 ? externalPaths : void 0);
|
|
33
|
+
};
|
|
34
|
+
fastify.get(`${prefix}/openapi.json`, async (request, reply) => {
|
|
35
|
+
if (authRoles.length > 0) {
|
|
36
|
+
const user = request.user;
|
|
37
|
+
if (!authRoles.some((role) => user?.roles?.includes(role)) && !user?.roles?.includes("superadmin")) {
|
|
38
|
+
reply.code(403).send({ error: "Access denied" });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return buildSpec();
|
|
43
|
+
});
|
|
44
|
+
fastify.log?.debug?.(`OpenAPI spec available at ${prefix}/openapi.json`);
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Build OpenAPI spec from registry resources.
|
|
48
|
+
* Shared by HTTP docs endpoint and CLI export command.
|
|
49
|
+
*/
|
|
50
|
+
function buildOpenApiSpec(resources, options = {}, externalPaths) {
|
|
51
|
+
const { title = "Arc API", version = "1.0.0", description, serverUrl, apiPrefix = "" } = options;
|
|
52
|
+
const paths = {};
|
|
53
|
+
const tags = [];
|
|
54
|
+
const additionalSecurity = externalPaths?.flatMap((ext) => ext.resourceSecurity ?? []) ?? [];
|
|
55
|
+
for (const resource of resources) {
|
|
56
|
+
const tagDescParts = [`${resource.displayName || resource.name} operations`];
|
|
57
|
+
if (resource.presets && resource.presets.length > 0) tagDescParts.push(`Presets: ${resource.presets.join(", ")}`);
|
|
58
|
+
if (resource.pipelineSteps && resource.pipelineSteps.length > 0) {
|
|
59
|
+
const stepNames = resource.pipelineSteps.map((s) => `${s.type}(${s.name})`);
|
|
60
|
+
tagDescParts.push(`Pipeline: ${stepNames.join(" → ")}`);
|
|
61
|
+
}
|
|
62
|
+
if (resource.events && resource.events.length > 0) tagDescParts.push(`Events: ${resource.events.join(", ")}`);
|
|
63
|
+
tags.push({
|
|
64
|
+
name: resource.tag || resource.name,
|
|
65
|
+
description: tagDescParts.join(". ")
|
|
66
|
+
});
|
|
67
|
+
const resourcePaths = generateResourcePaths(resource, apiPrefix, additionalSecurity);
|
|
68
|
+
Object.assign(paths, resourcePaths);
|
|
69
|
+
}
|
|
70
|
+
if (externalPaths) for (const ext of externalPaths) {
|
|
71
|
+
for (const [path, methods] of Object.entries(ext.paths)) paths[path] = paths[path] ? {
|
|
72
|
+
...paths[path],
|
|
73
|
+
...methods
|
|
74
|
+
} : methods;
|
|
75
|
+
if (ext.tags) {
|
|
76
|
+
for (const tag of ext.tags) if (!tags.find((t) => t.name === tag.name)) tags.push(tag);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const externalSecuritySchemes = externalPaths?.reduce((acc, ext) => ({
|
|
80
|
+
...acc,
|
|
81
|
+
...ext.securitySchemes
|
|
82
|
+
}), {}) ?? {};
|
|
83
|
+
const externalSchemas = externalPaths?.reduce((acc, ext) => ({
|
|
84
|
+
...acc,
|
|
85
|
+
...ext.schemas
|
|
86
|
+
}), {}) ?? {};
|
|
87
|
+
return {
|
|
88
|
+
openapi: "3.0.3",
|
|
89
|
+
info: {
|
|
90
|
+
title,
|
|
91
|
+
version,
|
|
92
|
+
...description && { description }
|
|
93
|
+
},
|
|
94
|
+
...serverUrl && { servers: [{ url: serverUrl }] },
|
|
95
|
+
paths,
|
|
96
|
+
components: {
|
|
97
|
+
schemas: {
|
|
98
|
+
...generateSchemas(resources),
|
|
99
|
+
...externalSchemas
|
|
100
|
+
},
|
|
101
|
+
securitySchemes: {
|
|
102
|
+
bearerAuth: {
|
|
103
|
+
type: "http",
|
|
104
|
+
scheme: "bearer",
|
|
105
|
+
bearerFormat: "JWT"
|
|
106
|
+
},
|
|
107
|
+
orgHeader: {
|
|
108
|
+
type: "apiKey",
|
|
109
|
+
in: "header",
|
|
110
|
+
name: "x-organization-id"
|
|
111
|
+
},
|
|
112
|
+
...externalSecuritySchemes
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
tags
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Convert Fastify-style params (/:id) to OpenAPI-style params (/{id})
|
|
120
|
+
*/
|
|
121
|
+
function toOpenApiPath(path) {
|
|
122
|
+
return path.replace(/:([^/]+)/g, "{$1}");
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Convert OpenAPI schema to query parameters array
|
|
126
|
+
* Transforms { properties: { page: { type: 'integer' } } } to [{ name: 'page', in: 'query', schema: { type: 'integer' } }]
|
|
127
|
+
*/
|
|
128
|
+
function convertSchemaToParameters(schema) {
|
|
129
|
+
const params = [];
|
|
130
|
+
const properties = schema.properties || {};
|
|
131
|
+
const required = schema.required || [];
|
|
132
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
133
|
+
const description = prop.description;
|
|
134
|
+
const { description: _, ...schemaProps } = prop;
|
|
135
|
+
const param = {
|
|
136
|
+
name,
|
|
137
|
+
in: "query",
|
|
138
|
+
required: required.includes(name),
|
|
139
|
+
schema: schemaProps
|
|
140
|
+
};
|
|
141
|
+
if (description) param.description = description;
|
|
142
|
+
params.push(param);
|
|
143
|
+
}
|
|
144
|
+
return params;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Default query parameters when no listQuery schema is provided
|
|
148
|
+
*/
|
|
149
|
+
const DEFAULT_LIST_PARAMS = [
|
|
150
|
+
{
|
|
151
|
+
name: "page",
|
|
152
|
+
in: "query",
|
|
153
|
+
schema: { type: "integer" },
|
|
154
|
+
description: "Page number"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "limit",
|
|
158
|
+
in: "query",
|
|
159
|
+
schema: { type: "integer" },
|
|
160
|
+
description: "Items per page"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "sort",
|
|
164
|
+
in: "query",
|
|
165
|
+
schema: { type: "string" },
|
|
166
|
+
description: "Sort field (prefix with - for descending)"
|
|
167
|
+
}
|
|
168
|
+
];
|
|
169
|
+
/**
|
|
170
|
+
* Generate paths for a resource
|
|
171
|
+
*/
|
|
172
|
+
function generateResourcePaths(resource, apiPrefix = "", additionalSecurity = []) {
|
|
173
|
+
const paths = {};
|
|
174
|
+
const basePath = `${apiPrefix}${resource.prefix}`;
|
|
175
|
+
if (resource.disableDefaultRoutes && (!resource.additionalRoutes || resource.additionalRoutes.length === 0)) return paths;
|
|
176
|
+
if (!resource.disableDefaultRoutes) {
|
|
177
|
+
const disabledSet = new Set(resource.disabledRoutes ?? []);
|
|
178
|
+
const updateMethod = resource.updateMethod ?? "PATCH";
|
|
179
|
+
const collectionPath = {};
|
|
180
|
+
if (!disabledSet.has("list")) collectionPath.get = createOperation(resource, "list", "List all", {
|
|
181
|
+
parameters: resource.openApiSchemas?.listQuery ? convertSchemaToParameters(resource.openApiSchemas.listQuery) : DEFAULT_LIST_PARAMS,
|
|
182
|
+
responses: { "200": {
|
|
183
|
+
description: "List of items",
|
|
184
|
+
content: { "application/json": { schema: {
|
|
185
|
+
type: "object",
|
|
186
|
+
properties: {
|
|
187
|
+
success: { type: "boolean" },
|
|
188
|
+
docs: {
|
|
189
|
+
type: "array",
|
|
190
|
+
items: { $ref: `#/components/schemas/${resource.name}` }
|
|
191
|
+
},
|
|
192
|
+
page: { type: "integer" },
|
|
193
|
+
limit: { type: "integer" },
|
|
194
|
+
total: { type: "integer" },
|
|
195
|
+
pages: { type: "integer" },
|
|
196
|
+
hasNext: { type: "boolean" },
|
|
197
|
+
hasPrev: { type: "boolean" }
|
|
198
|
+
}
|
|
199
|
+
} } }
|
|
200
|
+
} }
|
|
201
|
+
}, void 0, additionalSecurity);
|
|
202
|
+
if (!disabledSet.has("create")) collectionPath.post = createOperation(resource, "create", "Create new", {
|
|
203
|
+
requestBody: {
|
|
204
|
+
required: true,
|
|
205
|
+
content: { "application/json": { schema: { $ref: `#/components/schemas/${resource.name}Input` } } }
|
|
206
|
+
},
|
|
207
|
+
responses: { "201": {
|
|
208
|
+
description: "Created successfully",
|
|
209
|
+
content: { "application/json": { schema: {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
success: { type: "boolean" },
|
|
213
|
+
data: { $ref: `#/components/schemas/${resource.name}` },
|
|
214
|
+
message: { type: "string" }
|
|
215
|
+
}
|
|
216
|
+
} } }
|
|
217
|
+
} }
|
|
218
|
+
}, void 0, additionalSecurity);
|
|
219
|
+
if (Object.keys(collectionPath).length > 0) paths[basePath] = collectionPath;
|
|
220
|
+
const itemPath = {};
|
|
221
|
+
if (!disabledSet.has("get")) itemPath.get = createOperation(resource, "get", "Get by ID", {
|
|
222
|
+
parameters: [{
|
|
223
|
+
name: "id",
|
|
224
|
+
in: "path",
|
|
225
|
+
required: true,
|
|
226
|
+
schema: { type: "string" }
|
|
227
|
+
}],
|
|
228
|
+
responses: {
|
|
229
|
+
"200": {
|
|
230
|
+
description: "Item found",
|
|
231
|
+
content: { "application/json": { schema: {
|
|
232
|
+
type: "object",
|
|
233
|
+
properties: {
|
|
234
|
+
success: { type: "boolean" },
|
|
235
|
+
data: { $ref: `#/components/schemas/${resource.name}` }
|
|
236
|
+
}
|
|
237
|
+
} } }
|
|
238
|
+
},
|
|
239
|
+
"404": { description: "Not found" }
|
|
240
|
+
}
|
|
241
|
+
}, void 0, additionalSecurity);
|
|
242
|
+
if (!disabledSet.has("update")) {
|
|
243
|
+
const updateOp = createOperation(resource, "update", "Update", {
|
|
244
|
+
parameters: [{
|
|
245
|
+
name: "id",
|
|
246
|
+
in: "path",
|
|
247
|
+
required: true,
|
|
248
|
+
schema: { type: "string" }
|
|
249
|
+
}],
|
|
250
|
+
requestBody: {
|
|
251
|
+
required: true,
|
|
252
|
+
content: { "application/json": { schema: { $ref: `#/components/schemas/${resource.name}Input` } } }
|
|
253
|
+
},
|
|
254
|
+
responses: { "200": {
|
|
255
|
+
description: "Updated successfully",
|
|
256
|
+
content: { "application/json": { schema: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
success: { type: "boolean" },
|
|
260
|
+
data: { $ref: `#/components/schemas/${resource.name}` },
|
|
261
|
+
message: { type: "string" }
|
|
262
|
+
}
|
|
263
|
+
} } }
|
|
264
|
+
} }
|
|
265
|
+
}, void 0, additionalSecurity);
|
|
266
|
+
if (updateMethod === "both") {
|
|
267
|
+
itemPath.put = updateOp;
|
|
268
|
+
itemPath.patch = updateOp;
|
|
269
|
+
} else if (updateMethod === "PUT") itemPath.put = updateOp;
|
|
270
|
+
else itemPath.patch = updateOp;
|
|
271
|
+
}
|
|
272
|
+
if (!disabledSet.has("delete")) itemPath.delete = createOperation(resource, "delete", "Delete", {
|
|
273
|
+
parameters: [{
|
|
274
|
+
name: "id",
|
|
275
|
+
in: "path",
|
|
276
|
+
required: true,
|
|
277
|
+
schema: { type: "string" }
|
|
278
|
+
}],
|
|
279
|
+
responses: { "200": {
|
|
280
|
+
description: "Deleted successfully",
|
|
281
|
+
content: { "application/json": { schema: {
|
|
282
|
+
type: "object",
|
|
283
|
+
properties: {
|
|
284
|
+
success: { type: "boolean" },
|
|
285
|
+
message: { type: "string" }
|
|
286
|
+
}
|
|
287
|
+
} } }
|
|
288
|
+
} }
|
|
289
|
+
}, void 0, additionalSecurity);
|
|
290
|
+
if (Object.keys(itemPath).length > 0) paths[toOpenApiPath(`${basePath}/:id`)] = itemPath;
|
|
291
|
+
}
|
|
292
|
+
for (const route of resource.additionalRoutes || []) {
|
|
293
|
+
const fullPath = toOpenApiPath(`${basePath}${route.path}`);
|
|
294
|
+
const method = route.method.toLowerCase();
|
|
295
|
+
if (!paths[fullPath]) paths[fullPath] = {};
|
|
296
|
+
const handlerName = route.operation ?? (typeof route.handler === "string" ? route.handler : "handler");
|
|
297
|
+
const isPublicRoute = route.permissions?._isPublic === true;
|
|
298
|
+
const requiresAuthForRoute = !!route.permissions && !isPublicRoute;
|
|
299
|
+
const extras = {
|
|
300
|
+
parameters: extractPathParams(route.path),
|
|
301
|
+
responses: { "200": { description: route.description || "Success" } }
|
|
302
|
+
};
|
|
303
|
+
const rawSchema = route.schema;
|
|
304
|
+
const routeSchema = rawSchema ? convertRouteSchema(rawSchema) : void 0;
|
|
305
|
+
if (routeSchema?.body && [
|
|
306
|
+
"post",
|
|
307
|
+
"put",
|
|
308
|
+
"patch"
|
|
309
|
+
].includes(method)) extras.requestBody = {
|
|
310
|
+
required: true,
|
|
311
|
+
content: { "application/json": { schema: routeSchema.body } }
|
|
312
|
+
};
|
|
313
|
+
if (routeSchema?.querystring) {
|
|
314
|
+
const queryParams = convertSchemaToParameters(routeSchema.querystring);
|
|
315
|
+
extras.parameters = [...extras.parameters || [], ...queryParams];
|
|
316
|
+
}
|
|
317
|
+
if (routeSchema?.response) {
|
|
318
|
+
const responseSchemas = routeSchema.response;
|
|
319
|
+
for (const [statusCode, schema] of Object.entries(responseSchemas)) extras.responses[statusCode] = {
|
|
320
|
+
description: schema.description || `Response ${statusCode}`,
|
|
321
|
+
content: { "application/json": { schema } }
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
paths[fullPath][method] = createOperation(resource, handlerName, route.summary ?? handlerName, extras, requiresAuthForRoute, additionalSecurity);
|
|
325
|
+
}
|
|
326
|
+
return paths;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Create an operation object
|
|
330
|
+
* @param requiresAuthOverride - Override for whether auth is required (for additional routes)
|
|
331
|
+
* @param additionalSecurity - Extra security alternatives from external integrations (OR'd with bearerAuth)
|
|
332
|
+
*/
|
|
333
|
+
function createOperation(resource, operation, summary, extras, requiresAuthOverride, additionalSecurity = []) {
|
|
334
|
+
const operationPermission = (resource.permissions || {})[operation];
|
|
335
|
+
const isPublic = operationPermission?._isPublic === true;
|
|
336
|
+
operationPermission?._roles;
|
|
337
|
+
const requiresAuth = requiresAuthOverride !== void 0 ? requiresAuthOverride : typeof operationPermission === "function" && !isPublic;
|
|
338
|
+
const permAnnotation = describePermissionForOpenApi(operationPermission);
|
|
339
|
+
const descParts = [];
|
|
340
|
+
if (permAnnotation) descParts.push(`**Permission**: ${permAnnotation.type === "public" ? "Public" : permAnnotation.type === "requireRoles" ? `Requires roles: ${(permAnnotation.roles ?? []).join(", ")}` : "Requires authentication"}`);
|
|
341
|
+
if (resource.presets && resource.presets.length > 0) descParts.push(`**Presets**: ${resource.presets.join(", ")}`);
|
|
342
|
+
const applicableSteps = (resource.pipelineSteps ?? []).filter((s) => {
|
|
343
|
+
if (!s.operations) return true;
|
|
344
|
+
return s.operations.includes(operation);
|
|
345
|
+
});
|
|
346
|
+
return {
|
|
347
|
+
tags: [resource.tag || "Resource"],
|
|
348
|
+
summary: `${summary} ${(resource.displayName || resource.name).toLowerCase()}`,
|
|
349
|
+
operationId: `${resource.name}_${operation}`,
|
|
350
|
+
...descParts.length > 0 && { description: descParts.join("\n\n") },
|
|
351
|
+
...requiresAuth && { security: [{ bearerAuth: [] }, ...additionalSecurity] },
|
|
352
|
+
...permAnnotation && { "x-arc-permission": permAnnotation },
|
|
353
|
+
...applicableSteps.length > 0 && { "x-arc-pipeline": applicableSteps.map((s) => ({
|
|
354
|
+
type: s.type,
|
|
355
|
+
name: s.name
|
|
356
|
+
})) },
|
|
357
|
+
responses: {
|
|
358
|
+
...requiresAuth && {
|
|
359
|
+
"401": {
|
|
360
|
+
description: "Authentication required — no valid Bearer token provided",
|
|
361
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
|
|
362
|
+
},
|
|
363
|
+
"403": {
|
|
364
|
+
description: permAnnotation?.roles ? `Forbidden — requires one of: ${permAnnotation.roles.join(", ")}` : "Forbidden — insufficient permissions",
|
|
365
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
"500": { description: "Internal server error" }
|
|
369
|
+
},
|
|
370
|
+
...extras
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Describe a permission check function for OpenAPI.
|
|
375
|
+
* Extracts role, org role, and team permission metadata from permission functions.
|
|
376
|
+
*/
|
|
377
|
+
function describePermissionForOpenApi(check) {
|
|
378
|
+
if (!check || typeof check !== "function") return void 0;
|
|
379
|
+
const fn = check;
|
|
380
|
+
if (fn._isPublic === true) return { type: "public" };
|
|
381
|
+
const result = { type: "requireAuth" };
|
|
382
|
+
if (Array.isArray(fn._roles) && fn._roles.length > 0) {
|
|
383
|
+
result.type = "requireRoles";
|
|
384
|
+
result.roles = fn._roles;
|
|
385
|
+
}
|
|
386
|
+
if (Array.isArray(fn._orgRoles) && fn._orgRoles.length > 0) result.orgRoles = fn._orgRoles;
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Extract path parameters from route path
|
|
391
|
+
*/
|
|
392
|
+
function extractPathParams(path) {
|
|
393
|
+
const params = [];
|
|
394
|
+
const matches = path.matchAll(/:([^/]+)/g);
|
|
395
|
+
for (const match of matches) {
|
|
396
|
+
const paramName = match[1];
|
|
397
|
+
if (paramName) params.push({
|
|
398
|
+
name: paramName,
|
|
399
|
+
in: "path",
|
|
400
|
+
required: true,
|
|
401
|
+
schema: { type: "string" }
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return params;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Generate schema definitions from pre-stored registry schemas.
|
|
408
|
+
* Schemas are generated at resource definition time and stored in the registry.
|
|
409
|
+
*
|
|
410
|
+
* Response schema priority:
|
|
411
|
+
* 1. If resource provides explicit `openApiSchemas.response`, use it as-is
|
|
412
|
+
* 2. Otherwise, auto-generate from `createBody` + _id + timestamps
|
|
413
|
+
*
|
|
414
|
+
* Note: This is for OpenAPI documentation only - does NOT affect Fastify serialization.
|
|
415
|
+
*/
|
|
416
|
+
function generateSchemas(resources) {
|
|
417
|
+
const schemas = { Error: {
|
|
418
|
+
type: "object",
|
|
419
|
+
properties: {
|
|
420
|
+
success: {
|
|
421
|
+
type: "boolean",
|
|
422
|
+
example: false
|
|
423
|
+
},
|
|
424
|
+
error: { type: "string" },
|
|
425
|
+
code: { type: "string" },
|
|
426
|
+
requestId: { type: "string" },
|
|
427
|
+
timestamp: { type: "string" }
|
|
428
|
+
}
|
|
429
|
+
} };
|
|
430
|
+
for (const resource of resources) {
|
|
431
|
+
const storedSchemas = resource.openApiSchemas;
|
|
432
|
+
const fieldPerms = resource.fieldPermissions;
|
|
433
|
+
if (storedSchemas?.response) schemas[resource.name] = {
|
|
434
|
+
type: "object",
|
|
435
|
+
description: resource.displayName,
|
|
436
|
+
...storedSchemas.response
|
|
437
|
+
};
|
|
438
|
+
else if (storedSchemas?.createBody) schemas[resource.name] = {
|
|
439
|
+
type: "object",
|
|
440
|
+
description: resource.displayName,
|
|
441
|
+
properties: {
|
|
442
|
+
_id: {
|
|
443
|
+
type: "string",
|
|
444
|
+
description: "Unique identifier"
|
|
445
|
+
},
|
|
446
|
+
...storedSchemas.createBody.properties ?? {},
|
|
447
|
+
createdAt: {
|
|
448
|
+
type: "string",
|
|
449
|
+
format: "date-time",
|
|
450
|
+
description: "Creation timestamp"
|
|
451
|
+
},
|
|
452
|
+
updatedAt: {
|
|
453
|
+
type: "string",
|
|
454
|
+
format: "date-time",
|
|
455
|
+
description: "Last update timestamp"
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
else schemas[resource.name] = {
|
|
460
|
+
type: "object",
|
|
461
|
+
description: resource.displayName,
|
|
462
|
+
properties: {
|
|
463
|
+
_id: {
|
|
464
|
+
type: "string",
|
|
465
|
+
description: "Unique identifier"
|
|
466
|
+
},
|
|
467
|
+
createdAt: {
|
|
468
|
+
type: "string",
|
|
469
|
+
format: "date-time",
|
|
470
|
+
description: "Creation timestamp"
|
|
471
|
+
},
|
|
472
|
+
updatedAt: {
|
|
473
|
+
type: "string",
|
|
474
|
+
format: "date-time",
|
|
475
|
+
description: "Last update timestamp"
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
if (fieldPerms && schemas[resource.name]?.properties) {
|
|
480
|
+
const props = schemas[resource.name].properties;
|
|
481
|
+
for (const [field, perm] of Object.entries(fieldPerms)) if (props[field]) {
|
|
482
|
+
const desc = props[field].description ?? "";
|
|
483
|
+
const permDesc = formatFieldPermDescription(perm);
|
|
484
|
+
props[field].description = desc ? `${desc} (${permDesc})` : permDesc;
|
|
485
|
+
} else if (perm.type === "hidden") {}
|
|
486
|
+
}
|
|
487
|
+
if (storedSchemas?.createBody) {
|
|
488
|
+
schemas[`${resource.name}Input`] = {
|
|
489
|
+
type: "object",
|
|
490
|
+
description: `${resource.displayName} create input`,
|
|
491
|
+
...storedSchemas.createBody
|
|
492
|
+
};
|
|
493
|
+
if (storedSchemas.updateBody) schemas[`${resource.name}Update`] = {
|
|
494
|
+
type: "object",
|
|
495
|
+
description: `${resource.displayName} update input`,
|
|
496
|
+
...storedSchemas.updateBody
|
|
497
|
+
};
|
|
498
|
+
} else schemas[`${resource.name}Input`] = {
|
|
499
|
+
type: "object",
|
|
500
|
+
description: `${resource.displayName} input`
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
return schemas;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Format a field permission description for OpenAPI
|
|
507
|
+
*/
|
|
508
|
+
function formatFieldPermDescription(perm) {
|
|
509
|
+
switch (perm.type) {
|
|
510
|
+
case "hidden": return "Hidden — never returned in responses";
|
|
511
|
+
case "visibleTo": return `Visible to: ${(perm.roles ?? []).join(", ")}`;
|
|
512
|
+
case "writableBy": return `Writable by: ${(perm.roles ?? []).join(", ")}`;
|
|
513
|
+
case "redactFor": return `Redacted for: ${(perm.roles ?? []).join(", ")}`;
|
|
514
|
+
default: return perm.type;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
var openapi_default = fp(openApiPlugin, {
|
|
518
|
+
name: "arc-openapi",
|
|
519
|
+
fastify: "5.x"
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
//#endregion
|
|
523
|
+
export { openApiPlugin as n, openapi_default as r, buildOpenApiSpec as t };
|
|
524
|
+
//# sourceMappingURL=openapi-G3Cw7XuM.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-G3Cw7XuM.mjs","names":[],"sources":["../src/docs/openapi.ts"],"sourcesContent":["/**\n * OpenAPI Spec Generator\n *\n * Auto-generates OpenAPI 3.0 specification from Arc resource registry.\n *\n * @example\n * import { openApiPlugin } from '@classytic/arc/docs';\n *\n * await fastify.register(openApiPlugin, {\n * title: 'My API',\n * version: '1.0.0',\n * });\n *\n * // Spec available at /_docs/openapi.json\n */\n\nimport fp from 'fastify-plugin';\nimport type { FastifyInstance, FastifyPluginAsync } from 'fastify';\nimport type { RegistryEntry, FastifyWithDecorators } from '../types/index.js';\nimport type { PermissionCheck } from '../permissions/types.js';\nimport type { ExternalOpenApiPaths } from './externalPaths.js';\nimport { convertRouteSchema } from '../utils/schemaConverter.js';\n\nexport interface OpenApiOptions {\n /** API title */\n title?: string;\n /** API version */\n version?: string;\n /** API description */\n description?: string;\n /** Server URL */\n serverUrl?: string;\n /** Route prefix for spec endpoint (default: '/_docs') */\n prefix?: string;\n /** API prefix for all resource paths (e.g., '/api/v1') */\n apiPrefix?: string;\n /** Auth roles required to access spec (default: [] = public) */\n authRoles?: string[];\n /** Include internal routes (default: false) */\n includeInternal?: boolean;\n /** Custom OpenAPI extensions */\n extensions?: Record<string, unknown>;\n}\n\nexport interface OpenApiSpec {\n openapi: string;\n info: {\n title: string;\n version: string;\n description?: string;\n };\n servers?: Array<{ url: string; description?: string }>;\n paths: Record<string, PathItem>;\n components: {\n schemas: Record<string, SchemaObject>;\n securitySchemes?: Record<string, SecurityScheme>;\n };\n tags: Array<{ name: string; description?: string }>;\n security?: Array<Record<string, string[]>>;\n}\n\nexport interface OpenApiBuildOptions {\n title?: string;\n version?: string;\n description?: string;\n serverUrl?: string;\n apiPrefix?: string;\n}\n\ninterface PathItem {\n get?: Operation;\n post?: Operation;\n put?: Operation;\n patch?: Operation;\n delete?: Operation;\n options?: Operation;\n head?: Operation;\n}\n\ninterface Operation {\n tags: string[];\n summary: string;\n description?: string;\n operationId: string;\n parameters?: Parameter[];\n requestBody?: RequestBody;\n responses: Record<string, Response>;\n security?: Array<Record<string, string[]>>;\n /** Arc permission metadata (OpenAPI extension) */\n 'x-arc-permission'?: { type: string; roles?: readonly string[] };\n /** Arc pipeline steps (OpenAPI extension) */\n 'x-arc-pipeline'?: Array<{ type: string; name: string }>;\n}\n\ninterface Parameter {\n name: string;\n in: 'path' | 'query' | 'header';\n required?: boolean;\n schema: SchemaObject;\n description?: string;\n}\n\ninterface RequestBody {\n required?: boolean;\n content: Record<string, { schema: SchemaObject }>;\n}\n\ninterface Response {\n description: string;\n content?: Record<string, { schema: SchemaObject }>;\n}\n\ninterface SchemaObject {\n type?: string;\n format?: string;\n properties?: Record<string, SchemaObject>;\n items?: SchemaObject;\n required?: string[];\n $ref?: string;\n description?: string;\n example?: unknown;\n additionalProperties?: boolean | SchemaObject;\n enum?: string[];\n minimum?: number;\n maximum?: number;\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n}\n\ninterface SecurityScheme {\n type: string;\n scheme?: string;\n bearerFormat?: string;\n in?: string;\n name?: string;\n}\n\nconst openApiPlugin: FastifyPluginAsync<OpenApiOptions> = async (\n fastify: FastifyInstance,\n opts: OpenApiOptions = {}\n) => {\n const {\n title = 'Arc API',\n version = '1.0.0',\n description,\n serverUrl,\n prefix = '/_docs',\n apiPrefix = '',\n authRoles = [],\n } = opts;\n\n // Build spec from instance-scoped registry\n const buildSpec = (): OpenApiSpec => {\n const arc = (fastify as unknown as FastifyWithDecorators).arc;\n const resources = arc?.registry?.getAll() ?? [];\n const externalPaths = arc?.externalOpenApiPaths ?? [];\n return buildOpenApiSpec(resources, {\n title,\n version,\n description,\n serverUrl,\n apiPrefix,\n }, externalPaths.length > 0 ? externalPaths : undefined);\n };\n\n // Serve OpenAPI spec\n fastify.get(`${prefix}/openapi.json`, async (request, reply) => {\n // Check auth if required\n if (authRoles.length > 0) {\n const user = (request as { user?: { roles?: string[] } }).user;\n const hasRole = authRoles.some((role) => user?.roles?.includes(role));\n if (!hasRole && !user?.roles?.includes('superadmin')) {\n reply.code(403).send({ error: 'Access denied' });\n return;\n }\n }\n\n const spec = buildSpec();\n // Return object directly - let Fastify handle serialization & compression\n return spec;\n });\n\n fastify.log?.debug?.(`OpenAPI spec available at ${prefix}/openapi.json`);\n};\n\n/**\n * Build OpenAPI spec from registry resources.\n * Shared by HTTP docs endpoint and CLI export command.\n */\nexport function buildOpenApiSpec(\n resources: RegistryEntry[],\n options: OpenApiBuildOptions = {},\n externalPaths?: ExternalOpenApiPaths[],\n): OpenApiSpec {\n const {\n title = 'Arc API',\n version = '1.0.0',\n description,\n serverUrl,\n apiPrefix = '',\n } = options;\n\n const paths: Record<string, PathItem> = {};\n const tags: Array<{ name: string; description?: string }> = [];\n\n // Collect additional security alternatives from external integrations.\n // Each item is OR'd with bearerAuth on authenticated resource operations.\n const additionalSecurity = externalPaths\n ?.flatMap(ext => ext.resourceSecurity ?? []) ?? [];\n\n for (const resource of resources) {\n // Build tag description with preset/pipeline info\n const tagDescParts = [`${resource.displayName || resource.name} operations`];\n if (resource.presets && resource.presets.length > 0) {\n tagDescParts.push(`Presets: ${resource.presets.join(', ')}`);\n }\n if (resource.pipelineSteps && resource.pipelineSteps.length > 0) {\n const stepNames = resource.pipelineSteps.map((s) => `${s.type}(${s.name})`);\n tagDescParts.push(`Pipeline: ${stepNames.join(' → ')}`);\n }\n if (resource.events && resource.events.length > 0) {\n tagDescParts.push(`Events: ${resource.events.join(', ')}`);\n }\n\n tags.push({\n name: resource.tag || resource.name,\n description: tagDescParts.join('. '),\n });\n\n const resourcePaths = generateResourcePaths(resource, apiPrefix, additionalSecurity);\n Object.assign(paths, resourcePaths);\n }\n\n // Merge external paths (Better Auth, custom integrations, etc.)\n if (externalPaths) {\n for (const ext of externalPaths) {\n for (const [path, methods] of Object.entries(ext.paths)) {\n paths[path] = paths[path]\n ? { ...paths[path], ...methods } as PathItem\n : methods as PathItem;\n }\n if (ext.tags) {\n for (const tag of ext.tags) {\n if (!tags.find((t) => t.name === tag.name)) {\n tags.push(tag);\n }\n }\n }\n }\n }\n\n // Merge external security schemes and schemas\n const externalSecuritySchemes = externalPaths\n ?.reduce<Record<string, Record<string, unknown>>>((acc, ext) => ({ ...acc, ...ext.securitySchemes }), {}) ?? {};\n const externalSchemas = externalPaths\n ?.reduce<Record<string, Record<string, unknown>>>((acc, ext) => ({ ...acc, ...ext.schemas }), {}) ?? {};\n\n return {\n openapi: '3.0.3',\n info: {\n title,\n version,\n ...(description && { description }),\n },\n ...(serverUrl && {\n servers: [{ url: serverUrl }],\n }),\n paths,\n components: {\n schemas: {\n ...generateSchemas(resources),\n ...externalSchemas,\n } as Record<string, SchemaObject>,\n securitySchemes: {\n bearerAuth: {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n },\n orgHeader: {\n type: 'apiKey',\n in: 'header',\n name: 'x-organization-id',\n },\n // Plugin-specific schemes (e.g. apiKeyAuth) are auto-detected\n // and injected via externalSecuritySchemes from the auth extractor.\n ...externalSecuritySchemes,\n } as Record<string, SecurityScheme>,\n },\n tags,\n };\n}\n\n/**\n * Convert Fastify-style params (/:id) to OpenAPI-style params (/{id})\n */\nfunction toOpenApiPath(path: string): string {\n return path.replace(/:([^/]+)/g, '{$1}');\n}\n\n/**\n * Convert OpenAPI schema to query parameters array\n * Transforms { properties: { page: { type: 'integer' } } } to [{ name: 'page', in: 'query', schema: { type: 'integer' } }]\n */\nfunction convertSchemaToParameters(schema: Record<string, unknown>): Parameter[] {\n const params: Parameter[] = [];\n const properties = (schema.properties as Record<string, Record<string, unknown>>) || {};\n const required = (schema.required as string[]) || [];\n\n for (const [name, prop] of Object.entries(properties)) {\n // Extract description separately (goes to Parameter level, not schema)\n const description = prop.description as string | undefined;\n const { description: _, ...schemaProps } = prop;\n\n const param: Parameter = {\n name,\n in: 'query',\n required: required.includes(name),\n schema: schemaProps as SchemaObject,\n };\n\n if (description) {\n param.description = description;\n }\n\n params.push(param);\n }\n return params;\n}\n\n/**\n * Default query parameters when no listQuery schema is provided\n */\nconst DEFAULT_LIST_PARAMS: Parameter[] = [\n { name: 'page', in: 'query', schema: { type: 'integer' }, description: 'Page number' },\n { name: 'limit', in: 'query', schema: { type: 'integer' }, description: 'Items per page' },\n { name: 'sort', in: 'query', schema: { type: 'string' }, description: 'Sort field (prefix with - for descending)' },\n];\n\n/**\n * Generate paths for a resource\n */\nfunction generateResourcePaths(\n resource: RegistryEntry,\n apiPrefix = '',\n additionalSecurity: Array<Record<string, string[]>> = [],\n): Record<string, PathItem> {\n const paths: Record<string, PathItem> = {};\n const basePath = `${apiPrefix}${resource.prefix}`;\n\n // Skip if default routes are disabled and no additional routes\n if (resource.disableDefaultRoutes && (!resource.additionalRoutes || resource.additionalRoutes.length === 0)) {\n return paths;\n }\n\n // Default CRUD routes (respects disabledRoutes + updateMethod)\n if (!resource.disableDefaultRoutes) {\n const disabledSet = new Set(resource.disabledRoutes ?? []);\n const updateMethod = resource.updateMethod ?? 'PATCH';\n\n // Collection routes: GET / (list) + POST / (create)\n const collectionPath: PathItem = {};\n\n if (!disabledSet.has('list')) {\n const listParams = resource.openApiSchemas?.listQuery\n ? convertSchemaToParameters(resource.openApiSchemas.listQuery as Record<string, unknown>)\n : DEFAULT_LIST_PARAMS;\n\n collectionPath.get = createOperation(resource, 'list', 'List all', {\n parameters: listParams,\n responses: {\n '200': {\n description: 'List of items',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n success: { type: 'boolean' },\n docs: { type: 'array', items: { $ref: `#/components/schemas/${resource.name}` } },\n page: { type: 'integer' },\n limit: { type: 'integer' },\n total: { type: 'integer' },\n pages: { type: 'integer' },\n hasNext: { type: 'boolean' },\n hasPrev: { type: 'boolean' },\n },\n },\n },\n },\n },\n },\n }, undefined, additionalSecurity);\n }\n\n if (!disabledSet.has('create')) {\n collectionPath.post = createOperation(resource, 'create', 'Create new', {\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: { $ref: `#/components/schemas/${resource.name}Input` },\n },\n },\n },\n responses: {\n '201': {\n description: 'Created successfully',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n success: { type: 'boolean' },\n data: { $ref: `#/components/schemas/${resource.name}` },\n message: { type: 'string' },\n },\n },\n },\n },\n },\n },\n }, undefined, additionalSecurity);\n }\n\n if (Object.keys(collectionPath).length > 0) {\n paths[basePath] = collectionPath;\n }\n\n // Item routes: GET /:id + UPDATE /:id + DELETE /:id\n const itemPath: PathItem = {};\n\n if (!disabledSet.has('get')) {\n itemPath.get = createOperation(resource, 'get', 'Get by ID', {\n parameters: [\n { name: 'id', in: 'path', required: true, schema: { type: 'string' } },\n ],\n responses: {\n '200': {\n description: 'Item found',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n success: { type: 'boolean' },\n data: { $ref: `#/components/schemas/${resource.name}` },\n },\n },\n },\n },\n },\n '404': { description: 'Not found' },\n },\n }, undefined, additionalSecurity);\n }\n\n if (!disabledSet.has('update')) {\n const updateOp = createOperation(resource, 'update', 'Update', {\n parameters: [\n { name: 'id', in: 'path', required: true, schema: { type: 'string' } },\n ],\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: { $ref: `#/components/schemas/${resource.name}Input` },\n },\n },\n },\n responses: {\n '200': {\n description: 'Updated successfully',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n success: { type: 'boolean' },\n data: { $ref: `#/components/schemas/${resource.name}` },\n message: { type: 'string' },\n },\n },\n },\n },\n },\n },\n }, undefined, additionalSecurity);\n\n if (updateMethod === 'both') {\n itemPath.put = updateOp;\n itemPath.patch = updateOp;\n } else if (updateMethod === 'PUT') {\n itemPath.put = updateOp;\n } else {\n itemPath.patch = updateOp;\n }\n }\n\n if (!disabledSet.has('delete')) {\n itemPath.delete = createOperation(resource, 'delete', 'Delete', {\n parameters: [\n { name: 'id', in: 'path', required: true, schema: { type: 'string' } },\n ],\n responses: {\n '200': {\n description: 'Deleted successfully',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n success: { type: 'boolean' },\n message: { type: 'string' },\n },\n },\n },\n },\n },\n },\n }, undefined, additionalSecurity);\n }\n\n if (Object.keys(itemPath).length > 0) {\n paths[toOpenApiPath(`${basePath}/:id`)] = itemPath;\n }\n }\n\n // Additional routes from presets\n for (const route of resource.additionalRoutes || []) {\n const fullPath = toOpenApiPath(`${basePath}${route.path}`);\n const method = route.method.toLowerCase() as keyof PathItem;\n\n if (!paths[fullPath]) {\n paths[fullPath] = {};\n }\n\n // Check if route requires auth (not public)\n const handlerName = route.operation ?? (typeof route.handler === 'string' ? route.handler : 'handler');\n const isPublicRoute = (route.permissions as PermissionCheck)?._isPublic === true;\n const requiresAuthForRoute = !!route.permissions && !isPublicRoute;\n\n // Build extras from route schema\n const extras: Partial<Operation> = {\n parameters: extractPathParams(route.path),\n responses: {\n '200': { description: route.description || 'Success' },\n },\n };\n\n // Add request body from route.schema.body (for POST, PUT, PATCH)\n // Auto-convert Zod schemas to JSON Schema (no-op for plain JSON Schema)\n const rawSchema = route.schema as Record<string, unknown> | undefined;\n const routeSchema = rawSchema ? convertRouteSchema(rawSchema) : undefined;\n if (routeSchema?.body && ['post', 'put', 'patch'].includes(method)) {\n extras.requestBody = {\n required: true,\n content: {\n 'application/json': {\n schema: routeSchema.body as SchemaObject,\n },\n },\n };\n }\n\n // Add query parameters from route.schema.querystring\n if (routeSchema?.querystring) {\n const queryParams = convertSchemaToParameters(routeSchema.querystring as Record<string, unknown>);\n extras.parameters = [...(extras.parameters || []), ...queryParams];\n }\n\n // Add custom response schema if provided\n if (routeSchema?.response) {\n const responseSchemas = routeSchema.response as Record<string, unknown>;\n for (const [statusCode, schema] of Object.entries(responseSchemas)) {\n extras.responses![statusCode] = {\n description: (schema as Record<string, unknown>).description as string || `Response ${statusCode}`,\n content: {\n 'application/json': {\n schema: schema as SchemaObject,\n },\n },\n };\n }\n }\n\n paths[fullPath][method] = createOperation(\n resource,\n handlerName,\n route.summary ?? handlerName,\n extras,\n requiresAuthForRoute,\n additionalSecurity,\n );\n }\n\n return paths;\n}\n\n/**\n * Create an operation object\n * @param requiresAuthOverride - Override for whether auth is required (for additional routes)\n * @param additionalSecurity - Extra security alternatives from external integrations (OR'd with bearerAuth)\n */\nfunction createOperation(\n resource: RegistryEntry,\n operation: string,\n summary: string,\n extras: Partial<Operation>,\n requiresAuthOverride?: boolean,\n additionalSecurity: Array<Record<string, string[]>> = [],\n): Operation {\n const permissions = resource.permissions || {};\n // Check if permission check is defined for this operation\n const operationPermission = (permissions as Record<string, unknown>)[operation];\n // Check if it's marked as public (allowPublic())\n const isPublic = (operationPermission as PermissionCheck)?._isPublic === true;\n // Check for role requirements\n const requiredRoles = (operationPermission as PermissionCheck)?._roles;\n // If override is provided, use it; otherwise check if operation has a permission check that isn't public\n const requiresAuth = requiresAuthOverride !== undefined\n ? requiresAuthOverride\n : typeof operationPermission === 'function' && !isPublic;\n\n // Build permission annotation\n const permAnnotation = describePermissionForOpenApi(operationPermission);\n\n // Build description with permission + preset info\n const descParts: string[] = [];\n if (permAnnotation) {\n descParts.push(`**Permission**: ${permAnnotation.type === 'public' ? 'Public' : permAnnotation.type === 'requireRoles' ? `Requires roles: ${(permAnnotation.roles ?? []).join(', ')}` : 'Requires authentication'}`);\n }\n if (resource.presets && resource.presets.length > 0) {\n descParts.push(`**Presets**: ${resource.presets.join(', ')}`);\n }\n // Find pipeline steps that apply to this operation\n const applicableSteps = (resource.pipelineSteps ?? []).filter((s) => {\n if (!s.operations) return true; // applies to all\n return s.operations.includes(operation);\n });\n\n const op: Operation = {\n tags: [resource.tag || 'Resource'],\n summary: `${summary} ${(resource.displayName || resource.name).toLowerCase()}`,\n operationId: `${resource.name}_${operation}`,\n ...(descParts.length > 0 && { description: descParts.join('\\n\\n') }),\n // Only add security requirement if route requires auth\n ...(requiresAuth && {\n security: [{ bearerAuth: [] }, ...additionalSecurity],\n }),\n // Permission metadata extension\n ...(permAnnotation && { 'x-arc-permission': permAnnotation }),\n // Pipeline extension\n ...(applicableSteps.length > 0 && {\n 'x-arc-pipeline': applicableSteps.map((s) => ({ type: s.type, name: s.name })),\n }),\n responses: {\n ...(requiresAuth && {\n '401': {\n description: 'Authentication required — no valid Bearer token provided',\n content: {\n 'application/json': {\n schema: { $ref: '#/components/schemas/Error' },\n },\n },\n },\n '403': {\n description: permAnnotation?.roles\n ? `Forbidden — requires one of: ${(permAnnotation.roles as string[]).join(', ')}`\n : 'Forbidden — insufficient permissions',\n content: {\n 'application/json': {\n schema: { $ref: '#/components/schemas/Error' },\n },\n },\n },\n }),\n '500': { description: 'Internal server error' },\n },\n ...extras,\n };\n\n return op;\n}\n\n/**\n * Describe a permission check function for OpenAPI.\n * Extracts role, org role, and team permission metadata from permission functions.\n */\nfunction describePermissionForOpenApi(\n check: unknown,\n): { type: string; roles?: readonly string[]; orgRoles?: readonly string[] } | undefined {\n if (!check || typeof check !== 'function') return undefined;\n\n const fn = check as PermissionCheck & {\n _orgRoles?: readonly string[];\n _orgPermission?: string;\n _teamPermission?: string;\n };\n\n if (fn._isPublic === true) return { type: 'public' };\n\n const result: { type: string; roles?: readonly string[]; orgRoles?: readonly string[] } = {\n type: 'requireAuth',\n };\n\n if (Array.isArray(fn._roles) && fn._roles.length > 0) {\n result.type = 'requireRoles';\n result.roles = fn._roles as string[];\n }\n if (Array.isArray(fn._orgRoles) && fn._orgRoles.length > 0) {\n result.orgRoles = fn._orgRoles;\n }\n\n return result;\n}\n\n/**\n * Extract path parameters from route path\n */\nfunction extractPathParams(path: string): Parameter[] {\n const params: Parameter[] = [];\n const matches = path.matchAll(/:([^/]+)/g);\n\n for (const match of matches) {\n const paramName = match[1];\n if (paramName) {\n params.push({\n name: paramName,\n in: 'path',\n required: true,\n schema: { type: 'string' },\n });\n }\n }\n\n return params;\n}\n\n/**\n * Generate schema definitions from pre-stored registry schemas.\n * Schemas are generated at resource definition time and stored in the registry.\n *\n * Response schema priority:\n * 1. If resource provides explicit `openApiSchemas.response`, use it as-is\n * 2. Otherwise, auto-generate from `createBody` + _id + timestamps\n *\n * Note: This is for OpenAPI documentation only - does NOT affect Fastify serialization.\n */\nfunction generateSchemas(resources: RegistryEntry[]): Record<string, SchemaObject> {\n const schemas: Record<string, SchemaObject> = {\n // Common schemas (pagination fields are inlined in list responses)\n Error: {\n type: 'object',\n properties: {\n success: { type: 'boolean', example: false },\n error: { type: 'string' },\n code: { type: 'string' },\n requestId: { type: 'string' },\n timestamp: { type: 'string' },\n },\n },\n };\n\n for (const resource of resources) {\n const storedSchemas = resource.openApiSchemas;\n const fieldPerms = resource.fieldPermissions;\n\n // === RESPONSE SCHEMA (for GET responses) ===\n // Priority 1: Explicit response schema provided by user\n if (storedSchemas?.response) {\n schemas[resource.name] = {\n type: 'object',\n description: resource.displayName,\n ...(storedSchemas.response as SchemaObject),\n };\n }\n // Priority 2: Auto-generate from createBody\n else if (storedSchemas?.createBody) {\n schemas[resource.name] = {\n type: 'object',\n description: resource.displayName,\n properties: {\n _id: { type: 'string', description: 'Unique identifier' },\n ...((storedSchemas.createBody as SchemaObject).properties ?? {}),\n createdAt: { type: 'string', format: 'date-time', description: 'Creation timestamp' },\n updatedAt: { type: 'string', format: 'date-time', description: 'Last update timestamp' },\n },\n };\n }\n // Fallback: Placeholder schema\n else {\n schemas[resource.name] = {\n type: 'object',\n description: resource.displayName,\n properties: {\n _id: { type: 'string', description: 'Unique identifier' },\n createdAt: { type: 'string', format: 'date-time', description: 'Creation timestamp' },\n updatedAt: { type: 'string', format: 'date-time', description: 'Last update timestamp' },\n },\n };\n }\n\n // Annotate fields with permission info\n if (fieldPerms && schemas[resource.name]?.properties) {\n const props = schemas[resource.name]!.properties!;\n for (const [field, perm] of Object.entries(fieldPerms)) {\n if (props[field]) {\n // Add permission description to existing field\n const desc = props[field]!.description ?? '';\n const permDesc = formatFieldPermDescription(perm);\n props[field]!.description = desc ? `${desc} (${permDesc})` : permDesc;\n } else if (perm.type === 'hidden') {\n // Hidden fields won't appear in schema — note in schema description\n }\n }\n }\n\n // === INPUT SCHEMAS (for POST/PATCH requests) ===\n if (storedSchemas?.createBody) {\n schemas[`${resource.name}Input`] = {\n type: 'object',\n description: `${resource.displayName} create input`,\n ...(storedSchemas.createBody as SchemaObject),\n };\n\n if (storedSchemas.updateBody) {\n schemas[`${resource.name}Update`] = {\n type: 'object',\n description: `${resource.displayName} update input`,\n ...(storedSchemas.updateBody as SchemaObject),\n };\n }\n } else {\n schemas[`${resource.name}Input`] = {\n type: 'object',\n description: `${resource.displayName} input`,\n };\n }\n }\n\n return schemas;\n}\n\n/**\n * Format a field permission description for OpenAPI\n */\nfunction formatFieldPermDescription(\n perm: { type: string; roles?: readonly string[]; redactValue?: unknown },\n): string {\n switch (perm.type) {\n case 'hidden':\n return 'Hidden — never returned in responses';\n case 'visibleTo':\n return `Visible to: ${(perm.roles ?? []).join(', ')}`;\n case 'writableBy':\n return `Writable by: ${(perm.roles ?? []).join(', ')}`;\n case 'redactFor':\n return `Redacted for: ${(perm.roles ?? []).join(', ')}`;\n default:\n return perm.type;\n }\n}\n\nexport default fp(openApiPlugin, {\n name: 'arc-openapi',\n fastify: '5.x',\n});\n\nexport { openApiPlugin };\n"],"mappings":"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"}
|