@hasna/shortlinks 0.1.23 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import type { TypedQueryClient } from "./generated/storage-kit/query.js";
1
2
  import type { AddDomainInput, Click, ClickInput, CreateLinkInput, Domain, Link, LinkStats } from "./types.js";
2
3
  type PgAdapterLike = {
3
4
  get(sql: string, ...params: unknown[]): Promise<any>;
@@ -8,10 +9,19 @@ type PgAdapterLike = {
8
9
  export interface PgConnectionOptions {
9
10
  ssl?: boolean;
10
11
  }
12
+ /**
13
+ * Adapt a vendored storage-kit `TypedQueryClient` (which speaks `$1` positional
14
+ * params) to the `?`-placeholder `PgAdapterLike` the store queries are written
15
+ * against. This lets `shortlinks-serve` reuse the exact same SQL as the CLI while
16
+ * opening its cloud pool through the sanctioned kit (`createCloudPoolFromEnv`).
17
+ */
18
+ export declare function createKitPgAdapter(client: TypedQueryClient): PgAdapterLike;
11
19
  export declare class PgShortlinksStore {
12
20
  private readonly pg;
13
21
  constructor(pg: PgAdapterLike);
14
22
  static fromConnectionString(connectionString: string, options?: PgConnectionOptions): Promise<PgShortlinksStore>;
23
+ /** Build a store over a vendored storage-kit query client (the serve path). */
24
+ static fromQueryClient(client: TypedQueryClient): PgShortlinksStore;
15
25
  static fromEnv(env?: NodeJS.ProcessEnv): Promise<PgShortlinksStore>;
16
26
  close(): Promise<void>;
17
27
  addDomain(input: AddDomainInput): Promise<Domain>;
package/dist/pg-store.js CHANGED
@@ -1,4 +1,201 @@
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);
33
+ var __require = import.meta.require;
34
+
35
+ // src/runtime.ts
36
+ var SHORTLINKS_RUNTIME_ENV = {
37
+ store: "HASNA_SHORTLINKS_STORE",
38
+ databaseUrl: "HASNA_SHORTLINKS_DATABASE_URL",
39
+ databaseSsl: "HASNA_SHORTLINKS_DATABASE_SSL"
40
+ };
41
+ var SHORTLINKS_RUNTIME_FALLBACK_ENV = {
42
+ store: "SHORTLINKS_STORE",
43
+ databaseUrl: "SHORTLINKS_DATABASE_URL",
44
+ databaseSsl: "SHORTLINKS_DATABASE_SSL"
45
+ };
46
+ var CANONICAL_SHORTLINKS_POSTGRES_CLUSTER = "hasna-xyz-infra-apps-prod-postgres";
47
+ var CANONICAL_SHORTLINKS_POSTGRES_DATABASE = "shortlinks";
48
+ var CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH = "hasna/xyz/opensource/shortlinks/prod/postgres";
49
+ function getCanonicalShortlinksPostgresConfig() {
50
+ return {
51
+ cluster: CANONICAL_SHORTLINKS_POSTGRES_CLUSTER,
52
+ database: CANONICAL_SHORTLINKS_POSTGRES_DATABASE,
53
+ runtimeSecretPath: CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH,
54
+ primaryEnv: SHORTLINKS_RUNTIME_ENV.databaseUrl,
55
+ fallbackEnv: SHORTLINKS_RUNTIME_FALLBACK_ENV.databaseUrl
56
+ };
57
+ }
58
+ function parseShortlinksStoreMode(value) {
59
+ const normalized = clean(value)?.toLowerCase();
60
+ if (!normalized)
61
+ return "local";
62
+ if (normalized === "local" || normalized === "postgres")
63
+ return normalized;
64
+ if (normalized === "pg")
65
+ return "postgres";
66
+ throw new Error(`${SHORTLINKS_RUNTIME_ENV.store} must be local or postgres`);
67
+ }
68
+ function getShortlinksStoreMode(env = process.env) {
69
+ return parseShortlinksStoreMode(readRuntimeEnv(env, "store").value);
70
+ }
71
+ function getShortlinksDatabaseUrl(env = process.env) {
72
+ return readRuntimeEnv(env, "databaseUrl").value;
73
+ }
74
+ function getShortlinksDatabaseSsl(env = process.env) {
75
+ return parseBoolean(readRuntimeEnv(env, "databaseSsl").value, true);
76
+ }
77
+ function getShortlinksRuntimeEnvName(env, key) {
78
+ const primary = SHORTLINKS_RUNTIME_ENV[key];
79
+ if (clean(env[primary]))
80
+ return primary;
81
+ return SHORTLINKS_RUNTIME_FALLBACK_ENV[key];
82
+ }
83
+ function loadShortlinksRuntimeConfig(env = process.env) {
84
+ const mode = getShortlinksStoreMode(env);
85
+ const databaseUrl = getShortlinksDatabaseUrl(env);
86
+ return {
87
+ service: "shortlinks",
88
+ mode,
89
+ ...databaseUrl ? {
90
+ database: {
91
+ provider: "postgres",
92
+ url: databaseUrl,
93
+ ssl: getShortlinksDatabaseSsl(env)
94
+ }
95
+ } : {}
96
+ };
97
+ }
98
+ function assertShortlinksPostgresConfig(config) {
99
+ if (config.mode !== "postgres")
100
+ return;
101
+ if (!config.database?.url) {
102
+ throw new Error(`${SHORTLINKS_RUNTIME_ENV.databaseUrl} is required when ${SHORTLINKS_RUNTIME_ENV.store}=postgres`);
103
+ }
104
+ }
105
+ function getShortlinksRuntimeStatus(env = process.env) {
106
+ const issues = [];
107
+ const warnings = [];
108
+ let config;
109
+ try {
110
+ config = loadShortlinksRuntimeConfig(env);
111
+ } catch (error) {
112
+ issues.push(error instanceof Error ? error.message : String(error));
113
+ config = { service: "shortlinks", mode: "local" };
114
+ }
115
+ try {
116
+ assertShortlinksPostgresConfig(config);
117
+ } catch (error) {
118
+ issues.push(error instanceof Error ? error.message : String(error));
119
+ }
120
+ if (config.mode === "local" && config.database?.url) {
121
+ warnings.push(`${SHORTLINKS_RUNTIME_ENV.store}=local ignores configured Postgres database settings`);
122
+ }
123
+ return {
124
+ ok: issues.length === 0,
125
+ service: "shortlinks",
126
+ mode: config.mode,
127
+ local_default: config.mode === "local",
128
+ postgres_enabled: config.mode === "postgres",
129
+ database: {
130
+ configured: Boolean(config.database?.url),
131
+ provider: config.database?.provider ?? null,
132
+ redacted_url: redactDatabaseUrl(config.database?.url),
133
+ ssl: config.database?.ssl ?? null
134
+ },
135
+ env: runtimeEnvStatus(env),
136
+ canonical: getCanonicalShortlinksPostgresConfig(),
137
+ issues,
138
+ warnings,
139
+ no_network: true
140
+ };
141
+ }
142
+ function redactDatabaseUrl(value) {
143
+ if (!value)
144
+ return null;
145
+ try {
146
+ const url = new URL(value);
147
+ if (url.username)
148
+ url.username = "***";
149
+ if (url.password)
150
+ url.password = "***";
151
+ for (const key of Array.from(url.searchParams.keys())) {
152
+ if (isSensitiveQueryKey(key))
153
+ url.searchParams.set(key, "***");
154
+ }
155
+ return url.toString();
156
+ } catch {
157
+ return "(redacted)";
158
+ }
159
+ }
160
+ function runtimeEnvStatus(env) {
161
+ return Object.fromEntries(Object.entries(SHORTLINKS_RUNTIME_ENV).map(([key, name]) => {
162
+ const activeName = getShortlinksRuntimeEnvName(env, key);
163
+ return [
164
+ key,
165
+ {
166
+ name,
167
+ active_name: activeName,
168
+ configured: Boolean(clean(env[activeName]))
169
+ }
170
+ ];
171
+ }));
172
+ }
173
+ function readRuntimeEnv(env, key) {
174
+ const primary = SHORTLINKS_RUNTIME_ENV[key];
175
+ const primaryValue = clean(env[primary]);
176
+ if (primaryValue)
177
+ return { name: primary, value: primaryValue };
178
+ const fallback = SHORTLINKS_RUNTIME_FALLBACK_ENV[key];
179
+ return { name: fallback, value: clean(env[fallback]) };
180
+ }
181
+ function parseBoolean(value, fallback) {
182
+ const normalized = clean(value)?.toLowerCase();
183
+ if (!normalized)
184
+ return fallback;
185
+ if (["1", "true", "yes", "on"].includes(normalized))
186
+ return true;
187
+ if (["0", "false", "no", "off"].includes(normalized))
188
+ return false;
189
+ throw new Error(`${SHORTLINKS_RUNTIME_ENV.databaseSsl} must be true or false`);
190
+ }
191
+ function isSensitiveQueryKey(key) {
192
+ return /(?:^|[_-])(pass(?:word)?|pwd|secret|token|credential|auth|api[_-]?key|access[_-]?key)(?:$|[_-])/i.test(key);
193
+ }
194
+ function clean(value) {
195
+ const trimmed = value?.trim();
196
+ return trimmed ? trimmed : undefined;
197
+ }
198
+
2
199
  // src/pg-store.ts
3
200
  import { createHash } from "crypto";
4
201
 
@@ -17,9 +214,19 @@ function ensureDataDir() {
17
214
  mkdirSync(dir, { recursive: true });
18
215
  return dir;
19
216
  }
217
+ function getConfigPath() {
218
+ return join(ensureDataDir(), "config.json");
219
+ }
20
220
  function getClickSaltPath() {
21
221
  return join(ensureDataDir(), "click-salt");
22
222
  }
223
+ function getDatabasePath(explicitPath) {
224
+ if (explicitPath)
225
+ return resolve(explicitPath);
226
+ if (process.env.SHORTLINKS_DB)
227
+ return resolve(process.env.SHORTLINKS_DB);
228
+ return join(ensureDataDir(), `${SERVICE_NAME}.db`);
229
+ }
23
230
  function readClickSaltFile(path) {
24
231
  try {
25
232
  const saved = readFileSync(path, "utf-8").trim();
@@ -65,6 +272,35 @@ function getClickSalt() {
65
272
  throw clickSaltError(path, error);
66
273
  }
67
274
  }
275
+ function loadConfig() {
276
+ const path = getConfigPath();
277
+ if (!existsSync(path))
278
+ return {};
279
+ try {
280
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
281
+ return parsed && typeof parsed === "object" ? parsed : {};
282
+ } catch {
283
+ return {};
284
+ }
285
+ }
286
+ function saveConfig(config) {
287
+ const path = getConfigPath();
288
+ mkdirSync(dirname(path), { recursive: true });
289
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}
290
+ `);
291
+ }
292
+ function updateConfig(patch) {
293
+ const next = {
294
+ ...loadConfig(),
295
+ ...patch,
296
+ cloudflare: {
297
+ ...loadConfig().cloudflare,
298
+ ...patch.cloudflare
299
+ }
300
+ };
301
+ saveConfig(next);
302
+ return next;
303
+ }
68
304
  function normalizeHostname(input) {
69
305
  const raw = input.trim().toLowerCase();
70
306
  if (!raw)
@@ -93,6 +329,9 @@ function formatShortUrl(hostname, slug, publicBaseUrl) {
93
329
  }
94
330
 
95
331
  // src/database.ts
332
+ import { Database } from "bun:sqlite";
333
+ import { mkdirSync as mkdirSync2 } from "fs";
334
+ import { dirname as dirname2 } from "path";
96
335
  function now() {
97
336
  return new Date().toISOString();
98
337
  }
@@ -101,6 +340,105 @@ function makeId(prefix) {
101
340
  const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
102
341
  return `${prefix}_${hex}`;
103
342
  }
343
+ var SQLITE_MIGRATIONS = [
344
+ `
345
+ CREATE TABLE IF NOT EXISTS domains (
346
+ id TEXT PRIMARY KEY,
347
+ hostname TEXT NOT NULL UNIQUE,
348
+ provider TEXT NOT NULL DEFAULT 'manual',
349
+ default_domain INTEGER NOT NULL DEFAULT 0,
350
+ cloudflare_zone_id TEXT,
351
+ cloudflare_account_id TEXT,
352
+ cloudflare_worker_name TEXT,
353
+ origin_url TEXT,
354
+ notes TEXT,
355
+ metadata TEXT NOT NULL DEFAULT '{}',
356
+ machine_id TEXT,
357
+ synced_at TEXT,
358
+ created_at TEXT NOT NULL,
359
+ updated_at TEXT NOT NULL
360
+ );
361
+
362
+ CREATE TABLE IF NOT EXISTS links (
363
+ id TEXT PRIMARY KEY,
364
+ domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
365
+ slug TEXT NOT NULL,
366
+ destination_url TEXT NOT NULL,
367
+ title TEXT,
368
+ active INTEGER NOT NULL DEFAULT 1,
369
+ expires_at TEXT,
370
+ metadata TEXT NOT NULL DEFAULT '{}',
371
+ machine_id TEXT,
372
+ synced_at TEXT,
373
+ created_at TEXT NOT NULL,
374
+ updated_at TEXT NOT NULL,
375
+ UNIQUE(domain_id, slug)
376
+ );
377
+
378
+ CREATE TABLE IF NOT EXISTS clicks (
379
+ id TEXT PRIMARY KEY,
380
+ link_id TEXT NOT NULL REFERENCES links(id) ON DELETE CASCADE,
381
+ domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
382
+ slug TEXT NOT NULL,
383
+ clicked_at TEXT NOT NULL,
384
+ ip_hash TEXT,
385
+ user_agent TEXT,
386
+ referer TEXT,
387
+ country TEXT,
388
+ city TEXT,
389
+ metadata TEXT NOT NULL DEFAULT '{}',
390
+ machine_id TEXT,
391
+ synced_at TEXT,
392
+ created_at TEXT NOT NULL,
393
+ updated_at TEXT NOT NULL
394
+ );
395
+
396
+ CREATE INDEX IF NOT EXISTS idx_domains_hostname ON domains(hostname);
397
+ CREATE INDEX IF NOT EXISTS idx_domains_default ON domains(default_domain);
398
+ CREATE INDEX IF NOT EXISTS idx_links_domain_slug ON links(domain_id, slug);
399
+ CREATE INDEX IF NOT EXISTS idx_links_active ON links(active);
400
+ CREATE INDEX IF NOT EXISTS idx_links_updated ON links(updated_at);
401
+ CREATE INDEX IF NOT EXISTS idx_clicks_link ON clicks(link_id);
402
+ CREATE INDEX IF NOT EXISTS idx_clicks_domain ON clicks(domain_id);
403
+ CREATE INDEX IF NOT EXISTS idx_clicks_clicked_at ON clicks(clicked_at);
404
+ CREATE INDEX IF NOT EXISTS idx_clicks_updated ON clicks(updated_at);
405
+ `
406
+ ];
407
+
408
+ class ShortlinksDatabase {
409
+ db;
410
+ path;
411
+ constructor(path) {
412
+ this.path = getDatabasePath(path);
413
+ mkdirSync2(dirname2(this.path), { recursive: true });
414
+ this.db = new Database(this.path);
415
+ this.db.exec("PRAGMA foreign_keys = ON;");
416
+ this.applyMigrations();
417
+ }
418
+ close() {
419
+ this.db.close();
420
+ }
421
+ applyMigrations() {
422
+ this.db.exec(`
423
+ CREATE TABLE IF NOT EXISTS _migrations (
424
+ id INTEGER PRIMARY KEY,
425
+ applied_at TEXT NOT NULL
426
+ );
427
+ `);
428
+ for (let i = 0;i < SQLITE_MIGRATIONS.length; i += 1) {
429
+ const id = i + 1;
430
+ const applied = this.db.query("SELECT id FROM _migrations WHERE id = ?").get(id);
431
+ if (applied)
432
+ continue;
433
+ const migration = SQLITE_MIGRATIONS[i];
434
+ const apply = this.db.transaction(() => {
435
+ this.db.exec(migration);
436
+ this.db.query("INSERT INTO _migrations (id, applied_at) VALUES (?, ?)").run(id, now());
437
+ });
438
+ apply();
439
+ }
440
+ }
441
+ }
104
442
 
105
443
  // src/machine.ts
106
444
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -146,46 +484,6 @@ function getMachineId() {
146
484
  return id;
147
485
  }
148
486
 
149
- // src/runtime.ts
150
- var SHORTLINKS_RUNTIME_ENV = {
151
- store: "HASNA_SHORTLINKS_STORE",
152
- databaseUrl: "HASNA_SHORTLINKS_DATABASE_URL",
153
- databaseSsl: "HASNA_SHORTLINKS_DATABASE_SSL"
154
- };
155
- var SHORTLINKS_RUNTIME_FALLBACK_ENV = {
156
- store: "SHORTLINKS_STORE",
157
- databaseUrl: "SHORTLINKS_DATABASE_URL",
158
- databaseSsl: "SHORTLINKS_DATABASE_SSL"
159
- };
160
- function getShortlinksDatabaseUrl(env = process.env) {
161
- return readRuntimeEnv(env, "databaseUrl").value;
162
- }
163
- function getShortlinksDatabaseSsl(env = process.env) {
164
- return parseBoolean(readRuntimeEnv(env, "databaseSsl").value, true);
165
- }
166
- function readRuntimeEnv(env, key) {
167
- const primary = SHORTLINKS_RUNTIME_ENV[key];
168
- const primaryValue = clean(env[primary]);
169
- if (primaryValue)
170
- return { name: primary, value: primaryValue };
171
- const fallback = SHORTLINKS_RUNTIME_FALLBACK_ENV[key];
172
- return { name: fallback, value: clean(env[fallback]) };
173
- }
174
- function parseBoolean(value, fallback) {
175
- const normalized = clean(value)?.toLowerCase();
176
- if (!normalized)
177
- return fallback;
178
- if (["1", "true", "yes", "on"].includes(normalized))
179
- return true;
180
- if (["0", "false", "no", "off"].includes(normalized))
181
- return false;
182
- throw new Error(`${SHORTLINKS_RUNTIME_ENV.databaseSsl} must be true or false`);
183
- }
184
- function clean(value) {
185
- const trimmed = value?.trim();
186
- return trimmed ? trimmed : undefined;
187
- }
188
-
189
487
  // src/pg-store.ts
190
488
  function parseJsonObject(value) {
191
489
  if (!value)
@@ -302,6 +600,19 @@ function clickFromRow(row) {
302
600
  metadata: parseJsonObject(row.metadata)
303
601
  };
304
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
+ }
305
616
 
306
617
  class PgShortlinksStore {
307
618
  pg;
@@ -311,6 +622,9 @@ class PgShortlinksStore {
311
622
  static async fromConnectionString(connectionString, options = {}) {
312
623
  return new PgShortlinksStore(await PgPoolAdapter.create(connectionString, options));
313
624
  }
625
+ static fromQueryClient(client) {
626
+ return new PgShortlinksStore(createKitPgAdapter(client));
627
+ }
314
628
  static async fromEnv(env = process.env) {
315
629
  const connectionString = getShortlinksDatabaseUrl(env);
316
630
  if (!connectionString) {
@@ -593,6 +907,7 @@ async function applyPostgresMigrations(connectionString, migrations, options = {
593
907
  }
594
908
  }
595
909
  export {
910
+ createKitPgAdapter,
596
911
  applyPostgresMigrations,
597
912
  PgShortlinksStore
598
913
  };
package/dist/runtime.js CHANGED
@@ -1,4 +1,37 @@
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);
33
+ var __require = import.meta.require;
34
+
2
35
  // src/runtime.ts
3
36
  var SHORTLINKS_RUNTIME_ENV = {
4
37
  store: "HASNA_SHORTLINKS_STORE",
@@ -0,0 +1,178 @@
1
+ export interface Link {
2
+ "id": string;
3
+ "domain_id": string;
4
+ "hostname": string;
5
+ "slug": string;
6
+ "destination_url": string;
7
+ "title"?: string | null;
8
+ "active": boolean;
9
+ "expires_at"?: string | null;
10
+ "short_url"?: string;
11
+ "metadata"?: Record<string, unknown>;
12
+ "created_at": string;
13
+ "updated_at"?: string;
14
+ }
15
+ export interface Domain {
16
+ "id": string;
17
+ "hostname": string;
18
+ "provider": string;
19
+ "default_domain": boolean;
20
+ "origin_url"?: string | null;
21
+ "notes"?: string | null;
22
+ "metadata"?: Record<string, unknown>;
23
+ "created_at": string;
24
+ "updated_at"?: string;
25
+ }
26
+ export interface LinkStats {
27
+ "link": Link;
28
+ "clicks": number;
29
+ "last_clicked_at"?: string | null;
30
+ "top_referrers"?: Array<{
31
+ "referer"?: string | null;
32
+ "clicks"?: number;
33
+ }>;
34
+ "top_user_agents"?: Array<{
35
+ "user_agent"?: string | null;
36
+ "clicks"?: number;
37
+ }>;
38
+ }
39
+ export type LinkList = Array<{
40
+ "id": string;
41
+ "domain_id": string;
42
+ "hostname": string;
43
+ "slug": string;
44
+ "destination_url": string;
45
+ "title"?: string | null;
46
+ "active": boolean;
47
+ "expires_at"?: string | null;
48
+ "short_url"?: string;
49
+ "metadata"?: Record<string, unknown>;
50
+ "created_at": string;
51
+ "updated_at"?: string;
52
+ }>;
53
+ export type DomainList = Array<{
54
+ "id": string;
55
+ "hostname": string;
56
+ "provider": string;
57
+ "default_domain": boolean;
58
+ "origin_url"?: string | null;
59
+ "notes"?: string | null;
60
+ "metadata"?: Record<string, unknown>;
61
+ "created_at": string;
62
+ "updated_at"?: string;
63
+ }>;
64
+ export interface TotalStats {
65
+ "domains": number;
66
+ "links": number;
67
+ "clicks": number;
68
+ }
69
+ export interface CreateLinkRequest {
70
+ "url": string;
71
+ "domain"?: string;
72
+ "slug"?: string;
73
+ "title"?: string;
74
+ "expires_at"?: string;
75
+ "length"?: number;
76
+ "metadata"?: Record<string, unknown>;
77
+ }
78
+ export interface AddDomainRequest {
79
+ "hostname": string;
80
+ "provider"?: string;
81
+ "default"?: boolean;
82
+ "origin_url"?: string;
83
+ "notes"?: string;
84
+ "metadata"?: Record<string, unknown>;
85
+ }
86
+ export interface DeleteResponse {
87
+ "deleted": boolean;
88
+ "slug"?: string;
89
+ }
90
+ export interface HealthStatus {
91
+ "status": string;
92
+ "version": string;
93
+ "mode": string;
94
+ "db_latency_ms"?: number;
95
+ }
96
+ export interface ReadyStatus {
97
+ "status": string;
98
+ "version": string;
99
+ "mode": string;
100
+ "pending_migrations"?: Array<string>;
101
+ }
102
+ export interface VersionInfo {
103
+ "status": string;
104
+ "version": string;
105
+ "mode": string;
106
+ "name"?: string;
107
+ }
108
+ export interface ErrorResponse {
109
+ "error": string;
110
+ "reason"?: string;
111
+ }
112
+ export interface ShortlinksApiClientOptions {
113
+ /** Base URL, e.g. process.env.APP_API_URL. */
114
+ baseUrl: string;
115
+ /** API key, e.g. process.env.APP_API_KEY. Sent as the 'x-api-key' header. */
116
+ apiKey?: string;
117
+ /** Custom fetch (defaults to global fetch). */
118
+ fetch?: typeof fetch;
119
+ /** Extra headers merged into every request. */
120
+ headers?: Record<string, string>;
121
+ }
122
+ export declare class ApiError extends Error {
123
+ readonly status: number;
124
+ readonly body: unknown;
125
+ constructor(status: number, message: string, body: unknown);
126
+ }
127
+ export declare class ShortlinksApiClient {
128
+ private readonly baseUrl;
129
+ private readonly apiKey;
130
+ private readonly fetchImpl;
131
+ private readonly baseHeaders;
132
+ constructor(options: ShortlinksApiClientOptions);
133
+ private request;
134
+ /** Liveness probe. */
135
+ getHealth(init?: RequestInit): Promise<HealthStatus>;
136
+ /** Readiness probe (DB reachable and schema migrated). */
137
+ getReady(init?: RequestInit): Promise<ReadyStatus>;
138
+ /** List configured domains. */
139
+ listDomains(init?: RequestInit): Promise<DomainList>;
140
+ /** Add or update a domain. */
141
+ addDomain(body: AddDomainRequest, init?: RequestInit): Promise<Domain>;
142
+ /** List shortlinks. */
143
+ listLinks(query?: {
144
+ "domain"?: string;
145
+ "active"?: boolean;
146
+ "limit"?: number;
147
+ }, init?: RequestInit): Promise<LinkList>;
148
+ /** Create a shortlink. */
149
+ createLink(body: CreateLinkRequest, init?: RequestInit): Promise<Link>;
150
+ /** Get a shortlink by slug. */
151
+ getLink(slug: string, query?: {
152
+ "domain"?: string;
153
+ }, init?: RequestInit): Promise<Link>;
154
+ /** Delete a shortlink. */
155
+ deleteLink(slug: string, query?: {
156
+ "domain"?: string;
157
+ }, init?: RequestInit): Promise<DeleteResponse>;
158
+ /** Disable a shortlink. */
159
+ disableLink(slug: string, query?: {
160
+ "domain"?: string;
161
+ }, init?: RequestInit): Promise<Link>;
162
+ /** Enable a shortlink. */
163
+ enableLink(slug: string, query?: {
164
+ "domain"?: string;
165
+ }, init?: RequestInit): Promise<Link>;
166
+ /** Click stats for a shortlink. */
167
+ getLinkStats(slug: string, query?: {
168
+ "domain"?: string;
169
+ }, init?: RequestInit): Promise<LinkStats>;
170
+ /** Resolve a slug to its destination without recording a click. */
171
+ resolveLink(slug: string, query?: {
172
+ "domain"?: string;
173
+ }, init?: RequestInit): Promise<Link>;
174
+ /** Total domains/links/clicks counts. */
175
+ getStats(init?: RequestInit): Promise<TotalStats>;
176
+ /** Service version and mode. */
177
+ getVersion(init?: RequestInit): Promise<VersionInfo>;
178
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @hasna/shortlinks/sdk — typed client for the shortlinks REST API.
3
+ *
4
+ * The generated client (`ShortlinksApiClient`) is produced from the serve
5
+ * OpenAPI document (src/serve/openapi.ts) by `bun run sdk:generate`. It is the
6
+ * canonical client for the versioned `/v1` API with API-key auth.
7
+ *
8
+ * Client self_hosted mode reads `SHORTLINKS_API_URL` + `SHORTLINKS_API_KEY`
9
+ * (never a DSN).
10
+ */
11
+ export * from "./generated.js";
12
+ export { buildOpenApiDocument } from "../serve/openapi.js";