@hasna/shortlinks 0.1.22 → 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,10 +1,203 @@
1
1
  // @bun
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);
2
33
  var __require = import.meta.require;
3
34
 
4
- // src/database.ts
5
- import { Database } from "bun:sqlite";
6
- import { mkdirSync as mkdirSync2 } from "fs";
7
- import { dirname as dirname2 } from "path";
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";
8
201
 
9
202
  // src/config.ts
10
203
  import { existsSync, linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
@@ -136,6 +329,9 @@ function formatShortUrl(hostname, slug, publicBaseUrl) {
136
329
  }
137
330
 
138
331
  // src/database.ts
332
+ import { Database } from "bun:sqlite";
333
+ import { mkdirSync as mkdirSync2 } from "fs";
334
+ import { dirname as dirname2 } from "path";
139
335
  function now() {
140
336
  return new Date().toISOString();
141
337
  }
@@ -243,8 +439,6 @@ class ShortlinksDatabase {
243
439
  }
244
440
  }
245
441
  }
246
- // src/store.ts
247
- import { createHash } from "crypto";
248
442
 
249
443
  // src/machine.ts
250
444
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -290,38 +484,90 @@ function getMachineId() {
290
484
  return id;
291
485
  }
292
486
 
293
- // src/store.ts
487
+ // src/pg-store.ts
294
488
  function parseJsonObject(value) {
295
489
  if (!value)
296
490
  return {};
491
+ if (typeof value === "object" && !Array.isArray(value))
492
+ return value;
297
493
  try {
298
- const parsed = JSON.parse(value);
494
+ const parsed = JSON.parse(String(value));
299
495
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
300
496
  } catch {
301
497
  return {};
302
498
  }
303
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
+ }
304
551
  function domainFromRow(row) {
305
552
  return {
306
553
  ...row,
307
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),
308
558
  metadata: parseJsonObject(row.metadata)
309
559
  };
310
560
  }
311
561
  function linkFromRow(row) {
312
- const config = loadConfig();
313
- const publicBaseUrl = config.defaultDomain === row.hostname ? config.publicBaseUrl : undefined;
314
562
  return {
315
563
  ...row,
316
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),
317
569
  metadata: parseJsonObject(row.metadata),
318
- short_url: formatShortUrl(row.hostname, row.slug, publicBaseUrl)
319
- };
320
- }
321
- function clickFromRow(row) {
322
- return {
323
- ...row,
324
- metadata: parseJsonObject(row.metadata)
570
+ short_url: formatShortUrl(row.hostname, row.slug)
325
571
  };
326
572
  }
327
573
  function validateDestinationUrl(url) {
@@ -344,26 +590,61 @@ function isoOrNull(input) {
344
590
  throw new Error(`Invalid date: ${input}`);
345
591
  return date.toISOString();
346
592
  }
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
+ }
347
616
 
