@aglyn/plugins-redirects 1.0.0-beta.230 → 1.0.0-beta.231

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-redirects",
3
- "version": "1.0.0-beta.230",
3
+ "version": "1.0.0-beta.231",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,16 +25,16 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.230",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.230",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.230",
31
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.230",
32
- "@aglyn/shared-util-email": "1.0.0-beta.230",
33
- "@aglyn/shared-util-http": "1.0.0-beta.230",
34
- "@aglyn/shared-util-timestamp": "1.0.0-beta.230",
35
- "@aglyn/shared-util-tools": "1.0.0-beta.230",
36
- "@aglyn/tenant-data-admin": "1.0.0-beta.230",
37
- "@aglyn/tenant-feature-instance": "1.0.0-beta.230",
28
+ "@aglyn/aglyn": "1.0.0-beta.231",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.231",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.231",
31
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.231",
32
+ "@aglyn/shared-util-email": "1.0.0-beta.231",
33
+ "@aglyn/shared-util-http": "1.0.0-beta.231",
34
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.231",
35
+ "@aglyn/shared-util-tools": "1.0.0-beta.231",
36
+ "@aglyn/tenant-data-admin": "1.0.0-beta.231",
37
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.231",
38
38
  "@swc/helpers": "0.5.23"
39
39
  },
