@hasna/shortlinks 0.1.23 → 0.1.24

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/dist/index.js CHANGED
@@ -1,8 +1,203 @@
1
1
  // @bun
2
- // src/database.ts
3
- import { Database } from "bun:sqlite";
4
- import { mkdirSync as mkdirSync2 } from "fs";
5
- import { dirname as dirname2 } from "path";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
12
+ var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
20
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
21
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
22
+ for (let key of __getOwnPropNames(mod))
23
+ if (!__hasOwnProp.call(to, key))
24
+ __defProp(to, key, {
25
+ get: __accessProp.bind(mod, key),
26
+ enumerable: true
27
+ });
28
+ if (canCache)
29
+ cache.set(mod, to);
30
+ return to;
31
+ };
32
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
33
+ var __require = import.meta.require;
34
+
35
+ // src/runtime.ts
36
+ var SHORTLINKS_RUNTIME_ENV = {
37
+ store: "HASNA_SHORTLINKS_STORE",
38
+ databaseUrl: "HASNA_SHORTLINKS_DATABASE_URL",
39
+ databaseSsl: "HASNA_SHORTLINKS_DATABASE_SSL"
40
+ };
41
+ var SHORTLINKS_RUNTIME_FALLBACK_ENV = {
42
+ store: "SHORTLINKS_STORE",
43
+ databaseUrl: "SHORTLINKS_DATABASE_URL",
44
+ databaseSsl: "SHORTLINKS_DATABASE_SSL"
45
+ };
46
+ var CANONICAL_SHORTLINKS_POSTGRES_CLUSTER = "hasna-xyz-infra-apps-prod-postgres";
47
+ var CANONICAL_SHORTLINKS_POSTGRES_DATABASE = "shortlinks";
48
+ var CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH = "hasna/xyz/opensource/shortlinks/prod/postgres";
49
+ function getCanonicalShortlinksPostgresConfig() {
50
+ return {
51
+ cluster: CANONICAL_SHORTLINKS_POSTGRES_CLUSTER,
52
+ database: CANONICAL_SHORTLINKS_POSTGRES_DATABASE,
53
+ runtimeSecretPath: CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH,
54
+ primaryEnv: SHORTLINKS_RUNTIME_ENV.databaseUrl,
55
+ fallbackEnv: SHORTLINKS_RUNTIME_FALLBACK_ENV.databaseUrl
56
+ };
57
+ }
58
+ function parseShortlinksStoreMode(value) {
59
+ const normalized = clean(value)?.toLowerCase();
60
+ if (!normalized)
61
+ return "local";
62
+ if (normalized === "local" || normalized === "postgres")
63
+ return normalized;
64
+ if (normalized === "pg")
65
+ return "postgres";
66
+ throw new Error(`${SHORTLINKS_RUNTIME_ENV.store} must be local or postgres`);
67
+ }
68
+ function getShortlinksStoreMode(env = process.env) {
69
+ return parseShortlinksStoreMode(readRuntimeEnv(env, "store").value);
70
+ }
71
+ function getShortlinksDatabaseUrl(env = process.env) {
72
+ return readRuntimeEnv(env, "databaseUrl").value;
73
+ }
74
+ function getShortlinksDatabaseSsl(env = process.env) {
75
+ return parseBoolean(readRuntimeEnv(env, "databaseSsl").value, true);
76
+ }
77
+ function getShortlinksRuntimeEnvName(env, key) {
78
+ const primary = SHORTLINKS_RUNTIME_ENV[key];
79
+ if (clean(env[primary]))
80
+ return primary;
81
+ return SHORTLINKS_RUNTIME_FALLBACK_ENV[key];
82
+ }
83
+ function loadShortlinksRuntimeConfig(env = process.env) {
84
+ const mode = getShortlinksStoreMode(env);
85
+ const databaseUrl = getShortlinksDatabaseUrl(env);
86
+ return {
87
+ service: "shortlinks",
88
+ mode,
89
+ ...databaseUrl ? {
90
+ database: {
91
+ provider: "postgres",
92
+ url: databaseUrl,
93
+ ssl: getShortlinksDatabaseSsl(env)
94
+ }
95
+ } : {}
96
+ };
97
+ }
98
+ function assertShortlinksPostgresConfig(config) {
99
+ if (config.mode !== "postgres")
100
+ return;
101
+ if (!config.database?.url) {
102
+ throw new Error(`${SHORTLINKS_RUNTIME_ENV.databaseUrl} is required when ${SHORTLINKS_RUNTIME_ENV.store}=postgres`);
103
+ }
104
+ }
105
+ function getShortlinksRuntimeStatus(env = process.env) {
106
+ const issues = [];
107
+ const warnings = [];
108
+ let config;
109
+ try {
110
+ config = loadShortlinksRuntimeConfig(env);
111
+ } catch (error) {
112
+ issues.push(error instanceof Error ? error.message : String(error));
113
+ config = { service: "shortlinks", mode: "local" };
114
+ }
115
+ try {
116
+ assertShortlinksPostgresConfig(config);
117
+ } catch (error) {
118
+ issues.push(error instanceof Error ? error.message : String(error));
119
+ }
120
+ if (config.mode === "local" && config.database?.url) {
121
+ warnings.push(`${SHORTLINKS_RUNTIME_ENV.store}=local ignores configured Postgres database settings`);
122
+ }
123
+ return {
124
+ ok: issues.length === 0,
125
+ service: "shortlinks",
126
+ mode: config.mode,
127
+ local_default: config.mode === "local",
128
+ postgres_enabled: config.mode === "postgres",
129
+ database: {
130
+ configured: Boolean(config.database?.url),
131
+ provider: config.database?.provider ?? null,
132
+ redacted_url: redactDatabaseUrl(config.database?.url),
133
+ ssl: config.database?.ssl ?? null
134
+ },
135
+ env: runtimeEnvStatus(env),
136
+ canonical: getCanonicalShortlinksPostgresConfig(),
137
+ issues,
138
+ warnings,
139
+ no_network: true
140
+ };
141
+ }
142
+ function redactDatabaseUrl(value) {
143
+ if (!value)
144
+ return null;
145
+ try {
146
+ const url = new URL(value);
147
+ if (url.username)
148
+ url.username = "***";
149
+ if (url.password)
150
+ url.password = "***";
151
+ for (const key of Array.from(url.searchParams.keys())) {
152
+ if (isSensitiveQueryKey(key))
153
+ url.searchParams.set(key, "***");
154
+ }
155
+ return url.toString();
156
+ } catch {
157
+ return "(redacted)";
158
+ }
159
+ }
160
+ function runtimeEnvStatus(env) {
161
+ return Object.fromEntries(Object.entries(SHORTLINKS_RUNTIME_ENV).map(([key, name]) => {
162
+ const activeName = getShortlinksRuntimeEnvName(env, key);
163
+ return [
164
+ key,
165
+ {
166
+ name,
167
+ active_name: activeName,
168
+ configured: Boolean(clean(env[activeName]))
169
+ }
170
+ ];
171
+ }));
172
+ }
173
+ function readRuntimeEnv(env, key) {
174
+ const primary = SHORTLINKS_RUNTIME_ENV[key];
175
+ const primaryValue = clean(env[primary]);
176
+ if (primaryValue)
177
+ return { name: primary, value: primaryValue };
178
+ const fallback = SHORTLINKS_RUNTIME_FALLBACK_ENV[key];
179
+ return { name: fallback, value: clean(env[fallback]) };
180
+ }
181
+ function parseBoolean(value, fallback) {
182
+ const normalized = clean(value)?.toLowerCase();
183
+ if (!normalized)
184
+ return fallback;
185
+ if (["1", "true", "yes", "on"].includes(normalized))
186
+ return true;
187
+ if (["0", "false", "no", "off"].includes(normalized))
188
+ return false;
189
+ throw new Error(`${SHORTLINKS_RUNTIME_ENV.databaseSsl} must be true or false`);
190
+ }
191
+ function isSensitiveQueryKey(key) {
192
+ return /(?:^|[_-])(pass(?:word)?|pwd|secret|token|credential|auth|api[_-]?key|access[_-]?key)(?:$|[_-])/i.test(key);
193
+ }
194
+ function clean(value) {
195
+ const trimmed = value?.trim();
196
+ return trimmed ? trimmed : undefined;
197
+ }
198
+
199
+ // src/pg-store.ts
200
+ import { createHash } from "crypto";
6
201
 
7
202
  // src/config.ts
8
203
  import { existsSync, linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
@@ -134,6 +329,9 @@ function formatShortUrl(hostname, slug, publicBaseUrl) {
134
329
  }
135
330
 
136
331
  // src/database.ts
332
+ import { Database } from "bun:sqlite";
333
+ import { mkdirSync as mkdirSync2 } from "fs";
334
+ import { dirname as dirname2 } from "path";
137
335
  function now() {
138
336
  return new Date().toISOString();
139
337
  }
@@ -241,8 +439,6 @@ class ShortlinksDatabase {
241
439
  }
242
440
  }
243
441
  }
244
- // src/store.ts
245
- import { createHash } from "crypto";
246
442
 
247
443
  // src/machine.ts
248
444
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -288,38 +484,90 @@ function getMachineId() {
288
484
  return id;
289
485
  }
290
486
 
291
- // src/store.ts
487
+ // src/pg-store.ts
292
488
  function parseJsonObject(value) {
293
489
  if (!value)
294
490
  return {};
491
+ if (typeof value === "object" && !Array.isArray(value))
492
+ return value;
295
493
  try {
296
- const parsed = JSON.parse(value);
494
+ const parsed = JSON.parse(String(value));
297
495
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
298
496
  } catch {
299
497
  return {};
300
498
  }
301
499
  }
500
+ async function loadPgPool() {
501
+ const importer = new Function("specifier", "return import(specifier)");
502
+ const module = await importer("pg");
503
+ return module.Pool;
504
+ }
505
+ function toPostgresSql(sql) {
506
+ let index = 0;
507
+ return sql.replace(/\?/g, () => `$${++index}`);
508
+ }
509
+ function createPgPoolConfig(connectionString, options = {}) {
510
+ const ssl = options.ssl ?? true;
511
+ return {
512
+ connectionString,
513
+ ...ssl ? { ssl: { rejectUnauthorized: true } } : { ssl: false }
514
+ };
515
+ }
516
+
517
+ class PgPoolAdapter {
518
+ pool;
519
+ constructor(pool) {
520
+ this.pool = pool;
521
+ }
522
+ static async create(connectionString, options = {}) {
523
+ const Pool = await loadPgPool();
524
+ return new PgPoolAdapter(new Pool(createPgPoolConfig(connectionString, options)));
525
+ }
526
+ async get(sql, ...params) {
527
+ const result = await this.pool.query(toPostgresSql(sql), params);
528
+ return result.rows[0] ?? null;
529
+ }
530
+ async all(sql, ...params) {
531
+ const result = await this.pool.query(toPostgresSql(sql), params);
532
+ return result.rows;
533
+ }
534
+ async run(sql, ...params) {
535
+ return this.pool.query(toPostgresSql(sql), params);
536
+ }
537
+ async close() {
538
+ await this.pool.end();
539
+ }
540
+ }
541
+ function toIsoString(value) {
542
+ if (value instanceof Date)
543
+ return value.toISOString();
544
+ return String(value);
545
+ }
546
+ function nullableIso(value) {
547
+ if (value === null || value === undefined)
548
+ return null;
549
+ return toIsoString(value);
550
+ }
302
551
  function domainFromRow(row) {
303
552
  return {
304
553
  ...row,
305
554
  default_domain: Boolean(row.default_domain),
555
+ synced_at: nullableIso(row.synced_at),
556
+ created_at: toIsoString(row.created_at),
557
+ updated_at: toIsoString(row.updated_at),
306
558
  metadata: parseJsonObject(row.metadata)
307
559
  };
308
560
  }
309
561
  function linkFromRow(row) {
310
- const config = loadConfig();
311
- const publicBaseUrl = config.defaultDomain === row.hostname ? config.publicBaseUrl : undefined;
312
562
  return {
313
563
  ...row,
314
564
  active: Boolean(row.active),
565
+ expires_at: nullableIso(row.expires_at),
566
+ synced_at: nullableIso(row.synced_at),
567
+ created_at: toIsoString(row.created_at),
568
+ updated_at: toIsoString(row.updated_at),
315
569
  metadata: parseJsonObject(row.metadata),
316
- short_url: formatShortUrl(row.hostname, row.slug, publicBaseUrl)
317
- };
318
- }
319
- function clickFromRow(row) {
320
- return {
321
- ...row,
322
- metadata: parseJsonObject(row.metadata)
570
+ short_url: formatShortUrl(row.hostname, row.slug)
323
571
  };
324
572
  }
