@hasna/shortlinks 0.1.23 → 0.2.0

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>;
@@ -5,14 +6,24 @@ type PgAdapterLike = {
5
6
  run(sql: string, ...params: unknown[]): Promise<unknown>;
6
7
  close?: () => Promise<void>;
7
8
  };
8
- export interface PgConnectionOptions {
9
- ssl?: boolean;
10
- }
9
+ /**
10
+ * Adapt a vendored storage-kit `TypedQueryClient` (which speaks `$1` positional
11
+ * params) to the `?`-placeholder `PgAdapterLike` the store queries are written
12
+ * against. This lets `shortlinks-serve` reuse the exact same SQL as the CLI while
13
+ * opening its cloud pool through the sanctioned kit (`createCloudPoolFromEnv`).
14
+ */
15
+ export declare function createKitPgAdapter(client: TypedQueryClient): PgAdapterLike;
11
16
  export declare class PgShortlinksStore {
12
17
  private readonly pg;
13
18
  constructor(pg: PgAdapterLike);
14
- static fromConnectionString(connectionString: string, options?: PgConnectionOptions): Promise<PgShortlinksStore>;
15
- static fromEnv(env?: NodeJS.ProcessEnv): Promise<PgShortlinksStore>;
19
+ /**
20
+ * Build a store over a vendored storage-kit query client. This is the ONLY
21
+ * constructor: the serve entrypoint opens its pool via `createCloudPoolFromEnv`
22
+ * (server-side) and hands the client here. There is deliberately no
23
+ * DSN-from-env / connection-string path so this store can never be misused to
24
+ * open the raw RDS from a client.
25
+ */
26
+ static fromQueryClient(client: TypedQueryClient): PgShortlinksStore;
16
27
  close(): Promise<void>;
17
28
  addDomain(input: AddDomainInput): Promise<Domain>;
18
29
  listDomains(): Promise<Domain[]>;
@@ -38,9 +49,4 @@ export declare class PgShortlinksStore {
38
49
  private hashIp;
39
50
  private generateAvailableSlug;
40
51
  }
41
- export declare function applyPostgresMigrations(connectionString: string, migrations: string[], options?: PgConnectionOptions): Promise<{
42
- service: "shortlinks";
43
- applied: number[];
44
- skipped: number[];
45
- }>;
46
52
  export {};
package/dist/pg-store.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/pg-store.ts
3
36
  import { createHash } from "crypto";
4
37
 
@@ -17,9 +50,19 @@ function ensureDataDir() {
17
50
  mkdirSync(dir, { recursive: true });
18
51
  return dir;
19
52
  }
53
+ function getConfigPath() {
54
+ return join(ensureDataDir(), "config.json");
55
+ }
20
56
  function getClickSaltPath() {
21
57
  return join(ensureDataDir(), "click-salt");
22
58
  }
59
+ function getDatabasePath(explicitPath) {
60
+ if (explicitPath)
61
+ return resolve(explicitPath);
62
+ if (process.env.SHORTLINKS_DB)
63
+ return resolve(process.env.SHORTLINKS_DB);
64
+ return join(ensureDataDir(), `${SERVICE_NAME}.db`);
65
+ }
23
66
  function readClickSaltFile(path) {
24
67
  try {
25
68
  const saved = readFileSync(path, "utf-8").trim();
@@ -65,6 +108,35 @@ function getClickSalt() {
65
108
  throw clickSaltError(path, error);
66
109
  }
67
110
  }
111
+ function loadConfig() {
112
+ const path = getConfigPath();
113
+ if (!existsSync(path))
114
+ return {};
115
+ try {
116
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
117
+ return parsed && typeof parsed === "object" ? parsed : {};
118
+ } catch {
119
+ return {};
120
+ }
121
+ }
122
+ function saveConfig(config) {
123
+ const path = getConfigPath();
124
+ mkdirSync(dirname(path), { recursive: true });
125
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}
126
+ `);
127
+ }
128
+ function updateConfig(patch) {
129
+ const next = {
130
+ ...loadConfig(),
131
+ ...patch,
132
+ cloudflare: {
133
+ ...loadConfig().cloudflare,
134
+ ...patch.cloudflare
135
+ }
136
+ };
137
+ saveConfig(next);
138
+ return next;
139
+ }
68
140
  function normalizeHostname(input) {
69
141
  const raw = input.trim().toLowerCase();
70
142
  if (!raw)
@@ -93,6 +165,9 @@ function formatShortUrl(hostname, slug, publicBaseUrl) {
93
165
  }
94
166
 
95
167
  // src/database.ts
168
+ import { Database } from "bun:sqlite";
169
+ import { mkdirSync as mkdirSync2 } from "fs";
170
+ import { dirname as dirname2 } from "path";
96
171
  function now() {
97
172
  return new Date().toISOString();
98
173
  }
@@ -101,6 +176,105 @@ function makeId(prefix) {
101
176
  const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
102
177
  return `${prefix}_${hex}`;
103
178
  }
179
+ var SQLITE_MIGRATIONS = [
180
+ `
181
+ CREATE TABLE IF NOT EXISTS domains (
182
+ id TEXT PRIMARY KEY,
183
+ hostname TEXT NOT NULL UNIQUE,
184
+ provider TEXT NOT NULL DEFAULT 'manual',
185
+ default_domain INTEGER NOT NULL DEFAULT 0,
186
+ cloudflare_zone_id TEXT,
187
+ cloudflare_account_id TEXT,
188
+ cloudflare_worker_name TEXT,
189
+ origin_url TEXT,
190
+ notes TEXT,
191
+ metadata TEXT NOT NULL DEFAULT '{}',
192
+ machine_id TEXT,
193
+ synced_at TEXT,
194
+ created_at TEXT NOT NULL,
195
+ updated_at TEXT NOT NULL
196
+ );
197
+
198
+ CREATE TABLE IF NOT EXISTS links (
199
+ id TEXT PRIMARY KEY,
200
+ domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
201
+ slug TEXT NOT NULL,
202
+ destination_url TEXT NOT NULL,
203
+ title TEXT,
204
+ active INTEGER NOT NULL DEFAULT 1,
205
+ expires_at TEXT,
206
+ metadata TEXT NOT NULL DEFAULT '{}',
207
+ machine_id TEXT,
208
+ synced_at TEXT,
209
+ created_at TEXT NOT NULL,
210
+ updated_at TEXT NOT NULL,
211
+ UNIQUE(domain_id, slug)
212
+ );
213
+
214
+ CREATE TABLE IF NOT EXISTS clicks (
215
+ id TEXT PRIMARY KEY,
216
+ link_id TEXT NOT NULL REFERENCES links(id) ON DELETE CASCADE,
217
+ domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
218
+ slug TEXT NOT NULL,
219
+ clicked_at TEXT NOT NULL,
220
+ ip_hash TEXT,
221
+ user_agent TEXT,
222
+ referer TEXT,
223
+ country TEXT,
224
+ city TEXT,
225
+ metadata TEXT NOT NULL DEFAULT '{}',
226
+ machine_id TEXT,
227
+ synced_at TEXT,
228
+ created_at TEXT NOT NULL,
229
+ updated_at TEXT NOT NULL
230
+ );
231
+
232
+ CREATE INDEX IF NOT EXISTS idx_domains_hostname ON domains(hostname);
233
+ CREATE INDEX IF NOT EXISTS idx_domains_default ON domains(default_domain);
234
+ CREATE INDEX IF NOT EXISTS idx_links_domain_slug ON links(domain_id, slug);
235
+ CREATE INDEX IF NOT EXISTS idx_links_active ON links(active);
236
+ CREATE INDEX IF NOT EXISTS idx_links_updated ON links(updated_at);
237
+ CREATE INDEX IF NOT EXISTS idx_clicks_link ON clicks(link_id);
238
+ CREATE INDEX IF NOT EXISTS idx_clicks_domain ON clicks(domain_id);
239
+ CREATE INDEX IF NOT EXISTS idx_clicks_clicked_at ON clicks(clicked_at);
240
+ CREATE INDEX IF NOT EXISTS idx_clicks_updated ON clicks(updated_at);
241
+ `
242
+ ];
243
+
244
+ class ShortlinksDatabase {
245
+ db;
246
+ path;
247
+ constructor(path) {
248
+ this.path = getDatabasePath(path);
249
+ mkdirSync2(dirname2(this.path), { recursive: true });
250
+ this.db = new Database(this.path);
251
+ this.db.exec("PRAGMA foreign_keys = ON;");
252
+ this.applyMigrations();
253
+ }
254
+ close() {
255
+ this.db.close();
256
+ }
257
+ applyMigrations() {
258
+ this.db.exec(`
259
+ CREATE TABLE IF NOT EXISTS _migrations (
260
+ id INTEGER PRIMARY KEY,
261
+ applied_at TEXT NOT NULL
262
+ );
263
+ `);
264
+ for (let i = 0;i < SQLITE_MIGRATIONS.length; i += 1) {
265
+ const id = i + 1;
266
+ const applied = this.db.query("SELECT id FROM _migrations WHERE id = ?").get(id);
267
+ if (applied)
268
+ continue;
269
+ const migration = SQLITE_MIGRATIONS[i];
270
+ const apply = this.db.transaction(() => {
271
+ this.db.exec(migration);
272
+ this.db.query("INSERT INTO _migrations (id, applied_at) VALUES (?, ?)").run(id, now());
273
+ });
274
+ apply();
275
+ }
276
+ }
277
+ }
104
278
 
105
279
  // src/machine.ts
106
280
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -146,46 +320,6 @@ function getMachineId() {
146
320
  return id;
147
321
  }
148
322
 
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
323
  // src/pg-store.ts
190
324
  function parseJsonObject(value) {
191
325
  if (!value)
@@ -199,47 +333,10 @@ function parseJsonObject(value) {
199
333
  return {};
200
334
  }
201
335
  }
202
- async function loadPgPool() {
203
- const importer = new Function("specifier", "return import(specifier)");
204
- const module = await importer("pg");
205
- return module.Pool;
206
- }
207
336
  function toPostgresSql(sql) {
208
337
  let index = 0;
209
338
  return sql.replace(/\?/g, () => `$${++index}`);
210
339
  }
211
- function createPgPoolConfig(connectionString, options = {}) {
212
- const ssl = options.ssl ?? true;
213
- return {
214
- connectionString,
215
- ...ssl ? { ssl: { rejectUnauthorized: true } } : { ssl: false }
216
- };
217
- }
218
-
219
- class PgPoolAdapter {
220
- pool;
221
- constructor(pool) {
222
- this.pool = pool;
223
- }
224
- static async create(connectionString, options = {}) {
225
- const Pool = await loadPgPool();
226
- return new PgPoolAdapter(new Pool(createPgPoolConfig(connectionString, options)));
227
- }
228
- async get(sql, ...params) {
229
- const result = await this.pool.query(toPostgresSql(sql), params);
230
- return result.rows[0] ?? null;
231
- }
232
- async all(sql, ...params) {
233
- const result = await this.pool.query(toPostgresSql(sql), params);
234
- return result.rows;
235
- }
236
- async run(sql, ...params) {
237
- return this.pool.query(toPostgresSql(sql), params);
238
- }
239
- async close() {
240
- await this.pool.end();
241
- }
242
- }
243
340
  function toIsoString(value) {
244
341
  if (value instanceof Date)
245
342
  return value.toISOString();
@@ -302,21 +399,27 @@ function clickFromRow(row) {
302
399
  metadata: parseJsonObject(row.metadata)
303
400
  };
304
401
  }
402
+ function createKitPgAdapter(client) {
403
+ return {
404
+ async get(sql, ...params) {
405
+ return client.get(toPostgresSql(sql), params);
406
+ },
407
+ async all(sql, ...params) {
408
+ return client.many(toPostgresSql(sql), params);
409
+ },
410
+ async run(sql, ...params) {
411
+ return client.query(toPostgresSql(sql), params);
412
+ }
413
+ };
414
+ }
305
415
 
306
416
  class PgShortlinksStore {
307
417
  pg;
308
418
  constructor(pg) {
309
419
  this.pg = pg;
310
420
  }
311
- static async fromConnectionString(connectionString, options = {}) {
312
- return new PgShortlinksStore(await PgPoolAdapter.create(connectionString, options));
313
- }
314
- static async fromEnv(env = process.env) {
315
- const connectionString = getShortlinksDatabaseUrl(env);
316
- if (!connectionString) {
317
- throw new Error("HASNA_SHORTLINKS_DATABASE_URL is required when shortlinks uses the postgres store");
318
- }
319
- return PgShortlinksStore.fromConnectionString(connectionString, { ssl: getShortlinksDatabaseSsl(env) });
421
+ static fromQueryClient(client) {
422
+ return new PgShortlinksStore(createKitPgAdapter(client));
320
423
  }
321
424
  async close() {
322
425
  await this.pg.close?.();
@@ -546,53 +649,7 @@ class PgShortlinksStore {
546
649
  throw new Error("Could not generate an unused slug after 32 attempts.");
547
650
  }
548
651
  }
549
- async function applyPostgresMigrations(connectionString, migrations, options = {}) {
550
- const Pool = await loadPgPool();
551
- const pool = new Pool(createPgPoolConfig(connectionString, options));
552
- const client = await pool.connect();
553
- const run = (sql, ...params) => client.query(toPostgresSql(sql), params);
554
- const get = async (sql, ...params) => {
555
- const result = await run(sql, ...params);
556
- return result.rows[0] ?? null;
557
- };
558
- const applied = [];
559
- const skipped = [];
560
- try {
561
- await run("BEGIN");
562
- await run(`
563
- SELECT pg_advisory_xact_lock(hashtext(?))
564
- `, "shortlinks:migrations");
565
- await run(`
566
- CREATE TABLE IF NOT EXISTS _shortlinks_migrations (
567
- id INTEGER PRIMARY KEY,
568
- service TEXT NOT NULL DEFAULT 'shortlinks',
569
- applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
570
- )
571
- `);
572
- for (let i = 0;i < migrations.length; i += 1) {
573
- const id = i + 1;
574
- const existing = await get("SELECT id FROM _shortlinks_migrations WHERE id = ? LIMIT 1", id);
575
- if (existing) {
576
- skipped.push(id);
577
- continue;
578
- }
579
- await run(migrations[i]);
580
- await run("INSERT INTO _shortlinks_migrations (id, service, applied_at) VALUES (?, ?, now())", id, "shortlinks");
581
- applied.push(id);
582
- }
583
- await run("COMMIT");
584
- return { service: "shortlinks", applied, skipped };
585
- } catch (error) {
586
- try {
587
- await run("ROLLBACK");
588
- } catch {}
589
- throw error;
590
- } finally {
591
- client.release();
592
- await pool.end();
593
- }
594
- }
595
652
  export {
596
- applyPostgresMigrations,
653
+ createKitPgAdapter,
597
654
  PgShortlinksStore
598
655
  };
@@ -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";