@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.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * OpenAPI 3 description of the shortlinks serve HTTP API.
3
+ *
4
+ * This is the single source of truth for the generated SDK
5
+ * (`@hasna/shortlinks-sdk`) — run `bun run sdk:generate` after changing it —
6
+ * and is also served live at `GET /openapi.json`.
7
+ */
8
+ export declare function buildOpenApiDocument(version: string): Record<string, unknown>;
package/dist/server.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/store.ts
3
36
  import { createHash } from "crypto";
4
37
 
@@ -0,0 +1,37 @@
1
+ import type { AddDomainInput, Click, ClickInput, CreateLinkInput, Domain, Link, LinkStats } from "./types.js";
2
+ export interface ListLinksOptions {
3
+ domain?: string;
4
+ activeOnly?: boolean;
5
+ limit?: number;
6
+ }
7
+ export interface TotalStats {
8
+ domains: number;
9
+ links: number;
10
+ clicks: number;
11
+ }
12
+ /**
13
+ * The single storage surface shared by LocalStore and ApiStore. All methods are
14
+ * async so one call site works against either transport with no mode branching.
15
+ */
16
+ export interface Store {
17
+ /** Which transport backs this store (for banners/diagnostics only). */
18
+ readonly kind: "local" | "cloud-http";
19
+ addDomain(input: AddDomainInput): Promise<Domain>;
20
+ listDomains(): Promise<Domain[]>;
21
+ getDomain(hostnameOrId: string): Promise<Domain | null>;
22
+ getDefaultDomain(): Promise<Domain | null>;
23
+ createLink(input: CreateLinkInput): Promise<Link>;
24
+ listLinks(options?: ListLinksOptions): Promise<Link[]>;
25
+ getLink(domainOrSlug: string, maybeSlug?: string): Promise<Link | null>;
26
+ resolve(hostname: string, slug: string): Promise<Link | null>;
27
+ setLinkActive(domainOrSlug: string, slugOrActive: string | boolean, active?: boolean): Promise<Link>;
28
+ deleteLink(domainOrSlug: string, maybeSlug?: string): Promise<Link>;
29
+ /**
30
+ * Record a click. Only the on-box redirect server does this against a LocalStore;
31
+ * the cloud API records clicks server-side, so ApiStore rejects this call.
32
+ */
33
+ recordClick(link: Link, input?: ClickInput): Promise<Click>;
34
+ getStats(domainOrSlug: string, maybeSlug?: string): Promise<LinkStats>;
35
+ totalStats(): Promise<TotalStats>;
36
+ close(): Promise<void>;
37
+ }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@hasna/shortlinks",
3
- "version": "0.1.23",
4
- "description": "CLI-only shortlink manager for custom domains, click tracking, Cloudflare setup, and app-owned Postgres runtime support",
3
+ "version": "0.2.0",
4
+ "description": "Shortlink manager for custom domains and click tracking, with local SQLite or self-hosted cloud (/v1 API + bearer key) storage and Cloudflare setup helpers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "bin": {
9
- "shortlinks": "dist/cli/index.js"
9
+ "shortlinks": "dist/cli/index.js",
10
+ "shortlinks-mcp": "dist/mcp/index.js",
11
+ "shortlinks-serve": "dist/serve/index.js"
10
12
  },
