@actuate-media/cms-core 0.88.0 → 0.90.0

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 (130) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/api/route-helpers.d.ts.map +1 -1
  3. package/dist/api/route-helpers.js +1 -0
  4. package/dist/api/route-helpers.js.map +1 -1
  5. package/dist/api/routes/ai-seo.d.ts.map +1 -1
  6. package/dist/api/routes/ai-seo.js +65 -16
  7. package/dist/api/routes/ai-seo.js.map +1 -1
  8. package/dist/api/routes/media.d.ts.map +1 -1
  9. package/dist/api/routes/media.js +3 -0
  10. package/dist/api/routes/media.js.map +1 -1
  11. package/dist/api/routes/redirects.d.ts.map +1 -1
  12. package/dist/api/routes/redirects.js +185 -106
  13. package/dist/api/routes/redirects.js.map +1 -1
  14. package/dist/api/routes/saved-sections.d.ts.map +1 -1
  15. package/dist/api/routes/saved-sections.js +35 -3
  16. package/dist/api/routes/saved-sections.js.map +1 -1
  17. package/dist/api/routes/seo-public.d.ts.map +1 -1
  18. package/dist/api/routes/seo-public.js +42 -56
  19. package/dist/api/routes/seo-public.js.map +1 -1
  20. package/dist/api/routes/seo.d.ts.map +1 -1
  21. package/dist/api/routes/seo.js +325 -30
  22. package/dist/api/routes/seo.js.map +1 -1
  23. package/dist/api/routes/url-resolution.d.ts.map +1 -1
  24. package/dist/api/routes/url-resolution.js +1 -0
  25. package/dist/api/routes/url-resolution.js.map +1 -1
  26. package/dist/client.d.ts +1 -0
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +1 -0
  29. package/dist/client.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/media/index.d.ts +1 -0
  35. package/dist/media/index.d.ts.map +1 -1
  36. package/dist/media/index.js +1 -0
  37. package/dist/media/index.js.map +1 -1
  38. package/dist/media/optimize.d.ts +24 -2
  39. package/dist/media/optimize.d.ts.map +1 -1
  40. package/dist/media/optimize.js +55 -36
  41. package/dist/media/optimize.js.map +1 -1
  42. package/dist/redirects/fix-link.d.ts +15 -0
  43. package/dist/redirects/fix-link.d.ts.map +1 -0
  44. package/dist/redirects/fix-link.js +66 -0
  45. package/dist/redirects/fix-link.js.map +1 -0
  46. package/dist/redirects/index.d.ts +10 -0
  47. package/dist/redirects/index.d.ts.map +1 -1
  48. package/dist/redirects/index.js +6 -0
  49. package/dist/redirects/index.js.map +1 -1
  50. package/dist/redirects/legitimacy.d.ts +23 -0
  51. package/dist/redirects/legitimacy.d.ts.map +1 -0
  52. package/dist/redirects/legitimacy.js +64 -0
  53. package/dist/redirects/legitimacy.js.map +1 -0
  54. package/dist/redirects/recovery.d.ts +85 -0
  55. package/dist/redirects/recovery.d.ts.map +1 -0
  56. package/dist/redirects/recovery.js +176 -0
  57. package/dist/redirects/recovery.js.map +1 -0
  58. package/dist/redirects/rerank.d.ts +24 -0
  59. package/dist/redirects/rerank.d.ts.map +1 -0
  60. package/dist/redirects/rerank.js +82 -0
  61. package/dist/redirects/rerank.js.map +1 -0
  62. package/dist/redirects/suggest.d.ts +2 -0
  63. package/dist/redirects/suggest.d.ts.map +1 -1
  64. package/dist/redirects/suggest.js +25 -0
  65. package/dist/redirects/suggest.js.map +1 -1
  66. package/dist/redirects/typo.d.ts +10 -0
  67. package/dist/redirects/typo.d.ts.map +1 -0
  68. package/dist/redirects/typo.js +51 -0
  69. package/dist/redirects/typo.js.map +1 -0
  70. package/dist/redirects/validate-destination.d.ts +13 -0
  71. package/dist/redirects/validate-destination.d.ts.map +1 -0
  72. package/dist/redirects/validate-destination.js +51 -0
  73. package/dist/redirects/validate-destination.js.map +1 -0
  74. package/dist/script-tags/resolve.d.ts +7 -6
  75. package/dist/script-tags/resolve.d.ts.map +1 -1
  76. package/dist/script-tags/resolve.js +20 -8
  77. package/dist/script-tags/resolve.js.map +1 -1
  78. package/dist/sections/validate.d.ts +2 -0
  79. package/dist/sections/validate.d.ts.map +1 -1
  80. package/dist/sections/validate.js +23 -0
  81. package/dist/sections/validate.js.map +1 -1
  82. package/dist/seo/audit-engine.d.ts +4 -0
  83. package/dist/seo/audit-engine.d.ts.map +1 -1
  84. package/dist/seo/audit-engine.js +28 -3
  85. package/dist/seo/audit-engine.js.map +1 -1
  86. package/dist/seo/audit-runner.d.ts.map +1 -1
  87. package/dist/seo/audit-runner.js +29 -2
  88. package/dist/seo/audit-runner.js.map +1 -1
  89. package/dist/seo/config-store.d.ts +10 -0
  90. package/dist/seo/config-store.d.ts.map +1 -1
  91. package/dist/seo/config-store.js +14 -0
  92. package/dist/seo/config-store.js.map +1 -1
  93. package/dist/seo/index.d.ts +1 -1
  94. package/dist/seo/index.d.ts.map +1 -1
  95. package/dist/seo/index.js +1 -1
  96. package/dist/seo/index.js.map +1 -1
  97. package/dist/seo/issue-fix-ai.d.ts.map +1 -1
  98. package/dist/seo/issue-fix-ai.js +22 -13
  99. package/dist/seo/issue-fix-ai.js.map +1 -1
  100. package/dist/seo/issue-fix-redirect.d.ts +18 -0
  101. package/dist/seo/issue-fix-redirect.d.ts.map +1 -0
  102. package/dist/seo/issue-fix-redirect.js +152 -0
  103. package/dist/seo/issue-fix-redirect.js.map +1 -0
  104. package/dist/seo/issue-fix.d.ts +4 -0
  105. package/dist/seo/issue-fix.d.ts.map +1 -1
  106. package/dist/seo/issue-fix.js +51 -29
  107. package/dist/seo/issue-fix.js.map +1 -1
  108. package/dist/seo/link-graph.d.ts +46 -0
  109. package/dist/seo/link-graph.d.ts.map +1 -0
  110. package/dist/seo/link-graph.js +141 -0
  111. package/dist/seo/link-graph.js.map +1 -0
  112. package/dist/seo/migrate-meta.js +3 -2
  113. package/dist/seo/migrate-meta.js.map +1 -1
  114. package/dist/seo/page-meta.d.ts +5 -0
  115. package/dist/seo/page-meta.d.ts.map +1 -1
  116. package/dist/seo/page-meta.js +58 -66
  117. package/dist/seo/page-meta.js.map +1 -1
  118. package/dist/seo/robots.d.ts +22 -0
  119. package/dist/seo/robots.d.ts.map +1 -1
  120. package/dist/seo/robots.js +35 -0
  121. package/dist/seo/robots.js.map +1 -1
  122. package/dist/seo/title-templates.d.ts +28 -0
  123. package/dist/seo/title-templates.d.ts.map +1 -1
  124. package/dist/seo/title-templates.js +62 -2
  125. package/dist/seo/title-templates.js.map +1 -1
  126. package/dist/site.d.ts +7 -0
  127. package/dist/site.d.ts.map +1 -1
  128. package/dist/site.js +20 -0
  129. package/dist/site.js.map +1 -1
  130. package/package.json +1 -1
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Unified 404 redirect recovery — legitimacy gate + heuristic matching.
3
+ * Used by the Redirects tab generator and the SEO audit pipeline.
4
+ */
5
+ import { normalizeRedirectPath } from './graph.js';
6
+ import { scorePathLegitimacy } from './legitimacy.js';
7
+ import { resolveRedirectMatch } from './rerank.js';
8
+ import { validateRedirectDestination } from './validate-destination.js';
9
+ function normalizeBrokenHref(href, siteUrl) {
10
+ const trimmed = href.trim();
11
+ if (!trimmed ||
12
+ trimmed.startsWith('#') ||
13
+ trimmed.startsWith('mailto:') ||
14
+ trimmed.startsWith('tel:')) {
15
+ return null;
16
+ }
17
+ if (/^https?:\/\//i.test(trimmed)) {
18
+ const base = siteUrl.replace(/\/+$/, '').toLowerCase();
19
+ if (!base || !trimmed.toLowerCase().startsWith(base))
20
+ return null;
21
+ return normalizeRedirectPath(trimmed.slice(base.length) || '/');
22
+ }
23
+ return normalizeRedirectPath(trimmed);
24
+ }
25
+ /** Merge 404 hit paths and audit broken-link hrefs into one deduped map. */
26
+ export function collectRecoveryPaths(hits, brokenLinks, siteUrl = '') {
27
+ const map = new Map();
28
+ for (const hit of hits) {
29
+ const path = normalizeRedirectPath(hit.path);
30
+ if (!path || path === '/')
31
+ continue;
32
+ const prev = map.get(path) ?? { hasInternalLink: false, hitCount: 0 };
33
+ prev.hitCount = Math.max(prev.hitCount, Number(hit.hits ?? 0));
34
+ map.set(path, prev);
35
+ }
36
+ for (const link of brokenLinks) {
37
+ const path = normalizeBrokenHref(link.url, siteUrl);
38
+ if (!path || path === '/')
39
+ continue;
40
+ const prev = map.get(path) ?? { hasInternalLink: false, hitCount: 0 };
41
+ prev.hasInternalLink = true;
42
+ map.set(path, prev);
43
+ }
44
+ return map;
45
+ }
46
+ /** Plan a single recovery redirect, or null when illegitimate / no match. */
47
+ export async function planRecoveryRedirect(fromPath, signals, options) {
48
+ const path = normalizeRedirectPath(fromPath);
49
+ if (!path || path === '/')
50
+ return null;
51
+ const legitimacy = scorePathLegitimacy(path, {
52
+ hasInternalLink: signals.hasInternalLink,
53
+ hitCount: signals.hitCount,
54
+ pathPrefixes: options.pathPrefixes,
55
+ });
56
+ if (!legitimacy.legitimate)
57
+ return null;
58
+ const match = await resolveRedirectMatch(path, options.candidates, {
59
+ minConfidence: options.minConfidence ?? 30,
60
+ enableAiRerank: options.enableAiRerank,
61
+ semanticRerank: options.semanticRerank,
62
+ });
63
+ if (!match)
64
+ return null;
65
+ return {
66
+ fromPath: path,
67
+ toPath: match.toPath,
68
+ confidence: match.confidence,
69
+ reason: match.reason,
70
+ legitimacyScore: legitimacy.score,
71
+ legitimacyReasons: legitimacy.reasons,
72
+ matchSource: match.matchSource,
73
+ };
74
+ }
75
+ /** Plan recovery redirects for all collected paths. */
76
+ export async function planRecoveryRedirects(pathMap, options) {
77
+ const plans = [];
78
+ for (const [path, signals] of pathMap) {
79
+ const plan = await planRecoveryRedirect(path, signals, options);
80
+ if (plan)
81
+ plans.push(plan);
82
+ }
83
+ return plans;
84
+ }
85
+ /** Create a 301 redirect from a recovery plan (shared by auto-apply and audit fixes). */
86
+ export async function createRecoveryRedirect(db, plan, userId) {
87
+ if (!db.redirect?.upsert)
88
+ return;
89
+ const destErr = validateRedirectDestination(plan.toPath, 301);
90
+ if (destErr) {
91
+ throw new Error(`Recovery redirect rejected: ${destErr}`);
92
+ }
93
+ await db.redirect.upsert({
94
+ where: { source: plan.fromPath },
95
+ create: {
96
+ source: plan.fromPath,
97
+ destination: plan.toPath,
98
+ statusCode: 301,
99
+ enabled: true,
100
+ source_kind: 'ai-404-recovery',
101
+ createdById: userId || undefined,
102
+ },
103
+ update: {
104
+ destination: plan.toPath,
105
+ statusCode: 301,
106
+ enabled: true,
107
+ source_kind: 'ai-404-recovery',
108
+ },
109
+ });
110
+ if (db.redirect404Hit?.updateMany) {
111
+ await db.redirect404Hit
112
+ .updateMany({
113
+ where: { path: plan.fromPath },
114
+ data: { resolvedAt: new Date() },
115
+ })
116
+ .catch(() => { });
117
+ }
118
+ }
119
+ /** Persist open redirect suggestions; skips paths that already have one. */
120
+ export async function upsertRedirectSuggestions(db, plans) {
121
+ if (!db.redirectSuggestion?.create)
122
+ return 0;
123
+ let created = 0;
124
+ for (const plan of plans) {
125
+ const existing = await db.redirectSuggestion.findFirst({
126
+ where: { fromPath: plan.fromPath, status: 'open' },
127
+ });
128
+ if (existing)
129
+ continue;
130
+ await db.redirectSuggestion.create({
131
+ data: {
132
+ fromPath: plan.fromPath,
133
+ toPath: plan.toPath,
134
+ confidence: Math.round(plan.confidence),
135
+ reason: plan.reason,
136
+ status: 'open',
137
+ source: 'ai-404-recovery',
138
+ metadata: {
139
+ legitimacyScore: plan.legitimacyScore,
140
+ legitimacyReasons: plan.legitimacyReasons,
141
+ ...(plan.matchSource ? { matchSource: plan.matchSource } : {}),
142
+ },
143
+ },
144
+ });
145
+ created++;
146
+ }
147
+ return created;
148
+ }
149
+ /** Full pipeline: collect paths, plan matches, persist suggestions. */
150
+ export async function generateRecoverySuggestions(db, options) {
151
+ const hits = options.include404Hits !== false && db.redirect404Hit?.findMany
152
+ ? await db.redirect404Hit.findMany({ where: { resolvedAt: null }, take: 5000 })
153
+ : [];
154
+ const pathMap = collectRecoveryPaths(hits, options.brokenLinks ?? [], options.siteUrl ?? '');
155
+ const plans = await planRecoveryRedirects(pathMap, {
156
+ candidates: options.candidates,
157
+ pathPrefixes: options.pathPrefixes,
158
+ enableAiRerank: options.enableAiRerank,
159
+ semanticRerank: options.semanticRerank,
160
+ });
161
+ const autoMin = options.autoApplyMinConfidence ?? 85;
162
+ let created = 0;
163
+ let autoApplied = 0;
164
+ const pending = [];
165
+ for (const plan of plans) {
166
+ if (options.autoApply && plan.confidence >= autoMin) {
167
+ await createRecoveryRedirect(db, plan, options.autoApplyUserId);
168
+ autoApplied++;
169
+ continue;
170
+ }
171
+ pending.push(plan);
172
+ }
173
+ created = await upsertRedirectSuggestions(db, pending);
174
+ return { created, planned: plans.length, autoApplied };
175
+ }
176
+ //# sourceMappingURL=recovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery.js","sourceRoot":"","sources":["../../src/redirects/recovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * AI semantic re-ranking for ambiguous redirect matches (40–84% heuristic).
3
+ * Constrained to published candidate URLs only — the model never invents paths.
4
+ */
5
+ import { type RedirectSuggestionResult, type SuggestCandidate } from './suggest.js';
6
+ export type RedirectMatchSource = 'heuristic' | 'semantic';
7
+ export type RedirectMatchResult = RedirectSuggestionResult & {
8
+ matchSource: RedirectMatchSource;
9
+ };
10
+ export interface ResolveRedirectMatchOptions {
11
+ minConfidence?: number;
12
+ /** Run semantic re-rank for heuristic scores in [ambiguousLow, ambiguousHigh]. */
13
+ enableAiRerank?: boolean;
14
+ ambiguousLow?: number;
15
+ ambiguousHigh?: number;
16
+ /** Injected semantic ranker (for tests). When omitted, loads plugin-ai dynamically. */
17
+ semanticRerank?: SemanticRerankFn | null;
18
+ }
19
+ export type SemanticRerankFn = (fromPath: string, shortlist: SuggestCandidate[]) => Promise<RedirectSuggestionResult | null>;
20
+ /**
21
+ * Resolve the best redirect target — heuristic first, semantic re-rank when ambiguous.
22
+ */
23
+ export declare function resolveRedirectMatch(fromPath: string, candidates: SuggestCandidate[], options?: ResolveRedirectMatchOptions): Promise<RedirectMatchResult | null>;
24
+ //# sourceMappingURL=rerank.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rerank.d.ts","sourceRoot":"","sources":["../../src/redirects/rerank.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGL,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAA;AAErB,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,UAAU,CAAA;AAE1D,MAAM,MAAM,mBAAmB,GAAG,wBAAwB,GAAG;IAC3D,WAAW,EAAE,mBAAmB,CAAA;CACjC,CAAA;AAED,MAAM,WAAW,2BAA2B;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,kFAAkF;IAClF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uFAAuF;IACvF,cAAc,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;CACzC;AAED,MAAM,MAAM,gBAAgB,GAAG,CAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,gBAAgB,EAAE,KAC1B,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC,CAAA;AA6D7C;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,gBAAgB,EAAE,EAC9B,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAuCrC"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * AI semantic re-ranking for ambiguous redirect matches (40–84% heuristic).
3
+ * Constrained to published candidate URLs only — the model never invents paths.
4
+ */
5
+ import { rankRedirectCandidates, suggestRedirectTarget, } from './suggest.js';
6
+ const CLEAR_MATCH = 85;
7
+ async function loadSemanticRerank() {
8
+ try {
9
+ const mod = '@actuate-media/' + 'plugin-ai';
10
+ const ai = (await import(mod));
11
+ if (typeof ai.semanticSearch !== 'function')
12
+ return null;
13
+ return async (fromPath, shortlist) => {
14
+ if (shortlist.length === 0)
15
+ return null;
16
+ const query = `Best replacement page for broken URL path: ${fromPath}`;
17
+ const corpus = shortlist.map((c) => ({
18
+ id: c.url,
19
+ collectionSlug: 'content',
20
+ title: c.title ?? c.url,
21
+ content: `${c.title ?? ''} ${c.url}`.trim(),
22
+ }));
23
+ const results = await ai.semanticSearch(query, {
24
+ corpus,
25
+ limit: 3,
26
+ threshold: 0.42,
27
+ });
28
+ const top = results[0];
29
+ if (!top)
30
+ return null;
31
+ const matched = shortlist.find((c) => c.url === top.id);
32
+ if (!matched)
33
+ return null;
34
+ const confidence = Math.round(Math.min(1, Math.max(0, top.score)) * 100);
35
+ return {
36
+ toPath: matched.url,
37
+ confidence,
38
+ reason: `Semantic match to "${matched.title ?? matched.url}"`,
39
+ };
40
+ };
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ function withSource(match, matchSource) {
47
+ return { ...match, matchSource };
48
+ }
49
+ /**
50
+ * Resolve the best redirect target — heuristic first, semantic re-rank when ambiguous.
51
+ */
52
+ export async function resolveRedirectMatch(fromPath, candidates, options = {}) {
53
+ const minConfidence = options.minConfidence ?? 30;
54
+ const ambiguousLow = options.ambiguousLow ?? 40;
55
+ const ambiguousHigh = options.ambiguousHigh ?? 84;
56
+ const heuristic = suggestRedirectTarget(fromPath, candidates, minConfidence);
57
+ const needsRerank = options.enableAiRerank !== false &&
58
+ (!heuristic || (heuristic.confidence >= ambiguousLow && heuristic.confidence <= ambiguousHigh));
59
+ if (!needsRerank) {
60
+ return heuristic ? withSource(heuristic, 'heuristic') : null;
61
+ }
62
+ if (heuristic && heuristic.confidence >= CLEAR_MATCH) {
63
+ return withSource(heuristic, 'heuristic');
64
+ }
65
+ const rerank = options.semanticRerank ?? (await loadSemanticRerank());
66
+ if (!rerank) {
67
+ return heuristic ? withSource(heuristic, 'heuristic') : null;
68
+ }
69
+ const ranked = rankRedirectCandidates(fromPath, candidates, 8);
70
+ const shortlist = ranked.map((r) => {
71
+ const c = candidates.find((x) => x.url === r.toPath);
72
+ return { url: r.toPath, title: c?.title };
73
+ });
74
+ const semantic = await rerank(fromPath, shortlist).catch(() => null);
75
+ if (semantic && semantic.confidence >= minConfidence) {
76
+ if (!heuristic || semantic.confidence >= heuristic.confidence) {
77
+ return withSource(semantic, 'semantic');
78
+ }
79
+ }
80
+ return heuristic ? withSource(heuristic, 'heuristic') : null;
81
+ }
82
+ //# sourceMappingURL=rerank.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rerank.js","sourceRoot":"","sources":["../../src/redirects/rerank.ts"],"names":[],"mappings":"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"}
@@ -18,4 +18,6 @@ export interface RedirectSuggestionResult {
18
18
  * candidate clears the minimum confidence threshold (default 30).
19
19
  */
20
20
  export declare function suggestRedirectTarget(fromPath: string, candidates: SuggestCandidate[], minConfidence?: number): RedirectSuggestionResult | null;
21
+ /** Rank all candidates by path/title similarity (for AI re-ranking shortlists). */
22
+ export declare function rankRedirectCandidates(fromPath: string, candidates: SuggestCandidate[], limit?: number): RedirectSuggestionResult[];
21
23
  //# sourceMappingURL=suggest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"suggest.d.ts","sourceRoot":"","sources":["../../src/redirects/suggest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;CACf;AAmBD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,gBAAgB,EAAE,EAC9B,aAAa,SAAK,GACjB,wBAAwB,GAAG,IAAI,CA0BjC"}
1
+ {"version":3,"file":"suggest.d.ts","sourceRoot":"","sources":["../../src/redirects/suggest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;CACf;AAmBD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,gBAAgB,EAAE,EAC9B,aAAa,SAAK,GACjB,wBAAwB,GAAG,IAAI,CA0BjC;AAED,mFAAmF;AACnF,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,gBAAgB,EAAE,EAC9B,KAAK,SAAI,GACR,wBAAwB,EAAE,CAwB5B"}
@@ -52,4 +52,29 @@ export function suggestRedirectTarget(fromPath, candidates, minConfidence = 30)
52
52
  return null;
53
53
  return best;
54
54
  }
55
+ /** Rank all candidates by path/title similarity (for AI re-ranking shortlists). */
56
+ export function rankRedirectCandidates(fromPath, candidates, limit = 5) {
57
+ const fromTokens = new Set(tokens(fromPath));
58
+ if (fromTokens.size === 0 || candidates.length === 0)
59
+ return [];
60
+ const scored = [];
61
+ for (const c of candidates) {
62
+ const pathTokens = new Set(tokens(c.url));
63
+ const titleTokens = new Set(tokens(c.title ?? ''));
64
+ const pathSim = jaccard(fromTokens, pathTokens);
65
+ const titleSim = jaccard(fromTokens, titleTokens);
66
+ const confidence = Math.round((pathSim * 0.7 + titleSim * 0.3) * 100);
67
+ if (confidence <= 0)
68
+ continue;
69
+ scored.push({
70
+ toPath: c.url,
71
+ confidence,
72
+ reason: pathSim >= titleSim
73
+ ? `URL path similarity to ${c.url}`
74
+ : `Title similarity to "${c.title ?? c.url}"`,
75
+ });
76
+ }
77
+ scored.sort((a, b) => b.confidence - a.confidence);
78
+ return scored.slice(0, limit);
79
+ }
55
80
  //# sourceMappingURL=suggest.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"suggest.js","sourceRoot":"","sources":["../../src/redirects/suggest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAaH,SAAS,MAAM,CAAC,KAAa;IAC3B,OAAO,KAAK;SACT,WAAW,EAAE;SACb,OAAO,CAAC,oBAAoB,EAAE,EAAE,CAAC;SACjC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AAChC,CAAC;AAED,SAAS,OAAO,CAAC,CAAc,EAAE,CAAc;IAC7C,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IAC1C,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,MAAM,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,KAAK,EAAE,CAAA;IACxC,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,GAAG,KAAK,CAAA;IACrC,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,KAAK,CAAA;AACxC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAAgB,EAChB,UAA8B,EAC9B,aAAa,GAAG,EAAE;IAElB,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC5C,IAAI,UAAU,CAAC,IAAI,KAAK,CAAC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAEjE,IAAI,IAAI,GAAoC,IAAI,CAAA;IAChD,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QACzC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAA;QAClD,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,UAAU,CAAC,CAAA;QAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC,CAAA;QACjD,+CAA+C;QAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,GAAG,GAAG,QAAQ,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAA;QAChE,IAAI,CAAC,IAAI,IAAI,KAAK,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YACrC,IAAI,GAAG;gBACL,MAAM,EAAE,CAAC,CAAC,GAAG;gBACb,UAAU,EAAE,KAAK;gBACjB,MAAM,EACJ,OAAO,IAAI,QAAQ;oBACjB,CAAC,CAAC,0BAA0B,CAAC,CAAC,GAAG,EAAE;oBACnC,CAAC,CAAC,wBAAwB,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,GAAG;aAClD,CAAA;QACH,CAAC;IACH,CAAC;IAED,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,GAAG,aAAa;QAAE,OAAO,IAAI,CAAA;IACzD,OAAO,IAAI,CAAA;AACb,CAAC"}
1
+ {"version":3,"file":"suggest.js","sourceRoot":"","sources":["../../src/redirects/suggest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Detect editor typos vs slug migrations — typos should fix the source link,
3
+ * migrations should get a redirect.
4
+ */
5
+ /**
6
+ * True when paths differ by a small edit — likely a typo in content, not a
7
+ * retired URL that needs a site-wide 301.
8
+ */
9
+ export declare function isLikelyEditorTypo(fromPath: string, toPath: string): boolean;
10
+ //# sourceMappingURL=typo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typo.d.ts","sourceRoot":"","sources":["../../src/redirects/typo.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAyBH;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAmB5E"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Detect editor typos vs slug migrations — typos should fix the source link,
3
+ * migrations should get a redirect.
4
+ */
5
+ function levenshtein(a, b) {
6
+ if (a === b)
7
+ return 0;
8
+ if (a.length === 0)
9
+ return b.length;
10
+ if (b.length === 0)
11
+ return a.length;
12
+ const row = new Array(b.length + 1);
13
+ for (let j = 0; j <= b.length; j++)
14
+ row[j] = j;
15
+ for (let i = 1; i <= a.length; i++) {
16
+ let prev = row[0];
17
+ row[0] = i;
18
+ for (let j = 1; j <= b.length; j++) {
19
+ const tmp = row[j];
20
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
21
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + cost);
22
+ prev = tmp;
23
+ }
24
+ }
25
+ return row[b.length];
26
+ }
27
+ function segments(path) {
28
+ return path.split('/').filter(Boolean);
29
+ }
30
+ /**
31
+ * True when paths differ by a small edit — likely a typo in content, not a
32
+ * retired URL that needs a site-wide 301.
33
+ */
34
+ export function isLikelyEditorTypo(fromPath, toPath) {
35
+ if (!fromPath || !toPath || fromPath === toPath)
36
+ return false;
37
+ const fromSegs = segments(fromPath);
38
+ const toSegs = segments(toPath);
39
+ if (fromSegs.length === toSegs.length && fromSegs.length > 0) {
40
+ const samePrefix = fromSegs.slice(0, -1).join('/') === toSegs.slice(0, -1).join('/') || fromSegs.length === 1;
41
+ if (samePrefix) {
42
+ const lastFrom = fromSegs.at(-1) ?? '';
43
+ const lastTo = toSegs.at(-1) ?? '';
44
+ const dist = levenshtein(lastFrom, lastTo);
45
+ return dist > 0 && dist <= 3;
46
+ }
47
+ }
48
+ const fullDist = levenshtein(fromPath.replace(/^\//, ''), toPath.replace(/^\//, ''));
49
+ return fullDist > 0 && fullDist <= 4;
50
+ }
51
+ //# sourceMappingURL=typo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typo.js","sourceRoot":"","sources":["../../src/redirects/typo.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,SAAS,WAAW,CAAC,CAAS,EAAE,CAAS;IACvC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IACrB,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC,MAAM,CAAA;IACnC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC,MAAM,CAAA;IACnC,MAAM,GAAG,GAAG,IAAI,KAAK,CAAS,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IAC9C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,IAAI,IAAI,GAAG,GAAG,CAAC,CAAC,CAAE,CAAA;QAClB,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACnC,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAE,CAAA;YACnB,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YAC1C,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAE,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC,CAAA;YAC5D,IAAI,GAAG,GAAG,CAAA;QACZ,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC,CAAC,CAAC,MAAM,CAAE,CAAA;AACvB,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;AACxC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,MAAc;IACjE,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM,IAAI,QAAQ,KAAK,MAAM;QAAE,OAAO,KAAK,CAAA;IAE7D,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAA;IACnC,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAA;IAE/B,IAAI,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7D,MAAM,UAAU,GACd,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAA;QAC5F,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,QAAQ,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;YACtC,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;YAClC,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;YAC1C,OAAO,IAAI,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAA;QAC9B,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAA;IACpF,OAAO,QAAQ,GAAG,CAAC,IAAI,QAAQ,IAAI,CAAC,CAAA;AACtC,CAAC"}
@@ -0,0 +1,13 @@
1
+ export interface RedirectDestinationValidationOptions {
2
+ /** When set, used instead of `NEXT_PUBLIC_SITE_URL` for allowlist checks. */
3
+ siteUrl?: string | null;
4
+ }
5
+ /**
6
+ * Open-redirect defence for redirect destinations.
7
+ *
8
+ * - Relative paths must start with `/` but NOT `//` (protocol-relative URLs).
9
+ * - Absolute URLs must use http/https and point at an allowlisted host.
10
+ * - 410 Gone responses skip destination validation (destination is informational).
11
+ */
12
+ export declare function validateRedirectDestination(destination: string, statusCode: number, options?: RedirectDestinationValidationOptions): string | null;
13
+ //# sourceMappingURL=validate-destination.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-destination.d.ts","sourceRoot":"","sources":["../../src/redirects/validate-destination.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oCAAoC;IACnD,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,oCAAoC,GAC7C,MAAM,GAAG,IAAI,CA4Cf"}
@@ -0,0 +1,51 @@
1
+ import { getActuateConfig } from '../config/runtime.js';
2
+ /**
3
+ * Open-redirect defence for redirect destinations.
4
+ *
5
+ * - Relative paths must start with `/` but NOT `//` (protocol-relative URLs).
6
+ * - Absolute URLs must use http/https and point at an allowlisted host.
7
+ * - 410 Gone responses skip destination validation (destination is informational).
8
+ */
9
+ export function validateRedirectDestination(destination, statusCode, options) {
10
+ if (statusCode === 410)
11
+ return null;
12
+ const trimmed = destination.trim();
13
+ if (!trimmed)
14
+ return 'Destination is required';
15
+ if (trimmed.startsWith('//')) {
16
+ return 'Protocol-relative destinations (//…) are not allowed — use a path starting with / or an absolute https URL';
17
+ }
18
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
19
+ let destUrl;
20
+ try {
21
+ destUrl = new URL(trimmed);
22
+ }
23
+ catch {
24
+ return 'Invalid destination URL';
25
+ }
26
+ if (!['http:', 'https:'].includes(destUrl.protocol))
27
+ return 'Invalid destination URL';
28
+ const cmsConfig = getActuateConfig();
29
+ const allowed = new Set(Array.isArray(cmsConfig?.redirects?.allowedExternalHosts)
30
+ ? cmsConfig.redirects.allowedExternalHosts.map((h) => h.toLowerCase())
31
+ : []);
32
+ const siteUrl = options?.siteUrl ?? process.env.NEXT_PUBLIC_SITE_URL;
33
+ if (siteUrl) {
34
+ try {
35
+ allowed.add(new URL(siteUrl).hostname.toLowerCase());
36
+ }
37
+ catch {
38
+ /* noop */
39
+ }
40
+ }
41
+ if (!allowed.has(destUrl.hostname.toLowerCase())) {
42
+ return 'External redirect destinations must be to an allowlisted host.';
43
+ }
44
+ return null;
45
+ }
46
+ if (!trimmed.startsWith('/')) {
47
+ return 'Destination must be an absolute URL or a path beginning with /';
48
+ }
49
+ return null;
50
+ }
51
+ //# sourceMappingURL=validate-destination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-destination.js","sourceRoot":"","sources":["../../src/redirects/validate-destination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAOvD;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B,CACzC,WAAmB,EACnB,UAAkB,EAClB,OAA8C;IAE9C,IAAI,UAAU,KAAK,GAAG;QAAE,OAAO,IAAI,CAAA;IAEnC,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,EAAE,CAAA;IAClC,IAAI,CAAC,OAAO;QAAE,OAAO,yBAAyB,CAAA;IAE9C,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,OAAO,4GAA4G,CAAA;IACrH,CAAC;IAED,IAAI,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACpE,IAAI,OAAY,CAAA;QAChB,IAAI,CAAC;YACH,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAA;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,yBAAyB,CAAA;QAClC,CAAC;QACD,IAAI,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC;YAAE,OAAO,yBAAyB,CAAA;QAErF,MAAM,SAAS,GAAG,gBAAgB,EAAE,CAAA;QACpC,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,SAAS,EAAE,oBAAoB,CAAC;YACvD,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;YAC9E,CAAC,CAAC,EAAE,CACP,CAAA;QACD,MAAM,OAAO,GAAG,OAAO,EAAE,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAA;QACpE,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC;gBACH,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAA;YACtD,CAAC;YAAC,MAAM,CAAC;gBACP,UAAU;YACZ,CAAC;QACH,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACjD,OAAO,gEAAgE,CAAA;QACzE,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7B,OAAO,gEAAgE,CAAA;IACzE,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -24,14 +24,15 @@ export declare function applyAsyncToScriptCode(code: string): string;
24
24
  /**
25
25
  * Wrap a snippet in a consent gate keyed to `@actuate-media/plugin-consent`
26
26
  * storage (`am_cookie_consent`) and its `am-consent` event. The original
27
- * snippet is injected only after the visitor grants full consent.
27
+ * snippet is injected only once the tag's category is granted.
28
28
  *
29
- * Interim category model: the consent plugin currently stores a binary level
30
- * (`all` | `essential`), so every non-`none` category requires `all`. The
31
- * category is preserved as a `data-consent-category` marker for CMPs and for
32
- * future granular gating.
29
+ * Grant rules (matching the plugin's storage contract):
30
+ * - stored `'all'` / event level `'all'` every category granted
31
+ * - stored/event `categories` array containing this tag's category (the
32
+ * plugin persists custom selections as `{"level":"custom","categories":[…]}`)
33
+ * - `'essential'` or unset — blocked
33
34
  */
34
- export declare function wrapCodeInConsentGate(code: string, category: string): string;
35
+ export declare function wrapCodeInConsentGate(code: string, category: ScriptTagConsentCategory): string;
35
36
  /**
36
37
  * Resolve enabled script tags for a public URL path, grouped by placement.
37
38
  * Shared by the public API route and Next.js site layouts.
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/script-tags/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAEtD,eAAO,MAAM,qBAAqB,wBAAwB,CAAA;AAE1D,eAAO,MAAM,6BAA6B,0EAMhC,CAAA;AAEV,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,6BAA6B,CAAC,CAAC,MAAM,CAAC,CAAA;AAErF,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAID,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGhE;AAMD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO3D;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAoB5E;AAcD;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,SAAS,EACb,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,kBAAkB,CAAC,CAkD7B"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/script-tags/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAEtD,eAAO,MAAM,qBAAqB,wBAAwB,CAAA;AAE1D,eAAO,MAAM,6BAA6B,0EAMhC,CAAA;AAEV,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,6BAA6B,CAAC,CAAC,MAAM,CAAC,CAAA;AAErF,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAID,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGhE;AAMD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO3D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,wBAAwB,GAAG,MAAM,CA2B9F;AAqBD;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,SAAS,EACb,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,kBAAkB,CAAC,CAkD7B"}
@@ -32,12 +32,13 @@ export function applyAsyncToScriptCode(code) {
32
32
  /**
33
33
  * Wrap a snippet in a consent gate keyed to `@actuate-media/plugin-consent`
34
34
  * storage (`am_cookie_consent`) and its `am-consent` event. The original
35
- * snippet is injected only after the visitor grants full consent.
35
+ * snippet is injected only once the tag's category is granted.
36
36
  *
37
- * Interim category model: the consent plugin currently stores a binary level
38
- * (`all` | `essential`), so every non-`none` category requires `all`. The
39
- * category is preserved as a `data-consent-category` marker for CMPs and for
40
- * future granular gating.
37
+ * Grant rules (matching the plugin's storage contract):
38
+ * - stored `'all'` / event level `'all'` every category granted
39
+ * - stored/event `categories` array containing this tag's category (the
40
+ * plugin persists custom selections as `{"level":"custom","categories":[…]}`)
41
+ * - `'essential'` or unset — blocked
41
42
  */
42
43
  export function wrapCodeInConsentGate(code, category) {
43
44
  // \u003C-escape so the payload can never terminate the wrapper script early.
@@ -45,16 +46,23 @@ export function wrapCodeInConsentGate(code, category) {
45
46
  return (`<script data-consent-category="${category}">` +
46
47
  '(function(){' +
47
48
  `var p=${payload};` +
49
+ `var c="${category}";` +
48
50
  // currentScript is only defined during synchronous execution — capture it
49
51
  // now so deferred injection (after the am-consent event) keeps placement.
50
52
  'var s=document.currentScript;' +
51
- 'function g(){try{return window.localStorage.getItem("am_cookie_consent")==="all"}catch(e){return false}}' +
53
+ 'function ok(l,a){if(l==="all")return true;return!!a&&a.indexOf(c)>-1}' +
54
+ 'function g(){try{var r=window.localStorage.getItem("am_cookie_consent");' +
55
+ 'if(!r)return false;' +
56
+ 'if(r==="all")return true;' +
57
+ 'if(r.charAt(0)!=="{")return false;' +
58
+ 'var j=JSON.parse(r);return ok(j&&j.level,j&&j.categories)' +
59
+ '}catch(e){return false}}' +
52
60
  'var d=false;' +
53
61
  'function i(){if(d)return;d=true;' +
54
62
  'var f=document.createRange().createContextualFragment(p);' +
55
63
  'if(s&&s.parentNode){s.parentNode.insertBefore(f,s.nextSibling)}else{document.body.appendChild(f)}}' +
56
64
  'if(g()){i();return}' +
57
- 'window.addEventListener("am-consent",function(e){if((e.detail&&e.detail.level)==="all")i()});' +
65
+ 'window.addEventListener("am-consent",function(e){var t=e.detail||{};if(ok(t.level,t.categories))i()});' +
58
66
  '})()' +
59
67
  '</script>');
60
68
  }
@@ -63,8 +71,12 @@ function deliverableCode(tag) {
63
71
  if (tag.loadAsync)
64
72
  code = applyAsyncToScriptCode(code);
65
73
  const category = tag.consentCategory ?? 'none';
66
- if (category !== 'none')
74
+ // Categories are validated at write time; narrow defensively so an
75
+ // unexpected DB value ships ungated rather than with a broken gate.
76
+ if (category !== 'none' &&
77
+ SCRIPT_TAG_CONSENT_CATEGORIES.includes(category)) {
67
78
  code = wrapCodeInConsentGate(code, category);
79
+ }
68
80
  return code;
69
81
  }
70
82
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/script-tags/resolve.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/script-tags/resolve.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,6 @@
1
1
  import type { SectionSettings, SectionValidationResult } from './types.js';
2
+ /** Heuristic: URLs that look like static /public assets rather than library uploads. */
3
+ export declare function isLikelyOutsideMediaLibrary(url: string): boolean;
2
4
  /**
3
5
  * Validate one section's content + settings against its type schema.
4
6
  *