emdash-plugin-relink 0.2.2

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTRIBUTING.md +21 -0
  3. package/LICENSE +167 -0
  4. package/LICENSES/MIT-legacy.txt +21 -0
  5. package/README.md +95 -0
  6. package/THIRD_PARTY_NOTICES.md +19 -0
  7. package/dist/admin/app.d.ts +9 -0
  8. package/dist/admin/app.d.ts.map +1 -0
  9. package/dist/admin/app.js +333 -0
  10. package/dist/admin/app.js.map +1 -0
  11. package/dist/admin/client.d.ts +9 -0
  12. package/dist/admin/client.d.ts.map +1 -0
  13. package/dist/admin/client.js +22 -0
  14. package/dist/admin/client.js.map +1 -0
  15. package/dist/admin/i18n.d.ts +8 -0
  16. package/dist/admin/i18n.d.ts.map +1 -0
  17. package/dist/admin/i18n.js +35 -0
  18. package/dist/admin/i18n.js.map +1 -0
  19. package/dist/admin/index.d.ts +18 -0
  20. package/dist/admin/index.d.ts.map +1 -0
  21. package/dist/admin/index.js +22 -0
  22. package/dist/admin/index.js.map +1 -0
  23. package/dist/admin/messages.d.ts +270 -0
  24. package/dist/admin/messages.d.ts.map +1 -0
  25. package/dist/admin/messages.js +269 -0
  26. package/dist/admin/messages.js.map +1 -0
  27. package/dist/admin/styles.css +330 -0
  28. package/dist/core/api.d.ts +559 -0
  29. package/dist/core/api.d.ts.map +1 -0
  30. package/dist/core/api.js +76 -0
  31. package/dist/core/api.js.map +1 -0
  32. package/dist/core/engine.d.ts +30 -0
  33. package/dist/core/engine.d.ts.map +1 -0
  34. package/dist/core/engine.js +392 -0
  35. package/dist/core/engine.js.map +1 -0
  36. package/dist/core/extract.d.ts +11 -0
  37. package/dist/core/extract.d.ts.map +1 -0
  38. package/dist/core/extract.js +99 -0
  39. package/dist/core/extract.js.map +1 -0
  40. package/dist/core/html.d.ts +12 -0
  41. package/dist/core/html.d.ts.map +1 -0
  42. package/dist/core/html.js +103 -0
  43. package/dist/core/html.js.map +1 -0
  44. package/dist/core/http.d.ts +22 -0
  45. package/dist/core/http.d.ts.map +1 -0
  46. package/dist/core/http.js +217 -0
  47. package/dist/core/http.js.map +1 -0
  48. package/dist/core/index.d.ts +10 -0
  49. package/dist/core/index.d.ts.map +1 -0
  50. package/dist/core/index.js +10 -0
  51. package/dist/core/index.js.map +1 -0
  52. package/dist/core/paths.d.ts +4 -0
  53. package/dist/core/paths.d.ts.map +1 -0
  54. package/dist/core/paths.js +38 -0
  55. package/dist/core/paths.js.map +1 -0
  56. package/dist/core/providers.d.ts +32 -0
  57. package/dist/core/providers.d.ts.map +1 -0
  58. package/dist/core/providers.js +198 -0
  59. package/dist/core/providers.js.map +1 -0
  60. package/dist/core/schema.d.ts +526 -0
  61. package/dist/core/schema.d.ts.map +1 -0
  62. package/dist/core/schema.js +238 -0
  63. package/dist/core/schema.js.map +1 -0
  64. package/dist/core/storage.d.ts +47 -0
  65. package/dist/core/storage.d.ts.map +1 -0
  66. package/dist/core/storage.js +89 -0
  67. package/dist/core/storage.js.map +1 -0
  68. package/dist/core/url.d.ts +7 -0
  69. package/dist/core/url.d.ts.map +1 -0
  70. package/dist/core/url.js +62 -0
  71. package/dist/core/url.js.map +1 -0
  72. package/dist/index.d.ts +8 -0
  73. package/dist/index.d.ts.map +1 -0
  74. package/dist/index.js +16 -0
  75. package/dist/index.js.map +1 -0
  76. package/dist/integration.d.ts +5 -0
  77. package/dist/integration.d.ts.map +1 -0
  78. package/dist/integration.js +29 -0
  79. package/dist/integration.js.map +1 -0
  80. package/dist/metadata.d.ts +14 -0
  81. package/dist/metadata.d.ts.map +1 -0
  82. package/dist/metadata.js +6 -0
  83. package/dist/metadata.js.map +1 -0
  84. package/dist/middleware.d.ts +8 -0
  85. package/dist/middleware.d.ts.map +1 -0
  86. package/dist/middleware.js +97 -0
  87. package/dist/middleware.js.map +1 -0
  88. package/dist/plugin.d.ts +13 -0
  89. package/dist/plugin.d.ts.map +1 -0
  90. package/dist/plugin.js +260 -0
  91. package/dist/plugin.js.map +1 -0
  92. package/docs/architecture.md +50 -0
  93. package/docs/configuration.md +37 -0
  94. package/docs/installation.md +71 -0
  95. package/docs/licensing.md +42 -0
  96. package/docs/recovery.md +27 -0
  97. package/docs/testing.md +31 -0
  98. package/docs/translations.md +24 -0
  99. package/package.json +124 -0
