@jini-ai/cms 0.5.2 → 0.5.4

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 (261) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +14 -8
  3. package/dist/comments/errors.d.ts +23 -0
  4. package/dist/comments/errors.js +25 -0
  5. package/dist/comments/express/index.d.ts +2 -0
  6. package/dist/comments/express/index.js +2 -0
  7. package/dist/comments/express/submit-handler.d.ts +30 -0
  8. package/dist/comments/express/submit-handler.js +85 -0
  9. package/dist/comments/hooks.d.ts +39 -0
  10. package/dist/comments/hooks.js +41 -0
  11. package/dist/comments/index.d.ts +11 -0
  12. package/dist/comments/index.js +11 -0
  13. package/dist/comments/ingress.d.ts +57 -0
  14. package/dist/comments/ingress.js +173 -0
  15. package/dist/comments/module.d.ts +94 -0
  16. package/dist/comments/module.js +73 -0
  17. package/dist/comments/ports.d.ts +184 -0
  18. package/dist/comments/ports.js +2 -0
  19. package/dist/comments/repo.memory.d.ts +70 -0
  20. package/dist/comments/repo.memory.js +120 -0
  21. package/dist/comments/sanitize.d.ts +15 -0
  22. package/dist/comments/sanitize.js +31 -0
  23. package/dist/comments/spam.heuristic.d.ts +8 -0
  24. package/dist/comments/spam.heuristic.js +54 -0
  25. package/dist/comments/sql/index.d.ts +3 -0
  26. package/dist/comments/sql/index.js +3 -0
  27. package/dist/comments/sql/repo.d.ts +77 -0
  28. package/dist/comments/sql/repo.js +190 -0
  29. package/dist/comments/sql/repo.rows.d.ts +49 -0
  30. package/dist/comments/sql/repo.rows.js +71 -0
  31. package/dist/comments/types.d.ts +187 -0
  32. package/dist/comments/types.js +39 -0
  33. package/dist/comments/write-service.d.ts +121 -0
  34. package/dist/comments/write-service.js +144 -0
  35. package/dist/core/commands/command.d.ts +2 -9
  36. package/dist/core/commands/command.js +2 -9
  37. package/dist/core/index.d.ts +2 -1
  38. package/dist/core/index.js +1 -0
  39. package/dist/core/keyset-cursor.d.ts +25 -0
  40. package/dist/core/keyset-cursor.js +20 -0
  41. package/dist/core/ports.d.ts +30 -0
  42. package/dist/core/ports.js +0 -1
  43. package/dist/core/tools/args.d.ts +1 -46
  44. package/dist/core/tools/args.js +1 -44
  45. package/dist/forms/delete-submission.d.ts +38 -0
  46. package/dist/forms/delete-submission.js +25 -0
  47. package/dist/forms/duplicate-slug.d.ts +81 -0
  48. package/dist/forms/duplicate-slug.js +115 -0
  49. package/dist/forms/errors.d.ts +53 -0
  50. package/dist/forms/errors.js +25 -0
  51. package/dist/forms/express/handler.d.ts +59 -0
  52. package/dist/forms/express/handler.js +217 -0
  53. package/dist/forms/express/index.d.ts +2 -0
  54. package/dist/forms/express/index.js +2 -0
  55. package/dist/forms/fields-json.d.ts +14 -0
  56. package/dist/forms/fields-json.js +11 -0
  57. package/dist/forms/forms.d.ts +59 -0
  58. package/dist/forms/forms.js +213 -0
  59. package/dist/forms/html/authoring-adapter.d.ts +34 -0
  60. package/dist/forms/html/authoring-adapter.js +48 -0
  61. package/dist/forms/html/html-authoring.d.ts +12 -0
  62. package/dist/forms/html/html-authoring.js +133 -0
  63. package/dist/forms/html/html-submission-adapter.d.ts +10 -0
  64. package/dist/forms/html/html-submission-adapter.js +15 -0
  65. package/dist/forms/html/index.d.ts +4 -0
  66. package/dist/forms/html/index.js +4 -0
  67. package/dist/forms/html-render.d.ts +33 -0
  68. package/dist/forms/html-render.js +31 -0
  69. package/dist/forms/index.d.ts +17 -0
  70. package/dist/forms/index.js +17 -0
  71. package/dist/forms/notify-subscriber.d.ts +32 -0
  72. package/dist/forms/notify-subscriber.js +60 -0
  73. package/dist/forms/ports.d.ts +168 -0
  74. package/dist/forms/ports.js +2 -0
  75. package/dist/forms/rate-limit-key.d.ts +8 -0
  76. package/dist/forms/rate-limit-key.js +7 -0
  77. package/dist/forms/repo.memory.d.ts +120 -0
  78. package/dist/forms/repo.memory.js +188 -0
  79. package/dist/forms/sql/index.d.ts +5 -0
  80. package/dist/forms/sql/index.js +5 -0
  81. package/dist/forms/sql/repo.d.ts +94 -0
  82. package/dist/forms/sql/repo.js +175 -0
  83. package/dist/forms/sql/repo.rows.d.ts +33 -0
  84. package/dist/forms/sql/repo.rows.js +64 -0
  85. package/dist/forms/sql/submission-ip-retention-repo.d.ts +9 -0
  86. package/dist/forms/sql/submission-ip-retention-repo.js +40 -0
  87. package/dist/forms/sql/tables.d.ts +32 -0
  88. package/dist/forms/sql/tables.js +2 -0
  89. package/dist/forms/submission-attempts.d.ts +70 -0
  90. package/dist/forms/submission-attempts.js +70 -0
  91. package/dist/forms/submission-email-validation.d.ts +8 -0
  92. package/dist/forms/submission-email-validation.js +34 -0
  93. package/dist/forms/submission-ip-retention.d.ts +30 -0
  94. package/dist/forms/submission-ip-retention.js +27 -0
  95. package/dist/forms/submit-service.d.ts +78 -0
  96. package/dist/forms/submit-service.js +109 -0
  97. package/dist/forms/types.d.ts +49 -0
  98. package/dist/forms/types.js +2 -0
  99. package/dist/forms/write-service.d.ts +83 -0
  100. package/dist/forms/write-service.js +271 -0
  101. package/dist/media/index.d.ts +0 -3
  102. package/dist/media/index.js +1 -4
  103. package/dist/media/node.d.ts +5 -0
  104. package/dist/media/node.js +6 -0
  105. package/dist/redirects/capture.d.ts +80 -0
  106. package/dist/redirects/capture.js +66 -0
  107. package/dist/redirects/express/index.d.ts +27 -0
  108. package/dist/redirects/express/index.js +40 -0
  109. package/dist/redirects/hit-sink.d.ts +56 -0
  110. package/dist/redirects/hit-sink.js +44 -0
  111. package/dist/redirects/host-config/index.d.ts +40 -0
  112. package/dist/redirects/host-config/index.js +145 -0
  113. package/dist/redirects/index.d.ts +14 -0
  114. package/dist/redirects/index.js +14 -0
  115. package/dist/redirects/matcher.d.ts +58 -0
  116. package/dist/redirects/matcher.js +144 -0
  117. package/dist/redirects/order.d.ts +30 -0
  118. package/dist/redirects/order.js +26 -0
  119. package/dist/redirects/ports.d.ts +242 -0
  120. package/dist/redirects/ports.internal.d.ts +57 -0
  121. package/dist/redirects/ports.internal.js +14 -0
  122. package/dist/redirects/ports.js +2 -0
  123. package/dist/redirects/redirects.d.ts +226 -0
  124. package/dist/redirects/redirects.js +590 -0
  125. package/dist/redirects/referrer-alias.d.ts +31 -0
  126. package/dist/redirects/referrer-alias.js +33 -0
  127. package/dist/redirects/repo.memory.d.ts +51 -0
  128. package/dist/redirects/repo.memory.js +98 -0
  129. package/dist/redirects/reserved-destination.d.ts +58 -0
  130. package/dist/redirects/reserved-destination.js +59 -0
  131. package/dist/redirects/resolver.d.ts +100 -0
  132. package/dist/redirects/resolver.js +87 -0
  133. package/dist/redirects/spa/index.d.ts +30 -0
  134. package/dist/redirects/spa/index.js +27 -0
  135. package/dist/redirects/sql/index.d.ts +4 -0
  136. package/dist/redirects/sql/index.js +4 -0
  137. package/dist/redirects/sql/repo.d.ts +82 -0
  138. package/dist/redirects/sql/repo.js +143 -0
  139. package/dist/redirects/sql/repo.rows.d.ts +49 -0
  140. package/dist/redirects/sql/repo.rows.js +79 -0
  141. package/dist/redirects/sql/row-types.d.ts +34 -0
  142. package/dist/redirects/sql/row-types.js +2 -0
  143. package/dist/redirects/target-gate.d.ts +30 -0
  144. package/dist/redirects/target-gate.js +85 -0
  145. package/dist/redirects/types.d.ts +213 -0
  146. package/dist/redirects/types.js +42 -0
  147. package/dist/seo/absolute-url.d.ts +58 -0
  148. package/dist/seo/absolute-url.js +117 -0
  149. package/dist/seo/dom/index.d.ts +14 -0
  150. package/dist/seo/dom/index.js +52 -0
  151. package/dist/seo/errors.d.ts +35 -0
  152. package/dist/seo/errors.js +35 -0
  153. package/dist/seo/feed.d.ts +49 -0
  154. package/dist/seo/feed.js +61 -0
  155. package/dist/seo/index.d.ts +13 -0
  156. package/dist/seo/index.js +13 -0
  157. package/dist/seo/media.d.ts +57 -0
  158. package/dist/seo/media.js +134 -0
  159. package/dist/seo/page-head.d.ts +18 -0
  160. package/dist/seo/page-head.js +114 -0
  161. package/dist/seo/ports.d.ts +221 -0
  162. package/dist/seo/ports.js +27 -0
  163. package/dist/seo/seo.d.ts +73 -0
  164. package/dist/seo/seo.js +230 -0
  165. package/dist/seo/sitemap.d.ts +139 -0
  166. package/dist/seo/sitemap.js +217 -0
  167. package/dist/seo/types.d.ts +278 -0
  168. package/dist/seo/types.js +2 -0
  169. package/dist/seo/write-service.d.ts +79 -0
  170. package/dist/seo/write-service.js +273 -0
  171. package/dist/seo/xml-escape.d.ts +16 -0
  172. package/dist/seo/xml-escape.js +19 -0
  173. package/dist/settings/express/cms-adapter.d.ts +20 -0
  174. package/dist/settings/express/cms-adapter.js +124 -0
  175. package/dist/settings/express/contracts.d.ts +135 -0
  176. package/dist/settings/express/contracts.js +2 -0
  177. package/dist/settings/express/http.d.ts +10 -0
  178. package/dist/settings/express/http.js +211 -0
  179. package/dist/settings/express/index.d.ts +5 -0
  180. package/dist/settings/express/index.js +5 -0
  181. package/dist/settings/express/shared.d.ts +68 -0
  182. package/dist/settings/express/shared.js +54 -0
  183. package/dist/settings/express/stream.d.ts +24 -0
  184. package/dist/settings/express/stream.js +147 -0
  185. package/dist/settings/index.d.ts +3 -38
  186. package/dist/settings/index.js +3 -35
  187. package/dist/taxonomy/write-service.d.ts +2 -30
  188. package/dist/widgets/authorize-helper.d.ts +42 -0
  189. package/dist/widgets/authorize-helper.js +16 -0
  190. package/dist/widgets/concurrency.d.ts +12 -0
  191. package/dist/widgets/concurrency.js +44 -0
  192. package/dist/widgets/config-validation.d.ts +34 -0
  193. package/dist/widgets/config-validation.js +125 -0
  194. package/dist/widgets/embed-service.d.ts +133 -0
  195. package/dist/widgets/embed-service.js +460 -0
  196. package/dist/widgets/embed-validation.d.ts +23 -0
  197. package/dist/widgets/embed-validation.js +38 -0
  198. package/dist/widgets/entry-payload.d.ts +103 -0
  199. package/dist/widgets/entry-payload.js +160 -0
  200. package/dist/widgets/errors.d.ts +136 -0
  201. package/dist/widgets/errors.js +136 -0
  202. package/dist/widgets/host-ports.d.ts +149 -0
  203. package/dist/widgets/host-ports.js +2 -0
  204. package/dist/widgets/html/html-embeds.d.ts +247 -0
  205. package/dist/widgets/html/html-embeds.js +408 -0
  206. package/dist/widgets/html/index.d.ts +5 -0
  207. package/dist/widgets/html/index.js +4 -0
  208. package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
  209. package/dist/widgets/html/legacy-widget-markers.js +45 -0
  210. package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
  211. package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
  212. package/dist/widgets/index.d.ts +20 -0
  213. package/dist/widgets/index.js +19 -0
  214. package/dist/widgets/markers/html-attributes.d.ts +98 -0
  215. package/dist/widgets/markers/html-attributes.js +216 -0
  216. package/dist/widgets/markers/index.d.ts +3 -0
  217. package/dist/widgets/markers/index.js +3 -0
  218. package/dist/widgets/markers/marker.d.ts +496 -0
  219. package/dist/widgets/markers/marker.js +780 -0
  220. package/dist/widgets/page-embed-types.d.ts +42 -0
  221. package/dist/widgets/page-embed-types.js +42 -0
  222. package/dist/widgets/ports.d.ts +142 -0
  223. package/dist/widgets/ports.js +2 -0
  224. package/dist/widgets/read-service.d.ts +81 -0
  225. package/dist/widgets/read-service.js +79 -0
  226. package/dist/widgets/region-area-service.d.ts +110 -0
  227. package/dist/widgets/region-area-service.js +180 -0
  228. package/dist/widgets/registry.d.ts +217 -0
  229. package/dist/widgets/registry.js +172 -0
  230. package/dist/widgets/repo.memory.d.ts +41 -0
  231. package/dist/widgets/repo.memory.js +48 -0
  232. package/dist/widgets/resolver-service.d.ts +209 -0
  233. package/dist/widgets/resolver-service.js +909 -0
  234. package/dist/widgets/resolvers/contact-form.d.ts +26 -0
  235. package/dist/widgets/resolvers/contact-form.js +54 -0
  236. package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
  237. package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
  238. package/dist/widgets/resolvers/index.d.ts +65 -0
  239. package/dist/widgets/resolvers/index.js +155 -0
  240. package/dist/widgets/resolvers/menu.d.ts +42 -0
  241. package/dist/widgets/resolvers/menu.js +43 -0
  242. package/dist/widgets/resolvers/ports.d.ts +82 -0
  243. package/dist/widgets/resolvers/ports.js +2 -0
  244. package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
  245. package/dist/widgets/resolvers/recent-entries.js +165 -0
  246. package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
  247. package/dist/widgets/resolvers/taxonomy.js +34 -0
  248. package/dist/widgets/sql/index.d.ts +3 -0
  249. package/dist/widgets/sql/index.js +3 -0
  250. package/dist/widgets/sql/repo.d.ts +41 -0
  251. package/dist/widgets/sql/repo.js +82 -0
  252. package/dist/widgets/sql/tables.d.ts +13 -0
  253. package/dist/widgets/sql/tables.js +2 -0
  254. package/dist/widgets/types.d.ts +213 -0
  255. package/dist/widgets/types.js +17 -0
  256. package/dist/widgets/where-used.d.ts +41 -0
  257. package/dist/widgets/where-used.js +11 -0
  258. package/dist/widgets/write-service.d.ts +204 -0
  259. package/dist/widgets/write-service.js +332 -0
  260. package/dist/workspace/delete.d.ts +1 -1
  261. package/package.json +220 -12
