@b9g/platform 0.1.4 → 0.1.5

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.
@@ -0,0 +1,231 @@
1
+ /// <reference types="./cookie-store.d.ts" />
2
+ // src/cookie-store.ts
3
+ function parseCookieHeader(cookieHeader) {
4
+ const cookies = /* @__PURE__ */ new Map();
5
+ if (!cookieHeader)
6
+ return cookies;
7
+ const pairs = cookieHeader.split(/;\s*/);
8
+ for (const pair of pairs) {
9
+ const [name, ...valueParts] = pair.split("=");
10
+ if (name) {
11
+ const value = valueParts.join("=");
12
+ cookies.set(name.trim(), decodeURIComponent(value || ""));
13
+ }
14
+ }
15
+ return cookies;
16
+ }
17
+ function serializeCookie(cookie) {
18
+ let header = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
19
+ if (cookie.expires !== void 0 && cookie.expires !== null) {
20
+ const date = new Date(cookie.expires);
21
+ header += `; Expires=${date.toUTCString()}`;
22
+ }
23
+ if (cookie.domain) {
24
+ header += `; Domain=${cookie.domain}`;
25
+ }
26
+ if (cookie.path) {
27
+ header += `; Path=${cookie.path}`;
28
+ } else {
29
+ header += `; Path=/`;
30
+ }
31
+ if (cookie.sameSite) {
32
+ header += `; SameSite=${cookie.sameSite.charAt(0).toUpperCase() + cookie.sameSite.slice(1)}`;
33
+ } else {
34
+ header += `; SameSite=Strict`;
35
+ }
36
+ if (cookie.partitioned) {
37
+ header += `; Partitioned`;
38
+ }
39
+ header += `; Secure`;
40
+ return header;
41
+ }
42
+ function parseSetCookieHeader(setCookieHeader) {
43
+ const parts = setCookieHeader.split(/;\s*/);
44
+ const [nameValue, ...attributes] = parts;
45
+ const [name, ...valueParts] = nameValue.split("=");
46
+ const value = valueParts.join("=");
47
+ const cookie = {
48
+ name: decodeURIComponent(name.trim()),
49
+ value: decodeURIComponent(value || "")
50
+ };
51
+ for (const attr of attributes) {
52
+ const [key, ...attrValueParts] = attr.split("=");
53
+ const attrKey = key.trim().toLowerCase();
54
+ const attrValue = attrValueParts.join("=").trim();
55
+ switch (attrKey) {
56
+ case "expires":
57
+ cookie.expires = new Date(attrValue).getTime();
58
+ break;
59
+ case "max-age":
60
+ cookie.expires = Date.now() + parseInt(attrValue, 10) * 1e3;
61
+ break;
62
+ case "domain":
63
+ cookie.domain = attrValue;
64
+ break;
65
+ case "path":
66
+ cookie.path = attrValue;
67
+ break;
68
+ case "secure":
69
+ cookie.secure = true;
70
+ break;
71
+ case "samesite":
72
+ cookie.sameSite = attrValue.toLowerCase();
73
+ break;
74
+ case "partitioned":
75
+ cookie.partitioned = true;
76
+ break;
77
+ }
78
+ }
79
+ return cookie;
80
+ }
81
+ var RequestCookieStore = class extends EventTarget {
82
+ #cookies;
83
+ #changes;
84
+ // null = deleted
85
+ #request;
86
+ // Event handler for cookie changes (spec compliance)
87
+ // eslint-disable-next-line no-restricted-syntax
88
+ onchange = null;
89
+ constructor(request) {
90
+ super();
91
+ this.#cookies = /* @__PURE__ */ new Map();
92
+ this.#changes = /* @__PURE__ */ new Map();
93
+ this.#request = request || null;
94
+ if (request) {
95
+ const cookieHeader = request.headers.get("cookie");
96
+ if (cookieHeader) {
97
+ const parsed = parseCookieHeader(cookieHeader);
98
+ for (const [name, value] of parsed) {
99
+ this.#cookies.set(name, { name, value });
100
+ }
101
+ }
102
+ }
103
+ }
104
+ /**
105
+ * Get a single cookie by name
106
+ */
107
+ async get(nameOrOptions) {
108
+ const name = typeof nameOrOptions === "string" ? nameOrOptions : nameOrOptions.name;
109
+ if (!name) {
110
+ throw new TypeError("Cookie name is required");
111
+ }
112
+ if (this.#changes.has(name)) {
113
+ const change = this.#changes.get(name);
114
+ if (change === null || change === void 0)
115
+ return null;
116
+ return {
117
+ name: change.name,
118
+ value: change.value,
119
+ domain: change.domain ?? void 0,
120
+ path: change.path,
121
+ expires: change.expires ?? void 0,
122
+ sameSite: change.sameSite,
123
+ partitioned: change.partitioned
124
+ };
125
+ }
126
+ return this.#cookies.get(name) || null;
127
+ }
128
+ /**
129
+ * Get all cookies matching the filter
130
+ */
131
+ async getAll(nameOrOptions) {
132
+ const name = typeof nameOrOptions === "string" ? nameOrOptions : nameOrOptions?.name;
133
+ const result = [];
134
+ const allNames = /* @__PURE__ */ new Set([
135
+ ...this.#cookies.keys(),
136
+ ...this.#changes.keys()
137
+ ]);
138
+ for (const cookieName of allNames) {
139
+ if (name && cookieName !== name)
140
+ continue;
141
+ if (this.#changes.has(cookieName) && this.#changes.get(cookieName) === null) {
142
+ continue;
143
+ }
144
+ const cookie = await this.get(cookieName);
145
+ if (cookie) {
146
+ result.push(cookie);
147
+ }
148
+ }
149
+ return result;
150
+ }
151
+ /**
152
+ * Set a cookie
153
+ */
154
+ async set(nameOrOptions, value) {
155
+ let cookie;
156
+ if (typeof nameOrOptions === "string") {
157
+ if (value === void 0) {
158
+ throw new TypeError("Cookie value is required");
159
+ }
160
+ cookie = {
161
+ name: nameOrOptions,
162
+ value,
163
+ path: "/",
164
+ sameSite: "strict"
165
+ };
166
+ } else {
167
+ cookie = {
168
+ path: "/",
169
+ sameSite: "strict",
170
+ ...nameOrOptions
171
+ };
172
+ }
173
+ const size = cookie.name.length + cookie.value.length;
174
+ if (size > 4096) {
175
+ throw new TypeError(
176
+ `Cookie name+value too large: ${size} bytes (max 4096)`
177
+ );
178
+ }
179
+ this.#changes.set(cookie.name, cookie);
180
+ }
181
+ /**
182
+ * Delete a cookie
183
+ */
184
+ async delete(nameOrOptions) {
185
+ const name = typeof nameOrOptions === "string" ? nameOrOptions : nameOrOptions.name;
186
+ if (!name) {
187
+ throw new TypeError("Cookie name is required");
188
+ }
189
+ this.#changes.set(name, null);
190
+ }
191
+ /**
192
+ * Get Set-Cookie headers for all changes
193
+ * This should be called when constructing the Response
194
+ */
195
+ getSetCookieHeaders() {
196
+ const headers = [];
197
+ for (const [name, change] of this.#changes) {
198
+ if (change === null) {
199
+ headers.push(
200
+ serializeCookie({
201
+ name,
202
+ value: "",
203
+ expires: 0,
204
+ path: "/"
205
+ })
206
+ );
207
+ } else {
208
+ headers.push(serializeCookie(change));
209
+ }
210
+ }
211
+ return headers;
212
+ }
213
+ /**
214
+ * Check if there are any pending changes
215
+ */
216
+ hasChanges() {
217
+ return this.#changes.size > 0;
218
+ }
219
+ /**
220
+ * Clear all pending changes (for testing/reset)
221
+ */
222
+ clearChanges() {
223
+ this.#changes.clear();
224
+ }
225
+ };
226
+ export {
227
+ RequestCookieStore,
228
+ parseCookieHeader,
229
+ parseSetCookieHeader,
230
+ serializeCookie
231
+ };
package/src/index.d.ts CHANGED
@@ -4,12 +4,192 @@
4
4
  * Platform = "ServiceWorker entrypoint loader for JavaScript runtimes"
