@cogenta/schema 0.2.0 → 0.4.1
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/define-collection.d.ts.map +1 -1
- package/dist/define-collection.js +53 -3
- package/dist/define-collection.js.map +1 -1
- package/dist/generate-types.d.ts.map +1 -1
- package/dist/generate-types.js +6 -1
- package/dist/generate-types.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/media-usage.d.ts +56 -0
- package/dist/media-usage.d.ts.map +1 -0
- package/dist/media-usage.js +89 -0
- package/dist/media-usage.js.map +1 -0
- package/dist/rich-text.d.ts +28 -2
- package/dist/rich-text.d.ts.map +1 -1
- package/dist/rich-text.js +26 -3
- package/dist/rich-text.js.map +1 -1
- package/dist/routing/index.d.ts +5 -1
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/index.js +2 -0
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/not-found-log.d.ts +78 -0
- package/dist/routing/not-found-log.d.ts.map +1 -0
- package/dist/routing/not-found-log.js +153 -0
- package/dist/routing/not-found-log.js.map +1 -0
- package/dist/routing/redirect-patterns.d.ts +61 -0
- package/dist/routing/redirect-patterns.d.ts.map +1 -0
- package/dist/routing/redirect-patterns.js +173 -0
- package/dist/routing/redirect-patterns.js.map +1 -0
- package/dist/routing/redirects.d.ts +27 -4
- package/dist/routing/redirects.d.ts.map +1 -1
- package/dist/routing/redirects.js +160 -48
- package/dist/routing/redirects.js.map +1 -1
- package/dist/scheduling/index.d.ts +2 -0
- package/dist/scheduling/index.d.ts.map +1 -1
- package/dist/scheduling/index.js +1 -0
- package/dist/scheduling/index.js.map +1 -1
- package/dist/scheduling/registry.d.ts +147 -0
- package/dist/scheduling/registry.d.ts.map +1 -0
- package/dist/scheduling/registry.js +318 -0
- package/dist/scheduling/registry.js.map +1 -0
- package/dist/search/extract.d.ts +24 -0
- package/dist/search/extract.d.ts.map +1 -1
- package/dist/search/extract.js +63 -8
- package/dist/search/extract.js.map +1 -1
- package/dist/search/index.d.ts +2 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +1 -9
- package/dist/search/index.js.map +1 -1
- package/dist/store/admin-theme-store.d.ts +26 -0
- package/dist/store/admin-theme-store.d.ts.map +1 -0
- package/dist/store/admin-theme-store.js +81 -0
- package/dist/store/admin-theme-store.js.map +1 -0
- package/dist/store/admin-theme-tables.d.ts +24 -0
- package/dist/store/admin-theme-tables.d.ts.map +1 -0
- package/dist/store/admin-theme-tables.js +41 -0
- package/dist/store/admin-theme-tables.js.map +1 -0
- package/dist/store/admin-theme-templates.d.ts +135 -0
- package/dist/store/admin-theme-templates.d.ts.map +1 -0
- package/dist/store/admin-theme-templates.js +217 -0
- package/dist/store/admin-theme-templates.js.map +1 -0
- package/dist/store/columns.d.ts.map +1 -1
- package/dist/store/columns.js +8 -2
- package/dist/store/columns.js.map +1 -1
- package/dist/store/diff.d.ts +40 -0
- package/dist/store/diff.d.ts.map +1 -1
- package/dist/store/diff.js +148 -0
- package/dist/store/diff.js.map +1 -1
- package/dist/store/index.d.ts +47 -4
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +26 -2
- package/dist/store/index.js.map +1 -1
- package/dist/store/maintenance-store.d.ts +34 -0
- package/dist/store/maintenance-store.d.ts.map +1 -0
- package/dist/store/maintenance-store.js +92 -0
- package/dist/store/maintenance-store.js.map +1 -0
- package/dist/store/menu-store.d.ts +146 -0
- package/dist/store/menu-store.d.ts.map +1 -0
- package/dist/store/menu-store.js +565 -0
- package/dist/store/menu-store.js.map +1 -0
- package/dist/store/menu-tables.d.ts +38 -0
- package/dist/store/menu-tables.d.ts.map +1 -0
- package/dist/store/menu-tables.js +117 -0
- package/dist/store/menu-tables.js.map +1 -0
- package/dist/store/naming.d.ts +1 -1
- package/dist/store/naming.d.ts.map +1 -1
- package/dist/store/naming.js +2 -0
- package/dist/store/naming.js.map +1 -1
- package/dist/store/pattern-store.d.ts +75 -0
- package/dist/store/pattern-store.d.ts.map +1 -0
- package/dist/store/pattern-store.js +118 -0
- package/dist/store/pattern-store.js.map +1 -0
- package/dist/store/pattern-tables.d.ts +29 -0
- package/dist/store/pattern-tables.d.ts.map +1 -0
- package/dist/store/pattern-tables.js +53 -0
- package/dist/store/pattern-tables.js.map +1 -0
- package/dist/store/read-only.d.ts +3 -3
- package/dist/store/read-only.d.ts.map +1 -1
- package/dist/store/read-only.js +4 -3
- package/dist/store/read-only.js.map +1 -1
- package/dist/store/redirect-tracking.d.ts +50 -0
- package/dist/store/redirect-tracking.d.ts.map +1 -0
- package/dist/store/redirect-tracking.js +91 -0
- package/dist/store/redirect-tracking.js.map +1 -0
- package/dist/store/review-transitions.d.ts +34 -0
- package/dist/store/review-transitions.d.ts.map +1 -0
- package/dist/store/review-transitions.js +51 -0
- package/dist/store/review-transitions.js.map +1 -0
- package/dist/store/role-permission-export.d.ts +22 -0
- package/dist/store/role-permission-export.d.ts.map +1 -0
- package/dist/store/role-permission-export.js +0 -0
- package/dist/store/role-permission-export.js.map +1 -0
- package/dist/store/role-permission-overlay.d.ts +27 -0
- package/dist/store/role-permission-overlay.d.ts.map +1 -0
- package/dist/store/role-permission-overlay.js +0 -0
- package/dist/store/role-permission-overlay.js.map +1 -0
- package/dist/store/role-permission-store.d.ts +72 -0
- package/dist/store/role-permission-store.d.ts.map +1 -0
- package/dist/store/role-permission-store.js +148 -0
- package/dist/store/role-permission-store.js.map +1 -0
- package/dist/store/role-permission-tables.d.ts +27 -0
- package/dist/store/role-permission-tables.d.ts.map +1 -0
- package/dist/store/role-permission-tables.js +52 -0
- package/dist/store/role-permission-tables.js.map +1 -0
- package/dist/store/scheduled-publish-enqueue.d.ts +37 -0
- package/dist/store/scheduled-publish-enqueue.d.ts.map +1 -0
- package/dist/store/scheduled-publish-enqueue.js +51 -0
- package/dist/store/scheduled-publish-enqueue.js.map +1 -0
- package/dist/store/scheduled-publish-failures.d.ts +36 -0
- package/dist/store/scheduled-publish-failures.d.ts.map +1 -0
- package/dist/store/scheduled-publish-failures.js +67 -0
- package/dist/store/scheduled-publish-failures.js.map +1 -0
- package/dist/store/schema-2-1-migration.d.ts +29 -0
- package/dist/store/schema-2-1-migration.d.ts.map +1 -0
- package/dist/store/schema-2-1-migration.js +71 -0
- package/dist/store/schema-2-1-migration.js.map +1 -0
- package/dist/store/search-console-store.d.ts +44 -0
- package/dist/store/search-console-store.d.ts.map +1 -0
- package/dist/store/search-console-store.js +90 -0
- package/dist/store/search-console-store.js.map +1 -0
- package/dist/store/search-console-tables.d.ts +14 -0
- package/dist/store/search-console-tables.d.ts.map +1 -0
- package/dist/store/search-console-tables.js +29 -0
- package/dist/store/search-console-tables.js.map +1 -0
- package/dist/store/search-indexing.d.ts +36 -0
- package/dist/store/search-indexing.d.ts.map +1 -1
- package/dist/store/search-indexing.js +33 -0
- package/dist/store/search-indexing.js.map +1 -1
- package/dist/store/site-settings-registry.d.ts +164 -0
- package/dist/store/site-settings-registry.d.ts.map +1 -0
- package/dist/store/site-settings-registry.js +1160 -0
- package/dist/store/site-settings-registry.js.map +1 -0
- package/dist/store/site-settings-store.d.ts +42 -0
- package/dist/store/site-settings-store.d.ts.map +1 -0
- package/dist/store/site-settings-store.js +97 -0
- package/dist/store/site-settings-store.js.map +1 -0
- package/dist/store/site-settings-tables.d.ts +29 -0
- package/dist/store/site-settings-tables.d.ts.map +1 -0
- package/dist/store/site-settings-tables.js +48 -0
- package/dist/store/site-settings-tables.js.map +1 -0
- package/dist/store/store.d.ts +57 -2
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +278 -37
- package/dist/store/store.js.map +1 -1
- package/dist/store/tables.d.ts.map +1 -1
- package/dist/store/tables.js +11 -0
- package/dist/store/tables.js.map +1 -1
- package/dist/store/taxonomy-store.d.ts.map +1 -1
- package/dist/store/taxonomy-store.js +90 -39
- package/dist/store/taxonomy-store.js.map +1 -1
- package/dist/store/taxonomy-usage.d.ts +47 -0
- package/dist/store/taxonomy-usage.d.ts.map +1 -0
- package/dist/store/taxonomy-usage.js +101 -0
- package/dist/store/taxonomy-usage.js.map +1 -0
- package/dist/store/theme-store.d.ts +73 -0
- package/dist/store/theme-store.d.ts.map +1 -0
- package/dist/store/theme-store.js +193 -0
- package/dist/store/theme-store.js.map +1 -0
- package/dist/store/types.d.ts +36 -1
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/values.d.ts.map +1 -1
- package/dist/store/values.js +28 -1
- package/dist/store/values.js.map +1 -1
- package/dist/system-fields.d.ts +8 -1
- package/dist/system-fields.d.ts.map +1 -1
- package/dist/system-fields.js +14 -1
- package/dist/system-fields.js.map +1 -1
- package/dist/types.d.ts +54 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
/**
|
|
3
|
+
* The log of public URLs that answered a 404 (fiche 12 task 1).
|
|
4
|
+
*
|
|
5
|
+
* Half the point of a redirect table is knowing what to put in it, and
|
|
6
|
+
* without this a CMS only learns of a broken link when a visitor or a client
|
|
7
|
+
* complains. This is that missing half — but it is written by an anonymous
|
|
8
|
+
* request, on every path nobody asked for, which makes it the one part of
|
|
9
|
+
* this fiche that is a real attack surface rather than a convenience:
|
|
10
|
+
*
|
|
11
|
+
* - **One row per path, never one per request.** A second 404 at a path
|
|
12
|
+
* already tracked increments a counter; it never appends a row. Without
|
|
13
|
+
* this, a scanner requesting the same missing URL in a loop alone would
|
|
14
|
+
* grow the table without bound.
|
|
15
|
+
* - **`maxPaths` is a hard cap on distinct paths.** Once it is reached, a
|
|
16
|
+
* 404 at a genuinely new path is not recorded at all — existing paths keep
|
|
17
|
+
* accumulating hits, but the table cannot grow past its cap. A scanner
|
|
18
|
+
* that requests thousands of unique URLs a minute (the case one row per
|
|
19
|
+
* path alone does not defend against) hits this instead of a full disk.
|
|
20
|
+
* - **`purge` drops what has not been seen in `retainDays`.** Old noise does
|
|
21
|
+
* not sit forever, and it is what frees capacity back up under the cap.
|
|
22
|
+
* - **No personal data, ever.** Neither an IP address nor a user agent is a
|
|
23
|
+
* column here, full or truncated — `AGENTS.md` § Logs forbids storing
|
|
24
|
+
* either, and the path plus a same-origin-or-not referrer already answer
|
|
25
|
+
* the only question this log exists for: "what should I redirect?". The
|
|
26
|
+
* referrer itself is reduced to origin + pathname before it is kept, so a
|
|
27
|
+
* query string carrying a search term, a token or a session id never
|
|
28
|
+
* reaches this table either.
|
|
29
|
+
*/
|
|
30
|
+
export declare const NOT_FOUND_LOG_TABLE = "cogenta_not_found_log";
|
|
31
|
+
/** Distinct paths tracked before new ones stop being recorded, absent an explicit choice. */
|
|
32
|
+
export declare const DEFAULT_MAX_TRACKED_PATHS = 2000;
|
|
33
|
+
export interface NotFoundLogEntry {
|
|
34
|
+
readonly path: string;
|
|
35
|
+
readonly hits: number;
|
|
36
|
+
/** Epoch milliseconds. */
|
|
37
|
+
readonly firstSeen: number;
|
|
38
|
+
/** Epoch milliseconds. */
|
|
39
|
+
readonly lastSeen: number;
|
|
40
|
+
/** Origin + pathname only — never a query string or a fragment. Null when the request carried none, or an unparsable one. */
|
|
41
|
+
readonly lastReferrer: string | null;
|
|
42
|
+
}
|
|
43
|
+
export interface RecordNotFoundInput {
|
|
44
|
+
readonly path: string;
|
|
45
|
+
readonly referrer?: string | null;
|
|
46
|
+
}
|
|
47
|
+
export interface ListNotFoundOptions {
|
|
48
|
+
/** Highest hit count first. Defaults to 100. */
|
|
49
|
+
readonly limit?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface NotFoundLogStore {
|
|
52
|
+
/** Creates the table if it is missing. Called for you by every other method. */
|
|
53
|
+
ensureTable(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Aggregates one public 404 by path.
|
|
56
|
+
*
|
|
57
|
+
* Always a no-op rather than a throw when the path is unusably long or the
|
|
58
|
+
* log is already at its cap — a visitor requesting a broken link must never
|
|
59
|
+
* see a 500 because this bookkeeping table is full.
|
|
60
|
+
*/
|
|
61
|
+
record(input: RecordNotFoundInput): Promise<void>;
|
|
62
|
+
/** Sorted by `hits` descending, then by most recently seen. */
|
|
63
|
+
list(options?: ListNotFoundOptions): Promise<readonly NotFoundLogEntry[]>;
|
|
64
|
+
/** Drops one tracked path — used once a redirect has been created from it, or by an editor clearing noise. */
|
|
65
|
+
remove(path: string): Promise<boolean>;
|
|
66
|
+
/** Drops every path not requested in more than `retainDays` days. Returns how many were dropped. */
|
|
67
|
+
purge(retainDays: number): Promise<number>;
|
|
68
|
+
}
|
|
69
|
+
export interface NotFoundLogStoreOptions {
|
|
70
|
+
readonly db: DatabaseHandle;
|
|
71
|
+
/** Injected so the tests do not have to wait for a clock. */
|
|
72
|
+
readonly now?: () => number;
|
|
73
|
+
readonly table?: string;
|
|
74
|
+
/** Hard cap on distinct paths tracked at once. Defaults to `DEFAULT_MAX_TRACKED_PATHS`. */
|
|
75
|
+
readonly maxPaths?: number;
|
|
76
|
+
}
|
|
77
|
+
export declare function createNotFoundLogStore(options: NotFoundLogStoreOptions): NotFoundLogStore;
|
|
78
|
+
//# sourceMappingURL=not-found-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"not-found-log.d.ts","sourceRoot":"","sources":["../../src/routing/not-found-log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAG3E;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,eAAO,MAAM,mBAAmB,0BAA0B,CAAA;AAM1D,6FAA6F;AAC7F,eAAO,MAAM,yBAAyB,OAAO,CAAA;AAE7C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,0BAA0B;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,0BAA0B;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,6HAA6H;IAC7H,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CACrC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,gDAAgD;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjD,+DAA+D;IAC/D,IAAI,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,SAAS,gBAAgB,EAAE,CAAC,CAAA;IACzE,8GAA8G;IAC9G,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACtC,oGAAoG;IACpG,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CAC3C;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B;AAsBD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CA0GzF"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { identifier, limit, sql } from '@cogenta/core';
|
|
2
|
+
import { normalisePath } from './router.js';
|
|
3
|
+
/**
|
|
4
|
+
* The log of public URLs that answered a 404 (fiche 12 task 1).
|
|
5
|
+
*
|
|
6
|
+
* Half the point of a redirect table is knowing what to put in it, and
|
|
7
|
+
* without this a CMS only learns of a broken link when a visitor or a client
|
|
8
|
+
* complains. This is that missing half — but it is written by an anonymous
|
|
9
|
+
* request, on every path nobody asked for, which makes it the one part of
|
|
10
|
+
* this fiche that is a real attack surface rather than a convenience:
|
|
11
|
+
*
|
|
12
|
+
* - **One row per path, never one per request.** A second 404 at a path
|
|
13
|
+
* already tracked increments a counter; it never appends a row. Without
|
|
14
|
+
* this, a scanner requesting the same missing URL in a loop alone would
|
|
15
|
+
* grow the table without bound.
|
|
16
|
+
* - **`maxPaths` is a hard cap on distinct paths.** Once it is reached, a
|
|
17
|
+
* 404 at a genuinely new path is not recorded at all — existing paths keep
|
|
18
|
+
* accumulating hits, but the table cannot grow past its cap. A scanner
|
|
19
|
+
* that requests thousands of unique URLs a minute (the case one row per
|
|
20
|
+
* path alone does not defend against) hits this instead of a full disk.
|
|
21
|
+
* - **`purge` drops what has not been seen in `retainDays`.** Old noise does
|
|
22
|
+
* not sit forever, and it is what frees capacity back up under the cap.
|
|
23
|
+
* - **No personal data, ever.** Neither an IP address nor a user agent is a
|
|
24
|
+
* column here, full or truncated — `AGENTS.md` § Logs forbids storing
|
|
25
|
+
* either, and the path plus a same-origin-or-not referrer already answer
|
|
26
|
+
* the only question this log exists for: "what should I redirect?". The
|
|
27
|
+
* referrer itself is reduced to origin + pathname before it is kept, so a
|
|
28
|
+
* query string carrying a search term, a token or a session id never
|
|
29
|
+
* reaches this table either.
|
|
30
|
+
*/
|
|
31
|
+
export const NOT_FOUND_LOG_TABLE = 'cogenta_not_found_log';
|
|
32
|
+
/** Kept in step with the `varchar(512)` of the DDL below, the same way `redirects.ts` does. */
|
|
33
|
+
const MAX_PATH_LENGTH = 512;
|
|
34
|
+
const MAX_REFERRER_LENGTH = 512;
|
|
35
|
+
/** Distinct paths tracked before new ones stop being recorded, absent an explicit choice. */
|
|
36
|
+
export const DEFAULT_MAX_TRACKED_PATHS = 2000;
|
|
37
|
+
/** Origin + pathname only, capped in length. `null` for anything that does not parse as an absolute URL, or is empty. */
|
|
38
|
+
function sanitiseReferrer(referrer) {
|
|
39
|
+
if (referrer === null || referrer === undefined || referrer.length === 0)
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(referrer);
|
|
43
|
+
const reduced = `${url.origin}${url.pathname}`;
|
|
44
|
+
return reduced.slice(0, MAX_REFERRER_LENGTH);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function createNotFoundLogStore(options) {
|
|
51
|
+
const { db } = options;
|
|
52
|
+
const now = options.now ?? Date.now;
|
|
53
|
+
const maxPaths = options.maxPaths ?? DEFAULT_MAX_TRACKED_PATHS;
|
|
54
|
+
const table = identifier(options.table ?? NOT_FOUND_LOG_TABLE, db.dialect);
|
|
55
|
+
let ready = false;
|
|
56
|
+
async function ensureTable() {
|
|
57
|
+
if (ready)
|
|
58
|
+
return;
|
|
59
|
+
await db.query(sql `
|
|
60
|
+
create table if not exists ${table} (
|
|
61
|
+
path varchar(512) not null primary key,
|
|
62
|
+
hits integer not null,
|
|
63
|
+
first_seen bigint not null,
|
|
64
|
+
last_seen bigint not null,
|
|
65
|
+
last_referrer varchar(512)
|
|
66
|
+
)`);
|
|
67
|
+
ready = true;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
ensureTable,
|
|
71
|
+
record: async (input) => {
|
|
72
|
+
await ensureTable();
|
|
73
|
+
const path = normalisePath(input.path);
|
|
74
|
+
if (path.length > MAX_PATH_LENGTH)
|
|
75
|
+
return;
|
|
76
|
+
const referrer = sanitiseReferrer(input.referrer);
|
|
77
|
+
const at = now();
|
|
78
|
+
await db.transaction(async (tx) => {
|
|
79
|
+
const existing = await tx.query(sql `
|
|
80
|
+
select path from ${table} where path = ${path} limit ${limit(1)}`);
|
|
81
|
+
if (existing.rows.length === 0) {
|
|
82
|
+
// A genuinely new path: enforced against the cap so the table
|
|
83
|
+
// cannot grow past it, however many unique URLs a scanner tries.
|
|
84
|
+
// Best-effort — see the upsert below for what makes a race here
|
|
85
|
+
// safe rather than merely unlikely.
|
|
86
|
+
const counted = await tx.query(sql `
|
|
87
|
+
select count(*) as ${identifier('total', db.dialect)} from ${table}`);
|
|
88
|
+
const total = Number(counted.rows[0]?.total ?? 0);
|
|
89
|
+
if (total >= maxPaths)
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// An upsert, not a plain insert: `{ immediate: true }` only takes a
|
|
93
|
+
// real write lock on SQLite (`BEGIN IMMEDIATE`) — Postgres and MySQL
|
|
94
|
+
// both discard the option and run under their default isolation, so
|
|
95
|
+
// two anonymous requests hitting the same brand-new path at once can
|
|
96
|
+
// both pass the `existing.rows.length === 0` check above. A plain
|
|
97
|
+
// `insert` would then have the second one crash on the `path`
|
|
98
|
+
// primary key — precisely the 500 a 404 log must never cause. `on
|
|
99
|
+
// conflict do update` / `on duplicate key update` turns that race
|
|
100
|
+
// into a safe increment instead: whichever request loses the race
|
|
101
|
+
// still lands as exactly one more hit on one row, never a thrown
|
|
102
|
+
// duplicate-key error.
|
|
103
|
+
if (db.dialect === 'mysql') {
|
|
104
|
+
await tx.query(sql `
|
|
105
|
+
insert into ${table} (path, hits, first_seen, last_seen, last_referrer)
|
|
106
|
+
values (${path}, 1, ${at}, ${at}, ${referrer})
|
|
107
|
+
on duplicate key update
|
|
108
|
+
hits = hits + 1, last_seen = ${at}, last_referrer = ${referrer}`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
await tx.query(sql `
|
|
112
|
+
insert into ${table} (path, hits, first_seen, last_seen, last_referrer)
|
|
113
|
+
values (${path}, 1, ${at}, ${at}, ${referrer})
|
|
114
|
+
on conflict (path) do update set
|
|
115
|
+
hits = hits + 1,
|
|
116
|
+
last_seen = excluded.last_seen,
|
|
117
|
+
last_referrer = excluded.last_referrer`);
|
|
118
|
+
}
|
|
119
|
+
}, { immediate: true });
|
|
120
|
+
},
|
|
121
|
+
list: async (listOptions = {}) => {
|
|
122
|
+
await ensureTable();
|
|
123
|
+
const found = await db.query(sql `
|
|
124
|
+
select path, hits, first_seen, last_seen, last_referrer
|
|
125
|
+
from ${table}
|
|
126
|
+
order by hits desc, last_seen desc
|
|
127
|
+
limit ${limit(listOptions.limit ?? 100)}`);
|
|
128
|
+
return found.rows.map(toEntry);
|
|
129
|
+
},
|
|
130
|
+
remove: async (path) => {
|
|
131
|
+
await ensureTable();
|
|
132
|
+
const result = await db.query(sql `
|
|
133
|
+
delete from ${table} where path = ${normalisePath(path)}`);
|
|
134
|
+
return result.rowsAffected > 0;
|
|
135
|
+
},
|
|
136
|
+
purge: async (retainDays) => {
|
|
137
|
+
await ensureTable();
|
|
138
|
+
const cutoff = now() - retainDays * 24 * 60 * 60 * 1000;
|
|
139
|
+
const result = await db.query(sql `delete from ${table} where last_seen < ${cutoff}`);
|
|
140
|
+
return result.rowsAffected;
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function toEntry(row) {
|
|
145
|
+
return {
|
|
146
|
+
path: row.path,
|
|
147
|
+
hits: Number(row.hits),
|
|
148
|
+
firstSeen: Number(row.first_seen),
|
|
149
|
+
lastSeen: Number(row.last_seen),
|
|
150
|
+
lastReferrer: row.last_referrer,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=not-found-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"not-found-log.js","sourceRoot":"","sources":["../../src/routing/not-found-log.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '@cogenta/core';
|
|
2
|
+
/**
|
|
3
|
+
* Prefix redirects — `/blog/*` to `/actualites/*` — a second, deliberately
|
|
4
|
+
* simpler table beside `RedirectStore`.
|
|
5
|
+
*
|
|
6
|
+
* A prefix rule is **not** a regular expression, on purpose (fiche 12, R9 of
|
|
7
|
+
* the écarts list: "cela n'expose pas le routage public à une expression
|
|
8
|
+
* régulière catastrophique fournie par un utilisateur"). Matching is a single
|
|
9
|
+
* `startsWith`, which cannot backtrack and cannot be made to run slowly by
|
|
10
|
+
* any input — the property a user-supplied regex in the hot routing path
|
|
11
|
+
* would give up.
|
|
12
|
+
*
|
|
13
|
+
* It is also **not chained or flattened** the way `RedirectStore` is. A
|
|
14
|
+
* prefix rule is applied at most once per request, by construction: the
|
|
15
|
+
* caller checks `RedirectStore.resolve` first and only falls back to
|
|
16
|
+
* `resolve` here when that finds nothing, and this store's own `resolve`
|
|
17
|
+
* never re-enters itself. That sidesteps the loop/flattening machinery
|
|
18
|
+
* `redirects.ts` needs entirely, rather than reimplementing it for a second
|
|
19
|
+
* shape of rule — the "Confort" tier this feature belongs to does not carry
|
|
20
|
+
* the same weight as the exact-match table's chain-safety guarantees.
|
|
21
|
+
*/
|
|
22
|
+
export declare const REDIRECT_PATTERNS_TABLE = "cogenta_redirect_patterns";
|
|
23
|
+
export type RedirectPatternStatus = 301 | 302;
|
|
24
|
+
export interface RedirectPatternRecord {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
/** Always ends with `/`. `/blog/*` is stored and shown as `/blog/`. */
|
|
27
|
+
readonly fromPrefix: string;
|
|
28
|
+
readonly toPrefix: string;
|
|
29
|
+
readonly status: RedirectPatternStatus;
|
|
30
|
+
/** Epoch milliseconds. */
|
|
31
|
+
readonly createdAt: number;
|
|
32
|
+
}
|
|
33
|
+
export interface AddRedirectPatternInput {
|
|
34
|
+
/** `/blog/*` or `/blog/` — the trailing `*` is accepted and stripped. */
|
|
35
|
+
readonly fromPrefix: string;
|
|
36
|
+
readonly toPrefix: string;
|
|
37
|
+
readonly status?: RedirectPatternStatus;
|
|
38
|
+
}
|
|
39
|
+
export interface RedirectPatternResolution {
|
|
40
|
+
readonly to: string;
|
|
41
|
+
readonly status: RedirectPatternStatus;
|
|
42
|
+
}
|
|
43
|
+
export interface RedirectPatternStore {
|
|
44
|
+
ensureTable(): Promise<void>;
|
|
45
|
+
add(input: AddRedirectPatternInput): Promise<RedirectPatternRecord>;
|
|
46
|
+
list(): Promise<readonly RedirectPatternRecord[]>;
|
|
47
|
+
remove(fromPrefix: string): Promise<boolean>;
|
|
48
|
+
/**
|
|
49
|
+
* The longest matching prefix rule for `path`, rewritten once. `null` when
|
|
50
|
+
* no rule's prefix matches.
|
|
51
|
+
*/
|
|
52
|
+
resolve(path: string): Promise<RedirectPatternResolution | null>;
|
|
53
|
+
}
|
|
54
|
+
export interface RedirectPatternStoreOptions {
|
|
55
|
+
readonly db: DatabaseHandle;
|
|
56
|
+
readonly now?: () => number;
|
|
57
|
+
readonly newId?: () => string;
|
|
58
|
+
readonly table?: string;
|
|
59
|
+
}
|
|
60
|
+
export declare function createRedirectPatternStore(options: RedirectPatternStoreOptions): RedirectPatternStore;
|
|
61
|
+
//# sourceMappingURL=redirect-patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect-patterns.d.ts","sourceRoot":"","sources":["../../src/routing/redirect-patterns.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAIzF;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,eAAO,MAAM,uBAAuB,8BAA8B,CAAA;AAiBlE,MAAM,MAAM,qBAAqB,GAAG,GAAG,GAAG,GAAG,CAAA;AAE7C,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAA;IACtC,0BAA0B;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,qBAAqB,CAAA;CACxC;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAA;CACvC;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,GAAG,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACnE,IAAI,IAAI,OAAO,CAAC,SAAS,qBAAqB,EAAE,CAAC,CAAA;IACjD,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5C;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC,CAAA;CACjE;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;IAC3B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAA;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AA2BD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,2BAA2B,GACnC,oBAAoB,CAyItB"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { CogentaError, identifier, limit, sql } from '@cogenta/core';
|
|
2
|
+
import { newId } from '../id.js';
|
|
3
|
+
import { normalisePath } from './router.js';
|
|
4
|
+
/**
|
|
5
|
+
* Prefix redirects — `/blog/*` to `/actualites/*` — a second, deliberately
|
|
6
|
+
* simpler table beside `RedirectStore`.
|
|
7
|
+
*
|
|
8
|
+
* A prefix rule is **not** a regular expression, on purpose (fiche 12, R9 of
|
|
9
|
+
* the écarts list: "cela n'expose pas le routage public à une expression
|
|
10
|
+
* régulière catastrophique fournie par un utilisateur"). Matching is a single
|
|
11
|
+
* `startsWith`, which cannot backtrack and cannot be made to run slowly by
|
|
12
|
+
* any input — the property a user-supplied regex in the hot routing path
|
|
13
|
+
* would give up.
|
|
14
|
+
*
|
|
15
|
+
* It is also **not chained or flattened** the way `RedirectStore` is. A
|
|
16
|
+
* prefix rule is applied at most once per request, by construction: the
|
|
17
|
+
* caller checks `RedirectStore.resolve` first and only falls back to
|
|
18
|
+
* `resolve` here when that finds nothing, and this store's own `resolve`
|
|
19
|
+
* never re-enters itself. That sidesteps the loop/flattening machinery
|
|
20
|
+
* `redirects.ts` needs entirely, rather than reimplementing it for a second
|
|
21
|
+
* shape of rule — the "Confort" tier this feature belongs to does not carry
|
|
22
|
+
* the same weight as the exact-match table's chain-safety guarantees.
|
|
23
|
+
*/
|
|
24
|
+
export const REDIRECT_PATTERNS_TABLE = 'cogenta_redirect_patterns';
|
|
25
|
+
/** Same bound as `RedirectStore`, and for the same reason: kept in step with the DDL below. */
|
|
26
|
+
const MAX_PATH_LENGTH = 512;
|
|
27
|
+
/**
|
|
28
|
+
* Hard cap on distinct prefix rules.
|
|
29
|
+
*
|
|
30
|
+
* `resolve` below scans every rule to find the longest matching prefix — the
|
|
31
|
+
* only portable way to do "most specific prefix wins" across three SQL
|
|
32
|
+
* dialects without a second `LIKE`-based index scheme for a feature this
|
|
33
|
+
* small. That scan runs on every public request that reaches it, so the
|
|
34
|
+
* table this cap bounds is deliberately a short, curated list of URL-structure
|
|
35
|
+
* migrations, never an import target the way the exact-match table is.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_PATTERNS = 200;
|
|
38
|
+
/** `/blog/*` and `/blog` both mean the same directory prefix `/blog/`. */
|
|
39
|
+
function normalisePrefix(raw, field) {
|
|
40
|
+
const withoutWildcard = raw.endsWith('*') ? raw.slice(0, -1) : raw;
|
|
41
|
+
const normalised = normalisePath(withoutWildcard);
|
|
42
|
+
if (normalised.length > MAX_PATH_LENGTH) {
|
|
43
|
+
throw new CogentaError({
|
|
44
|
+
code: 'CONTENT_ROUTE_INVALID',
|
|
45
|
+
message: `A redirect prefix may be ${MAX_PATH_LENGTH} characters at most.`,
|
|
46
|
+
hint: 'Shorten the prefix.',
|
|
47
|
+
details: { field, length: normalised.length },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return normalised === '/' ? '/' : `${normalised}/`;
|
|
51
|
+
}
|
|
52
|
+
export function createRedirectPatternStore(options) {
|
|
53
|
+
const { db } = options;
|
|
54
|
+
const now = options.now ?? Date.now;
|
|
55
|
+
const nextId = options.newId ?? newId;
|
|
56
|
+
const table = identifier(options.table ?? REDIRECT_PATTERNS_TABLE, db.dialect);
|
|
57
|
+
let ready = false;
|
|
58
|
+
async function ensureTable() {
|
|
59
|
+
if (ready)
|
|
60
|
+
return;
|
|
61
|
+
await db.query(sql `
|
|
62
|
+
create table if not exists ${table} (
|
|
63
|
+
id varchar(64) not null primary key,
|
|
64
|
+
from_prefix varchar(512) not null,
|
|
65
|
+
to_prefix varchar(512) not null,
|
|
66
|
+
status integer not null,
|
|
67
|
+
created_at bigint not null
|
|
68
|
+
)`);
|
|
69
|
+
await db
|
|
70
|
+
.query(sql `create unique index ${identifier(`${options.table ?? REDIRECT_PATTERNS_TABLE}_from`, db.dialect)}
|
|
71
|
+
on ${table} (from_prefix)`)
|
|
72
|
+
.catch(() => undefined);
|
|
73
|
+
ready = true;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
ensureTable,
|
|
77
|
+
add: async (input) => {
|
|
78
|
+
await ensureTable();
|
|
79
|
+
const fromPrefix = normalisePrefix(input.fromPrefix, 'fromPrefix');
|
|
80
|
+
const toPrefix = normalisePrefix(input.toPrefix, 'toPrefix');
|
|
81
|
+
const status = input.status ?? 301;
|
|
82
|
+
if (fromPrefix === toPrefix) {
|
|
83
|
+
throw new CogentaError({
|
|
84
|
+
code: 'CONTENT_REDIRECT_LOOP',
|
|
85
|
+
message: `${fromPrefix}* cannot redirect to itself.`,
|
|
86
|
+
hint: 'Nothing needs a redirect here: the prefix did not change.',
|
|
87
|
+
details: { fromPrefix },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const existing = await db.query(sql `
|
|
91
|
+
select from_prefix from ${table} where from_prefix = ${fromPrefix} limit ${limit(1)}`);
|
|
92
|
+
if (existing.rows.length === 0) {
|
|
93
|
+
const counted = await db.query(sql `
|
|
94
|
+
select count(*) as ${identifier('total', db.dialect)} from ${table}`);
|
|
95
|
+
if (Number(counted.rows[0]?.total ?? 0) >= MAX_PATTERNS) {
|
|
96
|
+
throw new CogentaError({
|
|
97
|
+
code: 'CONTENT_ROUTE_INVALID',
|
|
98
|
+
message: `No more than ${MAX_PATTERNS} prefix redirects are supported.`,
|
|
99
|
+
hint: 'Remove an existing pattern before adding a new one, or use an exact redirect instead.',
|
|
100
|
+
details: { limit: MAX_PATTERNS },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const record = {
|
|
105
|
+
id: nextId(),
|
|
106
|
+
fromPrefix,
|
|
107
|
+
toPrefix,
|
|
108
|
+
status,
|
|
109
|
+
createdAt: now(),
|
|
110
|
+
};
|
|
111
|
+
// Delete-then-insert, same reasoning as `RedirectStore.add`: one
|
|
112
|
+
// behaviour across three dialects that each spell "replace" differently.
|
|
113
|
+
await db.transaction(async (tx) => {
|
|
114
|
+
await tx.query(sql `delete from ${table} where from_prefix = ${fromPrefix}`);
|
|
115
|
+
await tx.query(sql `
|
|
116
|
+
insert into ${table} (id, from_prefix, to_prefix, status, created_at)
|
|
117
|
+
values (${record.id}, ${record.fromPrefix}, ${record.toPrefix}, ${record.status}, ${record.createdAt})`);
|
|
118
|
+
}, { immediate: true });
|
|
119
|
+
return record;
|
|
120
|
+
},
|
|
121
|
+
list: async () => {
|
|
122
|
+
await ensureTable();
|
|
123
|
+
const found = await db.query(sql `
|
|
124
|
+
select id, from_prefix, to_prefix, status, created_at
|
|
125
|
+
from ${table}
|
|
126
|
+
order by created_at desc`);
|
|
127
|
+
return found.rows.map(toRecord);
|
|
128
|
+
},
|
|
129
|
+
remove: async (fromPrefix) => {
|
|
130
|
+
await ensureTable();
|
|
131
|
+
const result = await db.query(sql `
|
|
132
|
+
delete from ${table} where from_prefix = ${normalisePrefix(fromPrefix, 'fromPrefix')}`);
|
|
133
|
+
return result.rowsAffected > 0;
|
|
134
|
+
},
|
|
135
|
+
resolve: async (path) => {
|
|
136
|
+
await ensureTable();
|
|
137
|
+
const target = normalisePath(path);
|
|
138
|
+
const rules = await db.query(sql `
|
|
139
|
+
select from_prefix, to_prefix, status from ${table}`);
|
|
140
|
+
// Longest prefix wins — the most specific rule beats a broader one
|
|
141
|
+
// rather than whichever happened to be inserted first.
|
|
142
|
+
let best = null;
|
|
143
|
+
for (const rule of rules.rows) {
|
|
144
|
+
const matches = target === rule.from_prefix.slice(0, -1) || target.startsWith(rule.from_prefix);
|
|
145
|
+
if (!matches)
|
|
146
|
+
continue;
|
|
147
|
+
if (best === null || rule.from_prefix.length > best.fromPrefix.length) {
|
|
148
|
+
best = {
|
|
149
|
+
fromPrefix: rule.from_prefix,
|
|
150
|
+
toPrefix: rule.to_prefix,
|
|
151
|
+
status: Number(rule.status),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (best === null)
|
|
156
|
+
return null;
|
|
157
|
+
const rest = target === best.fromPrefix.slice(0, -1) ? '' : target.slice(best.fromPrefix.length);
|
|
158
|
+
const to = `${best.toPrefix}${rest}`.replace(/\/+$/u, '') || '/';
|
|
159
|
+
const status = best.status === 302 ? 302 : 301;
|
|
160
|
+
return { to, status };
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function toRecord(row) {
|
|
165
|
+
return {
|
|
166
|
+
id: row.id,
|
|
167
|
+
fromPrefix: row.from_prefix,
|
|
168
|
+
toPrefix: row.to_prefix,
|
|
169
|
+
status: Number(row.status) === 302 ? 302 : 301,
|
|
170
|
+
createdAt: Number(row.created_at),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=redirect-patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect-patterns.js","sourceRoot":"","sources":["../../src/routing/redirect-patterns.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AACzF,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,CAAC,MAAM,uBAAuB,GAAG,2BAA2B,CAAA;AAElE,+FAA+F;AAC/F,MAAM,eAAe,GAAG,GAAG,CAAA;AAE3B;;;;;;;;;GASG;AACH,MAAM,YAAY,GAAG,GAAG,CAAA;AAqDxB,0EAA0E;AAC1E,SAAS,eAAe,CAAC,GAAW,EAAE,KAAgC;IACpE,MAAM,eAAe,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA;IAClE,MAAM,UAAU,GAAG,aAAa,CAAC,eAAe,CAAC,CAAA;IAEjD,IAAI,UAAU,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;QACxC,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,4BAA4B,eAAe,sBAAsB;YAC1E,IAAI,EAAE,qBAAqB;YAC3B,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE;SAC9C,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,UAAU,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,UAAU,GAAG,CAAA;AACpD,CAAC;AAED,MAAM,UAAU,0BAA0B,CACxC,OAAoC;IAEpC,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAA;IACtB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAA;IACnC,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAA;IACrC,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,IAAI,uBAAuB,EAAE,EAAE,CAAC,OAAO,CAAC,CAAA;IAC9E,IAAI,KAAK,GAAG,KAAK,CAAA;IAEjB,KAAK,UAAU,WAAW;QACxB,IAAI,KAAK;YAAE,OAAM;QAEjB,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;mCACa,KAAK;;;;;;QAMhC,CAAC,CAAA;QAEL,MAAM,EAAE;aACL,KAAK,CACJ,GAAG,CAAA,uBAAuB,UAAU,CAAC,GAAG,OAAO,CAAC,KAAK,IAAI,uBAAuB,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC;iBAC3F,KAAK,gBAAgB,CAC/B;aACA,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QAEzB,KAAK,GAAG,IAAI,CAAA;IACd,CAAC;IAED,OAAO;QACL,WAAW;QAEX,GAAG,EAAE,KAAK,EAAE,KAA8B,EAAkC,EAAE;YAC5E,MAAM,WAAW,EAAE,CAAA;YAEnB,MAAM,UAAU,GAAG,eAAe,CAAC,KAAK,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;YAClE,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;YAC5D,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,GAAG,CAAA;YAElC,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;gBAC5B,MAAM,IAAI,YAAY,CAAC;oBACrB,IAAI,EAAE,uBAAuB;oBAC7B,OAAO,EAAE,GAAG,UAAU,8BAA8B;oBACpD,IAAI,EAAE,2DAA2D;oBACjE,OAAO,EAAE,EAAE,UAAU,EAAE;iBACxB,CAAC,CAAA;YACJ,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,KAAK,CAA0B,GAAG,CAAA;kCAChC,KAAK,wBAAwB,UAAU,UAAU,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;YACxF,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC/B,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,KAAK,CAAoB,GAAG,CAAA;+BAC9B,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,KAAK,EAAE,CAAC,CAAA;gBACvE,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,IAAI,YAAY,EAAE,CAAC;oBACxD,MAAM,IAAI,YAAY,CAAC;wBACrB,IAAI,EAAE,uBAAuB;wBAC7B,OAAO,EAAE,gBAAgB,YAAY,kCAAkC;wBACvE,IAAI,EAAE,uFAAuF;wBAC7F,OAAO,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE;qBACjC,CAAC,CAAA;gBACJ,CAAC;YACH,CAAC;YAED,MAAM,MAAM,GAA0B;gBACpC,EAAE,EAAE,MAAM,EAAE;gBACZ,UAAU;gBACV,QAAQ;gBACR,MAAM;gBACN,SAAS,EAAE,GAAG,EAAE;aACjB,CAAA;YAED,iEAAiE;YACjE,yEAAyE;YACzE,MAAM,EAAE,CAAC,WAAW,CAClB,KAAK,EAAE,EAAE,EAAE,EAAE;gBACX,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA,eAAe,KAAK,wBAAwB,UAAU,EAAE,CAAC,CAAA;gBAC3E,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;0BACF,KAAK;sBACT,MAAM,CAAC,EAAE,KAAK,MAAM,CAAC,UAAU,KAAK,MAAM,CAAC,QAAQ,KAAK,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,SAAS,GAAG,CAAC,CAAA;YAC5G,CAAC,EACD,EAAE,SAAS,EAAE,IAAI,EAAE,CACpB,CAAA;YAED,OAAO,MAAM,CAAA;QACf,CAAC;QAED,IAAI,EAAE,KAAK,IAA+C,EAAE;YAC1D,MAAM,WAAW,EAAE,CAAA;YACnB,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,KAAK,CAAqB,GAAG,CAAA;;eAE3C,KAAK;iCACa,CAAC,CAAA;YAC5B,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACjC,CAAC;QAED,MAAM,EAAE,KAAK,EAAE,UAAkB,EAAoB,EAAE;YACrD,MAAM,WAAW,EAAE,CAAA;YACnB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAA;sBACjB,KAAK,wBAAwB,eAAe,CAAC,UAAU,EAAE,YAAY,CAAC,EAAE,CAAC,CAAA;YACzF,OAAO,MAAM,CAAC,YAAY,GAAG,CAAC,CAAA;QAChC,CAAC;QAED,OAAO,EAAE,KAAK,EAAE,IAAY,EAA6C,EAAE;YACzE,MAAM,WAAW,EAAE,CAAA;YACnB,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;YAElC,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,KAAK,CAAqB,GAAG,CAAA;qDACL,KAAK,EAAE,CAAC,CAAA;YAEvD,mEAAmE;YACnE,uDAAuD;YACvD,IAAI,IAAI,GAIG,IAAI,CAAA;YACf,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;gBAC9B,MAAM,OAAO,GACX,MAAM,KAAK,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;gBACjF,IAAI,CAAC,OAAO;oBAAE,SAAQ;gBACtB,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;oBACtE,IAAI,GAAG;wBACL,UAAU,EAAE,IAAI,CAAC,WAAW;wBAC5B,QAAQ,EAAE,IAAI,CAAC,SAAS;wBACxB,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;qBAC5B,CAAA;gBACH,CAAC;YACH,CAAC;YACD,IAAI,IAAI,KAAK,IAAI;gBAAE,OAAO,IAAI,CAAA;YAE9B,MAAM,IAAI,GACR,MAAM,KAAK,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;YACrF,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,GAAG,CAAA;YAChE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;YAC9C,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,CAAA;QACvB,CAAC;KACF,CAAA;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,GAAuB;IACvC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,UAAU,EAAE,GAAG,CAAC,WAAW;QAC3B,QAAQ,EAAE,GAAG,CAAC,SAAS;QACvB,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;QAC9C,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;KAClC,CAAA;AACH,CAAC"}
|
|
@@ -13,10 +13,24 @@ import { type DatabaseHandle } from '@cogenta/core';
|
|
|
13
13
|
* is rejected at write time, where there is still a human to tell.
|
|
14
14
|
*/
|
|
15
15
|
export declare const REDIRECTS_TABLE = "cogenta_redirects";
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* `'agent'` (L22 task 3) marks a redirect an agent created itself —
|
|
18
|
+
* `redirects.create` (`@cogenta/agents`) always writes this reason, never
|
|
19
|
+
* `'manual'`, so an admin looking at `/seo?tab=redirects` can tell which
|
|
20
|
+
* rows a human typed and which one a monitoring agent proposed and had
|
|
21
|
+
* applied under `autopilot` autonomy. Additive to a stored, open list (not a
|
|
22
|
+
* versioned contract enum): `toRecord` below already falls back to
|
|
23
|
+
* `'manual'` for any string it does not recognise, so a row written by an
|
|
24
|
+
* older build still reads back fine.
|
|
25
|
+
*/
|
|
26
|
+
export declare const REDIRECT_REASONS: readonly ['slug-change', 'manual', 'import', 'agent'];
|
|
17
27
|
export type RedirectReason = (typeof REDIRECT_REASONS)[number];
|
|
18
|
-
/**
|
|
19
|
-
|
|
28
|
+
/**
|
|
29
|
+
* 301/308 are permanent (cacheable); 302/307 are temporary (never cached);
|
|
30
|
+
* 410 is not a redirect at all — it tells a visitor and a crawler the page is
|
|
31
|
+
* gone for good, which is honester than sending them to the home page.
|
|
32
|
+
*/
|
|
33
|
+
export type RedirectStatus = 301 | 302 | 307 | 308 | 410;
|
|
20
34
|
export interface RedirectRecord {
|
|
21
35
|
readonly id: string;
|
|
22
36
|
readonly from: string;
|
|
@@ -31,19 +45,26 @@ export interface RedirectRecord {
|
|
|
31
45
|
}
|
|
32
46
|
export interface AddRedirectInput {
|
|
33
47
|
readonly from: string;
|
|
34
|
-
|
|
48
|
+
/** Required unless `status` is 410 — a Gone rule has nothing to point at. */
|
|
49
|
+
readonly to?: string;
|
|
35
50
|
readonly status?: RedirectStatus;
|
|
36
51
|
readonly collection?: string;
|
|
37
52
|
readonly entryId?: string;
|
|
38
53
|
readonly locale?: string;
|
|
39
54
|
readonly reason?: RedirectReason;
|
|
40
55
|
}
|
|
56
|
+
/** A PATCH: only `to` and/or `status` may change. Everything else about a rule is set once, at `add()`. */
|
|
57
|
+
export interface UpdateRedirectInput {
|
|
58
|
+
readonly to?: string;
|
|
59
|
+
readonly status?: RedirectStatus;
|
|
60
|
+
}
|
|
41
61
|
export interface ListRedirectsOptions {
|
|
42
62
|
readonly limit?: number;
|
|
43
63
|
readonly collection?: string;
|
|
44
64
|
readonly locale?: string;
|
|
45
65
|
}
|
|
46
66
|
export interface RedirectResolution {
|
|
67
|
+
/** For a 410, this is `from` itself — there is no destination, and the caller must not send a `Location` header. */
|
|
47
68
|
readonly to: string;
|
|
48
69
|
readonly status: RedirectStatus;
|
|
49
70
|
}
|
|
@@ -51,6 +72,8 @@ export interface RedirectStore {
|
|
|
51
72
|
/** Creates the table if it is missing. Called for you by every other method. */
|
|
52
73
|
ensureTable(): Promise<void>;
|
|
53
74
|
add(input: AddRedirectInput): Promise<RedirectRecord>;
|
|
75
|
+
/** Changes `to` and/or `status` of an existing rule in place — no gap where the old URL 404s. */
|
|
76
|
+
update(from: string, input: UpdateRedirectInput): Promise<RedirectRecord>;
|
|
54
77
|
/** The final destination of a path, following any chain left by an import. */
|
|
55
78
|
resolve(path: string): Promise<RedirectResolution | null>;
|
|
56
79
|
list(options?: ListRedirectsOptions): Promise<RedirectRecord[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirects.d.ts","sourceRoot":"","sources":["../../src/routing/redirects.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAA;AAItB;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,eAAe,sBAAsB,CAAA;AAoBlD,eAAO,MAAM,gBAAgB,YAAI,aAAa,EAAE,QAAQ,EAAE,QAAQ,CAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"redirects.d.ts","sourceRoot":"","sources":["../../src/routing/redirects.ts"],"names":[],"mappings":"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"}
|