@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,51 @@
|
|
|
1
|
+
import type { RedirectDbHandle } from "./ports.internal.js";
|
|
2
|
+
import type { RedirectRepoPort } from "./ports.js";
|
|
3
|
+
import type { ListRedirectsFilter, RedirectRecord, RedirectRevision } from "./types.js";
|
|
4
|
+
export declare class InMemoryRedirectRepo implements RedirectRepoPort, RedirectDbHandle {
|
|
5
|
+
private readonly records;
|
|
6
|
+
private readonly revisionsByRedirectId;
|
|
7
|
+
constructor({ seed }?: {
|
|
8
|
+
seed?: RedirectRecord[];
|
|
9
|
+
}, _optional?: Record<string, never>);
|
|
10
|
+
private all;
|
|
11
|
+
findById(required: {
|
|
12
|
+
workspaceId: string;
|
|
13
|
+
id: string;
|
|
14
|
+
}, _optional?: Record<string, never>): Promise<RedirectRecord | null>;
|
|
15
|
+
lookupExact(required: {
|
|
16
|
+
workspaceId: string;
|
|
17
|
+
path: string;
|
|
18
|
+
includeOverrideOnly: boolean;
|
|
19
|
+
}, _optional?: Record<string, never>): Promise<RedirectRecord | null>;
|
|
20
|
+
lookupLongestPrefix(required: {
|
|
21
|
+
workspaceId: string;
|
|
22
|
+
path: string;
|
|
23
|
+
includeOverrideOnly: boolean;
|
|
24
|
+
}, _optional?: Record<string, never>): Promise<RedirectRecord | null>;
|
|
25
|
+
listDynamic(required: {
|
|
26
|
+
workspaceId: string;
|
|
27
|
+
includeOverrideOnly: boolean;
|
|
28
|
+
limit: number;
|
|
29
|
+
}, _optional?: Record<string, never>): Promise<RedirectRecord[]>;
|
|
30
|
+
list(filter: ListRedirectsFilter, _optional?: Record<string, never>): Promise<RedirectRecord[]>;
|
|
31
|
+
findByFromPattern(required: {
|
|
32
|
+
workspaceId: string;
|
|
33
|
+
fromPattern: string;
|
|
34
|
+
}, _optional?: Record<string, never>): Promise<RedirectRecord | null>;
|
|
35
|
+
save(required: {
|
|
36
|
+
record: RedirectRecord;
|
|
37
|
+
revision: RedirectRevision;
|
|
38
|
+
}, _optional?: Record<string, never>): Promise<void>;
|
|
39
|
+
tombstone(required: {
|
|
40
|
+
workspaceId: string;
|
|
41
|
+
id: string;
|
|
42
|
+
revision: RedirectRevision;
|
|
43
|
+
}, _optional?: Record<string, never>): Promise<void>;
|
|
44
|
+
insertRedirect(record: RedirectRecord, _optional?: Record<string, never>): void;
|
|
45
|
+
insertRevision(revision: RedirectRevision, _optional?: Record<string, never>): void;
|
|
46
|
+
/** Test-only helper: the append-only revision ledger for one redirect, in seq order. */
|
|
47
|
+
listRevisionsForTests({ redirectId }: {
|
|
48
|
+
redirectId: string;
|
|
49
|
+
}, _optional?: Record<string, never>): RedirectRevision[];
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=repo.memory.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file In-memory `RedirectRepoPort` adapter (rule-of-two adapter #1,
|
|
3
|
+
* ADR-006). Test/dev double — no persistence beyond process lifetime.
|
|
4
|
+
*
|
|
5
|
+
* Also satisfies `RedirectDbHandle` (`ports.internal.ts`) directly — `save()`
|
|
6
|
+
* is implemented as the same two-step write (`insertRedirect` then
|
|
7
|
+
* `insertRevision`) that `redirects.ts`/`capture.ts` call independently via
|
|
8
|
+
* `insertRedirectAndRevision`; both paths converge on identical storage
|
|
9
|
+
* semantics without this file importing `ports.internal.ts`'s function value
|
|
10
|
+
* (only its `RedirectDbHandle` type, structurally satisfied here).
|
|
11
|
+
*/
|
|
12
|
+
import { compareLongestFirst, compareSamePatternExactFirst, compareTieBreak } from "./order.js";
|
|
13
|
+
import { RedirectNotFoundError } from "./types.js";
|
|
14
|
+
export class InMemoryRedirectRepo {
|
|
15
|
+
records = new Map();
|
|
16
|
+
revisionsByRedirectId = new Map();
|
|
17
|
+
constructor({ seed = [] } = {}, _optional = {}) {
|
|
18
|
+
for (const record of seed)
|
|
19
|
+
this.records.set(record.id, { ...record });
|
|
20
|
+
}
|
|
21
|
+
all(workspaceId) {
|
|
22
|
+
return [...this.records.values()].filter((r) => r.workspaceId === workspaceId);
|
|
23
|
+
}
|
|
24
|
+
async findById(required, _optional = {}) {
|
|
25
|
+
const record = this.records.get(required.id);
|
|
26
|
+
return record && record.workspaceId === required.workspaceId ? { ...record } : null;
|
|
27
|
+
}
|
|
28
|
+
async lookupExact(required, _optional = {}) {
|
|
29
|
+
const candidates = this.all(required.workspaceId).filter((r) => r.status === "active" &&
|
|
30
|
+
r.matchType === "exact" &&
|
|
31
|
+
r.fromPattern === required.path &&
|
|
32
|
+
(!required.includeOverrideOnly || r.override));
|
|
33
|
+
candidates.sort((a, b) => compareTieBreak({ a, b }, {}));
|
|
34
|
+
return candidates[0] ? { ...candidates[0] } : null;
|
|
35
|
+
}
|
|
36
|
+
async lookupLongestPrefix(required, _optional = {}) {
|
|
37
|
+
const candidates = this.all(required.workspaceId).filter((r) => {
|
|
38
|
+
if (r.status !== "active" || r.matchType !== "prefix")
|
|
39
|
+
return false;
|
|
40
|
+
if (required.includeOverrideOnly && !r.override)
|
|
41
|
+
return false;
|
|
42
|
+
const pattern = r.fromPattern;
|
|
43
|
+
if (required.path === pattern)
|
|
44
|
+
return true;
|
|
45
|
+
return required.path.startsWith(pattern.endsWith("/") ? pattern : `${pattern}/`);
|
|
46
|
+
});
|
|
47
|
+
candidates.sort((a, b) => compareLongestFirst({ a, b }, {}));
|
|
48
|
+
return candidates[0] ? { ...candidates[0] } : null;
|
|
49
|
+
}
|
|
50
|
+
async listDynamic(required, _optional = {}) {
|
|
51
|
+
const candidates = this.all(required.workspaceId).filter((r) => r.status === "active" && r.matchType === "wildcard" && (!required.includeOverrideOnly || r.override));
|
|
52
|
+
candidates.sort((a, b) => compareLongestFirst({ a, b }, {}));
|
|
53
|
+
return candidates.slice(0, required.limit).map((r) => ({ ...r }));
|
|
54
|
+
}
|
|
55
|
+
async list(filter, _optional = {}) {
|
|
56
|
+
return this.all(filter.workspaceId)
|
|
57
|
+
.filter((r) => filter.status === undefined || r.status === filter.status)
|
|
58
|
+
.filter((r) => filter.source === undefined || r.source === filter.source)
|
|
59
|
+
.filter((r) => filter.matchType === undefined || r.matchType === filter.matchType)
|
|
60
|
+
.map((r) => ({ ...r }));
|
|
61
|
+
}
|
|
62
|
+
async findByFromPattern(required, _optional = {}) {
|
|
63
|
+
const candidates = this.all(required.workspaceId).filter((r) => r.status === "active" && r.fromPattern === required.fromPattern);
|
|
64
|
+
if (candidates.length === 0)
|
|
65
|
+
return null;
|
|
66
|
+
candidates.sort((a, b) => compareSamePatternExactFirst({ a, b }, {}));
|
|
67
|
+
const winner = candidates[0];
|
|
68
|
+
return winner ? { ...winner } : null;
|
|
69
|
+
}
|
|
70
|
+
async save(required, _optional = {}) {
|
|
71
|
+
this.insertRedirect(required.record);
|
|
72
|
+
this.insertRevision(required.revision);
|
|
73
|
+
}
|
|
74
|
+
async tombstone(required, _optional = {}) {
|
|
75
|
+
const existing = this.records.get(required.id);
|
|
76
|
+
if (!existing || existing.workspaceId !== required.workspaceId) {
|
|
77
|
+
throw new RedirectNotFoundError(`redirect '${required.id}' was not found`);
|
|
78
|
+
}
|
|
79
|
+
this.insertRedirect(required.revision.state);
|
|
80
|
+
this.insertRevision(required.revision);
|
|
81
|
+
}
|
|
82
|
+
// -------------------------------------------------------------------
|
|
83
|
+
// RedirectDbHandle (used by redirects.ts/capture.ts via ports.internal.ts)
|
|
84
|
+
// -------------------------------------------------------------------
|
|
85
|
+
insertRedirect(record, _optional = {}) {
|
|
86
|
+
this.records.set(record.id, { ...record });
|
|
87
|
+
}
|
|
88
|
+
insertRevision(revision, _optional = {}) {
|
|
89
|
+
const list = this.revisionsByRedirectId.get(revision.redirectId) ?? [];
|
|
90
|
+
list.push({ ...revision });
|
|
91
|
+
this.revisionsByRedirectId.set(revision.redirectId, list);
|
|
92
|
+
}
|
|
93
|
+
/** Test-only helper: the append-only revision ledger for one redirect, in seq order. */
|
|
94
|
+
listRevisionsForTests({ redirectId }, _optional = {}) {
|
|
95
|
+
return [...(this.revisionsByRedirectId.get(redirectId) ?? [])].sort((a, b) => a.seq - b.seq);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=repo.memory.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The one "same-origin destination lands on the admin surface" verdict, shared by the
|
|
3
|
+
* redirect READ gate (`phase-handler.ts`) and WRITE gate (`redirects.ts`) (t91 B1, 2026-09-16).
|
|
4
|
+
*
|
|
5
|
+
* Purpose:
|
|
6
|
+
* Before this file existed, `phase-handler.ts` decided "is this candidate on the workspace's own
|
|
7
|
+
* origin" by comparing `URL.origin` strings, and `redirects.ts` never asked the question for an
|
|
8
|
+
* absolute target at all — it only ever ran the reserved-path rule for a site-relative target. Both
|
|
9
|
+
* gaps let an absolute same-origin target land a visitor on `/admin` or `/api/...` while the
|
|
10
|
+
* oracle that approved the candidate had already normalized past the exact spelling the string
|
|
11
|
+
* comparison needed (`https://site./admin` and `https://site%2E/admin` both decode to the same
|
|
12
|
+
* host the oracle allows, but have a DIFFERENT `URL.origin` string from `https://site`). "Same
|
|
13
|
+
* origin" is decided here by `origin`'s own `normalizeOriginCandidate` + `isSameOrigin` — never a
|
|
14
|
+
* raw `URL.origin` string equality — because that is what the oracle that approved the candidate
|
|
15
|
+
* already normalizes against.
|
|
16
|
+
*
|
|
17
|
+
* A `{ kind: "ok" }` verdict for a CROSS-origin candidate means "not this rule's business", not
|
|
18
|
+
* "allowed" — callers must already have run the origin oracle (`isAllowedRedirectTarget`) first;
|
|
19
|
+
* this function only ever narrows what the oracle already approved.
|
|
20
|
+
*
|
|
21
|
+
* Interpolation happens on the READ path (a stored template like `/go/*` -> `/$1` becomes
|
|
22
|
+
* `Location: /admin` only once a request path fills it in), so the read gate calls this AFTER
|
|
23
|
+
* interpolation. The write gate can only ever see the template, so it calls this on an absolute
|
|
24
|
+
* target once the oracle allows it — a stored relative template is out of scope here, same as
|
|
25
|
+
* before this file existed.
|
|
26
|
+
*
|
|
27
|
+
* Architectural role:
|
|
28
|
+
* Feature logic, pure, no I/O. Imports only through `features/origin` and `platform/routing`'s own
|
|
29
|
+
* barrels (both are `no-deep-imports` promoted in `.dependency-cruiser.mjs`).
|
|
30
|
+
*/
|
|
31
|
+
import { type VerifiedOrigin } from "@jini-ai/http-kit/verified-origin";
|
|
32
|
+
import { type SitePathCheck } from "@jini-ai/http-kit/verified-origin";
|
|
33
|
+
/** {@link checkSameOriginDestination}'s verdict — every arm but `ok` is a refusal. */
|
|
34
|
+
export type SameOriginDestinationCheck = SitePathCheck | {
|
|
35
|
+
kind: "unparseable";
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Whether `candidate` is both same-origin with `canonical` AND lands on the admin application's
|
|
39
|
+
* own URL space. A cross-origin candidate (by construction, out of this rule's business) and an
|
|
40
|
+
* ordinary same-origin path both answer `{ kind: "ok" }` — the distinction does not matter to a
|
|
41
|
+
* caller that only wants to know "must this be refused".
|
|
42
|
+
*
|
|
43
|
+
* @param required.candidate - The candidate URL string to classify (already oracle-approved by the caller).
|
|
44
|
+
* @param required.canonical - The workspace's verified canonical origin.
|
|
45
|
+
* @returns `{ kind: "unparseable" }` if `candidate` fails the oracle's own normalizer (including a
|
|
46
|
+
* non-empty userinfo component — fail closed rather than guessing which host it means); otherwise
|
|
47
|
+
* `{ kind: "ok" }` for a cross-origin candidate or ordinary same-origin content, or the
|
|
48
|
+
* `checkSitePathname` refusal arm naming why a same-origin candidate is reserved.
|
|
49
|
+
* @param required.reservedSegments - Host-owned, case-folded first path segments.
|
|
50
|
+
* @complexity O(n) in the candidate length.
|
|
51
|
+
* @example checkSameOriginDestination({ candidate: "https://site.example/blog", canonical, reservedSegments }, {});
|
|
52
|
+
*/
|
|
53
|
+
export declare function checkSameOriginDestination({ candidate, canonical, reservedSegments }: {
|
|
54
|
+
candidate: string;
|
|
55
|
+
canonical: VerifiedOrigin;
|
|
56
|
+
reservedSegments: ReadonlySet<string>;
|
|
57
|
+
}, _optional?: Record<string, never>): SameOriginDestinationCheck;
|
|
58
|
+
//# sourceMappingURL=reserved-destination.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The one "same-origin destination lands on the admin surface" verdict, shared by the
|
|
3
|
+
* redirect READ gate (`phase-handler.ts`) and WRITE gate (`redirects.ts`) (t91 B1, 2026-09-16).
|
|
4
|
+
*
|
|
5
|
+
* Purpose:
|
|
6
|
+
* Before this file existed, `phase-handler.ts` decided "is this candidate on the workspace's own
|
|
7
|
+
* origin" by comparing `URL.origin` strings, and `redirects.ts` never asked the question for an
|
|
8
|
+
* absolute target at all — it only ever ran the reserved-path rule for a site-relative target. Both
|
|
9
|
+
* gaps let an absolute same-origin target land a visitor on `/admin` or `/api/...` while the
|
|
10
|
+
* oracle that approved the candidate had already normalized past the exact spelling the string
|
|
11
|
+
* comparison needed (`https://site./admin` and `https://site%2E/admin` both decode to the same
|
|
12
|
+
* host the oracle allows, but have a DIFFERENT `URL.origin` string from `https://site`). "Same
|
|
13
|
+
* origin" is decided here by `origin`'s own `normalizeOriginCandidate` + `isSameOrigin` — never a
|
|
14
|
+
* raw `URL.origin` string equality — because that is what the oracle that approved the candidate
|
|
15
|
+
* already normalizes against.
|
|
16
|
+
*
|
|
17
|
+
* A `{ kind: "ok" }` verdict for a CROSS-origin candidate means "not this rule's business", not
|
|
18
|
+
* "allowed" — callers must already have run the origin oracle (`isAllowedRedirectTarget`) first;
|
|
19
|
+
* this function only ever narrows what the oracle already approved.
|
|
20
|
+
*
|
|
21
|
+
* Interpolation happens on the READ path (a stored template like `/go/*` -> `/$1` becomes
|
|
22
|
+
* `Location: /admin` only once a request path fills it in), so the read gate calls this AFTER
|
|
23
|
+
* interpolation. The write gate can only ever see the template, so it calls this on an absolute
|
|
24
|
+
* target once the oracle allows it — a stored relative template is out of scope here, same as
|
|
25
|
+
* before this file existed.
|
|
26
|
+
*
|
|
27
|
+
* Architectural role:
|
|
28
|
+
* Feature logic, pure, no I/O. Imports only through `features/origin` and `platform/routing`'s own
|
|
29
|
+
* barrels (both are `no-deep-imports` promoted in `.dependency-cruiser.mjs`).
|
|
30
|
+
*/
|
|
31
|
+
import { isSameOrigin, normalizeOriginCandidate } from "@jini-ai/http-kit/verified-origin";
|
|
32
|
+
import { checkSitePathname } from "@jini-ai/http-kit/verified-origin";
|
|
33
|
+
/**
|
|
34
|
+
* Whether `candidate` is both same-origin with `canonical` AND lands on the admin application's
|
|
35
|
+
* own URL space. A cross-origin candidate (by construction, out of this rule's business) and an
|
|
36
|
+
* ordinary same-origin path both answer `{ kind: "ok" }` — the distinction does not matter to a
|
|
37
|
+
* caller that only wants to know "must this be refused".
|
|
38
|
+
*
|
|
39
|
+
* @param required.candidate - The candidate URL string to classify (already oracle-approved by the caller).
|
|
40
|
+
* @param required.canonical - The workspace's verified canonical origin.
|
|
41
|
+
* @returns `{ kind: "unparseable" }` if `candidate` fails the oracle's own normalizer (including a
|
|
42
|
+
* non-empty userinfo component — fail closed rather than guessing which host it means); otherwise
|
|
43
|
+
* `{ kind: "ok" }` for a cross-origin candidate or ordinary same-origin content, or the
|
|
44
|
+
* `checkSitePathname` refusal arm naming why a same-origin candidate is reserved.
|
|
45
|
+
* @param required.reservedSegments - Host-owned, case-folded first path segments.
|
|
46
|
+
* @complexity O(n) in the candidate length.
|
|
47
|
+
* @example checkSameOriginDestination({ candidate: "https://site.example/blog", canonical, reservedSegments }, {});
|
|
48
|
+
*/
|
|
49
|
+
export function checkSameOriginDestination({ candidate, canonical, reservedSegments }, _optional = {}) {
|
|
50
|
+
const target = normalizeOriginCandidate({ rawUrl: candidate });
|
|
51
|
+
if (target === null)
|
|
52
|
+
return { kind: "unparseable" };
|
|
53
|
+
if (!isSameOrigin({ target: target, canonical: canonical }))
|
|
54
|
+
return { kind: "ok" };
|
|
55
|
+
// Cannot throw: normalizeOriginCandidate just parsed this exact string with the same no-base
|
|
56
|
+
// `new URL()`, so re-parsing it here to hand checkSitePathname a real URL cannot fail differently.
|
|
57
|
+
return checkSitePathname({ url: new URL(candidate), reservedSegments }, {});
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=reserved-destination.js.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `RedirectResolver` (read-path resolution) + `registerRedirectsPhaseHandlers`
|
|
3
|
+
* (routing-chain adapter) — SPEC-009 REQ-09/10/18/19/20; ADR-PIPE-009 C-006/C-007.
|
|
4
|
+
*
|
|
5
|
+
* Purpose:
|
|
6
|
+
* Owns the read-path open-redirect oracle call (C-013 in the implementation
|
|
7
|
+
* outline) — the single highest-risk contract in this feature. Gathers
|
|
8
|
+
* phase-eligible candidate rules from `RedirectRepoPort`'s three lookup
|
|
9
|
+
* methods (`lookupExact`/`lookupLongestPrefix`/`listDynamic`), hands them to
|
|
10
|
+
* `RedirectMatcher.match` for precedence/tie-break/interpolation, then
|
|
11
|
+
* validates the FULLY-INTERPOLATED `location` via
|
|
12
|
+
* `OriginRegistryPort.isAllowedRedirectTarget` in EVERY code path before ever
|
|
13
|
+
* returning a `matched: true` outcome (INV-03), and then — for a destination that is same-origin
|
|
14
|
+
* with the workspace's own verified origin, which the oracle allows by construction — the shared
|
|
15
|
+
* reserved-path rule from `./reserved-destination.ts`'s `checkSameOriginDestination`. The second
|
|
16
|
+
* half has to live here rather than only at the write chokepoint because interpolation happens on
|
|
17
|
+
* THIS path: the stored `toTarget` is a template, and `Location` is what comes out of it.
|
|
18
|
+
*
|
|
19
|
+
* Oracle-call discipline (documented, load-bearing): `isAllowedRedirectTarget`
|
|
20
|
+
* only accepts a fully-qualified, WHATWG-parseable candidate URL (see
|
|
21
|
+
* `origin/origin.ts`'s `normalizeOriginCandidate` — a bare relative path like
|
|
22
|
+
* `/new` fails to parse with no base and would otherwise always resolve to
|
|
23
|
+
* "not allowed"). So a plain relative `location` is first resolved to an
|
|
24
|
+
* absolute URL against the workspace's own verified canonical origin (making
|
|
25
|
+
* the oracle call against a genuinely absolute, same-origin-by-construction
|
|
26
|
+
* candidate) before being handed to the oracle — this satisfies "call the
|
|
27
|
+
* oracle in every code path" literally while keeping ordinary same-site
|
|
28
|
+
* redirects (the overwhelmingly common case) working. A `location` that
|
|
29
|
+
* already looks absolute or protocol-relative (contains a scheme prefix or
|
|
30
|
+
* starts with `//`) is passed to the oracle exactly as interpolated, since
|
|
31
|
+
* resolving it against the canonical origin first would corrupt it. If the
|
|
32
|
+
* workspace has no verified origin registered at all, this fails closed
|
|
33
|
+
* (`{ matched: false }`) rather than guessing — matches `origin`'s own
|
|
34
|
+
* fail-closed posture (`OriginNotVerifiedError`).
|
|
35
|
+
*
|
|
36
|
+
* Architectural role:
|
|
37
|
+
* Feature logic (`RedirectPhaseHandlerResolver`, implements `RedirectResolver`)
|
|
38
|
+
* + a thin composition-root registration function
|
|
39
|
+
* (`registerRedirectsPhaseHandlers`) that adapts to/from `routing`'s
|
|
40
|
+
* `RouteResolvePhaseHandler` shape. No Express/route code.
|
|
41
|
+
*/
|
|
42
|
+
import type { Clock as ClockPort, IdGenerator as IdGeneratorPort } from "@jini-ai/core/primitives";
|
|
43
|
+
import type { OutboxPort } from "../core/index.js";
|
|
44
|
+
import { type RedirectTargetOracle } from "./target-gate.js";
|
|
45
|
+
import type { RedirectMatcher, RedirectRepoPort, RedirectResolverWithRefusals, RedirectResolutionReport } from "./ports.js";
|
|
46
|
+
import type { RedirectRequest, RedirectResolution } from "./types.js";
|
|
47
|
+
export interface RedirectPhaseHandlerDeps {
|
|
48
|
+
repo: RedirectRepoPort;
|
|
49
|
+
matcher: RedirectMatcher;
|
|
50
|
+
originRegistry: RedirectTargetOracle;
|
|
51
|
+
/** First path segments reserved by the composing host; no product-policy default. */
|
|
52
|
+
reservedSegments: ReadonlySet<string>;
|
|
53
|
+
/**
|
|
54
|
+
* Hit-recording deps (W-008) — OPTIONAL so unit tests exercising the
|
|
55
|
+
* oracle gate alone (T006) don't need to stub them. When present, a
|
|
56
|
+
* successful resolution enqueues a `redirect.hit` outbox event,
|
|
57
|
+
* fire-and-forget (never awaited before the outcome is returned, never
|
|
58
|
+
* allowed to affect the resolution — REQ-21/AC-26).
|
|
59
|
+
*/
|
|
60
|
+
hits?: {
|
|
61
|
+
outbox: OutboxPort;
|
|
62
|
+
clock: ClockPort;
|
|
63
|
+
idGen: IdGeneratorPort;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* `RedirectResolver` implementation — the resolution entry point the routing-
|
|
68
|
+
* chain adapter (`registerRedirectsPhaseHandlers`) calls per phase.
|
|
69
|
+
*/
|
|
70
|
+
export declare class RedirectPhaseHandlerResolver implements RedirectResolverWithRefusals {
|
|
71
|
+
private readonly deps;
|
|
72
|
+
constructor(deps: RedirectPhaseHandlerDeps, _optional?: Record<string, never>);
|
|
73
|
+
/**
|
|
74
|
+
* @complexity O(1) exact/prefix (index-backed) + O(k) dynamic scan, k =
|
|
75
|
+
* the capped dynamic-set size (500).
|
|
76
|
+
*/
|
|
77
|
+
resolve(request: RedirectRequest, _optional?: Record<string, never>): Promise<RedirectResolution>;
|
|
78
|
+
/**
|
|
79
|
+
* Same resolution and hit effects, with explicit refusal evidence for SPA/HTTP adapters.
|
|
80
|
+
* @returns The unchanged legacy resolution plus any rejected finished Location.
|
|
81
|
+
* @complexity O(1) exact/prefix retrieval plus O(k) capped dynamic candidate scan.
|
|
82
|
+
*/
|
|
83
|
+
resolveWithRefusals(request: RedirectRequest, _optional?: Record<string, never>): Promise<RedirectResolutionReport>;
|
|
84
|
+
/**
|
|
85
|
+
* W-008: enqueue a `redirect.hit` event, fire-and-forget. MUST NOT be
|
|
86
|
+
* awaited before the redirect response is sent, and MUST NOT let an
|
|
87
|
+
* enqueue failure affect the resolution outcome (REQ-21/AC-26).
|
|
88
|
+
*/
|
|
89
|
+
private recordHitFireAndForget;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Create the read-path resolver with the required origin oracle and host path policy.
|
|
93
|
+
* @param required Repository, matcher, oracle and reserved path segments.
|
|
94
|
+
* @param optional Optional best-effort hit recording, independent of resolution.
|
|
95
|
+
* @returns A resolver that checks the fully interpolated Location before returning a match.
|
|
96
|
+
* @complexity O(1) construction; resolve inherits the bounded candidate scan.
|
|
97
|
+
* @example createRedirectResolver({ repo, matcher, originRegistry, reservedSegments }, {});
|
|
98
|
+
*/
|
|
99
|
+
export declare function createRedirectResolver(required: Omit<RedirectPhaseHandlerDeps, "hits">, optional?: Pick<RedirectPhaseHandlerDeps, "hits">): RedirectResolverWithRefusals;
|
|
100
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { nowIso as clockNowIso } from "@jini-ai/core/primitives";
|
|
2
|
+
import { checkRedirectLocation } from "./target-gate.js";
|
|
3
|
+
/** The bounded dynamic (wildcard) set cap (behavior.spec.md §3/§4, OQ-01). */
|
|
4
|
+
const DYNAMIC_SET_LIMIT = 500;
|
|
5
|
+
/**
|
|
6
|
+
* `RedirectResolver` implementation — the resolution entry point the routing-
|
|
7
|
+
* chain adapter (`registerRedirectsPhaseHandlers`) calls per phase.
|
|
8
|
+
*/
|
|
9
|
+
export class RedirectPhaseHandlerResolver {
|
|
10
|
+
deps;
|
|
11
|
+
constructor(deps, _optional = {}) {
|
|
12
|
+
this.deps = deps;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* @complexity O(1) exact/prefix (index-backed) + O(k) dynamic scan, k =
|
|
16
|
+
* the capped dynamic-set size (500).
|
|
17
|
+
*/
|
|
18
|
+
async resolve(request, _optional = {}) {
|
|
19
|
+
return (await this.resolveWithRefusals(request, {})).resolution;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Same resolution and hit effects, with explicit refusal evidence for SPA/HTTP adapters.
|
|
23
|
+
* @returns The unchanged legacy resolution plus any rejected finished Location.
|
|
24
|
+
* @complexity O(1) exact/prefix retrieval plus O(k) capped dynamic candidate scan.
|
|
25
|
+
*/
|
|
26
|
+
async resolveWithRefusals(request, _optional = {}) {
|
|
27
|
+
const includeOverrideOnly = request.phase === "pre_content";
|
|
28
|
+
const lookupBase = { workspaceId: request.workspaceId, includeOverrideOnly };
|
|
29
|
+
const [exact, prefix, dynamic] = await Promise.all([
|
|
30
|
+
this.deps.repo.lookupExact({ ...lookupBase, path: request.path }),
|
|
31
|
+
this.deps.repo.lookupLongestPrefix({ ...lookupBase, path: request.path }),
|
|
32
|
+
this.deps.repo.listDynamic({ ...lookupBase, limit: DYNAMIC_SET_LIMIT }),
|
|
33
|
+
]);
|
|
34
|
+
const candidates = [exact, prefix, ...dynamic].filter((r) => r !== null);
|
|
35
|
+
if (candidates.length === 0)
|
|
36
|
+
return { resolution: { matched: false }, refused: [] };
|
|
37
|
+
const resolution = this.deps.matcher.match({ request, rules: candidates });
|
|
38
|
+
if (!resolution.matched)
|
|
39
|
+
return { resolution: { matched: false }, refused: [] };
|
|
40
|
+
// INV-03: the read-path open-redirect oracle gate — MUST run in every
|
|
41
|
+
// code path before ever returning a matched:true outcome.
|
|
42
|
+
const verdict = await checkRedirectLocation({
|
|
43
|
+
context: { workspaceId: request.workspaceId }, location: resolution.location,
|
|
44
|
+
oracle: this.deps.originRegistry, reservedSegments: this.deps.reservedSegments,
|
|
45
|
+
}, {});
|
|
46
|
+
if (verdict.kind === "refused") {
|
|
47
|
+
return { resolution: { matched: false }, refused: [{ redirectId: resolution.redirectId, location: resolution.location, reason: verdict.reason }] };
|
|
48
|
+
}
|
|
49
|
+
this.recordHitFireAndForget(request.workspaceId, resolution.redirectId);
|
|
50
|
+
return { resolution, refused: [] };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* W-008: enqueue a `redirect.hit` event, fire-and-forget. MUST NOT be
|
|
54
|
+
* awaited before the redirect response is sent, and MUST NOT let an
|
|
55
|
+
* enqueue failure affect the resolution outcome (REQ-21/AC-26).
|
|
56
|
+
*/
|
|
57
|
+
recordHitFireAndForget(workspaceId, redirectId) {
|
|
58
|
+
if (!this.deps.hits)
|
|
59
|
+
return;
|
|
60
|
+
const { outbox, clock, idGen } = this.deps.hits;
|
|
61
|
+
const event = {
|
|
62
|
+
id: idGen.newId(),
|
|
63
|
+
name: "redirect.hit",
|
|
64
|
+
occurredAt: clockNowIso({ clock: clock }),
|
|
65
|
+
aggregateId: redirectId,
|
|
66
|
+
workspaceId,
|
|
67
|
+
payload: { workspaceId, redirectId, at: clockNowIso({ clock: clock }) },
|
|
68
|
+
};
|
|
69
|
+
// See redirects.ts's `enqueueMutatedEvent` comment for why this cast is needed (a named
|
|
70
|
+
// interface payload isn't auto-assignable to DomainEvent's default Record<string,unknown>).
|
|
71
|
+
void outbox.enqueue(event).catch(() => {
|
|
72
|
+
// Best-effort — a dropped hit-count enqueue never affects the redirect response (INV-06).
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create the read-path resolver with the required origin oracle and host path policy.
|
|
78
|
+
* @param required Repository, matcher, oracle and reserved path segments.
|
|
79
|
+
* @param optional Optional best-effort hit recording, independent of resolution.
|
|
80
|
+
* @returns A resolver that checks the fully interpolated Location before returning a match.
|
|
81
|
+
* @complexity O(1) construction; resolve inherits the bounded candidate scan.
|
|
82
|
+
* @example createRedirectResolver({ repo, matcher, originRegistry, reservedSegments }, {});
|
|
83
|
+
*/
|
|
84
|
+
export function createRedirectResolver(required, optional = {}) {
|
|
85
|
+
return new RedirectPhaseHandlerResolver({ ...required, ...optional });
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=resolver.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RedirectTargetOracle } from "../target-gate.js";
|
|
2
|
+
import type { RedirectResolutionReport } from "../ports.js";
|
|
3
|
+
import type { RedirectRecord, RedirectRequest, RedirectStatusCode } from "../types.js";
|
|
4
|
+
export interface SpaRedirectRequired {
|
|
5
|
+
rules: readonly RedirectRecord[];
|
|
6
|
+
oracle: RedirectTargetOracle;
|
|
7
|
+
reservedSegments: ReadonlySet<string>;
|
|
8
|
+
navigate(required: {
|
|
9
|
+
location: string;
|
|
10
|
+
statusCode: RedirectStatusCode;
|
|
11
|
+
redirectId: string;
|
|
12
|
+
}, optional?: Record<string, never>): void | Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface SpaRedirectResult extends RedirectResolutionReport {
|
|
15
|
+
navigated: boolean;
|
|
16
|
+
}
|
|
17
|
+
export type SpaRedirectGuard = (required: {
|
|
18
|
+
request: RedirectRequest;
|
|
19
|
+
}, optional?: Record<string, never>) => Promise<SpaRedirectResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Create a router guard from a snapshot of rules and required host security/navigation ports.
|
|
22
|
+
* @param required Rules, verified-origin oracle, reserved segments and navigation effect.
|
|
23
|
+
* @returns A guard reporting no match or refusal; only a gate-approved finished Location navigates.
|
|
24
|
+
* @throws Repository/oracle errors and navigation errors propagate to the router's error boundary.
|
|
25
|
+
* @complexity O(n) rule snapshot; calls inherit the root memory repo and bounded dynamic scan.
|
|
26
|
+
* @example const guard = createSpaRedirectGuard({ rules, oracle, reservedSegments, navigate }, {});
|
|
27
|
+
* await guard({ request: { workspaceId: "site", path: "/old", phase: "post_content" } }, {});
|
|
28
|
+
*/
|
|
29
|
+
export declare function createSpaRedirectGuard({ rules, oracle, reservedSegments, navigate }: SpaRedirectRequired, _optional?: Record<string, never>): SpaRedirectGuard;
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Framework-free SPA navigation through the same matcher and INV-03 gate as server resolution. */
|
|
2
|
+
import { redirectMatcher } from "../matcher.js";
|
|
3
|
+
import { InMemoryRedirectRepo } from "../repo.memory.js";
|
|
4
|
+
import { createRedirectResolver } from "../resolver.js";
|
|
5
|
+
/**
|
|
6
|
+
* Create a router guard from a snapshot of rules and required host security/navigation ports.
|
|
7
|
+
* @param required Rules, verified-origin oracle, reserved segments and navigation effect.
|
|
8
|
+
* @returns A guard reporting no match or refusal; only a gate-approved finished Location navigates.
|
|
9
|
+
* @throws Repository/oracle errors and navigation errors propagate to the router's error boundary.
|
|
10
|
+
* @complexity O(n) rule snapshot; calls inherit the root memory repo and bounded dynamic scan.
|
|
11
|
+
* @example const guard = createSpaRedirectGuard({ rules, oracle, reservedSegments, navigate }, {});
|
|
12
|
+
* await guard({ request: { workspaceId: "site", path: "/old", phase: "post_content" } }, {});
|
|
13
|
+
*/
|
|
14
|
+
export function createSpaRedirectGuard({ rules, oracle, reservedSegments, navigate }, _optional = {}) {
|
|
15
|
+
const repo = new InMemoryRedirectRepo({ seed: [...rules] }, {});
|
|
16
|
+
const resolver = createRedirectResolver({ repo, matcher: redirectMatcher, originRegistry: oracle, reservedSegments }, {});
|
|
17
|
+
return async ({ request }, _options = {}) => {
|
|
18
|
+
const report = await resolver.resolveWithRefusals(request, {});
|
|
19
|
+
if (!report.resolution.matched)
|
|
20
|
+
return { ...report, navigated: false };
|
|
21
|
+
const { location, statusCode, redirectId } = report.resolution;
|
|
22
|
+
// INV-03: resolver checks the interpolated Location before this effect is reachable.
|
|
23
|
+
await navigate({ location, statusCode, redirectId }, {});
|
|
24
|
+
return { ...report, navigated: true };
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { SqlRedirectRepo, redirectRepoFor, type RedirectSqlRequired, type RedirectTables } from "./repo.js";
|
|
2
|
+
export { toRecord, toRow, toRevision, toRevisionRow, updatableColumns, type RedirectRow, type RedirectRevisionRow } from "./repo.rows.js";
|
|
3
|
+
export { insertRedirectAndRevision, type RedirectDbHandle, type InsertRedirectAndRevisionRequired } from "../ports.internal.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { StorageKernel } from "@jini-ai/db/kernel";
|
|
2
|
+
import type { RedirectDbHandle } from "../ports.internal.js";
|
|
3
|
+
import type { RedirectRepoPort } from "../ports.js";
|
|
4
|
+
import type { ListRedirectsFilter, RedirectRecord, RedirectRevision } from "../types.js";
|
|
5
|
+
/**
|
|
6
|
+
* @file THE redirect repository: one Kysely query body for every database the storage kernel drives
|
|
7
|
+
* (SQLite, PGlite, Postgres). Satisfies `RedirectRepoPort` and `RedirectDbHandle`. Every statement
|
|
8
|
+
* goes through `kernel.run` and is awaited.
|
|
9
|
+
*
|
|
10
|
+
* `(workspace_id, from_pattern)` uniqueness is NOT re-enforced in `save()`: `redirects.ts`'s write
|
|
11
|
+
* chokepoint is the sole enforcer (errors.spec.md names it as `REDIRECT_CONFLICT`'s producer) and
|
|
12
|
+
* runs its `findByFromPattern` check inside `transaction()`, which takes the redirect write lock so
|
|
13
|
+
* a concurrent chokepoint cannot interleave between that check and the write. `insertRedirect` /
|
|
14
|
+
* `insertRevision` open no transaction of their own (ADR-PIPE-009 Decision A): atomicity is the
|
|
15
|
+
* caller's, and nested calls join it.
|
|
16
|
+
*/
|
|
17
|
+
/** Host-owned table names. No schema or product table defaults live in the adapter. */
|
|
18
|
+
export interface RedirectTables {
|
|
19
|
+
redirects: string;
|
|
20
|
+
revisions: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RedirectSqlRequired {
|
|
23
|
+
kernel: StorageKernel<unknown>;
|
|
24
|
+
tables: RedirectTables;
|
|
25
|
+
}
|
|
26
|
+
export declare class SqlRedirectRepo implements RedirectRepoPort, RedirectDbHandle {
|
|
27
|
+
protected readonly kernel: StorageKernel<unknown>;
|
|
28
|
+
protected readonly tables: RedirectTables;
|
|
29
|
+
constructor({ kernel, tables }: RedirectSqlRequired, _optional?: Record<string, never>);
|
|
30
|
+
/** Every row of a workspace, optionally narrowed to active rows of one match type (and override-only). */
|
|
31
|
+
private rowsOf;
|
|
32
|
+
findById(required: {
|
|
33
|
+
workspaceId: string;
|
|
34
|
+
id: string;
|
|
35
|
+
}, _optional?: Record<string, never>): Promise<RedirectRecord | null>;
|
|
36
|
+
lookupExact(required: {
|
|
37
|
+
workspaceId: string;
|
|
38
|
+
path: string;
|
|
39
|
+
includeOverrideOnly: boolean;
|
|
40
|
+
}, _optional?: Record<string, never>): Promise<RedirectRecord | null>;
|
|
41
|
+
lookupLongestPrefix(required: {
|
|
42
|
+
workspaceId: string;
|
|
43
|
+
path: string;
|
|
44
|
+
includeOverrideOnly: boolean;
|
|
45
|
+
}, _optional?: Record<string, never>): Promise<RedirectRecord | null>;
|
|
46
|
+
listDynamic(required: {
|
|
47
|
+
workspaceId: string;
|
|
48
|
+
includeOverrideOnly: boolean;
|
|
49
|
+
limit: number;
|
|
50
|
+
}, _optional?: Record<string, never>): Promise<RedirectRecord[]>;
|
|
51
|
+
list(filter: ListRedirectsFilter, _optional?: Record<string, never>): Promise<RedirectRecord[]>;
|
|
52
|
+
findByFromPattern(required: {
|
|
53
|
+
workspaceId: string;
|
|
54
|
+
fromPattern: string;
|
|
55
|
+
}, _optional?: Record<string, never>): Promise<RedirectRecord | null>;
|
|
56
|
+
save(required: {
|
|
57
|
+
record: RedirectRecord;
|
|
58
|
+
revision: RedirectRevision;
|
|
59
|
+
}, _optional?: Record<string, never>): Promise<void>;
|
|
60
|
+
tombstone(required: {
|
|
61
|
+
workspaceId: string;
|
|
62
|
+
id: string;
|
|
63
|
+
revision: RedirectRevision;
|
|
64
|
+
}, _optional?: Record<string, never>): Promise<void>;
|
|
65
|
+
/** Upsert-by-id; no transaction control of its own. */
|
|
66
|
+
insertRedirect(record: RedirectRecord, _optional?: Record<string, never>): Promise<void>;
|
|
67
|
+
/** Append-only insert; no transaction control of its own. */
|
|
68
|
+
insertRevision(revision: RedirectRevision, _optional?: Record<string, never>): Promise<void>;
|
|
69
|
+
/** Test-only helper: the append-only revision ledger for one redirect, in seq order. */
|
|
70
|
+
listRevisionsForTests({ redirectId }: {
|
|
71
|
+
redirectId: string;
|
|
72
|
+
}, _optional?: Record<string, never>): Promise<RedirectRevision[]>;
|
|
73
|
+
/**
|
|
74
|
+
* The chokepoint's transaction (C-001..004). Takes the redirect write lock first so its
|
|
75
|
+
* uniqueness / chain-loop reads and the write that follows are one serialized unit; nested calls
|
|
76
|
+
* (including `save`/`tombstone`) join it. `capture.ts` never calls this (Decision A).
|
|
77
|
+
*/
|
|
78
|
+
transaction<T>(fn: () => Promise<T>, _optional?: Record<string, never>): Promise<T>;
|
|
79
|
+
}
|
|
80
|
+
/** The redirect repo for `kernel`. */
|
|
81
|
+
export declare function redirectRepoFor(required: RedirectSqlRequired, _optional?: Record<string, never>): SqlRedirectRepo;
|
|
82
|
+
//# sourceMappingURL=repo.d.ts.map
|