@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.
- package/README.md +43 -22
- package/dist/cli/index.js +8356 -2500
- package/dist/client-store.d.ts +47 -0
- package/dist/cloud-store.d.ts +46 -0
- 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 +7 -4
- package/dist/index.js +9424 -523
- package/dist/mcp/http.d.ts +26 -0
- package/dist/mcp/index.d.ts +15 -0
- package/dist/mcp/index.js +7429 -0
- package/dist/pg-store.d.ts +16 -10
- package/dist/pg-store.js +190 -133
- 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 +8521 -0
- package/dist/serve/openapi.d.ts +8 -0
- package/dist/server.js +33 -0
- package/dist/store-interface.d.ts +37 -0
- package/package.json +18 -8
- package/dist/pg-migrations.d.ts +0 -1
- package/dist/runtime.d.ts +0 -65
- package/dist/runtime.js +0 -181
|
@@ -0,0 +1,500 @@
|
|
|
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/sdk/generated.ts
|
|
36
|
+
class ApiError extends Error {
|
|
37
|
+
status;
|
|
38
|
+
body;
|
|
39
|
+
constructor(status, message, body) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.status = status;
|
|
42
|
+
this.body = body;
|
|
43
|
+
this.name = "ApiError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class ShortlinksApiClient {
|
|
48
|
+
baseUrl;
|
|
49
|
+
apiKey;
|
|
50
|
+
fetchImpl;
|
|
51
|
+
baseHeaders;
|
|
52
|
+
constructor(options) {
|
|
53
|
+
if (!options.baseUrl)
|
|
54
|
+
throw new Error("ShortlinksApiClient requires a baseUrl.");
|
|
55
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
56
|
+
this.apiKey = options.apiKey;
|
|
57
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
58
|
+
this.baseHeaders = options.headers ?? {};
|
|
59
|
+
}
|
|
60
|
+
async request(method, path, opts) {
|
|
61
|
+
const url = new URL(this.baseUrl + path);
|
|
62
|
+
if (opts.query) {
|
|
63
|
+
for (const [key, value] of Object.entries(opts.query)) {
|
|
64
|
+
if (value !== undefined && value !== null)
|
|
65
|
+
url.searchParams.set(key, String(value));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const headers = { Accept: "application/json", ...this.baseHeaders, ...opts.init?.headers };
|
|
69
|
+
if (this.apiKey)
|
|
70
|
+
headers["x-api-key"] = this.apiKey;
|
|
71
|
+
let payload;
|
|
72
|
+
if (opts.body !== undefined) {
|
|
73
|
+
headers["Content-Type"] = "application/json";
|
|
74
|
+
payload = JSON.stringify(opts.body);
|
|
75
|
+
}
|
|
76
|
+
const response = await this.fetchImpl(url.toString(), { ...opts.init, method, headers, body: payload });
|
|
77
|
+
const text = await response.text();
|
|
78
|
+
const data = text ? (() => {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(text);
|
|
81
|
+
} catch {
|
|
82
|
+
return text;
|
|
83
|
+
}
|
|
84
|
+
})() : undefined;
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new ApiError(response.status, `${method} ${path} failed: ${response.status}`, data);
|
|
87
|
+
}
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
async getHealth(init) {
|
|
91
|
+
return this.request("GET", `/health`, {
|
|
92
|
+
body: undefined,
|
|
93
|
+
query: undefined,
|
|
94
|
+
init
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async getReady(init) {
|
|
98
|
+
return this.request("GET", `/ready`, {
|
|
99
|
+
body: undefined,
|
|
100
|
+
query: undefined,
|
|
101
|
+
init
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async listDomains(init) {
|
|
105
|
+
return this.request("GET", `/v1/domains`, {
|
|
106
|
+
body: undefined,
|
|
107
|
+
query: undefined,
|
|
108
|
+
init
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async addDomain(body, init) {
|
|
112
|
+
return this.request("POST", `/v1/domains`, {
|
|
113
|
+
body,
|
|
114
|
+
query: undefined,
|
|
115
|
+
init
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async listLinks(query, init) {
|
|
119
|
+
return this.request("GET", `/v1/links`, {
|
|
120
|
+
body: undefined,
|
|
121
|
+
query,
|
|
122
|
+
init
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async createLink(body, init) {
|
|
126
|
+
return this.request("POST", `/v1/links`, {
|
|
127
|
+
body,
|
|
128
|
+
query: undefined,
|
|
129
|
+
init
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async getLink(slug, query, init) {
|
|
133
|
+
return this.request("GET", `/v1/links/${encodeURIComponent(String(slug))}`, {
|
|
134
|
+
body: undefined,
|
|
135
|
+
query,
|
|
136
|
+
init
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async deleteLink(slug, query, init) {
|
|
140
|
+
return this.request("DELETE", `/v1/links/${encodeURIComponent(String(slug))}`, {
|
|
141
|
+
body: undefined,
|
|
142
|
+
query,
|
|
143
|
+
init
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async disableLink(slug, query, init) {
|
|
147
|
+
return this.request("POST", `/v1/links/${encodeURIComponent(String(slug))}/disable`, {
|
|
148
|
+
body: undefined,
|
|
149
|
+
query,
|
|
150
|
+
init
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async enableLink(slug, query, init) {
|
|
154
|
+
return this.request("POST", `/v1/links/${encodeURIComponent(String(slug))}/enable`, {
|
|
155
|
+
body: undefined,
|
|
156
|
+
query,
|
|
157
|
+
init
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async getLinkStats(slug, query, init) {
|
|
161
|
+
return this.request("GET", `/v1/links/${encodeURIComponent(String(slug))}/stats`, {
|
|
162
|
+
body: undefined,
|
|
163
|
+
query,
|
|
164
|
+
init
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async resolveLink(slug, query, init) {
|
|
168
|
+
return this.request("GET", `/v1/resolve/${encodeURIComponent(String(slug))}`, {
|
|
169
|
+
body: undefined,
|
|
170
|
+
query,
|
|
171
|
+
init
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async getStats(init) {
|
|
175
|
+
return this.request("GET", `/v1/stats`, {
|
|
176
|
+
body: undefined,
|
|
177
|
+
query: undefined,
|
|
178
|
+
init
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async getVersion(init) {
|
|
182
|
+
return this.request("GET", `/version`, {
|
|
183
|
+
body: undefined,
|
|
184
|
+
query: undefined,
|
|
185
|
+
init
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// src/serve/openapi.ts
|
|
190
|
+
function buildOpenApiDocument(version) {
|
|
191
|
+
const linkSchema = {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: {
|
|
194
|
+
id: { type: "string" },
|
|
195
|
+
domain_id: { type: "string" },
|
|
196
|
+
hostname: { type: "string" },
|
|
197
|
+
slug: { type: "string" },
|
|
198
|
+
destination_url: { type: "string" },
|
|
199
|
+
title: { type: "string", nullable: true },
|
|
200
|
+
active: { type: "boolean" },
|
|
201
|
+
expires_at: { type: "string", nullable: true },
|
|
202
|
+
short_url: { type: "string" },
|
|
203
|
+
metadata: { type: "object", additionalProperties: true },
|
|
204
|
+
created_at: { type: "string" },
|
|
205
|
+
updated_at: { type: "string" }
|
|
206
|
+
},
|
|
207
|
+
required: ["id", "domain_id", "hostname", "slug", "destination_url", "active", "created_at"]
|
|
208
|
+
};
|
|
209
|
+
const domainSchema = {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
id: { type: "string" },
|
|
213
|
+
hostname: { type: "string" },
|
|
214
|
+
provider: { type: "string" },
|
|
215
|
+
default_domain: { type: "boolean" },
|
|
216
|
+
origin_url: { type: "string", nullable: true },
|
|
217
|
+
notes: { type: "string", nullable: true },
|
|
218
|
+
metadata: { type: "object", additionalProperties: true },
|
|
219
|
+
created_at: { type: "string" },
|
|
220
|
+
updated_at: { type: "string" }
|
|
221
|
+
},
|
|
222
|
+
required: ["id", "hostname", "provider", "default_domain", "created_at"]
|
|
223
|
+
};
|
|
224
|
+
const linkStatsSchema = {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {
|
|
227
|
+
link: { $ref: "#/components/schemas/Link" },
|
|
228
|
+
clicks: { type: "integer" },
|
|
229
|
+
last_clicked_at: { type: "string", nullable: true },
|
|
230
|
+
top_referrers: {
|
|
231
|
+
type: "array",
|
|
232
|
+
items: {
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: { referer: { type: "string", nullable: true }, clicks: { type: "integer" } }
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
top_user_agents: {
|
|
238
|
+
type: "array",
|
|
239
|
+
items: {
|
|
240
|
+
type: "object",
|
|
241
|
+
properties: { user_agent: { type: "string", nullable: true }, clicks: { type: "integer" } }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
required: ["link", "clicks"]
|
|
246
|
+
};
|
|
247
|
+
const probe = (extra = {}) => ({
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
status: { type: "string" },
|
|
251
|
+
version: { type: "string" },
|
|
252
|
+
mode: { type: "string" },
|
|
253
|
+
...extra
|
|
254
|
+
},
|
|
255
|
+
required: ["status", "version", "mode"]
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
openapi: "3.0.3",
|
|
259
|
+
info: {
|
|
260
|
+
title: "ShortlinksApi",
|
|
261
|
+
version,
|
|
262
|
+
description: "Shortlink manager \u2014 custom domains, click tracking, and shortlink CRUD with API-key auth. PURE REMOTE (Amendment A1): reads/writes RDS Postgres directly."
|
|
263
|
+
},
|
|
264
|
+
servers: [{ url: "/" }],
|
|
265
|
+
components: {
|
|
266
|
+
securitySchemes: {
|
|
267
|
+
apiKey: { type: "apiKey", in: "header", name: "x-api-key" }
|
|
268
|
+
},
|
|
269
|
+
schemas: {
|
|
270
|
+
Link: linkSchema,
|
|
271
|
+
Domain: domainSchema,
|
|
272
|
+
LinkStats: linkStatsSchema,
|
|
273
|
+
LinkList: { type: "array", items: linkSchema },
|
|
274
|
+
DomainList: { type: "array", items: domainSchema },
|
|
275
|
+
TotalStats: {
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: {
|
|
278
|
+
domains: { type: "integer" },
|
|
279
|
+
links: { type: "integer" },
|
|
280
|
+
clicks: { type: "integer" }
|
|
281
|
+
},
|
|
282
|
+
required: ["domains", "links", "clicks"]
|
|
283
|
+
},
|
|
284
|
+
CreateLinkRequest: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
url: { type: "string", description: "Destination URL (http/https)." },
|
|
288
|
+
domain: { type: "string", description: "Hostname; defaults to the default domain." },
|
|
289
|
+
slug: { type: "string", description: "Custom slug; generated when omitted." },
|
|
290
|
+
title: { type: "string" },
|
|
291
|
+
expires_at: { type: "string", description: "ISO date/time." },
|
|
292
|
+
length: { type: "integer", description: "Generated slug length." },
|
|
293
|
+
metadata: { type: "object", additionalProperties: true }
|
|
294
|
+
},
|
|
295
|
+
required: ["url"]
|
|
296
|
+
},
|
|
297
|
+
AddDomainRequest: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: {
|
|
300
|
+
hostname: { type: "string" },
|
|
301
|
+
provider: { type: "string" },
|
|
302
|
+
default: { type: "boolean" },
|
|
303
|
+
origin_url: { type: "string" },
|
|
304
|
+
notes: { type: "string" },
|
|
305
|
+
metadata: { type: "object", additionalProperties: true }
|
|
306
|
+
},
|
|
307
|
+
required: ["hostname"]
|
|
308
|
+
},
|
|
309
|
+
DeleteResponse: {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: { deleted: { type: "boolean" }, slug: { type: "string" } },
|
|
312
|
+
required: ["deleted"]
|
|
313
|
+
},
|
|
314
|
+
HealthStatus: probe({ db_latency_ms: { type: "integer" } }),
|
|
315
|
+
ReadyStatus: probe({ pending_migrations: { type: "array", items: { type: "string" } } }),
|
|
316
|
+
VersionInfo: probe({ name: { type: "string" } }),
|
|
317
|
+
ErrorResponse: {
|
|
318
|
+
type: "object",
|
|
319
|
+
properties: { error: { type: "string" }, reason: { type: "string" } },
|
|
320
|
+
required: ["error"]
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
paths: {
|
|
325
|
+
"/health": {
|
|
326
|
+
get: {
|
|
327
|
+
operationId: "getHealth",
|
|
328
|
+
summary: "Liveness probe.",
|
|
329
|
+
responses: {
|
|
330
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/HealthStatus" } } } }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
"/ready": {
|
|
335
|
+
get: {
|
|
336
|
+
operationId: "getReady",
|
|
337
|
+
summary: "Readiness probe (DB reachable and schema migrated).",
|
|
338
|
+
responses: {
|
|
339
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ReadyStatus" } } } }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
"/version": {
|
|
344
|
+
get: {
|
|
345
|
+
operationId: "getVersion",
|
|
346
|
+
summary: "Service version and mode.",
|
|
347
|
+
responses: {
|
|
348
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/VersionInfo" } } } }
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
"/v1/stats": {
|
|
353
|
+
get: {
|
|
354
|
+
operationId: "getStats",
|
|
355
|
+
summary: "Total domains/links/clicks counts.",
|
|
356
|
+
security: [{ apiKey: [] }],
|
|
357
|
+
responses: {
|
|
358
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/TotalStats" } } } }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
"/v1/domains": {
|
|
363
|
+
get: {
|
|
364
|
+
operationId: "listDomains",
|
|
365
|
+
summary: "List configured domains.",
|
|
366
|
+
security: [{ apiKey: [] }],
|
|
367
|
+
responses: {
|
|
368
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/DomainList" } } } }
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
post: {
|
|
372
|
+
operationId: "addDomain",
|
|
373
|
+
summary: "Add or update a domain.",
|
|
374
|
+
security: [{ apiKey: [] }],
|
|
375
|
+
requestBody: {
|
|
376
|
+
required: true,
|
|
377
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/AddDomainRequest" } } }
|
|
378
|
+
},
|
|
379
|
+
responses: {
|
|
380
|
+
"201": { content: { "application/json": { schema: { $ref: "#/components/schemas/Domain" } } } }
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
"/v1/links": {
|
|
385
|
+
get: {
|
|
386
|
+
operationId: "listLinks",
|
|
387
|
+
summary: "List shortlinks.",
|
|
388
|
+
security: [{ apiKey: [] }],
|
|
389
|
+
parameters: [
|
|
390
|
+
{ name: "domain", in: "query", schema: { type: "string" } },
|
|
391
|
+
{ name: "active", in: "query", schema: { type: "boolean" } },
|
|
392
|
+
{ name: "limit", in: "query", schema: { type: "integer" } }
|
|
393
|
+
],
|
|
394
|
+
responses: {
|
|
395
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/LinkList" } } } }
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
post: {
|
|
399
|
+
operationId: "createLink",
|
|
400
|
+
summary: "Create a shortlink.",
|
|
401
|
+
security: [{ apiKey: [] }],
|
|
402
|
+
requestBody: {
|
|
403
|
+
required: true,
|
|
404
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/CreateLinkRequest" } } }
|
|
405
|
+
},
|
|
406
|
+
responses: {
|
|
407
|
+
"201": { content: { "application/json": { schema: { $ref: "#/components/schemas/Link" } } } }
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
"/v1/links/{slug}": {
|
|
412
|
+
get: {
|
|
413
|
+
operationId: "getLink",
|
|
414
|
+
summary: "Get a shortlink by slug.",
|
|
415
|
+
security: [{ apiKey: [] }],
|
|
416
|
+
parameters: [
|
|
417
|
+
{ name: "slug", in: "path", required: true, schema: { type: "string" } },
|
|
418
|
+
{ name: "domain", in: "query", schema: { type: "string" } }
|
|
419
|
+
],
|
|
420
|
+
responses: {
|
|
421
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/Link" } } } }
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
delete: {
|
|
425
|
+
operationId: "deleteLink",
|
|
426
|
+
summary: "Delete a shortlink.",
|
|
427
|
+
security: [{ apiKey: [] }],
|
|
428
|
+
parameters: [
|
|
429
|
+
{ name: "slug", in: "path", required: true, schema: { type: "string" } },
|
|
430
|
+
{ name: "domain", in: "query", schema: { type: "string" } }
|
|
431
|
+
],
|
|
432
|
+
responses: {
|
|
433
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/DeleteResponse" } } } }
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
"/v1/links/{slug}/enable": {
|
|
438
|
+
post: {
|
|
439
|
+
operationId: "enableLink",
|
|
440
|
+
summary: "Enable a shortlink.",
|
|
441
|
+
security: [{ apiKey: [] }],
|
|
442
|
+
parameters: [
|
|
443
|
+
{ name: "slug", in: "path", required: true, schema: { type: "string" } },
|
|
444
|
+
{ name: "domain", in: "query", schema: { type: "string" } }
|
|
445
|
+
],
|
|
446
|
+
responses: {
|
|
447
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/Link" } } } }
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
"/v1/links/{slug}/disable": {
|
|
452
|
+
post: {
|
|
453
|
+
operationId: "disableLink",
|
|
454
|
+
summary: "Disable a shortlink.",
|
|
455
|
+
security: [{ apiKey: [] }],
|
|
456
|
+
parameters: [
|
|
457
|
+
{ name: "slug", in: "path", required: true, schema: { type: "string" } },
|
|
458
|
+
{ name: "domain", in: "query", schema: { type: "string" } }
|
|
459
|
+
],
|
|
460
|
+
responses: {
|
|
461
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/Link" } } } }
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
"/v1/links/{slug}/stats": {
|
|
466
|
+
get: {
|
|
467
|
+
operationId: "getLinkStats",
|
|
468
|
+
summary: "Click stats for a shortlink.",
|
|
469
|
+
security: [{ apiKey: [] }],
|
|
470
|
+
parameters: [
|
|
471
|
+
{ name: "slug", in: "path", required: true, schema: { type: "string" } },
|
|
472
|
+
{ name: "domain", in: "query", schema: { type: "string" } }
|
|
473
|
+
],
|
|
474
|
+
responses: {
|
|
475
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/LinkStats" } } } }
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
"/v1/resolve/{slug}": {
|
|
480
|
+
get: {
|
|
481
|
+
operationId: "resolveLink",
|
|
482
|
+
summary: "Resolve a slug to its destination without recording a click.",
|
|
483
|
+
security: [{ apiKey: [] }],
|
|
484
|
+
parameters: [
|
|
485
|
+
{ name: "slug", in: "path", required: true, schema: { type: "string" } },
|
|
486
|
+
{ name: "domain", in: "query", schema: { type: "string" } }
|
|
487
|
+
],
|
|
488
|
+
responses: {
|
|
489
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/Link" } } } }
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
export {
|
|
497
|
+
buildOpenApiDocument,
|
|
498
|
+
ShortlinksApiClient,
|
|
499
|
+
ApiError
|
|
500
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shortlinks serve HTTP app.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces the standard health/ready/version probes plus a versioned `/v1`
|
|
5
|
+
* REST API guarded by @hasna/contracts API-key auth. PURE REMOTE (Amendment
|
|
6
|
+
* A1): all reads/writes go through the injected Postgres store over the shared
|
|
7
|
+
* RDS — there is no sync engine or cache in the service.
|
|
8
|
+
*/
|
|
9
|
+
import { Hono } from "hono";
|
|
10
|
+
import type { PoolQueryClient } from "../generated/storage-kit/query.js";
|
|
11
|
+
import { PgShortlinksStore } from "../pg-store.js";
|
|
12
|
+
export interface ServeAppDeps {
|
|
13
|
+
client: PoolQueryClient;
|
|
14
|
+
store: PgShortlinksStore;
|
|
15
|
+
version: string;
|
|
16
|
+
mode: string;
|
|
17
|
+
signingSecret: string;
|
|
18
|
+
isRevoked?: (kid: string) => boolean | Promise<boolean>;
|
|
19
|
+
audit?: (event: unknown) => void;
|
|
20
|
+
}
|
|
21
|
+
export declare function createServeApp(deps: ServeAppDeps): Hono;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* `shortlinks-serve` — the cloud HTTP service entrypoint.
|
|
4
|
+
*
|
|
5
|
+
* PURE REMOTE (Amendment A1): reads/writes the shared RDS Postgres directly via
|
|
6
|
+
* the vendored storage kit. No sync engine, cache, or local database in the
|
|
7
|
+
* service.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* shortlinks-serve Run migrations (idempotent) then serve.
|
|
11
|
+
* shortlinks-serve migrate Run migrations and exit (one-shot task).
|
|
12
|
+
* shortlinks-serve --no-migrate Serve without running migrations on boot.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|