@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.
- package/CHANGELOG.md +15 -0
- package/README.md +14 -8
- package/dist/comments/errors.d.ts +23 -0
- package/dist/comments/errors.js +25 -0
- package/dist/comments/express/index.d.ts +2 -0
- package/dist/comments/express/index.js +2 -0
- package/dist/comments/express/submit-handler.d.ts +30 -0
- package/dist/comments/express/submit-handler.js +85 -0
- package/dist/comments/hooks.d.ts +39 -0
- package/dist/comments/hooks.js +41 -0
- package/dist/comments/index.d.ts +11 -0
- package/dist/comments/index.js +11 -0
- package/dist/comments/ingress.d.ts +57 -0
- package/dist/comments/ingress.js +173 -0
- package/dist/comments/module.d.ts +94 -0
- package/dist/comments/module.js +73 -0
- package/dist/comments/ports.d.ts +184 -0
- package/dist/comments/ports.js +2 -0
- package/dist/comments/repo.memory.d.ts +70 -0
- package/dist/comments/repo.memory.js +120 -0
- package/dist/comments/sanitize.d.ts +15 -0
- package/dist/comments/sanitize.js +31 -0
- package/dist/comments/spam.heuristic.d.ts +8 -0
- package/dist/comments/spam.heuristic.js +54 -0
- package/dist/comments/sql/index.d.ts +3 -0
- package/dist/comments/sql/index.js +3 -0
- package/dist/comments/sql/repo.d.ts +77 -0
- package/dist/comments/sql/repo.js +190 -0
- package/dist/comments/sql/repo.rows.d.ts +49 -0
- package/dist/comments/sql/repo.rows.js +71 -0
- package/dist/comments/types.d.ts +187 -0
- package/dist/comments/types.js +39 -0
- package/dist/comments/write-service.d.ts +121 -0
- package/dist/comments/write-service.js +144 -0
- package/dist/core/commands/command.d.ts +2 -9
- package/dist/core/commands/command.js +2 -9
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +1 -0
- package/dist/core/keyset-cursor.d.ts +25 -0
- package/dist/core/keyset-cursor.js +20 -0
- package/dist/core/ports.d.ts +30 -0
- package/dist/core/ports.js +0 -1
- package/dist/core/tools/args.d.ts +1 -46
- package/dist/core/tools/args.js +1 -44
- package/dist/forms/delete-submission.d.ts +38 -0
- package/dist/forms/delete-submission.js +25 -0
- package/dist/forms/duplicate-slug.d.ts +81 -0
- package/dist/forms/duplicate-slug.js +115 -0
- package/dist/forms/errors.d.ts +53 -0
- package/dist/forms/errors.js +25 -0
- package/dist/forms/express/handler.d.ts +59 -0
- package/dist/forms/express/handler.js +217 -0
- package/dist/forms/express/index.d.ts +2 -0
- package/dist/forms/express/index.js +2 -0
- package/dist/forms/fields-json.d.ts +14 -0
- package/dist/forms/fields-json.js +11 -0
- package/dist/forms/forms.d.ts +59 -0
- package/dist/forms/forms.js +213 -0
- package/dist/forms/html/authoring-adapter.d.ts +34 -0
- package/dist/forms/html/authoring-adapter.js +48 -0
- package/dist/forms/html/html-authoring.d.ts +12 -0
- package/dist/forms/html/html-authoring.js +133 -0
- package/dist/forms/html/html-submission-adapter.d.ts +10 -0
- package/dist/forms/html/html-submission-adapter.js +15 -0
- package/dist/forms/html/index.d.ts +4 -0
- package/dist/forms/html/index.js +4 -0
- package/dist/forms/html-render.d.ts +33 -0
- package/dist/forms/html-render.js +31 -0
- package/dist/forms/index.d.ts +17 -0
- package/dist/forms/index.js +17 -0
- package/dist/forms/notify-subscriber.d.ts +32 -0
- package/dist/forms/notify-subscriber.js +60 -0
- package/dist/forms/ports.d.ts +168 -0
- package/dist/forms/ports.js +2 -0
- package/dist/forms/rate-limit-key.d.ts +8 -0
- package/dist/forms/rate-limit-key.js +7 -0
- package/dist/forms/repo.memory.d.ts +120 -0
- package/dist/forms/repo.memory.js +188 -0
- package/dist/forms/sql/index.d.ts +5 -0
- package/dist/forms/sql/index.js +5 -0
- package/dist/forms/sql/repo.d.ts +94 -0
- package/dist/forms/sql/repo.js +175 -0
- package/dist/forms/sql/repo.rows.d.ts +33 -0
- package/dist/forms/sql/repo.rows.js +64 -0
- package/dist/forms/sql/submission-ip-retention-repo.d.ts +9 -0
- package/dist/forms/sql/submission-ip-retention-repo.js +40 -0
- package/dist/forms/sql/tables.d.ts +32 -0
- package/dist/forms/sql/tables.js +2 -0
- package/dist/forms/submission-attempts.d.ts +70 -0
- package/dist/forms/submission-attempts.js +70 -0
- package/dist/forms/submission-email-validation.d.ts +8 -0
- package/dist/forms/submission-email-validation.js +34 -0
- package/dist/forms/submission-ip-retention.d.ts +30 -0
- package/dist/forms/submission-ip-retention.js +27 -0
- package/dist/forms/submit-service.d.ts +78 -0
- package/dist/forms/submit-service.js +109 -0
- package/dist/forms/types.d.ts +49 -0
- package/dist/forms/types.js +2 -0
- package/dist/forms/write-service.d.ts +83 -0
- package/dist/forms/write-service.js +271 -0
- package/dist/media/index.d.ts +0 -3
- package/dist/media/index.js +1 -4
- package/dist/media/node.d.ts +5 -0
- package/dist/media/node.js +6 -0
- package/dist/redirects/capture.d.ts +80 -0
- package/dist/redirects/capture.js +66 -0
- package/dist/redirects/express/index.d.ts +27 -0
- package/dist/redirects/express/index.js +40 -0
- package/dist/redirects/hit-sink.d.ts +56 -0
- package/dist/redirects/hit-sink.js +44 -0
- package/dist/redirects/host-config/index.d.ts +40 -0
- package/dist/redirects/host-config/index.js +145 -0
- package/dist/redirects/index.d.ts +14 -0
- package/dist/redirects/index.js +14 -0
- package/dist/redirects/matcher.d.ts +58 -0
- package/dist/redirects/matcher.js +144 -0
- package/dist/redirects/order.d.ts +30 -0
- package/dist/redirects/order.js +26 -0
- package/dist/redirects/ports.d.ts +242 -0
- package/dist/redirects/ports.internal.d.ts +57 -0
- package/dist/redirects/ports.internal.js +14 -0
- package/dist/redirects/ports.js +2 -0
- package/dist/redirects/redirects.d.ts +226 -0
- package/dist/redirects/redirects.js +590 -0
- package/dist/redirects/referrer-alias.d.ts +31 -0
- package/dist/redirects/referrer-alias.js +33 -0
- package/dist/redirects/repo.memory.d.ts +51 -0
- package/dist/redirects/repo.memory.js +98 -0
- package/dist/redirects/reserved-destination.d.ts +58 -0
- package/dist/redirects/reserved-destination.js +59 -0
- package/dist/redirects/resolver.d.ts +100 -0
- package/dist/redirects/resolver.js +87 -0
- package/dist/redirects/spa/index.d.ts +30 -0
- package/dist/redirects/spa/index.js +27 -0
- package/dist/redirects/sql/index.d.ts +4 -0
- package/dist/redirects/sql/index.js +4 -0
- package/dist/redirects/sql/repo.d.ts +82 -0
- package/dist/redirects/sql/repo.js +143 -0
- package/dist/redirects/sql/repo.rows.d.ts +49 -0
- package/dist/redirects/sql/repo.rows.js +79 -0
- package/dist/redirects/sql/row-types.d.ts +34 -0
- package/dist/redirects/sql/row-types.js +2 -0
- package/dist/redirects/target-gate.d.ts +30 -0
- package/dist/redirects/target-gate.js +85 -0
- package/dist/redirects/types.d.ts +213 -0
- package/dist/redirects/types.js +42 -0
- package/dist/seo/absolute-url.d.ts +58 -0
- package/dist/seo/absolute-url.js +117 -0
- package/dist/seo/dom/index.d.ts +14 -0
- package/dist/seo/dom/index.js +52 -0
- package/dist/seo/errors.d.ts +35 -0
- package/dist/seo/errors.js +35 -0
- package/dist/seo/feed.d.ts +49 -0
- package/dist/seo/feed.js +61 -0
- package/dist/seo/index.d.ts +13 -0
- package/dist/seo/index.js +13 -0
- package/dist/seo/media.d.ts +57 -0
- package/dist/seo/media.js +134 -0
- package/dist/seo/page-head.d.ts +18 -0
- package/dist/seo/page-head.js +114 -0
- package/dist/seo/ports.d.ts +221 -0
- package/dist/seo/ports.js +27 -0
- package/dist/seo/seo.d.ts +73 -0
- package/dist/seo/seo.js +230 -0
- package/dist/seo/sitemap.d.ts +139 -0
- package/dist/seo/sitemap.js +217 -0
- package/dist/seo/types.d.ts +278 -0
- package/dist/seo/types.js +2 -0
- package/dist/seo/write-service.d.ts +79 -0
- package/dist/seo/write-service.js +273 -0
- package/dist/seo/xml-escape.d.ts +16 -0
- package/dist/seo/xml-escape.js +19 -0
- package/dist/settings/express/cms-adapter.d.ts +20 -0
- package/dist/settings/express/cms-adapter.js +124 -0
- package/dist/settings/express/contracts.d.ts +135 -0
- package/dist/settings/express/contracts.js +2 -0
- package/dist/settings/express/http.d.ts +10 -0
- package/dist/settings/express/http.js +211 -0
- package/dist/settings/express/index.d.ts +5 -0
- package/dist/settings/express/index.js +5 -0
- package/dist/settings/express/shared.d.ts +68 -0
- package/dist/settings/express/shared.js +54 -0
- package/dist/settings/express/stream.d.ts +24 -0
- package/dist/settings/express/stream.js +147 -0
- package/dist/settings/index.d.ts +3 -38
- package/dist/settings/index.js +3 -35
- package/dist/taxonomy/write-service.d.ts +2 -30
- package/dist/widgets/authorize-helper.d.ts +42 -0
- package/dist/widgets/authorize-helper.js +16 -0
- package/dist/widgets/concurrency.d.ts +12 -0
- package/dist/widgets/concurrency.js +44 -0
- package/dist/widgets/config-validation.d.ts +34 -0
- package/dist/widgets/config-validation.js +125 -0
- package/dist/widgets/embed-service.d.ts +133 -0
- package/dist/widgets/embed-service.js +460 -0
- package/dist/widgets/embed-validation.d.ts +23 -0
- package/dist/widgets/embed-validation.js +38 -0
- package/dist/widgets/entry-payload.d.ts +103 -0
- package/dist/widgets/entry-payload.js +160 -0
- package/dist/widgets/errors.d.ts +136 -0
- package/dist/widgets/errors.js +136 -0
- package/dist/widgets/host-ports.d.ts +149 -0
- package/dist/widgets/host-ports.js +2 -0
- package/dist/widgets/html/html-embeds.d.ts +247 -0
- package/dist/widgets/html/html-embeds.js +408 -0
- package/dist/widgets/html/index.d.ts +5 -0
- package/dist/widgets/html/index.js +4 -0
- package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
- package/dist/widgets/html/legacy-widget-markers.js +45 -0
- package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
- package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/index.js +19 -0
- package/dist/widgets/markers/html-attributes.d.ts +98 -0
- package/dist/widgets/markers/html-attributes.js +216 -0
- package/dist/widgets/markers/index.d.ts +3 -0
- package/dist/widgets/markers/index.js +3 -0
- package/dist/widgets/markers/marker.d.ts +496 -0
- package/dist/widgets/markers/marker.js +780 -0
- package/dist/widgets/page-embed-types.d.ts +42 -0
- package/dist/widgets/page-embed-types.js +42 -0
- package/dist/widgets/ports.d.ts +142 -0
- package/dist/widgets/ports.js +2 -0
- package/dist/widgets/read-service.d.ts +81 -0
- package/dist/widgets/read-service.js +79 -0
- package/dist/widgets/region-area-service.d.ts +110 -0
- package/dist/widgets/region-area-service.js +180 -0
- package/dist/widgets/registry.d.ts +217 -0
- package/dist/widgets/registry.js +172 -0
- package/dist/widgets/repo.memory.d.ts +41 -0
- package/dist/widgets/repo.memory.js +48 -0
- package/dist/widgets/resolver-service.d.ts +209 -0
- package/dist/widgets/resolver-service.js +909 -0
- package/dist/widgets/resolvers/contact-form.d.ts +26 -0
- package/dist/widgets/resolvers/contact-form.js +54 -0
- package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
- package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
- package/dist/widgets/resolvers/index.d.ts +65 -0
- package/dist/widgets/resolvers/index.js +155 -0
- package/dist/widgets/resolvers/menu.d.ts +42 -0
- package/dist/widgets/resolvers/menu.js +43 -0
- package/dist/widgets/resolvers/ports.d.ts +82 -0
- package/dist/widgets/resolvers/ports.js +2 -0
- package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
- package/dist/widgets/resolvers/recent-entries.js +165 -0
- package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
- package/dist/widgets/resolvers/taxonomy.js +34 -0
- package/dist/widgets/sql/index.d.ts +3 -0
- package/dist/widgets/sql/index.js +3 -0
- package/dist/widgets/sql/repo.d.ts +41 -0
- package/dist/widgets/sql/repo.js +82 -0
- package/dist/widgets/sql/tables.d.ts +13 -0
- package/dist/widgets/sql/tables.js +2 -0
- package/dist/widgets/types.d.ts +213 -0
- package/dist/widgets/types.js +17 -0
- package/dist/widgets/where-used.d.ts +41 -0
- package/dist/widgets/where-used.js +11 -0
- package/dist/widgets/write-service.d.ts +204 -0
- package/dist/widgets/write-service.js +332 -0
- package/dist/workspace/delete.d.ts +1 -1
- 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
|