348
- class ShortlinksStore {
349
- database;
350
- constructor(dbPath) {
351
- this.database = new ShortlinksDatabase(dbPath);
617
+ class PgShortlinksStore {
618
+ pg;
619
+ constructor(pg) {
620
+ this.pg = pg;
352
621
  }
353
- close() {
354
- this.database.close();
622
+ static async fromConnectionString(connectionString, options = {}) {
623
+ return new PgShortlinksStore(await PgPoolAdapter.create(connectionString, options));
355
624
  }
356
- 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) {
357
639
  const hostname2 = normalizeHostname(input.hostname);
358
640
  const timestamp = now();
359
641
  const machineId = getMachineId();
360
- const existing = this.getDomain(hostname2);
642
+ const existing = await this.getDomain(hostname2);
361
643
  const id = existing?.id || makeId("dom");
362
644
  if (input.defaultDomain) {
363
- this.database.db.query("UPDATE domains SET default_domain = 0, updated_at = ?, synced_at = NULL").run(timestamp);
364
- updateConfig({ defaultDomain: hostname2, publicBaseUrl: `https://${hostname2}` });
645
+ await this.pg.run("UPDATE domains SET default_domain = 0, updated_at = ? WHERE default_domain = 1", timestamp);
365
646
  }
366
- this.database.db.query(`
647
+ await this.pg.run(`
367
648
  INSERT INTO domains (
368
649
  id, hostname, provider, default_domain, cloudflare_zone_id, cloudflare_account_id,
369
650
  cloudflare_worker_name, origin_url, notes, metadata, machine_id, synced_at, created_at, updated_at
@@ -381,37 +662,31 @@ class ShortlinksStore {
381
662
  machine_id = excluded.machine_id,
382
663
  synced_at = NULL,
383
664
  updated_at = excluded.updated_at
384
- `).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);
385
- 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);
386
667
  }
387
- listDomains() {
388
- const rows = this.database.db.query(`
668
+ async listDomains() {
669
+ const rows = await this.pg.all(`
389
670
  SELECT * FROM domains
390
671
  ORDER BY default_domain DESC, hostname ASC
391
- `).all();
672
+ `);
392
673
  return rows.map(domainFromRow);
393
674
  }
394
- getDomain(hostnameOrId) {
675
+ async getDomain(hostnameOrId) {
395
676
  const normalized = hostnameOrId.includes(".") || hostnameOrId.includes("://") ? normalizeHostname(hostnameOrId) : hostnameOrId;
396
- const row = this.database.db.query(`
677
+ const row = await this.pg.get(`
397
678
  SELECT * FROM domains WHERE hostname = ? OR id = ? LIMIT 1
398
- `).get(normalized, hostnameOrId);
679
+ `, normalized, hostnameOrId);
399
680
  return row ? domainFromRow(row) : null;
400
681
  }
401
- getDefaultDomain() {
402
- const config = loadConfig();
403
- if (config.defaultDomain) {
404
- const configured = this.getDomain(config.defaultDomain);
405
- if (configured)
406
- return configured;
407
- }
408
- const row = this.database.db.query(`
682
+ async getDefaultDomain() {
683
+ const row = await this.pg.get(`
409
684
  SELECT * FROM domains ORDER BY default_domain DESC, created_at ASC LIMIT 1
410
- `).get();
685
+ `);
411
686
  return row ? domainFromRow(row) : null;
412
687
  }
413
- createLink(input) {
414
- 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();
415
690
  if (!domain) {
416
691
  throw new Error("No domain configured. Run `shortlinks domain add <domain> --default` first.");
417
692
  }
@@ -419,46 +694,45 @@ class ShortlinksStore {
419
694
  const timestamp = now();
420
695
  const machineId = getMachineId();
421
696
  const expiresAt = isoOrNull(input.expiresAt);
422
- 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);
423
698
  try {
424
- this.database.db.query(`
699
+ await this.pg.run(`
425
700
  INSERT INTO links (
426
701
  id, domain_id, slug, destination_url, title, active, expires_at, metadata,
427
702
  machine_id, synced_at, created_at, updated_at
428
703
  )
429
704
  VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, NULL, ?, ?)
430
- `).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);
431
706
  } catch (error) {
432
707
  const message = error instanceof Error ? error.message : String(error);
433
- if (message.includes("UNIQUE")) {
708
+ if (message.includes("unique") || message.includes("duplicate")) {
434
709
  throw new Error(`Slug already exists for ${domain.hostname}: ${slug}`);
435
710
  }
436
711
  throw error;
437
712
  }
438
- return this.getLink(domain.hostname, slug);
713
+ return await this.getLink(domain.hostname, slug);
439
714
  }
440
- listLinks(options = {}) {
715
+ async listLinks(options = {}) {
441
716
  const params = [];
442
717
  let where = "WHERE 1 = 1";
443
718
  if (options.domain) {
444
719
  where += " AND d.hostname = ?";
445
720
  params.push(normalizeHostname(options.domain));
446
721
  }
447
- if (options.activeOnly) {
722
+ if (options.activeOnly)
448
723
  where += " AND l.active = 1";
449
- }
450
724
  params.push(options.limit || 100);
451
- const rows = this.database.db.query(`
725
+ const rows = await this.pg.all(`
452
726
  SELECT l.*, d.hostname
453
727
  FROM links l
454
728
  JOIN domains d ON d.id = l.domain_id
455
729
  ${where}
456
730
  ORDER BY l.created_at DESC
457
731
  LIMIT ?
458
- `).all(...params);
732
+ `, ...params);
459
733
  return rows.map(linkFromRow);
460
734
  }
461
- getLink(domainOrSlug, maybeSlug) {
735
+ async getLink(domainOrSlug, maybeSlug) {
462
736
  const slug = normalizeSlug(maybeSlug || domainOrSlug);
463
737
  const params = [slug];
464
738
  let domainClause = "";
@@ -466,165 +740,206 @@ class ShortlinksStore {
466
740
  domainClause = "AND d.hostname = ?";
467
741
  params.push(normalizeHostname(domainOrSlug));
468
742
  }
469
- const row = this.database.db.query(`
743
+ const row = await this.pg.get(`
470
744
  SELECT l.*, d.hostname
471
745
  FROM links l
472
746
  JOIN domains d ON d.id = l.domain_id
473
747
  WHERE l.slug = ? ${domainClause}
474
748
  ORDER BY d.default_domain DESC, l.created_at ASC
475
749
  LIMIT 1
476
- `).get(...params);
750
+ `, ...params);
477
751
  return row ? linkFromRow(row) : null;
478
752
  }
479
- resolve(hostname2, slug) {
480
- 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) {
481
763
  const normalizedHost = normalizeHostname(hostname2);
482
- const row = this.database.db.query(`
764
+ const normalizedSlug = normalizeSlug(slug);
765
+ const row = await this.pg.get(`
483
766
  SELECT l.*, d.hostname
484
767
  FROM links l
485
768
  JOIN domains d ON d.id = l.domain_id
486
769
  WHERE d.hostname = ? AND l.slug = ?
487
770
  LIMIT 1
488
- `).get(normalizedHost, normalizedSlug);
771
+ `, normalizedHost, normalizedSlug);
489
772
  if (row)
490
773
  return linkFromRow(row);
491
- const fallback = this.getDefaultDomain();
492
- if (!fallback || fallback.hostname === normalizedHost)
493
- return null;
494
- 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;
495
783
  }
496
- setLinkActive(domainOrSlug, maybeSlugOrActive, maybeActive) {
784
+ async setLinkActive(domainOrSlug, maybeSlugOrActive, maybeActive) {
497
785
  const active = typeof maybeSlugOrActive === "boolean" ? maybeSlugOrActive : Boolean(maybeActive);
498
- 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);
499
787
  if (!link)
500
788
  throw new Error("Link not found.");
501
789
  const timestamp = now();
502
- this.database.db.query(`
790
+ await this.pg.run(`
503
791
  UPDATE links SET active = ?, updated_at = ?, synced_at = NULL WHERE id = ?
504
- `).run(active ? 1 : 0, timestamp, link.id);
505
- return this.getLink(link.hostname, link.slug);
792
+ `, active ? 1 : 0, timestamp, link.id);
793
+ return await this.getLink(link.hostname, link.slug);
506
794
  }
507
- deleteLink(domainOrSlug, maybeSlug) {
508
- 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);
509
797
  if (!link)
510
798
  throw new Error("Link not found.");
511
- this.database.db.query("DELETE FROM links WHERE id = ?").run(link.id);
799
+ await this.pg.run("DELETE FROM links WHERE id = ?", link.id);
512
800
  return link;
513
801
  }
514
- recordClick(link, input = {}) {
802
+ async recordClick(link, input = {}) {
515
803
  const timestamp = now();
516
804
  const machineId = getMachineId();
517
805
  const ipHash = input.ip ? this.hashIp(input.ip) : null;
518
806
  const id = makeId("clk");
519
- this.database.db.query(`
807
+ await this.pg.run(`
520
808
  INSERT INTO clicks (
521
809
  id, link_id, domain_id, slug, clicked_at, ip_hash, user_agent, referer,
522
810
  country, city, metadata, machine_id, synced_at, created_at, updated_at
523
811
  )
524
812
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
525
- `).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);
526
- 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);
527
815
  return clickFromRow(row);
528
816
  }
529
- getStats(domainOrSlug, maybeSlug) {
530
- 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);
531
819
  if (!link)
532
820
  throw new Error("Link not found.");
533
- const summary = this.database.db.query(`
534
- SELECT COUNT(*) AS clicks, MAX(clicked_at) AS last_clicked_at FROM clicks WHERE link_id = ?
535
- `).get(link.id);
536
- const topReferrers = this.database.db.query(`
537
- 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
538
826
  FROM clicks
539
827
  WHERE link_id = ?
540
828
  GROUP BY referer
541
829
  ORDER BY clicks DESC
542
830
  LIMIT 10
543
- `).all(link.id);
544
- const topUserAgents = this.database.db.query(`
545
- SELECT user_agent, COUNT(*) AS clicks
831
+ `, link.id);
832
+ const topUserAgents = await this.pg.all(`
833
+ SELECT user_agent, COUNT(*)::int AS clicks
546
834
  FROM clicks
547
835
  WHERE link_id = ?
548
836
  GROUP BY user_agent
549
837
  ORDER BY clicks DESC
550
838
  LIMIT 10
551
- `).all(link.id);
839
+ `, link.id);
552
840
  return {
553
841
  link,
554
842
  clicks: summary.clicks,
555
- last_clicked_at: summary.last_clicked_at,
843
+ last_clicked_at: nullableIso(summary.last_clicked_at),
556
844
  top_referrers: topReferrers,
557
845
  top_user_agents: topUserAgents
558
846
  };
559
847
  }
560
- totalStats() {
561
- const row = this.database.db.query(`
562
- SELECT
563
- (SELECT COUNT(*) FROM domains) AS domains,
564
- (SELECT COUNT(*) FROM links) AS links,
565
- (SELECT COUNT(*) FROM clicks) AS clicks
566
- `).get();
567
- return row;
848
+ hashIp(ip) {
849
+ return createHash("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
568
850
  }
569
- generateAvailableSlug(domainId, length) {
851
+ async generateAvailableSlug(domainId, length) {
570
852
  for (let attempt = 0;attempt < 32; attempt += 1) {
571
853
  const slug = randomToken(length);
572
- const exists = this.database.db.query(`
854
+ const exists = await this.pg.get(`
573
855
  SELECT 1 FROM links WHERE domain_id = ? AND slug = ? LIMIT 1
574
- `).get(domainId, slug);
856
+ `, domainId, slug);
575
857
  if (!exists)
576
858
  return slug;
577
859
  }
578
860
  throw new Error("Could not generate an unused slug after 32 attempts.");
579
861
  }
580
- hashIp(ip) {
581
- 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();
582
907
  }
583
908
  }
584
- // src/pg-store.ts
909
+
910
+ // src/store.ts
585
911
  import { createHash as createHash2 } from "crypto";
586
912
  function parseJsonObject2(value) {
587
913
  if (!value)
588
914
  return {};
589
- if (typeof value === "object" && !Array.isArray(value))
590
- return value;
591
915
  try {
592
- const parsed = JSON.parse(String(value));
916
+ const parsed = JSON.parse(value);
593
917
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
594
918
  } catch {
595
919
  return {};
596
920
  }
597
921
  }
598
- function toIsoString(value) {
599
- if (value instanceof Date)
600
- return value.toISOString();
601
- return String(value);
602
- }
603
- function nullableIso(value) {
604
- if (value === null || value === undefined)
605
- return null;
606
- return toIsoString(value);
607
- }
608
922
  function domainFromRow2(row) {
609
923
  return {
610
924
  ...row,
611
925
  default_domain: Boolean(row.default_domain),
612
- synced_at: nullableIso(row.synced_at),
613
- created_at: toIsoString(row.created_at),
614
- updated_at: toIsoString(row.updated_at),
615
926
  metadata: parseJsonObject2(row.metadata)
616
927
  };
617
928
  }
618
929
  function linkFromRow2(row) {
930
+ const config = loadConfig();
931
+ const publicBaseUrl = config.defaultDomain === row.hostname ? config.publicBaseUrl : undefined;
619
932
  return {
620
933
  ...row,
621
934
  active: Boolean(row.active),
622
- expires_at: nullableIso(row.expires_at),
623
- synced_at: nullableIso(row.synced_at),
624
- created_at: toIsoString(row.created_at),
625
- updated_at: toIsoString(row.updated_at),
626
935
  metadata: parseJsonObject2(row.metadata),
627
- 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)
628
943
  };
629
944
  }
630
945
  function validateDestinationUrl2(url) {
@@ -647,43 +962,26 @@ function isoOrNull2(input) {
647
962
  throw new Error(`Invalid date: ${input}`);
648
963
  return date.toISOString();
649
964
  }
650
- function clickFromRow2(row) {
651
- return {
652
- ...row,
653
- clicked_at: toIsoString(row.clicked_at),
654
- synced_at: nullableIso(row.synced_at),
655
- created_at: toIsoString(row.created_at),
656
- updated_at: toIsoString(row.updated_at),
657
- metadata: parseJsonObject2(row.metadata)
658
- };
659
- }
660
965
 
661
- class PgShortlinksStore {
662
- pg;
663
- constructor(pg) {
664
- this.pg = pg;
665
- }
666
- static async fromConnectionString(connectionString) {
667
- const { PgAdapterAsync } = await import("@hasna/cloud");
668
- return new PgShortlinksStore(new PgAdapterAsync(connectionString));
669
- }
670
- static async fromCloud(service = "shortlinks") {
671
- const { getConnectionString } = await import("@hasna/cloud");
672
- return PgShortlinksStore.fromConnectionString(getConnectionString(service));
966
+ class ShortlinksStore {
967
+ database;
968
+ constructor(dbPath) {
969
+ this.database = new ShortlinksDatabase(dbPath);
673
970
  }
674
- async close() {
675
- await this.pg.close?.();
971
+ close() {
972
+ this.database.close();
676
973
  }
677
- async addDomain(input) {
974
+ addDomain(input) {
678
975
  const hostname2 = normalizeHostname(input.hostname);
679
976
  const timestamp = now();
680
977
  const machineId = getMachineId();
681
- const existing = await this.getDomain(hostname2);
978
+ const existing = this.getDomain(hostname2);
682
979
  const id = existing?.id || makeId("dom");
683
980
  if (input.defaultDomain) {
684
- 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}` });
685
983
  }
686
- await this.pg.run(`
984
+ this.database.db.query(`
687
985
  INSERT INTO domains (
688
986
  id, hostname, provider, default_domain, cloudflare_zone_id, cloudflare_account_id,
689
987
  cloudflare_worker_name, origin_url, notes, metadata, machine_id, synced_at, created_at, updated_at
@@ -701,31 +999,37 @@ class PgShortlinksStore {
701
999
  machine_id = excluded.machine_id,
702
1000
  synced_at = NULL,
703
1001
  updated_at = excluded.updated_at
704
- `, 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);
705
- 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);
706
1004
  }
707
- async listDomains() {
708
- const rows = await this.pg.all(`
1005
+ listDomains() {
1006
+ const rows = this.database.db.query(`
709
1007
  SELECT * FROM domains
710
1008
  ORDER BY default_domain DESC, hostname ASC
711
- `);
1009
+ `).all();
712
1010
  return rows.map(domainFromRow2);
713
1011
  }
714
- async getDomain(hostnameOrId) {
1012
+ getDomain(hostnameOrId) {
715
1013
  const normalized = hostnameOrId.includes(".") || hostnameOrId.includes("://") ? normalizeHostname(hostnameOrId) : hostnameOrId;
716
- const row = await this.pg.get(`
1014
+ const row = this.database.db.query(`
717
1015
  SELECT * FROM domains WHERE hostname = ? OR id = ? LIMIT 1
718
- `, normalized, hostnameOrId);
1016
+ `).get(normalized, hostnameOrId);
719
1017
  return row ? domainFromRow2(row) : null;
720
1018
  }
721
- async getDefaultDomain() {
722
- 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(`
723
1027
  SELECT * FROM domains ORDER BY default_domain DESC, created_at ASC LIMIT 1
724
- `);
1028
+ `).get();
725
1029
  return row ? domainFromRow2(row) : null;
726
1030
  }
727
- async createLink(input) {
728
- 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();
729
1033
  if (!domain) {
730
1034
  throw new Error("No domain configured. Run `shortlinks domain add <domain> --default` first.");
731
1035
  }
@@ -733,45 +1037,46 @@ class PgShortlinksStore {
733
1037
  const timestamp = now();
734
1038
  const machineId = getMachineId();
735
1039
  const expiresAt = isoOrNull2(input.expiresAt);
736
- 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);
737
1041
  try {
738
- await this.pg.run(`
1042
+ this.database.db.query(`
739
1043
  INSERT INTO links (
740
1044
  id, domain_id, slug, destination_url, title, active, expires_at, metadata,
741
1045
  machine_id, synced_at, created_at, updated_at
742
1046
  )
743
1047
  VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, NULL, ?, ?)
744
- `, 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);
745
1049
  } catch (error) {
746
1050
  const message = error instanceof Error ? error.message : String(error);
747
- if (message.includes("unique") || message.includes("duplicate")) {
1051
+ if (message.includes("UNIQUE")) {
748
1052
  throw new Error(`Slug already exists for ${domain.hostname}: ${slug}`);
749
1053
  }
750
1054
  throw error;
751
1055
  }
752
- return await this.getLink(domain.hostname, slug);
1056
+ return this.getLink(domain.hostname, slug);
753
1057
  }
754
- async listLinks(options = {}) {
1058
+ listLinks(options = {}) {
755
1059
  const params = [];
756
1060
  let where = "WHERE 1 = 1";
757
1061
  if (options.domain) {
758
1062
  where += " AND d.hostname = ?";
759
1063
  params.push(normalizeHostname(options.domain));
760
1064
  }
761
- if (options.activeOnly)
1065
+ if (options.activeOnly) {
762
1066
  where += " AND l.active = 1";
1067
+ }
763
1068
  params.push(options.limit || 100);
764
- const rows = await this.pg.all(`
1069
+ const rows = this.database.db.query(`
765
1070
  SELECT l.*, d.hostname
766
1071
  FROM links l
767
1072
  JOIN domains d ON d.id = l.domain_id
768
1073
  ${where}
769
1074
  ORDER BY l.created_at DESC
770
1075
  LIMIT ?
771
- `, ...params);
1076
+ `).all(...params);
772
1077
  return rows.map(linkFromRow2);
773
1078
  }
774
- async getLink(domainOrSlug, maybeSlug) {
1079
+ getLink(domainOrSlug, maybeSlug) {
775
1080
  const slug = normalizeSlug(maybeSlug || domainOrSlug);
776
1081
  const params = [slug];
777
1082
  let domainClause = "";
@@ -779,126 +1084,122 @@ class PgShortlinksStore {
779
1084
  domainClause = "AND d.hostname = ?";
780
1085
  params.push(normalizeHostname(domainOrSlug));
781
1086
  }
782
- const row = await this.pg.get(`
1087
+ const row = this.database.db.query(`
783
1088
  SELECT l.*, d.hostname
784
1089
  FROM links l
785
1090
  JOIN domains d ON d.id = l.domain_id
786
1091
  WHERE l.slug = ? ${domainClause}
787
1092
  ORDER BY d.default_domain DESC, l.created_at ASC
788
1093
  LIMIT 1
789
- `, ...params);
1094
+ `).get(...params);
790
1095
  return row ? linkFromRow2(row) : null;
791
1096
  }
792
- async totalStats() {
793
- const row = await this.pg.get(`
794
- SELECT
795
- (SELECT COUNT(*)::int FROM domains) AS domains,
796
- (SELECT COUNT(*)::int FROM links) AS links,
797
- (SELECT COUNT(*)::int FROM clicks) AS clicks
798
- `);
799
- return row;
800
- }
801
- async resolve(hostname2, slug) {
802
- const normalizedHost = normalizeHostname(hostname2);
1097
+ resolve(hostname2, slug) {
803
1098
  const normalizedSlug = normalizeSlug(slug);
804
- const row = await this.pg.get(`
1099
+ const normalizedHost = normalizeHostname(hostname2);
1100
+ const row = this.database.db.query(`
805
1101
  SELECT l.*, d.hostname
806
1102
  FROM links l
807
1103
  JOIN domains d ON d.id = l.domain_id
808
1104
  WHERE d.hostname = ? AND l.slug = ?
809
1105
  LIMIT 1
810
- `, normalizedHost, normalizedSlug);
1106
+ `).get(normalizedHost, normalizedSlug);
811
1107
  if (row)
812
1108
  return linkFromRow2(row);
813
- const fallback = await this.pg.get(`
814
- SELECT l.*, d.hostname
815
- FROM links l
816
- JOIN domains d ON d.id = l.domain_id
817
- WHERE d.default_domain = 1 AND l.slug = ?
818
- ORDER BY d.created_at ASC
819
- LIMIT 1
820
- `, normalizedSlug);
821
- 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);
822
1113
  }
823
- async setLinkActive(domainOrSlug, maybeSlugOrActive, maybeActive) {
1114
+ setLinkActive(domainOrSlug, maybeSlugOrActive, maybeActive) {
824
1115
  const active = typeof maybeSlugOrActive === "boolean" ? maybeSlugOrActive : Boolean(maybeActive);
825
- 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);
826
1117
  if (!link)
827
1118
  throw new Error("Link not found.");
828
1119
  const timestamp = now();
829
- await this.pg.run(`
1120
+ this.database.db.query(`
830
1121
  UPDATE links SET active = ?, updated_at = ?, synced_at = NULL WHERE id = ?
831
- `, active ? 1 : 0, timestamp, link.id);
832
- return await this.getLink(link.hostname, link.slug);
1122
+ `).run(active ? 1 : 0, timestamp, link.id);
1123
+ return this.getLink(link.hostname, link.slug);
833
1124
  }
834
- async deleteLink(domainOrSlug, maybeSlug) {
835
- 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);
836
1127
  if (!link)
837
1128
  throw new Error("Link not found.");
838
- await this.pg.run("DELETE FROM links WHERE id = ?", link.id);
1129
+ this.database.db.query("DELETE FROM links WHERE id = ?").run(link.id);
839
1130
  return link;
840
1131
  }
841
- async recordClick(link, input = {}) {
1132
+ recordClick(link, input = {}) {
842
1133
  const timestamp = now();
843
1134
  const machineId = getMachineId();
844
1135
  const ipHash = input.ip ? this.hashIp(input.ip) : null;
845
1136
  const id = makeId("clk");
846
- await this.pg.run(`
1137
+ this.database.db.query(`
847
1138
  INSERT INTO clicks (
848
1139
  id, link_id, domain_id, slug, clicked_at, ip_hash, user_agent, referer,
849
1140
  country, city, metadata, machine_id, synced_at, created_at, updated_at
850
1141
  )
851
1142
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
852
- `, 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);
853
- 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);
854
1145
  return clickFromRow2(row);
855
1146
  }
856
- async getStats(domainOrSlug, maybeSlug) {
857
- 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);
858
1149
  if (!link)
859
1150
  throw new Error("Link not found.");
860
- const summary = await this.pg.get(`
861
- SELECT COUNT(*)::int AS clicks, MAX(clicked_at) AS last_clicked_at FROM clicks WHERE link_id = ?
862
- `, link.id);
863
- const topReferrers = await this.pg.all(`
864
- 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
865
1156
  FROM clicks
866
1157
  WHERE link_id = ?
867
1158
  GROUP BY referer
868
1159
  ORDER BY clicks DESC
869
1160
  LIMIT 10
870
- `, link.id);
871
- const topUserAgents = await this.pg.all(`
872
- 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
873
1164
  FROM clicks
874
1165
  WHERE link_id = ?
875
1166
  GROUP BY user_agent
876
1167
  ORDER BY clicks DESC
877
1168
  LIMIT 10
878
- `, link.id);
1169
+ `).all(link.id);
879
1170
  return {
880
1171
  link,
881
1172
  clicks: summary.clicks,
882
- last_clicked_at: nullableIso(summary.last_clicked_at),
1173
+ last_clicked_at: summary.last_clicked_at,
883
1174
  top_referrers: topReferrers,
884
1175
  top_user_agents: topUserAgents
885
1176
  };
886
1177
  }
887
- hashIp(ip) {
888
- 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;
889
1186
  }
890
- async generateAvailableSlug(domainId, length) {
1187
+ generateAvailableSlug(domainId, length) {
891
1188
  for (let attempt = 0;attempt < 32; attempt += 1) {
892
1189
  const slug = randomToken(length);
893
- const exists = await this.pg.get(`
1190
+ const exists = this.database.db.query(`
894
1191
  SELECT 1 FROM links WHERE domain_id = ? AND slug = ? LIMIT 1
895
- `, domainId, slug);
1192
+ `).get(domainId, slug);
896
1193
  if (!exists)
897
1194
  return slug;
898
1195
  }
899
1196
  throw new Error("Could not generate an unused slug after 32 attempts.");
900
1197
  }
1198
+ hashIp(ip) {
1199
+ return createHash2("sha256").update(`${getClickSalt()}:${ip}`).digest("hex");
1200
+ }
901
1201
  }
1202
+
902
1203
  // src/server.ts
903
1204
  var REDIRECT_ALLOW_HEADER = "GET, HEAD";
904
1205
  function json(data, status = 200, headers) {
@@ -996,6 +1297,7 @@ function serveShortlinks(options = {}) {
996
1297
  const fetch2 = createShortlinksHandler(options);
997
1298
  return Bun.serve({ hostname: host, port, fetch: fetch2 });
998
1299
  }
1300
+
999
1301
  // src/cloudflare.ts
1000
1302
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
1001
1303
  import { join as join3 } from "path";
@@ -1131,6 +1433,2591 @@ async function upsertCloudflareDnsRecord(options) {
1131
1433
  });
1132
1434
  return { id: created.id, action: "created" };
1133
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
+ }
1134
4021
  // src/local.ts
1135
4022
  import { spawnSync } from "child_process";
1136
4023
  import { homedir as homedir2 } from "os";
@@ -1249,23 +4136,43 @@ export {
1249
4136
  serveShortlinks,
1250
4137
  saveConfig,
1251
4138
  registerMachinesDns,
4139
+ redactDatabaseUrl,
1252
4140
  randomToken,
4141
+ parseShortlinksStoreMode,
1253
4142
  now,
1254
4143
  normalizeSlug,
1255
4144
  normalizeHostname,
1256
4145
  makeId,
4146
+ loadShortlinksRuntimeConfig,
1257
4147
  loadConfig,
4148
+ getShortlinksStoreMode,
4149
+ getShortlinksRuntimeStatus,
4150
+ getShortlinksRuntimeEnvName,
4151
+ getShortlinksDatabaseUrl,
4152
+ getShortlinksDatabaseSsl,
1258
4153
  getDatabasePath,
1259
4154
  getDataDir,
1260
4155
  getConfigPath,
4156
+ getCanonicalShortlinksPostgresConfig,
1261
4157
  generateWorkerScript,
1262
4158
  formatShortUrl,
1263
4159
  createShortlinksHandler,
4160
+ createServeApp,
1264
4161
  createLocalSetupPlan,
4162
+ createKitPgAdapter,
1265
4163
  createCloudflarePlan,
4164
+ buildOpenApiDocument,
4165
+ assertShortlinksPostgresConfig,
4166
+ applyPostgresMigrations,
1266
4167
  ShortlinksStore,
1267
4168
  ShortlinksDatabase,
1268
4169
  SQLITE_MIGRATIONS,
4170
+ SHORTLINKS_RUNTIME_FALLBACK_ENV,
4171
+ SHORTLINKS_RUNTIME_ENV,
4172
+ SHORTLINKS_MIGRATIONS,
1269
4173
  PgShortlinksStore,
1270
- PG_MIGRATIONS
4174
+ PG_MIGRATIONS,
4175
+ CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH,
4176
+ CANONICAL_SHORTLINKS_POSTGRES_DATABASE,
4177
+ CANONICAL_SHORTLINKS_POSTGRES_CLUSTER
1271
4178
  };