@hasna/shortlinks 0.1.14 → 0.1.16
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/dist/api-client.d.ts +30 -0
- package/dist/cli/index.js +416 -23
- package/dist/config.d.ts +8 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +362 -12
- package/dist/pg-store.d.ts +1 -0
- package/dist/server.d.ts +17 -1
- package/dist/server.js +165 -6
- package/dist/storage.js +24 -2
- package/dist/store.d.ts +1 -0
- package/dist/types.d.ts +3 -0
- package/infra/aws-ec2-user-data.sh +7 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -49,11 +49,16 @@ function saveConfig(config) {
|
|
|
49
49
|
`);
|
|
50
50
|
}
|
|
51
51
|
function updateConfig(patch) {
|
|
52
|
+
const current = loadConfig();
|
|
52
53
|
const next = {
|
|
53
|
-
...
|
|
54
|
+
...current,
|
|
54
55
|
...patch,
|
|
56
|
+
api: {
|
|
57
|
+
...current.api,
|
|
58
|
+
...patch.api
|
|
59
|
+
},
|
|
55
60
|
cloudflare: {
|
|
56
|
-
...
|
|
61
|
+
...current.cloudflare,
|
|
57
62
|
...patch.cloudflare
|
|
58
63
|
}
|
|
59
64
|
};
|
|
@@ -156,6 +161,10 @@ var SQLITE_MIGRATIONS = [
|
|
|
156
161
|
CREATE INDEX IF NOT EXISTS idx_clicks_domain ON clicks(domain_id);
|
|
157
162
|
CREATE INDEX IF NOT EXISTS idx_clicks_clicked_at ON clicks(clicked_at);
|
|
158
163
|
CREATE INDEX IF NOT EXISTS idx_clicks_updated ON clicks(updated_at);
|
|
164
|
+
`,
|
|
165
|
+
`
|
|
166
|
+
ALTER TABLE links ADD COLUMN max_uses INTEGER;
|
|
167
|
+
ALTER TABLE links ADD COLUMN used_count INTEGER NOT NULL DEFAULT 0;
|
|
159
168
|
`
|
|
160
169
|
];
|
|
161
170
|
|
|
@@ -262,6 +271,8 @@ function linkFromRow(row) {
|
|
|
262
271
|
return {
|
|
263
272
|
...row,
|
|
264
273
|
active: Boolean(row.active),
|
|
274
|
+
max_uses: row.max_uses ?? null,
|
|
275
|
+
used_count: row.used_count ?? 0,
|
|
265
276
|
metadata: parseJsonObject(row.metadata),
|
|
266
277
|
short_url: formatShortUrl(row.hostname, row.slug, publicBaseUrl)
|
|
267
278
|
};
|
|
@@ -292,6 +303,13 @@ function isoOrNull(input) {
|
|
|
292
303
|
throw new Error(`Invalid date: ${input}`);
|
|
293
304
|
return date.toISOString();
|
|
294
305
|
}
|
|
306
|
+
function normalizeMaxUses(value) {
|
|
307
|
+
if (value === null || value === undefined)
|
|
308
|
+
return null;
|
|
309
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
310
|
+
throw new Error("maxUses must be a positive integer.");
|
|
311
|
+
return value;
|
|
312
|
+
}
|
|
295
313
|
|
|
296
314
|
class ShortlinksStore {
|
|
297
315
|
database;
|
|
@@ -367,15 +385,16 @@ class ShortlinksStore {
|
|
|
367
385
|
const timestamp = now();
|
|
368
386
|
const machineId = getMachineId();
|
|
369
387
|
const expiresAt = isoOrNull(input.expiresAt);
|
|
388
|
+
const maxUses = normalizeMaxUses(input.maxUses);
|
|
370
389
|
const slug = input.slug ? normalizeSlug(input.slug) : this.generateAvailableSlug(domain.id, input.slugLength || DEFAULT_SLUG_LENGTH);
|
|
371
390
|
try {
|
|
372
391
|
this.database.db.query(`
|
|
373
392
|
INSERT INTO links (
|
|
374
|
-
id, domain_id, slug, destination_url, title, active, expires_at, metadata,
|
|
393
|
+
id, domain_id, slug, destination_url, title, active, expires_at, max_uses, used_count, metadata,
|
|
375
394
|
machine_id, synced_at, created_at, updated_at
|
|
376
395
|
)
|
|
377
|
-
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, NULL, ?, ?)
|
|
378
|
-
`).run(makeId("lnk"), domain.id, slug, destinationUrl, input.title || null, expiresAt, JSON.stringify(input.metadata || {}), machineId, timestamp, timestamp);
|
|
396
|
+
VALUES (?, ?, ?, ?, ?, 1, ?, ?, 0, ?, ?, NULL, ?, ?)
|
|
397
|
+
`).run(makeId("lnk"), domain.id, slug, destinationUrl, input.title || null, expiresAt, maxUses, JSON.stringify(input.metadata || {}), machineId, timestamp, timestamp);
|
|
379
398
|
} catch (error) {
|
|
380
399
|
const message = error instanceof Error ? error.message : String(error);
|
|
381
400
|
if (message.includes("UNIQUE")) {
|
|
@@ -385,6 +404,19 @@ class ShortlinksStore {
|
|
|
385
404
|
}
|
|
386
405
|
return this.getLink(domain.hostname, slug);
|
|
387
406
|
}
|
|
407
|
+
consumeLinkUse(link) {
|
|
408
|
+
const timestamp = now();
|
|
409
|
+
const result = this.database.db.query(`
|
|
410
|
+
UPDATE links
|
|
411
|
+
SET used_count = used_count + 1, updated_at = ?, synced_at = NULL
|
|
412
|
+
WHERE id = ?
|
|
413
|
+
AND active = 1
|
|
414
|
+
AND (max_uses IS NULL OR used_count < max_uses)
|
|
415
|
+
`).run(timestamp, link.id);
|
|
416
|
+
if (result.changes === 0)
|
|
417
|
+
return null;
|
|
418
|
+
return this.getLink(link.hostname, link.slug);
|
|
419
|
+
}
|
|
388
420
|
listLinks(options = {}) {
|
|
389
421
|
const params = [];
|
|
390
422
|
let where = "WHERE 1 = 1";
|
|
@@ -538,6 +570,121 @@ function json(data, status = 200) {
|
|
|
538
570
|
headers: { "content-type": "application/json; charset=utf-8" }
|
|
539
571
|
});
|
|
540
572
|
}
|
|
573
|
+
function apiToken(options) {
|
|
574
|
+
return options.apiToken || process.env.SHORTLINKS_API_TOKEN || process.env.HASNA_SHORTLINKS_API_TOKEN || null;
|
|
575
|
+
}
|
|
576
|
+
function requestToken(request) {
|
|
577
|
+
const auth = request.headers.get("authorization") || "";
|
|
578
|
+
const bearer = auth.toLowerCase().startsWith("bearer ") ? auth.slice(7).trim() : "";
|
|
579
|
+
return bearer || request.headers.get("x-shortlinks-token") || request.headers.get("x-api-key");
|
|
580
|
+
}
|
|
581
|
+
function isAuthorized(request, options) {
|
|
582
|
+
const token = apiToken(options);
|
|
583
|
+
if (!token)
|
|
584
|
+
return true;
|
|
585
|
+
return requestToken(request) === token;
|
|
586
|
+
}
|
|
587
|
+
async function readJsonBody(request) {
|
|
588
|
+
try {
|
|
589
|
+
const parsed = await request.json();
|
|
590
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
591
|
+
} catch {
|
|
592
|
+
return {};
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function requireMethod(store, method) {
|
|
596
|
+
const fn = store[method];
|
|
597
|
+
if (typeof fn !== "function")
|
|
598
|
+
throw new Error(`Store does not support ${String(method)}.`);
|
|
599
|
+
return fn.bind(store);
|
|
600
|
+
}
|
|
601
|
+
async function handleApi(request, apiPath, store, options) {
|
|
602
|
+
if (apiPath === "/health") {
|
|
603
|
+
return json({ ok: true, service: "shortlinks", api_auth_required: Boolean(apiToken(options)), stats: await store.totalStats() });
|
|
604
|
+
}
|
|
605
|
+
if (!isAuthorized(request, options))
|
|
606
|
+
return json({ error: "Unauthorized" }, 401);
|
|
607
|
+
const url = new URL(request.url);
|
|
608
|
+
const segments = apiPath.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
609
|
+
try {
|
|
610
|
+
if (apiPath === "/links" && request.method === "GET") {
|
|
611
|
+
const listLinks = requireMethod(store, "listLinks");
|
|
612
|
+
return json(await listLinks({
|
|
613
|
+
domain: url.searchParams.get("domain") || undefined,
|
|
614
|
+
activeOnly: url.searchParams.get("active") === "true",
|
|
615
|
+
limit: Number(url.searchParams.get("limit") || "100")
|
|
616
|
+
}));
|
|
617
|
+
}
|
|
618
|
+
if (apiPath === "/links" && request.method === "POST") {
|
|
619
|
+
const body = await readJsonBody(request);
|
|
620
|
+
const destinationUrl = String(body.destination_url || body.url || "");
|
|
621
|
+
if (!destinationUrl)
|
|
622
|
+
return json({ error: "destination_url is required" }, 400);
|
|
623
|
+
const createLink = requireMethod(store, "createLink");
|
|
624
|
+
return json(await createLink({
|
|
625
|
+
destinationUrl,
|
|
626
|
+
domain: typeof body.domain === "string" ? body.domain : undefined,
|
|
627
|
+
slug: typeof body.slug === "string" ? body.slug : undefined,
|
|
628
|
+
title: typeof body.title === "string" ? body.title : undefined,
|
|
629
|
+
expiresAt: typeof body.expires_at === "string" ? body.expires_at : typeof body.expires === "string" ? body.expires : undefined,
|
|
630
|
+
maxUses: typeof body.max_uses === "number" ? body.max_uses : typeof body.maxUses === "number" ? body.maxUses : undefined,
|
|
631
|
+
slugLength: typeof body.length === "number" ? body.length : undefined
|
|
632
|
+
}), 201);
|
|
633
|
+
}
|
|
634
|
+
if (segments[0] === "links" && segments[1] && request.method === "GET") {
|
|
635
|
+
const getLink = requireMethod(store, "getLink");
|
|
636
|
+
const domain = url.searchParams.get("domain") || undefined;
|
|
637
|
+
const link = domain ? await getLink(domain, segments[1]) : await getLink(segments[1]);
|
|
638
|
+
return link ? json(link) : json({ error: "Link not found." }, 404);
|
|
639
|
+
}
|
|
640
|
+
if (segments[0] === "links" && segments[1] && request.method === "DELETE") {
|
|
641
|
+
const deleteLink = requireMethod(store, "deleteLink");
|
|
642
|
+
const domain = url.searchParams.get("domain") || undefined;
|
|
643
|
+
return json(domain ? await deleteLink(domain, segments[1]) : await deleteLink(segments[1]));
|
|
644
|
+
}
|
|
645
|
+
if (segments[0] === "links" && segments[1] && segments[2] === "active" && request.method === "POST") {
|
|
646
|
+
const body = await readJsonBody(request);
|
|
647
|
+
const setLinkActive = requireMethod(store, "setLinkActive");
|
|
648
|
+
const domain = url.searchParams.get("domain") || undefined;
|
|
649
|
+
const active = Boolean(body.active);
|
|
650
|
+
return json(domain ? await setLinkActive(domain, segments[1], active) : await setLinkActive(segments[1], active));
|
|
651
|
+
}
|
|
652
|
+
if (apiPath === "/stats" && request.method === "GET") {
|
|
653
|
+
return json(await store.totalStats());
|
|
654
|
+
}
|
|
655
|
+
if (segments[0] === "stats" && segments[1] && request.method === "GET") {
|
|
656
|
+
const getStats = requireMethod(store, "getStats");
|
|
657
|
+
const domain = url.searchParams.get("domain") || undefined;
|
|
658
|
+
return json(domain ? await getStats(domain, segments[1]) : await getStats(segments[1]));
|
|
659
|
+
}
|
|
660
|
+
if (apiPath === "/domains" && request.method === "GET") {
|
|
661
|
+
const listDomains = requireMethod(store, "listDomains");
|
|
662
|
+
return json(await listDomains());
|
|
663
|
+
}
|
|
664
|
+
if (apiPath === "/domains" && request.method === "POST") {
|
|
665
|
+
const body = await readJsonBody(request);
|
|
666
|
+
const hostname2 = String(body.hostname || "");
|
|
667
|
+
if (!hostname2)
|
|
668
|
+
return json({ error: "hostname is required" }, 400);
|
|
669
|
+
const addDomain = requireMethod(store, "addDomain");
|
|
670
|
+
return json(await addDomain({
|
|
671
|
+
hostname: hostname2,
|
|
672
|
+
provider: typeof body.provider === "string" ? body.provider : "manual",
|
|
673
|
+
defaultDomain: Boolean(body.default_domain || body.default),
|
|
674
|
+
originUrl: typeof body.origin_url === "string" ? body.origin_url : undefined,
|
|
675
|
+
notes: typeof body.notes === "string" ? body.notes : undefined
|
|
676
|
+
}), 201);
|
|
677
|
+
}
|
|
678
|
+
if (segments[0] === "domains" && segments[1] && request.method === "GET") {
|
|
679
|
+
const getDomain = requireMethod(store, "getDomain");
|
|
680
|
+
const domain = await getDomain(segments[1]);
|
|
681
|
+
return domain ? json(domain) : json({ error: "Domain not found." }, 404);
|
|
682
|
+
}
|
|
683
|
+
} catch (error) {
|
|
684
|
+
return json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
685
|
+
}
|
|
686
|
+
return json({ error: "Not found." }, 404);
|
|
687
|
+
}
|
|
541
688
|
function getHost(request, fallback) {
|
|
542
689
|
const forwarded = request.headers.get("x-forwarded-host");
|
|
543
690
|
const host = forwarded || request.headers.get("host") || fallback || "";
|
|
@@ -556,8 +703,13 @@ function createShortlinksHandler(options = {}) {
|
|
|
556
703
|
const store = options.store || new ShortlinksStore(options.dbPath);
|
|
557
704
|
const redirectStatus = options.redirectStatus || 302;
|
|
558
705
|
const reservedPathPrefixes = new Set((options.reservedPathPrefixes ?? ["a"]).map((prefix) => prefix.toLowerCase()));
|
|
706
|
+
const apiPathPrefix = (options.apiPathPrefix || process.env.SHORTLINKS_API_PATH_PREFIX || "/api").replace(/\/+$/, "") || "/api";
|
|
559
707
|
return async (request) => {
|
|
560
708
|
const url = new URL(request.url);
|
|
709
|
+
if (url.pathname === apiPathPrefix || url.pathname.startsWith(`${apiPathPrefix}/`)) {
|
|
710
|
+
const apiPath = url.pathname.slice(apiPathPrefix.length) || "/";
|
|
711
|
+
return handleApi(request, apiPath, store, options);
|
|
712
|
+
}
|
|
561
713
|
if (url.pathname === "/healthz") {
|
|
562
714
|
return json({ ok: true, service: "shortlinks", stats: await store.totalStats() });
|
|
563
715
|
}
|
|
@@ -590,7 +742,14 @@ function createShortlinksHandler(options = {}) {
|
|
|
590
742
|
return json({ error: "Shortlink is disabled.", slug, host }, 410);
|
|
591
743
|
if (isExpired(link))
|
|
592
744
|
return json({ error: "Shortlink is expired.", slug, host }, 410);
|
|
593
|
-
|
|
745
|
+
if (link.max_uses !== null && link.used_count >= link.max_uses) {
|
|
746
|
+
return json({ error: "Shortlink max uses reached.", slug, host }, 410);
|
|
747
|
+
}
|
|
748
|
+
const consumed = store.consumeLinkUse ? await store.consumeLinkUse(link) : link;
|
|
749
|
+
if (!consumed) {
|
|
750
|
+
return json({ error: "Shortlink max uses reached.", slug, host }, 410);
|
|
751
|
+
}
|
|
752
|
+
await store.recordClick(consumed, {
|
|
594
753
|
ip: getClientIp(request),
|
|
595
754
|
userAgent: request.headers.get("user-agent"),
|
|
596
755
|
referer: request.headers.get("referer"),
|
package/dist/storage.js
CHANGED
|
@@ -5139,17 +5139,31 @@ function saveConfig(config) {
|
|
|
5139
5139
|
`);
|
|
5140
5140
|
}
|
|
5141
5141
|
function updateConfig(patch) {
|
|
5142
|
+
const current = loadConfig();
|
|
5142
5143
|
const next = {
|
|
5143
|
-
...
|
|
5144
|
+
...current,
|
|
5144
5145
|
...patch,
|
|
5146
|
+
api: {
|
|
5147
|
+
...current.api,
|
|
5148
|
+
...patch.api
|
|
5149
|
+
},
|
|
5145
5150
|
cloudflare: {
|
|
5146
|
-
...
|
|
5151
|
+
...current.cloudflare,
|
|
5147
5152
|
...patch.cloudflare
|
|
5148
5153
|
}
|
|
5149
5154
|
};
|
|
5150
5155
|
saveConfig(next);
|
|
5151
5156
|
return next;
|
|
5152
5157
|
}
|
|
5158
|
+
function getApiBaseUrl(config = loadConfig()) {
|
|
5159
|
+
const baseUrl = process.env.SHORTLINKS_API_URL || process.env.HASNA_SHORTLINKS_API_URL || config.api?.baseUrl || "";
|
|
5160
|
+
return baseUrl ? baseUrl.replace(/\/+$/, "") : null;
|
|
5161
|
+
}
|
|
5162
|
+
function getApiToken(config = loadConfig()) {
|
|
5163
|
+
const envName = config.api?.tokenEnv || "SHORTLINKS_API_TOKEN";
|
|
5164
|
+
const token = process.env[envName] || process.env.SHORTLINKS_API_TOKEN || process.env.HASNA_SHORTLINKS_API_TOKEN || config.api?.token || "";
|
|
5165
|
+
return token || null;
|
|
5166
|
+
}
|
|
5153
5167
|
function normalizeHostname(input) {
|
|
5154
5168
|
const raw = input.trim().toLowerCase();
|
|
5155
5169
|
if (!raw)
|
|
@@ -5246,6 +5260,10 @@ var SQLITE_MIGRATIONS = [
|
|
|
5246
5260
|
CREATE INDEX IF NOT EXISTS idx_clicks_domain ON clicks(domain_id);
|
|
5247
5261
|
CREATE INDEX IF NOT EXISTS idx_clicks_clicked_at ON clicks(clicked_at);
|
|
5248
5262
|
CREATE INDEX IF NOT EXISTS idx_clicks_updated ON clicks(updated_at);
|
|
5263
|
+
`,
|
|
5264
|
+
`
|
|
5265
|
+
ALTER TABLE links ADD COLUMN max_uses INTEGER;
|
|
5266
|
+
ALTER TABLE links ADD COLUMN used_count INTEGER NOT NULL DEFAULT 0;
|
|
5249
5267
|
`
|
|
5250
5268
|
];
|
|
5251
5269
|
|
|
@@ -5351,6 +5369,10 @@ var PG_MIGRATIONS = [
|
|
|
5351
5369
|
CREATE INDEX IF NOT EXISTS idx_clicks_domain ON clicks(domain_id);
|
|
5352
5370
|
CREATE INDEX IF NOT EXISTS idx_clicks_clicked_at ON clicks(clicked_at);
|
|
5353
5371
|
CREATE INDEX IF NOT EXISTS idx_clicks_updated ON clicks(updated_at);
|
|
5372
|
+
`,
|
|
5373
|
+
`
|
|
5374
|
+
ALTER TABLE links ADD COLUMN IF NOT EXISTS max_uses INTEGER;
|
|
5375
|
+
ALTER TABLE links ADD COLUMN IF NOT EXISTS used_count INTEGER NOT NULL DEFAULT 0;
|
|
5354
5376
|
`
|
|
5355
5377
|
];
|
|
5356
5378
|
|
package/dist/store.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare class ShortlinksStore {
|
|
|
9
9
|
getDomain(hostnameOrId: string): Domain | null;
|
|
10
10
|
getDefaultDomain(): Domain | null;
|
|
11
11
|
createLink(input: CreateLinkInput): Link;
|
|
12
|
+
consumeLinkUse(link: Link): Link | null;
|
|
12
13
|
listLinks(options?: {
|
|
13
14
|
domain?: string;
|
|
14
15
|
activeOnly?: boolean;
|
package/dist/types.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface Link {
|
|
|
23
23
|
title: string | null;
|
|
24
24
|
active: boolean;
|
|
25
25
|
expires_at: string | null;
|
|
26
|
+
max_uses: number | null;
|
|
27
|
+
used_count: number;
|
|
26
28
|
metadata: Record<string, unknown>;
|
|
27
29
|
machine_id: string | null;
|
|
28
30
|
synced_at: string | null;
|
|
@@ -66,6 +68,7 @@ export interface CreateLinkInput {
|
|
|
66
68
|
slug?: string;
|
|
67
69
|
title?: string;
|
|
68
70
|
expiresAt?: string;
|
|
71
|
+
maxUses?: number | null;
|
|
69
72
|
metadata?: Record<string, unknown>;
|
|
70
73
|
slugLength?: number;
|
|
71
74
|
}
|
|
@@ -9,6 +9,7 @@ export RDS_HOST="${RDS_HOST:-}"
|
|
|
9
9
|
export RDS_USERNAME="${RDS_USERNAME:-}"
|
|
10
10
|
export SHORTLINKS_DOMAIN="${SHORTLINKS_DOMAIN:-}"
|
|
11
11
|
export ATTACHMENTS_ORIGIN="${ATTACHMENTS_ORIGIN:-}"
|
|
12
|
+
export SHORTLINKS_API_PATH_PREFIX="${SHORTLINKS_API_PATH_PREFIX:-/_shortlinks/api}"
|
|
12
13
|
|
|
13
14
|
: "${RDS_SECRET_ID:?Set RDS_SECRET_ID to the AWS Secrets Manager secret for the PostgreSQL database_url}"
|
|
14
15
|
: "${SHORTLINKS_DOMAIN:?Set SHORTLINKS_DOMAIN to the public host served by Caddy}"
|
|
@@ -85,6 +86,7 @@ AWS_REGION=${AWS_REGION}
|
|
|
85
86
|
RDS_SECRET_ID=${RDS_SECRET_ID}
|
|
86
87
|
SHORTLINKS_DOMAIN=${SHORTLINKS_DOMAIN}
|
|
87
88
|
SHORTLINKS_STORE=remote
|
|
89
|
+
SHORTLINKS_API_PATH_PREFIX=${SHORTLINKS_API_PATH_PREFIX}
|
|
88
90
|
SHORTLINKS_ENV
|
|
89
91
|
chmod 640 /etc/default/shortlinks
|
|
90
92
|
chown root:shortlinks /etc/default/shortlinks
|
|
@@ -114,7 +116,7 @@ WorkingDirectory=/var/lib/shortlinks
|
|
|
114
116
|
Environment=HOME=/var/lib/shortlinks
|
|
115
117
|
Environment=PATH=/var/lib/shortlinks/.bun/bin:/usr/local/bin:/usr/bin:/bin
|
|
116
118
|
EnvironmentFile=/etc/default/shortlinks
|
|
117
|
-
ExecStart=/usr/local/bin/shortlinks-env-exec shortlinks serve --remote --host 127.0.0.1 --port 8787 --default-host ${SHORTLINKS_DOMAIN}
|
|
119
|
+
ExecStart=/usr/local/bin/shortlinks-env-exec shortlinks serve --remote --host 127.0.0.1 --port 8787 --default-host ${SHORTLINKS_DOMAIN} --api-path-prefix ${SHORTLINKS_API_PATH_PREFIX}
|
|
118
120
|
Restart=always
|
|
119
121
|
RestartSec=5
|
|
120
122
|
|
|
@@ -159,6 +161,10 @@ ${SHORTLINKS_DOMAIN} {
|
|
|
159
161
|
reverse_proxy ${ATTACHMENTS_ORIGIN}
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
handle /_shortlinks/* {
|
|
165
|
+
reverse_proxy 127.0.0.1:8787
|
|
166
|
+
}
|
|
167
|
+
|
|
162
168
|
handle {
|
|
163
169
|
reverse_proxy 127.0.0.1:8787
|
|
164
170
|
}
|
package/package.json
CHANGED