40
40
  "peerDependencies": {
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type PluginApiHandler } from '@aglyn/aglyn/server';
18
+ /** What a save is asked to write. */
19
+ export interface RedirectSaveDraft {
20
+ /** The rule being edited; absent for a new rule. */
21
+ id?: string;
22
+ kind?: string;
23
+ source: string;
24
+ destination: string;
25
+ statusCode?: number;
26
+ }
27
+ /** The answer: the page's refusal in its own words, or the rule as the page would store it. */
28
+ export type RedirectSaveCheck = {
29
+ ok: false;
30
+ problem: string;
31
+ } | {
32
+ ok: true;
33
+ source: string;
34
+ destination: string;
35
+ statusCode: number;
36
+ kind: string;
37
+ notice: string | null;
38
+ };
39
+ type Rule = {
40
+ $id: string;
41
+ source?: string;
42
+ destination?: string;
43
+ kind?: string;
44
+ deletedAt?: unknown;
45
+ };
46
+ /**
47
+ * The Redirects page's save checks, in its order and its words
48
+ * (`redirects-console-page.tsx` `handleSave`): the shared rule validation
49
+ * (match mode, from-path or pattern, destination, another company's address),
50
+ * a path redirected to itself, a duplicate rule, a chain that loops back, and
51
+ * the published-page notice that warns without refusing. One source of truth
52
+ * for every client that saves a rule: the native apps ask it here rather than
53
+ * carry their own copy of the phishing screen and the pattern engine.
54
+ */
55
+ export declare function checkRedirectSave(draft: RedirectSaveDraft, host: {
56
+ cname?: unknown;
57
+ subdomain?: unknown;
58
+ screens?: unknown;
59
+ }, rules: readonly Rule[]): RedirectSaveCheck;
60
+ /**
61
+ * `POST /api/redirects/check`: runs {@link checkRedirectSave} for a member
62
+ * who may publish on the site (the role the Firestore rules gate redirect
63
+ * writes on). It reads and answers; it writes nothing, so the save itself
64
+ * stays the same write the page makes, under the same rules.
65
+ */
66
+ export declare const redirectCheckHandler: PluginApiHandler;
67
+ export {};
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
18
+ import { TENANT_APEX } from "@aglyn/aglyn/app-utils/tenant-apex";
19
+ import { findDuplicateRedirect, isSelfRedirect, normalizeRedirectDestination, normalizeRedirectSource, redirectSourceIsLivePage, REDIRECT_STATUS_CODES, validateRedirectRule, walkRedirectChain } from "../model/redirects.js";
20
+ /**
21
+ * The Redirects page's save checks, in its order and its words
22
+ * (`redirects-console-page.tsx` `handleSave`): the shared rule validation
23
+ * (match mode, from-path or pattern, destination, another company's address),
24
+ * a path redirected to itself, a duplicate rule, a chain that loops back, and
25
+ * the published-page notice that warns without refusing. One source of truth
26
+ * for every client that saves a rule: the native apps ask it here rather than
27
+ * carry their own copy of the phishing screen and the pattern engine.
28
+ */ export function checkRedirectSave(draft, host, rules) {
29
+ const kind = draft.kind || 'exact';
30
+ const problem = validateRedirectRule({
31
+ kind,
32
+ source: draft.source,
33
+ destination: draft.destination
34
+ }, {
35
+ ownDomains: [
36
+ typeof host.cname === 'string' ? host.cname : null,
37
+ typeof host.subdomain === 'string' && host.subdomain ? `${host.subdomain}.${TENANT_APEX}` : null
38
+ ]
39
+ });
40
+ if (problem) return {
41
+ ok: false,
42
+ problem
43
+ };
44
+ const source = kind === 'regex' ? draft.source.trim() : normalizeRedirectSource(draft.source);
45
+ const destination = normalizeRedirectDestination(draft.destination);
46
+ if (kind !== 'regex' && isSelfRedirect({
47
+ source,
48
+ destination
49
+ })) {
50
+ return {
51
+ ok: false,
52
+ problem: 'That would redirect the path to itself'
53
+ };
54
+ }
55
+ const live = rules.filter((rule)=>!rule.deletedAt);
56
+ if (findDuplicateRedirect(live, {
57
+ $id: draft.id,
58
+ source,
59
+ kind
60
+ })) {
61
+ return {
62
+ ok: false,
63
+ problem: `A rule for ${source} already exists`
64
+ };
65
+ }
66
+ if (walkRedirectChain(live, {
67
+ $id: draft.id,
68
+ source,
69
+ destination
70
+ }).loop) {
71
+ return {
72
+ ok: false,
73
+ problem: 'That destination chains back to this rule — a redirect loop'
74
+ };
75
+ }
76
+ const statusCode = REDIRECT_STATUS_CODES.includes(Number(draft.statusCode)) ? Number(draft.statusCode) : 302;
77
+ const notice = redirectSourceIsLivePage(host.screens, source) ? `${source} is a published page — the redirect takes precedence` : null;
78
+ return {
79
+ ok: true,
80
+ source,
81
+ destination,
82
+ statusCode,
83
+ kind,
84
+ notice
85
+ };
86
+ }
87
+ /** Rules a check reads, past the page's 200-rule window, so a duplicate or loop beyond it is still caught. */ const RULES_WINDOW = 500;
88
+ /**
89
+ * `POST /api/redirects/check`: runs {@link checkRedirectSave} for a member
90
+ * who may publish on the site (the role the Firestore rules gate redirect
91
+ * writes on). It reads and answers; it writes nothing, so the save itself
92
+ * stays the same write the page makes, under the same rules.
93
+ */ export const redirectCheckHandler = async (req, res)=>{
94
+ var _req_headers_authorization, _req_body, _body_hostId, _body_source, _body_destination;
95
+ if (req.method !== 'POST') return res.status(405).json({
96
+ error: 'Method not allowed'
97
+ });
98
+ const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
99
+ const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : '';
100
+ if (!idToken) return res.status(401).json({
101
+ error: 'Unauthenticated'
102
+ });
103
+ const body = typeof req.body === 'string' ? JSON.parse(req.body || '{}') : (_req_body = req.body) != null ? _req_body : {};
104
+ const hostId = String((_body_hostId = body.hostId) != null ? _body_hostId : '');
105
+ if (!hostId || /^__.*__$/.test(hostId) || hostId.includes('/')) return res.status(400).json({
106
+ error: 'Missing hostId'
107
+ });
108
+ const draft = {
109
+ id: typeof body.id === 'string' && body.id ? body.id : undefined,
110
+ kind: typeof body.kind === 'string' ? body.kind : undefined,
111
+ source: String((_body_source = body.source) != null ? _body_source : '').slice(0, 2000),
112
+ destination: String((_body_destination = body.destination) != null ? _body_destination : '').slice(0, 2000),
113
+ statusCode: Number(body.statusCode)
114
+ };
115
+ try {
116
+ var _host_get, _host_data;
117
+ const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
118
+ const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId);
119
+ const host = await hostRef.get();
120
+ if (!host.exists) return res.status(404).json({
121
+ error: 'Unknown site'
122
+ });
123
+ const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[decoded.uid];
124
+ if (role !== 'admin' && role !== 'editor') {
125
+ return res.status(403).json({
126
+ error: 'Changing a redirect needs a publishing role — ask an editor or admin'
127
+ });
128
+ }
129
+ const snapshot = await hostRef.collection('redirects').limit(RULES_WINDOW).get();
130
+ const rules = snapshot.docs.map((doc)=>_extends({
131
+ $id: doc.id
132
+ }, doc.data()));
133
+ return res.status(200).json(checkRedirectSave(draft, (_host_data = host.data()) != null ? _host_data : {}, rules));
134
+ } catch (error) {
135
+ console.error('[redirects/check]', error);
136
+ return res.status(500).json({
137
+ error: 'The redirect could not be checked'
138
+ });
139
+ }
140
+ };
141
+
142
+ //# sourceMappingURL=redirect-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/redirects/src/lib/server/redirect-check.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { TENANT_APEX } from '@aglyn/aglyn/app-utils/tenant-apex'\nimport {\n findDuplicateRedirect,\n isSelfRedirect,\n normalizeRedirectDestination,\n normalizeRedirectSource,\n redirectSourceIsLivePage,\n REDIRECT_STATUS_CODES,\n validateRedirectRule,\n walkRedirectChain,\n} from '../model/redirects'\n\n/** What a save is asked to write. */\nexport interface RedirectSaveDraft {\n /** The rule being edited; absent for a new rule. */\n id?: string\n kind?: string\n source: string\n destination: string\n statusCode?: number\n}\n\n/** The answer: the page's refusal in its own words, or the rule as the page would store it. */\nexport type RedirectSaveCheck =\n | { ok: false; problem: string }\n | { ok: true; source: string; destination: string; statusCode: number; kind: string; notice: string | null }\n\ntype Rule = { $id: string; source?: string; destination?: string; kind?: string; deletedAt?: unknown }\n\n/**\n * The Redirects page's save checks, in its order and its words\n * (`redirects-console-page.tsx` `handleSave`): the shared rule validation\n * (match mode, from-path or pattern, destination, another company's address),\n * a path redirected to itself, a duplicate rule, a chain that loops back, and\n * the published-page notice that warns without refusing. One source of truth\n * for every client that saves a rule: the native apps ask it here rather than\n * carry their own copy of the phishing screen and the pattern engine.\n */\nexport function checkRedirectSave(\n draft: RedirectSaveDraft,\n host: { cname?: unknown; subdomain?: unknown; screens?: unknown },\n rules: readonly Rule[],\n): RedirectSaveCheck {\n const kind = draft.kind || 'exact'\n const problem = validateRedirectRule(\n { kind, source: draft.source, destination: draft.destination },\n {\n ownDomains: [\n typeof host.cname === 'string' ? host.cname : null,\n typeof host.subdomain === 'string' && host.subdomain ? `${host.subdomain}.${TENANT_APEX}` : null,\n ],\n },\n )\n if (problem) return { ok: false, problem }\n const source = kind === 'regex' ? draft.source.trim() : (normalizeRedirectSource(draft.source) as string)\n const destination = normalizeRedirectDestination(draft.destination) as string\n if (kind !== 'regex' && isSelfRedirect({ source, destination })) {\n return { ok: false, problem: 'That would redirect the path to itself' }\n }\n const live = rules.filter((rule) => !rule.deletedAt)\n if (findDuplicateRedirect(live as never, { $id: draft.id, source, kind })) {\n return { ok: false, problem: `A rule for ${source} already exists` }\n }\n if (walkRedirectChain(live as never, { $id: draft.id, source, destination }).loop) {\n return { ok: false, problem: 'That destination chains back to this rule — a redirect loop' }\n }\n const statusCode = (REDIRECT_STATUS_CODES as readonly number[]).includes(Number(draft.statusCode))\n ? Number(draft.statusCode)\n : 302\n const notice = redirectSourceIsLivePage(host.screens as never, source)\n ? `${source} is a published page — the redirect takes precedence`\n : null\n return { ok: true, source, destination, statusCode, kind, notice }\n}\n\n/** Rules a check reads, past the page's 200-rule window, so a duplicate or loop beyond it is still caught. */\nconst RULES_WINDOW = 500\n\n/**\n * `POST /api/redirects/check`: runs {@link checkRedirectSave} for a member\n * who may publish on the site (the role the Firestore rules gate redirect\n * writes on). It reads and answers; it writes nothing, so the save itself\n * stays the same write the page makes, under the same rules.\n */\nexport const redirectCheckHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n const body = typeof req.body === 'string' ? JSON.parse(req.body || '{}') : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n if (!hostId || /^__.*__$/.test(hostId) || hostId.includes('/')) return res.status(400).json({ error: 'Missing hostId' })\n const draft: RedirectSaveDraft = {\n id: typeof body.id === 'string' && body.id ? body.id : undefined,\n kind: typeof body.kind === 'string' ? body.kind : undefined,\n source: String(body.source ?? '').slice(0, 2000),\n destination: String(body.destination ?? '').slice(0, 2000),\n statusCode: Number(body.statusCode),\n }\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const role = (host.get('memberRoles') ?? {})[decoded.uid]\n if (role !== 'admin' && role !== 'editor') {\n return res.status(403).json({ error: 'Changing a redirect needs a publishing role — ask an editor or admin' })\n }\n const snapshot = await hostRef.collection('redirects').limit(RULES_WINDOW).get()\n const rules = snapshot.docs.map((doc: { id: string; data: () => Record<string, unknown> }) => ({ $id: doc.id, ...doc.data() }))\n return res.status(200).json(checkRedirectSave(draft, host.data() ?? {}, rules))\n } catch (error) {\n console.error('[redirects/check]', error)\n return res.status(500).json({ error: 'The redirect could not be checked' })\n }\n}\n"],"names":["firebaseAdmin","TENANT_APEX","findDuplicateRedirect","isSelfRedirect","normalizeRedirectDestination","normalizeRedirectSource","redirectSourceIsLivePage","REDIRECT_STATUS_CODES","validateRedirectRule","walkRedirectChain","checkRedirectSave","draft","host","rules","kind","problem","source","destination","ownDomains","cname","subdomain","ok","trim","live","filter","rule","deletedAt","$id","id","loop","statusCode","includes","Number","notice","screens","RULES_WINDOW","redirectCheckHandler","req","res","body","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","JSON","parse","hostId","test","undefined","decoded","app","auth","verifyIdToken","hostRef","firestore","collection","doc","get","exists","role","uid","snapshot","limit","docs","map","data","console"],"mappings":"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"}
@@ -21,3 +21,9 @@
21
21
  * the plugin behind the core site-page hook.
22
22
  */
23
23
  export declare function registerRedirectsApi(): void;
24
+ /**
25
+ * The redirects plugin's CONSOLE API: `POST /api/redirects/check`, the
26
+ * Redirects page's save checks for the native apps (`server/redirect-check.ts`).
27
+ * The handler loads with its first call, so neither boot pays for it.
28
+ */
29
+ export declare function registerRedirectsConsoleApi(): void;
package/src/lib/server.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ import { registerSiteRedirectResolver } from "@aglyn/aglyn/server";
16
+ */ import { registerPluginApiRoute, registerSiteRedirectResolver } from "@aglyn/aglyn/server";
17
17
  import { resolveRedirect } from "./server/resolve-redirect.js";
18
18
  /**
19
19
  * Registers the redirects plugin's tenant-side hooks (AGL-418): the rule
@@ -29,5 +29,12 @@ import { resolveRedirect } from "./server/resolve-redirect.js";
29
29
  } : undefined;
30
30
  });
31
31
  }
32
+ /**
33
+ * The redirects plugin's CONSOLE API: `POST /api/redirects/check`, the
34
+ * Redirects page's save checks for the native apps (`server/redirect-check.ts`).
35
+ * The handler loads with its first call, so neither boot pays for it.
36
+ */ export function registerRedirectsConsoleApi() {
37
+ registerPluginApiRoute('redirects/check', async (req, res)=>(await import("./server/redirect-check.js")).redirectCheckHandler(req, res));
38
+ }
32
39
 