11
13
  "exports": {
12
14
  ".": {
@@ -25,9 +27,9 @@
25
27
  "types": "./dist/pg-store.d.ts",
26
28
  "import": "./dist/pg-store.js"
27
29
  },
28
- "./runtime": {
29
- "types": "./dist/runtime.d.ts",
30
- "import": "./dist/runtime.js"
30
+ "./sdk": {
31
+ "types": "./dist/sdk/index.d.ts",
32
+ "import": "./dist/sdk/index.js"
31
33
  }
32
34
  },
33
35
  "files": [
@@ -39,11 +41,15 @@
39
41
  "SECURITY.md"
40
42
  ],
41
43
  "scripts": {
42
- "build": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun && bun build src/index.ts --outdir dist --target bun && bun build src/server.ts --outdir dist --target bun && bun build src/cloudflare.ts --outdir dist --target bun && bun build src/runtime.ts --outdir dist --target bun && bun build src/pg-store.ts --outdir dist --target bun && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
44
+ "build": "rm -rf dist && bun build src/cli/index.ts src/mcp/index.ts src/serve/index.ts src/sdk/index.ts src/index.ts src/server.ts src/cloudflare.ts src/pg-store.ts --root src --outdir dist --target bun --external @modelcontextprotocol/sdk --external @hasna/mcp-harness --external @hasna/mcp-harness/node && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
45
+ "sdk:generate": "bun run scripts/generate-sdk.ts",
43
46
  "typecheck": "tsc --noEmit",
44
47
  "test": "bun test",
45
48
  "dev:cli": "bun run src/cli/index.ts",
46
- "prepublishOnly": "bun run typecheck && bun run test && bun run build"
49
+ "dev:mcp": "bun run src/mcp/index.ts",
50
+ "dev:serve": "bun run src/serve/index.ts",
51
+ "verify:release": "bun run typecheck && bun run test && bun run build",
52
+ "prepublishOnly": "bun run verify:release"
47
53
  },
48
54
  "keywords": [
49
55
  "shortlinks",
@@ -75,11 +81,15 @@
75
81
  "license": "Apache-2.0",
76
82
  "dependencies": {
77
83
  "@hasna/events": "^0.1.6",
84
+ "@hasna/mcp-harness": "file:../open-mcp",
85
+ "@modelcontextprotocol/sdk": "^1.27.1",
78
86
  "chalk": "^5.4.1",
79
87
  "commander": "^13.1.0",
88
+ "hono": "^4.12.7",
80
89
  "pg": "^8.13.3"
81
90
  },
82
91
  "devDependencies": {
92
+ "@hasna/contracts": "file:../open-contracts",
83
93
  "@types/pg": "^8.11.11",
84
94
  "@types/bun": "^1.2.4",
85
95
  "typescript": "^5.7.3"
@@ -1 +0,0 @@
1
- export declare const PG_MIGRATIONS: string[];
package/dist/runtime.d.ts DELETED
@@ -1,65 +0,0 @@
1
- export type ShortlinksStoreMode = "local" | "postgres";
2
- export type ShortlinksRuntimeEnv = Record<string, string | undefined>;
3
- export interface ShortlinksPostgresConfig {
4
- provider: "postgres";
5
- url: string;
6
- ssl: boolean;
7
- }
8
- export interface ShortlinksRuntimeConfig {
9
- service: "shortlinks";
10
- mode: ShortlinksStoreMode;
11
- database?: ShortlinksPostgresConfig;
12
- }
13
- export declare const SHORTLINKS_RUNTIME_ENV: {
14
- readonly store: "HASNA_SHORTLINKS_STORE";
15
- readonly databaseUrl: "HASNA_SHORTLINKS_DATABASE_URL";
16
- readonly databaseSsl: "HASNA_SHORTLINKS_DATABASE_SSL";
17
- };
18
- export declare const SHORTLINKS_RUNTIME_FALLBACK_ENV: {
19
- readonly store: "SHORTLINKS_STORE";
20
- readonly databaseUrl: "SHORTLINKS_DATABASE_URL";
21
- readonly databaseSsl: "SHORTLINKS_DATABASE_SSL";
22
- };
23
- export declare const CANONICAL_SHORTLINKS_POSTGRES_CLUSTER = "hasna-xyz-infra-apps-prod-postgres";
24
- export declare const CANONICAL_SHORTLINKS_POSTGRES_DATABASE = "shortlinks";
25
- export declare const CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH = "hasna/xyz/opensource/shortlinks/prod/postgres";
26
- export interface CanonicalShortlinksPostgresConfig {
27
- cluster: typeof CANONICAL_SHORTLINKS_POSTGRES_CLUSTER;
28
- database: typeof CANONICAL_SHORTLINKS_POSTGRES_DATABASE;
29
- runtimeSecretPath: typeof CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH;
30
- primaryEnv: typeof SHORTLINKS_RUNTIME_ENV.databaseUrl;
31
- fallbackEnv: typeof SHORTLINKS_RUNTIME_FALLBACK_ENV.databaseUrl;
32
- }
33
- export interface RuntimeEnvStatus {
34
- name: string;
35
- active_name: string;
36
- configured: boolean;
37
- }
38
- export interface ShortlinksRuntimeStatus {
39
- ok: boolean;
40
- service: "shortlinks";
41
- mode: ShortlinksStoreMode;
42
- local_default: boolean;
43
- postgres_enabled: boolean;
44
- database: {
45
- configured: boolean;
46
- provider: "postgres" | null;
47
- redacted_url: string | null;
48
- ssl: boolean | null;
49
- };
50
- env: Record<keyof typeof SHORTLINKS_RUNTIME_ENV, RuntimeEnvStatus>;
51
- canonical: CanonicalShortlinksPostgresConfig;
52
- issues: string[];
53
- warnings: string[];
54
- no_network: true;
55
- }
56
- export declare function getCanonicalShortlinksPostgresConfig(): CanonicalShortlinksPostgresConfig;
57
- export declare function parseShortlinksStoreMode(value: string | undefined): ShortlinksStoreMode;
58
- export declare function getShortlinksStoreMode(env?: ShortlinksRuntimeEnv): ShortlinksStoreMode;
59
- export declare function getShortlinksDatabaseUrl(env?: ShortlinksRuntimeEnv): string | undefined;
60
- export declare function getShortlinksDatabaseSsl(env?: ShortlinksRuntimeEnv): boolean;
61
- export declare function getShortlinksRuntimeEnvName(env: ShortlinksRuntimeEnv, key: keyof typeof SHORTLINKS_RUNTIME_ENV): string;
62
- export declare function loadShortlinksRuntimeConfig(env?: ShortlinksRuntimeEnv): ShortlinksRuntimeConfig;
63
- export declare function assertShortlinksPostgresConfig(config: ShortlinksRuntimeConfig): void;
64
- export declare function getShortlinksRuntimeStatus(env?: ShortlinksRuntimeEnv): ShortlinksRuntimeStatus;
65
- export declare function redactDatabaseUrl(value: string | undefined): string | null;
package/dist/runtime.js DELETED
@@ -1,181 +0,0 @@
1
- // @bun
2
- // src/runtime.ts
3
- var SHORTLINKS_RUNTIME_ENV = {
4
- store: "HASNA_SHORTLINKS_STORE",
5
- databaseUrl: "HASNA_SHORTLINKS_DATABASE_URL",
6
- databaseSsl: "HASNA_SHORTLINKS_DATABASE_SSL"
7
- };
8
- var SHORTLINKS_RUNTIME_FALLBACK_ENV = {
9
- store: "SHORTLINKS_STORE",
10
- databaseUrl: "SHORTLINKS_DATABASE_URL",
11
- databaseSsl: "SHORTLINKS_DATABASE_SSL"
12
- };
13
- var CANONICAL_SHORTLINKS_POSTGRES_CLUSTER = "hasna-xyz-infra-apps-prod-postgres";
14
- var CANONICAL_SHORTLINKS_POSTGRES_DATABASE = "shortlinks";
15
- var CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH = "hasna/xyz/opensource/shortlinks/prod/postgres";
16
- function getCanonicalShortlinksPostgresConfig() {
17
- return {
18
- cluster: CANONICAL_SHORTLINKS_POSTGRES_CLUSTER,
19
- database: CANONICAL_SHORTLINKS_POSTGRES_DATABASE,
20
- runtimeSecretPath: CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH,
21
- primaryEnv: SHORTLINKS_RUNTIME_ENV.databaseUrl,
22
- fallbackEnv: SHORTLINKS_RUNTIME_FALLBACK_ENV.databaseUrl
23
- };
24
- }
25
- function parseShortlinksStoreMode(value) {
26
- const normalized = clean(value)?.toLowerCase();
27
- if (!normalized)
28
- return "local";
29
- if (normalized === "local" || normalized === "postgres")
30
- return normalized;
31
- if (normalized === "pg")
32
- return "postgres";
33
- throw new Error(`${SHORTLINKS_RUNTIME_ENV.store} must be local or postgres`);
34
- }
35
- function getShortlinksStoreMode(env = process.env) {
36
- return parseShortlinksStoreMode(readRuntimeEnv(env, "store").value);
37
- }
38
- function getShortlinksDatabaseUrl(env = process.env) {
39
- return readRuntimeEnv(env, "databaseUrl").value;
40
- }
41
- function getShortlinksDatabaseSsl(env = process.env) {
42
- return parseBoolean(readRuntimeEnv(env, "databaseSsl").value, true);
43
- }
44
- function getShortlinksRuntimeEnvName(env, key) {
45
- const primary = SHORTLINKS_RUNTIME_ENV[key];
46
- if (clean(env[primary]))
47
- return primary;
48
- return SHORTLINKS_RUNTIME_FALLBACK_ENV[key];
49
- }
50
- function loadShortlinksRuntimeConfig(env = process.env) {
51
- const mode = getShortlinksStoreMode(env);
52
- const databaseUrl = getShortlinksDatabaseUrl(env);
53
- return {
54
- service: "shortlinks",
55
- mode,
56
- ...databaseUrl ? {
57
- database: {
58
- provider: "postgres",
59
- url: databaseUrl,
60
- ssl: getShortlinksDatabaseSsl(env)
61
- }
62
- } : {}
63
- };
64
- }
65
- function assertShortlinksPostgresConfig(config) {
66
- if (config.mode !== "postgres")
67
- return;
68
- if (!config.database?.url) {
69
- throw new Error(`${SHORTLINKS_RUNTIME_ENV.databaseUrl} is required when ${SHORTLINKS_RUNTIME_ENV.store}=postgres`);
70
- }
71
- }
72
- function getShortlinksRuntimeStatus(env = process.env) {
73
- const issues = [];
74
- const warnings = [];
75
- let config;
76
- try {
77
- config = loadShortlinksRuntimeConfig(env);
78
- } catch (error) {
79
- issues.push(error instanceof Error ? error.message : String(error));
80
- config = { service: "shortlinks", mode: "local" };
81
- }
82
- try {
83
- assertShortlinksPostgresConfig(config);
84
- } catch (error) {
85
- issues.push(error instanceof Error ? error.message : String(error));
86
- }
87
- if (config.mode === "local" && config.database?.url) {
88
- warnings.push(`${SHORTLINKS_RUNTIME_ENV.store}=local ignores configured Postgres database settings`);
89
- }
90
- return {
91
- ok: issues.length === 0,
92
- service: "shortlinks",
93
- mode: config.mode,
94
- local_default: config.mode === "local",
95
- postgres_enabled: config.mode === "postgres",
96
- database: {
97
- configured: Boolean(config.database?.url),
98
- provider: config.database?.provider ?? null,
99
- redacted_url: redactDatabaseUrl(config.database?.url),
100
- ssl: config.database?.ssl ?? null
101
- },
102
- env: runtimeEnvStatus(env),
103
- canonical: getCanonicalShortlinksPostgresConfig(),
104
- issues,
105
- warnings,
106
- no_network: true
107
- };
108
- }
109
- function redactDatabaseUrl(value) {
110
- if (!value)
111
- return null;
112
- try {
113
- const url = new URL(value);
114
- if (url.username)
115
- url.username = "***";
116
- if (url.password)
117
- url.password = "***";
118
- for (const key of Array.from(url.searchParams.keys())) {
119
- if (isSensitiveQueryKey(key))
120
- url.searchParams.set(key, "***");
121
- }
122
- return url.toString();
123
- } catch {
124
- return "(redacted)";
125
- }
126
- }
127
- function runtimeEnvStatus(env) {
128
- return Object.fromEntries(Object.entries(SHORTLINKS_RUNTIME_ENV).map(([key, name]) => {
129
- const activeName = getShortlinksRuntimeEnvName(env, key);
130
- return [
131
- key,
132
- {
133
- name,
134
- active_name: activeName,
135
- configured: Boolean(clean(env[activeName]))
136
- }
137
- ];
138
- }));
139
- }
140
- function readRuntimeEnv(env, key) {
141
- const primary = SHORTLINKS_RUNTIME_ENV[key];
142
- const primaryValue = clean(env[primary]);
143
- if (primaryValue)
144
- return { name: primary, value: primaryValue };
145
- const fallback = SHORTLINKS_RUNTIME_FALLBACK_ENV[key];
146
- return { name: fallback, value: clean(env[fallback]) };
147
- }
148
- function parseBoolean(value, fallback) {
149
- const normalized = clean(value)?.toLowerCase();
150
- if (!normalized)
151
- return fallback;
152
- if (["1", "true", "yes", "on"].includes(normalized))
153
- return true;
154
- if (["0", "false", "no", "off"].includes(normalized))
155
- return false;
156
- throw new Error(`${SHORTLINKS_RUNTIME_ENV.databaseSsl} must be true or false`);
157
- }
158
- function isSensitiveQueryKey(key) {
159
- return /(?:^|[_-])(pass(?:word)?|pwd|secret|token|credential|auth|api[_-]?key|access[_-]?key)(?:$|[_-])/i.test(key);
160
- }
161
- function clean(value) {
162
- const trimmed = value?.trim();
163
- return trimmed ? trimmed : undefined;
164
- }
165
- export {
166
- redactDatabaseUrl,
167
- parseShortlinksStoreMode,
168
- loadShortlinksRuntimeConfig,
169
- getShortlinksStoreMode,
170
- getShortlinksRuntimeStatus,
171
- getShortlinksRuntimeEnvName,
172
- getShortlinksDatabaseUrl,
173
- getShortlinksDatabaseSsl,
174
- getCanonicalShortlinksPostgresConfig,
175
- assertShortlinksPostgresConfig,
176
- SHORTLINKS_RUNTIME_FALLBACK_ENV,
177
- SHORTLINKS_RUNTIME_ENV,
178
- CANONICAL_SHORTLINKS_RUNTIME_SECRET_PATH,
179
- CANONICAL_SHORTLINKS_POSTGRES_DATABASE,
180
- CANONICAL_SHORTLINKS_POSTGRES_CLUSTER
181
- };