325
573
  function validateDestinationUrl(url) {
@@ -342,26 +590,61 @@ function isoOrNull(input) {
342
590
  throw new Error(`Invalid date: ${input}`);
343
591
  return date.toISOString();
344
592
  }
345
-
346
- class ShortlinksStore {
347
- database;
348
- constructor(dbPath) {
349
- this.database = new ShortlinksDatabase(dbPath);
593
+ function clickFromRow(row) {
594
+ return {
595
+ ...row,
596
+ clicked_at: toIsoString(row.clicked_at),
597
+ synced_at: nullableIso(row.synced_at),
598
+ created_at: toIsoString(row.created_at),
599
+ updated_at: toIsoString(row.updated_at),
600
+ metadata: parseJsonObject(row.metadata)
601
+ };
602
+ }
603
+ function createKitPgAdapter(client) {
604
+ return {
605
+ async get(sql, ...params) {
606
+ return client.get(toPostgresSql(sql), params);
607
+ },
608
+ async all(sql, ...params) {
609
+ return client.many(toPostgresSql(sql), params);
610
+ },
611
+ async run(sql, ...params) {
612
+ return client.query(toPostgresSql(sql), params);
613
+ }
614
+ };
615
+ }
616
+
617
+ class PgShortlinksStore {
618
+ pg;
619
+ constructor(pg) {
620
+ this.pg = pg;
350
621
  }
351
- close() {
352
- this.database.close();
622
+ static async fromConnectionString(connectionString, options = {}) {
623
+ return new PgShortlinksStore(await PgPoolAdapter.create(connectionString, options));
353
624
  }
354
- addDomain(input) {
625
+ static fromQueryClient(client) {
626
+ return new PgShortlinksStore(createKitPgAdapter(client));
627
+ }
628
+ static async fromEnv(env = process.env) {
629
+ const connectionString = getShortlinksDatabaseUrl(env);
630
+ if (!connectionString) {
631
+ throw new Error("HASNA_SHORTLINKS_DATABASE_URL is required when shortlinks uses the postgres store");
632
+ }
633
+ return PgShortlinksStore.fromConnectionString(connectionString, { ssl: getShortlinksDatabaseSsl(env) });
634
+ }
635
+ async close() {
636
+ await this.pg.close?.();
637
+ }
638
+ async addDomain(input) {
355
639
  const hostname2 = normalizeHostname(input.hostname);
356
640
  const timestamp = now();
357
641
  const machineId = getMachineId();
358
- const existing = this.getDomain(hostname2);
642
+ const existing = await this.getDomain(hostname2);
359
643
  const id = existing?.id || makeId("dom");
360
644
  if (input.defaultDomain) {
361
- this.database.db.query("UPDATE domains SET default_domain = 0, updated_at = ?, synced_at = NULL").run(timestamp);
362
- updateConfig({ defaultDomain: hostname2, publicBaseUrl: `https://${hostname2}` });
645
+ await this.pg.run("UPDATE domains SET default_domain = 0, updated_at = ? WHERE default_domain = 1", timestamp);
363
646
  }
364
- this.database.db.query(`
647
+ await this.pg.run(`
365
648
  INSERT INTO domains (
366
649
  id, hostname, provider, default_domain, cloudflare_zone_id, cloudflare_account_id,
367
650
  cloudflare_worker_name, origin_url, notes, metadata, machine_id, synced_at, created_at, updated_at
@@ -379,37 +662,31 @@ class ShortlinksStore {
379
662
  machine_id = excluded.machine_id,
380
663
  synced_at = NULL,
381
664
  updated_at = excluded.updated_at
382
- `).run(id, hostname2, input.provider || existing?.provider || "manual", input.defaultDomain ?? existing?.default_domain ? 1 : 0, input.cloudflareZoneId || existing?.cloudflare_zone_id || null, input.cloudflareAccountId || existing?.cloudflare_account_id || null, input.cloudflareWorkerName || existing?.cloudflare_worker_name || null, input.originUrl || existing?.origin_url || null, input.notes || existing?.notes || null, JSON.stringify(input.metadata || existing?.metadata || {}), machineId, existing?.created_at || timestamp, timestamp);
383
- return this.getDomain(hostname2);
665
+ `, id, hostname2, input.provider || existing?.provider || "manual", input.defaultDomain ?? existing?.default_domain ? 1 : 0, input.cloudflareZoneId || existing?.cloudflare_zone_id || null, input.cloudflareAccountId || existing?.cloudflare_account_id || null, input.cloudflareWorkerName || existing?.cloudflare_worker_name || null, input.originUrl || existing?.origin_url || null, input.notes || existing?.notes || null, JSON.stringify(input.metadata || existing?.metadata || {}), machineId, existing?.created_at || timestamp, timestamp);
666
+ return await this.getDomain(hostname2);
384
667
  }
385
- listDomains() {
386
- const rows = this.database.db.query(`
668
+ async listDomains() {
669
+ const rows = await this.pg.all(`
387
670
  SELECT * FROM domains
388
671
  ORDER BY default_domain DESC, hostname ASC
389
- `).all();
672
+ `);
390
673
  return rows.map(domainFromRow);
391
674
  }
392
- getDomain(hostnameOrId) {
675
+ async getDomain(hostnameOrId) {
393
676
  const normalized = hostnameOrId.includes(".") || hostnameOrId.includes("://") ? normalizeHostname(hostnameOrId) : hostnameOrId;
394
- const row = this.database.db.query(`
677
+ const row = await this.pg.get(`
395
678
  SELECT * FROM domains WHERE hostname = ? OR id = ? LIMIT 1
396
- `).get(normalized, hostnameOrId);
679
+ `, normalized, hostnameOrId);
397
680
  return row ? domainFromRow(row) : null;
398
681
  }
399
- getDefaultDomain() {
400
- const config = loadConfig();
401
- if (config.defaultDomain) {
402
- const configured = this.getDomain(config.defaultDomain);
403
- if (configured)
404
- return configured;
405
- }
406
- const row = this.database.db.query(`
682
+ async getDefaultDomain() {
683
+ const row = await this.pg.get(`
407
684
  SELECT * FROM domains ORDER BY default_domain DESC, created_at ASC LIMIT 1
408
- `).get();
685
+ `);
409
686
  return row ? domainFromRow(row) : null;
410
687
  }
411
- createLink(input) {
412
- const domain = input.domain ? this.getDomain(input.domain) : this.getDefaultDomain();
688
+ async createLink(input) {
689
+ const domain = input.domain ? await this.getDomain(input.domain) : await this.getDefaultDomain();
413
690
  if (!domain) {
414
691
  throw new Error("No domain configured. Run `shortlinks domain add <domain> --default` first.");
415
692
  }
@@ -417,46 +694,45 @@ class ShortlinksStore {
417
694
  const timestamp = now();
418
695
  const machineId = getMachineId();
419
696
  const expiresAt = isoOrNull(input.expiresAt);
420
- const slug = input.slug ? normalizeSlug(input.slug) : this.generateAvailableSlug(domain.id, input.slugLength || DEFAULT_SLUG_LENGTH);
697
+ const slug = input.slug ? normalizeSlug(input.slug) : await this.generateAvailableSlug(domain.id, input.slugLength || DEFAULT_SLUG_LENGTH);
421
698
  try {
422
- this.database.db.query(`
699
+ await this.pg.run(`
423
700
  INSERT INTO links (
424
701
  id, domain_id, slug, destination_url, title, active, expires_at, metadata,
425
702
  machine_id, synced_at, created_at, updated_at
426
703
  )
427
704
  VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, NULL, ?, ?)
428
- `).run(makeId("lnk"), domain.id, slug, destinationUrl, input.title || null, expiresAt, JSON.stringify(input.metadata || {}), machineId, timestamp, timestamp);
705
+ `, makeId("lnk"), domain.id, slug, destinationUrl, input.title || null, expiresAt, JSON.stringify(input.metadata || {}), machineId, timestamp, timestamp);
429
706
  } catch (error) {
430
707
  const message = error instanceof Error ? error.message : String(error);
431
- if (message.includes("UNIQUE")) {
708
+ if (message.includes("unique") || message.includes("duplicate")) {
432
709
  throw new Error(`Slug already exists for ${domain.hostname}: ${slug}`);
433
710
  }
434
711
  throw error;
435
712
  }
436
- return this.getLink(domain.hostname, slug);
713
+ return await this.getLink(domain.hostname, slug);
437
714
  }
438
- listLinks(options = {}) {
715
+ async listLinks(options = {}) {
439
716
  const params = [];
440
717
  let where = "WHERE 1 = 1";
441
718
  if (options.domain) {
442
719
  where += " AND d.hostname = ?";
443
720
  params.push(normalizeHostname(options.domain));
444
721
  }
445
- if (options.activeOnly) {
722
+ if (options.activeOnly)
446
723
  where += " AND l.active = 1";
447
- }
448
724
  params.push(options.limit || 100);
449
- const rows = this.database.db.query(`
725
+ const rows = await this.pg.all(`
450
726
  SELECT l.*, d.hostname
451
727
  FROM links l
452
728
  JOIN domains d ON d.id = l.domain_id
453
729
  ${where}
454
730
  ORDER BY l.created_at DESC
455
731
  LIMIT ?
456
- `).all(...params);
732
+ `, ...params);
457
733
  return rows.map(linkFromRow);
458
734
  }
459
- getLink(domainOrSlug, maybeSlug) {
735
+ async getLink(domainOrSlug, maybeSlug) {
460
736
  const slug = normalizeSlug(maybeSlug || domainOrSlug);
461
737
  const params = [slug];
462
738
  let domainClause = "";
@@ -464,372 +740,206 @@ class ShortlinksStore {
464
740
  domainClause = "AND d.hostname = ?";
465
741
  params.push(normalizeHostname(domainOrSlug));
466
742
  }
467
- const row = this.database.db.query(`
743
+ const row = await this.pg.get(`
468
744
  SELECT l.*, d.hostname
469
745
  FROM links l
470
746
  JOIN domains d ON d.id = l.domain_id
471
747
  WHERE l.slug = ? ${domainClause}
472
748
  ORDER BY d.default_domain DESC, l.created_at ASC
473
749
  LIMIT 1
474
- `).get(...params);
750
+ `, ...params);
475
751
  return row ? linkFromRow(row) : null;
476
752
  }
477
- resolve(hostname2, slug) {
478
- const normalizedSlug = normalizeSlug(slug);
753
+ async totalStats() {
754
+ const row = await this.pg.get(`
755
+ SELECT
756
+ (SELECT COUNT(*)::int FROM domains) AS domains,
757
+ (SELECT COUNT(*)::int FROM links) AS links,
758
+ (SELECT COUNT(*)::int FROM clicks) AS clicks
759
+ `);
760
+ return row;
761
+ }
762
+ async resolve(hostname2, slug) {
479
763
  const normalizedHost = normalizeHostname(hostname2);
480
- const row = this.database.db.query(`
764
+ const normalizedSlug = normalizeSlug(slug);
765
+ const row = await this.pg.get(`
481
766
  SELECT l.*, d.hostname
482
767
  FROM links l
483
768
  JOIN domains d ON d.id = l.domain_id
484
769
  WHERE d.hostname = ? AND l.slug = ?
485
770
  LIMIT 1
486
- `).get(normalizedHost, normalizedSlug);
771
+ `, normalizedHost, normalizedSlug);
487
772
  if (row)
488
773
  return linkFromRow(row);
489
- const fallback = this.getDefaultDomain();
490
- if (!fallback || fallback.hostname === normalizedHost)
491
- return null;
492
- return this.getLink(fallback.hostname, normalizedSlug);
774
+ const fallback = await this.pg.get(`
775
+ SELECT l.*, d.hostname
776
+ FROM links l
777
+ JOIN domains d ON d.id = l.domain_id
778
+ WHERE d.default_domain = 1 AND l.slug = ?
779
+ ORDER BY d.created_at ASC
780
+ LIMIT 1
781
+ `, normalizedSlug);
782
+ return fallback ? linkFromRow(fallback) : null;
493
783
  }
494
- setLinkActive(domainOrSlug, maybeSlugOrActive, maybeActive) {
784
+ async setLinkActive(domainOrSlug, maybeSlugOrActive, maybeActive) {
495
785
  const active = typeof maybeSlugOrActive === "boolean" ? maybeSlugOrActive : Boolean(maybeActive);
496
- const link = typeof maybeSlugOrActive === "boolean" ? this.getLink(domainOrSlug) : this.getLink(domainOrSlug, maybeSlugOrActive);
786
+ const link = typeof maybeSlugOrActive === "boolean" ? await this.getLink(domainOrSlug) : await this.getLink(domainOrSlug, maybeSlugOrActive);
497
787
  if (!link)
498
788
  throw new Error("Link not found.");
499
789
  const timestamp = now();
500
- this.database.db.query(`
790
+ await this.pg.run(`
501
791
  UPDATE links SET active = ?, updated_at = ?, synced_at = NULL WHERE id = ?
502
- `).run(active ? 1 : 0, timestamp, link.id);
503
- return this.getLink(link.hostname, link.slug);
792
+ `, active ? 1 : 0, timestamp, link.id);
793
+ return await this.getLink(link.hostname, link.slug);
504
794
  }
505
- deleteLink(domainOrSlug, maybeSlug) {
506
- const link = maybeSlug ? this.getLink(domainOrSlug, maybeSlug) : this.getLink(domainOrSlug);
795
+ async deleteLink(domainOrSlug, maybeSlug) {
796
+ const link = maybeSlug ? await this.getLink(domainOrSlug, maybeSlug) : await this.getLink(domainOrSlug);
507
797
  if (!link)
508
798
  throw new Error("Link not found.");
509
- this.database.db.query("DELETE FROM links WHERE id = ?").run(link.id);
799
+ await this.pg.run("DELETE FROM links WHERE id = ?", link.id);
510
800
  return link;
511
801
  }
512
- recordClick(link, input = {}) {
802
+ async recordClick(link, input = {}) {
513
803
  const timestamp = now();
514
804
  const machineId = getMachineId();
515
805
  const ipHash = input.ip ? this.hashIp(input.ip) : null;
516
806
  const id = makeId("clk");
517
- this.database.db.query(`
807
+ await this.pg.run(`
518
808
  INSERT INTO clicks (
519
809
  id, link_id, domain_id, slug, clicked_at, ip_hash, user_agent, referer,
520
810
  country, city, metadata, machine_id, synced_at, created_at, updated_at
521
811
  )
522
812
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
523
- `).run(id, link.id, link.domain_id, link.slug, timestamp, ipHash, input.userAgent || null, input.referer || null, input.country || null, input.city || null, JSON.stringify(input.metadata || {}), machineId, timestamp, timestamp);
524
- const row = this.database.db.query("SELECT * FROM clicks WHERE id = ?").get(id);
813
+ `, id, link.id, link.domain_id, link.slug, timestamp, ipHash, input.userAgent || null, input.referer || null, input.country || null, input.city || null, JSON.stringify(input.metadata || {}), machineId, timestamp, timestamp);
814
+ const row = await this.pg.get("SELECT * FROM clicks WHERE id = ?", id);
525
815
  return clickFromRow(row);
526
816
  }
527
- getStats(domainOrSlug, maybeSlug) {
528
- const link = maybeSlug ? this.getLink(domainOrSlug, maybeSlug) : this.getLink(domainOrSlug);
817
+ async getStats(domainOrSlug, maybeSlug) {
818
+ const link = maybeSlug ? await this.getLink(domainOrSlug, maybeSlug) : await this.getLink(domainOrSlug);
529
819
  if (!link)
530
820
  throw new Error("Link not found.");
531
- const summary = this.database.db.query(`
532
- SELECT COUNT(*) AS clicks, MAX(clicked_at) AS last_clicked_at FROM clicks WHERE link_id = ?
533
- `).get(link.id);
534
- const topReferrers = this.database.db.query(`
535
- SELECT referer, COUNT(*) AS clicks
821
+ const summary = await this.pg.get(`
822
+ SELECT COUNT(*)::int AS clicks, MAX(clicked_at) AS last_clicked_at FROM clicks WHERE link_id = ?
823
+ `, link.id);
824
+ const topReferrers = await this.pg.all(`
825
+ SELECT referer, COUNT(*)::int AS clicks
536
826
  FROM clicks
537
827
  WHERE link_id = ?
538
828
  GROUP BY referer
539
829
  ORDER BY clicks DESC
540
830
  LIMIT 10
541
- `).all(link.id);
542
- const topUserAgents = this.database.db.query(`
543
- SELECT user_agent, COUNT(*) AS clicks
831
+ `, link.id);
832
+ const topUserAgents = await this.pg.all(`
833
+ SELECT user_agent, COUNT(*)::int AS clicks
544
834
  FROM clicks
545
835
  WHERE link_id = ?
546
836
  GROUP BY user_agent
547
837
  ORDER BY clicks DESC
548
838
  LIMIT 10
549
- `).all(link.id);
839
+ `, link.id);
550
840
  return {
551
841
  link,
552
842
  clicks: summary.clicks,
553
- last_clicked_at: summary.last_clicked_at,
843
+ last_clicked_at: nullableIso(summary.last_clicked_at),
554
844
  top_referrers: topReferrers,
555
845
  top_user_agents: topUserAgents
556
846
  };
557
847
  }
558
- totalStats() {
559
- const row = this.database.db.query(`
560
- SELECT
561
- (SELECT COUNT(*) FROM domains) AS domains,
562
- (SELECT COUNT(*) FROM links) AS links,
563
- (SELECT COUNT(*) FROM clicks) AS clicks
564
- `).get();
565
- return row;
848
+ hashIp(ip) {
849
+ return createHash("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
566
850
  }
567
- generateAvailableSlug(domainId, length) {
851
+ async generateAvailableSlug(domainId, length) {
568
852
  for (let attempt = 0;attempt < 32; attempt += 1) {
569
853
  const slug = randomToken(length);
570
- const exists = this.database.db.query(`
854
+ const exists = await this.pg.get(`
571
855
  SELECT 1 FROM links WHERE domain_id = ? AND slug = ? LIMIT 1
572
- `).get(domainId, slug);
856
+ `, domainId, slug);
573
857
  if (!exists)
574
858
  return slug;
575
859
  }
576
860
  throw new Error("Could not generate an unused slug after 32 attempts.");
577
861
  }
578
- hashIp(ip) {
579
- return createHash("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
862
+ }
863
+ async function applyPostgresMigrations(connectionString, migrations, options = {}) {
864
+ const Pool = await loadPgPool();
865
+ const pool = new Pool(createPgPoolConfig(connectionString, options));
866
+ const client = await pool.connect();
867
+ const run = (sql, ...params) => client.query(toPostgresSql(sql), params);
868
+ const get = async (sql, ...params) => {
869
+ const result = await run(sql, ...params);
870
+ return result.rows[0] ?? null;
871
+ };
872
+ const applied = [];
873
+ const skipped = [];
874
+ try {
875
+ await run("BEGIN");
876
+ await run(`
877
+ SELECT pg_advisory_xact_lock(hashtext(?))
878
+ `, "shortlinks:migrations");
879
+ await run(`
880
+ CREATE TABLE IF NOT EXISTS _shortlinks_migrations (
881
+ id INTEGER PRIMARY KEY,
882
+ service TEXT NOT NULL DEFAULT 'shortlinks',
883
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
884
+ )
885
+ `);
886
+ for (let i = 0;i < migrations.length; i += 1) {
887
+ const id = i + 1;
888
+ const existing = await get("SELECT id FROM _shortlinks_migrations WHERE id = ? LIMIT 1", id);
889
+ if (existing) {
890
+ skipped.push(id);
891
+ continue;
892
+ }
893
+ await run(migrations[i]);
894
+ await run("INSERT INTO _shortlinks_migrations (id, service, applied_at) VALUES (?, ?, now())", id, "shortlinks");
895
+ applied.push(id);
896
+ }
897
+ await run("COMMIT");
898
+ return { service: "shortlinks", applied, skipped };
899
+ } catch (error) {
900
+ try {
901
+ await run("ROLLBACK");
902
+ } catch {}
903
+ throw error;
904
+ } finally {
905
+ client.release();
906
+ await pool.end();
580
907
  }
581
908
  }
582
- // src/pg-store.ts
583
- import { createHash as createHash2 } from "crypto";
584
909
 
585
- // src/runtime.ts
586
- var SHORTLINKS_RUNTIME_ENV = {
587
- store: "HASNA_SHORTLINKS_STORE",
588
- databaseUrl: "HASNA_SHORTLINKS_DATABASE_URL",
589
- databaseSsl: "HASNA_SHORTLINKS_DATABASE_SSL"
590
- };
591
- var SHORTLINKS_RUNTIME_FALLBACK_ENV = {
592
- store: "SHORTLINKS_STORE",
593
- databaseUrl: "SHORTLINKS_DATABASE_URL",
594
- databaseSsl: "SHORTLINKS_DATABASE_SSL"
595
- };
596
- var CANONICAL_SHORTLINKS_POSTGRES_CLUSTER = "hasna-xyz-infra-apps-prod-postgres";
597
- var CANONICAL_SHORTLINKS_POSTGRES_DATABASE = "shortlinks";
598
- var CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH = "hasna/xyz/opensource/shortlinks/prod/postgres";
599
- function getCanonicalShortlinksPostgresConfig() {
600
- return {
601
- cluster: CANONICAL_SHORTLINKS_POSTGRES_CLUSTER,
602
- database: CANONICAL_SHORTLINKS_POSTGRES_DATABASE,
603
- runtimeSecretPath: CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH,
604
- primaryEnv: SHORTLINKS_RUNTIME_ENV.databaseUrl,
605
- fallbackEnv: SHORTLINKS_RUNTIME_FALLBACK_ENV.databaseUrl
606
- };
607
- }
608
- function parseShortlinksStoreMode(value) {
609
- const normalized = clean(value)?.toLowerCase();
610
- if (!normalized)
611
- return "local";
612
- if (normalized === "local" || normalized === "postgres")
613
- return normalized;
614
- if (normalized === "pg")
615
- return "postgres";
616
- throw new Error(`${SHORTLINKS_RUNTIME_ENV.store} must be local or postgres`);
617
- }
618
- function getShortlinksStoreMode(env = process.env) {
619
- return parseShortlinksStoreMode(readRuntimeEnv(env, "store").value);
620
- }
621
- function getShortlinksDatabaseUrl(env = process.env) {
622
- return readRuntimeEnv(env, "databaseUrl").value;
623
- }
624
- function getShortlinksDatabaseSsl(env = process.env) {
625
- return parseBoolean(readRuntimeEnv(env, "databaseSsl").value, true);
626
- }
627
- function getShortlinksRuntimeEnvName(env, key) {
628
- const primary = SHORTLINKS_RUNTIME_ENV[key];
629
- if (clean(env[primary]))
630
- return primary;
631
- return SHORTLINKS_RUNTIME_FALLBACK_ENV[key];
632
- }
633
- function loadShortlinksRuntimeConfig(env = process.env) {
634
- const mode = getShortlinksStoreMode(env);
635
- const databaseUrl = getShortlinksDatabaseUrl(env);
636
- return {
637
- service: "shortlinks",
638
- mode,
639
- ...databaseUrl ? {
640
- database: {
641
- provider: "postgres",
642
- url: databaseUrl,
643
- ssl: getShortlinksDatabaseSsl(env)
644
- }
645
- } : {}
646
- };
647
- }
648
- function assertShortlinksPostgresConfig(config) {
649
- if (config.mode !== "postgres")
650
- return;
651
- if (!config.database?.url) {
652
- throw new Error(`${SHORTLINKS_RUNTIME_ENV.databaseUrl} is required when ${SHORTLINKS_RUNTIME_ENV.store}=postgres`);
653
- }
654
- }
655
- function getShortlinksRuntimeStatus(env = process.env) {
656
- const issues = [];
657
- const warnings = [];
658
- let config;
659
- try {
660
- config = loadShortlinksRuntimeConfig(env);
661
- } catch (error) {
662
- issues.push(error instanceof Error ? error.message : String(error));
663
- config = { service: "shortlinks", mode: "local" };
664
- }
665
- try {
666
- assertShortlinksPostgresConfig(config);
667
- } catch (error) {
668
- issues.push(error instanceof Error ? error.message : String(error));
669
- }
670
- if (config.mode === "local" && config.database?.url) {
671
- warnings.push(`${SHORTLINKS_RUNTIME_ENV.store}=local ignores configured Postgres database settings`);
672
- }
673
- return {
674
- ok: issues.length === 0,
675
- service: "shortlinks",
676
- mode: config.mode,
677
- local_default: config.mode === "local",
678
- postgres_enabled: config.mode === "postgres",
679
- database: {
680
- configured: Boolean(config.database?.url),
681
- provider: config.database?.provider ?? null,
682
- redacted_url: redactDatabaseUrl(config.database?.url),
683
- ssl: config.database?.ssl ?? null
684
- },
685
- env: runtimeEnvStatus(env),
686
- canonical: getCanonicalShortlinksPostgresConfig(),
687
- issues,
688
- warnings,
689
- no_network: true
690
- };
691
- }
692
- function redactDatabaseUrl(value) {
693
- if (!value)
694
- return null;
695
- try {
696
- const url = new URL(value);
697
- if (url.username)
698
- url.username = "***";
699
- if (url.password)
700
- url.password = "***";
701
- for (const key of Array.from(url.searchParams.keys())) {
702
- if (isSensitiveQueryKey(key))
703
- url.searchParams.set(key, "***");
704
- }
705
- return url.toString();
706
- } catch {
707
- return "(redacted)";
708
- }
709
- }
710
- function runtimeEnvStatus(env) {
711
- return Object.fromEntries(Object.entries(SHORTLINKS_RUNTIME_ENV).map(([key, name]) => {
712
- const activeName = getShortlinksRuntimeEnvName(env, key);
713
- return [
714
- key,
715
- {
716
- name,
717
- active_name: activeName,
718
- configured: Boolean(clean(env[activeName]))
719
- }
720
- ];
721
- }));
722
- }
723
- function readRuntimeEnv(env, key) {
724
- const primary = SHORTLINKS_RUNTIME_ENV[key];
725
- const primaryValue = clean(env[primary]);
726
- if (primaryValue)
727
- return { name: primary, value: primaryValue };
728
- const fallback = SHORTLINKS_RUNTIME_FALLBACK_ENV[key];
729
- return { name: fallback, value: clean(env[fallback]) };
730
- }
731
- function parseBoolean(value, fallback) {
732
- const normalized = clean(value)?.toLowerCase();
733
- if (!normalized)
734
- return fallback;
735
- if (["1", "true", "yes", "on"].includes(normalized))
736
- return true;
737
- if (["0", "false", "no", "off"].includes(normalized))
738
- return false;
739
- throw new Error(`${SHORTLINKS_RUNTIME_ENV.databaseSsl} must be true or false`);
740
- }
741
- function isSensitiveQueryKey(key) {
742
- return /(?:^|[_-])(pass(?:word)?|pwd|secret|token|credential|auth|api[_-]?key|access[_-]?key)(?:$|[_-])/i.test(key);
743
- }
744
- function clean(value) {
745
- const trimmed = value?.trim();
746
- return trimmed ? trimmed : undefined;
747
- }
748
-
749
- // src/pg-store.ts
910
+ // src/store.ts
911
+ import { createHash as createHash2 } from "crypto";
750
912
  function parseJsonObject2(value) {
751
913
  if (!value)
752
914
  return {};
753
- if (typeof value === "object" && !Array.isArray(value))
754
- return value;
755
915
  try {
756
- const parsed = JSON.parse(String(value));
916
+ const parsed = JSON.parse(value);
757
917
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
758
918
  } catch {
759
919
  return {};
760
920
  }
761
921
  }
762
- async function loadPgPool() {
763
- const importer = new Function("specifier", "return import(specifier)");
764
- const module = await importer("pg");
765
- return module.Pool;
766
- }
767
- function toPostgresSql(sql) {
768
- let index = 0;
769
- return sql.replace(/\?/g, () => `$${++index}`);
770
- }
771
- function createPgPoolConfig(connectionString, options = {}) {
772
- const ssl = options.ssl ?? true;
773
- return {
774
- connectionString,
775
- ...ssl ? { ssl: { rejectUnauthorized: true } } : { ssl: false }
776
- };
777
- }
778
-
779
- class PgPoolAdapter {
780
- pool;
781
- constructor(pool) {
782
- this.pool = pool;
783
- }
784
- static async create(connectionString, options = {}) {
785
- const Pool = await loadPgPool();
786
- return new PgPoolAdapter(new Pool(createPgPoolConfig(connectionString, options)));
787
- }
788
- async get(sql, ...params) {
789
- const result = await this.pool.query(toPostgresSql(sql), params);
790
- return result.rows[0] ?? null;
791
- }
792
- async all(sql, ...params) {
793
- const result = await this.pool.query(toPostgresSql(sql), params);
794
- return result.rows;
795
- }
796
- async run(sql, ...params) {
797
- return this.pool.query(toPostgresSql(sql), params);
798
- }
799
- async close() {
800
- await this.pool.end();
801
- }
802
- }
803
- function toIsoString(value) {
804
- if (value instanceof Date)
805
- return value.toISOString();
806
- return String(value);
807
- }
808
- function nullableIso(value) {
809
- if (value === null || value === undefined)
810
- return null;
811
- return toIsoString(value);
812
- }
813
922
  function domainFromRow2(row) {
814
923
  return {
815
924
  ...row,
816
925
  default_domain: Boolean(row.default_domain),
817
- synced_at: nullableIso(row.synced_at),
818
- created_at: toIsoString(row.created_at),
819
- updated_at: toIsoString(row.updated_at),
820
926
  metadata: parseJsonObject2(row.metadata)
821
927
  };
822
928
  }
823
929
  function linkFromRow2(row) {
930
+ const config = loadConfig();
931
+ const publicBaseUrl = config.defaultDomain === row.hostname ? config.publicBaseUrl : undefined;
824
932
  return {
825
933
  ...row,
826
934
  active: Boolean(row.active),
827
- expires_at: nullableIso(row.expires_at),
828
- synced_at: nullableIso(row.synced_at),
829
- created_at: toIsoString(row.created_at),
830
- updated_at: toIsoString(row.updated_at),
831
935
  metadata: parseJsonObject2(row.metadata),
832
- short_url: formatShortUrl(row.hostname, row.slug)
936
+ short_url: formatShortUrl(row.hostname, row.slug, publicBaseUrl)
937
+ };
938
+ }
939
+ function clickFromRow2(row) {
940
+ return {
941
+ ...row,
942
+ metadata: parseJsonObject2(row.metadata)
833
943
  };
834
944
  }
835
945
  function validateDestinationUrl2(url) {
@@ -852,45 +962,26 @@ function isoOrNull2(input) {
852
962
  throw new Error(`Invalid date: ${input}`);
853
963
  return date.toISOString();
854
964
  }
855
- function clickFromRow2(row) {
856
- return {
857
- ...row,
858
- clicked_at: toIsoString(row.clicked_at),
859
- synced_at: nullableIso(row.synced_at),
860
- created_at: toIsoString(row.created_at),
861
- updated_at: toIsoString(row.updated_at),
862
- metadata: parseJsonObject2(row.metadata)
863
- };
864
- }
865
965
 
866
- class PgShortlinksStore {
867
- pg;
868
- constructor(pg) {
869
- this.pg = pg;
966
+ class ShortlinksStore {
967
+ database;
968
+ constructor(dbPath) {
969
+ this.database = new ShortlinksDatabase(dbPath);
870
970
  }
871
- static async fromConnectionString(connectionString, options = {}) {
872
- return new PgShortlinksStore(await PgPoolAdapter.create(connectionString, options));
971
+ close() {
972
+ this.database.close();
873
973
  }
874
- static async fromEnv(env = process.env) {
875
- const connectionString = getShortlinksDatabaseUrl(env);
876
- if (!connectionString) {
877
- throw new Error("HASNA_SHORTLINKS_DATABASE_URL is required when shortlinks uses the postgres store");
878
- }
879
- return PgShortlinksStore.fromConnectionString(connectionString, { ssl: getShortlinksDatabaseSsl(env) });
880
- }
881
- async close() {
882
- await this.pg.close?.();
883
- }
884
- async addDomain(input) {
974
+ addDomain(input) {
885
975
  const hostname2 = normalizeHostname(input.hostname);
886
976
  const timestamp = now();
887
977
  const machineId = getMachineId();
888
- const existing = await this.getDomain(hostname2);
978
+ const existing = this.getDomain(hostname2);
889
979
  const id = existing?.id || makeId("dom");
890
980
  if (input.defaultDomain) {
891
- await this.pg.run("UPDATE domains SET default_domain = 0, updated_at = ? WHERE default_domain = 1", timestamp);
981
+ this.database.db.query("UPDATE domains SET default_domain = 0, updated_at = ?, synced_at = NULL").run(timestamp);
982
+ updateConfig({ defaultDomain: hostname2, publicBaseUrl: `https://${hostname2}` });
892
983
  }
893
- await this.pg.run(`
984
+ this.database.db.query(`
894
985
  INSERT INTO domains (
895
986
  id, hostname, provider, default_domain, cloudflare_zone_id, cloudflare_account_id,
896
987
  cloudflare_worker_name, origin_url, notes, metadata, machine_id, synced_at, created_at, updated_at
@@ -908,31 +999,37 @@ class PgShortlinksStore {
908
999
  machine_id = excluded.machine_id,
909
1000
  synced_at = NULL,
910
1001
  updated_at = excluded.updated_at
911
- `, id, hostname2, input.provider || existing?.provider || "manual", input.defaultDomain ?? existing?.default_domain ? 1 : 0, input.cloudflareZoneId || existing?.cloudflare_zone_id || null, input.cloudflareAccountId || existing?.cloudflare_account_id || null, input.cloudflareWorkerName || existing?.cloudflare_worker_name || null, input.originUrl || existing?.origin_url || null, input.notes || existing?.notes || null, JSON.stringify(input.metadata || existing?.metadata || {}), machineId, existing?.created_at || timestamp, timestamp);
912
- return await this.getDomain(hostname2);
1002
+ `).run(id, hostname2, input.provider || existing?.provider || "manual", input.defaultDomain ?? existing?.default_domain ? 1 : 0, input.cloudflareZoneId || existing?.cloudflare_zone_id || null, input.cloudflareAccountId || existing?.cloudflare_account_id || null, input.cloudflareWorkerName || existing?.cloudflare_worker_name || null, input.originUrl || existing?.origin_url || null, input.notes || existing?.notes || null, JSON.stringify(input.metadata || existing?.metadata || {}), machineId, existing?.created_at || timestamp, timestamp);
1003
+ return this.getDomain(hostname2);
913
1004
  }
914
- async listDomains() {
915
- const rows = await this.pg.all(`
1005
+ listDomains() {
1006
+ const rows = this.database.db.query(`
916
1007
  SELECT * FROM domains
917
1008
  ORDER BY default_domain DESC, hostname ASC
918
- `);
1009
+ `).all();
919
1010
  return rows.map(domainFromRow2);
920
1011
  }
921
- async getDomain(hostnameOrId) {
1012
+ getDomain(hostnameOrId) {
922
1013
  const normalized = hostnameOrId.includes(".") || hostnameOrId.includes("://") ? normalizeHostname(hostnameOrId) : hostnameOrId;
923
- const row = await this.pg.get(`
1014
+ const row = this.database.db.query(`
924
1015
  SELECT * FROM domains WHERE hostname = ? OR id = ? LIMIT 1
925
- `, normalized, hostnameOrId);
1016
+ `).get(normalized, hostnameOrId);
926
1017
  return row ? domainFromRow2(row) : null;
927
1018
  }
928
- async getDefaultDomain() {
929
- const row = await this.pg.get(`
1019
+ getDefaultDomain() {
1020
+ const config = loadConfig();
1021
+ if (config.defaultDomain) {
1022
+ const configured = this.getDomain(config.defaultDomain);
1023
+ if (configured)
1024
+ return configured;
1025
+ }
1026
+ const row = this.database.db.query(`
930
1027
  SELECT * FROM domains ORDER BY default_domain DESC, created_at ASC LIMIT 1
931
- `);
1028
+ `).get();
932
1029
  return row ? domainFromRow2(row) : null;
933
1030
  }
934
- async createLink(input) {
935
- const domain = input.domain ? await this.getDomain(input.domain) : await this.getDefaultDomain();
1031
+ createLink(input) {
1032
+ const domain = input.domain ? this.getDomain(input.domain) : this.getDefaultDomain();
936
1033
  if (!domain) {
937
1034
  throw new Error("No domain configured. Run `shortlinks domain add <domain> --default` first.");
938
1035
  }
@@ -940,45 +1037,46 @@ class PgShortlinksStore {
940
1037
  const timestamp = now();
941
1038
  const machineId = getMachineId();
942
1039
  const expiresAt = isoOrNull2(input.expiresAt);
943
- const slug = input.slug ? normalizeSlug(input.slug) : await this.generateAvailableSlug(domain.id, input.slugLength || DEFAULT_SLUG_LENGTH);
1040
+ const slug = input.slug ? normalizeSlug(input.slug) : this.generateAvailableSlug(domain.id, input.slugLength || DEFAULT_SLUG_LENGTH);
944
1041
  try {
945
- await this.pg.run(`
1042
+ this.database.db.query(`
946
1043
  INSERT INTO links (
947
1044
  id, domain_id, slug, destination_url, title, active, expires_at, metadata,
948
1045
  machine_id, synced_at, created_at, updated_at
949
1046
  )
950
1047
  VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, NULL, ?, ?)
951
- `, makeId("lnk"), domain.id, slug, destinationUrl, input.title || null, expiresAt, JSON.stringify(input.metadata || {}), machineId, timestamp, timestamp);
1048
+ `).run(makeId("lnk"), domain.id, slug, destinationUrl, input.title || null, expiresAt, JSON.stringify(input.metadata || {}), machineId, timestamp, timestamp);
952
1049
  } catch (error) {
953
1050
  const message = error instanceof Error ? error.message : String(error);
954
- if (message.includes("unique") || message.includes("duplicate")) {
1051
+ if (message.includes("UNIQUE")) {
955
1052
  throw new Error(`Slug already exists for ${domain.hostname}: ${slug}`);
956
1053
  }
957
1054
  throw error;
958
1055
  }
959
- return await this.getLink(domain.hostname, slug);
1056
+ return this.getLink(domain.hostname, slug);
960
1057
  }
961
- async listLinks(options = {}) {
1058
+ listLinks(options = {}) {
962
1059
  const params = [];
963
1060
  let where = "WHERE 1 = 1";
964
1061
  if (options.domain) {
965
1062
  where += " AND d.hostname = ?";
966
1063
  params.push(normalizeHostname(options.domain));
967
1064
  }
968
- if (options.activeOnly)
1065
+ if (options.activeOnly) {
969
1066
  where += " AND l.active = 1";
1067
+ }
970
1068
  params.push(options.limit || 100);
971
- const rows = await this.pg.all(`
1069
+ const rows = this.database.db.query(`
972
1070
  SELECT l.*, d.hostname
973
1071
  FROM links l
974
1072
  JOIN domains d ON d.id = l.domain_id
975
1073
  ${where}
976
1074
  ORDER BY l.created_at DESC
977
1075
  LIMIT ?
978
- `, ...params);
1076
+ `).all(...params);
979
1077
  return rows.map(linkFromRow2);
980
1078
  }
981
- async getLink(domainOrSlug, maybeSlug) {
1079
+ getLink(domainOrSlug, maybeSlug) {
982
1080
  const slug = normalizeSlug(maybeSlug || domainOrSlug);
983
1081
  const params = [slug];
984
1082
  let domainClause = "";
@@ -986,172 +1084,122 @@ class PgShortlinksStore {
986
1084
  domainClause = "AND d.hostname = ?";
987
1085
  params.push(normalizeHostname(domainOrSlug));
988
1086
  }
989
- const row = await this.pg.get(`
1087
+ const row = this.database.db.query(`
990
1088
  SELECT l.*, d.hostname
991
1089
  FROM links l
992
1090
  JOIN domains d ON d.id = l.domain_id
993
1091
  WHERE l.slug = ? ${domainClause}
994
1092
  ORDER BY d.default_domain DESC, l.created_at ASC
995
1093
  LIMIT 1
996
- `, ...params);
1094
+ `).get(...params);
997
1095
  return row ? linkFromRow2(row) : null;
998
1096
  }
999
- async totalStats() {
1000
- const row = await this.pg.get(`
1001
- SELECT
1002
- (SELECT COUNT(*)::int FROM domains) AS domains,
1003
- (SELECT COUNT(*)::int FROM links) AS links,
1004
- (SELECT COUNT(*)::int FROM clicks) AS clicks
1005
- `);
1006
- return row;
1007
- }
1008
- async resolve(hostname2, slug) {
1009
- const normalizedHost = normalizeHostname(hostname2);
1097
+ resolve(hostname2, slug) {
1010
1098
  const normalizedSlug = normalizeSlug(slug);
1011
- const row = await this.pg.get(`
1099
+ const normalizedHost = normalizeHostname(hostname2);
1100
+ const row = this.database.db.query(`
1012
1101
  SELECT l.*, d.hostname
1013
1102
  FROM links l
1014
1103
  JOIN domains d ON d.id = l.domain_id
1015
1104
  WHERE d.hostname = ? AND l.slug = ?
1016
1105
  LIMIT 1
1017
- `, normalizedHost, normalizedSlug);
1106
+ `).get(normalizedHost, normalizedSlug);
1018
1107
  if (row)
1019
1108
  return linkFromRow2(row);
1020
- const fallback = await this.pg.get(`
1021
- SELECT l.*, d.hostname
1022
- FROM links l
1023
- JOIN domains d ON d.id = l.domain_id
1024
- WHERE d.default_domain = 1 AND l.slug = ?
1025
- ORDER BY d.created_at ASC
1026
- LIMIT 1
1027
- `, normalizedSlug);
1028
- return fallback ? linkFromRow2(fallback) : null;
1109
+ const fallback = this.getDefaultDomain();
1110
+ if (!fallback || fallback.hostname === normalizedHost)
1111
+ return null;
1112
+ return this.getLink(fallback.hostname, normalizedSlug);
1029
1113
  }
1030
- async setLinkActive(domainOrSlug, maybeSlugOrActive, maybeActive) {
1114
+ setLinkActive(domainOrSlug, maybeSlugOrActive, maybeActive) {
1031
1115
  const active = typeof maybeSlugOrActive === "boolean" ? maybeSlugOrActive : Boolean(maybeActive);
1032
- const link = typeof maybeSlugOrActive === "boolean" ? await this.getLink(domainOrSlug) : await this.getLink(domainOrSlug, maybeSlugOrActive);
1116
+ const link = typeof maybeSlugOrActive === "boolean" ? this.getLink(domainOrSlug) : this.getLink(domainOrSlug, maybeSlugOrActive);
1033
1117
  if (!link)
1034
1118
  throw new Error("Link not found.");
1035
1119
  const timestamp = now();
1036
- await this.pg.run(`
1120
+ this.database.db.query(`
1037
1121
  UPDATE links SET active = ?, updated_at = ?, synced_at = NULL WHERE id = ?
1038
- `, active ? 1 : 0, timestamp, link.id);
1039
- return await this.getLink(link.hostname, link.slug);
1122
+ `).run(active ? 1 : 0, timestamp, link.id);
1123
+ return this.getLink(link.hostname, link.slug);
1040
1124
  }
1041
- async deleteLink(domainOrSlug, maybeSlug) {
1042
- const link = maybeSlug ? await this.getLink(domainOrSlug, maybeSlug) : await this.getLink(domainOrSlug);
1125
+ deleteLink(domainOrSlug, maybeSlug) {
1126
+ const link = maybeSlug ? this.getLink(domainOrSlug, maybeSlug) : this.getLink(domainOrSlug);
1043
1127
  if (!link)
1044
1128
  throw new Error("Link not found.");
1045
- await this.pg.run("DELETE FROM links WHERE id = ?", link.id);
1129
+ this.database.db.query("DELETE FROM links WHERE id = ?").run(link.id);
1046
1130
  return link;
1047
1131
  }
1048
- async recordClick(link, input = {}) {
1132
+ recordClick(link, input = {}) {
1049
1133
  const timestamp = now();
1050
1134
  const machineId = getMachineId();
1051
1135
  const ipHash = input.ip ? this.hashIp(input.ip) : null;
1052
1136
  const id = makeId("clk");
1053
- await this.pg.run(`
1137
+ this.database.db.query(`
1054
1138
  INSERT INTO clicks (
1055
1139
  id, link_id, domain_id, slug, clicked_at, ip_hash, user_agent, referer,
1056
1140
  country, city, metadata, machine_id, synced_at, created_at, updated_at
1057
1141
  )
1058
1142
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
1059
- `, id, link.id, link.domain_id, link.slug, timestamp, ipHash, input.userAgent || null, input.referer || null, input.country || null, input.city || null, JSON.stringify(input.metadata || {}), machineId, timestamp, timestamp);
1060
- const row = await this.pg.get("SELECT * FROM clicks WHERE id = ?", id);
1143
+ `).run(id, link.id, link.domain_id, link.slug, timestamp, ipHash, input.userAgent || null, input.referer || null, input.country || null, input.city || null, JSON.stringify(input.metadata || {}), machineId, timestamp, timestamp);
1144
+ const row = this.database.db.query("SELECT * FROM clicks WHERE id = ?").get(id);
1061
1145
  return clickFromRow2(row);
1062
1146
  }
1063
- async getStats(domainOrSlug, maybeSlug) {
1064
- const link = maybeSlug ? await this.getLink(domainOrSlug, maybeSlug) : await this.getLink(domainOrSlug);
1147
+ getStats(domainOrSlug, maybeSlug) {
1148
+ const link = maybeSlug ? this.getLink(domainOrSlug, maybeSlug) : this.getLink(domainOrSlug);
1065
1149
  if (!link)
1066
1150
  throw new Error("Link not found.");
1067
- const summary = await this.pg.get(`
1068
- SELECT COUNT(*)::int AS clicks, MAX(clicked_at) AS last_clicked_at FROM clicks WHERE link_id = ?
1069
- `, link.id);
1070
- const topReferrers = await this.pg.all(`
1071
- SELECT referer, COUNT(*)::int AS clicks
1151
+ const summary = this.database.db.query(`
1152
+ SELECT COUNT(*) AS clicks, MAX(clicked_at) AS last_clicked_at FROM clicks WHERE link_id = ?
1153
+ `).get(link.id);
1154
+ const topReferrers = this.database.db.query(`
1155
+ SELECT referer, COUNT(*) AS clicks
1072
1156
  FROM clicks
1073
1157
  WHERE link_id = ?
1074
1158
  GROUP BY referer
1075
1159
  ORDER BY clicks DESC
1076
1160
  LIMIT 10
1077
- `, link.id);
1078
- const topUserAgents = await this.pg.all(`
1079
- SELECT user_agent, COUNT(*)::int AS clicks
1161
+ `).all(link.id);
1162
+ const topUserAgents = this.database.db.query(`
1163
+ SELECT user_agent, COUNT(*) AS clicks
1080
1164
  FROM clicks
1081
1165
  WHERE link_id = ?
1082
1166
  GROUP BY user_agent
1083
1167
  ORDER BY clicks DESC
1084
1168
  LIMIT 10
1085
- `, link.id);
1169
+ `).all(link.id);
1086
1170
  return {
1087
1171
  link,
1088
1172
  clicks: summary.clicks,
1089
- last_clicked_at: nullableIso(summary.last_clicked_at),
1173
+ last_clicked_at: summary.last_clicked_at,
1090
1174
  top_referrers: topReferrers,
1091
1175
  top_user_agents: topUserAgents
1092
1176
  };
1093
1177
  }
1094
- hashIp(ip) {
1095
- return createHash2("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
1178
+ totalStats() {
1179
+ const row = this.database.db.query(`
1180
+ SELECT
1181
+ (SELECT COUNT(*) FROM domains) AS domains,
1182
+ (SELECT COUNT(*) FROM links) AS links,
1183
+ (SELECT COUNT(*) FROM clicks) AS clicks
1184
+ `).get();
1185
+ return row;
1096
1186
  }
1097
- async generateAvailableSlug(domainId, length) {
1187
+ generateAvailableSlug(domainId, length) {
1098
1188
  for (let attempt = 0;attempt < 32; attempt += 1) {
1099
1189
  const slug = randomToken(length);
1100
- const exists = await this.pg.get(`
1190
+ const exists = this.database.db.query(`
1101
1191
  SELECT 1 FROM links WHERE domain_id = ? AND slug = ? LIMIT 1
1102
- `, domainId, slug);
1192
+ `).get(domainId, slug);
1103
1193
  if (!exists)
1104
1194
  return slug;
1105
1195
  }
1106
1196
  throw new Error("Could not generate an unused slug after 32 attempts.");
1107
1197
  }
1108
- }
1109
- async function applyPostgresMigrations(connectionString, migrations, options = {}) {
1110
- const Pool = await loadPgPool();
1111
- const pool = new Pool(createPgPoolConfig(connectionString, options));
1112
- const client = await pool.connect();
1113
- const run = (sql, ...params) => client.query(toPostgresSql(sql), params);
1114
- const get = async (sql, ...params) => {
1115
- const result = await run(sql, ...params);
1116
- return result.rows[0] ?? null;
1117
- };
1118
- const applied = [];
1119
- const skipped = [];
1120
- try {
1121
- await run("BEGIN");
1122
- await run(`
1123
- SELECT pg_advisory_xact_lock(hashtext(?))
1124
- `, "shortlinks:migrations");
1125
- await run(`
1126
- CREATE TABLE IF NOT EXISTS _shortlinks_migrations (
1127
- id INTEGER PRIMARY KEY,
1128
- service TEXT NOT NULL DEFAULT 'shortlinks',
1129
- applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
1130
- )
1131
- `);
1132
- for (let i = 0;i < migrations.length; i += 1) {
1133
- const id = i + 1;
1134
- const existing = await get("SELECT id FROM _shortlinks_migrations WHERE id = ? LIMIT 1", id);
1135
- if (existing) {
1136
- skipped.push(id);
1137
- continue;
1138
- }
1139
- await run(migrations[i]);
1140
- await run("INSERT INTO _shortlinks_migrations (id, service, applied_at) VALUES (?, ?, now())", id, "shortlinks");
1141
- applied.push(id);
1142
- }
1143
- await run("COMMIT");
1144
- return { service: "shortlinks", applied, skipped };
1145
- } catch (error) {
1146
- try {
1147
- await run("ROLLBACK");
1148
- } catch {}
1149
- throw error;
1150
- } finally {
1151
- client.release();
1152
- await pool.end();
1198
+ hashIp(ip) {
1199
+ return createHash2("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
1153
1200
  }
1154
1201
  }
1202
+
1155
1203
  // src/server.ts
1156
1204
  var REDIRECT_ALLOW_HEADER = "GET, HEAD";
1157
1205
  function json(data, status = 200, headers) {
@@ -1249,6 +1297,7 @@ function serveShortlinks(options = {}) {
1249
1297
  const fetch2 = createShortlinksHandler(options);
1250
1298
  return Bun.serve({ hostname: host, port, fetch: fetch2 });
1251
1299
  }
1300
+
1252
1301
  // src/cloudflare.ts
1253
1302
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
1254
1303
  import { join as join3 } from "path";
@@ -1384,6 +1433,2591 @@ async function upsertCloudflareDnsRecord(options) {
1384
1433
  });
1385
1434
  return { id: created.id, action: "created" };
1386
1435
  }
1436
+ // node_modules/.pnpm/@hasna+contracts@0.4.1/node_modules/@hasna/contracts/dist/auth/index.js
1437
+ import { createHash as createHash3, createHmac, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
1438
+ var API_KEY_TOKEN_VERSION = 1;
1439
+ var API_KEY_NAMESPACE = "hasna";
1440
+ var TOKEN_PATTERN = /^hasna_([a-z][a-z0-9-]*)_([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)$/;
1441
+ var DEFAULT_API_KEY_TTL_SECONDS = 90 * 24 * 60 * 60;
1442
+ function toBuffer(secret) {
1443
+ return typeof secret === "string" ? Buffer.from(secret, "utf8") : secret;
1444
+ }
1445
+ function hmac(signingSecret, message) {
1446
+ return createHmac("sha256", toBuffer(signingSecret)).update(message, "utf8").digest();
1447
+ }
1448
+ function apiKeyPrefix(app) {
1449
+ return `${API_KEY_NAMESPACE}_${app}_`;
1450
+ }
1451
+ function parseApiKey(token) {
1452
+ if (typeof token !== "string")
1453
+ return null;
1454
+ const match = TOKEN_PATTERN.exec(token);
1455
+ if (!match)
1456
+ return null;
1457
+ const [, app, body, sig] = match;
1458
+ if (!app || !body || !sig)
1459
+ return null;
1460
+ let claims;
1461
+ try {
1462
+ claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
1463
+ } catch {
1464
+ return null;
1465
+ }
1466
+ if (typeof claims !== "object" || claims === null || typeof claims.kid !== "string" || typeof claims.app !== "string" || !Array.isArray(claims.scopes)) {
1467
+ return null;
1468
+ }
1469
+ return { app, body, sig, claims };
1470
+ }
1471
+ function verifyApiKeyToken(token, options) {
1472
+ const parsed = parseApiKey(token);
1473
+ if (!parsed) {
1474
+ return { ok: false, reason: "malformed", message: "Token is malformed." };
1475
+ }
1476
+ const { app, body, sig, claims } = parsed;
1477
+ if (claims.v !== API_KEY_TOKEN_VERSION) {
1478
+ return { ok: false, reason: "unsupported_version", message: `Unsupported token version ${claims.v}.` };
1479
+ }
1480
+ if (claims.app !== app) {
1481
+ return { ok: false, reason: "app_mismatch", message: "Token prefix app does not match claims." };
1482
+ }
1483
+ if (options.expectedApp !== undefined && app !== options.expectedApp) {
1484
+ return { ok: false, reason: "app_mismatch", message: `Token is for app '${app}', expected '${options.expectedApp}'.` };
1485
+ }
1486
+ const expected = hmac(options.signingSecret, `${apiKeyPrefix(app)}${body}`);
1487
+ let provided;
1488
+ try {
1489
+ provided = Buffer.from(sig, "base64url");
1490
+ } catch {
1491
+ return { ok: false, reason: "bad_signature", message: "Signature is not valid base64url." };
1492
+ }
1493
+ if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) {
1494
+ return { ok: false, reason: "bad_signature", message: "Signature verification failed." };
1495
+ }
1496
+ const now2 = Math.floor((options.nowMs ?? Date.now()) / 1000);
1497
+ const leeway = options.leewaySeconds ?? 0;
1498
+ if (typeof claims.iat === "number" && now2 + leeway < claims.iat) {
1499
+ return { ok: false, reason: "not_yet_valid", message: "Token is not yet valid." };
1500
+ }
1501
+ if (claims.exp !== null && typeof claims.exp === "number" && now2 - leeway >= claims.exp) {
1502
+ return { ok: false, reason: "expired", message: "Token has expired." };
1503
+ }
1504
+ if (options.requiredScopes && options.requiredScopes.length > 0) {
1505
+ const granted = claims.scopes;
1506
+ const satisfies = (required) => granted.some((g) => {
1507
+ if (g === "*")
1508
+ return true;
1509
+ const gi = g.indexOf(":");
1510
+ const ri = required.indexOf(":");
1511
+ if (gi < 0 || ri < 0)
1512
+ return false;
1513
+ const gApp = g.slice(0, gi);
1514
+ const gAction = g.slice(gi + 1);
1515
+ const rApp = required.slice(0, ri);
1516
+ const rAction = required.slice(ri + 1);
1517
+ return (gApp === "*" || gApp === rApp) && (gAction === "*" || gAction === rAction);
1518
+ });
1519
+ for (const required of options.requiredScopes) {
1520
+ if (!satisfies(required)) {
1521
+ return { ok: false, reason: "insufficient_scope", message: `Missing required scope '${required}'.` };
1522
+ }
1523
+ }
1524
+ }
1525
+ return { ok: true, claims, kid: claims.kid, app };
1526
+ }
1527
+ var DEFAULT_API_KEYS_TABLE = "api_keys";
1528
+ function createTableSql(table) {
1529
+ return `CREATE TABLE IF NOT EXISTS ${table} (
1530
+ kid TEXT PRIMARY KEY,
1531
+ app TEXT NOT NULL,
1532
+ agent TEXT,
1533
+ scopes JSONB NOT NULL,
1534
+ token_hash TEXT NOT NULL UNIQUE,
1535
+ issued_at TIMESTAMPTZ NOT NULL,
1536
+ expires_at TIMESTAMPTZ,
1537
+ revoked_at TIMESTAMPTZ,
1538
+ revoked_reason TEXT,
1539
+ last_used_at TIMESTAMPTZ,
1540
+ created_by TEXT,
1541
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
1542
+ )`;
1543
+ }
1544
+ function apiKeyMigrations(table = DEFAULT_API_KEYS_TABLE) {
1545
+ return [
1546
+ { id: `hasna_auth_0001_${table}`, sql: createTableSql(table) },
1547
+ {
1548
+ id: `hasna_auth_0002_${table}_indexes`,
1549
+ sql: `CREATE INDEX IF NOT EXISTS ${table}_app_idx ON ${table} (app);
1550
+ CREATE INDEX IF NOT EXISTS ${table}_token_hash_idx ON ${table} (token_hash);`
1551
+ }
1552
+ ];
1553
+ }
1554
+ function toIso(value) {
1555
+ if (value === null || value === undefined)
1556
+ return null;
1557
+ if (value instanceof Date)
1558
+ return value.toISOString();
1559
+ return new Date(String(value)).toISOString();
1560
+ }
1561
+ function parseScopes(value) {
1562
+ if (Array.isArray(value))
1563
+ return value.map((v) => String(v));
1564
+ if (typeof value === "string") {
1565
+ try {
1566
+ const parsed = JSON.parse(value);
1567
+ return Array.isArray(parsed) ? parsed.map((v) => String(v)) : [];
1568
+ } catch {
1569
+ return [];
1570
+ }
1571
+ }
1572
+ return [];
1573
+ }
1574
+ function rowToRecord(row) {
1575
+ return {
1576
+ kid: String(row.kid),
1577
+ app: String(row.app),
1578
+ agent: row.agent === null || row.agent === undefined ? null : String(row.agent),
1579
+ scopes: parseScopes(row.scopes),
1580
+ tokenHash: String(row.token_hash),
1581
+ issuedAt: toIso(row.issued_at) ?? new Date(0).toISOString(),
1582
+ expiresAt: toIso(row.expires_at),
1583
+ revokedAt: toIso(row.revoked_at),
1584
+ revokedReason: row.revoked_reason === null || row.revoked_reason === undefined ? null : String(row.revoked_reason),
1585
+ lastUsedAt: toIso(row.last_used_at),
1586
+ createdBy: row.created_by === null || row.created_by === undefined ? null : String(row.created_by)
1587
+ };
1588
+ }
1589
+
1590
+ class ApiKeyStore {
1591
+ client;
1592
+ table;
1593
+ constructor(client, options = {}) {
1594
+ this.client = client;
1595
+ this.table = options.table ?? DEFAULT_API_KEYS_TABLE;
1596
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(this.table)) {
1597
+ throw new Error(`Invalid api-keys table name '${this.table}'.`);
1598
+ }
1599
+ }
1600
+ migrations() {
1601
+ return apiKeyMigrations(this.table);
1602
+ }
1603
+ async ensureSchema() {
1604
+ for (const migration of this.migrations()) {
1605
+ await this.client.execute(migration.sql);
1606
+ }
1607
+ }
1608
+ async insert(input) {
1609
+ await this.client.execute(`INSERT INTO ${this.table}
1610
+ (kid, app, agent, scopes, token_hash, issued_at, expires_at, created_by)
1611
+ VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8)`, [
1612
+ input.kid,
1613
+ input.app,
1614
+ input.agent ?? null,
1615
+ JSON.stringify(input.scopes),
1616
+ input.tokenHash,
1617
+ input.issuedAt.toISOString(),
1618
+ input.expiresAt ? input.expiresAt.toISOString() : null,
1619
+ input.createdBy ?? null
1620
+ ]);
1621
+ }
1622
+ async insertMinted(minted, createdBy) {
1623
+ const claims = minted.claims;
1624
+ await this.insert({
1625
+ kid: minted.kid,
1626
+ app: claims.app,
1627
+ agent: claims.agent ?? null,
1628
+ scopes: claims.scopes,
1629
+ tokenHash: minted.tokenHash,
1630
+ issuedAt: new Date(claims.iat * 1000),
1631
+ expiresAt: claims.exp === null ? null : new Date(claims.exp * 1000),
1632
+ createdBy: createdBy ?? null
1633
+ });
1634
+ }
1635
+ async findByKid(kid) {
1636
+ const row = await this.client.get(`SELECT * FROM ${this.table} WHERE kid = $1`, [kid]);
1637
+ return row ? rowToRecord(row) : null;
1638
+ }
1639
+ async findByTokenHash(tokenHash) {
1640
+ const row = await this.client.get(`SELECT * FROM ${this.table} WHERE token_hash = $1`, [tokenHash]);
1641
+ return row ? rowToRecord(row) : null;
1642
+ }
1643
+ isRevoked = async (kid) => {
1644
+ const row = await this.client.get(`SELECT revoked_at FROM ${this.table} WHERE kid = $1`, [kid]);
1645
+ if (!row)
1646
+ return false;
1647
+ return row.revoked_at !== null && row.revoked_at !== undefined;
1648
+ };
1649
+ async status(kid, nowMs = Date.now()) {
1650
+ const record = await this.findByKid(kid);
1651
+ if (!record)
1652
+ return "unknown";
1653
+ if (record.revokedAt)
1654
+ return "revoked";
1655
+ if (record.expiresAt && new Date(record.expiresAt).getTime() <= nowMs)
1656
+ return "expired";
1657
+ return "active";
1658
+ }
1659
+ statusChecker() {
1660
+ return async (kid) => {
1661
+ const status = await this.status(kid);
1662
+ return status !== "active";
1663
+ };
1664
+ }
1665
+ async revoke(kid, reason, atMs = Date.now()) {
1666
+ const row = await this.client.get(`UPDATE ${this.table}
1667
+ SET revoked_at = COALESCE(revoked_at, $2), revoked_reason = COALESCE(revoked_reason, $3)
1668
+ WHERE kid = $1
1669
+ RETURNING kid`, [kid, new Date(atMs).toISOString(), reason ?? null]);
1670
+ return row !== null;
1671
+ }
1672
+ async touchLastUsed(kid, atMs = Date.now()) {
1673
+ await this.client.execute(`UPDATE ${this.table} SET last_used_at = $2 WHERE kid = $1`, [
1674
+ kid,
1675
+ new Date(atMs).toISOString()
1676
+ ]);
1677
+ }
1678
+ async list(options = {}) {
1679
+ const clauses = [];
1680
+ const params = [];
1681
+ if (options.app) {
1682
+ params.push(options.app);
1683
+ clauses.push(`app = $${params.length}`);
1684
+ }
1685
+ if (!options.includeRevoked) {
1686
+ clauses.push("revoked_at IS NULL");
1687
+ }
1688
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
1689
+ const rows = await this.client.many(`SELECT * FROM ${this.table} ${where} ORDER BY issued_at DESC`);
1690
+ return rows.map(rowToRecord);
1691
+ }
1692
+ async revokedKids() {
1693
+ const rows = await this.client.many(`SELECT kid FROM ${this.table} WHERE revoked_at IS NOT NULL`);
1694
+ return rows.map((row) => String(row.kid));
1695
+ }
1696
+ }
1697
+ function readHeader(source, name) {
1698
+ const lower = name.toLowerCase();
1699
+ if (typeof source === "function") {
1700
+ return source(name) ?? source(lower) ?? null;
1701
+ }
1702
+ if (typeof Headers !== "undefined" && source instanceof Headers) {
1703
+ return source.get(name);
1704
+ }
1705
+ const record = source;
1706
+ const value = record[name] ?? record[lower] ?? record[name.toUpperCase()];
1707
+ if (Array.isArray(value))
1708
+ return value[0] ?? null;
1709
+ return value ?? null;
1710
+ }
1711
+ function extractToken(source, headerName = "x-api-key", scheme = "Bearer") {
1712
+ const direct = readHeader(source, headerName);
1713
+ if (direct && direct.trim().length > 0)
1714
+ return direct.trim();
1715
+ const authz = readHeader(source, "authorization");
1716
+ if (authz) {
1717
+ const prefix = `${scheme} `;
1718
+ if (authz.toLowerCase().startsWith(prefix.toLowerCase())) {
1719
+ const token = authz.slice(prefix.length).trim();
1720
+ if (token.length > 0)
1721
+ return token;
1722
+ }
1723
+ }
1724
+ return null;
1725
+ }
1726
+ function verifyApiKey(options) {
1727
+ if (!options.app)
1728
+ throw new Error("verifyApiKey requires an 'app' slug.");
1729
+ if (!options.signingSecret) {
1730
+ throw new Error("verifyApiKey requires a 'signingSecret'. Set it from HASNA_<APP>_API_SIGNING_KEY.");
1731
+ }
1732
+ const headerName = options.headerName ?? "x-api-key";
1733
+ const scheme = options.scheme ?? "Bearer";
1734
+ const clock = options.nowMs ?? (() => Date.now());
1735
+ async function emit(event) {
1736
+ if (!options.audit)
1737
+ return;
1738
+ try {
1739
+ await options.audit(event);
1740
+ } catch {}
1741
+ }
1742
+ async function authenticate(headers, context = {}) {
1743
+ const method = context.method ?? null;
1744
+ const path = context.path ?? null;
1745
+ const requiredScopes = [...options.requiredScopes ?? [], ...context.requiredScopes ?? []];
1746
+ const at = new Date(clock()).toISOString();
1747
+ const token = extractToken(headers, headerName, scheme);
1748
+ if (!token) {
1749
+ const decision = {
1750
+ ok: false,
1751
+ status: 401,
1752
+ reason: "missing_token",
1753
+ message: `Missing API key. Send it as '${headerName}: <key>' or 'Authorization: ${scheme} <key>'.`
1754
+ };
1755
+ await emit({ outcome: "deny", app: options.app, kid: null, reason: "missing_token", scopesRequired: requiredScopes, method, path, status: 401, at });
1756
+ return decision;
1757
+ }
1758
+ const verified = verifyApiKeyToken(token, {
1759
+ signingSecret: options.signingSecret,
1760
+ expectedApp: options.app,
1761
+ nowMs: clock(),
1762
+ ...options.leewaySeconds !== undefined ? { leewaySeconds: options.leewaySeconds } : {},
1763
+ requiredScopes
1764
+ });
1765
+ if (!verified.ok) {
1766
+ const status = verified.reason === "insufficient_scope" ? 403 : 401;
1767
+ await emit({ outcome: "deny", app: options.app, kid: null, reason: verified.reason, scopesRequired: requiredScopes, method, path, status, at });
1768
+ return { ok: false, status, reason: verified.reason, message: verified.message };
1769
+ }
1770
+ if (options.isRevoked) {
1771
+ const revoked = await options.isRevoked(verified.kid);
1772
+ if (revoked) {
1773
+ await emit({ outcome: "deny", app: options.app, kid: verified.kid, reason: "revoked", scopesRequired: requiredScopes, method, path, status: 401, at });
1774
+ return { ok: false, status: 401, reason: "revoked", message: "API key has been revoked." };
1775
+ }
1776
+ }
1777
+ const principal = {
1778
+ kid: verified.kid,
1779
+ app: verified.app,
1780
+ scopes: verified.claims.scopes,
1781
+ agent: verified.claims.agent ?? null,
1782
+ claims: verified.claims
1783
+ };
1784
+ await emit({ outcome: "allow", app: options.app, kid: verified.kid, reason: null, scopesRequired: requiredScopes, method, path, status: 200, at });
1785
+ return { ok: true, status: 200, principal };
1786
+ }
1787
+ return { authenticate, app: options.app };
1788
+ }
1789
+
1790
+ // src/generated/storage-kit/migrations.ts
1791
+ import { createHash as createHash4 } from "crypto";
1792
+ var DEFAULT_MIGRATION_LEDGER_TABLE = "schema_migrations";
1793
+ function checksumSql(sql) {
1794
+ const normalized = sql.trim().replace(/\r\n/g, `
1795
+ `);
1796
+ return `sha256:${createHash4("sha256").update(normalized).digest("hex")}`;
1797
+ }
1798
+ function defineMigration(id, sql) {
1799
+ return Object.freeze({ id, sql: sql.trim(), checksum: checksumSql(sql) });
1800
+ }
1801
+
1802
+ class MigrationLedger {
1803
+ client;
1804
+ migrations;
1805
+ ledgerTable;
1806
+ constructor(client, migrations, options = {}) {
1807
+ this.client = client;
1808
+ this.migrations = migrations;
1809
+ this.ledgerTable = options.ledgerTable ?? DEFAULT_MIGRATION_LEDGER_TABLE;
1810
+ const seen = new Set;
1811
+ for (const migration of migrations) {
1812
+ if (seen.has(migration.id))
1813
+ throw new Error(`Duplicate migration id: ${migration.id}`);
1814
+ seen.add(migration.id);
1815
+ }
1816
+ }
1817
+ async ensureLedger() {
1818
+ await this.client.execute(`CREATE TABLE IF NOT EXISTS ${this.ledgerTable} (
1819
+ id TEXT PRIMARY KEY,
1820
+ checksum TEXT NOT NULL,
1821
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
1822
+ )`);
1823
+ }
1824
+ async listApplied() {
1825
+ await this.ensureLedger();
1826
+ return this.readApplied();
1827
+ }
1828
+ async readApplied() {
1829
+ const rows = await this.client.many(`SELECT id, checksum, applied_at FROM ${this.ledgerTable} ORDER BY id ASC`);
1830
+ return rows.map((row) => ({
1831
+ id: row.id,
1832
+ checksum: row.checksum,
1833
+ appliedAt: row.applied_at instanceof Date ? row.applied_at.toISOString() : String(row.applied_at)
1834
+ }));
1835
+ }
1836
+ buildPlan(applied) {
1837
+ const known = new Set(this.migrations.map((m) => m.id));
1838
+ for (const row of applied) {
1839
+ if (!known.has(row.id)) {
1840
+ throw new Error(`Applied migration '${row.id}' is not recognized by this build (downgrade?).`);
1841
+ }
1842
+ }
1843
+ const appliedById = new Map(applied.map((row) => [row.id, row]));
1844
+ for (const migration of this.migrations) {
1845
+ const existing = appliedById.get(migration.id);
1846
+ if (existing && existing.checksum !== migration.checksum) {
1847
+ throw new Error(`Migration checksum mismatch for '${migration.id}': the SQL changed after it was applied.`);
1848
+ }
1849
+ }
1850
+ return this.migrations.map((migration) => ({
1851
+ migration,
1852
+ state: appliedById.has(migration.id) ? "already_applied" : "pending"
1853
+ }));
1854
+ }
1855
+ async migrate(opts = {}) {
1856
+ const dryRun = opts.dryRun === true;
1857
+ await this.ensureLedger();
1858
+ const applied = await this.readApplied();
1859
+ const plan = this.buildPlan(applied);
1860
+ if (dryRun)
1861
+ return { dryRun, applied, plan };
1862
+ for (const item of plan) {
1863
+ if (item.state === "already_applied")
1864
+ continue;
1865
+ await this.client.execute(item.migration.sql);
1866
+ await this.client.execute(`INSERT INTO ${this.ledgerTable} (id, checksum, applied_at) VALUES ($1, $2, now())`, [item.migration.id, item.migration.checksum]);
1867
+ }
1868
+ return { dryRun, applied: await this.readApplied(), plan };
1869
+ }
1870
+ }
1871
+
1872
+ // src/db/migrations.ts
1873
+ var CORE_MIGRATIONS = [
1874
+ defineMigration("shortlinks_0001_domains", `CREATE TABLE IF NOT EXISTS domains (
1875
+ id TEXT PRIMARY KEY,
1876
+ hostname TEXT NOT NULL UNIQUE,
1877
+ provider TEXT NOT NULL DEFAULT 'manual',
1878
+ default_domain INTEGER NOT NULL DEFAULT 0,
1879
+ cloudflare_zone_id TEXT,
1880
+ cloudflare_account_id TEXT,
1881
+ cloudflare_worker_name TEXT,
1882
+ origin_url TEXT,
1883
+ notes TEXT,
1884
+ metadata TEXT NOT NULL DEFAULT '{}',
1885
+ machine_id TEXT,
1886
+ synced_at TIMESTAMPTZ,
1887
+ created_at TIMESTAMPTZ NOT NULL,
1888
+ updated_at TIMESTAMPTZ NOT NULL
1889
+ )`),
1890
+ defineMigration("shortlinks_0002_links", `CREATE TABLE IF NOT EXISTS links (
1891
+ id TEXT PRIMARY KEY,
1892
+ domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
1893
+ slug TEXT NOT NULL,
1894
+ destination_url TEXT NOT NULL,
1895
+ title TEXT,
1896
+ active INTEGER NOT NULL DEFAULT 1,
1897
+ expires_at TIMESTAMPTZ,
1898
+ metadata TEXT NOT NULL DEFAULT '{}',
1899
+ machine_id TEXT,
1900
+ synced_at TIMESTAMPTZ,
1901
+ created_at TIMESTAMPTZ NOT NULL,
1902
+ updated_at TIMESTAMPTZ NOT NULL,
1903
+ UNIQUE(domain_id, slug)
1904
+ )`),
1905
+ defineMigration("shortlinks_0003_clicks", `CREATE TABLE IF NOT EXISTS clicks (
1906
+ id TEXT PRIMARY KEY,
1907
+ link_id TEXT NOT NULL REFERENCES links(id) ON DELETE CASCADE,
1908
+ domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
1909
+ slug TEXT NOT NULL,
1910
+ clicked_at TIMESTAMPTZ NOT NULL,
1911
+ ip_hash TEXT,
1912
+ user_agent TEXT,
1913
+ referer TEXT,
1914
+ country TEXT,
1915
+ city TEXT,
1916
+ metadata TEXT NOT NULL DEFAULT '{}',
1917
+ machine_id TEXT,
1918
+ synced_at TIMESTAMPTZ,
1919
+ created_at TIMESTAMPTZ NOT NULL,
1920
+ updated_at TIMESTAMPTZ NOT NULL
1921
+ )`),
1922
+ defineMigration("shortlinks_0004_indexes", `CREATE INDEX IF NOT EXISTS idx_domains_hostname ON domains(hostname);
1923
+ CREATE INDEX IF NOT EXISTS idx_domains_default ON domains(default_domain);
1924
+ CREATE INDEX IF NOT EXISTS idx_links_domain_slug ON links(domain_id, slug);
1925
+ CREATE INDEX IF NOT EXISTS idx_links_active ON links(active);
1926
+ CREATE INDEX IF NOT EXISTS idx_links_updated ON links(updated_at);
1927
+ CREATE INDEX IF NOT EXISTS idx_clicks_link ON clicks(link_id);
1928
+ CREATE INDEX IF NOT EXISTS idx_clicks_domain ON clicks(domain_id);
1929
+ CREATE INDEX IF NOT EXISTS idx_clicks_clicked_at ON clicks(clicked_at);
1930
+ CREATE INDEX IF NOT EXISTS idx_clicks_updated ON clicks(updated_at)`)
1931
+ ];
1932
+ var SHORTLINKS_MIGRATIONS = [
1933
+ ...CORE_MIGRATIONS,
1934
+ ...apiKeyMigrations().map((m) => defineMigration(m.id, m.sql))
1935
+ ];
1936
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/compose.js
1937
+ var compose = (middleware, onError, onNotFound) => {
1938
+ return (context, next) => {
1939
+ let index = -1;
1940
+ return dispatch(0);
1941
+ async function dispatch(i) {
1942
+ if (i <= index) {
1943
+ throw new Error("next() called multiple times");
1944
+ }
1945
+ index = i;
1946
+ let res;
1947
+ let isError = false;
1948
+ let handler;
1949
+ if (middleware[i]) {
1950
+ handler = middleware[i][0][0];
1951
+ context.req.routeIndex = i;
1952
+ } else {
1953
+ handler = i === middleware.length && next || undefined;
1954
+ }
1955
+ if (handler) {
1956
+ try {
1957
+ res = await handler(context, () => dispatch(i + 1));
1958
+ } catch (err) {
1959
+ if (err instanceof Error && onError) {
1960
+ context.error = err;
1961
+ res = await onError(err, context);
1962
+ isError = true;
1963
+ } else {
1964
+ throw err;
1965
+ }
1966
+ }
1967
+ } else {
1968
+ if (context.finalized === false && onNotFound) {
1969
+ res = await onNotFound(context);
1970
+ }
1971
+ }
1972
+ if (res && (context.finalized === false || isError)) {
1973
+ context.res = res;
1974
+ }
1975
+ return context;
1976
+ }
1977
+ };
1978
+ };
1979
+
1980
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/request/constants.js
1981
+ var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
1982
+
1983
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/body.js
1984
+ var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
1985
+ const { all = false, dot = false } = options;
1986
+ const headers = request instanceof HonoRequest ? request.raw.headers : request.headers;
1987
+ const contentType = headers.get("Content-Type");
1988
+ if (contentType?.startsWith("multipart/form-data") || contentType?.startsWith("application/x-www-form-urlencoded")) {
1989
+ return parseFormData(request, { all, dot });
1990
+ }
1991
+ return {};
1992
+ };
1993
+ async function parseFormData(request, options) {
1994
+ const formData = await request.formData();
1995
+ if (formData) {
1996
+ return convertFormDataToBodyData(formData, options);
1997
+ }
1998
+ return {};
1999
+ }
2000
+ function convertFormDataToBodyData(formData, options) {
2001
+ const form = /* @__PURE__ */ Object.create(null);
2002
+ formData.forEach((value, key) => {
2003
+ const shouldParseAllValues = options.all || key.endsWith("[]");
2004
+ if (!shouldParseAllValues) {
2005
+ form[key] = value;
2006
+ } else {
2007
+ handleParsingAllValues(form, key, value);
2008
+ }
2009
+ });
2010
+ if (options.dot) {
2011
+ Object.entries(form).forEach(([key, value]) => {
2012
+ const shouldParseDotValues = key.includes(".");
2013
+ if (shouldParseDotValues) {
2014
+ handleParsingNestedValues(form, key, value);
2015
+ delete form[key];
2016
+ }
2017
+ });
2018
+ }
2019
+ return form;
2020
+ }
2021
+ var handleParsingAllValues = (form, key, value) => {
2022
+ if (form[key] !== undefined) {
2023
+ if (Array.isArray(form[key])) {
2024
+ form[key].push(value);
2025
+ } else {
2026
+ form[key] = [form[key], value];
2027
+ }
2028
+ } else {
2029
+ if (!key.endsWith("[]")) {
2030
+ form[key] = value;
2031
+ } else {
2032
+ form[key] = [value];
2033
+ }
2034
+ }
2035
+ };
2036
+ var handleParsingNestedValues = (form, key, value) => {
2037
+ if (/(?:^|\.)__proto__\./.test(key)) {
2038
+ return;
2039
+ }
2040
+ let nestedForm = form;
2041
+ const keys = key.split(".");
2042
+ keys.forEach((key2, index) => {
2043
+ if (index === keys.length - 1) {
2044
+ nestedForm[key2] = value;
2045
+ } else {
2046
+ if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
2047
+ nestedForm[key2] = /* @__PURE__ */ Object.create(null);
2048
+ }
2049
+ nestedForm = nestedForm[key2];
2050
+ }
2051
+ });
2052
+ };
2053
+
2054
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/url.js
2055
+ var splitPath = (path) => {
2056
+ const paths = path.split("/");
2057
+ if (paths[0] === "") {
2058
+ paths.shift();
2059
+ }
2060
+ return paths;
2061
+ };
2062
+ var splitRoutingPath = (routePath) => {
2063
+ const { groups, path } = extractGroupsFromPath(routePath);
2064
+ const paths = splitPath(path);
2065
+ return replaceGroupMarks(paths, groups);
2066
+ };
2067
+ var extractGroupsFromPath = (path) => {
2068
+ const groups = [];
2069
+ path = path.replace(/\{[^}]+\}/g, (match, index) => {
2070
+ const mark = `@${index}`;
2071
+ groups.push([mark, match]);
2072
+ return mark;
2073
+ });
2074
+ return { groups, path };
2075
+ };
2076
+ var replaceGroupMarks = (paths, groups) => {
2077
+ for (let i = groups.length - 1;i >= 0; i--) {
2078
+ const [mark] = groups[i];
2079
+ for (let j = paths.length - 1;j >= 0; j--) {
2080
+ if (paths[j].includes(mark)) {
2081
+ paths[j] = paths[j].replace(mark, groups[i][1]);
2082
+ break;
2083
+ }
2084
+ }
2085
+ }
2086
+ return paths;
2087
+ };
2088
+ var patternCache = {};
2089
+ var getPattern = (label, next) => {
2090
+ if (label === "*") {
2091
+ return "*";
2092
+ }
2093
+ const match = label.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
2094
+ if (match) {
2095
+ const cacheKey = `${label}#${next}`;
2096
+ if (!patternCache[cacheKey]) {
2097
+ if (match[2]) {
2098
+ patternCache[cacheKey] = next && next[0] !== ":" && next[0] !== "*" ? [cacheKey, match[1], new RegExp(`^${match[2]}(?=/${next})`)] : [label, match[1], new RegExp(`^${match[2]}$`)];
2099
+ } else {
2100
+ patternCache[cacheKey] = [label, match[1], true];
2101
+ }
2102
+ }
2103
+ return patternCache[cacheKey];
2104
+ }
2105
+ return null;
2106
+ };
2107
+ var tryDecode = (str, decoder) => {
2108
+ try {
2109
+ return decoder(str);
2110
+ } catch {
2111
+ return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
2112
+ try {
2113
+ return decoder(match);
2114
+ } catch {
2115
+ return match;
2116
+ }
2117
+ });
2118
+ }
2119
+ };
2120
+ var tryDecodeURI = (str) => tryDecode(str, decodeURI);
2121
+ var getPath = (request) => {
2122
+ const url = request.url;
2123
+ const start = url.indexOf("/", url.indexOf(":") + 4);
2124
+ let i = start;
2125
+ for (;i < url.length; i++) {
2126
+ const charCode = url.charCodeAt(i);
2127
+ if (charCode === 37) {
2128
+ const queryIndex = url.indexOf("?", i);
2129
+ const hashIndex = url.indexOf("#", i);
2130
+ const end = queryIndex === -1 ? hashIndex === -1 ? undefined : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
2131
+ const path = url.slice(start, end);
2132
+ return tryDecodeURI(path.includes("%25") ? path.replace(/%25/g, "%2525") : path);
2133
+ } else if (charCode === 63 || charCode === 35) {
2134
+ break;
2135
+ }
2136
+ }
2137
+ return url.slice(start, i);
2138
+ };
2139
+ var getPathNoStrict = (request) => {
2140
+ const result = getPath(request);
2141
+ return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result;
2142
+ };
2143
+ var mergePath = (base, sub, ...rest) => {
2144
+ if (rest.length) {
2145
+ sub = mergePath(sub, ...rest);
2146
+ }
2147
+ return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
2148
+ };
2149
+ var checkOptionalParameter = (path) => {
2150
+ if (path.charCodeAt(path.length - 1) !== 63 || !path.includes(":")) {
2151
+ return null;
2152
+ }
2153
+ const segments = path.split("/");
2154
+ const results = [];
2155
+ let basePath = "";
2156
+ segments.forEach((segment) => {
2157
+ if (segment !== "" && !/\:/.test(segment)) {
2158
+ basePath += "/" + segment;
2159
+ } else if (/\:/.test(segment)) {
2160
+ if (/\?/.test(segment)) {
2161
+ if (results.length === 0 && basePath === "") {
2162
+ results.push("/");
2163
+ } else {
2164
+ results.push(basePath);
2165
+ }
2166
+ const optionalSegment = segment.replace("?", "");
2167
+ basePath += "/" + optionalSegment;
2168
+ results.push(basePath);
2169
+ } else {
2170
+ basePath += "/" + segment;
2171
+ }
2172
+ }
2173
+ });
2174
+ return results.filter((v, i, a) => a.indexOf(v) === i);
2175
+ };
2176
+ var _decodeURI = (value) => {
2177
+ if (!/[%+]/.test(value)) {
2178
+ return value;
2179
+ }
2180
+ if (value.indexOf("+") !== -1) {
2181
+ value = value.replace(/\+/g, " ");
2182
+ }
2183
+ return value.indexOf("%") !== -1 ? tryDecode(value, decodeURIComponent_) : value;
2184
+ };
2185
+ var _getQueryParam = (url, key, multiple) => {
2186
+ let encoded;
2187
+ if (!multiple && key && !/[%+]/.test(key)) {
2188
+ let keyIndex2 = url.indexOf("?", 8);
2189
+ if (keyIndex2 === -1) {
2190
+ return;
2191
+ }
2192
+ if (!url.startsWith(key, keyIndex2 + 1)) {
2193
+ keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
2194
+ }
2195
+ while (keyIndex2 !== -1) {
2196
+ const trailingKeyCode = url.charCodeAt(keyIndex2 + key.length + 1);
2197
+ if (trailingKeyCode === 61) {
2198
+ const valueIndex = keyIndex2 + key.length + 2;
2199
+ const endIndex = url.indexOf("&", valueIndex);
2200
+ return _decodeURI(url.slice(valueIndex, endIndex === -1 ? undefined : endIndex));
2201
+ } else if (trailingKeyCode == 38 || isNaN(trailingKeyCode)) {
2202
+ return "";
2203
+ }
2204
+ keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
2205
+ }
2206
+ encoded = /[%+]/.test(url);
2207
+ if (!encoded) {
2208
+ return;
2209
+ }
2210
+ }
2211
+ const results = {};
2212
+ encoded ??= /[%+]/.test(url);
2213
+ let keyIndex = url.indexOf("?", 8);
2214
+ while (keyIndex !== -1) {
2215
+ const nextKeyIndex = url.indexOf("&", keyIndex + 1);
2216
+ let valueIndex = url.indexOf("=", keyIndex);
2217
+ if (valueIndex > nextKeyIndex && nextKeyIndex !== -1) {
2218
+ valueIndex = -1;
2219
+ }
2220
+ let name = url.slice(keyIndex + 1, valueIndex === -1 ? nextKeyIndex === -1 ? undefined : nextKeyIndex : valueIndex);
2221
+ if (encoded) {
2222
+ name = _decodeURI(name);
2223
+ }
2224
+ keyIndex = nextKeyIndex;
2225
+ if (name === "") {
2226
+ continue;
2227
+ }
2228
+ let value;
2229
+ if (valueIndex === -1) {
2230
+ value = "";
2231
+ } else {
2232
+ value = url.slice(valueIndex + 1, nextKeyIndex === -1 ? undefined : nextKeyIndex);
2233
+ if (encoded) {
2234
+ value = _decodeURI(value);
2235
+ }
2236
+ }
2237
+ if (multiple) {
2238
+ if (!(results[name] && Array.isArray(results[name]))) {
2239
+ results[name] = [];
2240
+ }
2241
+ results[name].push(value);
2242
+ } else {
2243
+ results[name] ??= value;
2244
+ }
2245
+ }
2246
+ return key ? results[key] : results;
2247
+ };
2248
+ var getQueryParam = _getQueryParam;
2249
+ var getQueryParams = (url, key) => {
2250
+ return _getQueryParam(url, key, true);
2251
+ };
2252
+ var decodeURIComponent_ = decodeURIComponent;
2253
+
2254
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/request.js
2255
+ var tryDecodeURIComponent = (str) => tryDecode(str, decodeURIComponent_);
2256
+ var HonoRequest = class {
2257
+ raw;
2258
+ #validatedData;
2259
+ #matchResult;
2260
+ routeIndex = 0;
2261
+ path;
2262
+ bodyCache = {};
2263
+ constructor(request, path = "/", matchResult = [[]]) {
2264
+ this.raw = request;
2265
+ this.path = path;
2266
+ this.#matchResult = matchResult;
2267
+ this.#validatedData = {};
2268
+ }
2269
+ param(key) {
2270
+ return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams();
2271
+ }
2272
+ #getDecodedParam(key) {
2273
+ const paramKey = this.#matchResult[0][this.routeIndex][1][key];
2274
+ const param = this.#getParamValue(paramKey);
2275
+ return param && /\%/.test(param) ? tryDecodeURIComponent(param) : param;
2276
+ }
2277
+ #getAllDecodedParams() {
2278
+ const decoded = {};
2279
+ const keys = Object.keys(this.#matchResult[0][this.routeIndex][1]);
2280
+ for (const key of keys) {
2281
+ const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key]);
2282
+ if (value !== undefined) {
2283
+ decoded[key] = /\%/.test(value) ? tryDecodeURIComponent(value) : value;
2284
+ }
2285
+ }
2286
+ return decoded;
2287
+ }
2288
+ #getParamValue(paramKey) {
2289
+ return this.#matchResult[1] ? this.#matchResult[1][paramKey] : paramKey;
2290
+ }
2291
+ query(key) {
2292
+ return getQueryParam(this.url, key);
2293
+ }
2294
+ queries(key) {
2295
+ return getQueryParams(this.url, key);
2296
+ }
2297
+ header(name) {
2298
+ if (name) {
2299
+ return this.raw.headers.get(name) ?? undefined;
2300
+ }
2301
+ const headerData = {};
2302
+ this.raw.headers.forEach((value, key) => {
2303
+ headerData[key] = value;
2304
+ });
2305
+ return headerData;
2306
+ }
2307
+ async parseBody(options) {
2308
+ return parseBody(this, options);
2309
+ }
2310
+ #cachedBody = (key) => {
2311
+ const { bodyCache, raw } = this;
2312
+ const cachedBody = bodyCache[key];
2313
+ if (cachedBody) {
2314
+ return cachedBody;
2315
+ }
2316
+ const anyCachedKey = Object.keys(bodyCache)[0];
2317
+ if (anyCachedKey) {
2318
+ return bodyCache[anyCachedKey].then((body) => {
2319
+ if (anyCachedKey === "json") {
2320
+ body = JSON.stringify(body);
2321
+ }
2322
+ return new Response(body)[key]();
2323
+ });
2324
+ }
2325
+ return bodyCache[key] = raw[key]();
2326
+ };
2327
+ json() {
2328
+ return this.#cachedBody("text").then((text) => JSON.parse(text));
2329
+ }
2330
+ text() {
2331
+ return this.#cachedBody("text");
2332
+ }
2333
+ arrayBuffer() {
2334
+ return this.#cachedBody("arrayBuffer");
2335
+ }
2336
+ bytes() {
2337
+ return this.#cachedBody("arrayBuffer").then((buffer) => new Uint8Array(buffer));
2338
+ }
2339
+ blob() {
2340
+ return this.#cachedBody("blob");
2341
+ }
2342
+ formData() {
2343
+ return this.#cachedBody("formData");
2344
+ }
2345
+ addValidatedData(target, data) {
2346
+ this.#validatedData[target] = data;
2347
+ }
2348
+ valid(target) {
2349
+ return this.#validatedData[target];
2350
+ }
2351
+ get url() {
2352
+ return this.raw.url;
2353
+ }
2354
+ get method() {
2355
+ return this.raw.method;
2356
+ }
2357
+ get [GET_MATCH_RESULT]() {
2358
+ return this.#matchResult;
2359
+ }
2360
+ get matchedRoutes() {
2361
+ return this.#matchResult[0].map(([[, route]]) => route);
2362
+ }
2363
+ get routePath() {
2364
+ return this.#matchResult[0].map(([[, route]]) => route)[this.routeIndex].path;
2365
+ }
2366
+ };
2367
+
2368
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/html.js
2369
+ var HtmlEscapedCallbackPhase = {
2370
+ Stringify: 1,
2371
+ BeforeStream: 2,
2372
+ Stream: 3
2373
+ };
2374
+ var raw = (value, callbacks) => {
2375
+ const escapedString = new String(value);
2376
+ escapedString.isEscaped = true;
2377
+ escapedString.callbacks = callbacks;
2378
+ return escapedString;
2379
+ };
2380
+ var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => {
2381
+ if (typeof str === "object" && !(str instanceof String)) {
2382
+ if (!(str instanceof Promise)) {
2383
+ str = str.toString();
2384
+ }
2385
+ if (str instanceof Promise) {
2386
+ str = await str;
2387
+ }
2388
+ }
2389
+ const callbacks = str.callbacks;
2390
+ if (!callbacks?.length) {
2391
+ return Promise.resolve(str);
2392
+ }
2393
+ if (buffer) {
2394
+ buffer[0] += str;
2395
+ } else {
2396
+ buffer = [str];
2397
+ }
2398
+ const resStr = Promise.all(callbacks.map((c) => c({ phase, buffer, context }))).then((res) => Promise.all(res.filter(Boolean).map((str2) => resolveCallback(str2, phase, false, context, buffer))).then(() => buffer[0]));
2399
+ if (preserveCallbacks) {
2400
+ return raw(await resStr, callbacks);
2401
+ } else {
2402
+ return resStr;
2403
+ }
2404
+ };
2405
+
2406
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/context.js
2407
+ var TEXT_PLAIN = "text/plain; charset=UTF-8";
2408
+ var setDefaultContentType = (contentType, headers) => {
2409
+ return {
2410
+ "Content-Type": contentType,
2411
+ ...headers
2412
+ };
2413
+ };
2414
+ var createResponseInstance = (body, init) => new Response(body, init);
2415
+ var Context = class {
2416
+ #rawRequest;
2417
+ #req;
2418
+ env = {};
2419
+ #var;
2420
+ finalized = false;
2421
+ error;
2422
+ #status;
2423
+ #executionCtx;
2424
+ #res;
2425
+ #layout;
2426
+ #renderer;
2427
+ #notFoundHandler;
2428
+ #preparedHeaders;
2429
+ #matchResult;
2430
+ #path;
2431
+ constructor(req, options) {
2432
+ this.#rawRequest = req;
2433
+ if (options) {
2434
+ this.#executionCtx = options.executionCtx;
2435
+ this.env = options.env;
2436
+ this.#notFoundHandler = options.notFoundHandler;
2437
+ this.#path = options.path;
2438
+ this.#matchResult = options.matchResult;
2439
+ }
2440
+ }
2441
+ get req() {
2442
+ this.#req ??= new HonoRequest(this.#rawRequest, this.#path, this.#matchResult);
2443
+ return this.#req;
2444
+ }
2445
+ get event() {
2446
+ if (this.#executionCtx && "respondWith" in this.#executionCtx) {
2447
+ return this.#executionCtx;
2448
+ } else {
2449
+ throw Error("This context has no FetchEvent");
2450
+ }
2451
+ }
2452
+ get executionCtx() {
2453
+ if (this.#executionCtx) {
2454
+ return this.#executionCtx;
2455
+ } else {
2456
+ throw Error("This context has no ExecutionContext");
2457
+ }
2458
+ }
2459
+ get res() {
2460
+ return this.#res ||= createResponseInstance(null, {
2461
+ headers: this.#preparedHeaders ??= new Headers
2462
+ });
2463
+ }
2464
+ set res(_res) {
2465
+ if (this.#res && _res) {
2466
+ _res = createResponseInstance(_res.body, _res);
2467
+ for (const [k, v] of this.#res.headers.entries()) {
2468
+ if (k === "content-type") {
2469
+ continue;
2470
+ }
2471
+ if (k === "set-cookie") {
2472
+ const cookies = this.#res.headers.getSetCookie();
2473
+ _res.headers.delete("set-cookie");
2474
+ for (const cookie of cookies) {
2475
+ _res.headers.append("set-cookie", cookie);
2476
+ }
2477
+ } else {
2478
+ _res.headers.set(k, v);
2479
+ }
2480
+ }
2481
+ }
2482
+ this.#res = _res;
2483
+ this.finalized = true;
2484
+ }
2485
+ render = (...args) => {
2486
+ this.#renderer ??= (content) => this.html(content);
2487
+ return this.#renderer(...args);
2488
+ };
2489
+ setLayout = (layout) => this.#layout = layout;
2490
+ getLayout = () => this.#layout;
2491
+ setRenderer = (renderer) => {
2492
+ this.#renderer = renderer;
2493
+ };
2494
+ header = (name, value, options) => {
2495
+ if (this.finalized) {
2496
+ this.#res = createResponseInstance(this.#res.body, this.#res);
2497
+ }
2498
+ const headers = this.#res ? this.#res.headers : this.#preparedHeaders ??= new Headers;
2499
+ if (value === undefined) {
2500
+ headers.delete(name);
2501
+ } else if (options?.append) {
2502
+ headers.append(name, value);
2503
+ } else {
2504
+ headers.set(name, value);
2505
+ }
2506
+ };
2507
+ status = (status) => {
2508
+ this.#status = status;
2509
+ };
2510
+ set = (key, value) => {
2511
+ this.#var ??= /* @__PURE__ */ new Map;
2512
+ this.#var.set(key, value);
2513
+ };
2514
+ get = (key) => {
2515
+ return this.#var ? this.#var.get(key) : undefined;
2516
+ };
2517
+ get var() {
2518
+ if (!this.#var) {
2519
+ return {};
2520
+ }
2521
+ return Object.fromEntries(this.#var);
2522
+ }
2523
+ #newResponse(data, arg, headers) {
2524
+ const responseHeaders = this.#res ? new Headers(this.#res.headers) : this.#preparedHeaders ?? new Headers;
2525
+ if (typeof arg === "object" && "headers" in arg) {
2526
+ const argHeaders = arg.headers instanceof Headers ? arg.headers : new Headers(arg.headers);
2527
+ for (const [key, value] of argHeaders) {
2528
+ if (key.toLowerCase() === "set-cookie") {
2529
+ responseHeaders.append(key, value);
2530
+ } else {
2531
+ responseHeaders.set(key, value);
2532
+ }
2533
+ }
2534
+ }
2535
+ if (headers) {
2536
+ for (const [k, v] of Object.entries(headers)) {
2537
+ if (typeof v === "string") {
2538
+ responseHeaders.set(k, v);
2539
+ } else {
2540
+ responseHeaders.delete(k);
2541
+ for (const v2 of v) {
2542
+ responseHeaders.append(k, v2);
2543
+ }
2544
+ }
2545
+ }
2546
+ }
2547
+ const status = typeof arg === "number" ? arg : arg?.status ?? this.#status;
2548
+ return createResponseInstance(data, { status, headers: responseHeaders });
2549
+ }
2550
+ newResponse = (...args) => this.#newResponse(...args);
2551
+ body = (data, arg, headers) => this.#newResponse(data, arg, headers);
2552
+ text = (text, arg, headers) => {
2553
+ return !this.#preparedHeaders && !this.#status && !arg && !headers && !this.finalized ? new Response(text) : this.#newResponse(text, arg, setDefaultContentType(TEXT_PLAIN, headers));
2554
+ };
2555
+ json = (object, arg, headers) => {
2556
+ return this.#newResponse(JSON.stringify(object), arg, setDefaultContentType("application/json", headers));
2557
+ };
2558
+ html = (html, arg, headers) => {
2559
+ const res = (html2) => this.#newResponse(html2, arg, setDefaultContentType("text/html; charset=UTF-8", headers));
2560
+ return typeof html === "object" ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html);
2561
+ };
2562
+ redirect = (location, status) => {
2563
+ const locationString = String(location);
2564
+ this.header("Location", !/[^\x00-\xFF]/.test(locationString) ? locationString : encodeURI(locationString));
2565
+ return this.newResponse(null, status ?? 302);
2566
+ };
2567
+ notFound = () => {
2568
+ this.#notFoundHandler ??= () => createResponseInstance();
2569
+ return this.#notFoundHandler(this);
2570
+ };
2571
+ };
2572
+
2573
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/router.js
2574
+ var METHOD_NAME_ALL = "ALL";
2575
+ var METHOD_NAME_ALL_LOWERCASE = "all";
2576
+ var METHODS = ["get", "post", "put", "delete", "options", "patch"];
2577
+ var MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is already built.";
2578
+ var UnsupportedPathError = class extends Error {
2579
+ };
2580
+
2581
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/constants.js
2582
+ var COMPOSED_HANDLER = "__COMPOSED_HANDLER";
2583
+
2584
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/hono-base.js
2585
+ var notFoundHandler = (c) => {
2586
+ return c.text("404 Not Found", 404);
2587
+ };
2588
+ var errorHandler = (err, c) => {
2589
+ if ("getResponse" in err) {
2590
+ const res = err.getResponse();
2591
+ return c.newResponse(res.body, res);
2592
+ }
2593
+ console.error(err);
2594
+ return c.text("Internal Server Error", 500);
2595
+ };
2596
+ var Hono = class _Hono {
2597
+ get;
2598
+ post;
2599
+ put;
2600
+ delete;
2601
+ options;
2602
+ patch;
2603
+ all;
2604
+ on;
2605
+ use;
2606
+ router;
2607
+ getPath;
2608
+ _basePath = "/";
2609
+ #path = "/";
2610
+ routes = [];
2611
+ constructor(options = {}) {
2612
+ const allMethods = [...METHODS, METHOD_NAME_ALL_LOWERCASE];
2613
+ allMethods.forEach((method) => {
2614
+ this[method] = (args1, ...args) => {
2615
+ if (typeof args1 === "string") {
2616
+ this.#path = args1;
2617
+ } else {
2618
+ this.#addRoute(method, this.#path, args1);
2619
+ }
2620
+ args.forEach((handler) => {
2621
+ this.#addRoute(method, this.#path, handler);
2622
+ });
2623
+ return this;
2624
+ };
2625
+ });
2626
+ this.on = (method, path, ...handlers) => {
2627
+ for (const p of [path].flat()) {
2628
+ this.#path = p;
2629
+ for (const m of [method].flat()) {
2630
+ handlers.map((handler) => {
2631
+ this.#addRoute(m.toUpperCase(), this.#path, handler);
2632
+ });
2633
+ }
2634
+ }
2635
+ return this;
2636
+ };
2637
+ this.use = (arg1, ...handlers) => {
2638
+ if (typeof arg1 === "string") {
2639
+ this.#path = arg1;
2640
+ } else {
2641
+ this.#path = "*";
2642
+ handlers.unshift(arg1);
2643
+ }
2644
+ handlers.forEach((handler) => {
2645
+ this.#addRoute(METHOD_NAME_ALL, this.#path, handler);
2646
+ });
2647
+ return this;
2648
+ };
2649
+ const { strict, ...optionsWithoutStrict } = options;
2650
+ Object.assign(this, optionsWithoutStrict);
2651
+ this.getPath = strict ?? true ? options.getPath ?? getPath : getPathNoStrict;
2652
+ }
2653
+ #clone() {
2654
+ const clone = new _Hono({
2655
+ router: this.router,
2656
+ getPath: this.getPath
2657
+ });
2658
+ clone.errorHandler = this.errorHandler;
2659
+ clone.#notFoundHandler = this.#notFoundHandler;
2660
+ clone.routes = this.routes;
2661
+ return clone;
2662
+ }
2663
+ #notFoundHandler = notFoundHandler;
2664
+ errorHandler = errorHandler;
2665
+ route(path, app) {
2666
+ const subApp = this.basePath(path);
2667
+ app.routes.map((r) => {
2668
+ let handler;
2669
+ if (app.errorHandler === errorHandler) {
2670
+ handler = r.handler;
2671
+ } else {
2672
+ handler = async (c, next) => (await compose([], app.errorHandler)(c, () => r.handler(c, next))).res;
2673
+ handler[COMPOSED_HANDLER] = r.handler;
2674
+ }
2675
+ subApp.#addRoute(r.method, r.path, handler, r.basePath);
2676
+ });
2677
+ return this;
2678
+ }
2679
+ basePath(path) {
2680
+ const subApp = this.#clone();
2681
+ subApp._basePath = mergePath(this._basePath, path);
2682
+ return subApp;
2683
+ }
2684
+ onError = (handler) => {
2685
+ this.errorHandler = handler;
2686
+ return this;
2687
+ };
2688
+ notFound = (handler) => {
2689
+ this.#notFoundHandler = handler;
2690
+ return this;
2691
+ };
2692
+ mount(path, applicationHandler, options) {
2693
+ let replaceRequest;
2694
+ let optionHandler;
2695
+ if (options) {
2696
+ if (typeof options === "function") {
2697
+ optionHandler = options;
2698
+ } else {
2699
+ optionHandler = options.optionHandler;
2700
+ if (options.replaceRequest === false) {
2701
+ replaceRequest = (request) => request;
2702
+ } else {
2703
+ replaceRequest = options.replaceRequest;
2704
+ }
2705
+ }
2706
+ }
2707
+ const getOptions = optionHandler ? (c) => {
2708
+ const options2 = optionHandler(c);
2709
+ return Array.isArray(options2) ? options2 : [options2];
2710
+ } : (c) => {
2711
+ let executionContext = undefined;
2712
+ try {
2713
+ executionContext = c.executionCtx;
2714
+ } catch {}
2715
+ return [c.env, executionContext];
2716
+ };
2717
+ replaceRequest ||= (() => {
2718
+ const mergedPath = mergePath(this._basePath, path);
2719
+ const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
2720
+ return (request) => {
2721
+ const url = new URL(request.url);
2722
+ url.pathname = this.getPath(request).slice(pathPrefixLength) || "/";
2723
+ return new Request(url, request);
2724
+ };
2725
+ })();
2726
+ const handler = async (c, next) => {
2727
+ const res = await applicationHandler(replaceRequest(c.req.raw), ...getOptions(c));
2728
+ if (res) {
2729
+ return res;
2730
+ }
2731
+ await next();
2732
+ };
2733
+ this.#addRoute(METHOD_NAME_ALL, mergePath(path, "*"), handler);
2734
+ return this;
2735
+ }
2736
+ #addRoute(method, path, handler, baseRoutePath) {
2737
+ method = method.toUpperCase();
2738
+ path = mergePath(this._basePath, path);
2739
+ const r = {
2740
+ basePath: baseRoutePath !== undefined ? mergePath(this._basePath, baseRoutePath) : this._basePath,
2741
+ path,
2742
+ method,
2743
+ handler
2744
+ };
2745
+ this.router.add(method, path, [handler, r]);
2746
+ this.routes.push(r);
2747
+ }
2748
+ #handleError(err, c) {
2749
+ if (err instanceof Error) {
2750
+ return this.errorHandler(err, c);
2751
+ }
2752
+ throw err;
2753
+ }
2754
+ #dispatch(request, executionCtx, env, method) {
2755
+ if (method === "HEAD") {
2756
+ return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
2757
+ }
2758
+ const path = this.getPath(request, { env });
2759
+ const matchResult = this.router.match(method, path);
2760
+ const c = new Context(request, {
2761
+ path,
2762
+ matchResult,
2763
+ env,
2764
+ executionCtx,
2765
+ notFoundHandler: this.#notFoundHandler
2766
+ });
2767
+ if (matchResult[0].length === 1) {
2768
+ let res;
2769
+ try {
2770
+ res = matchResult[0][0][0][0](c, async () => {
2771
+ c.res = await this.#notFoundHandler(c);
2772
+ });
2773
+ } catch (err) {
2774
+ return this.#handleError(err, c);
2775
+ }
2776
+ return res instanceof Promise ? res.then((resolved) => resolved || (c.finalized ? c.res : this.#notFoundHandler(c))).catch((err) => this.#handleError(err, c)) : res ?? this.#notFoundHandler(c);
2777
+ }
2778
+ const composed = compose(matchResult[0], this.errorHandler, this.#notFoundHandler);
2779
+ return (async () => {
2780
+ try {
2781
+ const context = await composed(c);
2782
+ if (!context.finalized) {
2783
+ throw new Error("Context is not finalized. Did you forget to return a Response object or `await next()`?");
2784
+ }
2785
+ return context.res;
2786
+ } catch (err) {
2787
+ return this.#handleError(err, c);
2788
+ }
2789
+ })();
2790
+ }
2791
+ fetch = (request, ...rest) => {
2792
+ return this.#dispatch(request, rest[1], rest[0], request.method);
2793
+ };
2794
+ request = (input, requestInit, Env, executionCtx) => {
2795
+ if (input instanceof Request) {
2796
+ return this.fetch(requestInit ? new Request(input, requestInit) : input, Env, executionCtx);
2797
+ }
2798
+ input = input.toString();
2799
+ return this.fetch(new Request(/^https?:\/\//.test(input) ? input : `http://localhost${mergePath("/", input)}`, requestInit), Env, executionCtx);
2800
+ };
2801
+ fire = () => {
2802
+ addEventListener("fetch", (event) => {
2803
+ event.respondWith(this.#dispatch(event.request, event, undefined, event.request.method));
2804
+ });
2805
+ };
2806
+ };
2807
+
2808
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/router/reg-exp-router/matcher.js
2809
+ var emptyParam = [];
2810
+ function match(method, path) {
2811
+ const matchers = this.buildAllMatchers();
2812
+ const match2 = (method2, path2) => {
2813
+ const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
2814
+ const staticMatch = matcher[2][path2];
2815
+ if (staticMatch) {
2816
+ return staticMatch;
2817
+ }
2818
+ const match3 = path2.match(matcher[0]);
2819
+ if (!match3) {
2820
+ return [[], emptyParam];
2821
+ }
2822
+ const index = match3.indexOf("", 1);
2823
+ return [matcher[1][index], match3];
2824
+ };
2825
+ this.match = match2;
2826
+ return match2(method, path);
2827
+ }
2828
+
2829
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/router/reg-exp-router/node.js
2830
+ var LABEL_REG_EXP_STR = "[^/]+";
2831
+ var ONLY_WILDCARD_REG_EXP_STR = ".*";
2832
+ var TAIL_WILDCARD_REG_EXP_STR = "(?:|/.*)";
2833
+ var PATH_ERROR = /* @__PURE__ */ Symbol();
2834
+ var regExpMetaChars = new Set(".\\+*[^]$()");
2835
+ function compareKey(a, b) {
2836
+ if (a.length === 1) {
2837
+ return b.length === 1 ? a < b ? -1 : 1 : -1;
2838
+ }
2839
+ if (b.length === 1) {
2840
+ return 1;
2841
+ }
2842
+ if (a === ONLY_WILDCARD_REG_EXP_STR || a === TAIL_WILDCARD_REG_EXP_STR) {
2843
+ return 1;
2844
+ } else if (b === ONLY_WILDCARD_REG_EXP_STR || b === TAIL_WILDCARD_REG_EXP_STR) {
2845
+ return -1;
2846
+ }
2847
+ if (a === LABEL_REG_EXP_STR) {
2848
+ return 1;
2849
+ } else if (b === LABEL_REG_EXP_STR) {
2850
+ return -1;
2851
+ }
2852
+ return a.length === b.length ? a < b ? -1 : 1 : b.length - a.length;
2853
+ }
2854
+ var Node = class _Node {
2855
+ #index;
2856
+ #varIndex;
2857
+ #children = /* @__PURE__ */ Object.create(null);
2858
+ insert(tokens, index, paramMap, context, pathErrorCheckOnly) {
2859
+ if (tokens.length === 0) {
2860
+ if (this.#index !== undefined) {
2861
+ throw PATH_ERROR;
2862
+ }
2863
+ if (pathErrorCheckOnly) {
2864
+ return;
2865
+ }
2866
+ this.#index = index;
2867
+ return;
2868
+ }
2869
+ const [token, ...restTokens] = tokens;
2870
+ const pattern = token === "*" ? restTokens.length === 0 ? ["", "", ONLY_WILDCARD_REG_EXP_STR] : ["", "", LABEL_REG_EXP_STR] : token === "/*" ? ["", "", TAIL_WILDCARD_REG_EXP_STR] : token.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
2871
+ let node;
2872
+ if (pattern) {
2873
+ const name = pattern[1];
2874
+ let regexpStr = pattern[2] || LABEL_REG_EXP_STR;
2875
+ if (name && pattern[2]) {
2876
+ if (regexpStr === ".*") {
2877
+ throw PATH_ERROR;
2878
+ }
2879
+ regexpStr = regexpStr.replace(/^\((?!\?:)(?=[^)]+\)$)/, "(?:");
2880
+ if (/\((?!\?:)/.test(regexpStr)) {
2881
+ throw PATH_ERROR;
2882
+ }
2883
+ }
2884
+ node = this.#children[regexpStr];
2885
+ if (!node) {
2886
+ if (Object.keys(this.#children).some((k) => k !== ONLY_WILDCARD_REG_EXP_STR && k !== TAIL_WILDCARD_REG_EXP_STR)) {
2887
+ throw PATH_ERROR;
2888
+ }
2889
+ if (pathErrorCheckOnly) {
2890
+ return;
2891
+ }
2892
+ node = this.#children[regexpStr] = new _Node;
2893
+ if (name !== "") {
2894
+ node.#varIndex = context.varIndex++;
2895
+ }
2896
+ }
2897
+ if (!pathErrorCheckOnly && name !== "") {
2898
+ paramMap.push([name, node.#varIndex]);
2899
+ }
2900
+ } else {
2901
+ node = this.#children[token];
2902
+ if (!node) {
2903
+ if (Object.keys(this.#children).some((k) => k.length > 1 && k !== ONLY_WILDCARD_REG_EXP_STR && k !== TAIL_WILDCARD_REG_EXP_STR)) {
2904
+ throw PATH_ERROR;
2905
+ }
2906
+ if (pathErrorCheckOnly) {
2907
+ return;
2908
+ }
2909
+ node = this.#children[token] = new _Node;
2910
+ }
2911
+ }
2912
+ node.insert(restTokens, index, paramMap, context, pathErrorCheckOnly);
2913
+ }
2914
+ buildRegExpStr() {
2915
+ const childKeys = Object.keys(this.#children).sort(compareKey);
2916
+ const strList = childKeys.map((k) => {
2917
+ const c = this.#children[k];
2918
+ return (typeof c.#varIndex === "number" ? `(${k})@${c.#varIndex}` : regExpMetaChars.has(k) ? `\\${k}` : k) + c.buildRegExpStr();
2919
+ });
2920
+ if (typeof this.#index === "number") {
2921
+ strList.unshift(`#${this.#index}`);
2922
+ }
2923
+ if (strList.length === 0) {
2924
+ return "";
2925
+ }
2926
+ if (strList.length === 1) {
2927
+ return strList[0];
2928
+ }
2929
+ return "(?:" + strList.join("|") + ")";
2930
+ }
2931
+ };
2932
+
2933
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/router/reg-exp-router/trie.js
2934
+ var Trie = class {
2935
+ #context = { varIndex: 0 };
2936
+ #root = new Node;
2937
+ insert(path, index, pathErrorCheckOnly) {
2938
+ const paramAssoc = [];
2939
+ const groups = [];
2940
+ for (let i = 0;; ) {
2941
+ let replaced = false;
2942
+ path = path.replace(/\{[^}]+\}/g, (m) => {
2943
+ const mark = `@\\${i}`;
2944
+ groups[i] = [mark, m];
2945
+ i++;
2946
+ replaced = true;
2947
+ return mark;
2948
+ });
2949
+ if (!replaced) {
2950
+ break;
2951
+ }
2952
+ }
2953
+ const tokens = path.match(/(?::[^\/]+)|(?:\/\*$)|./g) || [];
2954
+ for (let i = groups.length - 1;i >= 0; i--) {
2955
+ const [mark] = groups[i];
2956
+ for (let j = tokens.length - 1;j >= 0; j--) {
2957
+ if (tokens[j].indexOf(mark) !== -1) {
2958
+ tokens[j] = tokens[j].replace(mark, groups[i][1]);
2959
+ break;
2960
+ }
2961
+ }
2962
+ }
2963
+ this.#root.insert(tokens, index, paramAssoc, this.#context, pathErrorCheckOnly);
2964
+ return paramAssoc;
2965
+ }
2966
+ buildRegExp() {
2967
+ let regexp = this.#root.buildRegExpStr();
2968
+ if (regexp === "") {
2969
+ return [/^$/, [], []];
2970
+ }
2971
+ let captureIndex = 0;
2972
+ const indexReplacementMap = [];
2973
+ const paramReplacementMap = [];
2974
+ regexp = regexp.replace(/#(\d+)|@(\d+)|\.\*\$/g, (_, handlerIndex, paramIndex) => {
2975
+ if (handlerIndex !== undefined) {
2976
+ indexReplacementMap[++captureIndex] = Number(handlerIndex);
2977
+ return "$()";
2978
+ }
2979
+ if (paramIndex !== undefined) {
2980
+ paramReplacementMap[Number(paramIndex)] = ++captureIndex;
2981
+ return "";
2982
+ }
2983
+ return "";
2984
+ });
2985
+ return [new RegExp(`^${regexp}`), indexReplacementMap, paramReplacementMap];
2986
+ }
2987
+ };
2988
+
2989
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/router/reg-exp-router/router.js
2990
+ var nullMatcher = [/^$/, [], /* @__PURE__ */ Object.create(null)];
2991
+ var wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
2992
+ function buildWildcardRegExp(path) {
2993
+ return wildcardRegExpCache[path] ??= new RegExp(path === "*" ? "" : `^${path.replace(/\/\*$|([.\\+*[^\]$()])/g, (_, metaChar) => metaChar ? `\\${metaChar}` : "(?:|/.*)")}$`);
2994
+ }
2995
+ function clearWildcardRegExpCache() {
2996
+ wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
2997
+ }
2998
+ function buildMatcherFromPreprocessedRoutes(routes) {
2999
+ const trie = new Trie;
3000
+ const handlerData = [];
3001
+ if (routes.length === 0) {
3002
+ return nullMatcher;
3003
+ }
3004
+ const routesWithStaticPathFlag = routes.map((route) => [!/\*|\/:/.test(route[0]), ...route]).sort(([isStaticA, pathA], [isStaticB, pathB]) => isStaticA ? 1 : isStaticB ? -1 : pathA.length - pathB.length);
3005
+ const staticMap = /* @__PURE__ */ Object.create(null);
3006
+ for (let i = 0, j = -1, len = routesWithStaticPathFlag.length;i < len; i++) {
3007
+ const [pathErrorCheckOnly, path, handlers] = routesWithStaticPathFlag[i];
3008
+ if (pathErrorCheckOnly) {
3009
+ staticMap[path] = [handlers.map(([h]) => [h, /* @__PURE__ */ Object.create(null)]), emptyParam];
3010
+ } else {
3011
+ j++;
3012
+ }
3013
+ let paramAssoc;
3014
+ try {
3015
+ paramAssoc = trie.insert(path, j, pathErrorCheckOnly);
3016
+ } catch (e) {
3017
+ throw e === PATH_ERROR ? new UnsupportedPathError(path) : e;
3018
+ }
3019
+ if (pathErrorCheckOnly) {
3020
+ continue;
3021
+ }
3022
+ handlerData[j] = handlers.map(([h, paramCount]) => {
3023
+ const paramIndexMap = /* @__PURE__ */ Object.create(null);
3024
+ paramCount -= 1;
3025
+ for (;paramCount >= 0; paramCount--) {
3026
+ const [key, value] = paramAssoc[paramCount];
3027
+ paramIndexMap[key] = value;
3028
+ }
3029
+ return [h, paramIndexMap];
3030
+ });
3031
+ }
3032
+ const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
3033
+ for (let i = 0, len = handlerData.length;i < len; i++) {
3034
+ for (let j = 0, len2 = handlerData[i].length;j < len2; j++) {
3035
+ const map = handlerData[i][j]?.[1];
3036
+ if (!map) {
3037
+ continue;
3038
+ }
3039
+ const keys = Object.keys(map);
3040
+ for (let k = 0, len3 = keys.length;k < len3; k++) {
3041
+ map[keys[k]] = paramReplacementMap[map[keys[k]]];
3042
+ }
3043
+ }
3044
+ }
3045
+ const handlerMap = [];
3046
+ for (const i in indexReplacementMap) {
3047
+ handlerMap[i] = handlerData[indexReplacementMap[i]];
3048
+ }
3049
+ return [regexp, handlerMap, staticMap];
3050
+ }
3051
+ function findMiddleware(middleware, path) {
3052
+ if (!middleware) {
3053
+ return;
3054
+ }
3055
+ for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) {
3056
+ if (buildWildcardRegExp(k).test(path)) {
3057
+ return [...middleware[k]];
3058
+ }
3059
+ }
3060
+ return;
3061
+ }
3062
+ var RegExpRouter = class {
3063
+ name = "RegExpRouter";
3064
+ #middleware;
3065
+ #routes;
3066
+ constructor() {
3067
+ this.#middleware = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
3068
+ this.#routes = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
3069
+ }
3070
+ add(method, path, handler) {
3071
+ const middleware = this.#middleware;
3072
+ const routes = this.#routes;
3073
+ if (!middleware || !routes) {
3074
+ throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
3075
+ }
3076
+ if (!middleware[method]) {
3077
+ [middleware, routes].forEach((handlerMap) => {
3078
+ handlerMap[method] = /* @__PURE__ */ Object.create(null);
3079
+ Object.keys(handlerMap[METHOD_NAME_ALL]).forEach((p) => {
3080
+ handlerMap[method][p] = [...handlerMap[METHOD_NAME_ALL][p]];
3081
+ });
3082
+ });
3083
+ }
3084
+ if (path === "/*") {
3085
+ path = "*";
3086
+ }
3087
+ const paramCount = (path.match(/\/:/g) || []).length;
3088
+ if (/\*$/.test(path)) {
3089
+ const re = buildWildcardRegExp(path);
3090
+ if (method === METHOD_NAME_ALL) {
3091
+ Object.keys(middleware).forEach((m) => {
3092
+ middleware[m][path] ||= findMiddleware(middleware[m], path) || findMiddleware(middleware[METHOD_NAME_ALL], path) || [];
3093
+ });
3094
+ } else {
3095
+ middleware[method][path] ||= findMiddleware(middleware[method], path) || findMiddleware(middleware[METHOD_NAME_ALL], path) || [];
3096
+ }
3097
+ Object.keys(middleware).forEach((m) => {
3098
+ if (method === METHOD_NAME_ALL || method === m) {
3099
+ Object.keys(middleware[m]).forEach((p) => {
3100
+ re.test(p) && middleware[m][p].push([handler, paramCount]);
3101
+ });
3102
+ }
3103
+ });
3104
+ Object.keys(routes).forEach((m) => {
3105
+ if (method === METHOD_NAME_ALL || method === m) {
3106
+ Object.keys(routes[m]).forEach((p) => re.test(p) && routes[m][p].push([handler, paramCount]));
3107
+ }
3108
+ });
3109
+ return;
3110
+ }
3111
+ const paths = checkOptionalParameter(path) || [path];
3112
+ for (let i = 0, len = paths.length;i < len; i++) {
3113
+ const path2 = paths[i];
3114
+ Object.keys(routes).forEach((m) => {
3115
+ if (method === METHOD_NAME_ALL || method === m) {
3116
+ routes[m][path2] ||= [
3117
+ ...findMiddleware(middleware[m], path2) || findMiddleware(middleware[METHOD_NAME_ALL], path2) || []
3118
+ ];
3119
+ routes[m][path2].push([handler, paramCount - len + i + 1]);
3120
+ }
3121
+ });
3122
+ }
3123
+ }
3124
+ match = match;
3125
+ buildAllMatchers() {
3126
+ const matchers = /* @__PURE__ */ Object.create(null);
3127
+ Object.keys(this.#routes).concat(Object.keys(this.#middleware)).forEach((method) => {
3128
+ matchers[method] ||= this.#buildMatcher(method);
3129
+ });
3130
+ this.#middleware = this.#routes = undefined;
3131
+ clearWildcardRegExpCache();
3132
+ return matchers;
3133
+ }
3134
+ #buildMatcher(method) {
3135
+ const routes = [];
3136
+ let hasOwnRoute = method === METHOD_NAME_ALL;
3137
+ [this.#middleware, this.#routes].forEach((r) => {
3138
+ const ownRoute = r[method] ? Object.keys(r[method]).map((path) => [path, r[method][path]]) : [];
3139
+ if (ownRoute.length !== 0) {
3140
+ hasOwnRoute ||= true;
3141
+ routes.push(...ownRoute);
3142
+ } else if (method !== METHOD_NAME_ALL) {
3143
+ routes.push(...Object.keys(r[METHOD_NAME_ALL]).map((path) => [path, r[METHOD_NAME_ALL][path]]));
3144
+ }
3145
+ });
3146
+ if (!hasOwnRoute) {
3147
+ return null;
3148
+ } else {
3149
+ return buildMatcherFromPreprocessedRoutes(routes);
3150
+ }
3151
+ }
3152
+ };
3153
+
3154
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/router/reg-exp-router/prepared-router.js
3155
+ var PreparedRegExpRouter = class {
3156
+ name = "PreparedRegExpRouter";
3157
+ #matchers;
3158
+ #relocateMap;
3159
+ constructor(matchers, relocateMap) {
3160
+ this.#matchers = matchers;
3161
+ this.#relocateMap = relocateMap;
3162
+ }
3163
+ #addWildcard(method, handlerData) {
3164
+ const matcher = this.#matchers[method];
3165
+ matcher[1].forEach((list) => list && list.push(handlerData));
3166
+ Object.values(matcher[2]).forEach((list) => list[0].push(handlerData));
3167
+ }
3168
+ #addPath(method, path, handler, indexes, map) {
3169
+ const matcher = this.#matchers[method];
3170
+ if (!map) {
3171
+ matcher[2][path][0].push([handler, {}]);
3172
+ } else {
3173
+ indexes.forEach((index) => {
3174
+ if (typeof index === "number") {
3175
+ matcher[1][index].push([handler, map]);
3176
+ } else {
3177
+ matcher[2][index || path][0].push([handler, map]);
3178
+ }
3179
+ });
3180
+ }
3181
+ }
3182
+ add(method, path, handler) {
3183
+ if (!this.#matchers[method]) {
3184
+ const all = this.#matchers[METHOD_NAME_ALL];
3185
+ const staticMap = {};
3186
+ for (const key in all[2]) {
3187
+ staticMap[key] = [all[2][key][0].slice(), emptyParam];
3188
+ }
3189
+ this.#matchers[method] = [
3190
+ all[0],
3191
+ all[1].map((list) => Array.isArray(list) ? list.slice() : 0),
3192
+ staticMap
3193
+ ];
3194
+ }
3195
+ if (path === "/*" || path === "*") {
3196
+ const handlerData = [handler, {}];
3197
+ if (method === METHOD_NAME_ALL) {
3198
+ for (const m in this.#matchers) {
3199
+ this.#addWildcard(m, handlerData);
3200
+ }
3201
+ } else {
3202
+ this.#addWildcard(method, handlerData);
3203
+ }
3204
+ return;
3205
+ }
3206
+ const data = this.#relocateMap[path];
3207
+ if (!data) {
3208
+ throw new Error(`Path ${path} is not registered`);
3209
+ }
3210
+ for (const [indexes, map] of data) {
3211
+ if (method === METHOD_NAME_ALL) {
3212
+ for (const m in this.#matchers) {
3213
+ this.#addPath(m, path, handler, indexes, map);
3214
+ }
3215
+ } else {
3216
+ this.#addPath(method, path, handler, indexes, map);
3217
+ }
3218
+ }
3219
+ }
3220
+ buildAllMatchers() {
3221
+ return this.#matchers;
3222
+ }
3223
+ match = match;
3224
+ };
3225
+
3226
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/router/smart-router/router.js
3227
+ var SmartRouter = class {
3228
+ name = "SmartRouter";
3229
+ #routers = [];
3230
+ #routes = [];
3231
+ constructor(init) {
3232
+ this.#routers = init.routers;
3233
+ }
3234
+ add(method, path, handler) {
3235
+ if (!this.#routes) {
3236
+ throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
3237
+ }
3238
+ this.#routes.push([method, path, handler]);
3239
+ }
3240
+ match(method, path) {
3241
+ if (!this.#routes) {
3242
+ throw new Error("Fatal error");
3243
+ }
3244
+ const routers = this.#routers;
3245
+ const routes = this.#routes;
3246
+ const len = routers.length;
3247
+ let i = 0;
3248
+ let res;
3249
+ for (;i < len; i++) {
3250
+ const router = routers[i];
3251
+ try {
3252
+ for (let i2 = 0, len2 = routes.length;i2 < len2; i2++) {
3253
+ router.add(...routes[i2]);
3254
+ }
3255
+ res = router.match(method, path);
3256
+ } catch (e) {
3257
+ if (e instanceof UnsupportedPathError) {
3258
+ continue;
3259
+ }
3260
+ throw e;
3261
+ }
3262
+ this.match = router.match.bind(router);
3263
+ this.#routers = [router];
3264
+ this.#routes = undefined;
3265
+ break;
3266
+ }
3267
+ if (i === len) {
3268
+ throw new Error("Fatal error");
3269
+ }
3270
+ this.name = `SmartRouter + ${this.activeRouter.name}`;
3271
+ return res;
3272
+ }
3273
+ get activeRouter() {
3274
+ if (this.#routes || this.#routers.length !== 1) {
3275
+ throw new Error("No active router has been determined yet.");
3276
+ }
3277
+ return this.#routers[0];
3278
+ }
3279
+ };
3280
+
3281
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/router/trie-router/node.js
3282
+ var emptyParams = /* @__PURE__ */ Object.create(null);
3283
+ var hasChildren = (children) => {
3284
+ for (const _ in children) {
3285
+ return true;
3286
+ }
3287
+ return false;
3288
+ };
3289
+ var Node2 = class _Node2 {
3290
+ #methods;
3291
+ #children;
3292
+ #patterns;
3293
+ #order = 0;
3294
+ #params = emptyParams;
3295
+ constructor(method, handler, children) {
3296
+ this.#children = children || /* @__PURE__ */ Object.create(null);
3297
+ this.#methods = [];
3298
+ if (method && handler) {
3299
+ const m = /* @__PURE__ */ Object.create(null);
3300
+ m[method] = { handler, possibleKeys: [], score: 0 };
3301
+ this.#methods = [m];
3302
+ }
3303
+ this.#patterns = [];
3304
+ }
3305
+ insert(method, path, handler) {
3306
+ this.#order = ++this.#order;
3307
+ let curNode = this;
3308
+ const parts = splitRoutingPath(path);
3309
+ const possibleKeys = [];
3310
+ for (let i = 0, len = parts.length;i < len; i++) {
3311
+ const p = parts[i];
3312
+ const nextP = parts[i + 1];
3313
+ const pattern = getPattern(p, nextP);
3314
+ const key = Array.isArray(pattern) ? pattern[0] : p;
3315
+ if (key in curNode.#children) {
3316
+ curNode = curNode.#children[key];
3317
+ if (pattern) {
3318
+ possibleKeys.push(pattern[1]);
3319
+ }
3320
+ continue;
3321
+ }
3322
+ curNode.#children[key] = new _Node2;
3323
+ if (pattern) {
3324
+ curNode.#patterns.push(pattern);
3325
+ possibleKeys.push(pattern[1]);
3326
+ }
3327
+ curNode = curNode.#children[key];
3328
+ }
3329
+ curNode.#methods.push({
3330
+ [method]: {
3331
+ handler,
3332
+ possibleKeys: possibleKeys.filter((v, i, a) => a.indexOf(v) === i),
3333
+ score: this.#order
3334
+ }
3335
+ });
3336
+ return curNode;
3337
+ }
3338
+ #pushHandlerSets(handlerSets, node, method, nodeParams, params) {
3339
+ for (let i = 0, len = node.#methods.length;i < len; i++) {
3340
+ const m = node.#methods[i];
3341
+ const handlerSet = m[method] || m[METHOD_NAME_ALL];
3342
+ const processedSet = {};
3343
+ if (handlerSet !== undefined) {
3344
+ handlerSet.params = /* @__PURE__ */ Object.create(null);
3345
+ handlerSets.push(handlerSet);
3346
+ if (nodeParams !== emptyParams || params && params !== emptyParams) {
3347
+ for (let i2 = 0, len2 = handlerSet.possibleKeys.length;i2 < len2; i2++) {
3348
+ const key = handlerSet.possibleKeys[i2];
3349
+ const processed = processedSet[handlerSet.score];
3350
+ handlerSet.params[key] = params?.[key] && !processed ? params[key] : nodeParams[key] ?? params?.[key];
3351
+ processedSet[handlerSet.score] = true;
3352
+ }
3353
+ }
3354
+ }
3355
+ }
3356
+ }
3357
+ search(method, path) {
3358
+ const handlerSets = [];
3359
+ this.#params = emptyParams;
3360
+ const curNode = this;
3361
+ let curNodes = [curNode];
3362
+ const parts = splitPath(path);
3363
+ const curNodesQueue = [];
3364
+ const len = parts.length;
3365
+ let partOffsets = null;
3366
+ for (let i = 0;i < len; i++) {
3367
+ const part = parts[i];
3368
+ const isLast = i === len - 1;
3369
+ const tempNodes = [];
3370
+ for (let j = 0, len2 = curNodes.length;j < len2; j++) {
3371
+ const node = curNodes[j];
3372
+ const nextNode = node.#children[part];
3373
+ if (nextNode) {
3374
+ nextNode.#params = node.#params;
3375
+ if (isLast) {
3376
+ if (nextNode.#children["*"]) {
3377
+ this.#pushHandlerSets(handlerSets, nextNode.#children["*"], method, node.#params);
3378
+ }
3379
+ this.#pushHandlerSets(handlerSets, nextNode, method, node.#params);
3380
+ } else {
3381
+ tempNodes.push(nextNode);
3382
+ }
3383
+ }
3384
+ for (let k = 0, len3 = node.#patterns.length;k < len3; k++) {
3385
+ const pattern = node.#patterns[k];
3386
+ const params = node.#params === emptyParams ? {} : { ...node.#params };
3387
+ if (pattern === "*") {
3388
+ const astNode = node.#children["*"];
3389
+ if (astNode) {
3390
+ this.#pushHandlerSets(handlerSets, astNode, method, node.#params);
3391
+ astNode.#params = params;
3392
+ tempNodes.push(astNode);
3393
+ }
3394
+ continue;
3395
+ }
3396
+ const [key, name, matcher] = pattern;
3397
+ if (!part && !(matcher instanceof RegExp)) {
3398
+ continue;
3399
+ }
3400
+ const child = node.#children[key];
3401
+ if (matcher instanceof RegExp) {
3402
+ if (partOffsets === null) {
3403
+ partOffsets = new Array(len);
3404
+ let offset = path[0] === "/" ? 1 : 0;
3405
+ for (let p = 0;p < len; p++) {
3406
+ partOffsets[p] = offset;
3407
+ offset += parts[p].length + 1;
3408
+ }
3409
+ }
3410
+ const restPathString = path.substring(partOffsets[i]);
3411
+ const m = matcher.exec(restPathString);
3412
+ if (m) {
3413
+ params[name] = m[0];
3414
+ this.#pushHandlerSets(handlerSets, child, method, node.#params, params);
3415
+ if (hasChildren(child.#children)) {
3416
+ child.#params = params;
3417
+ const componentCount = m[0].match(/\//)?.length ?? 0;
3418
+ const targetCurNodes = curNodesQueue[componentCount] ||= [];
3419
+ targetCurNodes.push(child);
3420
+ }
3421
+ continue;
3422
+ }
3423
+ }
3424
+ if (matcher === true || matcher.test(part)) {
3425
+ params[name] = part;
3426
+ if (isLast) {
3427
+ this.#pushHandlerSets(handlerSets, child, method, params, node.#params);
3428
+ if (child.#children["*"]) {
3429
+ this.#pushHandlerSets(handlerSets, child.#children["*"], method, params, node.#params);
3430
+ }
3431
+ } else {
3432
+ child.#params = params;
3433
+ tempNodes.push(child);
3434
+ }
3435
+ }
3436
+ }
3437
+ }
3438
+ const shifted = curNodesQueue.shift();
3439
+ curNodes = shifted ? tempNodes.concat(shifted) : tempNodes;
3440
+ }
3441
+ if (handlerSets.length > 1) {
3442
+ handlerSets.sort((a, b) => {
3443
+ return a.score - b.score;
3444
+ });
3445
+ }
3446
+ return [handlerSets.map(({ handler, params }) => [handler, params])];
3447
+ }
3448
+ };
3449
+
3450
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/router/trie-router/router.js
3451
+ var TrieRouter = class {
3452
+ name = "TrieRouter";
3453
+ #node;
3454
+ constructor() {
3455
+ this.#node = new Node2;
3456
+ }
3457
+ add(method, path, handler) {
3458
+ const results = checkOptionalParameter(path);
3459
+ if (results) {
3460
+ for (let i = 0, len = results.length;i < len; i++) {
3461
+ this.#node.insert(method, results[i], handler);
3462
+ }
3463
+ return;
3464
+ }
3465
+ this.#node.insert(method, path, handler);
3466
+ }
3467
+ match(method, path) {
3468
+ return this.#node.search(method, path);
3469
+ }
3470
+ };
3471
+
3472
+ // node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/hono.js
3473
+ var Hono2 = class extends Hono {
3474
+ constructor(options = {}) {
3475
+ super(options);
3476
+ this.router = options.router ?? new SmartRouter({
3477
+ routers: [new RegExpRouter, new TrieRouter]
3478
+ });
3479
+ }
3480
+ };
3481
+
3482
+ // src/generated/storage-kit/health.ts
3483
+ async function checkHealth(client) {
3484
+ const start = Date.now();
3485
+ try {
3486
+ await client.get("SELECT 1 AS ok");
3487
+ return { ok: true, latencyMs: Date.now() - start };
3488
+ } catch (error) {
3489
+ return {
3490
+ ok: false,
3491
+ latencyMs: Date.now() - start,
3492
+ error: error instanceof Error ? error.message : String(error)
3493
+ };
3494
+ }
3495
+ }
3496
+ async function checkReady(client, migrations, options = {}) {
3497
+ const start = Date.now();
3498
+ try {
3499
+ const ledger = new MigrationLedger(client, migrations, options);
3500
+ const result = await ledger.migrate({ dryRun: true });
3501
+ const pending = result.plan.filter((item) => item.state === "pending").map((item) => item.migration.id);
3502
+ return { ok: pending.length === 0, latencyMs: Date.now() - start, pendingMigrations: pending };
3503
+ } catch (error) {
3504
+ return {
3505
+ ok: false,
3506
+ latencyMs: Date.now() - start,
3507
+ pendingMigrations: [],
3508
+ error: error instanceof Error ? error.message : String(error)
3509
+ };
3510
+ }
3511
+ }
3512
+
3513
+ // src/serve/openapi.ts
3514
+ function buildOpenApiDocument(version) {
3515
+ const linkSchema = {
3516
+ type: "object",
3517
+ properties: {
3518
+ id: { type: "string" },
3519
+ domain_id: { type: "string" },
3520
+ hostname: { type: "string" },
3521
+ slug: { type: "string" },
3522
+ destination_url: { type: "string" },
3523
+ title: { type: "string", nullable: true },
3524
+ active: { type: "boolean" },
3525
+ expires_at: { type: "string", nullable: true },
3526
+ short_url: { type: "string" },
3527
+ metadata: { type: "object", additionalProperties: true },
3528
+ created_at: { type: "string" },
3529
+ updated_at: { type: "string" }
3530
+ },
3531
+ required: ["id", "domain_id", "hostname", "slug", "destination_url", "active", "created_at"]
3532
+ };
3533
+ const domainSchema = {
3534
+ type: "object",
3535
+ properties: {
3536
+ id: { type: "string" },
3537
+ hostname: { type: "string" },
3538
+ provider: { type: "string" },
3539
+ default_domain: { type: "boolean" },
3540
+ origin_url: { type: "string", nullable: true },
3541
+ notes: { type: "string", nullable: true },
3542
+ metadata: { type: "object", additionalProperties: true },
3543
+ created_at: { type: "string" },
3544
+ updated_at: { type: "string" }
3545
+ },
3546
+ required: ["id", "hostname", "provider", "default_domain", "created_at"]
3547
+ };
3548
+ const linkStatsSchema = {
3549
+ type: "object",
3550
+ properties: {
3551
+ link: { $ref: "#/components/schemas/Link" },
3552
+ clicks: { type: "integer" },
3553
+ last_clicked_at: { type: "string", nullable: true },
3554
+ top_referrers: {
3555
+ type: "array",
3556
+ items: {
3557
+ type: "object",
3558
+ properties: { referer: { type: "string", nullable: true }, clicks: { type: "integer" } }
3559
+ }
3560
+ },
3561
+ top_user_agents: {
3562
+ type: "array",
3563
+ items: {
3564
+ type: "object",
3565
+ properties: { user_agent: { type: "string", nullable: true }, clicks: { type: "integer" } }
3566
+ }
3567
+ }
3568
+ },
3569
+ required: ["link", "clicks"]
3570
+ };
3571
+ const probe = (extra = {}) => ({
3572
+ type: "object",
3573
+ properties: {
3574
+ status: { type: "string" },
3575
+ version: { type: "string" },
3576
+ mode: { type: "string" },
3577
+ ...extra
3578
+ },
3579
+ required: ["status", "version", "mode"]
3580
+ });
3581
+ return {
3582
+ openapi: "3.0.3",
3583
+ info: {
3584
+ title: "ShortlinksApi",
3585
+ version,
3586
+ description: "Shortlink manager \u2014 custom domains, click tracking, and shortlink CRUD with API-key auth. PURE REMOTE (Amendment A1): reads/writes RDS Postgres directly."
3587
+ },
3588
+ servers: [{ url: "/" }],
3589
+ components: {
3590
+ securitySchemes: {
3591
+ apiKey: { type: "apiKey", in: "header", name: "x-api-key" }
3592
+ },
3593
+ schemas: {
3594
+ Link: linkSchema,
3595
+ Domain: domainSchema,
3596
+ LinkStats: linkStatsSchema,
3597
+ LinkList: { type: "array", items: linkSchema },
3598
+ DomainList: { type: "array", items: domainSchema },
3599
+ TotalStats: {
3600
+ type: "object",
3601
+ properties: {
3602
+ domains: { type: "integer" },
3603
+ links: { type: "integer" },
3604
+ clicks: { type: "integer" }
3605
+ },
3606
+ required: ["domains", "links", "clicks"]
3607
+ },
3608
+ CreateLinkRequest: {
3609
+ type: "object",
3610
+ properties: {
3611
+ url: { type: "string", description: "Destination URL (http/https)." },
3612
+ domain: { type: "string", description: "Hostname; defaults to the default domain." },
3613
+ slug: { type: "string", description: "Custom slug; generated when omitted." },
3614
+ title: { type: "string" },
3615
+ expires_at: { type: "string", description: "ISO date/time." },
3616
+ length: { type: "integer", description: "Generated slug length." },
3617
+ metadata: { type: "object", additionalProperties: true }
3618
+ },
3619
+ required: ["url"]
3620
+ },
3621
+ AddDomainRequest: {
3622
+ type: "object",
3623
+ properties: {
3624
+ hostname: { type: "string" },
3625
+ provider: { type: "string" },
3626
+ default: { type: "boolean" },
3627
+ origin_url: { type: "string" },
3628
+ notes: { type: "string" },
3629
+ metadata: { type: "object", additionalProperties: true }
3630
+ },
3631
+ required: ["hostname"]
3632
+ },
3633
+ DeleteResponse: {
3634
+ type: "object",
3635
+ properties: { deleted: { type: "boolean" }, slug: { type: "string" } },
3636
+ required: ["deleted"]
3637
+ },
3638
+ HealthStatus: probe({ db_latency_ms: { type: "integer" } }),
3639
+ ReadyStatus: probe({ pending_migrations: { type: "array", items: { type: "string" } } }),
3640
+ VersionInfo: probe({ name: { type: "string" } }),
3641
+ ErrorResponse: {
3642
+ type: "object",
3643
+ properties: { error: { type: "string" }, reason: { type: "string" } },
3644
+ required: ["error"]
3645
+ }
3646
+ }
3647
+ },
3648
+ paths: {
3649
+ "/health": {
3650
+ get: {
3651
+ operationId: "getHealth",
3652
+ summary: "Liveness probe.",
3653
+ responses: {
3654
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/HealthStatus" } } } }
3655
+ }
3656
+ }
3657
+ },
3658
+ "/ready": {
3659
+ get: {
3660
+ operationId: "getReady",
3661
+ summary: "Readiness probe (DB reachable and schema migrated).",
3662
+ responses: {
3663
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ReadyStatus" } } } }
3664
+ }
3665
+ }
3666
+ },
3667
+ "/version": {
3668
+ get: {
3669
+ operationId: "getVersion",
3670
+ summary: "Service version and mode.",
3671
+ responses: {
3672
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/VersionInfo" } } } }
3673
+ }
3674
+ }
3675
+ },
3676
+ "/v1/stats": {
3677
+ get: {
3678
+ operationId: "getStats",
3679
+ summary: "Total domains/links/clicks counts.",
3680
+ security: [{ apiKey: [] }],
3681
+ responses: {
3682
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/TotalStats" } } } }
3683
+ }
3684
+ }
3685
+ },
3686
+ "/v1/domains": {
3687
+ get: {
3688
+ operationId: "listDomains",
3689
+ summary: "List configured domains.",
3690
+ security: [{ apiKey: [] }],
3691
+ responses: {
3692
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/DomainList" } } } }
3693
+ }
3694
+ },
3695
+ post: {
3696
+ operationId: "addDomain",
3697
+ summary: "Add or update a domain.",
3698
+ security: [{ apiKey: [] }],
3699
+ requestBody: {
3700
+ required: true,
3701
+ content: { "application/json": { schema: { $ref: "#/components/schemas/AddDomainRequest" } } }
3702
+ },
3703
+ responses: {
3704
+ "201": { content: { "application/json": { schema: { $ref: "#/components/schemas/Domain" } } } }
3705
+ }
3706
+ }
3707
+ },
3708
+ "/v1/links": {
3709
+ get: {
3710
+ operationId: "listLinks",
3711
+ summary: "List shortlinks.",
3712
+ security: [{ apiKey: [] }],
3713
+ parameters: [
3714
+ { name: "domain", in: "query", schema: { type: "string" } },
3715
+ { name: "active", in: "query", schema: { type: "boolean" } },
3716
+ { name: "limit", in: "query", schema: { type: "integer" } }
3717
+ ],
3718
+ responses: {
3719
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/LinkList" } } } }
3720
+ }
3721
+ },
3722
+ post: {
3723
+ operationId: "createLink",
3724
+ summary: "Create a shortlink.",
3725
+ security: [{ apiKey: [] }],
3726
+ requestBody: {
3727
+ required: true,
3728
+ content: { "application/json": { schema: { $ref: "#/components/schemas/CreateLinkRequest" } } }
3729
+ },
3730
+ responses: {
3731
+ "201": { content: { "application/json": { schema: { $ref: "#/components/schemas/Link" } } } }
3732
+ }
3733
+ }
3734
+ },
3735
+ "/v1/links/{slug}": {
3736
+ get: {
3737
+ operationId: "getLink",
3738
+ summary: "Get a shortlink by slug.",
3739
+ security: [{ apiKey: [] }],
3740
+ parameters: [
3741
+ { name: "slug", in: "path", required: true, schema: { type: "string" } },
3742
+ { name: "domain", in: "query", schema: { type: "string" } }
3743
+ ],
3744
+ responses: {
3745
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/Link" } } } }
3746
+ }
3747
+ },
3748
+ delete: {
3749
+ operationId: "deleteLink",
3750
+ summary: "Delete a shortlink.",
3751
+ security: [{ apiKey: [] }],
3752
+ parameters: [
3753
+ { name: "slug", in: "path", required: true, schema: { type: "string" } },
3754
+ { name: "domain", in: "query", schema: { type: "string" } }
3755
+ ],
3756
+ responses: {
3757
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/DeleteResponse" } } } }
3758
+ }
3759
+ }
3760
+ },
3761
+ "/v1/links/{slug}/enable": {
3762
+ post: {
3763
+ operationId: "enableLink",
3764
+ summary: "Enable a shortlink.",
3765
+ security: [{ apiKey: [] }],
3766
+ parameters: [
3767
+ { name: "slug", in: "path", required: true, schema: { type: "string" } },
3768
+ { name: "domain", in: "query", schema: { type: "string" } }
3769
+ ],
3770
+ responses: {
3771
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/Link" } } } }
3772
+ }
3773
+ }
3774
+ },
3775
+ "/v1/links/{slug}/disable": {
3776
+ post: {
3777
+ operationId: "disableLink",
3778
+ summary: "Disable a shortlink.",
3779
+ security: [{ apiKey: [] }],
3780
+ parameters: [
3781
+ { name: "slug", in: "path", required: true, schema: { type: "string" } },
3782
+ { name: "domain", in: "query", schema: { type: "string" } }
3783
+ ],
3784
+ responses: {
3785
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/Link" } } } }
3786
+ }
3787
+ }
3788
+ },
3789
+ "/v1/links/{slug}/stats": {
3790
+ get: {
3791
+ operationId: "getLinkStats",
3792
+ summary: "Click stats for a shortlink.",
3793
+ security: [{ apiKey: [] }],
3794
+ parameters: [
3795
+ { name: "slug", in: "path", required: true, schema: { type: "string" } },
3796
+ { name: "domain", in: "query", schema: { type: "string" } }
3797
+ ],
3798
+ responses: {
3799
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/LinkStats" } } } }
3800
+ }
3801
+ }
3802
+ },
3803
+ "/v1/resolve/{slug}": {
3804
+ get: {
3805
+ operationId: "resolveLink",
3806
+ summary: "Resolve a slug to its destination without recording a click.",
3807
+ security: [{ apiKey: [] }],
3808
+ parameters: [
3809
+ { name: "slug", in: "path", required: true, schema: { type: "string" } },
3810
+ { name: "domain", in: "query", schema: { type: "string" } }
3811
+ ],
3812
+ responses: {
3813
+ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/Link" } } } }
3814
+ }
3815
+ }
3816
+ }
3817
+ }
3818
+ };
3819
+ }
3820
+
3821
+ // src/serve/app.ts
3822
+ var APP_SLUG = "shortlinks";
3823
+ var SECURITY_HEADERS = {
3824
+ "X-Content-Type-Options": "nosniff",
3825
+ "X-Frame-Options": "DENY",
3826
+ "Referrer-Policy": "no-referrer",
3827
+ "Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=()"
3828
+ };
3829
+ function createServeApp(deps) {
3830
+ const app = new Hono2;
3831
+ const { store, client, version, mode } = deps;
3832
+ const verifier = verifyApiKey({
3833
+ app: APP_SLUG,
3834
+ signingSecret: deps.signingSecret,
3835
+ ...deps.isRevoked ? { isRevoked: deps.isRevoked } : {},
3836
+ ...deps.audit ? { audit: deps.audit } : {}
3837
+ });
3838
+ app.use("*", async (c, next) => {
3839
+ await next();
3840
+ for (const [k, v] of Object.entries(SECURITY_HEADERS))
3841
+ c.header(k, v);
3842
+ });
3843
+ async function requireScopes(c, scopes) {
3844
+ const decision = await verifier.authenticate(c.req.raw.headers, {
3845
+ method: c.req.method,
3846
+ path: c.req.path,
3847
+ requiredScopes: scopes
3848
+ });
3849
+ if (!decision.ok) {
3850
+ return c.json({ error: decision.message, reason: decision.reason }, decision.status);
3851
+ }
3852
+ c.set("apiKey", decision.principal);
3853
+ return null;
3854
+ }
3855
+ function handleError(c, error) {
3856
+ const message = error instanceof Error ? error.message : String(error);
3857
+ const notFound = /not found/i.test(message);
3858
+ return c.json({ error: message }, notFound ? 404 : 400);
3859
+ }
3860
+ app.get("/health", async (c) => {
3861
+ const health = await checkHealth(client);
3862
+ return c.json({ status: health.ok ? "ok" : "degraded", version, mode, db_latency_ms: health.latencyMs }, health.ok ? 200 : 503);
3863
+ });
3864
+ app.get("/ready", async (c) => {
3865
+ const ready = await checkReady(client, SHORTLINKS_MIGRATIONS);
3866
+ return c.json({
3867
+ status: ready.ok ? "ready" : "not_ready",
3868
+ version,
3869
+ mode,
3870
+ pending_migrations: ready.pendingMigrations,
3871
+ ...ready.error ? { error: ready.error } : {}
3872
+ }, ready.ok ? 200 : 503);
3873
+ });
3874
+ app.get("/version", (c) => c.json({ status: "ok", version, mode, name: `@hasna/${APP_SLUG}` }));
3875
+ app.get("/openapi.json", (c) => c.json(buildOpenApiDocument(version)));
3876
+ app.get("/v1/stats", async (c) => {
3877
+ const denied = await requireScopes(c, [`${APP_SLUG}:read`]);
3878
+ if (denied)
3879
+ return denied;
3880
+ return c.json(await store.totalStats());
3881
+ });
3882
+ app.get("/v1/domains", async (c) => {
3883
+ const denied = await requireScopes(c, [`${APP_SLUG}:read`]);
3884
+ if (denied)
3885
+ return denied;
3886
+ return c.json(await store.listDomains());
3887
+ });
3888
+ app.post("/v1/domains", async (c) => {
3889
+ const denied = await requireScopes(c, [`${APP_SLUG}:write`]);
3890
+ if (denied)
3891
+ return denied;
3892
+ const body = await c.req.json().catch(() => null);
3893
+ if (!body?.hostname)
3894
+ return c.json({ error: "hostname is required" }, 400);
3895
+ try {
3896
+ const domain = await store.addDomain({
3897
+ hostname: body.hostname,
3898
+ ...body.provider !== undefined ? { provider: body.provider } : {},
3899
+ ...body.default !== undefined ? { defaultDomain: body.default } : {},
3900
+ ...body.origin_url !== undefined ? { originUrl: body.origin_url } : {},
3901
+ ...body.notes !== undefined ? { notes: body.notes } : {},
3902
+ ...body.metadata !== undefined ? { metadata: body.metadata } : {}
3903
+ });
3904
+ return c.json(domain, 201);
3905
+ } catch (error) {
3906
+ return handleError(c, error);
3907
+ }
3908
+ });
3909
+ app.get("/v1/links", async (c) => {
3910
+ const denied = await requireScopes(c, [`${APP_SLUG}:read`]);
3911
+ if (denied)
3912
+ return denied;
3913
+ const domain = c.req.query("domain");
3914
+ const limit = c.req.query("limit") ? parseInt(c.req.query("limit"), 10) : 100;
3915
+ const activeOnly = c.req.query("active") === "true";
3916
+ const links = await store.listLinks({
3917
+ ...domain ? { domain } : {},
3918
+ activeOnly,
3919
+ limit
3920
+ });
3921
+ return c.json(links);
3922
+ });
3923
+ app.post("/v1/links", async (c) => {
3924
+ const denied = await requireScopes(c, [`${APP_SLUG}:write`]);
3925
+ if (denied)
3926
+ return denied;
3927
+ const body = await c.req.json().catch(() => null);
3928
+ if (!body?.url)
3929
+ return c.json({ error: "url is required" }, 400);
3930
+ try {
3931
+ const link = await store.createLink({
3932
+ destinationUrl: body.url,
3933
+ ...body.domain !== undefined ? { domain: body.domain } : {},
3934
+ ...body.slug !== undefined ? { slug: body.slug } : {},
3935
+ ...body.title !== undefined ? { title: body.title } : {},
3936
+ ...body.expires_at !== undefined ? { expiresAt: body.expires_at } : {},
3937
+ ...body.length !== undefined ? { slugLength: body.length } : {},
3938
+ ...body.metadata !== undefined ? { metadata: body.metadata } : {}
3939
+ });
3940
+ return c.json(link, 201);
3941
+ } catch (error) {
3942
+ return handleError(c, error);
3943
+ }
3944
+ });
3945
+ app.get("/v1/links/:slug", async (c) => {
3946
+ const denied = await requireScopes(c, [`${APP_SLUG}:read`]);
3947
+ if (denied)
3948
+ return denied;
3949
+ const slug = c.req.param("slug");
3950
+ const domain = c.req.query("domain");
3951
+ const link = domain ? await store.getLink(domain, slug) : await store.getLink(slug);
3952
+ if (!link)
3953
+ return c.json({ error: "Link not found." }, 404);
3954
+ return c.json(link);
3955
+ });
3956
+ app.delete("/v1/links/:slug", async (c) => {
3957
+ const denied = await requireScopes(c, [`${APP_SLUG}:write`]);
3958
+ if (denied)
3959
+ return denied;
3960
+ const slug = c.req.param("slug");
3961
+ const domain = c.req.query("domain");
3962
+ try {
3963
+ const link = domain ? await store.deleteLink(domain, slug) : await store.deleteLink(slug);
3964
+ return c.json({ deleted: true, slug: link.slug });
3965
+ } catch (error) {
3966
+ return handleError(c, error);
3967
+ }
3968
+ });
3969
+ app.post("/v1/links/:slug/enable", async (c) => {
3970
+ const denied = await requireScopes(c, [`${APP_SLUG}:write`]);
3971
+ if (denied)
3972
+ return denied;
3973
+ const slug = c.req.param("slug");
3974
+ const domain = c.req.query("domain");
3975
+ try {
3976
+ const link = domain ? await store.setLinkActive(domain, slug, true) : await store.setLinkActive(slug, true);
3977
+ return c.json(link);
3978
+ } catch (error) {
3979
+ return handleError(c, error);
3980
+ }
3981
+ });
3982
+ app.post("/v1/links/:slug/disable", async (c) => {
3983
+ const denied = await requireScopes(c, [`${APP_SLUG}:write`]);
3984
+ if (denied)
3985
+ return denied;
3986
+ const slug = c.req.param("slug");
3987
+ const domain = c.req.query("domain");
3988
+ try {
3989
+ const link = domain ? await store.setLinkActive(domain, slug, false) : await store.setLinkActive(slug, false);
3990
+ return c.json(link);
3991
+ } catch (error) {
3992
+ return handleError(c, error);
3993
+ }
3994
+ });
3995
+ app.get("/v1/links/:slug/stats", async (c) => {
3996
+ const denied = await requireScopes(c, [`${APP_SLUG}:read`]);
3997
+ if (denied)
3998
+ return denied;
3999
+ const slug = c.req.param("slug");
4000
+ const domain = c.req.query("domain");
4001
+ try {
4002
+ const stats = domain ? await store.getStats(domain, slug) : await store.getStats(slug);
4003
+ return c.json(stats);
4004
+ } catch (error) {
4005
+ return handleError(c, error);
4006
+ }
4007
+ });
4008
+ app.get("/v1/resolve/:slug", async (c) => {
4009
+ const denied = await requireScopes(c, [`${APP_SLUG}:read`]);
4010
+ if (denied)
4011
+ return denied;
4012
+ const slug = c.req.param("slug");
4013
+ const domain = c.req.query("domain");
4014
+ const link = domain ? await store.getLink(domain, slug) : await store.getLink(slug);
4015
+ if (!link)
4016
+ return c.json({ error: "Link not found." }, 404);
4017
+ return c.json(link);
4018
+ });
4019
+ return app;
4020
+ }
1387
4021
  // src/local.ts
1388
4022
  import { spawnSync } from "child_process";
1389
4023
  import { homedir as homedir2 } from "os";
@@ -1523,8 +4157,11 @@ export {
1523
4157
  generateWorkerScript,
1524
4158
  formatShortUrl,
1525
4159
  createShortlinksHandler,
4160
+ createServeApp,
1526
4161
  createLocalSetupPlan,
4162
+ createKitPgAdapter,
1527
4163
  createCloudflarePlan,
4164
+ buildOpenApiDocument,
1528
4165
  assertShortlinksPostgresConfig,
1529
4166
  applyPostgresMigrations,
1530
4167
  ShortlinksStore,
@@ -1532,6 +4169,7 @@ export {
1532
4169
  SQLITE_MIGRATIONS,
1533
4170
  SHORTLINKS_RUNTIME_FALLBACK_ENV,
1534
4171
  SHORTLINKS_RUNTIME_ENV,
4172
+ SHORTLINKS_MIGRATIONS,
1535
4173
  PgShortlinksStore,
1536
4174
  PG_MIGRATIONS,
1537
4175
  CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH,