@jini-ai/cms 0.5.1 → 0.5.3

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 (209) hide show
  1. package/CHANGELOG.md +16 -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/media/index.d.ts +1 -2
  46. package/dist/media/index.js +2 -2
  47. package/dist/media/node.d.ts +5 -0
  48. package/dist/media/node.js +6 -0
  49. package/dist/media/video-frames.d.ts +94 -0
  50. package/dist/media/video-frames.ffmpeg.d.ts +39 -0
  51. package/dist/media/video-frames.ffmpeg.js +261 -0
  52. package/dist/media/video-frames.js +97 -0
  53. package/dist/redirects/capture.d.ts +80 -0
  54. package/dist/redirects/capture.js +66 -0
  55. package/dist/redirects/express/index.d.ts +27 -0
  56. package/dist/redirects/express/index.js +40 -0
  57. package/dist/redirects/hit-sink.d.ts +56 -0
  58. package/dist/redirects/hit-sink.js +44 -0
  59. package/dist/redirects/host-config/index.d.ts +40 -0
  60. package/dist/redirects/host-config/index.js +145 -0
  61. package/dist/redirects/index.d.ts +14 -0
  62. package/dist/redirects/index.js +14 -0
  63. package/dist/redirects/matcher.d.ts +58 -0
  64. package/dist/redirects/matcher.js +144 -0
  65. package/dist/redirects/order.d.ts +30 -0
  66. package/dist/redirects/order.js +26 -0
  67. package/dist/redirects/ports.d.ts +242 -0
  68. package/dist/redirects/ports.internal.d.ts +57 -0
  69. package/dist/redirects/ports.internal.js +14 -0
  70. package/dist/redirects/ports.js +2 -0
  71. package/dist/redirects/redirects.d.ts +226 -0
  72. package/dist/redirects/redirects.js +590 -0
  73. package/dist/redirects/referrer-alias.d.ts +31 -0
  74. package/dist/redirects/referrer-alias.js +33 -0
  75. package/dist/redirects/repo.memory.d.ts +51 -0
  76. package/dist/redirects/repo.memory.js +98 -0
  77. package/dist/redirects/reserved-destination.d.ts +58 -0
  78. package/dist/redirects/reserved-destination.js +59 -0
  79. package/dist/redirects/resolver.d.ts +100 -0
  80. package/dist/redirects/resolver.js +87 -0
  81. package/dist/redirects/spa/index.d.ts +30 -0
  82. package/dist/redirects/spa/index.js +27 -0
  83. package/dist/redirects/sql/index.d.ts +4 -0
  84. package/dist/redirects/sql/index.js +4 -0
  85. package/dist/redirects/sql/repo.d.ts +82 -0
  86. package/dist/redirects/sql/repo.js +143 -0
  87. package/dist/redirects/sql/repo.rows.d.ts +49 -0
  88. package/dist/redirects/sql/repo.rows.js +79 -0
  89. package/dist/redirects/sql/row-types.d.ts +34 -0
  90. package/dist/redirects/sql/row-types.js +2 -0
  91. package/dist/redirects/target-gate.d.ts +30 -0
  92. package/dist/redirects/target-gate.js +85 -0
  93. package/dist/redirects/types.d.ts +213 -0
  94. package/dist/redirects/types.js +42 -0
  95. package/dist/seo/absolute-url.d.ts +58 -0
  96. package/dist/seo/absolute-url.js +117 -0
  97. package/dist/seo/dom/index.d.ts +14 -0
  98. package/dist/seo/dom/index.js +52 -0
  99. package/dist/seo/errors.d.ts +35 -0
  100. package/dist/seo/errors.js +35 -0
  101. package/dist/seo/feed.d.ts +49 -0
  102. package/dist/seo/feed.js +61 -0
  103. package/dist/seo/index.d.ts +13 -0
  104. package/dist/seo/index.js +13 -0
  105. package/dist/seo/media.d.ts +57 -0
  106. package/dist/seo/media.js +134 -0
  107. package/dist/seo/page-head.d.ts +18 -0
  108. package/dist/seo/page-head.js +114 -0
  109. package/dist/seo/ports.d.ts +221 -0
  110. package/dist/seo/ports.js +27 -0
  111. package/dist/seo/seo.d.ts +73 -0
  112. package/dist/seo/seo.js +230 -0
  113. package/dist/seo/sitemap.d.ts +139 -0
  114. package/dist/seo/sitemap.js +217 -0
  115. package/dist/seo/types.d.ts +278 -0
  116. package/dist/seo/types.js +2 -0
  117. package/dist/seo/write-service.d.ts +79 -0
  118. package/dist/seo/write-service.js +273 -0
  119. package/dist/seo/xml-escape.d.ts +16 -0
  120. package/dist/seo/xml-escape.js +19 -0
  121. package/dist/settings/express/cms-adapter.d.ts +20 -0
  122. package/dist/settings/express/cms-adapter.js +124 -0
  123. package/dist/settings/express/contracts.d.ts +135 -0
  124. package/dist/settings/express/contracts.js +2 -0
  125. package/dist/settings/express/http.d.ts +10 -0
  126. package/dist/settings/express/http.js +211 -0
  127. package/dist/settings/express/index.d.ts +5 -0
  128. package/dist/settings/express/index.js +5 -0
  129. package/dist/settings/express/shared.d.ts +68 -0
  130. package/dist/settings/express/shared.js +54 -0
  131. package/dist/settings/express/stream.d.ts +24 -0
  132. package/dist/settings/express/stream.js +147 -0
  133. package/dist/settings/index.d.ts +3 -38
  134. package/dist/settings/index.js +3 -35
  135. package/dist/taxonomy/write-service.d.ts +2 -30
  136. package/dist/widgets/authorize-helper.d.ts +42 -0
  137. package/dist/widgets/authorize-helper.js +16 -0
  138. package/dist/widgets/concurrency.d.ts +12 -0
  139. package/dist/widgets/concurrency.js +44 -0
  140. package/dist/widgets/config-validation.d.ts +34 -0
  141. package/dist/widgets/config-validation.js +125 -0
  142. package/dist/widgets/embed-service.d.ts +133 -0
  143. package/dist/widgets/embed-service.js +460 -0
  144. package/dist/widgets/embed-validation.d.ts +23 -0
  145. package/dist/widgets/embed-validation.js +38 -0
  146. package/dist/widgets/entry-payload.d.ts +103 -0
  147. package/dist/widgets/entry-payload.js +160 -0
  148. package/dist/widgets/errors.d.ts +136 -0
  149. package/dist/widgets/errors.js +136 -0
  150. package/dist/widgets/host-ports.d.ts +149 -0
  151. package/dist/widgets/host-ports.js +2 -0
  152. package/dist/widgets/html/html-embeds.d.ts +247 -0
  153. package/dist/widgets/html/html-embeds.js +408 -0
  154. package/dist/widgets/html/index.d.ts +5 -0
  155. package/dist/widgets/html/index.js +4 -0
  156. package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
  157. package/dist/widgets/html/legacy-widget-markers.js +45 -0
  158. package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
  159. package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
  160. package/dist/widgets/index.d.ts +20 -0
  161. package/dist/widgets/index.js +19 -0
  162. package/dist/widgets/markers/html-attributes.d.ts +98 -0
  163. package/dist/widgets/markers/html-attributes.js +216 -0
  164. package/dist/widgets/markers/index.d.ts +3 -0
  165. package/dist/widgets/markers/index.js +3 -0
  166. package/dist/widgets/markers/marker.d.ts +496 -0
  167. package/dist/widgets/markers/marker.js +780 -0
  168. package/dist/widgets/page-embed-types.d.ts +42 -0
  169. package/dist/widgets/page-embed-types.js +42 -0
  170. package/dist/widgets/ports.d.ts +142 -0
  171. package/dist/widgets/ports.js +2 -0
  172. package/dist/widgets/read-service.d.ts +81 -0
  173. package/dist/widgets/read-service.js +79 -0
  174. package/dist/widgets/region-area-service.d.ts +110 -0
  175. package/dist/widgets/region-area-service.js +180 -0
  176. package/dist/widgets/registry.d.ts +217 -0
  177. package/dist/widgets/registry.js +172 -0
  178. package/dist/widgets/repo.memory.d.ts +41 -0
  179. package/dist/widgets/repo.memory.js +48 -0
  180. package/dist/widgets/resolver-service.d.ts +209 -0
  181. package/dist/widgets/resolver-service.js +909 -0
  182. package/dist/widgets/resolvers/contact-form.d.ts +26 -0
  183. package/dist/widgets/resolvers/contact-form.js +54 -0
  184. package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
  185. package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
  186. package/dist/widgets/resolvers/index.d.ts +65 -0
  187. package/dist/widgets/resolvers/index.js +155 -0
  188. package/dist/widgets/resolvers/menu.d.ts +42 -0
  189. package/dist/widgets/resolvers/menu.js +43 -0
  190. package/dist/widgets/resolvers/ports.d.ts +82 -0
  191. package/dist/widgets/resolvers/ports.js +2 -0
  192. package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
  193. package/dist/widgets/resolvers/recent-entries.js +165 -0
  194. package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
  195. package/dist/widgets/resolvers/taxonomy.js +34 -0
  196. package/dist/widgets/sql/index.d.ts +3 -0
  197. package/dist/widgets/sql/index.js +3 -0
  198. package/dist/widgets/sql/repo.d.ts +41 -0
  199. package/dist/widgets/sql/repo.js +82 -0
  200. package/dist/widgets/sql/tables.d.ts +13 -0
  201. package/dist/widgets/sql/tables.js +2 -0
  202. package/dist/widgets/types.d.ts +213 -0
  203. package/dist/widgets/types.js +17 -0
  204. package/dist/widgets/where-used.d.ts +41 -0
  205. package/dist/widgets/where-used.js +11 -0
  206. package/dist/widgets/write-service.d.ts +204 -0
  207. package/dist/widgets/write-service.js +332 -0
  208. package/dist/workspace/delete.d.ts +1 -1
  209. package/package.json +220 -11
@@ -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
@@ -0,0 +1,226 @@
1
+ /**
2
+ * @file THE `redirects` write chokepoint (SPEC-009 REQ-01/04/05/06/07/08/13/
3
+ * 14/22/26; ADR-PIPE-009 C-001..C-004).
4
+ *
5
+ * Purpose:
6
+ * `createRedirect`/`updateRedirect`/`tombstoneRedirect`/`importRedirects` —
7
+ * the single manual-write entry point for redirect rules. Validates the
8
+ * pattern (via `RedirectMatcher.validatePattern`), rejects unsafe targets —
9
+ * absolute ones against the origin allowlist and site-relative ones against
10
+ * the admin application's own URL space (write-path open-redirect oracle,
11
+ * REQ-08; see `assertTargetAllowed`) — collapses/rejects
12
+ * one-hop chains (INV-04), enforces the exact-match dedup rule
13
+ * (behavior.spec.md §5.1), and writes the record + its revision in one
14
+ * transaction via `ports.internal.ts`'s shared `insertRedirectAndRevision`
15
+ * (INV-01/INV-07 — no other file writes `redirects`/`redirect_revisions`).
16
+ *
17
+ * Architectural role:
18
+ * Feature logic. No Express/route code — route handlers (Phase 2) call these
19
+ * functions directly and map thrown typed errors to HTTP codes per
20
+ * errors.spec.md.
21
+ */
22
+ import { type Clock as ClockPort, type IdGenerator as IdGeneratorPort } from "@jini-ai/core/primitives";
23
+ import { type OutboxPort } from "../core/index.js";
24
+ import type { OriginRegistryPort } from "@jini-ai/http-kit/verified-origin";
25
+ import { type RedirectDbHandle } from "./ports.internal.js";
26
+ import type { RedirectMatcher, RedirectRepoPort } from "./ports.js";
27
+ import type { CreateRedirectInput, RedirectMatchType, RedirectRecord, RedirectStatusCode, UpdateRedirectInput } from "./types.js";
28
+ export declare const MIN_TARGET_LENGTH = 1;
29
+ export declare const MAX_TARGET_LENGTH = 2048;
30
+ export declare const MIN_PRIORITY = 0;
31
+ export declare const MAX_PRIORITY = 1000;
32
+ export declare const VALID_STATUS_CODES: readonly RedirectStatusCode[];
33
+ export declare const MAX_IMPORT_BATCH_SIZE = 500;
34
+ /**
35
+ * The removal primitive this domain is handed, rather than one it implements.
36
+ *
37
+ * Structurally typed ON PURPOSE — `features/redirects` imports nothing from `features/trash`, and
38
+ * must not. The composition root binds the real implementation (marker flip AND Trash index row, in
39
+ * one transaction) pre-bound to this domain's entity type. `display` is required so the two strings
40
+ * the Trash screen shows come from columns the caller already holds.
41
+ */
42
+ export type RemoveRedirectFn = (required: {
43
+ workspaceId: string;
44
+ id: string;
45
+ display: {
46
+ title: string;
47
+ subtitle?: string | null;
48
+ };
49
+ at: string;
50
+ expectedVersion: number | null;
51
+ actor: {
52
+ principalId: string;
53
+ pluginId?: string | null;
54
+ };
55
+ }) => Promise<{
56
+ ok: true;
57
+ version: number | null;
58
+ } | {
59
+ ok: false;
60
+ reason: "not-found" | "version-changed";
61
+ }>;
62
+ export interface RedirectsWriteDeps {
63
+ repo: RedirectRepoPort;
64
+ /** See {@link RemoveRedirectFn}. Replaces `tombstoneRedirect`'s own `status` write. */
65
+ remove: RemoveRedirectFn;
66
+ /**
67
+ * Whether this rule has a Trash index row (S7, web-high fix plan 2026-09-24) — the thing
68
+ * `tombstoneRedirect` creates and Trash restore/purge match on, and NOT the same thing as
69
+ * `status === "disabled"` (an ordinary toggle-off has no Trash row at all). Structurally typed,
70
+ * same discipline as {@link RemoveRedirectFn}: this domain imports nothing from `features/trash`.
71
+ */
72
+ isInTrash: (required: {
73
+ workspaceId: string;
74
+ id: string;
75
+ }) => Promise<boolean>;
76
+ /**
77
+ * Un-trashes this rule (S7). Mirrors `TrashPort.restore`'s outcome union structurally, again
78
+ * without importing `features/trash`. `"adapter-unavailable"` is not reachable for redirects (the
79
+ * composition root always registers an adapter for `REDIRECT_ENTITY_TYPE`), but is part of the
80
+ * union it mirrors.
81
+ */
82
+ restore: (required: {
83
+ workspaceId: string;
84
+ id: string;
85
+ at: string;
86
+ actor: {
87
+ principalId: string;
88
+ pluginId?: string | null;
89
+ };
90
+ }) => Promise<"restored" | "not-found" | "version-changed" | "adapter-unavailable">;
91
+ /** The shared, non-tx-opening write handle (Decision A) — used via `ports.internal.ts`. */
92
+ db: RedirectDbHandle;
93
+ /** Opens/commits/rolls back the chokepoint's own transaction around a write. */
94
+ transaction: <T>(fn: () => Promise<T>) => Promise<T>;
95
+ matcher: RedirectMatcher;
96
+ originRegistry: OriginRegistryPort;
97
+ /** First path segments reserved by the composing host; no product-policy default. */
98
+ reservedSegments: ReadonlySet<string>;
99
+ clock: ClockPort;
100
+ idGen: IdGeneratorPort;
101
+ outbox: OutboxPort;
102
+ }
103
+ /**
104
+ * The site-relative half of the write gate: routing's structural + reserved-path verdict, then the
105
+ * redirect oracle's own raw-character rule. The read path hands a relative location to that oracle
106
+ * by concatenation, so a target it refuses (a raw backslash, any whitespace) would be stored and
107
+ * never fire; refusing it here keeps write and read agreeing. Checked SECOND so the more specific
108
+ * routing reasons (off-origin, control character, reserved) keep their own wording — a bare
109
+ * backslash target is `ok` to `checkSiteRelativeTarget` (on this site, `\` really does resolve to
110
+ * `/`), so only this second check ever refuses it (t91 B2, 2026-09-16). Last, the exact target
111
+ * `back`, which Express serves as the visitor's `Referer` instead of a path (see
112
+ * `./referrer-alias.ts`; t91 review F1, 2026-09-16).
113
+ *
114
+ * Exported as THE check for any caller-chosen, site-relative `Location` (t91 open-redirect fix,
115
+ * 2026-09-16): `routes/site/store.ts`'s `returnTo` had its own `startsWith("/") && !startsWith("//")`
116
+ * copy, which `/\evil.example` walks straight through. A caller that needs a path-absolute value
117
+ * still checks `startsWith("/")` itself — this function also accepts relative references (`new`,
118
+ * `?x`), which stay on-site but resolve against the current URL.
119
+ *
120
+ * @param required.target - The untrusted reference exactly as received, before any `new URL()` parse. Anything
121
+ * that is not site-relative (an absolute or protocol-relative URL, `javascript:`) is refused too.
122
+ * @returns The reason clause, or `null` when the target is allowed.
123
+ * @complexity O(n) in the target length.
124
+ * @example siteRelativeTargetReason({ target: "/products", reservedSegments }, {}); // => null
125
+ * @example siteRelativeTargetReason({ target: "/\\evil.example", reservedSegments }, {}) !== null; // => true
126
+ */
127
+ export declare function siteRelativeTargetReason({ target, reservedSegments }: {
128
+ target: string;
129
+ reservedSegments: ReadonlySet<string>;
130
+ }, _optional?: Record<string, never>): string | null;
131
+ /**
132
+ * Pure precondition check mirroring every validator `createRedirect`/`updateRedirect` run, with NO
133
+ * write — `features/redirects/publish-content.ts`'s `precheck()` reuses this so the two can never
134
+ * drift onto different rulesets. Returns the refusal message a create/update against these exact
135
+ * fields would throw, or `null` when nothing would block.
136
+ *
137
+ * @param excludeId Mirrors {@link assertNoDuplicate}'s own parameter — the destination's own row id,
138
+ * when checking an update against itself.
139
+ * @complexity O(n) in `toTarget`'s length, plus the repo/origin reads each check makes — identical
140
+ * cost to `createRedirect`'s own validation pass, since this runs the exact same sequence.
141
+ */
142
+ export declare function checkRedirectFieldsWouldWrite({ deps, fields }: {
143
+ deps: RedirectsWriteDeps;
144
+ fields: {
145
+ workspaceId: string;
146
+ matchType: RedirectMatchType;
147
+ fromPattern: string;
148
+ toTarget: string;
149
+ statusCode: RedirectStatusCode;
150
+ priority: number;
151
+ };
152
+ }, { excludeId }?: {
153
+ excludeId?: string;
154
+ }): Promise<string | null>;
155
+ export interface CreateRedirectRequired {
156
+ deps: RedirectsWriteDeps;
157
+ input: CreateRedirectInput;
158
+ /**
159
+ * Provenance to stamp on the created record (REQ-01/26). Defaults to
160
+ * `'manual'`. `importRedirects` (C-004) passes `'import'` here — this is
161
+ * an internal parameter, not part of `CreateRedirectInput`'s public shape
162
+ * (an API caller never chooses their own provenance).
163
+ */
164
+ source?: "manual" | "import";
165
+ }
166
+ /** C-001: manual-create entry point (REQ-01). */
167
+ export declare function createRedirect(required: CreateRedirectRequired, _optional?: Record<string, never>): Promise<{
168
+ record: RedirectRecord;
169
+ }>;
170
+ export interface UpdateRedirectRequired {
171
+ deps: RedirectsWriteDeps;
172
+ input: UpdateRedirectInput;
173
+ }
174
+ /**
175
+ * C-002: symmetric chokepoint entry for updates (REQ-04). A disable-only update
176
+ * ({@link isDisableOnlyUpdate}) writes the status change without re-validating the unchanged fields
177
+ * — it stores the existing target as-is, with no one-hop collapse.
178
+ *
179
+ * S7 (web-high fix plan 2026-09-24): a rule with a Trash index row (`deps.isInTrash`, distinct from
180
+ * `status === "disabled"` — see {@link RedirectsWriteDeps.isInTrash}'s own doc) refuses every update
181
+ * EXCEPT a pure `status: "active"` PATCH ({@link isRestoreOnlyUpdate}), which restores it instead of
182
+ * editing a row the caller cannot see the current shape of.
183
+ *
184
+ * @throws {EntityNotLiveError} On a non-restore update to a rule in the Trash.
185
+ * @throws {RedirectConflictError} If the Trash row's version moved before the restore landed.
186
+ */
187
+ export declare function updateRedirect(required: UpdateRedirectRequired, _optional?: Record<string, never>): Promise<{
188
+ record: RedirectRecord;
189
+ }>;
190
+ export interface TombstoneRedirectRequired {
191
+ deps: RedirectsWriteDeps;
192
+ input: {
193
+ workspaceId: string;
194
+ id: string;
195
+ actorId: string;
196
+ pluginId?: string;
197
+ };
198
+ }
199
+ /** C-003: soft-delete (REQ-05). */
200
+ export declare function tombstoneRedirect(required: TombstoneRedirectRequired, _optional?: Record<string, never>): Promise<{
201
+ record: RedirectRecord;
202
+ }>;
203
+ export interface ImportRedirectsRequired {
204
+ deps: RedirectsWriteDeps;
205
+ input: {
206
+ workspaceId: string;
207
+ actorId: string;
208
+ rules: CreateRedirectInput[];
209
+ };
210
+ }
211
+ export interface ImportRedirectsFailure {
212
+ index: number;
213
+ code: string;
214
+ message: string;
215
+ }
216
+ /**
217
+ * C-004: batch-create reusing the same chokepoint, no bypass (REQ-26). Each
218
+ * item is processed independently through `createRedirect` — NOT one shared
219
+ * transaction across items (EC-08); a per-item failure does not abort
220
+ * already-written prior items.
221
+ */
222
+ export declare function importRedirects(required: ImportRedirectsRequired, _optional?: Record<string, never>): Promise<{
223
+ created: RedirectRecord[];
224
+ failed: ImportRedirectsFailure[];
225
+ }>;
226
+ //# sourceMappingURL=redirects.d.ts.map