@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 @@
|
|
|
1
|
+
{"version":3,"file":"betterAuthOpenApi-BrHKeSAx.mjs","names":[],"sources":["../src/auth/betterAuthOpenApi.ts"],"sourcesContent":["/**\n * Better Auth OpenAPI Extractor\n *\n * Introspects a Better Auth instance's `api` object and extracts\n * OpenAPI-compatible path definitions from endpoint metadata.\n *\n * Schema conversion uses Zod v4's native `z.toJSONSchema()` via\n * the shared `schemaConverter` utility.\n *\n * @example\n * ```typescript\n * import { extractBetterAuthOpenApi } from '@classytic/arc/auth';\n *\n * const auth = betterAuth({ ... });\n * const openapi = extractBetterAuthOpenApi(auth.api, { basePath: '/api/auth' });\n * // openapi.paths, openapi.securitySchemes, openapi.tags\n * ```\n */\n\nimport type { ExternalOpenApiPaths } from '../docs/externalPaths.js';\nimport { toJsonSchema } from '../utils/schemaConverter.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BetterAuthOpenApiOptions {\n /** Base path prefix for auth routes (default: '/api/auth') */\n basePath?: string;\n /** Tag name for auth routes in OpenAPI (default: 'Authentication') */\n tagName?: string;\n /** Tag description */\n tagDescription?: string;\n /** Exclude specific paths from the spec (e.g. ['/ok', '/error']) */\n excludePaths?: string[];\n /** Exclude SERVER_ONLY endpoints (default: true) */\n excludeServerOnly?: boolean;\n /**\n * Additional user fields from Better Auth config.\n * These get merged into signUpEmail/updateUser request body schemas\n * and the User component schema for $ref resolution.\n *\n * Fields with `input: false` are excluded from request bodies\n * but still appear in the User component schema (output-only).\n */\n userFields?: Record<string, {\n type: string;\n description?: string;\n /** Whether this field is required in sign-up (default: false) */\n required?: boolean;\n /** Whether this field is accepted in request body (default: true). Set false for output-only fields. */\n input?: boolean;\n }>;\n}\n\n// ============================================================================\n// Better Auth Endpoint Discovery\n// ============================================================================\n\n/** Shape of a Better Auth endpoint function with metadata */\ninterface BetterAuthEndpoint {\n path: string;\n options: {\n method?: string | string[];\n body?: unknown;\n query?: unknown;\n metadata?: {\n openapi?: {\n summary?: string;\n description?: string;\n operationId?: string;\n responses?: Record<string, unknown>;\n requestBody?: {\n content: Record<string, { schema: Record<string, unknown> }>;\n required?: boolean;\n };\n tags?: string[];\n };\n SERVER_ONLY?: boolean;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n };\n}\n\n/**\n * Check if a value looks like a Better Auth endpoint (has .path and .options)\n */\nfunction isBetterAuthEndpoint(value: unknown): value is BetterAuthEndpoint {\n if (typeof value !== 'function' && typeof value !== 'object') return false;\n if (!value) return false;\n\n const v = value as Record<string, unknown>;\n return (\n typeof v.path === 'string' &&\n typeof v.options === 'object' &&\n v.options !== null\n );\n}\n\n/**\n * Convert Fastify-style path params (/:id) to OpenAPI-style (/{id})\n */\nfunction toOpenApiPath(path: string): string {\n return path.replace(/:([^/]+)/g, '{$1}');\n}\n\n/**\n * Extract path parameters from a path string\n */\nfunction extractPathParams(path: string): Array<{ name: string; in: 'path'; required: true; schema: { type: string } }> {\n const params: Array<{ name: string; in: 'path'; required: true; schema: { type: string } }> = [];\n const matches = path.matchAll(/:(\\w+)/g);\n for (const match of matches) {\n params.push({\n name: match[1]!,\n in: 'path',\n required: true,\n schema: { type: 'string' },\n });\n }\n return params;\n}\n\n/**\n * Extract OpenAPI paths from a Better Auth instance's API object.\n *\n * Walks `authApi` (the `auth.api` object from Better Auth), discovers\n * endpoints, converts their Zod schemas to JSON Schema via `z.toJSONSchema()`,\n * and returns a complete `ExternalOpenApiPaths` object ready for Arc's spec builder.\n */\nexport function extractBetterAuthOpenApi(\n authApi: Record<string, unknown>,\n options: BetterAuthOpenApiOptions = {},\n): ExternalOpenApiPaths {\n const {\n basePath = '/api/auth',\n tagName = 'Authentication',\n tagDescription = 'Better Auth authentication endpoints',\n excludePaths = [],\n excludeServerOnly = true,\n userFields,\n } = options;\n\n const normalizedBase = basePath.replace(/\\/+$/, '');\n const paths: Record<string, Record<string, unknown>> = {};\n\n // Auto-detect active plugins by inspecting available endpoints.\n // This avoids hardcoding plugin-specific schemes — the spec adapts to\n // whatever plugins the app has registered with Better Auth.\n const detectedPlugins = detectActivePlugins(authApi);\n\n // Build security options dynamically: session + bearer are always available,\n // others are added based on detected plugins.\n const securityOptions: Array<Record<string, unknown[]>> = [\n { cookieAuth: [] },\n { bearerAuth: [] },\n ];\n if (detectedPlugins.apiKey) {\n securityOptions.push({ apiKeyAuth: [] });\n }\n\n for (const [key, value] of Object.entries(authApi)) {\n if (!isBetterAuthEndpoint(value)) continue;\n\n const endpoint = value;\n const { path: endpointPath, options: endpointOpts } = endpoint;\n\n // Skip excluded paths\n if (excludePaths.includes(endpointPath)) continue;\n\n // Skip SERVER_ONLY endpoints\n if (excludeServerOnly && endpointOpts.metadata?.SERVER_ONLY) continue;\n\n // Build full path\n const fullPath = toOpenApiPath(`${normalizedBase}${endpointPath}`);\n\n // Determine HTTP method(s)\n const methods: string[] = [];\n if (endpointOpts.method) {\n if (Array.isArray(endpointOpts.method)) {\n methods.push(...endpointOpts.method.map(m => m.toLowerCase()));\n } else {\n methods.push(endpointOpts.method.toLowerCase());\n }\n } else {\n // Default: GET if no body, POST if body exists\n methods.push(endpointOpts.body ? 'post' : 'get');\n }\n\n // Extract OpenAPI metadata from endpoint\n const openApiMeta = endpointOpts.metadata?.openapi;\n\n // Build operation for each method\n for (const method of methods) {\n const operation: Record<string, unknown> = {\n tags: openApiMeta?.tags ?? [tagName],\n operationId: openApiMeta?.operationId ?? key,\n summary: openApiMeta?.summary ?? formatOperationSummary(key),\n security: securityOptions,\n };\n\n if (openApiMeta?.description) {\n operation.description = openApiMeta.description;\n }\n\n // Path parameters\n const pathParams = extractPathParams(endpointPath);\n const parameters: unknown[] = [...pathParams];\n\n // Query parameters (for GET/DELETE)\n if ((method === 'get' || method === 'delete') && endpointOpts.query) {\n const querySchema = toJsonSchema(endpointOpts.query);\n if (querySchema && querySchema.type === 'object' && querySchema.properties) {\n const props = querySchema.properties as Record<string, Record<string, unknown>>;\n const required = (querySchema.required as string[]) ?? [];\n for (const [name, prop] of Object.entries(props)) {\n const paramEntry: Record<string, unknown> = {\n name,\n in: 'query',\n required: required.includes(name),\n schema: prop,\n };\n if (prop.description) {\n paramEntry.description = prop.description;\n }\n parameters.push(paramEntry);\n }\n }\n }\n\n if (parameters.length > 0) {\n operation.parameters = parameters;\n }\n\n // Request body (for POST/PUT/PATCH)\n if (method === 'post' || method === 'put' || method === 'patch') {\n if (openApiMeta?.requestBody) {\n // Prefer metadata.openapi.requestBody (cleaner than Zod conversion)\n operation.requestBody = structuredClone(openApiMeta.requestBody);\n } else if (endpointOpts.body) {\n // Fall back to Zod body conversion\n const bodySchema = toJsonSchema(endpointOpts.body);\n if (bodySchema) {\n operation.requestBody = {\n required: true,\n content: {\n 'application/json': { schema: bodySchema },\n },\n };\n }\n }\n\n // Merge userFields into sign-up and update-user request bodies\n if (userFields && isUserFieldEndpoint(endpointPath) && operation.requestBody) {\n mergeUserFieldsIntoRequestBody(\n operation.requestBody as Record<string, unknown>,\n userFields,\n endpointPath,\n );\n }\n }\n\n // Responses\n if (openApiMeta?.responses) {\n operation.responses = openApiMeta.responses;\n } else {\n // Default responses\n operation.responses = {\n '200': { description: 'Success' },\n '400': { description: 'Bad request' },\n '401': { description: 'Unauthorized' },\n };\n }\n\n // Add to paths\n if (!paths[fullPath]) paths[fullPath] = {};\n (paths[fullPath] as Record<string, unknown>)[method] = operation;\n }\n }\n\n // Build component schemas for $ref resolution (e.g. $ref: \"#/components/schemas/User\")\n const schemas: Record<string, Record<string, unknown>> = {\n User: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n name: { type: 'string' },\n email: { type: 'string', format: 'email' },\n emailVerified: { type: 'boolean' },\n image: { type: 'string', nullable: true },\n createdAt: { type: 'string', format: 'date-time' },\n updatedAt: { type: 'string', format: 'date-time' },\n },\n },\n Session: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n userId: { type: 'string' },\n token: { type: 'string' },\n expiresAt: { type: 'string', format: 'date-time' },\n ipAddress: { type: 'string', nullable: true },\n userAgent: { type: 'string', nullable: true },\n createdAt: { type: 'string', format: 'date-time' },\n updatedAt: { type: 'string', format: 'date-time' },\n },\n },\n };\n\n // Merge userFields into User component schema (all fields, including input: false)\n if (userFields) {\n const userProps = schemas.User!.properties as Record<string, unknown>;\n for (const [name, field] of Object.entries(userFields)) {\n const prop: Record<string, unknown> = { type: field.type };\n if (field.description) prop.description = field.description;\n userProps[name] = prop;\n }\n }\n\n // Build security schemes — always include cookieAuth, add plugin-specific schemes dynamically\n const securitySchemes: Record<string, Record<string, unknown>> = {\n cookieAuth: {\n type: 'apiKey',\n in: 'cookie',\n name: 'better-auth.session_token',\n description: 'Session cookie set by Better Auth after sign-in',\n },\n };\n\n if (detectedPlugins.apiKey) {\n securitySchemes.apiKeyAuth = {\n type: 'apiKey',\n in: 'header',\n name: 'x-api-key',\n description: 'API key for programmatic access. Pass org context via x-organization-id header.',\n };\n }\n\n // Build resourceSecurity — additional auth alternatives for Arc resource paths.\n // Each item is OR'd with bearerAuth; keys within the same object are AND'd.\n const resourceSecurity: Array<Record<string, string[]>> = [];\n if (detectedPlugins.apiKey) {\n // API key requires org header for tenant context (AND = same object)\n resourceSecurity.push({ apiKeyAuth: [], orgHeader: [] });\n }\n\n return {\n paths,\n schemas,\n securitySchemes,\n tags: [{ name: tagName, description: tagDescription }],\n resourceSecurity: resourceSecurity.length > 0 ? resourceSecurity : undefined,\n };\n}\n\n// ============================================================================\n// Plugin Detection\n// ============================================================================\n\ninterface DetectedPlugins {\n apiKey: boolean;\n organization: boolean;\n}\n\n/**\n * Auto-detect active Better Auth plugins by inspecting the API object.\n *\n * Rather than hardcoding plugin-specific behavior, we check for known\n * endpoint signatures that each plugin registers. This way the OpenAPI\n * spec adapts automatically to whatever plugins the app has enabled —\n * no Arc update needed when adding/removing plugins.\n */\nfunction detectActivePlugins(authApi: Record<string, unknown>): DetectedPlugins {\n const endpointPaths = new Set<string>();\n\n for (const value of Object.values(authApi)) {\n if (isBetterAuthEndpoint(value)) {\n endpointPaths.add(value.path);\n }\n }\n\n return {\n // apiKey plugin registers /api-key/create\n apiKey: endpointPaths.has('/api-key/create'),\n // organization plugin registers /organization/create\n organization: endpointPaths.has('/organization/create'),\n };\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Convert a camelCase key like 'signInEmail' to a readable summary like 'Sign in email'\n */\nfunction formatOperationSummary(key: string): string {\n return key\n .replace(/([A-Z])/g, ' $1')\n .replace(/^./, (s) => s.toUpperCase())\n .trim();\n}\n\n/**\n * Check if an endpoint path should have userFields merged into its request body.\n */\nfunction isUserFieldEndpoint(path: string): boolean {\n return path === '/sign-up/email' || path === '/update-user';\n}\n\n/**\n * Merge user-defined fields into an existing requestBody schema.\n * For updateUser, all fields are treated as optional regardless of their `required` setting.\n */\nfunction mergeUserFieldsIntoRequestBody(\n requestBody: Record<string, unknown>,\n userFields: NonNullable<BetterAuthOpenApiOptions['userFields']>,\n endpointPath: string,\n): void {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const content = (requestBody as any)?.content?.['application/json'];\n if (!content?.schema) return;\n\n const schema = content.schema as Record<string, unknown>;\n\n if (!schema.properties) schema.properties = {};\n if (!schema.required) schema.required = [];\n\n const props = schema.properties as Record<string, unknown>;\n const required = schema.required as string[];\n\n for (const [name, field] of Object.entries(userFields)) {\n // Skip input: false fields (output-only, not accepted in request body)\n if (field.input === false) continue;\n\n // For updateUser, all fields are optional\n const isRequired = endpointPath === '/update-user' ? false : (field.required ?? false);\n\n const prop: Record<string, unknown> = { type: field.type };\n if (field.description) prop.description = field.description;\n props[name] = prop;\n\n if (isRequired && !required.includes(name)) {\n required.push(name);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { i as CacheStore, n as CacheSetOptions, r as CacheStats, t as CacheLogger } from "../interface-CZe8IkMf.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-7THaI5mt.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 };
|
|
146
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/cache/memory.ts","../../src/cache/redis.ts","../../src/cache/keys.ts"],"mappings":";;;;UAQiB,uBAAA;;EAEf,YAAA;EAFe;EAIf,UAAA;;EAEA,iBAAA;EAJA;;;;EASA,aAAA;EAWA;;;;;EALA,cAAA;EAiB2B;;;;EAZ3B,iBAAA;EA0CwB;EAxCxB,MAAA,GAAS,WAAA;AAAA;;;;;;;;cAUE,gBAAA,8BAA8C,UAAA,CAAW,MAAA;EAAA,SAC3D,IAAA;EAAA,iBAEQ,KAAA;EAAA,iBACA,YAAA;EAAA,iBACA,UAAA;EAAA,iBACA,aAAA;EAAA,iBACA,cAAA;EAAA,iBACA,iBAAA;EAAA,iBACA,MAAA;EAAA,iBACA,YAAA;EAAA,QAET,YAAA;EAAA,QACA,KAAA;EAAA,QACA,OAAA;EAAA,QACA,UAAA;cAEI,OAAA,GAAS,uBAAA;EAaf,GAAA,CAAI,GAAA,WAAc,OAAA,CAAQ,MAAA;EAoB1B,GAAA,CAAI,GAAA,UAAa,KAAA,EAAO,MAAA,EAAQ,OAAA,GAAS,eAAA,GAAuB,OAAA;EA0BhE,MAAA,CAAO,GAAA,WAAc,OAAA;EAKrB,KAAA,CAAA,GAAS,OAAA;EAKT,KAAA,CAAA,GAAS,OAAA;EAMf,KAAA,CAAA,GAAS,UAAA;EAAA,QAUD,WAAA;EAAA,QAKA,YAAA;EAAA,QAUA,kBAAA;EAAA,QAYA,cAAA;EAAA,QASA,YAAA;AAAA;;;UClLO,gBAAA;EACf,GAAA,CAAI,GAAA,WAAc,OAAA;EAClB,GAAA,CACE,GAAA,UACA,KAAA,UACA,OAAA;IACE,EAAA;IACA,EAAA;IACA,EAAA;IACA,EAAA;EAAA,IAED,OAAA;EACH,GAAA,CAAI,GAAA,sBAAyB,OAAA;EDA7B;;;;;ECMA,IAAA,EACE,MAAA,sBACG,IAAA,wBACF,OAAA;EDSiB;ECPpB,QAAA,KAAa,aAAA;AAAA;AAAA,UAGE,aAAA;EACf,GAAA,CAAI,GAAA;EACJ,IAAA,IAAQ,OAAA;AAAA;AAAA,UAGO,sBAAA;EDuCS;ECrCxB,MAAA,EAAQ,gBAAA;EDyDuC;ECvD/C,MAAA;EDiF2B;EC/E3B,YAAA;EDyFe;ECvFf,aAAA;AAAA;;;;;;cAQW,eAAA,8BAA6C,UAAA,CAAW,MAAA;EAAA,SAC1D,IAAA;EAAA,iBAEQ,MAAA;EAAA,iBACA,MAAA;EAAA,iBACA,YAAA;EAAA,iBACA,aAAA;EAAA,QAET,KAAA;EAAA,QACA,OAAA;cAEI,OAAA,EAAS,sBAAA;EAOf,GAAA,CAAI,GAAA,WAAc,OAAA,CAAQ,MAAA;EAiB1B,GAAA,CAAI,GAAA,UAAa,KAAA,EAAO,MAAA,EAAQ,OAAA,GAAS,eAAA,GAAuB,OAAA;EAahE,MAAA,CAAO,GAAA,WAAc,OAAA;EAIrB,KAAA,CAAA,GAAS,OAAA;;EAKT,cAAA,CAAe,MAAA,WAAiB,OAAA;EAItC,KAAA,CAAA,GAAS,UAAA;EAAA,QAUK,aAAA;EAAA,QA2BN,UAAA;AAAA;;;;;;;AD3IV;;;;iBECgB,aAAA,CACd,QAAA,UACA,SAAA,UACA,eAAA,UACA,MAAA,EAAQ,MAAA,mBACR,MAAA,WACA,KAAA;;iBASc,UAAA,CAAW,QAAA;;iBAKX,aAAA,CAAc,GAAA;;;;;;iBASd,UAAA,CAAW,MAAA,EAAQ,MAAA"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { i as versionKey, n as hashParams, r as tagVersionKey, t as buildQueryKey } from "../keys-BqNejWup.mjs";
|
|
2
|
+
import { t as MemoryCacheStore } from "../memory-cQgelFOj.mjs";
|
|
3
|
+
import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-DMBnp2Q0.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 };
|
|
92
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/cache/redis.ts"],"sourcesContent":["import type { CacheSetOptions, CacheStats, CacheStore } from './interface.js';\n\nexport interface RedisCacheClient {\n get(key: string): Promise<string | null>;\n set(\n key: string,\n value: string,\n options?: {\n EX?: number;\n PX?: number;\n NX?: boolean;\n XX?: boolean;\n }\n ): Promise<string | null | unknown>;\n del(key: string | string[]): Promise<number>;\n /**\n * Optional: enables prefix-based `clear()` and `deleteByPrefix()` via SCAN.\n * Compatible with both ioredis and node-redis.\n * If not provided, `clear()` is a safe no-op.\n */\n scan?(\n cursor: string | number,\n ...args: (string | number)[]\n ): Promise<[string | number, string[]]>;\n /** Optional: pipeline for batched commands (ioredis compatible) */\n pipeline?(): RedisPipeline;\n}\n\nexport interface RedisPipeline {\n del(key: string): unknown;\n exec(): Promise<unknown>;\n}\n\nexport interface RedisCacheStoreOptions {\n /** Redis client instance */\n client: RedisCacheClient;\n /** Key prefix for namespacing (default: 'arc:cache:') */\n prefix?: string;\n /** Default TTL in milliseconds (default: 60_000) */\n defaultTtlMs?: number;\n /** Maximum serialized entry size in bytes. Oversized entries are skipped. */\n maxEntryBytes?: number;\n}\n\n/**\n * Redis-backed cache store.\n * Suitable for multi-instance and horizontally scaled deployments.\n * Uses pipeline batching when available for bulk operations.\n */\nexport class RedisCacheStore<TValue = unknown> implements CacheStore<TValue> {\n readonly name = 'redis-cache';\n\n private readonly client: RedisCacheClient;\n private readonly prefix: string;\n private readonly defaultTtlMs: number;\n private readonly maxEntryBytes: number;\n\n private _hits = 0;\n private _misses = 0;\n\n constructor(options: RedisCacheStoreOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? 'arc:cache:';\n this.defaultTtlMs = options.defaultTtlMs ?? 60_000;\n this.maxEntryBytes = options.maxEntryBytes ?? 0; // 0 = no limit\n }\n\n async get(key: string): Promise<TValue | undefined> {\n const data = await this.client.get(this.withPrefix(key));\n if (!data) {\n this._misses++;\n return undefined;\n }\n\n try {\n this._hits++;\n return JSON.parse(data) as TValue;\n } catch {\n this._misses++;\n this._hits--; // undo the hit — it's a corrupt entry\n return undefined;\n }\n }\n\n async set(key: string, value: TValue, options: CacheSetOptions = {}): Promise<void> {\n const ttlMs = options.ttlMs ?? this.defaultTtlMs;\n if (!Number.isFinite(ttlMs) || ttlMs <= 0) return;\n\n const payload = JSON.stringify(value);\n\n if (this.maxEntryBytes > 0 && Buffer.byteLength(payload, 'utf8') > this.maxEntryBytes) {\n return; // skip oversized entry\n }\n\n await this.client.set(this.withPrefix(key), payload, { PX: Math.ceil(ttlMs) });\n }\n\n async delete(key: string): Promise<void> {\n await this.client.del(this.withPrefix(key));\n }\n\n async clear(): Promise<void> {\n await this.scanAndDelete(`${this.prefix}*`);\n }\n\n /** Delete all keys matching `this.prefix + prefix + *`. Returns count deleted. */\n async deleteByPrefix(prefix: string): Promise<number> {\n return this.scanAndDelete(`${this.prefix}${prefix}*`);\n }\n\n stats(): CacheStats {\n return {\n entries: -1, // not cheaply available in Redis\n memoryBytes: -1,\n hits: this._hits,\n misses: this._misses,\n evictions: -1, // Redis handles eviction internally\n };\n }\n\n private async scanAndDelete(pattern: string): Promise<number> {\n if (!this.client.scan) return 0;\n\n const BATCH_SIZE = 200;\n let cursor: string | number = '0';\n let deleted = 0;\n\n do {\n const [nextCursor, keys] = await this.client.scan(\n cursor, 'MATCH', pattern, 'COUNT', BATCH_SIZE,\n );\n cursor = nextCursor;\n if (keys.length > 0) {\n if (this.client.pipeline) {\n const pipe = this.client.pipeline();\n for (const key of keys) pipe.del(key);\n await pipe.exec();\n } else {\n await this.client.del(keys);\n }\n deleted += keys.length;\n }\n } while (String(cursor) !== '0');\n\n return deleted;\n }\n\n private withPrefix(key: string): string {\n return `${this.prefix}${key}`;\n }\n}\n\nexport default RedisCacheStore;\n"],"mappings":";;;;;;;;;;AAiDA,IAAa,kBAAb,MAA6E;CAC3E,AAAS,OAAO;CAEhB,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CAEjB,AAAQ,QAAQ;CAChB,AAAQ,UAAU;CAElB,YAAY,SAAiC;AAC3C,OAAK,SAAS,QAAQ;AACtB,OAAK,SAAS,QAAQ,UAAU;AAChC,OAAK,eAAe,QAAQ,gBAAgB;AAC5C,OAAK,gBAAgB,QAAQ,iBAAiB;;CAGhD,MAAM,IAAI,KAA0C;EAClD,MAAM,OAAO,MAAM,KAAK,OAAO,IAAI,KAAK,WAAW,IAAI,CAAC;AACxD,MAAI,CAAC,MAAM;AACT,QAAK;AACL;;AAGF,MAAI;AACF,QAAK;AACL,UAAO,KAAK,MAAM,KAAK;UACjB;AACN,QAAK;AACL,QAAK;AACL;;;CAIJ,MAAM,IAAI,KAAa,OAAe,UAA2B,EAAE,EAAiB;EAClF,MAAM,QAAQ,QAAQ,SAAS,KAAK;AACpC,MAAI,CAAC,OAAO,SAAS,MAAM,IAAI,SAAS,EAAG;EAE3C,MAAM,UAAU,KAAK,UAAU,MAAM;AAErC,MAAI,KAAK,gBAAgB,KAAK,OAAO,WAAW,SAAS,OAAO,GAAG,KAAK,cACtE;AAGF,QAAM,KAAK,OAAO,IAAI,KAAK,WAAW,IAAI,EAAE,SAAS,EAAE,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;;CAGhF,MAAM,OAAO,KAA4B;AACvC,QAAM,KAAK,OAAO,IAAI,KAAK,WAAW,IAAI,CAAC;;CAG7C,MAAM,QAAuB;AAC3B,QAAM,KAAK,cAAc,GAAG,KAAK,OAAO,GAAG;;;CAI7C,MAAM,eAAe,QAAiC;AACpD,SAAO,KAAK,cAAc,GAAG,KAAK,SAAS,OAAO,GAAG;;CAGvD,QAAoB;AAClB,SAAO;GACL,SAAS;GACT,aAAa;GACb,MAAM,KAAK;GACX,QAAQ,KAAK;GACb,WAAW;GACZ;;CAGH,MAAc,cAAc,SAAkC;AAC5D,MAAI,CAAC,KAAK,OAAO,KAAM,QAAO;EAE9B,MAAM,aAAa;EACnB,IAAI,SAA0B;EAC9B,IAAI,UAAU;AAEd,KAAG;GACD,MAAM,CAAC,YAAY,QAAQ,MAAM,KAAK,OAAO,KAC3C,QAAQ,SAAS,SAAS,SAAS,WACpC;AACD,YAAS;AACT,OAAI,KAAK,SAAS,GAAG;AACnB,QAAI,KAAK,OAAO,UAAU;KACxB,MAAM,OAAO,KAAK,OAAO,UAAU;AACnC,UAAK,MAAM,OAAO,KAAM,MAAK,IAAI,IAAI;AACrC,WAAM,KAAK,MAAM;UAEjB,OAAM,KAAK,OAAO,IAAI,KAAK;AAE7B,eAAW,KAAK;;WAEX,OAAO,OAAO,KAAK;AAE5B,SAAO;;CAGT,AAAQ,WAAW,KAAqB;AACtC,SAAO,GAAG,KAAK,SAAS"}
|
|
@@ -0,0 +1,94 @@
|
|
|
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 };
|
|
94
|
+
//# sourceMappingURL=caching-Bl28lYsR.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caching-Bl28lYsR.mjs","names":[],"sources":["../src/plugins/caching.ts"],"sourcesContent":["/**\n * Caching Plugin\n *\n * Adds ETag and Cache-Control headers to GET/HEAD responses.\n * Supports conditional requests (304 Not Modified) for bandwidth savings.\n *\n * @example\n * import { cachingPlugin } from '@classytic/arc/plugins';\n *\n * // Basic — ETag + conditional requests, no browser caching\n * await fastify.register(cachingPlugin);\n *\n * // With cache rules per path\n * await fastify.register(cachingPlugin, {\n * rules: [\n * { match: '/api/products', maxAge: 60 },\n * { match: '/api/categories', maxAge: 300, staleWhileRevalidate: 60 },\n * ],\n * });\n */\n\nimport fp from 'fastify-plugin';\nimport type { FastifyInstance, FastifyPluginAsync } from 'fastify';\n\nexport interface CachingRule {\n /** Path prefix to match (e.g., '/api/products') */\n match: string;\n /** Cache-Control max-age in seconds */\n maxAge: number;\n /** Cache-Control: private vs public (default: public) */\n private?: boolean;\n /** stale-while-revalidate directive in seconds */\n staleWhileRevalidate?: number;\n}\n\nexport interface CachingOptions {\n /** Default max-age in seconds for Cache-Control (default: 0 = no-cache) */\n maxAge?: number;\n /** Enable ETag generation (default: true) */\n etag?: boolean;\n /** Enable conditional requests — 304 Not Modified (default: true) */\n conditional?: boolean;\n /** HTTP methods to cache (default: ['GET', 'HEAD']) */\n methods?: string[];\n /** Paths to exclude from caching (prefix match) */\n exclude?: string[];\n /** Custom cache rules per path prefix */\n rules?: CachingRule[];\n}\n\n// ============================================================================\n// FNV-1a Hash (fast, non-cryptographic)\n// ============================================================================\n\nconst FNV_OFFSET = 2166136261;\nconst FNV_PRIME = 16777619;\n\n/** Fast non-cryptographic hash for ETag generation */\nfunction fnv1a(data: string): string {\n let hash = FNV_OFFSET;\n for (let i = 0; i < data.length; i++) {\n hash ^= data.charCodeAt(i);\n hash = (hash * FNV_PRIME) >>> 0;\n }\n return hash.toString(36);\n}\n\n// ============================================================================\n// Plugin\n// ============================================================================\n\nconst cachingPlugin: FastifyPluginAsync<CachingOptions> = async (\n fastify: FastifyInstance,\n opts: CachingOptions = {},\n) => {\n const {\n maxAge = 0,\n etag = true,\n conditional = true,\n methods = ['GET', 'HEAD'],\n exclude = [],\n rules = [],\n } = opts;\n\n const methodSet = new Set(methods.map((m) => m.toUpperCase()));\n\n /** Find the first matching rule for a URL path */\n function findRule(url: string): CachingRule | undefined {\n // Strip query string\n const path = url.split('?')[0]!;\n return rules.find((r) => path.startsWith(r.match));\n }\n\n /** Build Cache-Control header value */\n function buildCacheControl(rule?: CachingRule): string {\n const age = rule?.maxAge ?? maxAge;\n if (age <= 0) return 'no-cache';\n\n const parts: string[] = [];\n parts.push(rule?.private ? 'private' : 'public');\n parts.push(`max-age=${age}`);\n if (rule?.staleWhileRevalidate) {\n parts.push(`stale-while-revalidate=${rule.staleWhileRevalidate}`);\n }\n return parts.join(', ');\n }\n\n // onSend hook — runs just before the response is sent\n fastify.addHook('onSend', async (request, reply, payload) => {\n const url = request.url;\n\n // Skip excluded paths\n if (exclude.some((p) => url.startsWith(p))) {\n return payload;\n }\n\n const method = request.method.toUpperCase();\n\n // Mutation methods always get no-store\n if (!methodSet.has(method)) {\n if (!reply.hasHeader('cache-control')) {\n reply.header('cache-control', 'no-store');\n }\n return payload;\n }\n\n // Only cache 2xx responses\n const statusCode = reply.statusCode;\n if (statusCode < 200 || statusCode >= 300) {\n return payload;\n }\n\n // Don't override user-set Cache-Control\n if (!reply.hasHeader('cache-control')) {\n const rule = findRule(url);\n reply.header('cache-control', buildCacheControl(rule));\n }\n\n // ETag generation\n if (etag && payload) {\n const body = typeof payload === 'string' ? payload : String(payload);\n const tag = `\"${fnv1a(body)}\"`;\n reply.header('etag', tag);\n\n // Conditional request: check If-None-Match\n if (conditional) {\n const ifNoneMatch = request.headers['if-none-match'];\n if (ifNoneMatch && ifNoneMatch === tag) {\n reply.code(304);\n return '';\n }\n }\n }\n\n return payload;\n });\n\n fastify.log?.debug?.('Caching plugin registered');\n};\n\nexport default fp(cachingPlugin, {\n name: 'arc-caching',\n fastify: '5.x',\n});\n\nexport { cachingPlugin };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAM,aAAa;AACnB,MAAM,YAAY;;AAGlB,SAAS,MAAM,MAAsB;CACnC,IAAI,OAAO;AACX,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAQ,KAAK,WAAW,EAAE;AAC1B,SAAQ,OAAO,cAAe;;AAEhC,QAAO,KAAK,SAAS,GAAG;;AAO1B,MAAM,gBAAoD,OACxD,SACA,OAAuB,EAAE,KACtB;CACH,MAAM,EACJ,SAAS,GACT,OAAO,MACP,cAAc,MACd,UAAU,CAAC,OAAO,OAAO,EACzB,UAAU,EAAE,EACZ,QAAQ,EAAE,KACR;CAEJ,MAAM,YAAY,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,aAAa,CAAC,CAAC;;CAG9D,SAAS,SAAS,KAAsC;EAEtD,MAAM,OAAO,IAAI,MAAM,IAAI,CAAC;AAC5B,SAAO,MAAM,MAAM,MAAM,KAAK,WAAW,EAAE,MAAM,CAAC;;;CAIpD,SAAS,kBAAkB,MAA4B;EACrD,MAAM,MAAM,MAAM,UAAU;AAC5B,MAAI,OAAO,EAAG,QAAO;EAErB,MAAM,QAAkB,EAAE;AAC1B,QAAM,KAAK,MAAM,UAAU,YAAY,SAAS;AAChD,QAAM,KAAK,WAAW,MAAM;AAC5B,MAAI,MAAM,qBACR,OAAM,KAAK,0BAA0B,KAAK,uBAAuB;AAEnE,SAAO,MAAM,KAAK,KAAK;;AAIzB,SAAQ,QAAQ,UAAU,OAAO,SAAS,OAAO,YAAY;EAC3D,MAAM,MAAM,QAAQ;AAGpB,MAAI,QAAQ,MAAM,MAAM,IAAI,WAAW,EAAE,CAAC,CACxC,QAAO;EAGT,MAAM,SAAS,QAAQ,OAAO,aAAa;AAG3C,MAAI,CAAC,UAAU,IAAI,OAAO,EAAE;AAC1B,OAAI,CAAC,MAAM,UAAU,gBAAgB,CACnC,OAAM,OAAO,iBAAiB,WAAW;AAE3C,UAAO;;EAIT,MAAM,aAAa,MAAM;AACzB,MAAI,aAAa,OAAO,cAAc,IACpC,QAAO;AAIT,MAAI,CAAC,MAAM,UAAU,gBAAgB,EAAE;GACrC,MAAM,OAAO,SAAS,IAAI;AAC1B,SAAM,OAAO,iBAAiB,kBAAkB,KAAK,CAAC;;AAIxD,MAAI,QAAQ,SAAS;GAEnB,MAAM,MAAM,IAAI,MADH,OAAO,YAAY,WAAW,UAAU,OAAO,QAAQ,CACzC,CAAC;AAC5B,SAAM,OAAO,QAAQ,IAAI;AAGzB,OAAI,aAAa;IACf,MAAM,cAAc,QAAQ,QAAQ;AACpC,QAAI,eAAe,gBAAgB,KAAK;AACtC,WAAM,KAAK,IAAI;AACf,YAAO;;;;AAKb,SAAO;GACP;AAEF,SAAQ,KAAK,QAAQ,4BAA4B;;AAGnD,sBAAe,GAAG,eAAe;CAC/B,MAAM;CACN,SAAS;CACV,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
//#region \0rolldown/runtime.js
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __exportAll = (all, no_symbols) => {
|
|
6
|
+
let target = {};
|
|
7
|
+
for (var name in all) {
|
|
8
|
+
__defProp(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
if (!no_symbols) {
|
|
14
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
15
|
+
}
|
|
16
|
+
return target;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { __exportAll as t };
|