@@ -0,0 +1,144 @@
1
+ const MIN_PATTERN_LENGTH = 1;
2
+ const MAX_PATTERN_LENGTH = 2048;
3
+ /** Precedence order, highest first — index doubles as the sort key. */
4
+ const PRECEDENCE_ORDER = {
5
+ exact: 0,
6
+ prefix: 1,
7
+ wildcard: 2,
8
+ regex: 3, // never actually reachable — validatePattern always rejects it at write time.
9
+ };
10
+ /**
11
+ * Write-time pattern safety gate (REQ-07/19/20/22). Total — never throws.
12
+ *
13
+ * @complexity O(1).
14
+ */
15
+ export function validatePattern(required, _optional = {}) {
16
+ const { matchType, fromPattern } = required;
17
+ if (matchType === "regex") {
18
+ return { ok: false, reason: "matchType 'regex' is not enabled in v1 (REQ-22)" };
19
+ }
20
+ const length = fromPattern.length;
21
+ if (length < MIN_PATTERN_LENGTH || length > MAX_PATTERN_LENGTH) {
22
+ return {
23
+ ok: false,
24
+ reason: `fromPattern must be ${MIN_PATTERN_LENGTH}-${MAX_PATTERN_LENGTH} characters (got ${length})`,
25
+ };
26
+ }
27
+ if (!fromPattern.startsWith("/")) {
28
+ return { ok: false, reason: "fromPattern must start with '/'" };
29
+ }
30
+ return { ok: true };
31
+ }
32
+ function matchExact(rule, path) {
33
+ if (rule.matchType !== "exact")
34
+ return null;
35
+ if (rule.fromPattern !== path)
36
+ return null;
37
+ return { rule, location: rule.toTarget, specificity: rule.fromPattern.length };
38
+ }
39
+ function matchPrefix(rule, path) {
40
+ if (rule.matchType !== "prefix")
41
+ return null;
42
+ const pattern = rule.fromPattern;
43
+ const isExactPrefix = path === pattern;
44
+ const isNestedPrefix = path.startsWith(pattern.endsWith("/") ? pattern : `${pattern}/`);
45
+ if (!isExactPrefix && !isNestedPrefix)
46
+ return null;
47
+ const tail = isExactPrefix ? "" : path.slice(pattern.length);
48
+ return { rule, location: `${rule.toTarget}${tail}`, specificity: pattern.length };
49
+ }
50
+ /** Compile a `*`-glob wildcard pattern into an anchored, non-backtracking capture regex. */
51
+ function compileWildcard(pattern) {
52
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
53
+ const withCaptures = escaped.replace(/\*/g, "([^/]+)");
54
+ return new RegExp(`^${withCaptures}$`);
55
+ }
56
+ function interpolate(template, captures) {
57
+ return template.replace(/\$(\d+)/g, (whole, indexStr) => {
58
+ const index = Number(indexStr) - 1;
59
+ return index >= 0 && index < captures.length ? captures[index] : whole;
60
+ });
61
+ }
62
+ function matchWildcard(rule, path) {
63
+ if (rule.matchType !== "wildcard")
64
+ return null;
65
+ const regex = compileWildcard(rule.fromPattern);
66
+ const result = regex.exec(path);
67
+ if (!result)
68
+ return null;
69
+ const captures = result.slice(1);
70
+ return {
71
+ rule,
72
+ location: interpolate(rule.toTarget, captures),
73
+ specificity: rule.fromPattern.length,
74
+ };
75
+ }
76
+ function matchOne(rule, path) {
77
+ switch (rule.matchType) {
78
+ case "exact":
79
+ return matchExact(rule, path);
80
+ case "prefix":
81
+ return matchPrefix(rule, path);
82
+ case "wildcard":
83
+ return matchWildcard(rule, path);
84
+ case "regex":
85
+ // Deferred (ADR-033 §8) — never authored in v1 (validatePattern rejects it at write
86
+ // time), so no live rule should ever carry this matchType. Defensive no-match.
87
+ return null;
88
+ default:
89
+ return null;
90
+ }
91
+ }
92
+ /**
93
+ * Tie-break comparator (behavior.spec.md §2.1/§6.1): higher `priority` wins;
94
+ * else more recent `updatedAt` (falling back to `createdAt`) wins; else the
95
+ * lexicographically smaller `id` wins. Returns <0 if `a` should win over `b`.
96
+ */
97
+ function compareTieBreak(a, b) {
98
+ if (a.rule.priority !== b.rule.priority)
99
+ return b.rule.priority - a.rule.priority;
100
+ const aRecency = a.rule.updatedAt || a.rule.createdAt;
101
+ const bRecency = b.rule.updatedAt || b.rule.createdAt;
102
+ if (aRecency !== bRecency)
103
+ return aRecency > bRecency ? -1 : 1;
104
+ return a.rule.id < b.rule.id ? -1 : a.rule.id > b.rule.id ? 1 : 0;
105
+ }
106
+ /** Within a precedence band, longest/most-specific `fromPattern` wins first, then the tie-break chain. */
107
+ function compareWithinBand(a, b) {
108
+ if (a.specificity !== b.specificity)
109
+ return b.specificity - a.specificity;
110
+ return compareTieBreak(a, b);
111
+ }
112
+ /**
113
+ * Resolve the highest-precedence, tie-broken match for `request.path` among
114
+ * `rules` (behavior.spec.md §1.1, §2.1, §2.2, §6.1). Total — never throws.
115
+ *
116
+ * @complexity O(n) in `rules.length`.
117
+ */
118
+ export const match = (required, _optional = {}) => {
119
+ const { request, rules } = required;
120
+ const candidates = [];
121
+ for (const rule of rules) {
122
+ const candidate = matchOne(rule, request.path);
123
+ if (candidate)
124
+ candidates.push(candidate);
125
+ }
126
+ if (candidates.length === 0)
127
+ return { matched: false };
128
+ candidates.sort((a, b) => {
129
+ const precedenceDelta = PRECEDENCE_ORDER[a.rule.matchType] - PRECEDENCE_ORDER[b.rule.matchType];
130
+ if (precedenceDelta !== 0)
131
+ return precedenceDelta;
132
+ return compareWithinBand(a, b);
133
+ });
134
+ const winner = candidates[0];
135
+ return {
136
+ matched: true,
137
+ redirectId: winner.rule.id,
138
+ location: winner.location,
139
+ statusCode: winner.rule.statusCode,
140
+ matchType: winner.rule.matchType,
141
+ };
142
+ };
143
+ export const redirectMatcher = { match, validatePattern };
144
+ //# sourceMappingURL=matcher.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @file The rule orderings both `RedirectRepoPort` adapters apply (`repo.ts` on SQL, `repo.memory.ts`
3
+ * in memory). One module so the two adapters cannot drift: the shared contract suite
4
+ * (`repo.contract.test.ts`) only checks the cases it seeds, while every comparator arm is pinned
5
+ * here directly (`order.test.ts`).
6
+ */
7
+ import type { RedirectRecord } from "./types.js";
8
+ /**
9
+ * Shared tie-break between rules that match equally well: higher `priority` first, then the more
10
+ * recent `updatedAt` (falling back to `createdAt` when `updatedAt` is empty), then `id` ascending.
11
+ * Returns 0 only for the same id, as a sort comparator must.
12
+ */
13
+ export declare function compareTieBreak({ a, b }: {
14
+ a: RedirectRecord;
15
+ b: RedirectRecord;
16
+ }, _optional?: Record<string, never>): number;
17
+ /** Longest pattern first, then the shared tie-break (`lookupLongestPrefix`, `listDynamic`). */
18
+ export declare function compareLongestFirst({ a, b }: {
19
+ a: RedirectRecord;
20
+ b: RedirectRecord;
21
+ }, _optional?: Record<string, never>): number;
22
+ /**
23
+ * `findByFromPattern`'s order among active rules sharing one `fromPattern`: an `exact` rule before
24
+ * any other match type, then `id` ascending.
25
+ */
26
+ export declare function compareSamePatternExactFirst({ a, b }: {
27
+ a: RedirectRecord;
28
+ b: RedirectRecord;
29
+ }, _optional?: Record<string, never>): number;
30
+ //# sourceMappingURL=order.d.ts.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared tie-break between rules that match equally well: higher `priority` first, then the more
3
+ * recent `updatedAt` (falling back to `createdAt` when `updatedAt` is empty), then `id` ascending.
4
+ * Returns 0 only for the same id, as a sort comparator must.
5
+ */
6
+ export function compareTieBreak({ a, b }, _optional = {}) {
7
+ if (a.priority !== b.priority)
8
+ return b.priority - a.priority;
9
+ const aRecency = a.updatedAt || a.createdAt;
10
+ const bRecency = b.updatedAt || b.createdAt;
11
+ if (aRecency !== bRecency)
12
+ return aRecency > bRecency ? -1 : 1;
13
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
14
+ }
15
+ /** Longest pattern first, then the shared tie-break (`lookupLongestPrefix`, `listDynamic`). */
16
+ export function compareLongestFirst({ a, b }, _optional = {}) {
17
+ return b.fromPattern.length - a.fromPattern.length || compareTieBreak({ a, b }, {});
18
+ }
19
+ /**
20
+ * `findByFromPattern`'s order among active rules sharing one `fromPattern`: an `exact` rule before
21
+ * any other match type, then `id` ascending.
22
+ */
23
+ export function compareSamePatternExactFirst({ a, b }, _optional = {}) {
24
+ return (a.matchType === "exact" ? 0 : 1) - (b.matchType === "exact" ? 0 : 1) || (a.id < b.id ? -1 : 1);
25
+ }
26
+ //# sourceMappingURL=order.js.map
@@ -0,0 +1,242 @@
1
+ /**
2
+ * @file Port + seam contracts for the `redirects` Tier-2 library (ADR-033).
3
+ *
4
+ * ADR-006 accounting (rule-of-two):
5
+ * - `RedirectRepoPort` is a genuine port — it rides the ADR-015 seam with two
6
+ * adapters built now (in-memory for tests + Drizzle/SQLite for persistence),
7
+ * matching every other feature in `src/features/*`.
8
+ * - `RedirectMatcher` and `RedirectHitSink` are declared here as ordinary
9
+ * INTERNAL SEAMS, not ADR-006 ports. Neither has a second adapter under
10
+ * construction today: the matcher's plausible-second is an edge/CDN compiler
11
+ * (emit an nginx `map` / Cloudflare ruleset), the hit sink's is an external
12
+ * analytics store — both real, neither built now. Per ADR-006 they stay
13
+ * ordinary code with a documented seam, promoted to a port the day the second
14
+ * adapter is real. They are typed as interfaces purely for testability.
15
+ *
16
+ * Architectural role:
17
+ * INTERFACES ONLY — no feature logic.
18
+ *
19
+ * Grounding: ADR-006 (rule-of-two), ADR-015 (repo behind a port), ADR-009
20
+ * (typed calls / outbox / hooks), ADR-022 (chokepoint writes record a revision
21
+ * in the same tx; bounded matching = the totality/bounded-cost trust primitive),
22
+ * ADR-021 (permission strings enforced by the calling gateway, not the repo).
23
+ */
24
+ import type { ISODateTime, UUID } from "@jini-ai/core/primitives";
25
+ import type { RedirectLocationRefusalReason } from "./target-gate.js";
26
+ import type { DomainEvent } from "../core/index.js";
27
+ import type { ListRedirectsFilter, RedirectHitStats, RedirectRecord, RedirectRequest, RedirectResolution, RedirectRevision } from "./types.js";
28
+ /**
29
+ * Persistence port for redirect rules + their revision ledger + tombstones.
30
+ * The single write chokepoint (ADR-022 §4a): `save` MUST persist the record and
31
+ * its {@link RedirectRevision} in one transaction; no other module writes the
32
+ * `redirects` / `redirect_revisions` tables (CI import-graph canary, ADR-027
33
+ * precedent).
34
+ */
35
+ export interface RedirectRepoPort {
36
+ findById(required: {
37
+ workspaceId: UUID;
38
+ id: UUID;
39
+ }, optional?: Record<string, never>): Promise<RedirectRecord | null>;
40
+ /** Index-backed O(1) exact-path lookup (active rules only). */
41
+ lookupExact(required: {
42
+ workspaceId: UUID;
43
+ path: string;
44
+ includeOverrideOnly: boolean;
45
+ }, optional?: Record<string, never>): Promise<RedirectRecord | null>;
46
+ /** Longest-prefix match over active `prefix` rules (index/trie-backed). */
47
+ lookupLongestPrefix(required: {
48
+ workspaceId: UUID;
49
+ path: string;
50
+ includeOverrideOnly: boolean;
51
+ }, optional?: Record<string, never>): Promise<RedirectRecord | null>;
52
+ /**
53
+ * The bounded "dynamic" set: active `wildcard` + `regex` rules in evaluation
54
+ * order (specificity → priority → recency). Returned capped; callers evaluate
55
+ * them through {@link RedirectMatcher} only after exact/prefix miss.
56
+ */
57
+ listDynamic(required: {
58
+ workspaceId: UUID;
59
+ includeOverrideOnly: boolean;
60
+ limit: number;
61
+ }, optional?: Record<string, never>): Promise<RedirectRecord[]>;
62
+ list(filter: ListRedirectsFilter, optional?: Record<string, never>): Promise<RedirectRecord[]>;
63
+ /**
64
+ * Resolve where an existing rule with the given `fromPattern` already points,
65
+ * used at write time for cycle detection + chain collapsing (ADR-033 §6).
66
+ */
67
+ findByFromPattern(required: {
68
+ workspaceId: UUID;
69
+ fromPattern: string;
70
+ }, optional?: Record<string, never>): Promise<RedirectRecord | null>;
71
+ /** Chokepoint write: upsert the record and append its revision in one tx. */
72
+ save(required: {
73
+ record: RedirectRecord;
74
+ revision: RedirectRevision;
75
+ }, optional?: Record<string, never>): Promise<void>;
76
+ /** Tombstone (soft-delete): flip status + append a tombstone revision in one tx. */
77
+ tombstone(required: {
78
+ workspaceId: UUID;
79
+ id: UUID;
80
+ revision: RedirectRevision;
81
+ }, optional?: Record<string, never>): Promise<void>;
82
+ }
83
+ /**
84
+ * INTERNAL SEAM (not an ADR-006 port). Pure, bounded matching of a request
85
+ * against the dynamic rule set. Implementations MUST be total and
86
+ * bounded-cost — linear-time / RE2 semantics, an anchored pattern, and a
87
+ * per-request evaluation cap — because this is the request-hot-path expression
88
+ * evaluator ADR-022's amendment designates a trust-boundary primitive. No
89
+ * backtracking regex engine may back this.
90
+ */
91
+ export interface RedirectMatcher {
92
+ /**
93
+ * Return the first matching rule with captures interpolated into `location`,
94
+ * or `{ matched: false }`. `rules` is the already-ordered, already-capped
95
+ * dynamic set from {@link RedirectRepoPort.listDynamic}.
96
+ */
97
+ match(required: {
98
+ request: RedirectRequest;
99
+ rules: RedirectRecord[];
100
+ }, optional?: Record<string, never>): RedirectResolution;
101
+ /**
102
+ * Compile-time validation of a single pattern (called at the write
103
+ * chokepoint): rejects unbounded/backtracking constructs before a rule is
104
+ * ever stored, so an unsafe pattern cannot reach the hot path.
105
+ */
106
+ validatePattern(required: {
107
+ matchType: RedirectRecord["matchType"];
108
+ fromPattern: string;
109
+ }, optional?: Record<string, never>): {
110
+ ok: true;
111
+ } | {
112
+ ok: false;
113
+ reason: string;
114
+ };
115
+ }
116
+ /**
117
+ * INTERNAL SEAM (not an ADR-006 port). Best-effort, off-hot-path hit
118
+ * aggregation. The resolver emits a {@link RedirectHitEvent} via the outbox
119
+ * (ADR-009 async lane); an idempotent handler folds it into the
120
+ * `redirect_hits` sidecar through this sink. A dropped increment loses a
121
+ * statistic, never a redirect.
122
+ */
123
+ export interface RedirectHitSink {
124
+ record(required: {
125
+ workspaceId: UUID;
126
+ redirectId: UUID;
127
+ at: ISODateTime;
128
+ }, optional?: Record<string, never>): Promise<void>;
129
+ getStats(required: {
130
+ workspaceId: UUID;
131
+ redirectId: UUID;
132
+ }, optional?: Record<string, never>): Promise<RedirectHitStats | null>;
133
+ listStats(required: {
134
+ workspaceId: UUID;
135
+ }, optional?: Record<string, never>): Promise<RedirectHitStats[]>;
136
+ }
137
+ /**
138
+ * Application service consulted by the routing resolution chain. It is invoked
139
+ * twice per request lifecycle: `pre_content` (override rules only) and
140
+ * `post_content` (the 404-fill pass). The exact registration contract with the
141
+ * (not-yet-designed) `routing` library is OWED by the future routing ADR —
142
+ * ADR-033 §4 specifies the redirects side and flags the seam as co-dependent.
143
+ */
144
+ export interface RedirectResolver {
145
+ resolve(request: RedirectRequest, optional?: Record<string, never>): Promise<RedirectResolution>;
146
+ }
147
+ export interface RedirectMutatedEventPayload {
148
+ workspaceId: UUID;
149
+ redirectId: UUID;
150
+ change: "created" | "updated" | "tombstoned";
151
+ }
152
+ export type RedirectMutatedEvent = DomainEvent<RedirectMutatedEventPayload>;
153
+ export interface RedirectHitEventPayload {
154
+ workspaceId: UUID;
155
+ redirectId: UUID;
156
+ at: ISODateTime;
157
+ }
158
+ export type RedirectHitEvent = DomainEvent<RedirectHitEventPayload>;
159
+ /**
160
+ * Sync filter hook (ADR-009 lane 3): lets a plugin transform or short-circuit a
161
+ * resolution — e.g. an i18n plugin rewriting the target per locale, or an SEO
162
+ * plugin adding canonicalization. Declared-before-attached; ordered by explicit
163
+ * priority. Conditional (geo/device/auth) redirects are the named v1-deferred
164
+ * feature that lands ON this hook, not in core.
165
+ */
166
+ export interface RedirectResolveHookContext {
167
+ request: RedirectRequest;
168
+ /** The resolution core computed; the hook returns it unchanged or replaces it. */
169
+ resolution: RedirectResolution;
170
+ }
171
+ export type RedirectResolveHook = (ctx: RedirectResolveHookContext) => RedirectResolution | Promise<RedirectResolution>;
172
+ export interface RouteResolveContext {
173
+ readonly workspaceId: UUID;
174
+ readonly siteId?: string;
175
+ readonly locale?: string;
176
+ readonly originKey?: string;
177
+ /**
178
+ * TODO(ADR-040): compose canonicalUrl from OriginRegistryPort.canonicalOrigin(ctx)
179
+ * once src/origin exists. Until then, a caller that knows the verified origin
180
+ * may pass it here; absent, canonicalUrl falls back to the relative path.
181
+ */
182
+ readonly originOverride?: string;
183
+ }
184
+ /** Terminal decision a phase resolver may return; anything else falls through. */
185
+ export type RouteResolvePhaseOutcome = {
186
+ readonly kind: "redirect";
187
+ readonly location: string;
188
+ readonly statusCode: number;
189
+ } | {
190
+ readonly kind: "resolved";
191
+ readonly path: string;
192
+ } | {
193
+ readonly kind: "not_found";
194
+ };
195
+ /** Payload handed to the bound {@link SlugChangeCapture} inside the rename transaction. */
196
+ export interface SlugChangeCaptureInput {
197
+ readonly workspaceId: UUID;
198
+ readonly entryId: UUID;
199
+ readonly oldPath: string;
200
+ readonly newPath: string;
201
+ readonly actor: UUID;
202
+ readonly changeSetId?: UUID;
203
+ }
204
+ /**
205
+ * The single named slot content's write chokepoint calls inside the rename
206
+ * transaction (ADR-039 §4). Declared here by routing (content knows nothing of
207
+ * redirects); implemented by the redirects library; wired by the composition
208
+ * root. **No slot implementation may be plugin-supplied** (Round-2 amendment 1
209
+ * — the slot is core-resident by definition).
210
+ *
211
+ * Constraints on any bound implementation (Round-2 amendment 2): idempotent by
212
+ * `changeSetId`, no external I/O, no in-tx publish (an outbox row may be
213
+ * written in-tx per the transactional-outbox pattern, but dispatch happens
214
+ * post-commit), and a strict lock order of content row before redirect rows.
215
+ *
216
+ * Fail-closed nuance (Round-2 amendment 3, not enforced by this library):
217
+ * absent binding is a no-op only before Redirects is installed, or while
218
+ * slug-preservation is disabled for the workspace. Once link-preservation is
219
+ * enabled, a slug-changing rename with no bound capture MUST fail
220
+ * `LINK_PRESERVATION_UNAVAILABLE` rather than silently skip the redirect. That
221
+ * enforcement belongs to the content write chokepoint (which knows whether
222
+ * preservation is enabled) and to Redirects (which doesn't exist yet in this
223
+ * repo) — this library only exposes the registration slot.
224
+ */
225
+ export interface SlugChangeCapture {
226
+ onSlugChange(input: SlugChangeCaptureInput, optional?: Record<string, never>): Promise<void>;
227
+ }
228
+ /** Refusal evidence never exposes a redirect that bypassed the INV-03 gate. */
229
+ export interface RedirectRefusal {
230
+ redirectId: UUID;
231
+ location: string;
232
+ reason: RedirectLocationRefusalReason;
233
+ }
234
+ export interface RedirectResolutionReport {
235
+ resolution: RedirectResolution;
236
+ refused: RedirectRefusal[];
237
+ }
238
+ /** The legacy resolver plus refusal evidence for adapters that must report declines. */
239
+ export interface RedirectResolverWithRefusals extends RedirectResolver {
240
+ resolveWithRefusals(request: RedirectRequest, optional?: Record<string, never>): Promise<RedirectResolutionReport>;
241
+ }
242
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @file Package-private shared insert helper for the `redirects` write path
3
+ * (ADR-PIPE-009 Decision A).
4
+ *
5
+ * Purpose:
6
+ * `insertRedirectAndRevision` is the sole path INV-01 depends on: exactly one
7
+ * `redirects` row write + one `redirect_revisions` row write, in call order,
8
+ * against whatever `RedirectDbHandle` the caller passes in. It performs NO
9
+ * transaction control of its own (no `BEGIN`/`COMMIT`) — atomicity is the
10
+ * CALLER's responsibility: `redirects.ts`'s chokepoint wraps a call to this
11
+ * function in its own `BEGIN IMMEDIATE`/`COMMIT`; `capture.ts` calls it
12
+ * directly, relying on the (future) content chokepoint's already-open ambient
13
+ * transaction on the same single-connection `better-sqlite3` handle
14
+ * (ADR-PIPE-009 Decision A, EC-05).
15
+ *
16
+ * Import-graph note: the domain-logic callers of this function are
17
+ * `redirects.ts` and `capture.ts` only (ADR-PIPE-009/tasks.md T012) — no route
18
+ * handler or UI code may import it. `repo.memory.ts`/`repo.sqlite.ts` import
19
+ * only the `RedirectDbHandle` TYPE (not this function) to declare the shape
20
+ * their own storage satisfies; they do not call `insertRedirectAndRevision`
21
+ * themselves (each adapter's own `save`/`tombstone` port methods write via
22
+ * their own direct storage calls, independently testable by the
23
+ * `RedirectRepoPort` contract suite, T007). This keeps the chokepoint
24
+ * boundary (INV-07) at {`redirects.ts`, `capture.ts`, `ports.internal.ts`},
25
+ * matching the Enforcement section's review-time check.
26
+ *
27
+ * Architectural role:
28
+ * Package-private. NOT exported from `index.ts` (ADR-PIPE-009 Migration
29
+ * Safety / File Map).
30
+ */
31
+ import type { RedirectRecord, RedirectRevision } from "./types.js";
32
+ /**
33
+ * The minimal storage seam `insertRedirectAndRevision` writes through.
34
+ * Deliberately narrower than `RedirectRepoPort` (no reads, no transaction
35
+ * control) — both `repo.memory.ts` and `repo.sqlite.ts` (or a small internal
36
+ * handle each of them owns) satisfy this shape so `redirects.ts`/`capture.ts`
37
+ * can share one write path regardless of which adapter is active.
38
+ */
39
+ export interface RedirectDbHandle {
40
+ insertRedirect(record: RedirectRecord, optional?: Record<string, never>): void | Promise<void>;
41
+ insertRevision(revision: RedirectRevision, optional?: Record<string, never>): void | Promise<void>;
42
+ }
43
+ export interface InsertRedirectAndRevisionRequired {
44
+ db: RedirectDbHandle;
45
+ record: RedirectRecord;
46
+ revision: RedirectRevision;
47
+ }
48
+ /**
49
+ * Insert a `redirects` row and its paired `redirect_revisions` row, in that
50
+ * order, against `db`. Never opens or closes a transaction; any error from
51
+ * `db` propagates uncaught so the caller's own transaction (or lack thereof)
52
+ * decides the outcome.
53
+ *
54
+ * @complexity O(1) — two writes, no I/O beyond what `db` itself performs.
55
+ */
56
+ export declare function insertRedirectAndRevision(required: InsertRedirectAndRevisionRequired, _optional?: Record<string, never>): Promise<void>;
57
+ //# sourceMappingURL=ports.internal.d.ts.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Insert a `redirects` row and its paired `redirect_revisions` row, in that
3
+ * order, against `db`. Never opens or closes a transaction; any error from
4
+ * `db` propagates uncaught so the caller's own transaction (or lack thereof)
5
+ * decides the outcome.
6
+ *
7
+ * @complexity O(1) — two writes, no I/O beyond what `db` itself performs.
8
+ */
9
+ export async function insertRedirectAndRevision(required, _optional = {}) {
10
+ const { db, record, revision } = required;
11
+ await db.insertRedirect(record);
12
+ await db.insertRevision(revision);
13
+ }
14
+ //# sourceMappingURL=ports.internal.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map