@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,43 @@
1
+ import { i as SessionData, s as SessionStore } from "../sessionManager-D_iEHjQl.mjs";
2
+
3
+ //#region src/auth/redis-session.d.ts
4
+ /** Minimal Redis client interface — compatible with ioredis */
5
+ interface RedisLike {
6
+ get(key: string): Promise<string | null>;
7
+ set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
8
+ del(...keys: string[]): Promise<number>;
9
+ smembers(key: string): Promise<string[]>;
10
+ sadd(key: string, ...members: string[]): Promise<number>;
11
+ srem(key: string, ...members: string[]): Promise<number>;
12
+ expire(key: string, seconds: number): Promise<number>;
13
+ }
14
+ interface RedisSessionStoreOptions {
15
+ /** Redis client instance (ioredis or compatible) */
16
+ redis: RedisLike;
17
+ /** Key prefix for session keys (default: 'arc:session:') */
18
+ prefix?: string;
19
+ /** Key prefix for user-to-sessions index (default: 'arc:user-sessions:') */
20
+ userPrefix?: string;
21
+ }
22
+ /**
23
+ * Redis-backed session store for distributed deployments.
24
+ *
25
+ * Uses two key patterns:
26
+ * - `{prefix}{sessionId}` — stores serialized SessionData with TTL
27
+ * - `{userPrefix}{userId}` — Redis Set of sessionIds for bulk operations
28
+ *
29
+ * Session expiration is handled by Redis TTL — no cleanup interval needed.
30
+ */
31
+ declare class RedisSessionStore implements SessionStore {
32
+ private redis;
33
+ private prefix;
34
+ private userPrefix;
35
+ constructor(options: RedisSessionStoreOptions);
36
+ get(sessionId: string): Promise<SessionData | null>;
37
+ set(sessionId: string, data: SessionData): Promise<void>;
38
+ delete(sessionId: string): Promise<void>;
39
+ deleteAll(userId: string): Promise<void>;
40
+ deleteAllExcept(userId: string, currentSessionId: string): Promise<void>;
41
+ }
42
+ //#endregion
43
+ export { RedisLike, RedisSessionStore, RedisSessionStoreOptions };
@@ -0,0 +1,75 @@
1
+ //#region src/auth/redis-session.ts
2
+ /**
3
+ * Redis-backed session store for distributed deployments.
4
+ *
5
+ * Uses two key patterns:
6
+ * - `{prefix}{sessionId}` — stores serialized SessionData with TTL
7
+ * - `{userPrefix}{userId}` — Redis Set of sessionIds for bulk operations
8
+ *
9
+ * Session expiration is handled by Redis TTL — no cleanup interval needed.
10
+ */
11
+ var RedisSessionStore = class {
12
+ redis;
13
+ prefix;
14
+ userPrefix;
15
+ constructor(options) {
16
+ this.redis = options.redis;
17
+ this.prefix = options.prefix ?? "arc:session:";
18
+ this.userPrefix = options.userPrefix ?? "arc:user-sessions:";
19
+ }
20
+ async get(sessionId) {
21
+ const raw = await this.redis.get(this.prefix + sessionId);
22
+ if (!raw) return null;
23
+ let session;
24
+ try {
25
+ session = JSON.parse(raw);
26
+ } catch {
27
+ await this.delete(sessionId);
28
+ return null;
29
+ }
30
+ if (Date.now() > session.expiresAt) {
31
+ await this.delete(sessionId);
32
+ return null;
33
+ }
34
+ return session;
35
+ }
36
+ async set(sessionId, data) {
37
+ const ttlMs = data.expiresAt - Date.now();
38
+ if (ttlMs <= 0) return;
39
+ const ttlSeconds = Math.ceil(ttlMs / 1e3);
40
+ const serialized = JSON.stringify(data);
41
+ await this.redis.set(this.prefix + sessionId, serialized, "EX", ttlSeconds);
42
+ const userKey = this.userPrefix + data.userId;
43
+ await this.redis.sadd(userKey, sessionId);
44
+ await this.redis.expire(userKey, ttlSeconds + 3600);
45
+ }
46
+ async delete(sessionId) {
47
+ const raw = await this.redis.get(this.prefix + sessionId);
48
+ if (raw) try {
49
+ const session = JSON.parse(raw);
50
+ await this.redis.srem(this.userPrefix + session.userId, sessionId);
51
+ } catch {}
52
+ await this.redis.del(this.prefix + sessionId);
53
+ }
54
+ async deleteAll(userId) {
55
+ const userKey = this.userPrefix + userId;
56
+ const sessionIds = await this.redis.smembers(userKey);
57
+ if (sessionIds.length > 0) {
58
+ const keys = sessionIds.map((id) => this.prefix + id);
59
+ await this.redis.del(...keys);
60
+ }
61
+ await this.redis.del(userKey);
62
+ }
63
+ async deleteAllExcept(userId, currentSessionId) {
64
+ const userKey = this.userPrefix + userId;
65
+ const toDelete = (await this.redis.smembers(userKey)).filter((id) => id !== currentSessionId);
66
+ if (toDelete.length > 0) {
67
+ const keys = toDelete.map((id) => this.prefix + id);
68
+ await this.redis.del(...keys);
69
+ await this.redis.srem(userKey, ...toDelete);
70
+ }
71
+ }
72
+ };
73
+
74
+ //#endregion
75
+ export { RedisSessionStore };
@@ -0,0 +1,249 @@
1
+ import { t as __exportAll } from "./chunk-C7Uep-_p.mjs";
2
+ import { a as toJsonSchema } from "./schemaConverter-Dtg0Kt9T.mjs";
3
+
4
+ //#region src/auth/betterAuthOpenApi.ts
5
+ var betterAuthOpenApi_exports = /* @__PURE__ */ __exportAll({ extractBetterAuthOpenApi: () => extractBetterAuthOpenApi });
6
+ /**
7
+ * Check if a value looks like a Better Auth endpoint (has .path and .options)
8
+ */
9
+ function isBetterAuthEndpoint(value) {
10
+ if (typeof value !== "function" && typeof value !== "object") return false;
11
+ if (!value) return false;
12
+ const v = value;
13
+ return typeof v.path === "string" && typeof v.options === "object" && v.options !== null;
14
+ }
15
+ /**
16
+ * Convert Fastify-style path params (/:id) to OpenAPI-style (/{id})
17
+ */
18
+ function toOpenApiPath(path) {
19
+ return path.replace(/:([^/]+)/g, "{$1}");
20
+ }
21
+ /**
22
+ * Extract path parameters from a path string
23
+ */
24
+ function extractPathParams(path) {
25
+ const params = [];
26
+ const matches = path.matchAll(/:(\w+)/g);
27
+ for (const match of matches) params.push({
28
+ name: match[1],
29
+ in: "path",
30
+ required: true,
31
+ schema: { type: "string" }
32
+ });
33
+ return params;
34
+ }
35
+ /**
36
+ * Extract OpenAPI paths from a Better Auth instance's API object.
37
+ *
38
+ * Walks `authApi` (the `auth.api` object from Better Auth), discovers
39
+ * endpoints, converts their Zod schemas to JSON Schema via `z.toJSONSchema()`,
40
+ * and returns a complete `ExternalOpenApiPaths` object ready for Arc's spec builder.
41
+ */
42
+ function extractBetterAuthOpenApi(authApi, options = {}) {
43
+ const { basePath = "/api/auth", tagName = "Authentication", tagDescription = "Better Auth authentication endpoints", excludePaths = [], excludeServerOnly = true, userFields } = options;
44
+ const normalizedBase = basePath.replace(/\/+$/, "");
45
+ const paths = {};
46
+ const detectedPlugins = detectActivePlugins(authApi);
47
+ const securityOptions = [{ cookieAuth: [] }, { bearerAuth: [] }];
48
+ if (detectedPlugins.apiKey) securityOptions.push({ apiKeyAuth: [] });
49
+ for (const [key, value] of Object.entries(authApi)) {
50
+ if (!isBetterAuthEndpoint(value)) continue;
51
+ const { path: endpointPath, options: endpointOpts } = value;
52
+ if (excludePaths.includes(endpointPath)) continue;
53
+ if (excludeServerOnly && endpointOpts.metadata?.SERVER_ONLY) continue;
54
+ const fullPath = toOpenApiPath(`${normalizedBase}${endpointPath}`);
55
+ const methods = [];
56
+ if (endpointOpts.method) if (Array.isArray(endpointOpts.method)) methods.push(...endpointOpts.method.map((m) => m.toLowerCase()));
57
+ else methods.push(endpointOpts.method.toLowerCase());
58
+ else methods.push(endpointOpts.body ? "post" : "get");
59
+ const openApiMeta = endpointOpts.metadata?.openapi;
60
+ for (const method of methods) {
61
+ const operation = {
62
+ tags: openApiMeta?.tags ?? [tagName],
63
+ operationId: openApiMeta?.operationId ?? key,
64
+ summary: openApiMeta?.summary ?? formatOperationSummary(key),
65
+ security: securityOptions
66
+ };
67
+ if (openApiMeta?.description) operation.description = openApiMeta.description;
68
+ const parameters = [...extractPathParams(endpointPath)];
69
+ if ((method === "get" || method === "delete") && endpointOpts.query) {
70
+ const querySchema = toJsonSchema(endpointOpts.query);
71
+ if (querySchema && querySchema.type === "object" && querySchema.properties) {
72
+ const props = querySchema.properties;
73
+ const required = querySchema.required ?? [];
74
+ for (const [name, prop] of Object.entries(props)) {
75
+ const paramEntry = {
76
+ name,
77
+ in: "query",
78
+ required: required.includes(name),
79
+ schema: prop
80
+ };
81
+ if (prop.description) paramEntry.description = prop.description;
82
+ parameters.push(paramEntry);
83
+ }
84
+ }
85
+ }
86
+ if (parameters.length > 0) operation.parameters = parameters;
87
+ if (method === "post" || method === "put" || method === "patch") {
88
+ if (openApiMeta?.requestBody) operation.requestBody = structuredClone(openApiMeta.requestBody);
89
+ else if (endpointOpts.body) {
90
+ const bodySchema = toJsonSchema(endpointOpts.body);
91
+ if (bodySchema) operation.requestBody = {
92
+ required: true,
93
+ content: { "application/json": { schema: bodySchema } }
94
+ };
95
+ }
96
+ if (userFields && isUserFieldEndpoint(endpointPath) && operation.requestBody) mergeUserFieldsIntoRequestBody(operation.requestBody, userFields, endpointPath);
97
+ }
98
+ if (openApiMeta?.responses) operation.responses = openApiMeta.responses;
99
+ else operation.responses = {
100
+ "200": { description: "Success" },
101
+ "400": { description: "Bad request" },
102
+ "401": { description: "Unauthorized" }
103
+ };
104
+ if (!paths[fullPath]) paths[fullPath] = {};
105
+ paths[fullPath][method] = operation;
106
+ }
107
+ }
108
+ const schemas = {
109
+ User: {
110
+ type: "object",
111
+ properties: {
112
+ id: { type: "string" },
113
+ name: { type: "string" },
114
+ email: {
115
+ type: "string",
116
+ format: "email"
117
+ },
118
+ emailVerified: { type: "boolean" },
119
+ image: {
120
+ type: "string",
121
+ nullable: true
122
+ },
123
+ createdAt: {
124
+ type: "string",
125
+ format: "date-time"
126
+ },
127
+ updatedAt: {
128
+ type: "string",
129
+ format: "date-time"
130
+ }
131
+ }
132
+ },
133
+ Session: {
134
+ type: "object",
135
+ properties: {
136
+ id: { type: "string" },
137
+ userId: { type: "string" },
138
+ token: { type: "string" },
139
+ expiresAt: {
140
+ type: "string",
141
+ format: "date-time"
142
+ },
143
+ ipAddress: {
144
+ type: "string",
145
+ nullable: true
146
+ },
147
+ userAgent: {
148
+ type: "string",
149
+ nullable: true
150
+ },
151
+ createdAt: {
152
+ type: "string",
153
+ format: "date-time"
154
+ },
155
+ updatedAt: {
156
+ type: "string",
157
+ format: "date-time"
158
+ }
159
+ }
160
+ }
161
+ };
162
+ if (userFields) {
163
+ const userProps = schemas.User.properties;
164
+ for (const [name, field] of Object.entries(userFields)) {
165
+ const prop = { type: field.type };
166
+ if (field.description) prop.description = field.description;
167
+ userProps[name] = prop;
168
+ }
169
+ }
170
+ const securitySchemes = { cookieAuth: {
171
+ type: "apiKey",
172
+ in: "cookie",
173
+ name: "better-auth.session_token",
174
+ description: "Session cookie set by Better Auth after sign-in"
175
+ } };
176
+ if (detectedPlugins.apiKey) securitySchemes.apiKeyAuth = {
177
+ type: "apiKey",
178
+ in: "header",
179
+ name: "x-api-key",
180
+ description: "API key for programmatic access. Pass org context via x-organization-id header."
181
+ };
182
+ const resourceSecurity = [];
183
+ if (detectedPlugins.apiKey) resourceSecurity.push({
184
+ apiKeyAuth: [],
185
+ orgHeader: []
186
+ });
187
+ return {
188
+ paths,
189
+ schemas,
190
+ securitySchemes,
191
+ tags: [{
192
+ name: tagName,
193
+ description: tagDescription
194
+ }],
195
+ resourceSecurity: resourceSecurity.length > 0 ? resourceSecurity : void 0
196
+ };
197
+ }
198
+ /**
199
+ * Auto-detect active Better Auth plugins by inspecting the API object.
200
+ *
201
+ * Rather than hardcoding plugin-specific behavior, we check for known
202
+ * endpoint signatures that each plugin registers. This way the OpenAPI
203
+ * spec adapts automatically to whatever plugins the app has enabled —
204
+ * no Arc update needed when adding/removing plugins.
205
+ */
206
+ function detectActivePlugins(authApi) {
207
+ const endpointPaths = /* @__PURE__ */ new Set();
208
+ for (const value of Object.values(authApi)) if (isBetterAuthEndpoint(value)) endpointPaths.add(value.path);
209
+ return {
210
+ apiKey: endpointPaths.has("/api-key/create"),
211
+ organization: endpointPaths.has("/organization/create")
212
+ };
213
+ }
214
+ /**
215
+ * Convert a camelCase key like 'signInEmail' to a readable summary like 'Sign in email'
216
+ */
217
+ function formatOperationSummary(key) {
218
+ return key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
219
+ }
220
+ /**
221
+ * Check if an endpoint path should have userFields merged into its request body.
222
+ */
223
+ function isUserFieldEndpoint(path) {
224
+ return path === "/sign-up/email" || path === "/update-user";
225
+ }
226
+ /**
227
+ * Merge user-defined fields into an existing requestBody schema.
228
+ * For updateUser, all fields are treated as optional regardless of their `required` setting.
229
+ */
230
+ function mergeUserFieldsIntoRequestBody(requestBody, userFields, endpointPath) {
231
+ const content = requestBody?.content?.["application/json"];
232
+ if (!content?.schema) return;
233
+ const schema = content.schema;
234
+ if (!schema.properties) schema.properties = {};
235
+ if (!schema.required) schema.required = [];
236
+ const props = schema.properties;
237
+ const required = schema.required;
238
+ for (const [name, field] of Object.entries(userFields)) {
239
+ if (field.input === false) continue;
240
+ const isRequired = endpointPath === "/update-user" ? false : field.required ?? false;
241
+ const prop = { type: field.type };
242
+ if (field.description) prop.description = field.description;
243
+ props[name] = prop;
244
+ if (isRequired && !required.includes(name)) required.push(name);
245
+ }
246
+ }
247
+
248
+ //#endregion
249
+ export { extractBetterAuthOpenApi as n, betterAuthOpenApi_exports as t };
@@ -0,0 +1,145 @@
1
+ import { i as CacheStore, n as CacheSetOptions, r as CacheStats, t as CacheLogger } from "../interface-DTbsvIWe.mjs";
2
+ import { a as CacheEnvelope, c as QueryCache, i as queryCachePlugin, l as QueryCacheConfig, n as QueryCacheDefaults, o as CacheResult, r as QueryCachePluginOptions, s as CacheStatus, t as CrossResourceRule } from "../queryCachePlugin-Q6SYuHZ6.mjs";
3
+
4
+ //#region src/cache/memory.d.ts
5
+ interface MemoryCacheStoreOptions {
6
+ /** Default TTL in milliseconds (default: 60_000) */
7
+ defaultTtlMs?: number;
8
+ /** Hard upper bound for entries (default: 1000) */
9
+ maxEntries?: number;
10
+ /** Background cleanup interval in milliseconds (default: 30_000) */
11
+ cleanupIntervalMs?: number;
12
+ /**
13
+ * Maximum serialized entry size in bytes (default: 256 KiB).
14
+ * Oversized entries are skipped to prevent memory pressure.
15
+ */
16
+ maxEntryBytes?: number;
17
+ /**
18
+ * Total memory budget in bytes (default: 50 MiB).
19
+ * When exceeded, LRU entries are evicted until usage drops below watermark.
20
+ * Set to 0 to disable (rely on maxEntries only).
21
+ */
22
+ maxMemoryBytes?: number;
23
+ /**
24
+ * Eviction watermark as fraction of maxMemoryBytes (default: 0.9).
25
+ * When memory exceeds budget, evict until usage drops to budget * watermark.
26
+ */
27
+ evictionWatermark?: number;
28
+ /** Logger for warnings/errors (default: console) */
29
+ logger?: CacheLogger;
30
+ }
31
+ /**
32
+ * In-memory LRU+TTL cache store with hard entry cap and memory budget.
33
+ * - LRU eviction when `maxEntries` or `maxMemoryBytes` is reached
34
+ * - TTL expiration on read + periodic cleanup
35
+ * - Entry size guard to avoid runaway memory usage
36
+ * - Stats tracking for observability
37
+ */
38
+ declare class MemoryCacheStore<TValue = unknown> implements CacheStore<TValue> {
39
+ readonly name = "memory-cache";
40
+ private readonly cache;
41
+ private readonly defaultTtlMs;
42
+ private readonly maxEntries;
43
+ private readonly maxEntryBytes;
44
+ private readonly maxMemoryBytes;
45
+ private readonly evictionWatermark;
46
+ private readonly logger;
47
+ private readonly cleanupTimer;
48
+ private currentBytes;
49
+ private _hits;
50
+ private _misses;
51
+ private _evictions;
52
+ constructor(options?: MemoryCacheStoreOptions);
53
+ get(key: string): Promise<TValue | undefined>;
54
+ set(key: string, value: TValue, options?: CacheSetOptions): Promise<void>;
55
+ delete(key: string): Promise<void>;
56
+ clear(): Promise<void>;
57
+ close(): Promise<void>;
58
+ stats(): CacheStats;
59
+ private removeEntry;
60
+ private evictToLimit;
61
+ private evictToMemoryLimit;
62
+ private cleanupExpired;
63
+ private estimateSize;
64
+ }
65
+ //#endregion
66
+ //#region src/cache/redis.d.ts
67
+ interface RedisCacheClient {
68
+ get(key: string): Promise<string | null>;
69
+ set(key: string, value: string, options?: {
70
+ EX?: number;
71
+ PX?: number;
72
+ NX?: boolean;
73
+ XX?: boolean;
74
+ }): Promise<string | null | unknown>;
75
+ del(key: string | string[]): Promise<number>;
76
+ /**
77
+ * Optional: enables prefix-based `clear()` and `deleteByPrefix()` via SCAN.
78
+ * Compatible with both ioredis and node-redis.
79
+ * If not provided, `clear()` is a safe no-op.
80
+ */
81
+ scan?(cursor: string | number, ...args: (string | number)[]): Promise<[string | number, string[]]>;
82
+ /** Optional: pipeline for batched commands (ioredis compatible) */
83
+ pipeline?(): RedisPipeline;
84
+ }
85
+ interface RedisPipeline {
86
+ del(key: string): unknown;
87
+ exec(): Promise<unknown>;
88
+ }
89
+ interface RedisCacheStoreOptions {
90
+ /** Redis client instance */
91
+ client: RedisCacheClient;
92
+ /** Key prefix for namespacing (default: 'arc:cache:') */
93
+ prefix?: string;
94
+ /** Default TTL in milliseconds (default: 60_000) */
95
+ defaultTtlMs?: number;
96
+ /** Maximum serialized entry size in bytes. Oversized entries are skipped. */
97
+ maxEntryBytes?: number;
98
+ }
99
+ /**
100
+ * Redis-backed cache store.
101
+ * Suitable for multi-instance and horizontally scaled deployments.
102
+ * Uses pipeline batching when available for bulk operations.
103
+ */
104
+ declare class RedisCacheStore<TValue = unknown> implements CacheStore<TValue> {
105
+ readonly name = "redis-cache";
106
+ private readonly client;
107
+ private readonly prefix;
108
+ private readonly defaultTtlMs;
109
+ private readonly maxEntryBytes;
110
+ private _hits;
111
+ private _misses;
112
+ constructor(options: RedisCacheStoreOptions);
113
+ get(key: string): Promise<TValue | undefined>;
114
+ set(key: string, value: TValue, options?: CacheSetOptions): Promise<void>;
115
+ delete(key: string): Promise<void>;
116
+ clear(): Promise<void>;
117
+ /** Delete all keys matching `this.prefix + prefix + *`. Returns count deleted. */
118
+ deleteByPrefix(prefix: string): Promise<number>;
119
+ stats(): CacheStats;
120
+ private scanAndDelete;
121
+ private withPrefix;
122
+ }
123
+ //#endregion
124
+ //#region src/cache/keys.d.ts
125
+ /**
126
+ * Cache Key Utilities
127
+ *
128
+ * Deterministic, scope-safe key generation for QueryCache.
129
+ * Keys include resource version, operation, params hash, and user/org scope
130
+ * to ensure multi-tenant isolation and O(1) version-based invalidation.
131
+ */
132
+ /** Build a deterministic cache key for a query */
133
+ declare function buildQueryKey(resource: string, operation: string, resourceVersion: number, params: Record<string, unknown>, userId?: string, orgId?: string): string;
134
+ /** Resource version key — stored in CacheStore, bumped on mutations */
135
+ declare function versionKey(resource: string): string;
136
+ /** Tag version key — stored in CacheStore, bumped on cross-resource invalidation */
137
+ declare function tagVersionKey(tag: string): string;
138
+ /**
139
+ * Stable hash for query params.
140
+ * Sorts keys recursively, serializes to JSON, then applies djb2 hash.
141
+ * Returns hex string.
142
+ */
143
+ declare function hashParams(params: Record<string, unknown>): string;
144
+ //#endregion
145
+ export { type CacheEnvelope, type CacheLogger, type CacheResult, type CacheSetOptions, type CacheStats, type CacheStatus, type CacheStore, type CrossResourceRule, MemoryCacheStore, type MemoryCacheStoreOptions, QueryCache, type QueryCacheConfig, type QueryCacheDefaults, type QueryCachePluginOptions, type RedisCacheClient, RedisCacheStore, type RedisCacheStoreOptions, type RedisPipeline, buildQueryKey, hashParams, queryCachePlugin, tagVersionKey, versionKey };
@@ -0,0 +1,91 @@
1
+ import { i as versionKey, n as hashParams, r as tagVersionKey, t as buildQueryKey } from "../keys-DhqDRxv3.mjs";
2
+ import { t as MemoryCacheStore } from "../memory-B2v7KrCB.mjs";
3
+ import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-B6R0d4av.mjs";
4
+
5
+ //#region src/cache/redis.ts
6
+ /**
7
+ * Redis-backed cache store.
8
+ * Suitable for multi-instance and horizontally scaled deployments.
9
+ * Uses pipeline batching when available for bulk operations.
10
+ */
11
+ var RedisCacheStore = class {
12
+ name = "redis-cache";
13
+ client;
14
+ prefix;
15
+ defaultTtlMs;
16
+ maxEntryBytes;
17
+ _hits = 0;
18
+ _misses = 0;
19
+ constructor(options) {
20
+ this.client = options.client;
21
+ this.prefix = options.prefix ?? "arc:cache:";
22
+ this.defaultTtlMs = options.defaultTtlMs ?? 6e4;
23
+ this.maxEntryBytes = options.maxEntryBytes ?? 0;
24
+ }
25
+ async get(key) {
26
+ const data = await this.client.get(this.withPrefix(key));
27
+ if (!data) {
28
+ this._misses++;
29
+ return;
30
+ }
31
+ try {
32
+ this._hits++;
33
+ return JSON.parse(data);
34
+ } catch {
35
+ this._misses++;
36
+ this._hits--;
37
+ return;
38
+ }
39
+ }
40
+ async set(key, value, options = {}) {
41
+ const ttlMs = options.ttlMs ?? this.defaultTtlMs;
42
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) return;
43
+ const payload = JSON.stringify(value);
44
+ if (this.maxEntryBytes > 0 && Buffer.byteLength(payload, "utf8") > this.maxEntryBytes) return;
45
+ await this.client.set(this.withPrefix(key), payload, { PX: Math.ceil(ttlMs) });
46
+ }
47
+ async delete(key) {
48
+ await this.client.del(this.withPrefix(key));
49
+ }
50
+ async clear() {
51
+ await this.scanAndDelete(`${this.prefix}*`);
52
+ }
53
+ /** Delete all keys matching `this.prefix + prefix + *`. Returns count deleted. */
54
+ async deleteByPrefix(prefix) {
55
+ return this.scanAndDelete(`${this.prefix}${prefix}*`);
56
+ }
57
+ stats() {
58
+ return {
59
+ entries: -1,
60
+ memoryBytes: -1,
61
+ hits: this._hits,
62
+ misses: this._misses,
63
+ evictions: -1
64
+ };
65
+ }
66
+ async scanAndDelete(pattern) {
67
+ if (!this.client.scan) return 0;
68
+ const BATCH_SIZE = 200;
69
+ let cursor = "0";
70
+ let deleted = 0;
71
+ do {
72
+ const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", BATCH_SIZE);
73
+ cursor = nextCursor;
74
+ if (keys.length > 0) {
75
+ if (this.client.pipeline) {
76
+ const pipe = this.client.pipeline();
77
+ for (const key of keys) pipe.del(key);
78
+ await pipe.exec();
79
+ } else await this.client.del(keys);
80
+ deleted += keys.length;
81
+ }
82
+ } while (String(cursor) !== "0");
83
+ return deleted;
84
+ }
85
+ withPrefix(key) {
86
+ return `${this.prefix}${key}`;
87
+ }
88
+ };
89
+
90
+ //#endregion
91
+ export { MemoryCacheStore, QueryCache, RedisCacheStore, buildQueryKey, hashParams, queryCachePlugin, tagVersionKey, versionKey };
@@ -0,0 +1,93 @@
1
+ import { t as __exportAll } from "./chunk-C7Uep-_p.mjs";
2
+ import fp from "fastify-plugin";
3
+
4
+ //#region src/plugins/caching.ts
5
+ /**
6
+ * Caching Plugin
7
+ *
8
+ * Adds ETag and Cache-Control headers to GET/HEAD responses.
9
+ * Supports conditional requests (304 Not Modified) for bandwidth savings.
10
+ *
11
+ * @example
12
+ * import { cachingPlugin } from '@classytic/arc/plugins';
13
+ *
14
+ * // Basic — ETag + conditional requests, no browser caching
15
+ * await fastify.register(cachingPlugin);
16
+ *
17
+ * // With cache rules per path
18
+ * await fastify.register(cachingPlugin, {
19
+ * rules: [
20
+ * { match: '/api/products', maxAge: 60 },
21
+ * { match: '/api/categories', maxAge: 300, staleWhileRevalidate: 60 },
22
+ * ],
23
+ * });
24
+ */
25
+ var caching_exports = /* @__PURE__ */ __exportAll({
26
+ cachingPlugin: () => cachingPlugin,
27
+ default: () => caching_default
28
+ });
29
+ const FNV_OFFSET = 2166136261;
30
+ const FNV_PRIME = 16777619;
31
+ /** Fast non-cryptographic hash for ETag generation */
32
+ function fnv1a(data) {
33
+ let hash = FNV_OFFSET;
34
+ for (let i = 0; i < data.length; i++) {
35
+ hash ^= data.charCodeAt(i);
36
+ hash = hash * FNV_PRIME >>> 0;
37
+ }
38
+ return hash.toString(36);
39
+ }
40
+ const cachingPlugin = async (fastify, opts = {}) => {
41
+ const { maxAge = 0, etag = true, conditional = true, methods = ["GET", "HEAD"], exclude = [], rules = [] } = opts;
42
+ const methodSet = new Set(methods.map((m) => m.toUpperCase()));
43
+ /** Find the first matching rule for a URL path */
44
+ function findRule(url) {
45
+ const path = url.split("?")[0];
46
+ return rules.find((r) => path.startsWith(r.match));
47
+ }
48
+ /** Build Cache-Control header value */
49
+ function buildCacheControl(rule) {
50
+ const age = rule?.maxAge ?? maxAge;
51
+ if (age <= 0) return "no-cache";
52
+ const parts = [];
53
+ parts.push(rule?.private ? "private" : "public");
54
+ parts.push(`max-age=${age}`);
55
+ if (rule?.staleWhileRevalidate) parts.push(`stale-while-revalidate=${rule.staleWhileRevalidate}`);
56
+ return parts.join(", ");
57
+ }
58
+ fastify.addHook("onSend", async (request, reply, payload) => {
59
+ const url = request.url;
60
+ if (exclude.some((p) => url.startsWith(p))) return payload;
61
+ const method = request.method.toUpperCase();
62
+ if (!methodSet.has(method)) {
63
+ if (!reply.hasHeader("cache-control")) reply.header("cache-control", "no-store");
64
+ return payload;
65
+ }
66
+ const statusCode = reply.statusCode;
67
+ if (statusCode < 200 || statusCode >= 300) return payload;
68
+ if (!reply.hasHeader("cache-control")) {
69
+ const rule = findRule(url);
70
+ reply.header("cache-control", buildCacheControl(rule));
71
+ }
72
+ if (etag && payload) {
73
+ const tag = `"${fnv1a(typeof payload === "string" ? payload : String(payload))}"`;
74
+ reply.header("etag", tag);
75
+ if (conditional) {
76
+ const ifNoneMatch = request.headers["if-none-match"];
77
+ if (ifNoneMatch && ifNoneMatch === tag) {
78
+ reply.code(304);
79
+ return "";
80
+ }
81
+ }
82
+ }
83
+ return payload;
84
+ });
85
+ fastify.log?.debug?.("Caching plugin registered");
86
+ };
87
+ var caching_default = fp(cachingPlugin, {
88
+ name: "arc-caching",
89
+ fastify: "5.x"
90
+ });
91
+
92
+ //#endregion
93
+ export { caching_default as n, caching_exports as r, cachingPlugin as t };