33
40
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/redirects/src/lib/server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { registerSiteRedirectResolver } from '@aglyn/aglyn/server'\nimport { resolveRedirect } from './server/resolve-redirect'\n\n/**\n * Registers the redirects plugin's tenant-side hooks (AGL-418): the rule\n * engine (AGL-155) answers site requests with a redirect before any route\n * resolution — relocated from the tenant app's resolve-redirect util into\n * the plugin behind the core site-page hook.\n */\nexport function registerRedirectsApi(): void {\n registerSiteRedirectResolver(async ({ host, path }) => {\n const rule = await resolveRedirect(host, path)\n return rule\n ? { destination: rule.destination, statusCode: rule.statusCode }\n : undefined\n })\n}\n"],"names":["registerSiteRedirectResolver","resolveRedirect","registerRedirectsApi","host","path","rule","destination","statusCode","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,4BAA4B,QAAQ,sBAAqB;AAClE,SAASC,eAAe,QAAQ,+BAA2B;AAE3D;;;;;CAKC,GACD,OAAO,SAASC;IACdF,6BAA6B,OAAO,EAAEG,IAAI,EAAEC,IAAI,EAAE;QAChD,MAAMC,OAAO,MAAMJ,gBAAgBE,MAAMC;QACzC,OAAOC,OACH;YAAEC,aAAaD,KAAKC,WAAW;YAAEC,YAAYF,KAAKE,UAAU;QAAC,IAC7DC;IACN;AACF"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/redirects/src/lib/server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { registerPluginApiRoute, registerSiteRedirectResolver } from '@aglyn/aglyn/server'\nimport { resolveRedirect } from './server/resolve-redirect'\n\n/**\n * Registers the redirects plugin's tenant-side hooks (AGL-418): the rule\n * engine (AGL-155) answers site requests with a redirect before any route\n * resolution — relocated from the tenant app's resolve-redirect util into\n * the plugin behind the core site-page hook.\n */\nexport function registerRedirectsApi(): void {\n registerSiteRedirectResolver(async ({ host, path }) => {\n const rule = await resolveRedirect(host, path)\n return rule\n ? { destination: rule.destination, statusCode: rule.statusCode }\n : undefined\n })\n}\n\n/**\n * The redirects plugin's CONSOLE API: `POST /api/redirects/check`, the\n * Redirects page's save checks for the native apps (`server/redirect-check.ts`).\n * The handler loads with its first call, so neither boot pays for it.\n */\nexport function registerRedirectsConsoleApi(): void {\n registerPluginApiRoute('redirects/check', async (req, res) =>\n (await import('./server/redirect-check')).redirectCheckHandler(req, res),\n )\n}\n"],"names":["registerPluginApiRoute","registerSiteRedirectResolver","resolveRedirect","registerRedirectsApi","host","path","rule","destination","statusCode","undefined","registerRedirectsConsoleApi","req","res","redirectCheckHandler"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,sBAAsB,EAAEC,4BAA4B,QAAQ,sBAAqB;AAC1F,SAASC,eAAe,QAAQ,+BAA2B;AAE3D;;;;;CAKC,GACD,OAAO,SAASC;IACdF,6BAA6B,OAAO,EAAEG,IAAI,EAAEC,IAAI,EAAE;QAChD,MAAMC,OAAO,MAAMJ,gBAAgBE,MAAMC;QACzC,OAAOC,OACH;YAAEC,aAAaD,KAAKC,WAAW;YAAEC,YAAYF,KAAKE,UAAU;QAAC,IAC7DC;IACN;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASC;IACdV,uBAAuB,mBAAmB,OAAOW,KAAKC,MACpD,AAAC,CAAA,MAAM,MAAM,CAAC,6BAAyB,EAAGC,oBAAoB,CAACF,KAAKC;AAExE"}