@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.
Files changed (193) hide show
  1. package/dist/define-collection.d.ts.map +1 -1
  2. package/dist/define-collection.js +53 -3
  3. package/dist/define-collection.js.map +1 -1
  4. package/dist/generate-types.d.ts.map +1 -1
  5. package/dist/generate-types.js +6 -1
  6. package/dist/generate-types.js.map +1 -1
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/media-usage.d.ts +56 -0
  12. package/dist/media-usage.d.ts.map +1 -0
  13. package/dist/media-usage.js +89 -0
  14. package/dist/media-usage.js.map +1 -0
  15. package/dist/rich-text.d.ts +28 -2
  16. package/dist/rich-text.d.ts.map +1 -1
  17. package/dist/rich-text.js +26 -3
  18. package/dist/rich-text.js.map +1 -1
  19. package/dist/routing/index.d.ts +5 -1
  20. package/dist/routing/index.d.ts.map +1 -1
  21. package/dist/routing/index.js +2 -0
  22. package/dist/routing/index.js.map +1 -1
  23. package/dist/routing/not-found-log.d.ts +78 -0
  24. package/dist/routing/not-found-log.d.ts.map +1 -0
  25. package/dist/routing/not-found-log.js +153 -0
  26. package/dist/routing/not-found-log.js.map +1 -0
  27. package/dist/routing/redirect-patterns.d.ts +61 -0
  28. package/dist/routing/redirect-patterns.d.ts.map +1 -0
  29. package/dist/routing/redirect-patterns.js +173 -0
  30. package/dist/routing/redirect-patterns.js.map +1 -0
  31. package/dist/routing/redirects.d.ts +27 -4
  32. package/dist/routing/redirects.d.ts.map +1 -1
  33. package/dist/routing/redirects.js +160 -48
  34. package/dist/routing/redirects.js.map +1 -1
  35. package/dist/scheduling/index.d.ts +2 -0
  36. package/dist/scheduling/index.d.ts.map +1 -1
  37. package/dist/scheduling/index.js +1 -0
  38. package/dist/scheduling/index.js.map +1 -1
  39. package/dist/scheduling/registry.d.ts +147 -0
  40. package/dist/scheduling/registry.d.ts.map +1 -0
  41. package/dist/scheduling/registry.js +318 -0
  42. package/dist/scheduling/registry.js.map +1 -0
  43. package/dist/search/extract.d.ts +24 -0
  44. package/dist/search/extract.d.ts.map +1 -1
  45. package/dist/search/extract.js +63 -8
  46. package/dist/search/extract.js.map +1 -1
  47. package/dist/search/index.d.ts +2 -1
  48. package/dist/search/index.d.ts.map +1 -1
  49. package/dist/search/index.js +1 -9
  50. package/dist/search/index.js.map +1 -1
  51. package/dist/store/admin-theme-store.d.ts +26 -0
  52. package/dist/store/admin-theme-store.d.ts.map +1 -0
  53. package/dist/store/admin-theme-store.js +81 -0
  54. package/dist/store/admin-theme-store.js.map +1 -0
  55. package/dist/store/admin-theme-tables.d.ts +24 -0
  56. package/dist/store/admin-theme-tables.d.ts.map +1 -0
  57. package/dist/store/admin-theme-tables.js +41 -0
  58. package/dist/store/admin-theme-tables.js.map +1 -0
  59. package/dist/store/admin-theme-templates.d.ts +135 -0
  60. package/dist/store/admin-theme-templates.d.ts.map +1 -0
  61. package/dist/store/admin-theme-templates.js +217 -0
  62. package/dist/store/admin-theme-templates.js.map +1 -0
  63. package/dist/store/columns.d.ts.map +1 -1
  64. package/dist/store/columns.js +8 -2
  65. package/dist/store/columns.js.map +1 -1
  66. package/dist/store/diff.d.ts +40 -0
  67. package/dist/store/diff.d.ts.map +1 -1
  68. package/dist/store/diff.js +148 -0
  69. package/dist/store/diff.js.map +1 -1
  70. package/dist/store/index.d.ts +47 -4
  71. package/dist/store/index.d.ts.map +1 -1
  72. package/dist/store/index.js +26 -2
  73. package/dist/store/index.js.map +1 -1
  74. package/dist/store/maintenance-store.d.ts +34 -0
  75. package/dist/store/maintenance-store.d.ts.map +1 -0
  76. package/dist/store/maintenance-store.js +92 -0
  77. package/dist/store/maintenance-store.js.map +1 -0
  78. package/dist/store/menu-store.d.ts +146 -0
  79. package/dist/store/menu-store.d.ts.map +1 -0
  80. package/dist/store/menu-store.js +565 -0
  81. package/dist/store/menu-store.js.map +1 -0
  82. package/dist/store/menu-tables.d.ts +38 -0
  83. package/dist/store/menu-tables.d.ts.map +1 -0
  84. package/dist/store/menu-tables.js +117 -0
  85. package/dist/store/menu-tables.js.map +1 -0
  86. package/dist/store/naming.d.ts +1 -1
  87. package/dist/store/naming.d.ts.map +1 -1
  88. package/dist/store/naming.js +2 -0
  89. package/dist/store/naming.js.map +1 -1
  90. package/dist/store/pattern-store.d.ts +75 -0
  91. package/dist/store/pattern-store.d.ts.map +1 -0
  92. package/dist/store/pattern-store.js +118 -0
  93. package/dist/store/pattern-store.js.map +1 -0
  94. package/dist/store/pattern-tables.d.ts +29 -0
  95. package/dist/store/pattern-tables.d.ts.map +1 -0
  96. package/dist/store/pattern-tables.js +53 -0
  97. package/dist/store/pattern-tables.js.map +1 -0
  98. package/dist/store/read-only.d.ts +3 -3
  99. package/dist/store/read-only.d.ts.map +1 -1
  100. package/dist/store/read-only.js +4 -3
  101. package/dist/store/read-only.js.map +1 -1
  102. package/dist/store/redirect-tracking.d.ts +50 -0
  103. package/dist/store/redirect-tracking.d.ts.map +1 -0
  104. package/dist/store/redirect-tracking.js +91 -0
  105. package/dist/store/redirect-tracking.js.map +1 -0
  106. package/dist/store/review-transitions.d.ts +34 -0
  107. package/dist/store/review-transitions.d.ts.map +1 -0
  108. package/dist/store/review-transitions.js +51 -0
  109. package/dist/store/review-transitions.js.map +1 -0
  110. package/dist/store/role-permission-export.d.ts +22 -0
  111. package/dist/store/role-permission-export.d.ts.map +1 -0
  112. package/dist/store/role-permission-export.js +0 -0
  113. package/dist/store/role-permission-export.js.map +1 -0
  114. package/dist/store/role-permission-overlay.d.ts +27 -0
  115. package/dist/store/role-permission-overlay.d.ts.map +1 -0
  116. package/dist/store/role-permission-overlay.js +0 -0
  117. package/dist/store/role-permission-overlay.js.map +1 -0
  118. package/dist/store/role-permission-store.d.ts +72 -0
  119. package/dist/store/role-permission-store.d.ts.map +1 -0
  120. package/dist/store/role-permission-store.js +148 -0
  121. package/dist/store/role-permission-store.js.map +1 -0
  122. package/dist/store/role-permission-tables.d.ts +27 -0
  123. package/dist/store/role-permission-tables.d.ts.map +1 -0
  124. package/dist/store/role-permission-tables.js +52 -0
  125. package/dist/store/role-permission-tables.js.map +1 -0
  126. package/dist/store/scheduled-publish-enqueue.d.ts +37 -0
  127. package/dist/store/scheduled-publish-enqueue.d.ts.map +1 -0
  128. package/dist/store/scheduled-publish-enqueue.js +51 -0
  129. package/dist/store/scheduled-publish-enqueue.js.map +1 -0
  130. package/dist/store/scheduled-publish-failures.d.ts +36 -0
  131. package/dist/store/scheduled-publish-failures.d.ts.map +1 -0
  132. package/dist/store/scheduled-publish-failures.js +67 -0
  133. package/dist/store/scheduled-publish-failures.js.map +1 -0
  134. package/dist/store/schema-2-1-migration.d.ts +29 -0
  135. package/dist/store/schema-2-1-migration.d.ts.map +1 -0
  136. package/dist/store/schema-2-1-migration.js +71 -0
  137. package/dist/store/schema-2-1-migration.js.map +1 -0
  138. package/dist/store/search-console-store.d.ts +44 -0
  139. package/dist/store/search-console-store.d.ts.map +1 -0
  140. package/dist/store/search-console-store.js +90 -0
  141. package/dist/store/search-console-store.js.map +1 -0
  142. package/dist/store/search-console-tables.d.ts +14 -0
  143. package/dist/store/search-console-tables.d.ts.map +1 -0
  144. package/dist/store/search-console-tables.js +29 -0
  145. package/dist/store/search-console-tables.js.map +1 -0
  146. package/dist/store/search-indexing.d.ts +36 -0
  147. package/dist/store/search-indexing.d.ts.map +1 -1
  148. package/dist/store/search-indexing.js +33 -0
  149. package/dist/store/search-indexing.js.map +1 -1
  150. package/dist/store/site-settings-registry.d.ts +164 -0
  151. package/dist/store/site-settings-registry.d.ts.map +1 -0
  152. package/dist/store/site-settings-registry.js +1160 -0
  153. package/dist/store/site-settings-registry.js.map +1 -0
  154. package/dist/store/site-settings-store.d.ts +42 -0
  155. package/dist/store/site-settings-store.d.ts.map +1 -0
  156. package/dist/store/site-settings-store.js +97 -0
  157. package/dist/store/site-settings-store.js.map +1 -0
  158. package/dist/store/site-settings-tables.d.ts +29 -0
  159. package/dist/store/site-settings-tables.d.ts.map +1 -0
  160. package/dist/store/site-settings-tables.js +48 -0
  161. package/dist/store/site-settings-tables.js.map +1 -0
  162. package/dist/store/store.d.ts +57 -2
  163. package/dist/store/store.d.ts.map +1 -1
  164. package/dist/store/store.js +278 -37
  165. package/dist/store/store.js.map +1 -1
  166. package/dist/store/tables.d.ts.map +1 -1
  167. package/dist/store/tables.js +11 -0
  168. package/dist/store/tables.js.map +1 -1
  169. package/dist/store/taxonomy-store.d.ts.map +1 -1
  170. package/dist/store/taxonomy-store.js +90 -39
  171. package/dist/store/taxonomy-store.js.map +1 -1
  172. package/dist/store/taxonomy-usage.d.ts +47 -0
  173. package/dist/store/taxonomy-usage.d.ts.map +1 -0
  174. package/dist/store/taxonomy-usage.js +101 -0
  175. package/dist/store/taxonomy-usage.js.map +1 -0
  176. package/dist/store/theme-store.d.ts +73 -0
  177. package/dist/store/theme-store.d.ts.map +1 -0
  178. package/dist/store/theme-store.js +193 -0
  179. package/dist/store/theme-store.js.map +1 -0
  180. package/dist/store/types.d.ts +36 -1
  181. package/dist/store/types.d.ts.map +1 -1
  182. package/dist/store/values.d.ts.map +1 -1
  183. package/dist/store/values.js +28 -1
  184. package/dist/store/values.js.map +1 -1
  185. package/dist/system-fields.d.ts +8 -1
  186. package/dist/system-fields.d.ts.map +1 -1
  187. package/dist/system-fields.js +14 -1
  188. package/dist/system-fields.js.map +1 -1
  189. package/dist/types.d.ts +54 -1
  190. package/dist/types.d.ts.map +1 -1
  191. package/dist/types.js +21 -0
  192. package/dist/types.js.map +1 -1
  193. 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":"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"}
@@ -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
- export declare const REDIRECT_REASONS: readonly ['slug-change', 'manual', 'import'];
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
- /** 308 and 307 are deliberately absent: a moved page is a GET, and 301 is what search engines act on. */
19
- export type RedirectStatus = 301 | 302;
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
- readonly to: string;
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;AAE5E,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE9D,yGAAyG;AACzG,MAAM,MAAM,cAAc,GAAG,GAAG,GAAG,GAAG,CAAA;AAEtC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,0BAA0B;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAA;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAA;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,GAAG,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IACrD,8EAA8E;IAC9E,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAA;IACzD,IAAI,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAA;IAC/D,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACtC;;;;;;OAMG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAC3B,mFAAmF;IACnF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAA;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAcD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAgOhF"}
1
+ {"version":3,"file":"redirects.d.ts","sourceRoot":"","sources":["../../src/routing/redirects.ts"],"names":[],"mappings":"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"}