@jini-ai/cms 0.5.2 → 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.
- package/CHANGELOG.md +11 -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/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 -11
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { compareLongestFirst, compareSamePatternExactFirst, compareTieBreak } from "../order.js";
|
|
2
|
+
import { toRecord, toRevision, toRevisionRow, toRow, updatableColumns } from "./repo.rows.js";
|
|
3
|
+
import { RedirectNotFoundError } from "../types.js";
|
|
4
|
+
export class SqlRedirectRepo {
|
|
5
|
+
kernel;
|
|
6
|
+
tables;
|
|
7
|
+
constructor({ kernel, tables }, _optional = {}) {
|
|
8
|
+
this.kernel = kernel;
|
|
9
|
+
this.tables = tables;
|
|
10
|
+
}
|
|
11
|
+
/** Every row of a workspace, optionally narrowed to active rows of one match type (and override-only). */
|
|
12
|
+
async rowsOf(workspaceId, narrow = {}) {
|
|
13
|
+
const rows = await this.kernel.run((db) => {
|
|
14
|
+
// Host-owned table names need an explicit column schema, as in the comments adapter.
|
|
15
|
+
let query = db.withTables().selectFrom(this.tables.redirects).selectAll().where("workspace_id", "=", workspaceId);
|
|
16
|
+
if (narrow.activeOnly)
|
|
17
|
+
query = query.where("status", "=", "active");
|
|
18
|
+
if (narrow.matchType !== undefined)
|
|
19
|
+
query = query.where("match_type", "=", narrow.matchType);
|
|
20
|
+
if (narrow.overrideOnly)
|
|
21
|
+
query = query.where("override", "=", 1);
|
|
22
|
+
return query.execute();
|
|
23
|
+
});
|
|
24
|
+
return rows.map((row) => toRecord({ row }, {}));
|
|
25
|
+
}
|
|
26
|
+
async findById(required, _optional = {}) {
|
|
27
|
+
const row = await this.kernel.run((db) => db
|
|
28
|
+
.withTables()
|
|
29
|
+
.selectFrom(this.tables.redirects)
|
|
30
|
+
.selectAll()
|
|
31
|
+
.where("workspace_id", "=", required.workspaceId)
|
|
32
|
+
.where("id", "=", required.id)
|
|
33
|
+
.limit(1)
|
|
34
|
+
.executeTakeFirst());
|
|
35
|
+
return row ? toRecord({ row }, {}) : null;
|
|
36
|
+
}
|
|
37
|
+
async lookupExact(required, _optional = {}) {
|
|
38
|
+
const candidates = (await this.rowsOf(required.workspaceId, {
|
|
39
|
+
activeOnly: true,
|
|
40
|
+
matchType: "exact",
|
|
41
|
+
overrideOnly: required.includeOverrideOnly,
|
|
42
|
+
})).filter((r) => r.fromPattern === required.path);
|
|
43
|
+
candidates.sort((a, b) => compareTieBreak({ a, b }, {}));
|
|
44
|
+
return candidates[0] ?? null;
|
|
45
|
+
}
|
|
46
|
+
async lookupLongestPrefix(required, _optional = {}) {
|
|
47
|
+
const candidates = (await this.rowsOf(required.workspaceId, {
|
|
48
|
+
activeOnly: true,
|
|
49
|
+
matchType: "prefix",
|
|
50
|
+
overrideOnly: required.includeOverrideOnly,
|
|
51
|
+
})).filter((r) => {
|
|
52
|
+
const pattern = r.fromPattern;
|
|
53
|
+
if (required.path === pattern)
|
|
54
|
+
return true;
|
|
55
|
+
return required.path.startsWith(pattern.endsWith("/") ? pattern : `${pattern}/`);
|
|
56
|
+
});
|
|
57
|
+
candidates.sort((a, b) => compareLongestFirst({ a, b }, {}));
|
|
58
|
+
return candidates[0] ?? null;
|
|
59
|
+
}
|
|
60
|
+
async listDynamic(required, _optional = {}) {
|
|
61
|
+
const candidates = await this.rowsOf(required.workspaceId, {
|
|
62
|
+
activeOnly: true,
|
|
63
|
+
matchType: "wildcard",
|
|
64
|
+
overrideOnly: required.includeOverrideOnly,
|
|
65
|
+
});
|
|
66
|
+
candidates.sort((a, b) => compareLongestFirst({ a, b }, {}));
|
|
67
|
+
return candidates.slice(0, required.limit);
|
|
68
|
+
}
|
|
69
|
+
async list(filter, _optional = {}) {
|
|
70
|
+
return (await this.rowsOf(filter.workspaceId))
|
|
71
|
+
.filter((r) => filter.status === undefined || r.status === filter.status)
|
|
72
|
+
.filter((r) => filter.source === undefined || r.source === filter.source)
|
|
73
|
+
.filter((r) => filter.matchType === undefined || r.matchType === filter.matchType);
|
|
74
|
+
}
|
|
75
|
+
async findByFromPattern(required, _optional = {}) {
|
|
76
|
+
const candidates = (await this.rowsOf(required.workspaceId, { activeOnly: true })).filter((r) => r.fromPattern === required.fromPattern);
|
|
77
|
+
if (candidates.length === 0)
|
|
78
|
+
return null;
|
|
79
|
+
candidates.sort((a, b) => compareSamePatternExactFirst({ a, b }, {}));
|
|
80
|
+
return candidates[0];
|
|
81
|
+
}
|
|
82
|
+
async save(required, _optional = {}) {
|
|
83
|
+
await this.kernel.transaction(async () => {
|
|
84
|
+
await this.kernel.lockKey(`redirects:${required.record.workspaceId}`);
|
|
85
|
+
await this.insertRedirect(required.record);
|
|
86
|
+
await this.insertRevision(required.revision);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async tombstone(required, _optional = {}) {
|
|
90
|
+
await this.kernel.transaction(async () => {
|
|
91
|
+
await this.kernel.lockKey(`redirects:${required.workspaceId}`);
|
|
92
|
+
const existing = await this.findById({ workspaceId: required.workspaceId, id: required.id });
|
|
93
|
+
if (!existing)
|
|
94
|
+
throw new RedirectNotFoundError(`redirect '${required.id}' was not found`);
|
|
95
|
+
await this.insertRedirect(required.revision.state);
|
|
96
|
+
await this.insertRevision(required.revision);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// -------------------------------------------------------------------
|
|
100
|
+
// RedirectDbHandle (used by redirects.ts/capture.ts via ports.internal.ts)
|
|
101
|
+
// -------------------------------------------------------------------
|
|
102
|
+
/** Upsert-by-id; no transaction control of its own. */
|
|
103
|
+
async insertRedirect(record, _optional = {}) {
|
|
104
|
+
const row = toRow({ record }, {});
|
|
105
|
+
await this.kernel.run((db) => db
|
|
106
|
+
.withTables()
|
|
107
|
+
.insertInto(this.tables.redirects)
|
|
108
|
+
.values(row)
|
|
109
|
+
.onConflict((oc) => oc.column("id").doUpdateSet(updatableColumns({ row }, {})))
|
|
110
|
+
.execute());
|
|
111
|
+
}
|
|
112
|
+
/** Append-only insert; no transaction control of its own. */
|
|
113
|
+
async insertRevision(revision, _optional = {}) {
|
|
114
|
+
await this.kernel.run((db) => db.withTables().insertInto(this.tables.revisions).values(toRevisionRow({ revision }, {})).execute());
|
|
115
|
+
}
|
|
116
|
+
/** Test-only helper: the append-only revision ledger for one redirect, in seq order. */
|
|
117
|
+
async listRevisionsForTests({ redirectId }, _optional = {}) {
|
|
118
|
+
const rows = await this.kernel.run((db) => db
|
|
119
|
+
.withTables()
|
|
120
|
+
.selectFrom(this.tables.revisions)
|
|
121
|
+
.selectAll()
|
|
122
|
+
.where("redirect_id", "=", redirectId)
|
|
123
|
+
.orderBy("seq", "asc")
|
|
124
|
+
.execute());
|
|
125
|
+
return rows.map((row) => toRevision({ row }, {}));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The chokepoint's transaction (C-001..004). Takes the redirect write lock first so its
|
|
129
|
+
* uniqueness / chain-loop reads and the write that follows are one serialized unit; nested calls
|
|
130
|
+
* (including `save`/`tombstone`) join it. `capture.ts` never calls this (Decision A).
|
|
131
|
+
*/
|
|
132
|
+
async transaction(fn, _optional = {}) {
|
|
133
|
+
return this.kernel.transaction(async () => {
|
|
134
|
+
await this.kernel.lockKey("redirects:write");
|
|
135
|
+
return fn();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** The redirect repo for `kernel`. */
|
|
140
|
+
export function redirectRepoFor(required, _optional = {}) {
|
|
141
|
+
return new SqlRedirectRepo(required, {});
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=repo.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Insertable, Selectable } from "kysely";
|
|
2
|
+
import type { RedirectsTable, RedirectRevisionsTable } from "./row-types.js";
|
|
3
|
+
import type { RedirectRecord, RedirectRevision } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* @file Row mapping for `redirects` / `redirect_revisions`, shared by every dialect: the columns are
|
|
6
|
+
* the generated `ContentDatabase` types (snake_case, JSON as text, booleans as 0/1 integers). Neutral
|
|
7
|
+
* on purpose — no repo, no driver.
|
|
8
|
+
*/
|
|
9
|
+
export type RedirectRow = Selectable<RedirectsTable>;
|
|
10
|
+
export type RedirectRevisionRow = Selectable<RedirectRevisionsTable>;
|
|
11
|
+
/** One `redirects` row as a {@link RedirectRecord}. */
|
|
12
|
+
export declare function toRecord({ row }: {
|
|
13
|
+
row: RedirectRow;
|
|
14
|
+
}, _optional?: Record<string, never>): RedirectRecord;
|
|
15
|
+
/** `toRecord`'s inverse: the exact column values one whole-row upsert persists. */
|
|
16
|
+
export declare function toRow({ record }: {
|
|
17
|
+
record: RedirectRecord;
|
|
18
|
+
}, _optional?: Record<string, never>): Insertable<RedirectsTable>;
|
|
19
|
+
/** The columns an existing row's upsert replaces — everything but the match key `id`. */
|
|
20
|
+
export declare function updatableColumns({ row }: {
|
|
21
|
+
row: ReturnType<typeof toRow>;
|
|
22
|
+
}, _optional?: Record<string, never>): {
|
|
23
|
+
source: string;
|
|
24
|
+
status: string;
|
|
25
|
+
version: number;
|
|
26
|
+
workspace_id: string;
|
|
27
|
+
created_at: string;
|
|
28
|
+
updated_at: string;
|
|
29
|
+
override: number;
|
|
30
|
+
priority: number;
|
|
31
|
+
match_type: string;
|
|
32
|
+
from_pattern: string;
|
|
33
|
+
to_target: string;
|
|
34
|
+
status_code: number;
|
|
35
|
+
created_by_principal: string;
|
|
36
|
+
source_entry_id?: string | null;
|
|
37
|
+
from_path_at_capture?: string | null;
|
|
38
|
+
to_path_at_capture?: string | null;
|
|
39
|
+
created_by_plugin_id?: string | null;
|
|
40
|
+
};
|
|
41
|
+
/** The `redirect_revisions` row an append writes (compact JSON state; `id` is database-assigned). */
|
|
42
|
+
export declare function toRevisionRow({ revision }: {
|
|
43
|
+
revision: RedirectRevision;
|
|
44
|
+
}, _optional?: Record<string, never>): Insertable<RedirectRevisionsTable>;
|
|
45
|
+
/** One `redirect_revisions` row as a {@link RedirectRevision}. */
|
|
46
|
+
export declare function toRevision({ row }: {
|
|
47
|
+
row: RedirectRevisionRow;
|
|
48
|
+
}, _optional?: Record<string, never>): RedirectRevision;
|
|
49
|
+
//# sourceMappingURL=repo.rows.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { toBool } from "@jini-ai/db/kernel";
|
|
2
|
+
/** One `redirects` row as a {@link RedirectRecord}. */
|
|
3
|
+
export function toRecord({ row }, _optional = {}) {
|
|
4
|
+
return {
|
|
5
|
+
id: row.id,
|
|
6
|
+
workspaceId: row.workspace_id,
|
|
7
|
+
matchType: row.match_type,
|
|
8
|
+
fromPattern: row.from_pattern,
|
|
9
|
+
toTarget: row.to_target,
|
|
10
|
+
statusCode: row.status_code,
|
|
11
|
+
status: row.status,
|
|
12
|
+
override: toBool(row.override) === true,
|
|
13
|
+
priority: row.priority,
|
|
14
|
+
source: row.source,
|
|
15
|
+
sourceEntryId: row.source_entry_id ?? undefined,
|
|
16
|
+
fromPathAtCapture: row.from_path_at_capture ?? undefined,
|
|
17
|
+
toPathAtCapture: row.to_path_at_capture ?? undefined,
|
|
18
|
+
createdByPrincipal: row.created_by_principal,
|
|
19
|
+
createdByPluginId: row.created_by_plugin_id ?? undefined,
|
|
20
|
+
createdAt: row.created_at,
|
|
21
|
+
updatedAt: row.updated_at,
|
|
22
|
+
version: row.version,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** `toRecord`'s inverse: the exact column values one whole-row upsert persists. */
|
|
26
|
+
export function toRow({ record }, _optional = {}) {
|
|
27
|
+
return {
|
|
28
|
+
id: record.id,
|
|
29
|
+
workspace_id: record.workspaceId,
|
|
30
|
+
match_type: record.matchType,
|
|
31
|
+
from_pattern: record.fromPattern,
|
|
32
|
+
to_target: record.toTarget,
|
|
33
|
+
status_code: record.statusCode,
|
|
34
|
+
status: record.status,
|
|
35
|
+
override: record.override ? 1 : 0,
|
|
36
|
+
priority: record.priority,
|
|
37
|
+
source: record.source,
|
|
38
|
+
source_entry_id: record.sourceEntryId ?? null,
|
|
39
|
+
from_path_at_capture: record.fromPathAtCapture ?? null,
|
|
40
|
+
to_path_at_capture: record.toPathAtCapture ?? null,
|
|
41
|
+
created_by_principal: record.createdByPrincipal,
|
|
42
|
+
created_by_plugin_id: record.createdByPluginId ?? null,
|
|
43
|
+
created_at: record.createdAt,
|
|
44
|
+
updated_at: record.updatedAt,
|
|
45
|
+
version: record.version,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** The columns an existing row's upsert replaces — everything but the match key `id`. */
|
|
49
|
+
export function updatableColumns({ row }, _optional = {}) {
|
|
50
|
+
const { id: _id, ...rest } = row;
|
|
51
|
+
return rest;
|
|
52
|
+
}
|
|
53
|
+
/** The `redirect_revisions` row an append writes (compact JSON state; `id` is database-assigned). */
|
|
54
|
+
export function toRevisionRow({ revision }, _optional = {}) {
|
|
55
|
+
return {
|
|
56
|
+
redirect_id: revision.redirectId,
|
|
57
|
+
workspace_id: revision.workspaceId,
|
|
58
|
+
seq: revision.seq,
|
|
59
|
+
state_json: JSON.stringify(revision.state),
|
|
60
|
+
tombstoned: revision.tombstoned ? 1 : 0,
|
|
61
|
+
actor_id: revision.actorId,
|
|
62
|
+
plugin_id: revision.pluginId ?? null,
|
|
63
|
+
recorded_at: revision.recordedAt,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** One `redirect_revisions` row as a {@link RedirectRevision}. */
|
|
67
|
+
export function toRevision({ row }, _optional = {}) {
|
|
68
|
+
return {
|
|
69
|
+
redirectId: row.redirect_id,
|
|
70
|
+
workspaceId: row.workspace_id,
|
|
71
|
+
seq: row.seq,
|
|
72
|
+
state: JSON.parse(row.state_json),
|
|
73
|
+
tombstoned: toBool(row.tombstoned) === true,
|
|
74
|
+
actorId: row.actor_id,
|
|
75
|
+
pluginId: row.plugin_id ?? undefined,
|
|
76
|
+
recordedAt: row.recorded_at,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=repo.rows.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Generated } from "kysely";
|
|
2
|
+
/** Stored column shapes copied without schema changes; table names are host-owned. */
|
|
3
|
+
export interface RedirectsTable {
|
|
4
|
+
id: string;
|
|
5
|
+
workspace_id: string;
|
|
6
|
+
match_type: string;
|
|
7
|
+
from_pattern: string;
|
|
8
|
+
to_target: string;
|
|
9
|
+
status_code: number;
|
|
10
|
+
status: string;
|
|
11
|
+
override: number;
|
|
12
|
+
priority: number;
|
|
13
|
+
source: string;
|
|
14
|
+
source_entry_id: string | null;
|
|
15
|
+
from_path_at_capture: string | null;
|
|
16
|
+
to_path_at_capture: string | null;
|
|
17
|
+
created_by_principal: string;
|
|
18
|
+
created_by_plugin_id: string | null;
|
|
19
|
+
created_at: string;
|
|
20
|
+
updated_at: string;
|
|
21
|
+
version: number;
|
|
22
|
+
}
|
|
23
|
+
export interface RedirectRevisionsTable {
|
|
24
|
+
id: Generated<number>;
|
|
25
|
+
redirect_id: string;
|
|
26
|
+
workspace_id: string;
|
|
27
|
+
seq: number;
|
|
28
|
+
state_json: string;
|
|
29
|
+
tombstoned: number;
|
|
30
|
+
actor_id: string;
|
|
31
|
+
plugin_id: string | null;
|
|
32
|
+
recorded_at: string;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=row-types.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** The shared INV-03 read gate used by runtime resolution and host export. */
|
|
2
|
+
import type { OriginRegistryPort, RedirectTargetContext } from "@jini-ai/http-kit/verified-origin";
|
|
3
|
+
import { type SameOriginDestinationCheck } from "./reserved-destination.js";
|
|
4
|
+
/** Only the verified-origin owner's two redirect reads; no egress dependency. */
|
|
5
|
+
export type RedirectTargetOracle = Pick<OriginRegistryPort, "canonicalOrigin" | "isAllowedRedirectTarget">;
|
|
6
|
+
export type RedirectLocationRefusalReason = "referrer-alias" | "origin-unavailable" | "target-not-allowed" | "reserved-destination";
|
|
7
|
+
export type RedirectLocationCheck = {
|
|
8
|
+
kind: "allowed";
|
|
9
|
+
candidate: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: "refused";
|
|
12
|
+
reason: RedirectLocationRefusalReason;
|
|
13
|
+
destinationCheck?: SameOriginDestinationCheck;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Check a FULLY-INTERPOLATED Location with the required verified-origin oracle, then host path policy.
|
|
17
|
+
* Never checks a stored wildcard template in place of the Location a visitor would receive (INV-03).
|
|
18
|
+
* @param required Context, finished location, oracle and host-owned reserved first segments.
|
|
19
|
+
* @returns An explicit refusal or the absolute oracle candidate. No allow-all default exists.
|
|
20
|
+
* @throws Oracle infrastructure errors propagate, preserving the original resolver contract.
|
|
21
|
+
* @complexity O(location length), plus the origin reads. Constant additional state.
|
|
22
|
+
* @example checkRedirectLocation({ context, location: "/new", oracle, reservedSegments }, {});
|
|
23
|
+
*/
|
|
24
|
+
export declare function checkRedirectLocation({ context, location, oracle, reservedSegments }: {
|
|
25
|
+
context: RedirectTargetContext;
|
|
26
|
+
location: string;
|
|
27
|
+
oracle: RedirectTargetOracle;
|
|
28
|
+
reservedSegments: ReadonlySet<string>;
|
|
29
|
+
}, _optional?: Record<string, never>): Promise<RedirectLocationCheck>;
|
|
30
|
+
//# sourceMappingURL=target-gate.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { isReferrerAliasLocation } from "./referrer-alias.js";
|
|
2
|
+
import { checkSameOriginDestination } from "./reserved-destination.js";
|
|
3
|
+
/** A candidate `location` that already carries (or claims to carry) its own scheme/authority. */
|
|
4
|
+
function isPotentiallyCrossOrigin(location) {
|
|
5
|
+
return location.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(location);
|
|
6
|
+
}
|
|
7
|
+
function isDefaultPort(scheme, port) {
|
|
8
|
+
if (port === undefined)
|
|
9
|
+
return true;
|
|
10
|
+
return (scheme === "https" && port === 443) || (scheme === "http" && port === 80);
|
|
11
|
+
}
|
|
12
|
+
function composeOriginUrl(origin) {
|
|
13
|
+
const authority = isDefaultPort(origin.scheme === "https" ? "https" : "http", origin.port)
|
|
14
|
+
? origin.host
|
|
15
|
+
: `${origin.host}:${origin.port}`;
|
|
16
|
+
return `${origin.scheme}://${authority}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve `location` to a candidate suitable for `isAllowedRedirectTarget`:
|
|
20
|
+
* pass an already-absolute/protocol-relative location through unchanged;
|
|
21
|
+
* resolve a relative location against the workspace's verified canonical
|
|
22
|
+
* origin. Returns `null` if no verified origin exists and `location` is
|
|
23
|
+
* relative (cannot even form a candidate to check — fails closed), and for
|
|
24
|
+
* Express's `back` alias, which is served as the request's `Referer` rather than
|
|
25
|
+
* as a path, so no candidate describes where it goes (see `./referrer-alias.ts`).
|
|
26
|
+
*/
|
|
27
|
+
async function toOracleCandidate(ctx, location, originRegistry) {
|
|
28
|
+
if (isReferrerAliasLocation({ location: location }, {}))
|
|
29
|
+
return null;
|
|
30
|
+
if (isPotentiallyCrossOrigin(location))
|
|
31
|
+
return location;
|
|
32
|
+
try {
|
|
33
|
+
const canonical = await originRegistry.canonicalOrigin(ctx);
|
|
34
|
+
const path = location.startsWith("/") ? location : `/${location}`;
|
|
35
|
+
return `${composeOriginUrl(canonical)}${path}`;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check a FULLY-INTERPOLATED Location with the required verified-origin oracle, then host path policy.
|
|
43
|
+
* Never checks a stored wildcard template in place of the Location a visitor would receive (INV-03).
|
|
44
|
+
* @param required Context, finished location, oracle and host-owned reserved first segments.
|
|
45
|
+
* @returns An explicit refusal or the absolute oracle candidate. No allow-all default exists.
|
|
46
|
+
* @throws Oracle infrastructure errors propagate, preserving the original resolver contract.
|
|
47
|
+
* @complexity O(location length), plus the origin reads. Constant additional state.
|
|
48
|
+
* @example checkRedirectLocation({ context, location: "/new", oracle, reservedSegments }, {});
|
|
49
|
+
*/
|
|
50
|
+
export async function checkRedirectLocation({ context, location, oracle, reservedSegments }, _optional = {}) {
|
|
51
|
+
const candidate = await toOracleCandidate(context, location, oracle);
|
|
52
|
+
if (candidate === null) {
|
|
53
|
+
return { kind: "refused", reason: isReferrerAliasLocation({ location }, {}) ? "referrer-alias" : "origin-unavailable" };
|
|
54
|
+
}
|
|
55
|
+
const allowed = await oracle.isAllowedRedirectTarget({ context, url: candidate });
|
|
56
|
+
if (!allowed) {
|
|
57
|
+
// T050 (Polish) adds a warn-level log line here (normalized host only,
|
|
58
|
+
// never the full raw candidate) — named but not yet implemented.
|
|
59
|
+
return { kind: "refused", reason: "target-not-allowed" };
|
|
60
|
+
}
|
|
61
|
+
// The oracle answers "is that HOST allowed", which a same-origin destination passes by
|
|
62
|
+
// construction — including `/admin` and `/api`. See `checkSameOriginDestination`.
|
|
63
|
+
return checkReservedDestination({ context, candidate, oracle, reservedSegments }, {});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Second half of the read-path target gate: the oracle's host verdict, then this pathname
|
|
67
|
+
* verdict for a destination that is same-origin with the workspace's verified origin.
|
|
68
|
+
*
|
|
69
|
+
* @returns An explicit refusal when the redirect must not be served. Fails closed when no verified origin
|
|
70
|
+
* exists — the same posture `toOracleCandidate` already takes for a relative location.
|
|
71
|
+
* @complexity One `canonicalOrigin` lookup plus O(n) in the candidate length.
|
|
72
|
+
*/
|
|
73
|
+
async function checkReservedDestination({ context, candidate, oracle, reservedSegments }, _optional = {}) {
|
|
74
|
+
try {
|
|
75
|
+
const canonical = await oracle.canonicalOrigin(context);
|
|
76
|
+
const destinationCheck = checkSameOriginDestination({ candidate, canonical, reservedSegments }, {});
|
|
77
|
+
if (destinationCheck.kind !== "ok")
|
|
78
|
+
return { kind: "refused", reason: "reserved-destination", destinationCheck };
|
|
79
|
+
return { kind: "allowed", candidate };
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return { kind: "refused", reason: "origin-unavailable" };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=target-gate.js.map
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Core type definitions for the `redirects` Tier-2 library (ADR-033).
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* Declares the row/record shapes, the resolution result, and the command
|
|
6
|
+
* envelopes the redirects subsystem introduces. Redirect rules are operational
|
|
7
|
+
* routing state layered over the (future) `routing` library — they are NOT
|
|
8
|
+
* `entries` (ADR-022): modelling them as content would generate an editorial
|
|
9
|
+
* revision on every hit-count touch (INV-3 violation) and put high-write
|
|
10
|
+
* telemetry on the content chokepoint. Redirects therefore own dedicated,
|
|
11
|
+
* core-owned tables that reuse ADR-022's *discipline* (single write chokepoint,
|
|
12
|
+
* append-only revisions, ULIDs, bounded validation, CI canary) — the same call
|
|
13
|
+
* ADR-027 (media sidecars) and ADR-028 (settings ledger) made.
|
|
14
|
+
*
|
|
15
|
+
* Architectural role:
|
|
16
|
+
* INTERFACES AND TYPES ONLY — no feature logic. Concrete matchers, repos, and
|
|
17
|
+
* the resolver service satisfy these contracts elsewhere.
|
|
18
|
+
*
|
|
19
|
+
* Grounding: ADR-033 (this subsystem), ADR-022 (chokepoint/revision discipline,
|
|
20
|
+
* pluginId attribution, bounded expression language), ADR-021 (flat permission
|
|
21
|
+
* strings, composite workspace scoping), ADR-007 (workspaceId everywhere),
|
|
22
|
+
* ADR-009 (typed calls / outbox events / hooks), ADR-015 (repo behind a port).
|
|
23
|
+
*/
|
|
24
|
+
import type { ISODateTime, UUID } from "@jini-ai/core/primitives";
|
|
25
|
+
/**
|
|
26
|
+
* HTTP status a matched rule emits. 301/308 are permanent (cacheable, SEO
|
|
27
|
+
* link-equity transfer); 302/307 are temporary. 307/308 preserve the request
|
|
28
|
+
* method + body (the RFC 7538 successors to 301/302), which matters for
|
|
29
|
+
* non-GET routes; the admin default is 301.
|
|
30
|
+
*/
|
|
31
|
+
export type RedirectStatusCode = 301 | 302 | 307 | 308;
|
|
32
|
+
/**
|
|
33
|
+
* How a rule's `fromPattern` is matched against an incoming path.
|
|
34
|
+
* - `exact` — full-path equality; index-backed O(1) lookup.
|
|
35
|
+
* - `prefix` — longest-prefix match; index/trie-backed, with optional tail
|
|
36
|
+
* pass-through to the target (`/old` → `/new` maps `/old/x` → `/new/x`).
|
|
37
|
+
* - `wildcard` — glob with `*` capture segments; interpolated into the target
|
|
38
|
+
* via `$1..$n`. Part of the bounded "dynamic" seam.
|
|
39
|
+
* - `regex` — anchored, bounded (linear-time / RE2-semantics) regular
|
|
40
|
+
* expression with `$1..$n` capture interpolation. Authoring is
|
|
41
|
+
* gated behind `redirects.use_regex` (ADR-021) and OFF by default:
|
|
42
|
+
* arbitrary regex is exactly the unbounded-expression trust hazard
|
|
43
|
+
* ADR-022's amendment forbids, so it stays a named, gated seam.
|
|
44
|
+
*/
|
|
45
|
+
export declare const REDIRECT_MATCH_TYPES: readonly ["exact", "prefix", "wildcard", "regex"];
|
|
46
|
+
export type RedirectMatchType = (typeof REDIRECT_MATCH_TYPES)[number];
|
|
47
|
+
/**
|
|
48
|
+
* Provenance of a rule. `auto_slug_change` rows are minted by core inside the
|
|
49
|
+
* content write transaction (never-break-links, ADR-033 §5) and carry the entry
|
|
50
|
+
* lineage below; `manual` and `import` come from operators/AI/migration.
|
|
51
|
+
*/
|
|
52
|
+
export declare const REDIRECT_SOURCES: readonly ["manual", "auto_slug_change", "import"];
|
|
53
|
+
export type RedirectSource = (typeof REDIRECT_SOURCES)[number];
|
|
54
|
+
/** Lifecycle of a rule. Disabled rules are retained (audit) but never matched. */
|
|
55
|
+
export declare const REDIRECT_STATUSES: readonly ["active", "disabled"];
|
|
56
|
+
export type RedirectStatus = (typeof REDIRECT_STATUSES)[number];
|
|
57
|
+
/**
|
|
58
|
+
* A single redirect rule — a row in the core-owned `redirects` table.
|
|
59
|
+
* Written only through the redirects chokepoint, which records a
|
|
60
|
+
* {@link RedirectRevision} in the same transaction (ADR-022 §4a).
|
|
61
|
+
*/
|
|
62
|
+
export interface RedirectRecord {
|
|
63
|
+
/** ULID — stable identity (ADR-022 §4c). */
|
|
64
|
+
id: UUID;
|
|
65
|
+
/** Tenant boundary; part of every composite FK (ADR-007/021). */
|
|
66
|
+
workspaceId: UUID;
|
|
67
|
+
matchType: RedirectMatchType;
|
|
68
|
+
/** Normalized source pattern (leading slash, no trailing slash, lower-cased host-relative path). */
|
|
69
|
+
fromPattern: string;
|
|
70
|
+
/**
|
|
71
|
+
* Resolved destination — a normalized site-relative path, or an absolute URL.
|
|
72
|
+
* Absolute/off-site targets are an open-redirect vector: they require
|
|
73
|
+
* `redirects.manage` + host allowlist (ADR-033 §7), enforced at write time.
|
|
74
|
+
*/
|
|
75
|
+
toTarget: string;
|
|
76
|
+
statusCode: RedirectStatusCode;
|
|
77
|
+
status: RedirectStatus;
|
|
78
|
+
/**
|
|
79
|
+
* When true the rule is consulted BEFORE live content resolution (retire a
|
|
80
|
+
* live URL). Default false: rules only fill the 404 path so reusing an old
|
|
81
|
+
* slug for new content transparently reclaims it. Setting `override` needs
|
|
82
|
+
* `redirects.manage`.
|
|
83
|
+
*/
|
|
84
|
+
override: boolean;
|
|
85
|
+
/** Explicit tie-breaker within an equal-specificity band (higher wins). */
|
|
86
|
+
priority: number;
|
|
87
|
+
source: RedirectSource;
|
|
88
|
+
/** Provenance for `auto_slug_change` rows — the entry whose slug moved. */
|
|
89
|
+
sourceEntryId?: UUID | undefined;
|
|
90
|
+
/** Old/new path captured at the slug change (auto rows only). */
|
|
91
|
+
fromPathAtCapture?: string | undefined;
|
|
92
|
+
toPathAtCapture?: string | undefined;
|
|
93
|
+
/** Principal that caused the write (ADR-021). For auto rows, the slug-change actor. */
|
|
94
|
+
createdByPrincipal: UUID;
|
|
95
|
+
/** Originating plugin, or undefined for core/operator writes (ADR-022 attribution). */
|
|
96
|
+
createdByPluginId?: string | undefined;
|
|
97
|
+
createdAt: ISODateTime;
|
|
98
|
+
updatedAt: ISODateTime;
|
|
99
|
+
/** Monotonic per-record revision counter (ADR-022 §4c). */
|
|
100
|
+
version: number;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Append-only revision of a rule (ADR-022 §4b): full post-state snapshot +
|
|
104
|
+
* actor + attribution + monotonic sequence, written in the same transaction as
|
|
105
|
+
* the mutation. Deletes tombstone; the ledger is never rewritten.
|
|
106
|
+
*/
|
|
107
|
+
export interface RedirectRevision {
|
|
108
|
+
redirectId: UUID;
|
|
109
|
+
workspaceId: UUID;
|
|
110
|
+
/** Monotonic sequence per redirect. */
|
|
111
|
+
seq: number;
|
|
112
|
+
/** Complete state after the mutation (or the tombstone marker). */
|
|
113
|
+
state: RedirectRecord;
|
|
114
|
+
tombstoned: boolean;
|
|
115
|
+
actorId: UUID;
|
|
116
|
+
pluginId?: string | undefined;
|
|
117
|
+
recordedAt: ISODateTime;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Operational hit telemetry — a row in the core-owned `redirect_hits` sidecar.
|
|
121
|
+
* DELIBERATELY excluded from the revision discipline above (narrows ADR-022
|
|
122
|
+
* INV-3, exactly as ADR-027 does for `asset_renditions`): counters are
|
|
123
|
+
* high-write, best-effort, and non-critical to the never-break-links guarantee.
|
|
124
|
+
* Updated asynchronously off the request hot path (ADR-009 outbox lane), so a
|
|
125
|
+
* dropped increment loses a statistic, never a redirect.
|
|
126
|
+
*/
|
|
127
|
+
export interface RedirectHitStats {
|
|
128
|
+
redirectId: UUID;
|
|
129
|
+
workspaceId: UUID;
|
|
130
|
+
hitCount: number;
|
|
131
|
+
lastHitAt?: ISODateTime | undefined;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Where in the request pipeline a resolution attempt runs.
|
|
135
|
+
* - `pre_content` — only `override` rules are eligible (retire a live URL).
|
|
136
|
+
* - `post_content` — the default 404-fill pass (all active rules).
|
|
137
|
+
*/
|
|
138
|
+
export type RedirectResolvePhase = "pre_content" | "post_content";
|
|
139
|
+
/** Normalized inbound request handed to the resolver by the routing chain. */
|
|
140
|
+
export interface RedirectRequest {
|
|
141
|
+
workspaceId: UUID;
|
|
142
|
+
/** Normalized, query-stripped request path (e.g. `/old/page`). */
|
|
143
|
+
path: string;
|
|
144
|
+
phase: RedirectResolvePhase;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Outcome of a resolution attempt. `location` already has wildcard/regex
|
|
148
|
+
* captures interpolated and (for prefix rules) any tail appended. Resolution
|
|
149
|
+
* follows AT MOST ONE hop — chains are collapsed at write time (ADR-033 §6), so
|
|
150
|
+
* the request path never chases A→B→C.
|
|
151
|
+
*/
|
|
152
|
+
export type RedirectResolution = {
|
|
153
|
+
matched: false;
|
|
154
|
+
} | {
|
|
155
|
+
matched: true;
|
|
156
|
+
redirectId: UUID;
|
|
157
|
+
location: string;
|
|
158
|
+
statusCode: RedirectStatusCode;
|
|
159
|
+
matchType: RedirectMatchType;
|
|
160
|
+
};
|
|
161
|
+
/** Create-rule command envelope (mirrors the features/* Required/Deps/Input idiom). */
|
|
162
|
+
export interface CreateRedirectInput {
|
|
163
|
+
workspaceId: UUID;
|
|
164
|
+
matchType: RedirectMatchType;
|
|
165
|
+
fromPattern: string;
|
|
166
|
+
toTarget: string;
|
|
167
|
+
statusCode: RedirectStatusCode;
|
|
168
|
+
override?: boolean | undefined;
|
|
169
|
+
priority?: number | undefined;
|
|
170
|
+
actorId: UUID;
|
|
171
|
+
pluginId?: string | undefined;
|
|
172
|
+
}
|
|
173
|
+
/** Update-rule command envelope. */
|
|
174
|
+
export interface UpdateRedirectInput {
|
|
175
|
+
workspaceId: UUID;
|
|
176
|
+
id: UUID;
|
|
177
|
+
matchType?: RedirectMatchType | undefined;
|
|
178
|
+
fromPattern?: string | undefined;
|
|
179
|
+
toTarget?: string | undefined;
|
|
180
|
+
statusCode?: RedirectStatusCode | undefined;
|
|
181
|
+
status?: RedirectStatus | undefined;
|
|
182
|
+
override?: boolean | undefined;
|
|
183
|
+
priority?: number | undefined;
|
|
184
|
+
actorId: UUID;
|
|
185
|
+
pluginId?: string | undefined;
|
|
186
|
+
}
|
|
187
|
+
/** Filter for admin/AI listing. */
|
|
188
|
+
export interface ListRedirectsFilter {
|
|
189
|
+
workspaceId: UUID;
|
|
190
|
+
status?: RedirectStatus | undefined;
|
|
191
|
+
source?: RedirectSource | undefined;
|
|
192
|
+
matchType?: RedirectMatchType | undefined;
|
|
193
|
+
}
|
|
194
|
+
/** Validation / safety failures raised by the redirects chokepoint. */
|
|
195
|
+
export declare class RedirectNotFoundError extends Error {
|
|
196
|
+
}
|
|
197
|
+
export declare class RedirectValidationError extends Error {
|
|
198
|
+
}
|
|
199
|
+
export declare class RedirectConflictError extends Error {
|
|
200
|
+
}
|
|
201
|
+
/** Raised when a create/update would introduce a cycle that cannot be collapsed. */
|
|
202
|
+
export declare class RedirectLoopError extends Error {
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Raised by the write chokepoint (`redirects.ts`) when an absolute/off-site
|
|
206
|
+
* `toTarget` fails `OriginRegistryPort.isAllowedRedirectTarget` (REQ-08,
|
|
207
|
+
* errors.spec.md `REDIRECT_TARGET_NOT_ALLOWED`). The read-path oracle
|
|
208
|
+
* rejection (`phase-handler.ts`, REQ-09/10) never raises this — it resolves
|
|
209
|
+
* to `{ matched: false }` instead (never surfaced as an HTTP error).
|
|
210
|
+
*/
|
|
211
|
+
export declare class RedirectTargetNotAllowedError extends Error {
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=types.d.ts.map
|