5
5
  * Core responsibility: Take a ServiceWorker-style app file and make it run in this environment.
6
6
  */
7
- export type { Platform, CacheConfig, CacheBackendConfig, ServerOptions, CorsConfig, Handler, Server, ServiceWorkerOptions, ServiceWorkerInstance, PlatformDetection, PlatformRegistry, } from "./types.js";
8
- export { BasePlatform } from "./types.js";
9
- export { ServiceWorkerRuntime, createServiceWorkerGlobals, type ShovelFetchEvent, type ShovelInstallEvent, type ShovelActivateEvent, type ShovelStaticEvent, type BucketStorage as BucketStorageInterface, } from "./service-worker.js";
10
- export { PlatformBucketStorage, createBucketStorage, } from "./directory-storage.js";
11
- export { platformRegistry, detectPlatform, getPlatform, getPlatformAsync, } from "./registry.js";
12
- export { detectRuntime, detectDevelopmentPlatform, detectPlatforms, getBestPlatformDetection, resolvePlatform, createPlatform, displayPlatformInfo, } from "./detection.js";
13
- export { parseTTL, mergeCacheConfig, validateCacheConfig, createCorsHeaders, mergeHeaders, isPreflightRequest, createPreflightResponse, } from "./utils.js";
14
- export { getDirectoryHandle, getBucket, getFileSystemRoot } from "./filesystem.js";
15
- export { WorkerPool, type WorkerFactory, type PlatformWorker, type WorkerPoolOptions } from "./worker-pool.js";
7
+ /**
8
+ * Platform configuration
9
+ * Extended by platform-specific implementations (NodePlatformOptions, etc.)
10
+ */
11
+ export interface PlatformConfig {
12
+ }
13
+ /**
14
+ * Server options for platform implementations
15
+ */
16
+ export interface ServerOptions {
17
+ /** Port to listen on */
18
+ port?: number;
19
+ /** Host to bind to */
20
+ host?: string;
21
+ /** Development mode settings */
22
+ development?: {
23
+ /** Source maps support */
24
+ sourceMaps?: boolean;
25
+ /** Verbose logging */
26
+ verbose?: boolean;
27
+ };
28
+ }
29
+ /**
30
+ * Request handler function (Web Fetch API compatible)
31
+ */
32
+ export type Handler = (request: Request, context?: any) => Promise<Response> | Response;
33
+ /**
34
+ * Server instance returned by platform.createServer()
35
+ */
36
+ export interface Server {
37
+ /** Start listening for requests */
38
+ listen(): Promise<void>;
39
+ /** Stop the server */
40
+ close(): Promise<void>;
41
+ /** Get server address information */
42
+ address(): {
43
+ port: number;
44
+ host: string;
45
+ };
46
+ /** Get server URL */
47
+ readonly url: string;
48
+ /** Whether server is ready to accept requests */
49
+ readonly ready: boolean;
50
+ }
51
+ /**
52
+ * ServiceWorker entrypoint options
53
+ */
54
+ export interface ServiceWorkerOptions {
55
+ /** Additional context to provide */
56
+ context?: any;
57
+ /** Number of worker threads (Node/Bun only) */
58
+ workerCount?: number;
59
+ /** Enable hot reload (dev mode) - forces worker mode for reliable reloading */
60
+ hotReload?: boolean;
61
+ }
62
+ /**
63
+ * ServiceWorker instance returned by platform
64
+ */
65
+ export interface ServiceWorkerInstance {
66
+ /** The ServiceWorker runtime */
67
+ runtime: any;
68
+ /** Handle HTTP request */
69
+ handleRequest(request: Request): Promise<Response>;
70
+ /** Install the ServiceWorker */
71
+ install(): Promise<void>;
72
+ /** Activate the ServiceWorker */
73
+ activate(): Promise<void>;
74
+ /** Check if ready to handle requests */
75
+ readonly ready: boolean;
76
+ /** Dispose of resources */
77
+ dispose(): Promise<void>;
78
+ }
79
+ /**
80
+ * Platform interface - ServiceWorker entrypoint loader for JavaScript runtimes
81
+ *
82
+ * The core responsibility: "Take a ServiceWorker-style app file and make it run in this environment"
83
+ */
84
+ export interface Platform {
85
+ /**
86
+ * Platform name for identification
87
+ */
88
+ readonly name: string;
89
+ /**
90
+ * Load and run a ServiceWorker-style entrypoint
91
+ * This is where all the platform-specific complexity lives
92
+ */
93
+ loadServiceWorker(entrypoint: string, options?: ServiceWorkerOptions): Promise<ServiceWorkerInstance>;
94
+ /**
95
+ * SUPPORTING UTILITY - Create cache storage
96
+ * Returns empty CacheStorage - applications create caches on-demand via caches.open()
97
+ */
98
+ createCaches(): Promise<CacheStorage>;
99
+ /**
100
+ * SUPPORTING UTILITY - Create server instance for this platform
101
+ */
102
+ createServer(handler: Handler, options?: ServerOptions): Server;
103
+ }
104
+ /**
105
+ * Platform registry - internal implementation
106
+ */
107
+ interface PlatformRegistry {
108
+ /** Register a platform implementation */
109
+ register(name: string, platform: any): void;
110
+ /** Get platform by name */
111
+ get(name: string): any | undefined;
112
+ /** Get all registered platforms */
113
+ list(): string[];
114
+ }
115
+ /**
116
+ * Detect the current JavaScript runtime
117
+ */
118
+ export declare function detectRuntime(): "bun" | "deno" | "node";
119
+ /**
120
+ * Detect deployment platform from environment
121
+ *
122
+ * Supports:
123
+ * - Cloudflare Workers
124
+ *
125
+ * Future platforms (Lambda, Vercel, Netlify, Deno) will be added post-launch
126
+ */
127
+ export declare function detectDeploymentPlatform(): string | null;
128
+ /**
129
+ * Detect platform for development
130
+ *
131
+ * Priority:
132
+ * 1. Check package.json for installed @b9g/platform-* package
133
+ * 2. Fallback to current runtime (bun/node/deno)
134
+ */
135
+ export declare function detectDevelopmentPlatform(): string;
136
+ /**
137
+ * Resolve platform name from options or auto-detect
138
+ *
139
+ * Priority:
140
+ * 1. Explicit --platform or --target flag
141
+ * 2. Deployment platform detection (production environments)
142
+ * 3. Development platform detection (local runtime)
143
+ */
144
+ export declare function resolvePlatform(options: {
145
+ platform?: string;
146
+ target?: string;
147
+ }): string;
148
+ /**
149
+ * Create platform instance based on name
150
+ */
151
+ export declare function createPlatform(platformName: string, options?: any): Promise<any>;
152
+ /**
153
+ * Base platform class with shared adapter loading logic
154
+ * Platform implementations extend this and provide platform-specific methods
155
+ */
156
+ export declare abstract class BasePlatform implements Platform {
157
+ config: PlatformConfig;
158
+ constructor(config?: PlatformConfig);
159
+ abstract readonly name: string;
160
+ abstract loadServiceWorker(entrypoint: string, options?: any): Promise<any>;
161
+ abstract createServer(handler: any, options?: any): any;
162
+ /**
163
+ * Create cache storage
164
+ * Returns empty CacheStorage - applications create caches on-demand via caches.open()
165
+ */
166
+ createCaches(): Promise<CacheStorage>;
167
+ }
168
+ /**
169
+ * Global platform registry
170
+ */
171
+ declare class DefaultPlatformRegistry implements PlatformRegistry {
172
+ #private;
173
+ constructor();
174
+ register(name: string, platform: Platform): void;
175
+ get(name: string): Platform | undefined;
176
+ list(): string[];
177
+ }
178
+ /**
179
+ * Global platform registry instance
180
+ */
181
+ export declare const platformRegistry: DefaultPlatformRegistry;
182
+ /**
183
+ * Get platform by name with error handling
184
+ */
185
+ export declare function getPlatform(name?: string): Platform;
186
+ /**
187
+ * Get platform with async auto-registration fallback
188
+ */
189
+ export declare function getPlatformAsync(name?: string): Promise<Platform>;
190
+ export { ServiceWorkerPool, type WorkerPoolOptions, type WorkerMessage, type WorkerRequest, type WorkerResponse, type WorkerLoadMessage, type WorkerReadyMessage, type WorkerErrorMessage, type WorkerInitMessage, type WorkerInitializedMessage, } from "./worker-pool.js";
191
+ export { SingleThreadedRuntime, type SingleThreadedRuntimeOptions, } from "./single-threaded.js";
192
+ export { ShovelServiceWorkerRegistration, ShovelGlobalScope, FetchEvent, InstallEvent, ActivateEvent, ExtendableEvent, } from "./runtime.js";
193
+ export { RequestCookieStore, type CookieListItem, type CookieInit, type CookieStoreGetOptions, type CookieStoreDeleteOptions, type CookieSameSite, type CookieList, parseCookieHeader, serializeCookie, parseSetCookieHeader, } from "./cookie-store.js";
194
+ export { CustomBucketStorage } from "@b9g/filesystem";
195
+ export { loadConfig, getCacheConfig, getBucketConfig, parseConfigExpr, processConfigValue, matchPattern, createBucketFactory, createCacheFactory, type ShovelConfig, type CacheConfig, type BucketConfig, type BucketFactoryOptions, type CacheFactoryOptions, type ProcessedShovelConfig, } from "./config.js";