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