@b9g/platform 0.1.4 → 0.1.6

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/src/config.js ADDED
@@ -0,0 +1,517 @@
1
+ /// <reference types="./config.d.ts" />
2
+ // src/config.ts
3
+ import { readFileSync } from "fs";
4
+ import { resolve } from "path";
5
+ import { Cache } from "@b9g/cache";
6
+ function getEnv() {
7
+ if (typeof import.meta !== "undefined" && import.meta.env) {
8
+ return import.meta.env;
9
+ }
10
+ if (typeof process !== "undefined" && process.env) {
11
+ return process.env;
12
+ }
13
+ return {};
14
+ }
15
+ var Tokenizer = class {
16
+ #input;
17
+ #pos;
18
+ constructor(input) {
19
+ this.#input = input;
20
+ this.#pos = 0;
21
+ }
22
+ #peek() {
23
+ return this.#input[this.#pos] || "";
24
+ }
25
+ #advance() {
26
+ return this.#input[this.#pos++] || "";
27
+ }
28
+ #skipWhitespace() {
29
+ while (/\s/.test(this.#peek())) {
30
+ this.#advance();
31
+ }
32
+ }
33
+ next() {
34
+ this.#skipWhitespace();
35
+ const start = this.#pos;
36
+ const ch = this.#peek();
37
+ if (!ch) {
38
+ return { type: "EOF" /* EOF */, value: null, start, end: start };
39
+ }
40
+ if (ch === '"') {
41
+ this.#advance();
42
+ let value = "";
43
+ while (this.#peek() && this.#peek() !== '"') {
44
+ if (this.#peek() === "\\") {
45
+ this.#advance();
46
+ const next = this.#advance();
47
+ if (next === "n")
48
+ value += "\n";
49
+ else if (next === "t")
50
+ value += " ";
51
+ else
52
+ value += next;
53
+ } else {
54
+ value += this.#advance();
55
+ }
56
+ }
57
+ if (this.#peek() !== '"') {
58
+ throw new Error(`Unterminated string at position ${start}`);
59
+ }
60
+ this.#advance();
61
+ return { type: "STRING" /* STRING */, value, start, end: this.#pos };
62
+ }
63
+ if (/\d/.test(ch)) {
64
+ let value = "";
65
+ while (/\d/.test(this.#peek())) {
66
+ value += this.#advance();
67
+ }
68
+ return {
69
+ type: "NUMBER" /* NUMBER */,
70
+ value: parseInt(value, 10),
71
+ start,
72
+ end: this.#pos
73
+ };
74
+ }
75
+ if (ch === "=" && this.#input[this.#pos + 1] === "=" && this.#input[this.#pos + 2] === "=") {
76
+ this.#pos += 3;
77
+ return { type: "===" /* EQ_STRICT */, value: "===", start, end: this.#pos };
78
+ }
79
+ if (ch === "!" && this.#input[this.#pos + 1] === "=" && this.#input[this.#pos + 2] === "=") {
80
+ this.#pos += 3;
81
+ return { type: "!==" /* NE_STRICT */, value: "!==", start, end: this.#pos };
82
+ }
83
+ if (ch === "=" && this.#input[this.#pos + 1] === "=") {
84
+ this.#pos += 2;
85
+ return { type: "==" /* EQ */, value: "==", start, end: this.#pos };
86
+ }
87
+ if (ch === "!" && this.#input[this.#pos + 1] === "=") {
88
+ this.#pos += 2;
89
+ return { type: "!=" /* NE */, value: "!=", start, end: this.#pos };
90
+ }
91
+ if (ch === "|" && this.#input[this.#pos + 1] === "|") {
92
+ this.#pos += 2;
93
+ return { type: "||" /* OR */, value: "||", start, end: this.#pos };
94
+ }
95
+ if (ch === "&" && this.#input[this.#pos + 1] === "&") {
96
+ this.#pos += 2;
97
+ return { type: "&&" /* AND */, value: "&&", start, end: this.#pos };
98
+ }
99
+ if (ch === "?") {
100
+ this.#advance();
101
+ return { type: "?" /* QUESTION */, value: "?", start, end: this.#pos };
102
+ }
103
+ if (ch === "!") {
104
+ this.#advance();
105
+ return { type: "!" /* NOT */, value: "!", start, end: this.#pos };
106
+ }
107
+ if (ch === "(") {
108
+ this.#advance();
109
+ return { type: "(" /* LPAREN */, value: "(", start, end: this.#pos };
110
+ }
111
+ if (ch === ")") {
112
+ this.#advance();
113
+ return { type: ")" /* RPAREN */, value: ")", start, end: this.#pos };
114
+ }
115
+ if (ch === ":") {
116
+ const next = this.#input[this.#pos + 1];
117
+ if (next !== "/" && !/\d/.test(next)) {
118
+ this.#advance();
119
+ return { type: ":" /* COLON */, value: ":", start, end: this.#pos };
120
+ }
121
+ }
122
+ if (/\S/.test(ch) && !/[?!()=|&]/.test(ch)) {
123
+ let value = "";
124
+ while (/\S/.test(this.#peek()) && !/[?!()=|&]/.test(this.#peek())) {
125
+ if (this.#peek() === ":") {
126
+ const next = this.#input[this.#pos + 1];
127
+ if (next !== "/" && !/\d/.test(next)) {
128
+ break;
129
+ }
130
+ }
131
+ value += this.#advance();
132
+ }
133
+ if (value === "true")
134
+ return { type: "TRUE" /* TRUE */, value: true, start, end: this.#pos };
135
+ if (value === "false")
136
+ return { type: "FALSE" /* FALSE */, value: false, start, end: this.#pos };
137
+ if (value === "null")
138
+ return { type: "NULL" /* NULL */, value: null, start, end: this.#pos };
139
+ if (value === "undefined")
140
+ return {
141
+ type: "UNDEFINED" /* UNDEFINED */,
142
+ value: void 0,
143
+ start,
144
+ end: this.#pos
145
+ };
146
+ return { type: "IDENTIFIER" /* IDENTIFIER */, value, start, end: this.#pos };
147
+ }
148
+ throw new Error(`Unexpected character '${ch}' at position ${start}`);
149
+ }
150
+ };
151
+ var Parser = class {
152
+ #tokens;
153
+ #pos;
154
+ #env;
155
+ #strict;
156
+ constructor(input, env, strict) {
157
+ const tokenizer = new Tokenizer(input);
158
+ this.#tokens = [];
159
+ let token;
160
+ do {
161
+ token = tokenizer.next();
162
+ this.#tokens.push(token);
163
+ } while (token.type !== "EOF" /* EOF */);
164
+ this.#pos = 0;
165
+ this.#env = env;
166
+ this.#strict = strict;
167
+ }
168
+ #peek() {
169
+ return this.#tokens[this.#pos];
170
+ }
171
+ #advance() {
172
+ return this.#tokens[this.#pos++];
173
+ }
174
+ #expect(type) {
175
+ const token = this.#peek();
176
+ if (token.type !== type) {
177
+ throw new Error(
178
+ `Expected ${type} but got ${token.type} at position ${token.start}`
179
+ );
180
+ }
181
+ return this.#advance();
182
+ }
183
+ parse() {
184
+ const result = this.#parseExpr();
185
+ this.#expect("EOF" /* EOF */);
186
+ return result;
187
+ }
188
+ // Expr := Ternary
189
+ #parseExpr() {
190
+ return this.#parseTernary();
191
+ }
192
+ // Ternary := LogicalOr ('?' Expr ':' Expr)?
193
+ #parseTernary() {
194
+ let left = this.#parseLogicalOr();
195
+ if (this.#peek().type === "?" /* QUESTION */) {
196
+ this.#advance();
197
+ const trueBranch = this.#parseExpr();
198
+ this.#expect(":" /* COLON */);
199
+ const falseBranch = this.#parseExpr();
200
+ return left ? trueBranch : falseBranch;
201
+ }
202
+ return left;
203
+ }
204
+ // LogicalOr := LogicalAnd ('||' LogicalAnd)*
205
+ #parseLogicalOr() {
206
+ let left = this.#parseLogicalAnd();
207
+ while (this.#peek().type === "||" /* OR */) {
208
+ this.#advance();
209
+ const right = this.#parseLogicalAnd();
210
+ left = left || right;
211
+ }
212
+ return left;
213
+ }
214
+ // LogicalAnd := Equality ('&&' Equality)*
215
+ #parseLogicalAnd() {
216
+ let left = this.#parseEquality();
217
+ while (this.#peek().type === "&&" /* AND */) {
218
+ this.#advance();
219
+ const right = this.#parseEquality();
220
+ left = left && right;
221
+ }
222
+ return left;
223
+ }
224
+ // Equality := Unary (('===' | '!==' | '==' | '!=') Unary)*
225
+ #parseEquality() {
226
+ let left = this.#parseUnary();
227
+ while (true) {
228
+ const token = this.#peek();
229
+ if (token.type === "===" /* EQ_STRICT */) {
230
+ this.#advance();
231
+ const right = this.#parseUnary();
232
+ left = left === right;
233
+ } else if (token.type === "!==" /* NE_STRICT */) {
234
+ this.#advance();
235
+ const right = this.#parseUnary();
236
+ left = left !== right;
237
+ } else if (token.type === "==" /* EQ */) {
238
+ this.#advance();
239
+ const right = this.#parseUnary();
240
+ left = left == right;
241
+ } else if (token.type === "!=" /* NE */) {
242
+ this.#advance();
243
+ const right = this.#parseUnary();
244
+ left = left != right;
245
+ } else {
246
+ break;
247
+ }
248
+ }
249
+ return left;
250
+ }
251
+ // Unary := '!' Unary | Primary
252
+ #parseUnary() {
253
+ if (this.#peek().type === "!" /* NOT */) {
254
+ this.#advance();
255
+ return !this.#parseUnary();
256
+ }
257
+ return this.#parsePrimary();
258
+ }
259
+ // Primary := EnvVar | Literal | '(' Expr ')'
260
+ #parsePrimary() {
261
+ const token = this.#peek();
262
+ if (token.type === "(" /* LPAREN */) {
263
+ this.#advance();
264
+ const value = this.#parseExpr();
265
+ this.#expect(")" /* RPAREN */);
266
+ return value;
267
+ }
268
+ if (token.type === "STRING" /* STRING */) {
269
+ this.#advance();
270
+ return token.value;
271
+ }
272
+ if (token.type === "NUMBER" /* NUMBER */) {
273
+ this.#advance();
274
+ return token.value;
275
+ }
276
+ if (token.type === "TRUE" /* TRUE */) {
277
+ this.#advance();
278
+ return true;
279
+ }
280
+ if (token.type === "FALSE" /* FALSE */) {
281
+ this.#advance();
282
+ return false;
283
+ }
284
+ if (token.type === "NULL" /* NULL */) {
285
+ this.#advance();
286
+ return null;
287
+ }
288
+ if (token.type === "UNDEFINED" /* UNDEFINED */) {
289
+ this.#advance();
290
+ return void 0;
291
+ }
292
+ if (token.type === "IDENTIFIER" /* IDENTIFIER */) {
293
+ this.#advance();
294
+ const name = token.value;
295
+ if (/^[A-Z][A-Z0-9_]*$/.test(name)) {
296
+ const value = this.#env[name];
297
+ if (this.#strict && value === void 0) {
298
+ throw new Error(
299
+ `Undefined environment variable: ${name}
300
+ Fix:
301
+ 1. Set the env var: export ${name}=value
302
+ 2. Add a fallback: ${name} || defaultValue
303
+ 3. Add null check: ${name} == null ? ... : ...
304
+ 4. Use empty string for falsy: export ${name}=""`
305
+ );
306
+ }
307
+ if (typeof value === "string" && /^\d+$/.test(value)) {
308
+ return parseInt(value, 10);
309
+ }
310
+ return value;
311
+ }
312
+ return name;
313
+ }
314
+ throw new Error(
315
+ `Unexpected token ${token.type} at position ${token.start}`
316
+ );
317
+ }
318
+ };
319
+ function parseConfigExpr(expr, env = getEnv(), options = {}) {
320
+ const strict = options.strict !== false;
321
+ try {
322
+ const parser = new Parser(expr, env, strict);
323
+ return parser.parse();
324
+ } catch (error) {
325
+ throw new Error(
326
+ `Invalid config expression: ${expr}
327
+ Error: ${error instanceof Error ? error.message : String(error)}`
328
+ );
329
+ }
330
+ }
331
+ function processConfigValue(value, env = getEnv(), options = {}) {
332
+ if (typeof value === "string") {
333
+ if (/(\|\||&&|===|!==|==|!=|[?:!]|^[A-Z][A-Z0-9_]*$)/.test(value)) {
334
+ return parseConfigExpr(value, env, options);
335
+ }
336
+ return value;
337
+ }
338
+ if (Array.isArray(value)) {
339
+ return value.map((item) => processConfigValue(item, env, options));
340
+ }
341
+ if (value !== null && typeof value === "object") {
342
+ const processed = {};
343
+ for (const [key, val] of Object.entries(value)) {
344
+ processed[key] = processConfigValue(val, env, options);
345
+ }
346
+ return processed;
347
+ }
348
+ return value;
349
+ }
350
+ function matchPattern(name, config) {
351
+ if (config[name]) {
352
+ return config[name];
353
+ }
354
+ const patterns = [];
355
+ for (const [pattern, cfg] of Object.entries(config)) {
356
+ if (pattern === "*")
357
+ continue;
358
+ if (pattern.endsWith("*")) {
359
+ const prefix = pattern.slice(0, -1);
360
+ if (name.startsWith(prefix)) {
361
+ patterns.push({
362
+ pattern,
363
+ config: cfg,
364
+ prefixLength: prefix.length
365
+ });
366
+ }
367
+ }
368
+ }
369
+ if (patterns.length > 0) {
370
+ patterns.sort((a, b) => b.prefixLength - a.prefixLength);
371
+ return patterns[0].config;
372
+ }
373
+ return config["*"];
374
+ }
375
+ function loadConfig(cwd) {
376
+ const env = getEnv();
377
+ let rawConfig = {};
378
+ try {
379
+ const shovelPath = `${cwd}/shovel.json`;
380
+ const content = readFileSync(shovelPath, "utf-8");
381
+ rawConfig = JSON.parse(content);
382
+ } catch (error) {
383
+ try {
384
+ const pkgPath = `${cwd}/package.json`;
385
+ const content = readFileSync(pkgPath, "utf-8");
386
+ const pkgJSON = JSON.parse(content);
387
+ rawConfig = pkgJSON.shovel || {};
388
+ } catch (error2) {
389
+ }
390
+ }
391
+ const processed = processConfigValue(rawConfig, env, {
392
+ strict: true
393
+ });
394
+ const config = {
395
+ port: typeof processed.port === "number" ? processed.port : 3e3,
396
+ host: processed.host || "localhost",
397
+ workers: typeof processed.workers === "number" ? processed.workers : 1,
398
+ caches: processed.caches || {},
399
+ buckets: processed.buckets || {}
400
+ };
401
+ return config;
402
+ }
403
+ function getCacheConfig(config, name) {
404
+ return matchPattern(name, config.caches) || {};
405
+ }
406
+ function getBucketConfig(config, name) {
407
+ return matchPattern(name, config.buckets) || {};
408
+ }
409
+ var WELL_KNOWN_BUCKET_PATHS = {
410
+ static: (baseDir) => resolve(baseDir, "../static"),
411
+ server: (baseDir) => baseDir
412
+ };
413
+ var BUILTIN_BUCKET_PROVIDERS = {
414
+ node: "@b9g/filesystem/node.js",
415
+ memory: "@b9g/filesystem/memory.js",
416
+ s3: "@b9g/filesystem-s3"
417
+ };
418
+ function createBucketFactory(options) {
419
+ const { baseDir, config } = options;
420
+ return async (name) => {
421
+ const bucketConfig = config ? getBucketConfig(config, name) : {};
422
+ let bucketPath;
423
+ if (bucketConfig.path) {
424
+ bucketPath = String(bucketConfig.path);
425
+ } else if (WELL_KNOWN_BUCKET_PATHS[name]) {
426
+ bucketPath = WELL_KNOWN_BUCKET_PATHS[name](baseDir);
427
+ } else {
428
+ bucketPath = resolve(baseDir, `../${name}`);
429
+ }
430
+ const provider = String(bucketConfig.provider || "node");
431
+ const modulePath = BUILTIN_BUCKET_PROVIDERS[provider] || provider;
432
+ if (modulePath === "@b9g/filesystem/node.js") {
433
+ const { NodeBucket } = await import("@b9g/filesystem/node.js");
434
+ return new NodeBucket(bucketPath);
435
+ }
436
+ if (modulePath === "@b9g/filesystem/memory.js") {
437
+ const { MemoryBucket } = await import("@b9g/filesystem/memory.js");
438
+ return new MemoryBucket(name);
439
+ }
440
+ try {
441
+ const module = await import(modulePath);
442
+ const BucketClass = module.default || // Default export
443
+ module.S3Bucket || // Named export for s3
444
+ module.Bucket || // Generic Bucket export
445
+ Object.values(module).find(
446
+ (v) => typeof v === "function" && v.name?.includes("Bucket")
447
+ );
448
+ if (!BucketClass) {
449
+ throw new Error(
450
+ `Bucket module "${modulePath}" does not export a valid bucket class. Expected a default export or named export (S3Bucket, Bucket).`
451
+ );
452
+ }
453
+ const { provider: _, path: __, ...bucketOptions } = bucketConfig;
454
+ return new BucketClass(name, { path: bucketPath, ...bucketOptions });
455
+ } catch (error) {
456
+ if (error.code === "ERR_MODULE_NOT_FOUND" || error.code === "MODULE_NOT_FOUND") {
457
+ throw new Error(
458
+ `Bucket provider "${provider}" not found. Make sure the module "${modulePath}" is installed.
459
+ For S3: npm install @b9g/filesystem-s3`
460
+ );
461
+ }
462
+ throw error;
463
+ }
464
+ };
465
+ }
466
+ var BUILTIN_CACHE_PROVIDERS = {
467
+ memory: "@b9g/cache/memory.js",
468
+ redis: "@b9g/cache-redis"
469
+ };
470
+ function createCacheFactory(options = {}) {
471
+ const { config } = options;
472
+ return async (name) => {
473
+ const cacheConfig = config ? getCacheConfig(config, name) : {};
474
+ const provider = String(cacheConfig.provider || "memory");
475
+ const modulePath = BUILTIN_CACHE_PROVIDERS[provider] || provider;
476
+ if (modulePath === "@b9g/cache/memory.js") {
477
+ const { MemoryCache } = await import("@b9g/cache/memory.js");
478
+ return new MemoryCache(name, {
479
+ maxEntries: typeof cacheConfig.maxEntries === "number" ? cacheConfig.maxEntries : 1e3
480
+ });
481
+ }
482
+ try {
483
+ const module = await import(modulePath);
484
+ const CacheClass = module.default || // Default export
485
+ module.RedisCache || // Named export for redis
486
+ module.Cache || // Generic Cache export
487
+ Object.values(module).find(
488
+ (v) => typeof v === "function" && v.prototype instanceof Cache
489
+ );
490
+ if (!CacheClass) {
491
+ throw new Error(
492
+ `Cache module "${modulePath}" does not export a valid cache class. Expected a default export or named export (RedisCache, Cache) that extends Cache.`
493
+ );
494
+ }
495
+ const { provider: _, ...cacheOptions } = cacheConfig;
496
+ return new CacheClass(name, cacheOptions);
497
+ } catch (error) {
498
+ if (error.code === "ERR_MODULE_NOT_FOUND" || error.code === "MODULE_NOT_FOUND") {
499
+ throw new Error(
500
+ `Cache provider "${provider}" not found. Make sure the module "${modulePath}" is installed.
501
+ For redis: npm install @b9g/cache-redis`
502
+ );
503
+ }
504
+ throw error;
505
+ }
506
+ };
507
+ }
508
+ export {
509
+ createBucketFactory,
510
+ createCacheFactory,
511
+ getBucketConfig,
512
+ getCacheConfig,
513
+ loadConfig,
514
+ matchPattern,
515
+ parseConfigExpr,
516
+ processConfigValue
517
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Cookie Store API Implementation
3
+ * https://cookiestore.spec.whatwg.org/
4
+ *
5
+ * Provides asynchronous cookie management for ServiceWorker contexts
6
+ */
7
+ declare global {
8
+ interface CookieListItem {
9
+ domain?: string;
10
+ path?: string;
11
+ expires?: number;
12
+ secure?: boolean;
13
+ sameSite?: CookieSameSite;
14
+ partitioned?: boolean;
15
+ }
16
+ }
17
+ export type CookieSameSite = globalThis.CookieSameSite;
18
+ export type CookieInit = globalThis.CookieInit;
19
+ export type CookieStoreGetOptions = globalThis.CookieStoreGetOptions;
20
+ export type CookieStoreDeleteOptions = globalThis.CookieStoreDeleteOptions;
21
+ export type CookieListItem = globalThis.CookieListItem;
22
+ export type CookieList = CookieListItem[];
23
+ /**
24
+ * Parse Cookie header value into key-value pairs
25
+ * Cookie: name=value; name2=value2
26
+ */
27
+ export declare function parseCookieHeader(cookieHeader: string): Map<string, string>;
28
+ /**
29
+ * Serialize cookie into Set-Cookie header value
30
+ */
31
+ export declare function serializeCookie(cookie: CookieInit): string;
32
+ /**
33
+ * Parse Set-Cookie header into CookieListItem
34
+ */
35
+ export declare function parseSetCookieHeader(setCookieHeader: string): CookieListItem;
36
+ /**
37
+ * RequestCookieStore - Cookie Store implementation for ServiceWorker contexts
38
+ *
39
+ * This implementation:
40
+ * - Reads cookies from the incoming Request's Cookie header
41
+ * - Tracks changes (set/delete operations)
42
+ * - Exports changes as Set-Cookie headers for the Response
43
+ *
44
+ * It follows the Cookie Store API spec but is designed for server-side
45
+ * request handling rather than browser contexts.
46
+ */
47
+ export declare class RequestCookieStore extends EventTarget {
48
+ #private;
49
+ onchange: ((this: RequestCookieStore, ev: Event) => any) | null;
50
+ constructor(request?: Request);
51
+ /**
52
+ * Get a single cookie by name
53
+ */
54
+ get(nameOrOptions: string | CookieStoreGetOptions): Promise<CookieListItem | null>;
55
+ /**
56
+ * Get all cookies matching the filter
57
+ */
58
+ getAll(nameOrOptions?: string | CookieStoreGetOptions): Promise<CookieList>;
59
+ /**
60
+ * Set a cookie
61
+ */
62
+ set(nameOrOptions: string | CookieInit, value?: string): Promise<void>;
63
+ /**
64
+ * Delete a cookie
65
+ */
66
+ delete(nameOrOptions: string | CookieStoreDeleteOptions): Promise<void>;
67
+ /**
68
+ * Get Set-Cookie headers for all changes
69
+ * This should be called when constructing the Response
70
+ */
71
+ getSetCookieHeaders(): string[];
72
+ /**
73
+ * Check if there are any pending changes
74
+ */
75
+ hasChanges(): boolean;
76
+ /**
77
+ * Clear all pending changes (for testing/reset)
78
+ */
79
+ clearChanges(): void;
80
+ }