alepha 0.13.1 → 0.13.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 +1 -1
- package/dist/api-files/index.d.ts +28 -91
- package/dist/api-files/index.js +10 -755
- package/dist/api-files/index.js.map +1 -1
- package/dist/api-jobs/index.d.ts +46 -46
- package/dist/api-jobs/index.js +13 -13
- package/dist/api-jobs/index.js.map +1 -1
- package/dist/api-notifications/index.d.ts +129 -146
- package/dist/api-notifications/index.js +17 -39
- package/dist/api-notifications/index.js.map +1 -1
- package/dist/api-parameters/index.d.ts +21 -22
- package/dist/api-parameters/index.js +22 -22
- package/dist/api-parameters/index.js.map +1 -1
- package/dist/api-users/index.d.ts +223 -2000
- package/dist/api-users/index.js +914 -4787
- package/dist/api-users/index.js.map +1 -1
- package/dist/api-verifications/index.d.ts +96 -96
- package/dist/batch/index.d.ts +13 -13
- package/dist/batch/index.js +8 -8
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +14 -14
- package/dist/bucket/index.js +12 -12
- package/dist/bucket/index.js.map +1 -1
- package/dist/cache/index.d.ts +11 -11
- package/dist/cache/index.js +9 -9
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.d.ts +28 -26
- package/dist/cli/index.js +50 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +19 -19
- package/dist/command/index.js +25 -25
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +218 -218
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +232 -232
- package/dist/core/index.js +218 -218
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +2113 -0
- package/dist/core/index.native.js.map +1 -0
- package/dist/datetime/index.d.ts +9 -9
- package/dist/datetime/index.js +7 -7
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/index.d.ts +16 -16
- package/dist/email/index.js +9 -9
- package/dist/email/index.js.map +1 -1
- package/dist/file/index.js +1 -1
- package/dist/file/index.js.map +1 -1
- package/dist/lock/index.d.ts +9 -9
- package/dist/lock/index.js +8 -8
- package/dist/lock/index.js.map +1 -1
- package/dist/lock-redis/index.js +3 -66
- package/dist/lock-redis/index.js.map +1 -1
- package/dist/logger/index.d.ts +5 -5
- package/dist/logger/index.js +8 -8
- package/dist/logger/index.js.map +1 -1
- package/dist/orm/index.browser.js +114 -114
- package/dist/orm/index.browser.js.map +1 -1
- package/dist/orm/index.d.ts +218 -218
- package/dist/orm/index.js +46 -46
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/index.d.ts +29 -29
- package/dist/queue/index.js +20 -20
- package/dist/queue/index.js.map +1 -1
- package/dist/queue-redis/index.d.ts +2 -2
- package/dist/redis/index.d.ts +10 -10
- package/dist/retry/index.d.ts +19 -19
- package/dist/retry/index.js +7 -7
- package/dist/retry/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +16 -16
- package/dist/scheduler/index.js +9 -9
- package/dist/scheduler/index.js.map +1 -1
- package/dist/security/index.d.ts +80 -80
- package/dist/security/index.js +32 -32
- package/dist/security/index.js.map +1 -1
- package/dist/server/index.browser.js +1 -1
- package/dist/server/index.browser.js.map +1 -1
- package/dist/server/index.d.ts +101 -101
- package/dist/server/index.js +16 -16
- package/dist/server/index.js.map +1 -1
- package/dist/server-auth/index.browser.js +4 -982
- package/dist/server-auth/index.browser.js.map +1 -1
- package/dist/server-auth/index.d.ts +204 -785
- package/dist/server-auth/index.js +47 -1239
- package/dist/server-auth/index.js.map +1 -1
- package/dist/server-cache/index.d.ts +10 -10
- package/dist/server-cache/index.js +2 -2
- package/dist/server-cache/index.js.map +1 -1
- package/dist/server-compress/index.d.ts +4 -4
- package/dist/server-compress/index.js +1 -1
- package/dist/server-compress/index.js.map +1 -1
- package/dist/server-cookies/index.browser.js +8 -8
- package/dist/server-cookies/index.browser.js.map +1 -1
- package/dist/server-cookies/index.d.ts +17 -17
- package/dist/server-cookies/index.js +10 -10
- package/dist/server-cookies/index.js.map +1 -1
- package/dist/server-cors/index.d.ts +17 -17
- package/dist/server-cors/index.js +9 -9
- package/dist/server-cors/index.js.map +1 -1
- package/dist/server-health/index.d.ts +19 -19
- package/dist/server-helmet/index.d.ts +1 -1
- package/dist/server-links/index.browser.js +12 -12
- package/dist/server-links/index.browser.js.map +1 -1
- package/dist/server-links/index.d.ts +59 -251
- package/dist/server-links/index.js +23 -502
- package/dist/server-links/index.js.map +1 -1
- package/dist/server-metrics/index.d.ts +4 -4
- package/dist/server-multipart/index.d.ts +2 -2
- package/dist/server-proxy/index.d.ts +12 -12
- package/dist/server-proxy/index.js +10 -10
- package/dist/server-proxy/index.js.map +1 -1
- package/dist/server-rate-limit/index.d.ts +22 -22
- package/dist/server-rate-limit/index.js +12 -12
- package/dist/server-rate-limit/index.js.map +1 -1
- package/dist/server-security/index.d.ts +22 -22
- package/dist/server-security/index.js +15 -15
- package/dist/server-security/index.js.map +1 -1
- package/dist/server-static/index.d.ts +14 -14
- package/dist/server-static/index.js +8 -8
- package/dist/server-static/index.js.map +1 -1
- package/dist/server-swagger/index.d.ts +25 -184
- package/dist/server-swagger/index.js +21 -724
- package/dist/server-swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +14 -14
- package/dist/sms/index.js +9 -9
- package/dist/sms/index.js.map +1 -1
- package/dist/thread/index.d.ts +11 -11
- package/dist/thread/index.js +17 -17
- package/dist/thread/index.js.map +1 -1
- package/dist/topic/index.d.ts +26 -26
- package/dist/topic/index.js +16 -16
- package/dist/topic/index.js.map +1 -1
- package/dist/topic-redis/index.d.ts +1 -1
- package/dist/vite/index.d.ts +3 -3
- package/dist/vite/index.js +8 -8
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.browser.js +11 -11
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.d.ts +58 -58
- package/dist/websocket/index.js +13 -13
- package/dist/websocket/index.js.map +1 -1
- package/package.json +113 -52
- package/src/api-files/services/FileService.ts +5 -7
- package/src/api-jobs/index.ts +1 -1
- package/src/api-jobs/{descriptors → primitives}/$job.ts +8 -8
- package/src/api-jobs/providers/JobProvider.ts +9 -9
- package/src/api-jobs/services/JobService.ts +5 -5
- package/src/api-notifications/index.ts +5 -15
- package/src/api-notifications/{descriptors → primitives}/$notification.ts +10 -10
- package/src/api-notifications/services/NotificationSenderService.ts +3 -3
- package/src/api-parameters/index.ts +1 -1
- package/src/api-parameters/{descriptors → primitives}/$config.ts +7 -12
- package/src/api-users/index.ts +1 -1
- package/src/api-users/{descriptors → primitives}/$userRealm.ts +8 -8
- package/src/api-users/providers/UserRealmProvider.ts +1 -1
- package/src/batch/index.ts +3 -3
- package/src/batch/{descriptors → primitives}/$batch.ts +13 -16
- package/src/bucket/index.ts +8 -8
- package/src/bucket/{descriptors → primitives}/$bucket.ts +8 -8
- package/src/bucket/providers/LocalFileStorageProvider.ts +3 -3
- package/src/cache/index.ts +4 -4
- package/src/cache/{descriptors → primitives}/$cache.ts +15 -15
- package/src/cli/apps/AlephaPackageBuilderCli.ts +24 -2
- package/src/cli/commands/DrizzleCommands.ts +6 -6
- package/src/cli/commands/VerifyCommands.ts +1 -1
- package/src/cli/commands/ViteCommands.ts +6 -1
- package/src/cli/services/ProjectUtils.ts +34 -3
- package/src/command/index.ts +5 -5
- package/src/command/{descriptors → primitives}/$command.ts +9 -12
- package/src/command/providers/CliProvider.ts +10 -10
- package/src/core/Alepha.ts +30 -33
- package/src/core/constants/KIND.ts +1 -1
- package/src/core/constants/OPTIONS.ts +1 -1
- package/src/core/helpers/{descriptor.ts → primitive.ts} +18 -18
- package/src/core/helpers/ref.ts +1 -1
- package/src/core/index.shared.ts +8 -8
- package/src/core/{descriptors → primitives}/$context.ts +5 -5
- package/src/core/{descriptors → primitives}/$hook.ts +4 -4
- package/src/core/{descriptors → primitives}/$inject.ts +2 -2
- package/src/core/{descriptors → primitives}/$module.ts +9 -9
- package/src/core/{descriptors → primitives}/$use.ts +2 -2
- package/src/core/providers/CodecManager.ts +1 -1
- package/src/core/providers/JsonSchemaCodec.ts +1 -1
- package/src/core/providers/StateManager.ts +2 -2
- package/src/datetime/index.ts +3 -3
- package/src/datetime/{descriptors → primitives}/$interval.ts +6 -6
- package/src/email/index.ts +4 -4
- package/src/email/{descriptors → primitives}/$email.ts +8 -8
- package/src/file/index.ts +1 -1
- package/src/lock/index.ts +3 -3
- package/src/lock/{descriptors → primitives}/$lock.ts +10 -10
- package/src/logger/index.ts +8 -8
- package/src/logger/{descriptors → primitives}/$logger.ts +2 -2
- package/src/logger/services/Logger.ts +1 -1
- package/src/orm/constants/PG_SYMBOLS.ts +2 -2
- package/src/orm/index.browser.ts +2 -2
- package/src/orm/index.ts +8 -8
- package/src/orm/{descriptors → primitives}/$entity.ts +11 -11
- package/src/orm/{descriptors → primitives}/$repository.ts +2 -2
- package/src/orm/{descriptors → primitives}/$sequence.ts +8 -8
- package/src/orm/{descriptors → primitives}/$transaction.ts +4 -4
- package/src/orm/providers/PostgresTypeProvider.ts +3 -3
- package/src/orm/providers/RepositoryProvider.ts +4 -4
- package/src/orm/providers/drivers/DatabaseProvider.ts +7 -7
- package/src/orm/services/ModelBuilder.ts +9 -9
- package/src/orm/services/PgRelationManager.ts +2 -2
- package/src/orm/services/PostgresModelBuilder.ts +5 -5
- package/src/orm/services/Repository.ts +7 -7
- package/src/orm/services/SqliteModelBuilder.ts +5 -5
- package/src/queue/index.ts +7 -7
- package/src/queue/{descriptors → primitives}/$consumer.ts +15 -15
- package/src/queue/{descriptors → primitives}/$queue.ts +12 -12
- package/src/queue/providers/WorkerProvider.ts +7 -7
- package/src/retry/index.ts +3 -3
- package/src/retry/{descriptors → primitives}/$retry.ts +14 -14
- package/src/scheduler/index.ts +3 -3
- package/src/scheduler/{descriptors → primitives}/$scheduler.ts +9 -9
- package/src/scheduler/providers/CronProvider.ts +1 -1
- package/src/security/index.ts +9 -9
- package/src/security/{descriptors → primitives}/$permission.ts +7 -7
- package/src/security/{descriptors → primitives}/$realm.ts +6 -12
- package/src/security/{descriptors → primitives}/$role.ts +12 -12
- package/src/security/{descriptors → primitives}/$serviceAccount.ts +8 -8
- package/src/server/index.browser.ts +1 -1
- package/src/server/index.ts +14 -14
- package/src/server/{descriptors → primitives}/$action.ts +13 -13
- package/src/server/{descriptors → primitives}/$route.ts +9 -9
- package/src/server/providers/NodeHttpServerProvider.ts +1 -1
- package/src/server/services/HttpClient.ts +1 -1
- package/src/server-auth/index.browser.ts +1 -1
- package/src/server-auth/index.ts +6 -6
- package/src/server-auth/{descriptors → primitives}/$auth.ts +10 -10
- package/src/server-auth/{descriptors → primitives}/$authCredentials.ts +4 -4
- package/src/server-auth/{descriptors → primitives}/$authGithub.ts +4 -4
- package/src/server-auth/{descriptors → primitives}/$authGoogle.ts +4 -4
- package/src/server-auth/providers/ServerAuthProvider.ts +4 -4
- package/src/server-cache/providers/ServerCacheProvider.ts +7 -7
- package/src/server-compress/providers/ServerCompressProvider.ts +3 -3
- package/src/server-cookies/index.browser.ts +2 -2
- package/src/server-cookies/index.ts +5 -5
- package/src/server-cookies/{descriptors → primitives}/$cookie.browser.ts +12 -12
- package/src/server-cookies/{descriptors → primitives}/$cookie.ts +13 -13
- package/src/server-cookies/providers/ServerCookiesProvider.ts +4 -4
- package/src/server-cookies/services/CookieParser.ts +1 -1
- package/src/server-cors/index.ts +3 -3
- package/src/server-cors/{descriptors → primitives}/$cors.ts +11 -13
- package/src/server-cors/providers/ServerCorsProvider.ts +5 -5
- package/src/server-links/index.browser.ts +5 -5
- package/src/server-links/index.ts +9 -9
- package/src/server-links/{descriptors → primitives}/$remote.ts +11 -11
- package/src/server-links/providers/LinkProvider.ts +7 -7
- package/src/server-links/providers/{RemoteDescriptorProvider.ts → RemotePrimitiveProvider.ts} +6 -6
- package/src/server-links/providers/ServerLinksProvider.ts +3 -3
- package/src/server-proxy/index.ts +3 -3
- package/src/server-proxy/{descriptors → primitives}/$proxy.ts +8 -8
- package/src/server-proxy/providers/ServerProxyProvider.ts +4 -4
- package/src/server-rate-limit/index.ts +6 -6
- package/src/server-rate-limit/{descriptors → primitives}/$rateLimit.ts +13 -13
- package/src/server-rate-limit/providers/ServerRateLimitProvider.ts +5 -5
- package/src/server-security/index.ts +3 -3
- package/src/server-security/{descriptors → primitives}/$basicAuth.ts +13 -13
- package/src/server-security/providers/ServerBasicAuthProvider.ts +5 -5
- package/src/server-security/providers/ServerSecurityProvider.ts +4 -4
- package/src/server-static/index.ts +3 -3
- package/src/server-static/{descriptors → primitives}/$serve.ts +8 -10
- package/src/server-static/providers/ServerStaticProvider.ts +6 -6
- package/src/server-swagger/index.ts +5 -5
- package/src/server-swagger/{descriptors → primitives}/$swagger.ts +9 -9
- package/src/server-swagger/providers/ServerSwaggerProvider.ts +11 -10
- package/src/sms/index.ts +4 -4
- package/src/sms/{descriptors → primitives}/$sms.ts +8 -8
- package/src/thread/index.ts +3 -3
- package/src/thread/{descriptors → primitives}/$thread.ts +13 -13
- package/src/thread/providers/ThreadProvider.ts +7 -9
- package/src/topic/index.ts +5 -5
- package/src/topic/{descriptors → primitives}/$subscriber.ts +14 -14
- package/src/topic/{descriptors → primitives}/$topic.ts +10 -10
- package/src/topic/providers/TopicProvider.ts +4 -4
- package/src/vite/tasks/copyAssets.ts +1 -1
- package/src/vite/tasks/generateSitemap.ts +3 -3
- package/src/vite/tasks/prerenderPages.ts +2 -2
- package/src/vite/tasks/runAlepha.ts +2 -2
- package/src/websocket/index.browser.ts +3 -3
- package/src/websocket/index.shared.ts +2 -2
- package/src/websocket/index.ts +4 -4
- package/src/websocket/interfaces/WebSocketInterfaces.ts +3 -3
- package/src/websocket/{descriptors → primitives}/$channel.ts +10 -10
- package/src/websocket/{descriptors → primitives}/$websocket.ts +8 -8
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +7 -7
- package/src/websocket/providers/WebSocketServerProvider.ts +3 -3
- package/src/websocket/services/WebSocketClient.ts +5 -5
- package/src/api-notifications/providers/MemorySmsProvider.ts +0 -20
- package/src/api-notifications/providers/SmsProvider.ts +0 -8
- /package/src/core/{descriptors → primitives}/$atom.ts +0 -0
- /package/src/core/{descriptors → primitives}/$env.ts +0 -0
- /package/src/server-auth/{descriptors → primitives}/$authApple.ts +0 -0
- /package/src/server-links/{descriptors → primitives}/$client.ts +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as alepha1 from "alepha";
|
|
2
2
|
import { Alepha } from "alepha";
|
|
3
3
|
import * as alepha_cache0 from "alepha/cache";
|
|
4
|
-
import {
|
|
4
|
+
import { CachePrimitiveOptions } from "alepha/cache";
|
|
5
5
|
import { BinaryLike } from "node:crypto";
|
|
6
6
|
import { DateTimeProvider, DurationLike } from "alepha/datetime";
|
|
7
7
|
import * as alepha_logger0 from "alepha/logger";
|
|
@@ -19,7 +19,7 @@ declare module "alepha/server" {
|
|
|
19
19
|
*/
|
|
20
20
|
cache?: ServerRouteCache;
|
|
21
21
|
}
|
|
22
|
-
interface
|
|
22
|
+
interface ActionPrimitive<TConfig extends RequestConfigSchema> {
|
|
23
23
|
invalidate: () => Promise<void>;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -27,14 +27,14 @@ declare class ServerCacheProvider {
|
|
|
27
27
|
protected readonly log: alepha_logger0.Logger;
|
|
28
28
|
protected readonly alepha: Alepha;
|
|
29
29
|
protected readonly time: DateTimeProvider;
|
|
30
|
-
protected readonly cache: alepha_cache0.
|
|
30
|
+
protected readonly cache: alepha_cache0.CachePrimitiveFn<RouteCacheEntry, any[]>;
|
|
31
31
|
generateETag(content: BinaryLike): string;
|
|
32
32
|
invalidate(route: ServerRoute): Promise<void>;
|
|
33
|
-
protected readonly onActionRequest: alepha1.
|
|
34
|
-
protected readonly onActionResponse: alepha1.
|
|
35
|
-
protected readonly onRequest: alepha1.
|
|
36
|
-
protected readonly onSend: alepha1.
|
|
37
|
-
protected readonly onResponse: alepha1.
|
|
33
|
+
protected readonly onActionRequest: alepha1.HookPrimitive<"action:onRequest">;
|
|
34
|
+
protected readonly onActionResponse: alepha1.HookPrimitive<"action:onResponse">;
|
|
35
|
+
protected readonly onRequest: alepha1.HookPrimitive<"server:onRequest">;
|
|
36
|
+
protected readonly onSend: alepha1.HookPrimitive<"server:onSend">;
|
|
37
|
+
protected readonly onResponse: alepha1.HookPrimitive<"server:onResponse">;
|
|
38
38
|
buildCacheControlHeader(cache?: ServerRouteCache): string | undefined;
|
|
39
39
|
protected durationToSeconds(duration: number | DurationLike): number;
|
|
40
40
|
protected shouldStore(cache?: ServerRouteCache): boolean;
|
|
@@ -56,11 +56,11 @@ boolean
|
|
|
56
56
|
/**
|
|
57
57
|
* If true, enables storing cached responses. (in-memory, Redis, @see alepha/cache for other providers)
|
|
58
58
|
* If a DurationLike is provided, it will be used as the TTL for the cache.
|
|
59
|
-
* If
|
|
59
|
+
* If CachePrimitiveOptions is provided, it will be used to configure the cache storage.
|
|
60
60
|
*
|
|
61
61
|
* @default false
|
|
62
62
|
*/
|
|
63
|
-
store?: true | DurationLike |
|
|
63
|
+
store?: true | DurationLike | CachePrimitiveOptions;
|
|
64
64
|
/**
|
|
65
65
|
* If true, enables ETag support for the cached responses.
|
|
66
66
|
*/
|
|
@@ -3,10 +3,10 @@ import { $cache, AlephaCache } from "alepha/cache";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { DateTimeProvider } from "alepha/datetime";
|
|
5
5
|
import { $logger } from "alepha/logger";
|
|
6
|
-
import {
|
|
6
|
+
import { ActionPrimitive } from "alepha/server";
|
|
7
7
|
|
|
8
8
|
//#region src/server-cache/providers/ServerCacheProvider.ts
|
|
9
|
-
|
|
9
|
+
ActionPrimitive.prototype.invalidate = async function() {
|
|
10
10
|
await this.alepha.inject(ServerCacheProvider).invalidate(this.route);
|
|
11
11
|
};
|
|
12
12
|
var ServerCacheProvider = class {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["directives: string[]","params: string[]"],"sources":["../../src/server-cache/providers/ServerCacheProvider.ts","../../src/server-cache/index.ts"],"sourcesContent":["import type { BinaryLike } from \"node:crypto\";\nimport { createHash } from \"node:crypto\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { $cache, type CacheDescriptorOptions } from \"alepha/cache\";\nimport { DateTimeProvider, type DurationLike } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport {\n ActionDescriptor,\n type RequestConfigSchema,\n type ServerRequest,\n type ServerRoute,\n} from \"alepha/server\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha/server\" {\n interface ServerRoute {\n /**\n * Enable caching for this route.\n * - If true: enables both store and etag\n * - If object: fine-grained control over store, etag, and cache-control headers\n *\n * @default false\n */\n cache?: ServerRouteCache;\n }\n\n interface ActionDescriptor<TConfig extends RequestConfigSchema> {\n invalidate: () => Promise<void>;\n }\n}\n\nActionDescriptor.prototype.invalidate = async function (\n this: ActionDescriptor<RequestConfigSchema>,\n) {\n await this.alepha.inject(ServerCacheProvider).invalidate(this.route);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class ServerCacheProvider {\n protected readonly log = $logger();\n protected readonly alepha = $inject(Alepha);\n protected readonly time = $inject(DateTimeProvider);\n protected readonly cache = $cache<RouteCacheEntry>({\n provider: \"memory\",\n });\n\n public generateETag(content: BinaryLike): string {\n return `\"${createHash(\"md5\").update(content).digest(\"hex\")}\"`;\n }\n\n public async invalidate(route: ServerRoute) {\n const cache = route.cache;\n if (!cache) {\n return;\n }\n\n await this.cache.invalidate(this.createCacheKey(route));\n }\n\n protected readonly onActionRequest = $hook({\n on: \"action:onRequest\",\n handler: async ({ action, request }) => {\n const cache = action.route.cache;\n\n const shouldStore = this.shouldStore(cache);\n\n // Only check cache if storing is enabled\n if (shouldStore) {\n const key = this.createCacheKey(action.route, request);\n const cached = await this.cache.get(key);\n\n if (cached) {\n const body =\n cached.contentType === \"application/json\"\n ? JSON.parse(cached.body)\n : cached.body;\n\n this.log.trace(\"Cache hit for action\", {\n key,\n action: action.name,\n });\n\n request.reply.body = body; // just re-use, full trust\n } else {\n this.log.trace(\"Cache miss for action\", {\n key,\n action: action.name,\n });\n }\n }\n },\n });\n\n protected readonly onActionResponse = $hook({\n on: \"action:onResponse\",\n handler: async ({ action, request, response }) => {\n const cache = action.route.cache;\n\n const shouldStore = this.shouldStore(cache);\n\n if (!shouldStore || !response) {\n return;\n }\n\n // Don't cache error responses (status >= 400)\n if (request.reply.status && request.reply.status >= 400) {\n return;\n }\n\n // TODO: serialize the response body, exactly like in the server response hook\n // this is bad\n const contentType =\n typeof response === \"string\" ? \"text/plain\" : \"application/json\";\n const body =\n contentType === \"text/plain\" ? response : JSON.stringify(response);\n\n const generatedEtag = this.generateETag(body);\n const lastModified = this.time.toISOString();\n\n // Store response for cached actions\n const key = this.createCacheKey(action.route, request);\n\n this.log.trace(\"Storing response\", {\n key,\n action: action.name,\n });\n\n await this.cache.set(key, {\n body: body,\n lastModified,\n contentType: contentType,\n hash: generatedEtag,\n });\n\n // Set Cache-Control header if configured (for HTTP responses)\n const cacheControl = this.buildCacheControlHeader(cache);\n if (cacheControl) {\n request.reply.setHeader(\"cache-control\", cacheControl);\n }\n },\n });\n\n protected readonly onRequest = $hook({\n on: \"server:onRequest\",\n handler: async ({ route, request }) => {\n const cache = route.cache;\n\n const shouldStore = this.shouldStore(cache);\n const shouldUseEtag = this.shouldUseEtag(cache);\n\n // Check for cached response or ETag\n if (!shouldStore && !shouldUseEtag) {\n return;\n }\n\n const key = this.createCacheKey(route, request);\n const cached = await this.cache.get(key);\n\n if (cached) {\n // Check if client has matching ETag - return 304 for both cached and etag-only routes\n if (\n request.headers[\"if-none-match\"] === cached.hash ||\n request.headers[\"if-modified-since\"] === cached.lastModified\n ) {\n request.reply.status = 304;\n request.reply.setHeader(\"etag\", cached.hash);\n request.reply.setHeader(\"last-modified\", cached.lastModified);\n this.log.trace(\"ETag match, returning 304\", {\n route: route.path,\n etag: cached.hash,\n });\n return;\n }\n\n // Only serve cached content if storing is enabled (not for etag-only routes)\n if (shouldStore) {\n this.log.trace(\"Cache hit for route\", {\n key,\n route: route.path,\n });\n\n // if the cache is found, we can skip the request processing\n // and return the cached response\n request.reply.body = cached.body;\n request.reply.status = cached.status ?? 200;\n\n if (cached.contentType) {\n request.reply.setHeader(\"Content-Type\", cached.contentType);\n }\n\n request.reply.setHeader(\"etag\", cached.hash);\n request.reply.setHeader(\"last-modified\", cached.lastModified);\n }\n } else if (shouldStore) {\n this.log.trace(\"Cache miss for route\", {\n key,\n route: route.path,\n });\n }\n },\n });\n\n protected readonly onSend = $hook({\n on: \"server:onSend\",\n handler: async ({ route, request }) => {\n // before sending the response, check if the ETag matches\n // and if so, return a 304 Not Modified response\n // -> this is only relevant for etag-only routes, not cached routes <-\n const cache = route.cache;\n\n const shouldStore = this.shouldStore(cache);\n const shouldUseEtag = this.shouldUseEtag(cache);\n\n if (request.reply.headers.etag) {\n // ETag already set, skip\n return;\n }\n\n if (\n !shouldStore &&\n shouldUseEtag &&\n request.reply.body != null &&\n (typeof request.reply.body === \"string\" ||\n Buffer.isBuffer(request.reply.body))\n ) {\n const generatedEtag = this.generateETag(request.reply.body);\n\n if (request.headers[\"if-none-match\"] === generatedEtag) {\n request.reply.status = 304;\n request.reply.body = undefined;\n request.reply.setHeader(\"etag\", generatedEtag);\n this.log.trace(\"ETag match on send, returning 304\", {\n route: route.path,\n etag: generatedEtag,\n });\n return;\n }\n }\n },\n });\n\n protected readonly onResponse = $hook({\n on: \"server:onResponse\",\n priority: \"first\",\n handler: async ({ route, request, response }) => {\n const cache = route.cache;\n\n // Set Cache-Control header if configured\n const cacheControl = this.buildCacheControlHeader(cache);\n if (cacheControl) {\n response.headers[\"cache-control\"] = cacheControl;\n }\n\n const shouldStore = this.shouldStore(cache);\n const shouldUseEtag = this.shouldUseEtag(cache);\n\n // Skip if neither cache nor etag is enabled\n if (!shouldStore && !shouldUseEtag) {\n return;\n }\n\n // Only process string responses (text, html, json, etc.)\n // Buffer is not supported by alepha/cache for now\n if (typeof response.body !== \"string\") {\n return;\n }\n\n // Don't cache error responses (status >= 400)\n if (response.status && response.status >= 400) {\n return;\n }\n\n const key = this.createCacheKey(route, request);\n const generatedEtag = this.generateETag(response.body);\n const lastModified = this.time.toISOString();\n\n // Initialize headers if not present\n response.headers ??= {};\n\n // Store response if storing is enabled\n if (shouldStore) {\n this.log.trace(\"Storing response\", {\n key,\n route: route.path,\n cache: !!cache,\n etag: shouldUseEtag,\n });\n\n await this.cache.set(key, {\n body: response.body,\n status: response.status,\n contentType: response.headers?.[\"content-type\"],\n lastModified,\n hash: generatedEtag,\n });\n }\n\n // Set ETag headers if etag is enabled\n if (shouldUseEtag) {\n response.headers.etag = generatedEtag;\n response.headers[\"last-modified\"] = lastModified;\n }\n },\n });\n\n public buildCacheControlHeader(cache?: ServerRouteCache): string | undefined {\n if (!cache) {\n return undefined;\n }\n\n // If cache is true or a DurationLike, no Cache-Control header is set\n if (\n cache === true ||\n typeof cache === \"string\" ||\n typeof cache === \"number\"\n ) {\n return undefined;\n }\n\n const control = cache.control;\n if (!control) {\n return undefined;\n }\n\n // If control is a string, return it directly\n if (typeof control === \"string\") {\n return control;\n }\n\n // If control is true, return default Cache-Control\n if (control === true) {\n return \"public, max-age=300\";\n }\n\n // Build Cache-Control from object directives\n const directives: string[] = [];\n\n if (control.public) {\n directives.push(\"public\");\n }\n if (control.private) {\n directives.push(\"private\");\n }\n if (control.noCache) {\n directives.push(\"no-cache\");\n }\n if (control.noStore) {\n directives.push(\"no-store\");\n }\n if (control.maxAge !== undefined) {\n const maxAgeSeconds = this.durationToSeconds(control.maxAge);\n directives.push(`max-age=${maxAgeSeconds}`);\n }\n if (control.sMaxAge !== undefined) {\n const sMaxAgeSeconds = this.durationToSeconds(control.sMaxAge);\n directives.push(`s-maxage=${sMaxAgeSeconds}`);\n }\n if (control.mustRevalidate) {\n directives.push(\"must-revalidate\");\n }\n if (control.proxyRevalidate) {\n directives.push(\"proxy-revalidate\");\n }\n if (control.immutable) {\n directives.push(\"immutable\");\n }\n\n return directives.length > 0 ? directives.join(\", \") : undefined;\n }\n\n protected durationToSeconds(duration: number | DurationLike): number {\n if (typeof duration === \"number\") {\n return duration;\n }\n\n return this.time.duration(duration).asSeconds();\n }\n\n protected shouldStore(cache?: ServerRouteCache): boolean {\n if (!cache) return false;\n if (cache === true) return true;\n if (typeof cache === \"object\" && cache.store) return true;\n return false;\n }\n\n protected shouldUseEtag(cache?: ServerRouteCache): boolean {\n // cache: true enables etag\n if (cache === true) return true;\n // Check object form\n if (typeof cache === \"object\" && cache.etag) return true;\n return false;\n }\n\n protected createCacheKey(route: ServerRoute, config?: ServerRequest): string {\n const params: string[] = [];\n for (const [key, value] of Object.entries(config?.params ?? {})) {\n params.push(`${key}=${value}`);\n }\n for (const [key, value] of Object.entries(config?.query ?? {})) {\n params.push(`${key}=${value}`);\n }\n\n return `${route.method}:${route.path.replaceAll(\":\", \"\")}:${params.join(\",\").replaceAll(\":\", \"\")}`;\n }\n}\n\nexport type ServerRouteCache =\n /**\n * If true, enables caching with:\n * - store: true\n * - etag: true\n */\n | boolean\n /**\n * Object configuration for fine-grained cache control.\n *\n * If empty, no caching will be applied.\n */\n | {\n /**\n * If true, enables storing cached responses. (in-memory, Redis, @see alepha/cache for other providers)\n * If a DurationLike is provided, it will be used as the TTL for the cache.\n * If CacheDescriptorOptions is provided, it will be used to configure the cache storage.\n *\n * @default false\n */\n store?: true | DurationLike | CacheDescriptorOptions;\n /**\n * If true, enables ETag support for the cached responses.\n */\n etag?: true;\n /**\n * - If true, sets Cache-Control to \"public, max-age=300\" (5 minutes).\n * - If string, sets Cache-Control to the provided value directly.\n * - If object, configures Cache-Control directives.\n */\n control?: /**\n * If true, sets Cache-Control to \"public, max-age=300\" (5 minutes).\n */\n | true\n /**\n * If string, sets Cache-Control to the provided value directly.\n */\n | string\n /**\n * If object, configures Cache-Control directives.\n */\n | {\n /**\n * Indicates that the response may be cached by any cache.\n */\n public?: boolean;\n /**\n * Indicates that the response is intended for a single user and must not be stored by a shared cache.\n */\n private?: boolean;\n /**\n * Forces caches to submit the request to the origin server for validation before releasing a cached copy.\n */\n noCache?: boolean;\n /**\n * Instructs caches not to store the response.\n */\n noStore?: boolean;\n /**\n * Maximum amount of time a resource is considered fresh.\n * Can be specified as a number (seconds) or as a DurationLike object.\n *\n * @example 300 // 5 minutes in seconds\n * @example { minutes: 5 } // 5 minutes\n * @example { hours: 1 } // 1 hour\n */\n maxAge?: number | DurationLike;\n /**\n * Overrides max-age for shared caches (e.g., CDNs).\n * Can be specified as a number (seconds) or as a DurationLike object.\n */\n sMaxAge?: number | DurationLike;\n /**\n * Indicates that once a resource becomes stale, caches must not use it without successful validation.\n */\n mustRevalidate?: boolean;\n /**\n * Similar to must-revalidate, but only for shared caches.\n */\n proxyRevalidate?: boolean;\n /**\n * Indicates that the response can be stored but must be revalidated before each use.\n */\n immutable?: boolean;\n };\n };\n\ninterface RouteCacheEntry {\n contentType?: string;\n body: any;\n status?: number;\n lastModified: string;\n hash: string;\n}\n","import { $module } from \"alepha\";\nimport { AlephaCache } from \"alepha/cache\";\nimport { ServerCacheProvider } from \"./providers/ServerCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/ServerCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Server that provides server-side caching capabilities.\n * It uses the Alepha Cache module to cache responses from server actions ($action).\n * It also provides a ETag-based cache invalidation mechanism.\n *\n * @example\n * ```ts\n * import { Alepha } from \"alepha\";\n * import { $action } from \"alepha/server\";\n * import { AlephaServerCache } from \"alepha/server/cache\";\n *\n * class ApiServer {\n * hello = $action({\n * cache: true,\n * handler: () => \"Hello, World!\",\n * });\n * }\n *\n * const alepha = Alepha.create()\n * .with(AlephaServerCache)\n * .with(ApiServer);\n *\n * run(alepha);\n * ```\n *\n * @see {@link ServerCacheProvider}\n * @module alepha.server.cache\n */\nexport const AlephaServerCache = $module({\n name: \"alepha.server.cache\",\n services: [AlephaCache, ServerCacheProvider],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["directives: string[]","params: string[]"],"sources":["../../src/server-cache/providers/ServerCacheProvider.ts","../../src/server-cache/index.ts"],"sourcesContent":["import type { BinaryLike } from \"node:crypto\";\nimport { createHash } from \"node:crypto\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport { $cache, type CachePrimitiveOptions } from \"alepha/cache\";\nimport { DateTimeProvider, type DurationLike } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport {\n ActionPrimitive,\n type RequestConfigSchema,\n type ServerRequest,\n type ServerRoute,\n} from \"alepha/server\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha/server\" {\n interface ServerRoute {\n /**\n * Enable caching for this route.\n * - If true: enables both store and etag\n * - If object: fine-grained control over store, etag, and cache-control headers\n *\n * @default false\n */\n cache?: ServerRouteCache;\n }\n\n interface ActionPrimitive<TConfig extends RequestConfigSchema> {\n invalidate: () => Promise<void>;\n }\n}\n\nActionPrimitive.prototype.invalidate = async function (\n this: ActionPrimitive<RequestConfigSchema>,\n) {\n await this.alepha.inject(ServerCacheProvider).invalidate(this.route);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class ServerCacheProvider {\n protected readonly log = $logger();\n protected readonly alepha = $inject(Alepha);\n protected readonly time = $inject(DateTimeProvider);\n protected readonly cache = $cache<RouteCacheEntry>({\n provider: \"memory\",\n });\n\n public generateETag(content: BinaryLike): string {\n return `\"${createHash(\"md5\").update(content).digest(\"hex\")}\"`;\n }\n\n public async invalidate(route: ServerRoute) {\n const cache = route.cache;\n if (!cache) {\n return;\n }\n\n await this.cache.invalidate(this.createCacheKey(route));\n }\n\n protected readonly onActionRequest = $hook({\n on: \"action:onRequest\",\n handler: async ({ action, request }) => {\n const cache = action.route.cache;\n\n const shouldStore = this.shouldStore(cache);\n\n // Only check cache if storing is enabled\n if (shouldStore) {\n const key = this.createCacheKey(action.route, request);\n const cached = await this.cache.get(key);\n\n if (cached) {\n const body =\n cached.contentType === \"application/json\"\n ? JSON.parse(cached.body)\n : cached.body;\n\n this.log.trace(\"Cache hit for action\", {\n key,\n action: action.name,\n });\n\n request.reply.body = body; // just re-use, full trust\n } else {\n this.log.trace(\"Cache miss for action\", {\n key,\n action: action.name,\n });\n }\n }\n },\n });\n\n protected readonly onActionResponse = $hook({\n on: \"action:onResponse\",\n handler: async ({ action, request, response }) => {\n const cache = action.route.cache;\n\n const shouldStore = this.shouldStore(cache);\n\n if (!shouldStore || !response) {\n return;\n }\n\n // Don't cache error responses (status >= 400)\n if (request.reply.status && request.reply.status >= 400) {\n return;\n }\n\n // TODO: serialize the response body, exactly like in the server response hook\n // this is bad\n const contentType =\n typeof response === \"string\" ? \"text/plain\" : \"application/json\";\n const body =\n contentType === \"text/plain\" ? response : JSON.stringify(response);\n\n const generatedEtag = this.generateETag(body);\n const lastModified = this.time.toISOString();\n\n // Store response for cached actions\n const key = this.createCacheKey(action.route, request);\n\n this.log.trace(\"Storing response\", {\n key,\n action: action.name,\n });\n\n await this.cache.set(key, {\n body: body,\n lastModified,\n contentType: contentType,\n hash: generatedEtag,\n });\n\n // Set Cache-Control header if configured (for HTTP responses)\n const cacheControl = this.buildCacheControlHeader(cache);\n if (cacheControl) {\n request.reply.setHeader(\"cache-control\", cacheControl);\n }\n },\n });\n\n protected readonly onRequest = $hook({\n on: \"server:onRequest\",\n handler: async ({ route, request }) => {\n const cache = route.cache;\n\n const shouldStore = this.shouldStore(cache);\n const shouldUseEtag = this.shouldUseEtag(cache);\n\n // Check for cached response or ETag\n if (!shouldStore && !shouldUseEtag) {\n return;\n }\n\n const key = this.createCacheKey(route, request);\n const cached = await this.cache.get(key);\n\n if (cached) {\n // Check if client has matching ETag - return 304 for both cached and etag-only routes\n if (\n request.headers[\"if-none-match\"] === cached.hash ||\n request.headers[\"if-modified-since\"] === cached.lastModified\n ) {\n request.reply.status = 304;\n request.reply.setHeader(\"etag\", cached.hash);\n request.reply.setHeader(\"last-modified\", cached.lastModified);\n this.log.trace(\"ETag match, returning 304\", {\n route: route.path,\n etag: cached.hash,\n });\n return;\n }\n\n // Only serve cached content if storing is enabled (not for etag-only routes)\n if (shouldStore) {\n this.log.trace(\"Cache hit for route\", {\n key,\n route: route.path,\n });\n\n // if the cache is found, we can skip the request processing\n // and return the cached response\n request.reply.body = cached.body;\n request.reply.status = cached.status ?? 200;\n\n if (cached.contentType) {\n request.reply.setHeader(\"Content-Type\", cached.contentType);\n }\n\n request.reply.setHeader(\"etag\", cached.hash);\n request.reply.setHeader(\"last-modified\", cached.lastModified);\n }\n } else if (shouldStore) {\n this.log.trace(\"Cache miss for route\", {\n key,\n route: route.path,\n });\n }\n },\n });\n\n protected readonly onSend = $hook({\n on: \"server:onSend\",\n handler: async ({ route, request }) => {\n // before sending the response, check if the ETag matches\n // and if so, return a 304 Not Modified response\n // -> this is only relevant for etag-only routes, not cached routes <-\n const cache = route.cache;\n\n const shouldStore = this.shouldStore(cache);\n const shouldUseEtag = this.shouldUseEtag(cache);\n\n if (request.reply.headers.etag) {\n // ETag already set, skip\n return;\n }\n\n if (\n !shouldStore &&\n shouldUseEtag &&\n request.reply.body != null &&\n (typeof request.reply.body === \"string\" ||\n Buffer.isBuffer(request.reply.body))\n ) {\n const generatedEtag = this.generateETag(request.reply.body);\n\n if (request.headers[\"if-none-match\"] === generatedEtag) {\n request.reply.status = 304;\n request.reply.body = undefined;\n request.reply.setHeader(\"etag\", generatedEtag);\n this.log.trace(\"ETag match on send, returning 304\", {\n route: route.path,\n etag: generatedEtag,\n });\n return;\n }\n }\n },\n });\n\n protected readonly onResponse = $hook({\n on: \"server:onResponse\",\n priority: \"first\",\n handler: async ({ route, request, response }) => {\n const cache = route.cache;\n\n // Set Cache-Control header if configured\n const cacheControl = this.buildCacheControlHeader(cache);\n if (cacheControl) {\n response.headers[\"cache-control\"] = cacheControl;\n }\n\n const shouldStore = this.shouldStore(cache);\n const shouldUseEtag = this.shouldUseEtag(cache);\n\n // Skip if neither cache nor etag is enabled\n if (!shouldStore && !shouldUseEtag) {\n return;\n }\n\n // Only process string responses (text, html, json, etc.)\n // Buffer is not supported by alepha/cache for now\n if (typeof response.body !== \"string\") {\n return;\n }\n\n // Don't cache error responses (status >= 400)\n if (response.status && response.status >= 400) {\n return;\n }\n\n const key = this.createCacheKey(route, request);\n const generatedEtag = this.generateETag(response.body);\n const lastModified = this.time.toISOString();\n\n // Initialize headers if not present\n response.headers ??= {};\n\n // Store response if storing is enabled\n if (shouldStore) {\n this.log.trace(\"Storing response\", {\n key,\n route: route.path,\n cache: !!cache,\n etag: shouldUseEtag,\n });\n\n await this.cache.set(key, {\n body: response.body,\n status: response.status,\n contentType: response.headers?.[\"content-type\"],\n lastModified,\n hash: generatedEtag,\n });\n }\n\n // Set ETag headers if etag is enabled\n if (shouldUseEtag) {\n response.headers.etag = generatedEtag;\n response.headers[\"last-modified\"] = lastModified;\n }\n },\n });\n\n public buildCacheControlHeader(cache?: ServerRouteCache): string | undefined {\n if (!cache) {\n return undefined;\n }\n\n // If cache is true or a DurationLike, no Cache-Control header is set\n if (\n cache === true ||\n typeof cache === \"string\" ||\n typeof cache === \"number\"\n ) {\n return undefined;\n }\n\n const control = cache.control;\n if (!control) {\n return undefined;\n }\n\n // If control is a string, return it directly\n if (typeof control === \"string\") {\n return control;\n }\n\n // If control is true, return default Cache-Control\n if (control === true) {\n return \"public, max-age=300\";\n }\n\n // Build Cache-Control from object directives\n const directives: string[] = [];\n\n if (control.public) {\n directives.push(\"public\");\n }\n if (control.private) {\n directives.push(\"private\");\n }\n if (control.noCache) {\n directives.push(\"no-cache\");\n }\n if (control.noStore) {\n directives.push(\"no-store\");\n }\n if (control.maxAge !== undefined) {\n const maxAgeSeconds = this.durationToSeconds(control.maxAge);\n directives.push(`max-age=${maxAgeSeconds}`);\n }\n if (control.sMaxAge !== undefined) {\n const sMaxAgeSeconds = this.durationToSeconds(control.sMaxAge);\n directives.push(`s-maxage=${sMaxAgeSeconds}`);\n }\n if (control.mustRevalidate) {\n directives.push(\"must-revalidate\");\n }\n if (control.proxyRevalidate) {\n directives.push(\"proxy-revalidate\");\n }\n if (control.immutable) {\n directives.push(\"immutable\");\n }\n\n return directives.length > 0 ? directives.join(\", \") : undefined;\n }\n\n protected durationToSeconds(duration: number | DurationLike): number {\n if (typeof duration === \"number\") {\n return duration;\n }\n\n return this.time.duration(duration).asSeconds();\n }\n\n protected shouldStore(cache?: ServerRouteCache): boolean {\n if (!cache) return false;\n if (cache === true) return true;\n if (typeof cache === \"object\" && cache.store) return true;\n return false;\n }\n\n protected shouldUseEtag(cache?: ServerRouteCache): boolean {\n // cache: true enables etag\n if (cache === true) return true;\n // Check object form\n if (typeof cache === \"object\" && cache.etag) return true;\n return false;\n }\n\n protected createCacheKey(route: ServerRoute, config?: ServerRequest): string {\n const params: string[] = [];\n for (const [key, value] of Object.entries(config?.params ?? {})) {\n params.push(`${key}=${value}`);\n }\n for (const [key, value] of Object.entries(config?.query ?? {})) {\n params.push(`${key}=${value}`);\n }\n\n return `${route.method}:${route.path.replaceAll(\":\", \"\")}:${params.join(\",\").replaceAll(\":\", \"\")}`;\n }\n}\n\nexport type ServerRouteCache =\n /**\n * If true, enables caching with:\n * - store: true\n * - etag: true\n */\n | boolean\n /**\n * Object configuration for fine-grained cache control.\n *\n * If empty, no caching will be applied.\n */\n | {\n /**\n * If true, enables storing cached responses. (in-memory, Redis, @see alepha/cache for other providers)\n * If a DurationLike is provided, it will be used as the TTL for the cache.\n * If CachePrimitiveOptions is provided, it will be used to configure the cache storage.\n *\n * @default false\n */\n store?: true | DurationLike | CachePrimitiveOptions;\n /**\n * If true, enables ETag support for the cached responses.\n */\n etag?: true;\n /**\n * - If true, sets Cache-Control to \"public, max-age=300\" (5 minutes).\n * - If string, sets Cache-Control to the provided value directly.\n * - If object, configures Cache-Control directives.\n */\n control?: /**\n * If true, sets Cache-Control to \"public, max-age=300\" (5 minutes).\n */\n | true\n /**\n * If string, sets Cache-Control to the provided value directly.\n */\n | string\n /**\n * If object, configures Cache-Control directives.\n */\n | {\n /**\n * Indicates that the response may be cached by any cache.\n */\n public?: boolean;\n /**\n * Indicates that the response is intended for a single user and must not be stored by a shared cache.\n */\n private?: boolean;\n /**\n * Forces caches to submit the request to the origin server for validation before releasing a cached copy.\n */\n noCache?: boolean;\n /**\n * Instructs caches not to store the response.\n */\n noStore?: boolean;\n /**\n * Maximum amount of time a resource is considered fresh.\n * Can be specified as a number (seconds) or as a DurationLike object.\n *\n * @example 300 // 5 minutes in seconds\n * @example { minutes: 5 } // 5 minutes\n * @example { hours: 1 } // 1 hour\n */\n maxAge?: number | DurationLike;\n /**\n * Overrides max-age for shared caches (e.g., CDNs).\n * Can be specified as a number (seconds) or as a DurationLike object.\n */\n sMaxAge?: number | DurationLike;\n /**\n * Indicates that once a resource becomes stale, caches must not use it without successful validation.\n */\n mustRevalidate?: boolean;\n /**\n * Similar to must-revalidate, but only for shared caches.\n */\n proxyRevalidate?: boolean;\n /**\n * Indicates that the response can be stored but must be revalidated before each use.\n */\n immutable?: boolean;\n };\n };\n\ninterface RouteCacheEntry {\n contentType?: string;\n body: any;\n status?: number;\n lastModified: string;\n hash: string;\n}\n","import { $module } from \"alepha\";\nimport { AlephaCache } from \"alepha/cache\";\nimport { ServerCacheProvider } from \"./providers/ServerCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/ServerCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Server that provides server-side caching capabilities.\n * It uses the Alepha Cache module to cache responses from server actions ($action).\n * It also provides a ETag-based cache invalidation mechanism.\n *\n * @example\n * ```ts\n * import { Alepha } from \"alepha\";\n * import { $action } from \"alepha/server\";\n * import { AlephaServerCache } from \"alepha/server/cache\";\n *\n * class ApiServer {\n * hello = $action({\n * cache: true,\n * handler: () => \"Hello, World!\",\n * });\n * }\n *\n * const alepha = Alepha.create()\n * .with(AlephaServerCache)\n * .with(ApiServer);\n *\n * run(alepha);\n * ```\n *\n * @see {@link ServerCacheProvider}\n * @module alepha.server.cache\n */\nexport const AlephaServerCache = $module({\n name: \"alepha.server.cache\",\n services: [AlephaCache, ServerCacheProvider],\n});\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import { Alepha
|
|
1
|
+
import * as alepha1 from "alepha";
|
|
2
|
+
import { Alepha } from "alepha";
|
|
3
3
|
import { ServerResponse } from "alepha/server";
|
|
4
4
|
import { Transform } from "node:stream";
|
|
5
5
|
|
|
@@ -16,7 +16,7 @@ declare class ServerCompressProvider {
|
|
|
16
16
|
} | undefined>;
|
|
17
17
|
protected readonly alepha: Alepha;
|
|
18
18
|
protected get options(): ServerCompressProviderOptions;
|
|
19
|
-
readonly onResponse:
|
|
19
|
+
readonly onResponse: alepha1.HookPrimitive<"server:onResponse">;
|
|
20
20
|
protected isAllowedContentType(contentType: string | undefined): boolean;
|
|
21
21
|
protected compress(encoding: keyof typeof ServerCompressProvider.compressors, response: ServerResponse): Promise<void>;
|
|
22
22
|
protected getParams(encoding: keyof typeof ServerCompressProvider.compressors): Record<number, any>;
|
|
@@ -32,7 +32,7 @@ interface ServerCompressProviderOptions {
|
|
|
32
32
|
*
|
|
33
33
|
* Compresses responses using gzip, brotli, or zstd based on the `Accept-Encoding` header.
|
|
34
34
|
*/
|
|
35
|
-
declare const AlephaServerCompress:
|
|
35
|
+
declare const AlephaServerCompress: alepha1.Service<alepha1.Module>;
|
|
36
36
|
//#endregion
|
|
37
37
|
export { AlephaServerCompress, ServerCompressProvider, ServerCompressProviderOptions };
|
|
38
38
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/server-compress/providers/ServerCompressProvider.ts","../../src/server-compress/index.ts"],"sourcesContent":["import { Readable, type Transform } from \"node:stream\";\nimport { ReadableStream } from \"node:stream/web\";\nimport { promisify } from \"node:util\";\nimport * as zlib from \"node:zlib\";\nimport { $hook, $inject, Alepha
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/server-compress/providers/ServerCompressProvider.ts","../../src/server-compress/index.ts"],"sourcesContent":["import { Readable, type Transform } from \"node:stream\";\nimport { ReadableStream } from \"node:stream/web\";\nimport { promisify } from \"node:util\";\nimport * as zlib from \"node:zlib\";\nimport { $hook, $inject, Alepha } from \"alepha\";\nimport type { ServerResponse } from \"alepha/server\";\n\nconst gzip = promisify(zlib.gzip);\nconst createGzip = zlib.createGzip;\nconst brotli = promisify(zlib.brotliCompress);\nconst createBrotliCompress = zlib.createBrotliCompress;\nconst zstd = zlib.zstdCompress ? promisify(zlib.zstdCompress) : undefined;\nconst createZstdCompress = zstd ? zlib.createZstdCompress : undefined;\n\ndeclare module \"alepha\" {\n interface State {\n \"alepha.server.compress.options\"?: ServerCompressProviderOptions;\n }\n}\n\nexport class ServerCompressProvider {\n static compressors: Record<\n string,\n | {\n compress: (...args: any[]) => Promise<Buffer>;\n stream: (options?: any) => Transform;\n }\n | undefined\n > = {\n gzip: {\n compress: gzip,\n stream: createGzip,\n },\n br: {\n compress: brotli,\n stream: createBrotliCompress,\n },\n zstd:\n zstd && createZstdCompress\n ? {\n compress: zstd,\n stream: createZstdCompress,\n }\n : undefined,\n };\n\n protected readonly alepha = $inject(Alepha);\n\n protected get options(): ServerCompressProviderOptions {\n return {\n allowedContentTypes: [\n \"application/json\",\n \"text/html\",\n \"application/javascript\",\n \"text/plain\",\n \"text/css\",\n ],\n ...this.alepha.store.get(\"alepha.server.compress.options\"),\n };\n }\n\n public readonly onResponse = $hook({\n on: \"server:onResponse\",\n handler: async ({ request, response }) => {\n // skip if already compressed\n if (response.headers[\"content-encoding\"]) {\n return;\n }\n\n const acceptEncoding = request.headers[\"accept-encoding\"]; // skip if no accept-encoding header\n if (!acceptEncoding) {\n return;\n }\n\n // skip if not json or html (for now)\n if (!this.isAllowedContentType(response.headers[\"content-type\"])) {\n return;\n }\n\n for (const encoding of [\"zstd\", \"br\", \"gzip\"] as const) {\n if (\n acceptEncoding.includes(encoding) &&\n ServerCompressProvider.compressors[encoding]\n ) {\n await this.compress(encoding, response);\n return;\n }\n }\n },\n });\n\n protected isAllowedContentType(contentType: string | undefined): boolean {\n if (!contentType) {\n return false;\n }\n\n const lowerContentType = contentType.toLowerCase();\n\n return !!this.options.allowedContentTypes.find((it) =>\n lowerContentType.includes(it),\n );\n }\n\n protected async compress(\n encoding: keyof typeof ServerCompressProvider.compressors,\n response: ServerResponse,\n ): Promise<void> {\n const body = response.body; // can be string or Buffer or ArrayBuffer or Readable\n\n const compressor = ServerCompressProvider.compressors[encoding];\n if (!compressor) {\n return;\n }\n\n const params = this.getParams(encoding);\n\n if (\n typeof body === \"string\" ||\n Buffer.isBuffer(body) ||\n body instanceof ArrayBuffer\n ) {\n const compressed = await compressor.compress(body, {\n params,\n });\n this.setHeaders(response, encoding);\n response.headers[\"content-length\"] = compressed.length.toString();\n response.body = compressed;\n }\n\n if (typeof body === \"object\" && body instanceof Readable) {\n this.setHeaders(response, encoding);\n response.body = body.pipe(compressor.stream({ params }));\n }\n\n if (typeof body === \"object\" && body instanceof ReadableStream) {\n this.setHeaders(response, encoding);\n response.body = Readable.fromWeb(body).pipe(\n compressor.stream({ params }),\n );\n }\n }\n\n protected getParams(\n encoding: keyof typeof ServerCompressProvider.compressors,\n ): Record<number, any> {\n if (encoding === \"zstd\") {\n return {\n [zlib.constants.ZSTD_c_compressionLevel]: 3, // default compression level for zstd\n };\n }\n if (encoding === \"br\") {\n return {};\n }\n if (encoding === \"gzip\") {\n return {};\n }\n return {};\n }\n\n protected setHeaders(\n response: ServerResponse,\n encoding: keyof typeof ServerCompressProvider.compressors,\n ): void {\n response.headers.vary = \"content-encoding\";\n response.headers[\"content-encoding\"] = encoding;\n response.headers[\"cache-control\"] = \"no-cache\";\n }\n}\n\nexport interface ServerCompressProviderOptions {\n allowedContentTypes: string[];\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { ServerCompressProvider } from \"./providers/ServerCompressProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/ServerCompressProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Server that provides server-side compression capabilities.\n *\n * Compresses responses using gzip, brotli, or zstd based on the `Accept-Encoding` header.\n */\nexport const AlephaServerCompress = $module({\n name: \"alepha.server.compress\",\n services: [AlephaServer, ServerCompressProvider],\n});\n"],"mappings":";;;;;;;;AAOA,MAAM,OAAO,UAAU,KAAK,KAAK;AACjC,MAAM,aAAa,KAAK;AACxB,MAAM,SAAS,UAAU,KAAK,eAAe;AAC7C,MAAM,uBAAuB,KAAK;AAClC,MAAM,OAAO,KAAK,eAAe,UAAU,KAAK,aAAa,GAAG;AAChE,MAAM,qBAAqB,OAAO,KAAK,qBAAqB;AAQ5D,IAAa,yBAAb,MAAa,uBAAuB;CAClC,OAAO,cAOH;EACF,MAAM;GACJ,UAAU;GACV,QAAQ;GACT;EACD,IAAI;GACF,UAAU;GACV,QAAQ;GACT;EACD,MACE,QAAQ,qBACJ;GACE,UAAU;GACV,QAAQ;GACT,GACD;EACP;CAED,AAAmB,SAAS,QAAQ,OAAO;CAE3C,IAAc,UAAyC;AACrD,SAAO;GACL,qBAAqB;IACnB;IACA;IACA;IACA;IACA;IACD;GACD,GAAG,KAAK,OAAO,MAAM,IAAI,iCAAiC;GAC3D;;CAGH,AAAgB,aAAa,MAAM;EACjC,IAAI;EACJ,SAAS,OAAO,EAAE,SAAS,eAAe;AAExC,OAAI,SAAS,QAAQ,oBACnB;GAGF,MAAM,iBAAiB,QAAQ,QAAQ;AACvC,OAAI,CAAC,eACH;AAIF,OAAI,CAAC,KAAK,qBAAqB,SAAS,QAAQ,gBAAgB,CAC9D;AAGF,QAAK,MAAM,YAAY;IAAC;IAAQ;IAAM;IAAO,CAC3C,KACE,eAAe,SAAS,SAAS,IACjC,uBAAuB,YAAY,WACnC;AACA,UAAM,KAAK,SAAS,UAAU,SAAS;AACvC;;;EAIP,CAAC;CAEF,AAAU,qBAAqB,aAA0C;AACvE,MAAI,CAAC,YACH,QAAO;EAGT,MAAM,mBAAmB,YAAY,aAAa;AAElD,SAAO,CAAC,CAAC,KAAK,QAAQ,oBAAoB,MAAM,OAC9C,iBAAiB,SAAS,GAAG,CAC9B;;CAGH,MAAgB,SACd,UACA,UACe;EACf,MAAM,OAAO,SAAS;EAEtB,MAAM,aAAa,uBAAuB,YAAY;AACtD,MAAI,CAAC,WACH;EAGF,MAAM,SAAS,KAAK,UAAU,SAAS;AAEvC,MACE,OAAO,SAAS,YAChB,OAAO,SAAS,KAAK,IACrB,gBAAgB,aAChB;GACA,MAAM,aAAa,MAAM,WAAW,SAAS,MAAM,EACjD,QACD,CAAC;AACF,QAAK,WAAW,UAAU,SAAS;AACnC,YAAS,QAAQ,oBAAoB,WAAW,OAAO,UAAU;AACjE,YAAS,OAAO;;AAGlB,MAAI,OAAO,SAAS,YAAY,gBAAgB,UAAU;AACxD,QAAK,WAAW,UAAU,SAAS;AACnC,YAAS,OAAO,KAAK,KAAK,WAAW,OAAO,EAAE,QAAQ,CAAC,CAAC;;AAG1D,MAAI,OAAO,SAAS,YAAY,gBAAgB,gBAAgB;AAC9D,QAAK,WAAW,UAAU,SAAS;AACnC,YAAS,OAAO,SAAS,QAAQ,KAAK,CAAC,KACrC,WAAW,OAAO,EAAE,QAAQ,CAAC,CAC9B;;;CAIL,AAAU,UACR,UACqB;AACrB,MAAI,aAAa,OACf,QAAO,GACJ,KAAK,UAAU,0BAA0B,GAC3C;AAEH,MAAI,aAAa,KACf,QAAO,EAAE;AAEX,MAAI,aAAa,OACf,QAAO,EAAE;AAEX,SAAO,EAAE;;CAGX,AAAU,WACR,UACA,UACM;AACN,WAAS,QAAQ,OAAO;AACxB,WAAS,QAAQ,sBAAsB;AACvC,WAAS,QAAQ,mBAAmB;;;;;;;;;;;ACtJxC,MAAa,uBAAuB,QAAQ;CAC1C,MAAM;CACN,UAAU,CAAC,cAAc,uBAAuB;CACjD,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $inject, $module, Alepha, AlephaError,
|
|
1
|
+
import { $inject, $module, Alepha, AlephaError, KIND, Primitive, createPrimitive } from "alepha";
|
|
2
2
|
import { AlephaServer } from "alepha/server";
|
|
3
3
|
import { DateTimeProvider } from "alepha/datetime";
|
|
4
4
|
|
|
@@ -40,9 +40,9 @@ var CookieParser = class {
|
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
//#endregion
|
|
43
|
-
//#region src/server-cookies/
|
|
43
|
+
//#region src/server-cookies/primitives/$cookie.browser.ts
|
|
44
44
|
/**
|
|
45
|
-
* Creates a browser-side cookie
|
|
45
|
+
* Creates a browser-side cookie primitive for client-side cookie management.
|
|
46
46
|
*
|
|
47
47
|
* Browser-specific version of $cookie that uses document.cookie API. Supports type-safe
|
|
48
48
|
* cookie operations with schema validation but excludes encryption/signing (use server-side
|
|
@@ -70,9 +70,9 @@ var CookieParser = class {
|
|
|
70
70
|
* ```
|
|
71
71
|
*/
|
|
72
72
|
const $cookie = (options) => {
|
|
73
|
-
return
|
|
73
|
+
return createPrimitive(BrowserCookiePrimitive, options);
|
|
74
74
|
};
|
|
75
|
-
var
|
|
75
|
+
var BrowserCookiePrimitive = class extends Primitive {
|
|
76
76
|
cookieParser = $inject(CookieParser);
|
|
77
77
|
alepha = $inject(Alepha);
|
|
78
78
|
dateTimeProvider = $inject(DateTimeProvider);
|
|
@@ -120,16 +120,16 @@ var BrowserCookieDescriptor = class extends Descriptor {
|
|
|
120
120
|
document.cookie = this.cookieParser.cookieToString(this.name, cookie);
|
|
121
121
|
}
|
|
122
122
|
};
|
|
123
|
-
$cookie[KIND] =
|
|
123
|
+
$cookie[KIND] = BrowserCookiePrimitive;
|
|
124
124
|
|
|
125
125
|
//#endregion
|
|
126
126
|
//#region src/server-cookies/index.browser.ts
|
|
127
127
|
const AlephaServerCookies = $module({
|
|
128
128
|
name: "alepha.server.cookies",
|
|
129
|
-
|
|
129
|
+
primitives: [],
|
|
130
130
|
services: [AlephaServer]
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
//#endregion
|
|
134
|
-
export { $cookie, AlephaServerCookies,
|
|
134
|
+
export { $cookie, AlephaServerCookies, BrowserCookiePrimitive };
|
|
135
135
|
//# sourceMappingURL=index.browser.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.browser.js","names":["cookies: Record<string, string>","parts: string[]","cookie: Cookie"],"sources":["../../src/server-cookies/services/CookieParser.ts","../../src/server-cookies/
|
|
1
|
+
{"version":3,"file":"index.browser.js","names":["cookies: Record<string, string>","parts: string[]","cookie: Cookie"],"sources":["../../src/server-cookies/services/CookieParser.ts","../../src/server-cookies/primitives/$cookie.browser.ts","../../src/server-cookies/index.browser.ts"],"sourcesContent":["import type { Cookie } from \"../primitives/$cookie.ts\";\n\nexport class CookieParser {\n public parseRequestCookies(header: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n const parts = header.split(\";\");\n for (const part of parts) {\n const [key, value] = part.split(\"=\");\n if (!key || !value) {\n continue;\n }\n\n cookies[key.trim()] = value.trim();\n }\n\n return cookies;\n }\n\n public serializeResponseCookies(\n cookies: Record<string, Cookie | null>,\n isHttps: boolean,\n ): string[] {\n const headers = [];\n\n for (const [name, cookie] of Object.entries(cookies)) {\n // If the cookie is null, we need to delete it\n if (cookie == null) {\n headers.push(`${name}=; Path=/; Max-Age=0`);\n continue;\n }\n\n if (!cookie.value) {\n continue;\n }\n\n headers.push(this.cookieToString(name, cookie, isHttps));\n }\n\n return headers;\n }\n\n public cookieToString(\n name: string,\n cookie: Cookie,\n isHttps?: boolean,\n ): string {\n const parts: string[] = [];\n\n parts.push(`${name}=${cookie.value}`);\n\n if (cookie.path) {\n parts.push(`Path=${cookie.path}`);\n }\n if (cookie.maxAge) {\n parts.push(`Max-Age=${cookie.maxAge}`);\n }\n if (cookie.secure !== false && isHttps) {\n parts.push(\"Secure\");\n }\n if (cookie.httpOnly) {\n parts.push(\"HttpOnly\");\n }\n if (cookie.sameSite) {\n parts.push(`SameSite=${cookie.sameSite}`);\n }\n if (cookie.domain) {\n parts.push(`Domain=${cookie.domain}`);\n }\n\n return parts.join(\"; \");\n }\n}\n","import {\n $inject,\n Alepha,\n AlephaError,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type TSchema,\n} from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { CookieParser } from \"../services/CookieParser.ts\";\nimport type {\n AbstractCookiePrimitive,\n Cookie,\n CookiePrimitiveOptions,\n Cookies,\n} from \"./$cookie.ts\";\n\n/**\n * Creates a browser-side cookie primitive for client-side cookie management.\n *\n * Browser-specific version of $cookie that uses document.cookie API. Supports type-safe\n * cookie operations with schema validation but excludes encryption/signing (use server-side\n * $cookie for secure operations).\n *\n * **Note**: This is the browser version - encryption, signing, and compression are not supported.\n *\n * @example\n * ```ts\n * class ClientCookies {\n * preferences = $cookie({\n * name: \"user-prefs\",\n * schema: t.object({ theme: t.text(), language: t.text() }),\n * ttl: [30, \"days\"]\n * });\n *\n * savePreferences() {\n * this.preferences.set({ theme: \"dark\", language: \"en\" });\n * }\n *\n * getPreferences() {\n * return this.preferences.get() ?? { theme: \"light\", language: \"en\" };\n * }\n * }\n * ```\n */\nexport const $cookie = <T extends TSchema>(\n options: CookiePrimitiveOptions<T>,\n): AbstractCookiePrimitive<T> => {\n return createPrimitive(BrowserCookiePrimitive<T>, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class BrowserCookiePrimitive<T extends TSchema>\n extends Primitive<CookiePrimitiveOptions<T>>\n implements AbstractCookiePrimitive<T>\n{\n protected cookieParser = $inject(CookieParser);\n protected alepha = $inject(Alepha);\n protected dateTimeProvider = $inject(DateTimeProvider);\n protected cookie?: Cookie;\n\n public get name(): string {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n public set(data: Static<T>): void {\n const value = JSON.stringify(\n this.alepha.codec.decode(this.options.schema, data),\n );\n const options = this.options;\n\n if (options.compress) {\n throw new AlephaError(\"Compression is not supported in browser cookies.\");\n }\n\n if (options.encrypt) {\n throw new AlephaError(\"Encryption is not supported in browser cookies.\");\n }\n\n if (options.sign) {\n throw new AlephaError(\"Signing is not supported in browser cookies.\");\n }\n\n const cookie: Cookie = {\n value: encodeURIComponent(value),\n path: options.path ?? \"/\",\n sameSite: options.sameSite ?? \"lax\",\n secure: false,\n httpOnly: false,\n domain: options.domain,\n };\n\n if (options.ttl) {\n cookie.maxAge = this.dateTimeProvider.duration(options.ttl).as(\"seconds\");\n }\n\n // biome-ignore lint/suspicious/noDocumentCookie: ...\n document.cookie = this.cookieParser.cookieToString(this.name, cookie);\n }\n\n public get(options?: { cookies?: Cookies }): Static<T> | undefined {\n const cookie = this.cookieParser.parseRequestCookies(document.cookie)[\n this.name\n ];\n if (!cookie) {\n return undefined;\n }\n\n const rawValue = decodeURIComponent(cookie);\n\n if (this.options.compress) {\n throw new AlephaError(\"Compression is not supported in browser cookies.\");\n }\n\n if (this.options.encrypt) {\n throw new AlephaError(\"Encryption is not supported in browser cookies.\");\n }\n\n if (this.options.sign) {\n throw new AlephaError(\"Signing is not supported in browser cookies.\");\n }\n\n return this.alepha.codec.decode(this.options.schema, JSON.parse(rawValue));\n }\n\n public del(): void {\n const options = this.options;\n const cookie: Cookie = {\n value: \"\",\n path: options.path ?? \"/\",\n sameSite: options.sameSite ?? \"lax\",\n secure: false,\n httpOnly: false,\n domain: options.domain,\n maxAge: 0, // Set maxAge to 0 to delete the cookie\n };\n\n // biome-ignore lint/suspicious/noDocumentCookie: ...\n document.cookie = this.cookieParser.cookieToString(this.name, cookie);\n }\n}\n\n$cookie[KIND] = BrowserCookiePrimitive;\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$cookie.browser.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport const AlephaServerCookies = $module({\n name: \"alepha.server.cookies\",\n primitives: [],\n services: [AlephaServer],\n});\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as alepha1 from "alepha";
|
|
2
|
-
import { Alepha,
|
|
2
|
+
import { Alepha, KIND, Primitive, Static, TSchema } from "alepha";
|
|
3
3
|
import { DateTimeProvider, DurationLike } from "alepha/datetime";
|
|
4
4
|
import * as alepha_logger0 from "alepha/logger";
|
|
5
5
|
|
|
@@ -23,12 +23,12 @@ declare class ServerCookiesProvider {
|
|
|
23
23
|
protected readonly IV_LENGTH = 16;
|
|
24
24
|
protected readonly AUTH_TAG_LENGTH = 16;
|
|
25
25
|
protected readonly SIGNATURE_LENGTH = 32;
|
|
26
|
-
readonly onRequest: alepha1.
|
|
27
|
-
readonly onAction: alepha1.
|
|
28
|
-
readonly onSend: alepha1.
|
|
26
|
+
readonly onRequest: alepha1.HookPrimitive<"server:onRequest">;
|
|
27
|
+
readonly onAction: alepha1.HookPrimitive<"action:onRequest">;
|
|
28
|
+
readonly onSend: alepha1.HookPrimitive<"server:onSend">;
|
|
29
29
|
protected getCookiesFromContext(cookies?: Cookies): Cookies;
|
|
30
|
-
getCookie<T extends TSchema>(name: string, options:
|
|
31
|
-
setCookie<T extends TSchema>(name: string, options:
|
|
30
|
+
getCookie<T extends TSchema>(name: string, options: CookiePrimitiveOptions<T>, contextCookies?: Cookies): Static<T> | undefined;
|
|
31
|
+
setCookie<T extends TSchema>(name: string, options: CookiePrimitiveOptions<T>, data: Static<T>, contextCookies?: Cookies): void;
|
|
32
32
|
deleteCookie<T extends TSchema>(name: string, contextCookies?: Cookies): void;
|
|
33
33
|
protected encrypt(text: string): string;
|
|
34
34
|
protected decrypt(encryptedText: string): string;
|
|
@@ -36,17 +36,17 @@ declare class ServerCookiesProvider {
|
|
|
36
36
|
protected sign(data: string): string;
|
|
37
37
|
}
|
|
38
38
|
//#endregion
|
|
39
|
-
//#region src/server-cookies/
|
|
39
|
+
//#region src/server-cookies/primitives/$cookie.d.ts
|
|
40
40
|
/**
|
|
41
41
|
* Declares a type-safe, configurable HTTP cookie.
|
|
42
|
-
* This
|
|
42
|
+
* This primitive provides methods to get, set, and delete the cookie
|
|
43
43
|
* within the server request/response cycle.
|
|
44
44
|
*/
|
|
45
45
|
declare const $cookie: {
|
|
46
|
-
<T extends TSchema>(options:
|
|
47
|
-
[KIND]: typeof
|
|
46
|
+
<T extends TSchema>(options: CookiePrimitiveOptions<T>): AbstractCookiePrimitive<T>;
|
|
47
|
+
[KIND]: typeof CookiePrimitive;
|
|
48
48
|
};
|
|
49
|
-
interface
|
|
49
|
+
interface CookiePrimitiveOptions<T extends TSchema> {
|
|
50
50
|
/** The schema for the cookie's value, used for validation and type safety. */
|
|
51
51
|
schema: T;
|
|
52
52
|
/** The name of the cookie. */
|
|
@@ -70,9 +70,9 @@ interface CookieDescriptorOptions<T extends TSchema> {
|
|
|
70
70
|
/** If true, the cookie will be signed to prevent tampering. Requires `COOKIE_SECRET` env var. */
|
|
71
71
|
sign?: boolean;
|
|
72
72
|
}
|
|
73
|
-
interface
|
|
73
|
+
interface AbstractCookiePrimitive<T extends TSchema> {
|
|
74
74
|
readonly name: string;
|
|
75
|
-
readonly options:
|
|
75
|
+
readonly options: CookiePrimitiveOptions<T>;
|
|
76
76
|
set(value: Static<T>, options?: {
|
|
77
77
|
cookies?: Cookies;
|
|
78
78
|
ttl?: DurationLike;
|
|
@@ -84,7 +84,7 @@ interface AbstractCookieDescriptor<T extends TSchema> {
|
|
|
84
84
|
cookies?: Cookies;
|
|
85
85
|
}): void;
|
|
86
86
|
}
|
|
87
|
-
declare class
|
|
87
|
+
declare class CookiePrimitive<T extends TSchema> extends Primitive<CookiePrimitiveOptions<T>> implements AbstractCookiePrimitive<T> {
|
|
88
88
|
protected readonly serverCookiesProvider: ServerCookiesProvider;
|
|
89
89
|
get schema(): T;
|
|
90
90
|
get name(): string;
|
|
@@ -129,9 +129,9 @@ declare module "alepha/server" {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
|
-
* Provides HTTP cookie management capabilities for server requests and responses with type-safe cookie
|
|
132
|
+
* Provides HTTP cookie management capabilities for server requests and responses with type-safe cookie primitives.
|
|
133
133
|
*
|
|
134
|
-
* The server-cookies module enables declarative cookie handling using the `$cookie`
|
|
134
|
+
* The server-cookies module enables declarative cookie handling using the `$cookie` primitive on class properties.
|
|
135
135
|
* It offers automatic cookie parsing, secure cookie configuration, and seamless integration with server routes
|
|
136
136
|
* for managing user sessions, preferences, and authentication tokens.
|
|
137
137
|
*
|
|
@@ -140,5 +140,5 @@ declare module "alepha/server" {
|
|
|
140
140
|
*/
|
|
141
141
|
declare const AlephaServerCookies: alepha1.Service<alepha1.Module>;
|
|
142
142
|
//#endregion
|
|
143
|
-
export { $cookie,
|
|
143
|
+
export { $cookie, AbstractCookiePrimitive, AlephaServerCookies, Cookie, CookiePrimitive, CookiePrimitiveOptions, Cookies, ServerCookiesProvider };
|
|
144
144
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $env, $hook, $inject, $module, Alepha,
|
|
1
|
+
import { $env, $hook, $inject, $module, Alepha, KIND, Primitive, createPrimitive, t } from "alepha";
|
|
2
2
|
import { AlephaServer } from "alepha/server";
|
|
3
3
|
import { createCipheriv, createDecipheriv, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { deflateRawSync, inflateRawSync } from "node:zlib";
|
|
@@ -167,16 +167,16 @@ var ServerCookiesProvider = class {
|
|
|
167
167
|
};
|
|
168
168
|
|
|
169
169
|
//#endregion
|
|
170
|
-
//#region src/server-cookies/
|
|
170
|
+
//#region src/server-cookies/primitives/$cookie.ts
|
|
171
171
|
/**
|
|
172
172
|
* Declares a type-safe, configurable HTTP cookie.
|
|
173
|
-
* This
|
|
173
|
+
* This primitive provides methods to get, set, and delete the cookie
|
|
174
174
|
* within the server request/response cycle.
|
|
175
175
|
*/
|
|
176
176
|
const $cookie = (options) => {
|
|
177
|
-
return
|
|
177
|
+
return createPrimitive(CookiePrimitive, options);
|
|
178
178
|
};
|
|
179
|
-
var
|
|
179
|
+
var CookiePrimitive = class extends Primitive {
|
|
180
180
|
serverCookiesProvider = $inject(ServerCookiesProvider);
|
|
181
181
|
get schema() {
|
|
182
182
|
return this.options.schema;
|
|
@@ -206,14 +206,14 @@ var CookieDescriptor = class extends Descriptor {
|
|
|
206
206
|
this.serverCookiesProvider.deleteCookie(this.name, options?.cookies);
|
|
207
207
|
}
|
|
208
208
|
};
|
|
209
|
-
$cookie[KIND] =
|
|
209
|
+
$cookie[KIND] = CookiePrimitive;
|
|
210
210
|
|
|
211
211
|
//#endregion
|
|
212
212
|
//#region src/server-cookies/index.ts
|
|
213
213
|
/**
|
|
214
|
-
* Provides HTTP cookie management capabilities for server requests and responses with type-safe cookie
|
|
214
|
+
* Provides HTTP cookie management capabilities for server requests and responses with type-safe cookie primitives.
|
|
215
215
|
*
|
|
216
|
-
* The server-cookies module enables declarative cookie handling using the `$cookie`
|
|
216
|
+
* The server-cookies module enables declarative cookie handling using the `$cookie` primitive on class properties.
|
|
217
217
|
* It offers automatic cookie parsing, secure cookie configuration, and seamless integration with server routes
|
|
218
218
|
* for managing user sessions, preferences, and authentication tokens.
|
|
219
219
|
*
|
|
@@ -222,10 +222,10 @@ $cookie[KIND] = CookieDescriptor;
|
|
|
222
222
|
*/
|
|
223
223
|
const AlephaServerCookies = $module({
|
|
224
224
|
name: "alepha.server.cookies",
|
|
225
|
-
|
|
225
|
+
primitives: [$cookie],
|
|
226
226
|
services: [AlephaServer, ServerCookiesProvider]
|
|
227
227
|
});
|
|
228
228
|
|
|
229
229
|
//#endregion
|
|
230
|
-
export { $cookie, AlephaServerCookies,
|
|
230
|
+
export { $cookie, AlephaServerCookies, CookiePrimitive, ServerCookiesProvider };
|
|
231
231
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["cookies: Record<string, string>","parts: string[]","cookie: Cookie"],"sources":["../../src/server-cookies/services/CookieParser.ts","../../src/server-cookies/providers/ServerCookiesProvider.ts","../../src/server-cookies/descriptors/$cookie.ts","../../src/server-cookies/index.ts"],"sourcesContent":["import type { Cookie } from \"../descriptors/$cookie.ts\";\n\nexport class CookieParser {\n public parseRequestCookies(header: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n const parts = header.split(\";\");\n for (const part of parts) {\n const [key, value] = part.split(\"=\");\n if (!key || !value) {\n continue;\n }\n\n cookies[key.trim()] = value.trim();\n }\n\n return cookies;\n }\n\n public serializeResponseCookies(\n cookies: Record<string, Cookie | null>,\n isHttps: boolean,\n ): string[] {\n const headers = [];\n\n for (const [name, cookie] of Object.entries(cookies)) {\n // If the cookie is null, we need to delete it\n if (cookie == null) {\n headers.push(`${name}=; Path=/; Max-Age=0`);\n continue;\n }\n\n if (!cookie.value) {\n continue;\n }\n\n headers.push(this.cookieToString(name, cookie, isHttps));\n }\n\n return headers;\n }\n\n public cookieToString(\n name: string,\n cookie: Cookie,\n isHttps?: boolean,\n ): string {\n const parts: string[] = [];\n\n parts.push(`${name}=${cookie.value}`);\n\n if (cookie.path) {\n parts.push(`Path=${cookie.path}`);\n }\n if (cookie.maxAge) {\n parts.push(`Max-Age=${cookie.maxAge}`);\n }\n if (cookie.secure !== false && isHttps) {\n parts.push(\"Secure\");\n }\n if (cookie.httpOnly) {\n parts.push(\"HttpOnly\");\n }\n if (cookie.sameSite) {\n parts.push(`SameSite=${cookie.sameSite}`);\n }\n if (cookie.domain) {\n parts.push(`Domain=${cookie.domain}`);\n }\n\n return parts.join(\"; \");\n }\n}\n","import {\n createCipheriv,\n createDecipheriv,\n createHmac,\n randomBytes,\n timingSafeEqual,\n} from \"node:crypto\";\nimport { deflateRawSync, inflateRawSync } from \"node:zlib\";\nimport {\n $env,\n $hook,\n $inject,\n Alepha,\n type Static,\n type TSchema,\n t,\n} from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { DEFAULT_APP_SECRET } from \"alepha/security\";\nimport type { ServerRequest } from \"alepha/server\";\nimport type {\n Cookie,\n CookieDescriptorOptions,\n Cookies,\n} from \"../descriptors/$cookie.ts\";\nimport { CookieParser } from \"../services/CookieParser.ts\";\n\nconst envSchema = t.object({\n APP_SECRET: t.text({\n default: DEFAULT_APP_SECRET,\n }),\n});\n\nexport class ServerCookiesProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly cookieParser = $inject(CookieParser);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly env = $env(envSchema);\n\n // crypto constants\n protected readonly ALGORITHM = \"aes-256-gcm\";\n protected readonly IV_LENGTH = 16; // For GCM\n protected readonly AUTH_TAG_LENGTH = 16;\n protected readonly SIGNATURE_LENGTH = 32; // For SHA256\n\n public readonly onRequest = $hook({\n on: \"server:onRequest\",\n handler: async ({ request }) => {\n request.cookies = {\n req: this.cookieParser.parseRequestCookies(\n request.headers.cookie ?? \"\",\n ),\n res: {},\n };\n },\n });\n\n public readonly onAction = $hook({\n on: \"action:onRequest\",\n handler: async ({ request }) => {\n request.cookies = {\n req: this.cookieParser.parseRequestCookies(\n request.headers.cookie ?? \"\",\n ),\n res: {},\n };\n },\n });\n\n public readonly onSend = $hook({\n on: \"server:onSend\",\n handler: async ({ request }) => {\n if (request.cookies && Object.keys(request.cookies.res).length > 0) {\n const setCookieHeaders = this.cookieParser.serializeResponseCookies(\n request.cookies.res,\n request.url.protocol === \"https:\",\n );\n if (setCookieHeaders.length > 0) {\n request.reply.headers[\"set-cookie\"] = setCookieHeaders;\n }\n }\n },\n });\n\n protected getCookiesFromContext(cookies?: Cookies): Cookies {\n const contextCookies =\n this.alepha.context.get<ServerRequest>(\"request\")?.cookies;\n if (cookies) return cookies;\n if (contextCookies) return contextCookies;\n throw new Error(\n \"Cookie context is not available. This method must be called within a server request cycle.\",\n );\n }\n\n public getCookie<T extends TSchema>(\n name: string,\n options: CookieDescriptorOptions<T>,\n contextCookies?: Cookies,\n ): Static<T> | undefined {\n const cookies = this.getCookiesFromContext(contextCookies);\n let rawValue = cookies.req[name];\n\n if (!rawValue) return undefined;\n\n try {\n rawValue = decodeURIComponent(rawValue);\n\n if (options.sign) {\n const signature = rawValue.substring(0, this.SIGNATURE_LENGTH * 2);\n const value = rawValue.substring(this.SIGNATURE_LENGTH * 2);\n const expectedSignature = this.sign(value);\n\n if (\n !timingSafeEqual(\n Buffer.from(signature, \"hex\"),\n Buffer.from(expectedSignature, \"hex\"),\n )\n ) {\n this.log.warn(`Invalid signature for cookie \"${name}\".`);\n return undefined;\n }\n rawValue = value;\n }\n\n if (options.encrypt) {\n rawValue = this.decrypt(rawValue);\n }\n\n if (options.compress) {\n rawValue = inflateRawSync(Buffer.from(rawValue, \"base64\")).toString(\n \"utf8\",\n );\n }\n\n return this.alepha.codec.decode(options.schema, JSON.parse(rawValue));\n } catch (error) {\n this.log.warn(`Failed to parse cookie \"${name}\"`, error);\n // corrupted or invalid cookie, instruct browser to delete it on next response\n this.deleteCookie(name, cookies);\n return undefined;\n }\n }\n\n public setCookie<T extends TSchema>(\n name: string,\n options: CookieDescriptorOptions<T>,\n data: Static<T>,\n contextCookies?: Cookies,\n ): void {\n const cookies = this.getCookiesFromContext(contextCookies);\n let value = JSON.stringify(this.alepha.codec.decode(options.schema, data));\n\n if (options.compress) {\n value = deflateRawSync(value).toString(\"base64\");\n }\n\n if (options.encrypt) {\n value = this.encrypt(value);\n }\n\n if (options.sign) {\n value = this.sign(value) + value;\n }\n\n const cookie: Cookie = {\n value: encodeURIComponent(value),\n path: options.path ?? \"/\",\n sameSite: options.sameSite ?? \"lax\",\n secure: options.secure ?? this.alepha.isProduction(),\n httpOnly: options.httpOnly,\n domain: options.domain,\n };\n\n if (options.ttl) {\n cookie.maxAge = this.dateTimeProvider.duration(options.ttl).as(\"seconds\");\n }\n\n cookies.res[name] = cookie;\n }\n\n public deleteCookie<T extends TSchema>(\n name: string,\n contextCookies?: Cookies,\n ): void {\n const cookies = this.getCookiesFromContext(contextCookies);\n cookies.res[name] = null;\n }\n\n // --- Crypto & Parsing ---\n\n protected encrypt(text: string): string {\n const iv = randomBytes(this.IV_LENGTH);\n const cipher = createCipheriv(\n this.ALGORITHM,\n Buffer.from(this.secretKey()),\n iv,\n );\n const encrypted = Buffer.concat([\n cipher.update(text, \"utf8\"),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n return Buffer.concat([iv, authTag, encrypted]).toString(\"base64\");\n }\n\n protected decrypt(encryptedText: string): string {\n const data = Buffer.from(encryptedText, \"base64\");\n const iv = data.subarray(0, this.IV_LENGTH);\n const authTag = data.subarray(\n this.IV_LENGTH,\n this.IV_LENGTH + this.AUTH_TAG_LENGTH,\n );\n\n const encrypted = data.subarray(this.IV_LENGTH + this.AUTH_TAG_LENGTH);\n const decipher = createDecipheriv(\n this.ALGORITHM,\n Buffer.from(this.secretKey()),\n iv,\n );\n\n decipher.setAuthTag(authTag);\n\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final(),\n ]);\n\n return decrypted.toString(\"utf8\");\n }\n\n public secretKey(): string {\n let secret = this.env.APP_SECRET;\n if (secret.length < 32) {\n // pad secret to 32 bytes\n secret = secret.padEnd(32, \"0\");\n } else if (secret.length > 32) {\n // truncate secret to 32 bytes\n secret = secret.substring(0, 32);\n }\n return secret;\n }\n\n protected sign(data: string): string {\n return createHmac(\"sha256\", this.secretKey()).update(data).digest(\"hex\");\n }\n}\n","import {\n $inject,\n createDescriptor,\n Descriptor,\n KIND,\n type Static,\n type TSchema,\n} from \"alepha\";\nimport type { DurationLike } from \"alepha/datetime\";\nimport { ServerCookiesProvider } from \"../providers/ServerCookiesProvider.ts\";\n\n/**\n * Declares a type-safe, configurable HTTP cookie.\n * This descriptor provides methods to get, set, and delete the cookie\n * within the server request/response cycle.\n */\nexport const $cookie = <T extends TSchema>(\n options: CookieDescriptorOptions<T>,\n): AbstractCookieDescriptor<T> => {\n return createDescriptor(CookieDescriptor<T>, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface CookieDescriptorOptions<T extends TSchema> {\n /** The schema for the cookie's value, used for validation and type safety. */\n schema: T;\n\n /** The name of the cookie. */\n name?: string;\n\n /** The cookie's path. Defaults to \"/\". */\n path?: string;\n\n /** Time-to-live for the cookie. Maps to `Max-Age`. */\n ttl?: DurationLike;\n\n /** If true, the cookie is only sent over HTTPS. Defaults to true in production. */\n secure?: boolean;\n\n /** If true, the cookie cannot be accessed by client-side scripts. */\n httpOnly?: boolean;\n\n /** SameSite policy for the cookie. Defaults to \"lax\". */\n sameSite?: \"strict\" | \"lax\" | \"none\";\n\n /** The domain for the cookie. */\n domain?: string;\n\n /** If true, the cookie value will be compressed using zlib. */\n compress?: boolean;\n\n /** If true, the cookie value will be encrypted. Requires `COOKIE_SECRET` env var. */\n encrypt?: boolean;\n\n /** If true, the cookie will be signed to prevent tampering. Requires `COOKIE_SECRET` env var. */\n sign?: boolean;\n}\n\nexport interface AbstractCookieDescriptor<T extends TSchema> {\n readonly name: string;\n readonly options: CookieDescriptorOptions<T>;\n set(\n value: Static<T>,\n options?: { cookies?: Cookies; ttl?: DurationLike },\n ): void;\n get(options?: { cookies?: Cookies }): Static<T> | undefined;\n del(options?: { cookies?: Cookies }): void;\n}\n\nexport class CookieDescriptor<T extends TSchema>\n extends Descriptor<CookieDescriptorOptions<T>>\n implements AbstractCookieDescriptor<T>\n{\n protected readonly serverCookiesProvider = $inject(ServerCookiesProvider);\n\n public get schema(): T {\n return this.options.schema;\n }\n\n public get name(): string {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n /**\n * Sets the cookie with the given value in the current request's response.\n */\n public set(\n value: Static<T>,\n options?: { cookies?: Cookies; ttl?: DurationLike },\n ): void {\n this.serverCookiesProvider.setCookie(\n this.name,\n {\n ...this.options,\n ttl: options?.ttl ?? this.options.ttl,\n },\n value,\n options?.cookies,\n );\n }\n\n /**\n * Gets the cookie value from the current request. Returns undefined if not found or invalid.\n */\n public get(options?: { cookies?: Cookies }): Static<T> | undefined {\n return this.serverCookiesProvider.getCookie(\n this.name,\n this.options,\n options?.cookies,\n );\n }\n\n /**\n * Deletes the cookie in the current request's response.\n */\n public del(options?: { cookies?: Cookies }): void {\n this.serverCookiesProvider.deleteCookie(this.name, options?.cookies);\n }\n}\n\n$cookie[KIND] = CookieDescriptor;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface Cookies {\n req: Record<string, string>;\n res: Record<string, Cookie | null>;\n}\n\nexport interface Cookie {\n value: string;\n path?: string;\n maxAge?: number;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: \"strict\" | \"lax\" | \"none\";\n domain?: string;\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { $cookie, type Cookies } from \"./descriptors/$cookie.ts\";\nimport { ServerCookiesProvider } from \"./providers/ServerCookiesProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./descriptors/$cookie.ts\";\nexport * from \"./providers/ServerCookiesProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha/server\" {\n interface ServerRequest {\n cookies: Cookies;\n }\n}\n\n/**\n * Provides HTTP cookie management capabilities for server requests and responses with type-safe cookie descriptors.\n *\n * The server-cookies module enables declarative cookie handling using the `$cookie` descriptor on class properties.\n * It offers automatic cookie parsing, secure cookie configuration, and seamless integration with server routes\n * for managing user sessions, preferences, and authentication tokens.\n *\n * @see {@link $cookie}\n * @module alepha.server.cookies\n */\nexport const AlephaServerCookies = $module({\n name: \"alepha.server.cookies\",\n descriptors: [$cookie],\n services: [AlephaServer, ServerCookiesProvider],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["cookies: Record<string, string>","parts: string[]","cookie: Cookie"],"sources":["../../src/server-cookies/services/CookieParser.ts","../../src/server-cookies/providers/ServerCookiesProvider.ts","../../src/server-cookies/primitives/$cookie.ts","../../src/server-cookies/index.ts"],"sourcesContent":["import type { Cookie } from \"../primitives/$cookie.ts\";\n\nexport class CookieParser {\n public parseRequestCookies(header: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n const parts = header.split(\";\");\n for (const part of parts) {\n const [key, value] = part.split(\"=\");\n if (!key || !value) {\n continue;\n }\n\n cookies[key.trim()] = value.trim();\n }\n\n return cookies;\n }\n\n public serializeResponseCookies(\n cookies: Record<string, Cookie | null>,\n isHttps: boolean,\n ): string[] {\n const headers = [];\n\n for (const [name, cookie] of Object.entries(cookies)) {\n // If the cookie is null, we need to delete it\n if (cookie == null) {\n headers.push(`${name}=; Path=/; Max-Age=0`);\n continue;\n }\n\n if (!cookie.value) {\n continue;\n }\n\n headers.push(this.cookieToString(name, cookie, isHttps));\n }\n\n return headers;\n }\n\n public cookieToString(\n name: string,\n cookie: Cookie,\n isHttps?: boolean,\n ): string {\n const parts: string[] = [];\n\n parts.push(`${name}=${cookie.value}`);\n\n if (cookie.path) {\n parts.push(`Path=${cookie.path}`);\n }\n if (cookie.maxAge) {\n parts.push(`Max-Age=${cookie.maxAge}`);\n }\n if (cookie.secure !== false && isHttps) {\n parts.push(\"Secure\");\n }\n if (cookie.httpOnly) {\n parts.push(\"HttpOnly\");\n }\n if (cookie.sameSite) {\n parts.push(`SameSite=${cookie.sameSite}`);\n }\n if (cookie.domain) {\n parts.push(`Domain=${cookie.domain}`);\n }\n\n return parts.join(\"; \");\n }\n}\n","import {\n createCipheriv,\n createDecipheriv,\n createHmac,\n randomBytes,\n timingSafeEqual,\n} from \"node:crypto\";\nimport { deflateRawSync, inflateRawSync } from \"node:zlib\";\nimport {\n $env,\n $hook,\n $inject,\n Alepha,\n type Static,\n type TSchema,\n t,\n} from \"alepha\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { DEFAULT_APP_SECRET } from \"alepha/security\";\nimport type { ServerRequest } from \"alepha/server\";\nimport type {\n Cookie,\n CookiePrimitiveOptions,\n Cookies,\n} from \"../primitives/$cookie.ts\";\nimport { CookieParser } from \"../services/CookieParser.ts\";\n\nconst envSchema = t.object({\n APP_SECRET: t.text({\n default: DEFAULT_APP_SECRET,\n }),\n});\n\nexport class ServerCookiesProvider {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly cookieParser = $inject(CookieParser);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly env = $env(envSchema);\n\n // crypto constants\n protected readonly ALGORITHM = \"aes-256-gcm\";\n protected readonly IV_LENGTH = 16; // For GCM\n protected readonly AUTH_TAG_LENGTH = 16;\n protected readonly SIGNATURE_LENGTH = 32; // For SHA256\n\n public readonly onRequest = $hook({\n on: \"server:onRequest\",\n handler: async ({ request }) => {\n request.cookies = {\n req: this.cookieParser.parseRequestCookies(\n request.headers.cookie ?? \"\",\n ),\n res: {},\n };\n },\n });\n\n public readonly onAction = $hook({\n on: \"action:onRequest\",\n handler: async ({ request }) => {\n request.cookies = {\n req: this.cookieParser.parseRequestCookies(\n request.headers.cookie ?? \"\",\n ),\n res: {},\n };\n },\n });\n\n public readonly onSend = $hook({\n on: \"server:onSend\",\n handler: async ({ request }) => {\n if (request.cookies && Object.keys(request.cookies.res).length > 0) {\n const setCookieHeaders = this.cookieParser.serializeResponseCookies(\n request.cookies.res,\n request.url.protocol === \"https:\",\n );\n if (setCookieHeaders.length > 0) {\n request.reply.headers[\"set-cookie\"] = setCookieHeaders;\n }\n }\n },\n });\n\n protected getCookiesFromContext(cookies?: Cookies): Cookies {\n const contextCookies =\n this.alepha.context.get<ServerRequest>(\"request\")?.cookies;\n if (cookies) return cookies;\n if (contextCookies) return contextCookies;\n throw new Error(\n \"Cookie context is not available. This method must be called within a server request cycle.\",\n );\n }\n\n public getCookie<T extends TSchema>(\n name: string,\n options: CookiePrimitiveOptions<T>,\n contextCookies?: Cookies,\n ): Static<T> | undefined {\n const cookies = this.getCookiesFromContext(contextCookies);\n let rawValue = cookies.req[name];\n\n if (!rawValue) return undefined;\n\n try {\n rawValue = decodeURIComponent(rawValue);\n\n if (options.sign) {\n const signature = rawValue.substring(0, this.SIGNATURE_LENGTH * 2);\n const value = rawValue.substring(this.SIGNATURE_LENGTH * 2);\n const expectedSignature = this.sign(value);\n\n if (\n !timingSafeEqual(\n Buffer.from(signature, \"hex\"),\n Buffer.from(expectedSignature, \"hex\"),\n )\n ) {\n this.log.warn(`Invalid signature for cookie \"${name}\".`);\n return undefined;\n }\n rawValue = value;\n }\n\n if (options.encrypt) {\n rawValue = this.decrypt(rawValue);\n }\n\n if (options.compress) {\n rawValue = inflateRawSync(Buffer.from(rawValue, \"base64\")).toString(\n \"utf8\",\n );\n }\n\n return this.alepha.codec.decode(options.schema, JSON.parse(rawValue));\n } catch (error) {\n this.log.warn(`Failed to parse cookie \"${name}\"`, error);\n // corrupted or invalid cookie, instruct browser to delete it on next response\n this.deleteCookie(name, cookies);\n return undefined;\n }\n }\n\n public setCookie<T extends TSchema>(\n name: string,\n options: CookiePrimitiveOptions<T>,\n data: Static<T>,\n contextCookies?: Cookies,\n ): void {\n const cookies = this.getCookiesFromContext(contextCookies);\n let value = JSON.stringify(this.alepha.codec.decode(options.schema, data));\n\n if (options.compress) {\n value = deflateRawSync(value).toString(\"base64\");\n }\n\n if (options.encrypt) {\n value = this.encrypt(value);\n }\n\n if (options.sign) {\n value = this.sign(value) + value;\n }\n\n const cookie: Cookie = {\n value: encodeURIComponent(value),\n path: options.path ?? \"/\",\n sameSite: options.sameSite ?? \"lax\",\n secure: options.secure ?? this.alepha.isProduction(),\n httpOnly: options.httpOnly,\n domain: options.domain,\n };\n\n if (options.ttl) {\n cookie.maxAge = this.dateTimeProvider.duration(options.ttl).as(\"seconds\");\n }\n\n cookies.res[name] = cookie;\n }\n\n public deleteCookie<T extends TSchema>(\n name: string,\n contextCookies?: Cookies,\n ): void {\n const cookies = this.getCookiesFromContext(contextCookies);\n cookies.res[name] = null;\n }\n\n // --- Crypto & Parsing ---\n\n protected encrypt(text: string): string {\n const iv = randomBytes(this.IV_LENGTH);\n const cipher = createCipheriv(\n this.ALGORITHM,\n Buffer.from(this.secretKey()),\n iv,\n );\n const encrypted = Buffer.concat([\n cipher.update(text, \"utf8\"),\n cipher.final(),\n ]);\n const authTag = cipher.getAuthTag();\n return Buffer.concat([iv, authTag, encrypted]).toString(\"base64\");\n }\n\n protected decrypt(encryptedText: string): string {\n const data = Buffer.from(encryptedText, \"base64\");\n const iv = data.subarray(0, this.IV_LENGTH);\n const authTag = data.subarray(\n this.IV_LENGTH,\n this.IV_LENGTH + this.AUTH_TAG_LENGTH,\n );\n\n const encrypted = data.subarray(this.IV_LENGTH + this.AUTH_TAG_LENGTH);\n const decipher = createDecipheriv(\n this.ALGORITHM,\n Buffer.from(this.secretKey()),\n iv,\n );\n\n decipher.setAuthTag(authTag);\n\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final(),\n ]);\n\n return decrypted.toString(\"utf8\");\n }\n\n public secretKey(): string {\n let secret = this.env.APP_SECRET;\n if (secret.length < 32) {\n // pad secret to 32 bytes\n secret = secret.padEnd(32, \"0\");\n } else if (secret.length > 32) {\n // truncate secret to 32 bytes\n secret = secret.substring(0, 32);\n }\n return secret;\n }\n\n protected sign(data: string): string {\n return createHmac(\"sha256\", this.secretKey()).update(data).digest(\"hex\");\n }\n}\n","import {\n $inject,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type TSchema,\n} from \"alepha\";\nimport type { DurationLike } from \"alepha/datetime\";\nimport { ServerCookiesProvider } from \"../providers/ServerCookiesProvider.ts\";\n\n/**\n * Declares a type-safe, configurable HTTP cookie.\n * This primitive provides methods to get, set, and delete the cookie\n * within the server request/response cycle.\n */\nexport const $cookie = <T extends TSchema>(\n options: CookiePrimitiveOptions<T>,\n): AbstractCookiePrimitive<T> => {\n return createPrimitive(CookiePrimitive<T>, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface CookiePrimitiveOptions<T extends TSchema> {\n /** The schema for the cookie's value, used for validation and type safety. */\n schema: T;\n\n /** The name of the cookie. */\n name?: string;\n\n /** The cookie's path. Defaults to \"/\". */\n path?: string;\n\n /** Time-to-live for the cookie. Maps to `Max-Age`. */\n ttl?: DurationLike;\n\n /** If true, the cookie is only sent over HTTPS. Defaults to true in production. */\n secure?: boolean;\n\n /** If true, the cookie cannot be accessed by client-side scripts. */\n httpOnly?: boolean;\n\n /** SameSite policy for the cookie. Defaults to \"lax\". */\n sameSite?: \"strict\" | \"lax\" | \"none\";\n\n /** The domain for the cookie. */\n domain?: string;\n\n /** If true, the cookie value will be compressed using zlib. */\n compress?: boolean;\n\n /** If true, the cookie value will be encrypted. Requires `COOKIE_SECRET` env var. */\n encrypt?: boolean;\n\n /** If true, the cookie will be signed to prevent tampering. Requires `COOKIE_SECRET` env var. */\n sign?: boolean;\n}\n\nexport interface AbstractCookiePrimitive<T extends TSchema> {\n readonly name: string;\n readonly options: CookiePrimitiveOptions<T>;\n set(\n value: Static<T>,\n options?: { cookies?: Cookies; ttl?: DurationLike },\n ): void;\n get(options?: { cookies?: Cookies }): Static<T> | undefined;\n del(options?: { cookies?: Cookies }): void;\n}\n\nexport class CookiePrimitive<T extends TSchema>\n extends Primitive<CookiePrimitiveOptions<T>>\n implements AbstractCookiePrimitive<T>\n{\n protected readonly serverCookiesProvider = $inject(ServerCookiesProvider);\n\n public get schema(): T {\n return this.options.schema;\n }\n\n public get name(): string {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n /**\n * Sets the cookie with the given value in the current request's response.\n */\n public set(\n value: Static<T>,\n options?: { cookies?: Cookies; ttl?: DurationLike },\n ): void {\n this.serverCookiesProvider.setCookie(\n this.name,\n {\n ...this.options,\n ttl: options?.ttl ?? this.options.ttl,\n },\n value,\n options?.cookies,\n );\n }\n\n /**\n * Gets the cookie value from the current request. Returns undefined if not found or invalid.\n */\n public get(options?: { cookies?: Cookies }): Static<T> | undefined {\n return this.serverCookiesProvider.getCookie(\n this.name,\n this.options,\n options?.cookies,\n );\n }\n\n /**\n * Deletes the cookie in the current request's response.\n */\n public del(options?: { cookies?: Cookies }): void {\n this.serverCookiesProvider.deleteCookie(this.name, options?.cookies);\n }\n}\n\n$cookie[KIND] = CookiePrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface Cookies {\n req: Record<string, string>;\n res: Record<string, Cookie | null>;\n}\n\nexport interface Cookie {\n value: string;\n path?: string;\n maxAge?: number;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: \"strict\" | \"lax\" | \"none\";\n domain?: string;\n}\n","import { $module } from \"alepha\";\nimport { AlephaServer } from \"alepha/server\";\nimport { $cookie, type Cookies } from \"./primitives/$cookie.ts\";\nimport { ServerCookiesProvider } from \"./providers/ServerCookiesProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$cookie.ts\";\nexport * from \"./providers/ServerCookiesProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha/server\" {\n interface ServerRequest {\n cookies: Cookies;\n }\n}\n\n/**\n * Provides HTTP cookie management capabilities for server requests and responses with type-safe cookie primitives.\n *\n * The server-cookies module enables declarative cookie handling using the `$cookie` primitive on class properties.\n * It offers automatic cookie parsing, secure cookie configuration, and seamless integration with server routes\n * for managing user sessions, preferences, and authentication tokens.\n *\n * @see {@link $cookie}\n * @module alepha.server.cookies\n */\nexport const AlephaServerCookies = $module({\n name: \"alepha.server.cookies\",\n primitives: [$cookie],\n services: [AlephaServer, ServerCookiesProvider],\n});\n"],"mappings":";;;;;;;;;AAEA,IAAa,eAAb,MAA0B;CACxB,AAAO,oBAAoB,QAAwC;EACjE,MAAMA,UAAkC,EAAE;EAC1C,MAAM,QAAQ,OAAO,MAAM,IAAI;AAC/B,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,CAAC,KAAK,SAAS,KAAK,MAAM,IAAI;AACpC,OAAI,CAAC,OAAO,CAAC,MACX;AAGF,WAAQ,IAAI,MAAM,IAAI,MAAM,MAAM;;AAGpC,SAAO;;CAGT,AAAO,yBACL,SACA,SACU;EACV,MAAM,UAAU,EAAE;AAElB,OAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,QAAQ,EAAE;AAEpD,OAAI,UAAU,MAAM;AAClB,YAAQ,KAAK,GAAG,KAAK,sBAAsB;AAC3C;;AAGF,OAAI,CAAC,OAAO,MACV;AAGF,WAAQ,KAAK,KAAK,eAAe,MAAM,QAAQ,QAAQ,CAAC;;AAG1D,SAAO;;CAGT,AAAO,eACL,MACA,QACA,SACQ;EACR,MAAMC,QAAkB,EAAE;AAE1B,QAAM,KAAK,GAAG,KAAK,GAAG,OAAO,QAAQ;AAErC,MAAI,OAAO,KACT,OAAM,KAAK,QAAQ,OAAO,OAAO;AAEnC,MAAI,OAAO,OACT,OAAM,KAAK,WAAW,OAAO,SAAS;AAExC,MAAI,OAAO,WAAW,SAAS,QAC7B,OAAM,KAAK,SAAS;AAEtB,MAAI,OAAO,SACT,OAAM,KAAK,WAAW;AAExB,MAAI,OAAO,SACT,OAAM,KAAK,YAAY,OAAO,WAAW;AAE3C,MAAI,OAAO,OACT,OAAM,KAAK,UAAU,OAAO,SAAS;AAGvC,SAAO,MAAM,KAAK,KAAK;;;;;;ACzC3B,MAAM,YAAY,EAAE,OAAO,EACzB,YAAY,EAAE,KAAK,EACjB,SAAS,oBACV,CAAC,EACH,CAAC;AAEF,IAAa,wBAAb,MAAmC;CACjC,AAAmB,SAAS,QAAQ,OAAO;CAC3C,AAAmB,MAAM,SAAS;CAClC,AAAmB,eAAe,QAAQ,aAAa;CACvD,AAAmB,mBAAmB,QAAQ,iBAAiB;CAC/D,AAAmB,MAAM,KAAK,UAAU;CAGxC,AAAmB,YAAY;CAC/B,AAAmB,YAAY;CAC/B,AAAmB,kBAAkB;CACrC,AAAmB,mBAAmB;CAEtC,AAAgB,YAAY,MAAM;EAChC,IAAI;EACJ,SAAS,OAAO,EAAE,cAAc;AAC9B,WAAQ,UAAU;IAChB,KAAK,KAAK,aAAa,oBACrB,QAAQ,QAAQ,UAAU,GAC3B;IACD,KAAK,EAAE;IACR;;EAEJ,CAAC;CAEF,AAAgB,WAAW,MAAM;EAC/B,IAAI;EACJ,SAAS,OAAO,EAAE,cAAc;AAC9B,WAAQ,UAAU;IAChB,KAAK,KAAK,aAAa,oBACrB,QAAQ,QAAQ,UAAU,GAC3B;IACD,KAAK,EAAE;IACR;;EAEJ,CAAC;CAEF,AAAgB,SAAS,MAAM;EAC7B,IAAI;EACJ,SAAS,OAAO,EAAE,cAAc;AAC9B,OAAI,QAAQ,WAAW,OAAO,KAAK,QAAQ,QAAQ,IAAI,CAAC,SAAS,GAAG;IAClE,MAAM,mBAAmB,KAAK,aAAa,yBACzC,QAAQ,QAAQ,KAChB,QAAQ,IAAI,aAAa,SAC1B;AACD,QAAI,iBAAiB,SAAS,EAC5B,SAAQ,MAAM,QAAQ,gBAAgB;;;EAI7C,CAAC;CAEF,AAAU,sBAAsB,SAA4B;EAC1D,MAAM,iBACJ,KAAK,OAAO,QAAQ,IAAmB,UAAU,EAAE;AACrD,MAAI,QAAS,QAAO;AACpB,MAAI,eAAgB,QAAO;AAC3B,QAAM,IAAI,MACR,6FACD;;CAGH,AAAO,UACL,MACA,SACA,gBACuB;EACvB,MAAM,UAAU,KAAK,sBAAsB,eAAe;EAC1D,IAAI,WAAW,QAAQ,IAAI;AAE3B,MAAI,CAAC,SAAU,QAAO;AAEtB,MAAI;AACF,cAAW,mBAAmB,SAAS;AAEvC,OAAI,QAAQ,MAAM;IAChB,MAAM,YAAY,SAAS,UAAU,GAAG,KAAK,mBAAmB,EAAE;IAClE,MAAM,QAAQ,SAAS,UAAU,KAAK,mBAAmB,EAAE;IAC3D,MAAM,oBAAoB,KAAK,KAAK,MAAM;AAE1C,QACE,CAAC,gBACC,OAAO,KAAK,WAAW,MAAM,EAC7B,OAAO,KAAK,mBAAmB,MAAM,CACtC,EACD;AACA,UAAK,IAAI,KAAK,iCAAiC,KAAK,IAAI;AACxD;;AAEF,eAAW;;AAGb,OAAI,QAAQ,QACV,YAAW,KAAK,QAAQ,SAAS;AAGnC,OAAI,QAAQ,SACV,YAAW,eAAe,OAAO,KAAK,UAAU,SAAS,CAAC,CAAC,SACzD,OACD;AAGH,UAAO,KAAK,OAAO,MAAM,OAAO,QAAQ,QAAQ,KAAK,MAAM,SAAS,CAAC;WAC9D,OAAO;AACd,QAAK,IAAI,KAAK,2BAA2B,KAAK,IAAI,MAAM;AAExD,QAAK,aAAa,MAAM,QAAQ;AAChC;;;CAIJ,AAAO,UACL,MACA,SACA,MACA,gBACM;EACN,MAAM,UAAU,KAAK,sBAAsB,eAAe;EAC1D,IAAI,QAAQ,KAAK,UAAU,KAAK,OAAO,MAAM,OAAO,QAAQ,QAAQ,KAAK,CAAC;AAE1E,MAAI,QAAQ,SACV,SAAQ,eAAe,MAAM,CAAC,SAAS,SAAS;AAGlD,MAAI,QAAQ,QACV,SAAQ,KAAK,QAAQ,MAAM;AAG7B,MAAI,QAAQ,KACV,SAAQ,KAAK,KAAK,MAAM,GAAG;EAG7B,MAAMC,SAAiB;GACrB,OAAO,mBAAmB,MAAM;GAChC,MAAM,QAAQ,QAAQ;GACtB,UAAU,QAAQ,YAAY;GAC9B,QAAQ,QAAQ,UAAU,KAAK,OAAO,cAAc;GACpD,UAAU,QAAQ;GAClB,QAAQ,QAAQ;GACjB;AAED,MAAI,QAAQ,IACV,QAAO,SAAS,KAAK,iBAAiB,SAAS,QAAQ,IAAI,CAAC,GAAG,UAAU;AAG3E,UAAQ,IAAI,QAAQ;;CAGtB,AAAO,aACL,MACA,gBACM;EACN,MAAM,UAAU,KAAK,sBAAsB,eAAe;AAC1D,UAAQ,IAAI,QAAQ;;CAKtB,AAAU,QAAQ,MAAsB;EACtC,MAAM,KAAK,YAAY,KAAK,UAAU;EACtC,MAAM,SAAS,eACb,KAAK,WACL,OAAO,KAAK,KAAK,WAAW,CAAC,EAC7B,GACD;EACD,MAAM,YAAY,OAAO,OAAO,CAC9B,OAAO,OAAO,MAAM,OAAO,EAC3B,OAAO,OAAO,CACf,CAAC;EACF,MAAM,UAAU,OAAO,YAAY;AACnC,SAAO,OAAO,OAAO;GAAC;GAAI;GAAS;GAAU,CAAC,CAAC,SAAS,SAAS;;CAGnE,AAAU,QAAQ,eAA+B;EAC/C,MAAM,OAAO,OAAO,KAAK,eAAe,SAAS;EACjD,MAAM,KAAK,KAAK,SAAS,GAAG,KAAK,UAAU;EAC3C,MAAM,UAAU,KAAK,SACnB,KAAK,WACL,KAAK,YAAY,KAAK,gBACvB;EAED,MAAM,YAAY,KAAK,SAAS,KAAK,YAAY,KAAK,gBAAgB;EACtE,MAAM,WAAW,iBACf,KAAK,WACL,OAAO,KAAK,KAAK,WAAW,CAAC,EAC7B,GACD;AAED,WAAS,WAAW,QAAQ;AAO5B,SALkB,OAAO,OAAO,CAC9B,SAAS,OAAO,UAAU,EAC1B,SAAS,OAAO,CACjB,CAAC,CAEe,SAAS,OAAO;;CAGnC,AAAO,YAAoB;EACzB,IAAI,SAAS,KAAK,IAAI;AACtB,MAAI,OAAO,SAAS,GAElB,UAAS,OAAO,OAAO,IAAI,IAAI;WACtB,OAAO,SAAS,GAEzB,UAAS,OAAO,UAAU,GAAG,GAAG;AAElC,SAAO;;CAGT,AAAU,KAAK,MAAsB;AACnC,SAAO,WAAW,UAAU,KAAK,WAAW,CAAC,CAAC,OAAO,KAAK,CAAC,OAAO,MAAM;;;;;;;;;;;ACrO5E,MAAa,WACX,YAC+B;AAC/B,QAAO,gBAAgB,iBAAoB,QAAQ;;AAmDrD,IAAa,kBAAb,cACU,UAEV;CACE,AAAmB,wBAAwB,QAAQ,sBAAsB;CAEzE,IAAW,SAAY;AACrB,SAAO,KAAK,QAAQ;;CAGtB,IAAW,OAAe;AACxB,SAAO,KAAK,QAAQ,QAAQ,GAAG,KAAK,OAAO;;;;;CAM7C,AAAO,IACL,OACA,SACM;AACN,OAAK,sBAAsB,UACzB,KAAK,MACL;GACE,GAAG,KAAK;GACR,KAAK,SAAS,OAAO,KAAK,QAAQ;GACnC,EACD,OACA,SAAS,QACV;;;;;CAMH,AAAO,IAAI,SAAwD;AACjE,SAAO,KAAK,sBAAsB,UAChC,KAAK,MACL,KAAK,SACL,SAAS,QACV;;;;;CAMH,AAAO,IAAI,SAAuC;AAChD,OAAK,sBAAsB,aAAa,KAAK,MAAM,SAAS,QAAQ;;;AAIxE,QAAQ,QAAQ;;;;;;;;;;;;;;AC7FhB,MAAa,sBAAsB,QAAQ;CACzC,MAAM;CACN,YAAY,CAAC,QAAQ;CACrB,UAAU,CAAC,cAAc,sBAAsB;CAChD,CAAC"}
|