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