@@ -0,0 +1,97 @@
1
+ import { getPublicPluginApiRouteHandler } from "emdash/plugin-utils";
2
+ import { projectionSchema } from "./core/api.js";
3
+ import { rewriteLinks } from "./core/html.js";
4
+ import { readLimited } from "./core/http.js";
5
+ import { stableId } from "./core/url.js";
6
+ const fingerprints = new WeakMap();
7
+ /** No external I/O: only the local, public EmDash dispatcher is used. */
8
+ export async function renderResponse(response, request, locals) {
9
+ const url = new URL(request.url);
10
+ if (request.method !== "GET" ||
11
+ url.pathname.startsWith("/_emdash") ||
12
+ [...url.searchParams.keys()].some((key) => /preview|edit/iu.test(key)) ||
13
+ response.status !== 200 ||
14
+ !response.headers.get("content-type")?.includes("text/html"))
15
+ return response;
16
+ const headers = new Headers(response.headers);
17
+ // HTML must reach the origin within five minutes, including failure fallback.
18
+ // Private/no-store pages keep their stronger policy; public pages receive a cap.
19
+ if (!/private|no-store/iu.test(headers.get("cache-control") ?? ""))
20
+ headers.set("cache-control", "public, max-age=0, s-maxage=300, must-revalidate");
21
+ headers.delete("etag");
22
+ headers.delete("last-modified");
23
+ const fallback = () => new Response(response.body, {
24
+ status: response.status,
25
+ statusText: response.statusText,
26
+ headers,
27
+ });
28
+ const dispatch = getPublicPluginApiRouteHandler(locals);
29
+ if (!dispatch || response.headers.has("content-encoding"))
30
+ return fallback();
31
+ let timeout;
32
+ try {
33
+ const requestUrl = new URL("/_emdash/api/plugins/relink/projection", url);
34
+ requestUrl.searchParams.set("path", url.pathname);
35
+ const result = await Promise.race([
36
+ dispatch("relink", "GET", "/projection", new Request(requestUrl)),
37
+ new Promise((_, reject) => {
38
+ timeout = setTimeout(() => reject(new Error("Relink projection timeout")), 1000);
39
+ }),
40
+ ]);
41
+ if (!result.success)
42
+ return fallback();
43
+ const projection = projectionSchema.parse(result.data);
44
+ if (projection.rules.length === 0)
45
+ return fallback();
46
+ const html = await readLimited(response.clone(), 2_000_000, 2000);
47
+ const output = rewriteLinks(html, new Map(projection.rules), url.href);
48
+ headers.delete("content-length");
49
+ headers.delete("content-encoding");
50
+ void response.body?.cancel().catch(() => undefined);
51
+ const resultResponse = new Response(output, {
52
+ status: response.status,
53
+ statusText: response.statusText,
54
+ headers,
55
+ });
56
+ fingerprints.set(resultResponse, await stableId(JSON.stringify(projection.rules)));
57
+ return resultResponse;
58
+ }
59
+ catch {
60
+ return fallback();
61
+ }
62
+ finally {
63
+ if (timeout)
64
+ clearTimeout(timeout);
65
+ }
66
+ }
67
+ /** Keep Astro's origin cache and downstream caches within one freshness budget. */
68
+ export async function capRenderCache(result, request, cache) {
69
+ const url = new URL(request.url);
70
+ if (request.method === "GET" &&
71
+ result.status === 200 &&
72
+ result.headers.get("content-type")?.includes("text/html") &&
73
+ !url.pathname.startsWith("/_emdash") &&
74
+ ![...url.searchParams.keys()].some((key) => /preview|edit/iu.test(key)) &&
75
+ cache.enabled &&
76
+ (cache.options.maxAge !== undefined || cache.tags.length > 0)) {
77
+ if (/private|no-store/iu.test(result.headers.get("cache-control") ?? ""))
78
+ cache.set(false);
79
+ else {
80
+ // Astro's provider wraps middleware and can overwrite response headers.
81
+ // Cap that inner cache too, then prevent another full TTL at an outer CDN.
82
+ cache.set({
83
+ maxAge: Math.min(cache.options.maxAge ?? 300, 300),
84
+ swr: 0,
85
+ etag: `"${await stableId(`${cache.options.etag ?? Date.now()}:${fingerprints.get(result) ?? "original"}`)}"`,
86
+ lastModified: new Date(),
87
+ });
88
+ result.headers.set("cache-control", "public, max-age=0, must-revalidate");
89
+ }
90
+ }
91
+ }
92
+ export const onRequest = async (context, next) => {
93
+ const result = await renderResponse(await next(), context.request, context.locals);
94
+ await capRenderCache(result, context.request, context.cache);
95
+ return result;
96
+ };
97
+ //# sourceMappingURL=middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { PluginContext, ResolvedPlugin, RouteContext } from "emdash";
2
+ import type { AdminSnapshot } from "./core/api.js";
3
+ import type { ResolvedOptions } from "./core/schema.js";
4
+ import { RelinkStore } from "./core/storage.js";
5
+ export declare function requireMethod(request: Request, method: "GET" | "POST"): void;
6
+ export declare function ensureScheduled(context: Pick<PluginContext, "cron">): Promise<void>;
7
+ export declare function adminSnapshot(store: RelinkStore, context: Pick<RouteContext, "request" | "cron" | "user">): Promise<AdminSnapshot>;
8
+ /** Public output contains only destinations already exposed by the published page. */
9
+ export declare function pageProjection(store: RelinkStore, context: Pick<PluginContext, "content">, options: ResolvedOptions, pathname: string): Promise<{
10
+ rules: Array<[string, string]>;
11
+ }>;
12
+ export declare function createPlugin(rawOptions: unknown): ResolvedPlugin;
13
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAI1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAInD,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,WAAW,EAAqB,MAAM,mBAAmB,CAAC;AAKnE,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,IAAI,CAM5E;AACD,wBAAsB,eAAe,CACnC,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,GACnC,OAAO,CAAC,IAAI,CAAC,CAKf;AAUD,wBAAsB,aAAa,CACjC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC,GACvD,OAAO,CAAC,aAAa,CAAC,CA+GxB;AAED,sFAAsF;AACtF,wBAAsB,cAAc,CAClC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,EACvC,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,KAAK,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,CAAC,CAgE7C;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,OAAO,GAAG,cAAc,CAsGhE"}
package/dist/plugin.js ADDED
@@ -0,0 +1,260 @@
1
+ import { definePlugin, PluginRouteError } from "emdash";
2
+ import { z } from "zod";
3
+ import { RelinkEngine } from "./core/engine.js";
4
+ import { effectiveDestination, listQuerySchema } from "./core/api.js";
5
+ import { extractOccurrences } from "./core/extract.js";
6
+ import { indexedPaths } from "./core/paths.js";
7
+ import { commandInputSchema, optionsSchema } from "./core/schema.js";
8
+ import { RelinkStore, storageDefinition } from "./core/storage.js";
9
+ import { isPublicUrl } from "./core/url.js";
10
+ import { adminConfiguration } from "./metadata.js";
11
+ const TASK = "maintenance-v1";
12
+ export function requireMethod(request, method) {
13
+ if (request.method !== method &&
14
+ !(method === "GET" && request.method === "HEAD"))
15
+ throw new PluginRouteError("METHOD_NOT_ALLOWED", "Method not allowed", 405);
16
+ }
17
+ export async function ensureScheduled(context) {
18
+ if (!context.cron)
19
+ throw new Error("EmDash cron is required");
20
+ const tasks = await context.cron.list();
21
+ if (!tasks.some((task) => task.name === TASK))
22
+ await context.cron.schedule(TASK, { schedule: "* * * * *" });
23
+ }
24
+ async function enqueue(store, input) {
25
+ const id = crypto.randomUUID();
26
+ await store.commands.put(id, { id, createdAt: Date.now(), input });
27
+ return id;
28
+ }
29
+ export async function adminSnapshot(store, context) {
30
+ const query = listQuerySchema.parse(Object.fromEntries(new URL(context.request.url).searchParams));
31
+ const [allLinks, allOccurrences, settings, worker, scheduled, commands] = await Promise.all([
32
+ store.links.all(),
33
+ store.occurrences.all(),
34
+ store.settings(),
35
+ store.state(),
36
+ context.cron?.list() ?? [],
37
+ store.commands.all(),
38
+ ]);
39
+ const now = Date.now();
40
+ const active = allLinks.filter((link) => link.active);
41
+ const matches = (query.view === "history" ? allLinks : active)
42
+ .filter((link) => {
43
+ const occurrences = allOccurrences.filter((occurrence) => occurrence.linkId === link.id);
44
+ if (query.view === "problems" && link.lastCheck?.kind !== "broken")
45
+ return false;
46
+ if (query.view === "archives" && link.replacement?.kind !== "archive")
47
+ return false;
48
+ if (query.view === "reviews" && (!link.review || link.review.dueAt > now))
49
+ return false;
50
+ if (query.view === "grokipedia" &&
51
+ !link.host.endsWith("wikipedia.org") &&
52
+ link.replacement?.kind !== "grokipedia")
53
+ return false;
54
+ if (query.domain && !link.host.includes(query.domain.toLowerCase()))
55
+ return false;
56
+ if (query.state &&
57
+ query.state !==
58
+ (link.excluded ? "excluded" : (link.lastCheck?.kind ?? "pending")))
59
+ return false;
60
+ if (query.reviewBefore !== undefined &&
61
+ (!link.review || link.review.dueAt > query.reviewBefore))
62
+ return false;
63
+ const search = query.search.toLowerCase();
64
+ if (search &&
65
+ !`${link.url} ${occurrences.map((entry) => `${entry.title} ${entry.text}`).join(" ")}`
66
+ .toLowerCase()
67
+ .includes(search))
68
+ return false;
69
+ return ((!query.collection && !query.contentId && !query.language) ||
70
+ occurrences.some((entry) => (!query.collection || entry.collection === query.collection) &&
71
+ (!query.contentId || entry.contentId === query.contentId) &&
72
+ (!query.language ||
73
+ entry.locale.toLowerCase().split("-")[0] ===
74
+ query.language.toLowerCase().split("-")[0])));
75
+ })
76
+ .sort((a, b) => a.nextCheckAt - b.nextCheckAt || a.id.localeCompare(b.id));
77
+ const matchingIds = new Set(matches.map((link) => link.id));
78
+ const matchingHistory = query.view === "history"
79
+ ? (await store.history.all({ orderBy: { at: "desc" } })).filter((entry) => matchingIds.has(entry.linkId))
80
+ : [];
81
+ const history = matchingHistory.slice(query.offset, query.offset + query.limit);
82
+ const historyIds = new Set(history.map((entry) => entry.linkId));
83
+ const links = query.view === "history"
84
+ ? matches.filter((link) => historyIds.has(link.id))
85
+ : matches.slice(query.offset, query.offset + query.limit);
86
+ const ids = new Set(links.map((link) => link.id));
87
+ return {
88
+ links,
89
+ occurrences: allOccurrences.filter((entry) => ids.has(entry.linkId)),
90
+ history,
91
+ total: query.view === "history" ? matchingHistory.length : matches.length,
92
+ settings,
93
+ worker,
94
+ scheduled: scheduled.some((task) => task.name === TASK),
95
+ nextRunAt: scheduled.find((task) => task.name === TASK)?.nextRunAt
96
+ ? Date.parse(scheduled.find((task) => task.name === TASK)?.nextRunAt ?? "")
97
+ : null,
98
+ pendingCommands: commands.length,
99
+ canManage: (context.user?.role ?? 0) >= 50,
100
+ summary: {
101
+ attention: active.filter((link) => link.lastCheck?.kind === "broken")
102
+ .length,
103
+ archives: active.filter((link) => link.replacement?.kind === "archive")
104
+ .length,
105
+ reviews: active.filter((link) => link.review !== null && link.review.dueAt <= now).length,
106
+ unverifiable: active.filter((link) => link.lastCheck?.kind === "unverifiable").length,
107
+ },
108
+ };
109
+ }
110
+ /** Public output contains only destinations already exposed by the published page. */
111
+ export async function pageProjection(store, context, options, pathname) {
112
+ const settings = await store.settings();
113
+ if (!context.content)
114
+ return { rules: [] };
115
+ if (!settings.enabled)
116
+ return { rules: [] };
117
+ const candidates = await Promise.all(options.sources.map(async (source) => {
118
+ const pages = await Promise.all(indexedPaths(pathname, source).map((pagePath) => store.occurrences.all({
119
+ where: { pagePath, collection: source.collection },
120
+ })));
121
+ return pages.flat();
122
+ }));
123
+ const occurrences = [
124
+ ...new Map(candidates.flat().map((entry) => [entry.id, entry])).values(),
125
+ ];
126
+ const verified = new Set();
127
+ const documents = new Set();
128
+ for (const occurrence of occurrences) {
129
+ const key = `${occurrence.collection}/${occurrence.contentId}`;
130
+ if (documents.has(key))
131
+ continue;
132
+ documents.add(key);
133
+ const source = options.sources.find((entry) => entry.collection === occurrence.collection);
134
+ if (!source)
135
+ continue;
136
+ const content = await context.content.get(occurrence.collection, occurrence.contentId);
137
+ if (!content || content.status !== "published")
138
+ continue;
139
+ const live = await extractOccurrences({
140
+ id: content.id,
141
+ slug: content.slug ?? content.id,
142
+ locale: content.locale ?? "en",
143
+ title: "",
144
+ data: content.data,
145
+ }, source, options.siteUrl, "projection");
146
+ for (const entry of live)
147
+ if (indexedPaths(pathname, source).includes(entry.pagePath))
148
+ verified.add(entry.id);
149
+ }
150
+ const rules = [];
151
+ for (const occurrence of occurrences) {
152
+ if (!verified.has(occurrence.id))
153
+ continue;
154
+ const link = await store.links.get(occurrence.linkId);
155
+ if (!link)
156
+ continue;
157
+ const destination = effectiveDestination(link, occurrence.originalUrl, settings);
158
+ if (destination !== occurrence.originalUrl && isPublicUrl(destination))
159
+ rules.push([occurrence.originalUrl, destination]);
160
+ }
161
+ return { rules };
162
+ }
163
+ export function createPlugin(rawOptions) {
164
+ const options = optionsSchema.parse(rawOptions);
165
+ const store = (ctx) => new RelinkStore(ctx, options.settings);
166
+ const changed = async (_event, ctx) => {
167
+ await ensureScheduled(ctx);
168
+ await enqueue(store(ctx), { action: "scan" });
169
+ };
170
+ return definePlugin({
171
+ id: "relink",
172
+ version: "0.2.2",
173
+ capabilities: ["content:read", "network:request:unrestricted"],
174
+ storage: storageDefinition,
175
+ admin: adminConfiguration,
176
+ hooks: {
177
+ "plugin:install": async (_event, ctx) => {
178
+ await ensureScheduled(ctx);
179
+ },
180
+ "plugin:activate": async (_event, ctx) => {
181
+ await ensureScheduled(ctx);
182
+ },
183
+ "plugin:deactivate": async (_event, ctx) => {
184
+ await ctx.cron?.cancel(TASK);
185
+ },
186
+ "content:afterPublish": changed,
187
+ "content:afterSave": changed,
188
+ "content:afterUnpublish": changed,
189
+ "content:afterDelete": changed,
190
+ cron: {
191
+ timeout: 540000,
192
+ handler: async (event, ctx) => {
193
+ if (!ctx.content || !ctx.http)
194
+ throw new Error("Relink requires content and network access");
195
+ if (event.name === TASK)
196
+ await new RelinkEngine({
197
+ store: store(ctx),
198
+ content: ctx.content,
199
+ transport: ctx.http,
200
+ options,
201
+ }).tick();
202
+ },
203
+ },
204
+ },
205
+ routes: {
206
+ snapshot: {
207
+ permission: "content:read",
208
+ handler: async (ctx) => {
209
+ requireMethod(ctx.request, "GET");
210
+ return adminSnapshot(store(ctx), ctx);
211
+ },
212
+ },
213
+ detail: {
214
+ permission: "content:read",
215
+ handler: async (ctx) => {
216
+ requireMethod(ctx.request, "GET");
217
+ const id = z
218
+ .string()
219
+ .min(1)
220
+ .max(100)
221
+ .parse(new URL(ctx.request.url).searchParams.get("id"));
222
+ const storage = store(ctx);
223
+ return {
224
+ link: await storage.links.get(id),
225
+ occurrences: await storage.occurrences.all({
226
+ where: { linkId: id },
227
+ }),
228
+ history: (await storage.history.page({
229
+ where: { linkId: id },
230
+ orderBy: { at: "desc" },
231
+ limit: 100,
232
+ })).items,
233
+ };
234
+ },
235
+ },
236
+ commands: {
237
+ handler: async (ctx) => {
238
+ requireMethod(ctx.request, "POST");
239
+ const input = commandInputSchema.parse(ctx.input);
240
+ await ensureScheduled(ctx);
241
+ return { queued: await enqueue(store(ctx), input) };
242
+ },
243
+ },
244
+ projection: {
245
+ public: true,
246
+ cacheControl: "no-store",
247
+ handler: async (ctx) => {
248
+ requireMethod(ctx.request, "GET");
249
+ const path = z
250
+ .string()
251
+ .startsWith("/")
252
+ .max(2000)
253
+ .parse(new URL(ctx.request.url).searchParams.get("path"));
254
+ return pageProjection(store(ctx), ctx, options, path);
255
+ },
256
+ },
257
+ },
258
+ });
259
+ }
260
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ # Architecture and API
2
+
3
+ Relink separates discovery, network evidence, decisions and rendering. The native plugin uses EmDash content access, plugin storage, KV, native routes and the existing scheduler. The Astro integration calls only the in-process public route dispatcher. No EmDash core modification or visitor-side JavaScript is needed.
4
+
5
+ ```mermaid
6
+ flowchart LR
7
+ P[Published content] --> I[Occurrence index]
8
+ I --> J[Persisted background job]
9
+ J --> C[HTTP evidence]
10
+ C --> W[Wayback / optional Grokipedia]
11
+ W --> R[Saved replacement rules]
12
+ A[Admin commands] --> J
13
+ R --> H[Astro HTML response]
14
+ P --> H
15
+ ```
16
+
17
+ ## Type and trust boundaries
18
+
19
+ TypeScript enables `strict`, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `noImplicitOverride`, `noImplicitReturns`, unused checks and exhaustive switches. Project code disallows explicit `any`, unsafe assertions, unsafe calls/member access/assignment/returns, floating promises and type-check suppression. `skipLibCheck` skips third-party declaration internals only; all Relink source and tests are type checked. Zod schemas validate requests, configuration, HTTP provider responses and every persisted record read/write.
20
+
21
+ `CheckResult` distinguishes healthy, broken and unverifiable evidence. `ReplacementRule` distinguishes archive, Grokipedia and manual destinations. Exported declarations preserve those unions. Exceptions are durable state, not a volatile UI preference.
22
+
23
+ ## Persistence and claims
24
+
25
+ EmDash owns database migrations. Relink uses declared `links`, `occurrences`, `history` and `commands` storage collections plus `settings:v1` and `worker:v1` KV entries. Domain records carry `schemaVersion: 1`; unknown versions fail validation. Plugin namespacing prevents collisions with content or other plugins.
26
+
27
+ Only the `maintenance-v1` recurring job writes decisions and job cursors. Admin requests append commands. EmDash atomically claims the task in SQL and recovers running claims after ten minutes. A tick processes at most four commands, ten published entries from one collection and four target hosts. One initial target per host is selected per tick; archive/provider traffic is bounded by the four concurrent chains. Network requests have ten-second timeouts, at most five redirects and bounded response bodies. Relink's native hook budget is nine minutes, below the host's stale-claim window. Very large sites and slow database backends require capacity testing.
28
+
29
+ Scan cursors and generations are persisted after each page. Stale occurrences are swept only after a complete collection scan; interrupted scans preserve previous entries. Orphaned links become inactive while keeping their exceptions and history. Operations are idempotent and retryable; the storage API does not offer a transaction across all records, so this is at-least-once processing, not a claim of universal exactly-once execution.
30
+
31
+ ## Routes
32
+
33
+ Base path: `/_emdash/api/plugins/relink`. EmDash wraps results in its normal `{ success, data }` envelope. The client unwraps and validates it. Private routes inherit host authentication and CSRF protection; all cookie-authenticated calls require `X-EmDash-Request: 1`, and mutations additionally require `plugins:manage`. The client uses EmDash's API helper. Native route names in 0.37.0 do not select handlers by HTTP method, so every Relink handler explicitly rejects unexpected methods.
34
+
35
+ | Method and path | Access | Purpose |
36
+ | ------------------------ | ---------------- | --------------------------------------------------------------------------------------- |
37
+ | `GET /snapshot` | `content:read` | Filtered/paginated links or history, occurrences, totals, settings and scheduler health |
38
+ | `GET /detail?id=…` | `content:read` | One link, current occurrences and its latest 100 history entries |
39
+ | `POST /commands` | `plugins:manage` | Queue `scan`, `check`, `exclude`, `undo`, `destination`, `reschedule` or `settings` |
40
+ | `GET /projection?path=…` | Public, no-store | Only original/effective URL pairs belonging to a currently published page |
41
+
42
+ Snapshot query fields: `view`, `search`, `domain`, `collection`, `contentId`, `language`, `state`, `reviewBefore` (epoch milliseconds), `offset` and `limit` (1–100). History has the same pagination. Counts are calculated from the plugin index. CSV export walks all matching link pages and neutralizes spreadsheet formula prefixes; from the History tab it exports the matching links.
43
+
44
+ The public projection discloses no excerpts, drafts, settings or history. It revalidates publication state, the current slug/path and each indexed occurrence against live content before returning mappings. It does not trust an outdated index. Automatic mappings contain a verified fragment allowlist, so a newly added section cannot inherit an unverified replacement.
45
+
46
+ ## Rendering
47
+
48
+ The middleware parses HTML with parse5 and edits only anchor `href` source ranges, preserving the rest of the document. Only targets in the current page projection are changed. Image sources, scripts and other resources remain untouched. If another anchor on that page uses the same indexed URL, it receives the same rule. Preview/edit URLs, admin routes and non-HTML/partial responses are excluded.
49
+
50
+ Projection has a one-second deadline; HTML inspection has a two-second deadline and a 2 MB limit. Missing dispatch, invalid state, an outage or excessive response size preserves the original HTML. Link checking/provider calls never occur on this rendering path.
@@ -0,0 +1,37 @@
1
+ # Configuration
2
+
3
+ `relink(options: RelinkOptions)` validates configuration at startup. Invalid values cause an explicit validation error. Configuration is JSON-serializable and remains inside the package's descriptor/runtime boundary.
4
+
5
+ | Option | Meaning |
6
+ | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `siteUrl` | Absolute public site URL. Same-origin links are excluded. |
8
+ | `sources` | Explicit collections and fields to index. At least one is required. |
9
+ | `sources[].collection` | Existing EmDash collection slug. |
10
+ | `sources[].path` | Published pathname template, default `/{slug}`. Supports `{slug}` and `{locale}`. Must match the theme's actual path, including trailing slash. |
11
+ | `sources[].portableTextFields` | Root Portable Text fields, default `['content']`. Only used link annotations are scanned. |
12
+ | `sources[].urlFields` | Explicit selectors for URL strings. Dot notation and `[]` array wildcards are supported. |
13
+ | `settings` | Initial validated defaults; admin-saved values take precedence. |
14
+ | `networkProbeUrls` | Two independent public endpoints used to distinguish local network failures from destination failures. |
15
+
16
+ Custom blocks are supported through explicit field selectors, for example `content[].url`, `content[].button.href` or `resources[].link`. Relink does not guess URL-bearing fields or scan embedded media. Portable Text collects each contiguous linked span group as an occurrence. Identical normalized URLs share a network check; fragments remain on individual occurrences and must be separately verified for automatic replacement.
17
+
18
+ `ContentDocument`, `CollectionSource` and `extractOccurrences` provide a typed content-adapter boundary. `ArchiveProvider` and `EncyclopediaProvider` define typed provider contracts. External responses and persisted values pass runtime schemas before use.
19
+
20
+ ## Settings
21
+
22
+ | Setting | Default | Accepted values |
23
+ | ------------------- | ------- | -------------------------------------------- |
24
+ | `enabled` | `true` | Boolean |
25
+ | `archiveEnabled` | `true` | Boolean |
26
+ | `grokipediaEnabled` | `false` | Boolean |
27
+ | `direction` | `auto` | `auto`, `ltr`, `rtl` |
28
+ | `healthyDays` | `7` | Integer 1–365 |
29
+ | `reviewDays` | `7` | Integer 1–90 |
30
+ | `archiveRetryDays` | `30` | Integer 1–365 |
31
+ | `timeoutMs` | `10000` | Integer 1000–10000 |
32
+ | `batchSize` | `4` | Integer 1–4 |
33
+ | `excludedDomains` | `[]` | Domain names; also excludes their subdomains |
34
+
35
+ The initial settings form exposes activation, archives, Grokipedia, direction and domain exclusions. Timing/batch settings are configurable through typed configuration or the authenticated settings command. Changes are queued and applied by the single background writer, so the UI can display their pending state.
36
+
37
+ Manual overrides are checked before activation and take priority over automatic destinations. Undo creates a persistent automatic-replacement exception. Excluding a link suppresses checking and rewriting; including it again does not erase an undo exception. Setting a new manual destination remains possible.
@@ -0,0 +1,71 @@
1
+ # Installation and operation
2
+
3
+ Relink 0.2.2 targets **EmDash 0.37.0**. It requires a server-rendered Astro site. `relink()` registers the native plugin and its admin extensions; `relinkAstro()` registers output middleware. Both registrations are required. Include `react()` so EmDash's admin can hydrate. Do not place native plugins in EmDash's sandboxed plugin list.
4
+
5
+ Read the [licensing terms](licensing.md) before use. Material governed by the Relink Noncommercial License requires a separate agreement for commercial use; existing MIT rights in the 0.1.0 baseline remain unaffected.
6
+
7
+ The package exports JavaScript and TypeScript declarations from `.`, `/astro`, `/runtime`, `/middleware`, `/admin` and `/core`, plus `/styles.css`. The host loads `/runtime` and `/admin` from the descriptor; applications normally import only `relink` and `relinkAstro`.
8
+
9
+ ## Node and SQLite
10
+
11
+ Use the Node example under `examples/node`. After installing dependencies and building Relink:
12
+
13
+ ```sh
14
+ pnpm --filter relink-example-node seed
15
+ pnpm --filter relink-example-node dev
16
+ ```
17
+
18
+ Complete EmDash's normal setup and authentication. The sample seed creates one published article. The example does not include test authentication. EmDash schedules native cron tasks while the Node process is running. Keep the process supervised and retain its database on durable storage. Restart recovery uses the same persisted task and worker cursor.
19
+
20
+ Choose **Scan published content** once after installation. Configuration-registered native plugins do not reliably receive an install lifecycle callback on every existing deployment; the explicit action and content-change hooks initialize scheduling idempotently. The admin shows missing scheduling, queued actions, the last completed run and stalled work.
21
+
22
+ ## Cloudflare and D1
23
+
24
+ Use the configuration in `examples/cloudflare`. Set a real D1 database binding before deployment, apply EmDash's supported migrations, and configure the site's normal authentication and storage. The repository contains a placeholder database identifier and does not create Cloudflare resources.
25
+
26
+ ```ts
27
+ // src/worker.ts
28
+ export { default, PluginBridge } from "@emdash-cms/cloudflare/worker";
29
+ ```
30
+
31
+ ```jsonc
32
+ // Relevant wrangler.jsonc fields
33
+ {
34
+ "main": "./src/worker.ts",
35
+ "compatibility_date": "2026-09-01",
36
+ "compatibility_flags": ["nodejs_compat"],
37
+ "d1_databases": [
38
+ {
39
+ "binding": "DB",
40
+ "database_name": "your-database",
41
+ "database_id": "your-database-id",
42
+ },
43
+ ],
44
+ "triggers": { "crons": ["* * * * *"] },
45
+ }
46
+ ```
47
+
48
+ The supported EmDash Worker runs `runScheduledTasks()` from `scheduled()`. A request-only Worker cannot provide unattended link maintenance. Verify real Cron Trigger delivery and resource limits after deployment; the repository's D1 test runs in local workerd, and the Cloudflare example build checks the Worker entry and bindings. No remote deployment is part of package validation.
49
+
50
+ ## Cache behaviour
51
+
52
+ Relink uses saved rules and local EmDash dispatch during rendering. Visitor requests never check external URLs. Public HTML receives a maximum five-minute shared-cache lifetime with no stale-while-revalidate allowance. When Astro's own route cache is active, its lifetime is capped at 300 seconds, its validators include replacement state, and downstream caches must revalidate to avoid stacking another five minutes on top. Explicit private/no-store responses keep their stronger policy.
53
+
54
+ Purge previously cached pages when first enabling Relink. A cache entry created before installation cannot be shortened retroactively. CDN rules that override origin cache headers must also enforce the five-minute bound. Static/prerendered pages require a rebuild and are outside this release's automatic replacement guarantee. Compressed responses must be transformed before compression; Relink preserves an already compressed response rather than damaging it.
55
+
56
+ ## Disable and remove
57
+
58
+ Disabling maintenance in Relink Settings stops scanning/checking and output replacement after the queued settings action runs. Disabling the plugin through EmDash cancels its cron task; removing both registrations stops loading Relink entirely. Output falls back to originals. Stored data remains plugin-owned; back up the database before using EmDash's uninstall/data-removal options. No content rollback migration is required because content was never changed.
59
+
60
+ ## Content route aliases
61
+
62
+ Set each source's `path` to its indexed route. If the same published content is also served through other routes, list those explicitly in `pathAliases`. Both templates support `{slug}` and `{locale}`; locale-bearing routes must preserve the locale. Aliases reuse the same occurrences and check history, and still require live publication validation. Unconfigured routes do not receive replacement rules. For example:
63
+
64
+ ```ts
65
+ {
66
+ collection: "posts",
67
+ path: "/posts/{slug}",
68
+ pathAliases: ["/{slug}", "/{slug}/", "/posts/{slug}/"],
69
+ portableTextFields: ["content"],
70
+ }
71
+ ```
@@ -0,0 +1,42 @@
1
+ # Licensing and commercial use
2
+
3
+ Relink is offered by **gekko, Gesellschaft für Kommunikation und Kooperation mbH**, Pappelallee 78/79, 10437 Berlin, Germany. The public licensing contact is [info2504@gekko.de](mailto:info2504@gekko.de). Company and contact details were verified against the [official imprint](https://gekko.de/pages/imprint).
4
+
5
+ Registration number: HRB 141428 B. VAT identification number: DE179073258.
6
+
7
+ The [Relink Noncommercial License 1.0](../LICENSE) permits free noncommercial use, inspection, modification, and free redistribution subject to its conditions. It reserves commercial use for a separate agreement with gekko. This is source-available software; the license is not an OSI-approved open-source license.
8
+
9
+ ## Which uses need an agreement?
10
+
11
+ | Use of material governed by the new license | Position |
12
+ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
13
+ | Personal hobby website without a commercial purpose | Permitted without a fee |
14
+ | Private study or independent noncommercial research | Permitted without a fee |
15
+ | Free sharing of a noncommercial modification with all required notices | Permitted under the license conditions |
16
+ | Company website or internal business operations | Commercial agreement required |
17
+ | Agency work, consulting, or a client project | Commercial agreement required |
18
+ | Advertising, affiliate income, paid access, or marketing | Commercial agreement required |
19
+ | Commercial hosted service, paid distribution, or product integration | Commercial agreement required |
20
+ | Nonprofit or educational organization | The actual activity determines eligibility; status alone is not an exemption |
21
+
22
+ This table summarizes the license. The complete license and any applicable separate agreement govern. Mixed-purpose use requires commercial permission when it supports a commercial activity. Contact gekko if the proposed use does not fit clearly within the permitted purposes. No commercial pricing, automatic purchase, or payment obligation is established by this repository.
23
+
24
+ ## Existing MIT rights remain
25
+
26
+ Relink **0.1.0**, through commit [`cd88fcc95999976d3744b64331bbf657975328c2`](https://github.com/giffeler/relink/tree/cd88fcc95999976d3744b64331bbf657975328c2), was publicly distributed under MIT. Its [original license notice](../LICENSES/MIT-legacy.txt) is retained. Recipients can continue to use, modify, distribute, and commercialize that material under MIT, subject to its conditions.
27
+
28
+ Version **0.2.0** records the licensing transition. It adds licensing documents and updates version metadata; it introduces no new link-maintenance functionality. Changing the license notice or the version number cannot remove MIT rights in the existing code. The new restrictions cover otherwise unlicensed additions and changes offered under the new license, rather than retroactively reserving the MIT baseline to gekko.
29
+
30
+ The commercial model therefore depends on future protected additions, separately licensed features, hosting, support, or other services. A historical MIT fork remains possible. Existing Git history, MIT artifacts, and third-party notices must not be deleted or rewritten to suggest otherwise.
31
+
32
+ ## Contributions and third-party material
33
+
34
+ Before accepting external code, documentation, or translation contributions, gekko must obtain the rights needed to distribute them under both this license and separate commercial terms. A DCO sign-off, an ordinary pull request, or the project's noncommercial license alone does not establish those additional commercial rights. See [CONTRIBUTING.md](../CONTRIBUTING.md).
35
+
36
+ Dependencies retain their own licenses. Relink does not claim exclusive rights over EmDash, Astro, React, Lingui, Kumo, parse5, Zod, or other third-party components. Preserve their notices when distributing those components. See [third-party licensing](../THIRD_PARTY_NOTICES.md).
37
+
38
+ ## Future releases and legal review
39
+
40
+ gekko reserves the option to offer new releases, features, subscriptions, services, and support commercially. New terms do not retrospectively change the permissions on copies already licensed. Separate commercial agreements must define their scope, fees, service obligations, and any warranties independently.
41
+
42
+ This project-specific license has not been reviewed by qualified legal counsel. Before relying on it for commercial enforcement or customer contracts, obtain review of enforceability, applicable mandatory law, the chain of rights, and the final contributor and commercial agreements. In particular, a repository notice cannot create copyright or other exclusive rights that the licensor does not hold.