@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.
- package/README.md +247 -794
- package/bin/arc.js +91 -52
- package/dist/EventTransport-BkUDYZEb.d.mts +99 -0
- package/dist/HookSystem-BsGV-j2l.mjs +404 -0
- package/dist/ResourceRegistry-7Ic20ZMw.mjs +249 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.mjs +3 -0
- package/dist/audit/index.d.mts +81 -0
- package/dist/audit/index.mjs +275 -0
- package/dist/audit/mongodb.d.mts +5 -0
- package/dist/audit/mongodb.mjs +3 -0
- package/dist/audited-CGdLiSlE.mjs +140 -0
- package/dist/auth/index.d.mts +188 -0
- package/dist/auth/index.mjs +1096 -0
- package/dist/auth/redis-session.d.mts +43 -0
- package/dist/auth/redis-session.mjs +75 -0
- package/dist/betterAuthOpenApi-DjWDddNc.mjs +249 -0
- package/dist/cache/index.d.mts +145 -0
- package/dist/cache/index.mjs +91 -0
- package/dist/caching-GSDJcA6-.mjs +93 -0
- package/dist/chunk-C7Uep-_p.mjs +20 -0
- package/dist/circuitBreaker-DYhWBW_D.mjs +1096 -0
- package/dist/cli/commands/describe.d.mts +18 -0
- package/dist/cli/commands/describe.mjs +238 -0
- package/dist/cli/commands/docs.d.mts +13 -0
- package/dist/cli/commands/docs.mjs +52 -0
- package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -2
- package/dist/cli/commands/generate.mjs +357 -0
- package/dist/cli/commands/{init.d.ts → init.d.mts} +11 -8
- package/dist/cli/commands/{init.js → init.mjs} +807 -617
- package/dist/cli/commands/introspect.d.mts +10 -0
- package/dist/cli/commands/introspect.mjs +75 -0
- package/dist/cli/index.d.mts +16 -0
- package/dist/cli/index.mjs +156 -0
- package/dist/constants-DdXFXQtN.mjs +84 -0
- package/dist/core/index.d.mts +5 -0
- package/dist/core/index.mjs +4 -0
- package/dist/createApp-D2D5XXaV.mjs +559 -0
- package/dist/defineResource-PXzSJ15_.mjs +2197 -0
- package/dist/discovery/index.d.mts +46 -0
- package/dist/discovery/index.mjs +109 -0
- package/dist/docs/index.d.mts +162 -0
- package/dist/docs/index.mjs +74 -0
- package/dist/elevation-DGo5shaX.d.mts +87 -0
- package/dist/elevation-DSTbVvYj.mjs +113 -0
- package/dist/errorHandler-C3GY3_ow.mjs +108 -0
- package/dist/errorHandler-CW3OOeYq.d.mts +72 -0
- package/dist/errors-DAWRdiYP.d.mts +124 -0
- package/dist/errors-DBANPbGr.mjs +211 -0
- package/dist/eventPlugin-BEOvaDqo.mjs +229 -0
- package/dist/eventPlugin-H6wDDjGO.d.mts +124 -0
- package/dist/events/index.d.mts +53 -0
- package/dist/events/index.mjs +51 -0
- package/dist/events/transports/redis-stream-entry.d.mts +2 -0
- package/dist/events/transports/redis-stream-entry.mjs +177 -0
- package/dist/events/transports/redis.d.mts +76 -0
- package/dist/events/transports/redis.mjs +124 -0
- package/dist/externalPaths-SyPF2tgK.d.mts +50 -0
- package/dist/factory/index.d.mts +63 -0
- package/dist/factory/index.mjs +3 -0
- package/dist/fastifyAdapter-C8DlE0YH.d.mts +216 -0
- package/dist/fields-Bi_AVKSo.d.mts +109 -0
- package/dist/fields-CTd_CrKr.mjs +114 -0
- package/dist/hooks/index.d.mts +4 -0
- package/dist/hooks/index.mjs +3 -0
- package/dist/idempotency/index.d.mts +96 -0
- package/dist/idempotency/index.mjs +319 -0
- package/dist/idempotency/mongodb.d.mts +2 -0
- package/dist/idempotency/mongodb.mjs +114 -0
- package/dist/idempotency/redis.d.mts +2 -0
- package/dist/idempotency/redis.mjs +103 -0
- package/dist/index.d.mts +260 -0
- package/dist/index.mjs +104 -0
- package/dist/integrations/event-gateway.d.mts +46 -0
- package/dist/integrations/event-gateway.mjs +43 -0
- package/dist/integrations/index.d.mts +5 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs.d.mts +103 -0
- package/dist/integrations/jobs.mjs +123 -0
- package/dist/integrations/streamline.d.mts +60 -0
- package/dist/integrations/streamline.mjs +125 -0
- package/dist/integrations/websocket.d.mts +82 -0
- package/dist/integrations/websocket.mjs +288 -0
- package/dist/interface-CSNjltAc.d.mts +77 -0
- package/dist/interface-DTbsvIWe.d.mts +54 -0
- package/dist/interface-e9XfSsUV.d.mts +1097 -0
- package/dist/introspectionPlugin-B3JkrjwU.mjs +53 -0
- package/dist/keys-DhqDRxv3.mjs +42 -0
- package/dist/logger-ByrvQWZO.mjs +78 -0
- package/dist/memory-B2v7KrCB.mjs +143 -0
- package/dist/migrations/index.d.mts +156 -0
- package/dist/migrations/index.mjs +260 -0
- package/dist/mongodb-ClykrfGo.d.mts +118 -0
- package/dist/mongodb-DNKEExbf.mjs +93 -0
- package/dist/mongodb-Dg8O_gvd.d.mts +71 -0
- package/dist/openapi-9nB_kiuR.mjs +525 -0
- package/dist/org/index.d.mts +68 -0
- package/dist/org/index.mjs +513 -0
- package/dist/org/types.d.mts +82 -0
- package/dist/org/types.mjs +1 -0
- package/dist/permissions/index.d.mts +278 -0
- package/dist/permissions/index.mjs +579 -0
- package/dist/plugins/index.d.mts +172 -0
- package/dist/plugins/index.mjs +522 -0
- package/dist/plugins/response-cache.d.mts +87 -0
- package/dist/plugins/response-cache.mjs +283 -0
- package/dist/plugins/tracing-entry.d.mts +2 -0
- package/dist/plugins/tracing-entry.mjs +185 -0
- package/dist/pluralize-CM-jZg7p.mjs +86 -0
- package/dist/policies/{index.d.ts → index.d.mts} +204 -170
- package/dist/policies/index.mjs +321 -0
- package/dist/presets/{index.d.ts → index.d.mts} +62 -131
- package/dist/presets/index.mjs +143 -0
- package/dist/presets/multiTenant.d.mts +24 -0
- package/dist/presets/multiTenant.mjs +113 -0
- package/dist/presets-BTeYbw7h.d.mts +57 -0
- package/dist/presets-CeFtfDR8.mjs +119 -0
- package/dist/prisma-C3iornoK.d.mts +274 -0
- package/dist/prisma-DJbMt3yf.mjs +627 -0
- package/dist/queryCachePlugin-B6R0d4av.mjs +138 -0
- package/dist/queryCachePlugin-Q6SYuHZ6.d.mts +71 -0
- package/dist/redis-UwjEp8Ea.d.mts +49 -0
- package/dist/redis-stream-CBg0upHI.d.mts +103 -0
- package/dist/registry/index.d.mts +11 -0
- package/dist/registry/index.mjs +4 -0
- package/dist/requestContext-xi6OKBL-.mjs +55 -0
- package/dist/schemaConverter-Dtg0Kt9T.mjs +98 -0
- package/dist/schemas/index.d.mts +63 -0
- package/dist/schemas/index.mjs +82 -0
- package/dist/scope/index.d.mts +21 -0
- package/dist/scope/index.mjs +65 -0
- package/dist/sessionManager-D_iEHjQl.d.mts +186 -0
- package/dist/sse-DkqQ1uxb.mjs +123 -0
- package/dist/testing/index.d.mts +907 -0
- package/dist/testing/index.mjs +1976 -0
- package/dist/tracing-8CEbhF0w.d.mts +70 -0
- package/dist/typeGuards-DwxA1t_L.mjs +9 -0
- package/dist/types/index.d.mts +946 -0
- package/dist/types/index.mjs +14 -0
- package/dist/types-B0dhNrnd.d.mts +445 -0
- package/dist/types-Beqn1Un7.mjs +38 -0
- package/dist/types-DelU6kln.mjs +25 -0
- package/dist/types-RLkFVgaw.d.mts +101 -0
- package/dist/utils/index.d.mts +747 -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,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 };
|