@botiverse/porthole 0.2.0 → 0.2.2
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/dist/conformance/entry.d.ts +8 -2
- package/dist/conformance/entry.js +23 -8
- package/dist/conformance/harness.js +4 -4
- package/dist/conformance/host-kit.d.ts +8 -3
- package/dist/conformance/host-kit.js +58 -23
- package/dist/conformance/limits.js +6 -0
- package/dist/conformance/served.d.ts +4 -4
- package/dist/conformance/served.js +26 -16
- package/dist/guide/guide.js +4 -4
- package/dist/guide/limits.js +1 -1
- package/dist/node/index.d.ts +11 -3
- package/dist/node/index.js +70 -10
- package/dist/server/host.d.ts +42 -8
- package/dist/server/host.js +35 -5
- package/dist/server/index.d.ts +3 -1
- package/dist/server/pages.js +15 -1
- package/dist/server/proxy.js +2 -0
- package/dist/server/server.js +9 -2
- package/dist/server/types.d.ts +15 -6
- package/docs/ARCHITECTURE.md +277 -0
- package/docs/DECISIONS.md +1774 -0
- package/docs/DESIGN.md +1067 -0
- package/docs/OBJECTIVES.md +116 -0
- package/docs/PRINCIPLES.md +155 -0
- package/package.json +4 -3
- package/src/conformance/entry.ts +39 -10
- package/src/conformance/host-kit.ts +71 -28
- package/src/conformance/limits.ts +8 -0
- package/src/conformance/served.ts +34 -21
- package/src/guide/guide.ts +4 -4
- package/src/guide/limits.ts +1 -1
- package/src/node/index.ts +73 -10
- package/src/server/host.ts +84 -14
- package/src/server/index.ts +4 -0
- package/src/server/pages.ts +15 -1
- package/src/server/proxy.ts +2 -0
- package/src/server/server.ts +11 -2
- package/src/server/types.ts +18 -7
package/dist/node/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { request as httpRequest } from "node:http";
|
|
2
2
|
import { request as httpsRequest } from "node:https";
|
|
3
3
|
import { Readable } from "node:stream";
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The Node request as a fetch `Request`: an absolute-form target kept as it came (P056). `signal`
|
|
6
|
+
* (`nodeHost` gives one, aborted when the client goes away) lets an upstream stop its work.
|
|
7
|
+
*/
|
|
8
|
+
export function toRequest(request, scheme, signal) {
|
|
6
9
|
const headers = new Headers();
|
|
7
10
|
for (const [name, value] of Object.entries(request.headers)) {
|
|
8
11
|
for (const each of Array.isArray(value) ? value : value === undefined ? [] : [value]) {
|
|
@@ -15,9 +18,20 @@ export function toRequest(request, scheme) {
|
|
|
15
18
|
: target;
|
|
16
19
|
const method = request.method ?? "GET";
|
|
17
20
|
const body = method === "GET" || method === "HEAD" ? null : Readable.toWeb(request);
|
|
18
|
-
return new Request(url, {
|
|
21
|
+
return new Request(url, {
|
|
22
|
+
method,
|
|
23
|
+
headers,
|
|
24
|
+
body,
|
|
25
|
+
duplex: "half",
|
|
26
|
+
...(signal === undefined ? {} : { signal }),
|
|
27
|
+
});
|
|
19
28
|
}
|
|
20
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Writes a fetch `Response` to a Node response, cookies one header each (P070): it waits for the
|
|
31
|
+
* socket to drain before reading on (a slow client never makes it buffer the body), cancels the
|
|
32
|
+
* body when the client goes away (an event stream ends with the page that asked for it), and on
|
|
33
|
+
* an error mid-body destroys the response rather than end it looking complete.
|
|
34
|
+
*/
|
|
21
35
|
export async function sendResponse(response, answer) {
|
|
22
36
|
const headers = {};
|
|
23
37
|
for (const [name, value] of answer.headers) {
|
|
@@ -34,10 +48,46 @@ export async function sendResponse(response, answer) {
|
|
|
34
48
|
response.end();
|
|
35
49
|
return;
|
|
36
50
|
}
|
|
37
|
-
|
|
38
|
-
|
|
51
|
+
const reader = answer.body.getReader();
|
|
52
|
+
let gone = false;
|
|
53
|
+
const leave = () => {
|
|
54
|
+
gone = true;
|
|
55
|
+
reader.cancel().catch(() => undefined);
|
|
56
|
+
};
|
|
57
|
+
// The client went away before the end: stop reading, and let the body's source know.
|
|
58
|
+
response.once("close", () => {
|
|
59
|
+
if (!response.writableFinished) {
|
|
60
|
+
leave();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
for (;;) {
|
|
65
|
+
const { done, value } = await reader.read();
|
|
66
|
+
if (done || gone) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
if (!response.write(value)) {
|
|
70
|
+
await new Promise((resolve) => {
|
|
71
|
+
const on = () => {
|
|
72
|
+
response.off("drain", on);
|
|
73
|
+
response.off("close", on);
|
|
74
|
+
resolve();
|
|
75
|
+
};
|
|
76
|
+
response.on("drain", on);
|
|
77
|
+
response.on("close", on);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// The head is sent: a body cut short must not look whole.
|
|
84
|
+
leave();
|
|
85
|
+
response.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!gone) {
|
|
89
|
+
response.end();
|
|
39
90
|
}
|
|
40
|
-
response.end();
|
|
41
91
|
}
|
|
42
92
|
/** Whether a Node request's Host (or absolute-form target) is under the content domain. */
|
|
43
93
|
function forContent(content, request) {
|
|
@@ -67,7 +117,15 @@ export function nodeHost(content, app) {
|
|
|
67
117
|
app.request(request, response);
|
|
68
118
|
return;
|
|
69
119
|
}
|
|
70
|
-
|
|
120
|
+
// Aborted when the client goes away first, so an upstream (a proxied app's event stream)
|
|
121
|
+
// stops with it.
|
|
122
|
+
const gone = new AbortController();
|
|
123
|
+
response.once("close", () => {
|
|
124
|
+
if (!response.writableFinished) {
|
|
125
|
+
gone.abort();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
content.fetch(toRequest(request, scheme(), gone.signal)).then((answer) => {
|
|
71
129
|
if (answer === null) {
|
|
72
130
|
// A label the app routes itself.
|
|
73
131
|
app.request(request, response);
|
|
@@ -75,9 +133,11 @@ export function nodeHost(content, app) {
|
|
|
75
133
|
}
|
|
76
134
|
return sendResponse(response, answer);
|
|
77
135
|
}, () => {
|
|
78
|
-
if (
|
|
79
|
-
response.
|
|
136
|
+
if (response.headersSent) {
|
|
137
|
+
response.destroy();
|
|
138
|
+
return;
|
|
80
139
|
}
|
|
140
|
+
response.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
|
|
81
141
|
response.end("Content couldn't be served.");
|
|
82
142
|
});
|
|
83
143
|
},
|
package/dist/server/host.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface HealthOptions {
|
|
|
21
21
|
/** How the check is fetched (default: `fetch`; on Node, `nodeProbe()`). */
|
|
22
22
|
readonly probe?: HealthProbe;
|
|
23
23
|
readonly timeoutMs?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Told each time the gate's state changes (P070): checking to healthy or unhealthy, and every
|
|
26
|
+
* flip between them, with the new view. `meta().served` changes with it, so an app that tells
|
|
27
|
+
* its clients whether frames are served pushes it from here.
|
|
28
|
+
*/
|
|
29
|
+
readonly onChange?: (view: ContentHealthView) => void;
|
|
24
30
|
}
|
|
25
31
|
/** Where content stands, for the app's own meta and logs. */
|
|
26
32
|
export interface ContentHealthView {
|
|
@@ -64,7 +70,13 @@ export interface ContentHostOptions<V> {
|
|
|
64
70
|
* that says which scope a label names (`prefixedName` does).
|
|
65
71
|
*/
|
|
66
72
|
readonly name?: Naming;
|
|
67
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Labels the app itself serves on the content domain, which `fetch` hands back (null) so the
|
|
75
|
+
* app's own routes answer there: Ferry's `p-*` previews, another server's namespace it routes
|
|
76
|
+
* on. Not for labels nothing should answer: a label the naming never makes, the bare domain
|
|
77
|
+
* and an address `entry.at` says names nothing all get Porthole's 404, with nothing to list
|
|
78
|
+
* (P070). The health label is reserved by the gate itself.
|
|
79
|
+
*/
|
|
68
80
|
readonly reserved?: readonly string[] | ((label: string) => boolean);
|
|
69
81
|
/**
|
|
70
82
|
* Policy 1, mapping: the instance a request proposes (`{ instance }`), as the app means it,
|
|
@@ -105,17 +117,39 @@ export interface EntryOptions<V> {
|
|
|
105
117
|
/** The app's authorize route (`ContentHost.authorize`), an absolute URL on an app origin. */
|
|
106
118
|
readonly authorizeUrl: string;
|
|
107
119
|
/**
|
|
108
|
-
* Policy 1 for a clean address: the instance a scope's path means, or null
|
|
109
|
-
* the address's pathname; its query rides along but never chooses the
|
|
120
|
+
* Policy 1 for a clean address: the instance a scope's path means, or null/undefined: nothing
|
|
121
|
+
* (404). The path is the address's pathname; its query rides along but never chooses the
|
|
122
|
+
* instance. Asked twice: on the content origin, with no viewer, whether the address names
|
|
123
|
+
* anything at all (nothing: Porthole's 404 there, never the app's sign-in, P070); then at the
|
|
124
|
+
* authorize route with the viewer. Say what exists the same way for both; `access` says who
|
|
125
|
+
* may see it.
|
|
110
126
|
*/
|
|
111
|
-
readonly at: (scope: string, path: string, viewer: V | null, request: Request) => Awaitable<InstanceRef | null>;
|
|
127
|
+
readonly at: (scope: string, path: string, viewer: V | null, request: Request) => Awaitable<InstanceRef | null | undefined>;
|
|
112
128
|
/**
|
|
113
129
|
* The app's own sign-in, which comes back to `returnTo` (the authorize URL) once signed in.
|
|
114
|
-
* Only ever on a tab: in a frame, the person is asked to open a new tab instead.
|
|
130
|
+
* Only ever on a tab: in a frame, the person is asked to open a new tab instead. `entry` is the
|
|
131
|
+
* address being opened, decoded (P070).
|
|
115
132
|
*/
|
|
116
|
-
readonly signIn: (returnTo: string, request: Request) => Awaitable<Response>;
|
|
117
|
-
/**
|
|
118
|
-
|
|
133
|
+
readonly signIn: (returnTo: string, request: Request, entry: EntryContext) => Awaitable<Response>;
|
|
134
|
+
/**
|
|
135
|
+
* The app's refusal page on a tab (a frame gets Porthole's plain one), for the address in
|
|
136
|
+
* `entry`. Nothing (null, undefined): Porthole's own (P070).
|
|
137
|
+
*/
|
|
138
|
+
readonly refusal?: (status: 403 | 404, request: Request, entry: EntryContext) => Awaitable<Response | null | undefined>;
|
|
139
|
+
}
|
|
140
|
+
/** The clean address a redirect entry is opening, as its authorize route decoded it (P070). */
|
|
141
|
+
export interface EntryContext {
|
|
142
|
+
/** The address's label, and the scope it names (null: one the naming never makes). */
|
|
143
|
+
readonly label: string;
|
|
144
|
+
readonly scope: string | null;
|
|
145
|
+
/** The path (and query) asked for. */
|
|
146
|
+
readonly path: string;
|
|
147
|
+
/** The clean address itself: where the person comes back to. */
|
|
148
|
+
readonly url: string;
|
|
149
|
+
/** The instance the path names for this viewer (`entry.at`), or null. */
|
|
150
|
+
readonly instance: InstanceRef | null;
|
|
151
|
+
/** Whether it was asked from a frame. */
|
|
152
|
+
readonly framed: boolean;
|
|
119
153
|
}
|
|
120
154
|
export interface ContentHost<V> {
|
|
121
155
|
/** The parsed content domain, or null (off, or unusable settings under the fallback). */
|
package/dist/server/host.js
CHANGED
|
@@ -88,7 +88,19 @@ export function createContentHost(options) {
|
|
|
88
88
|
...(options.upstream === undefined ? {} : { upstream: options.upstream }),
|
|
89
89
|
...(options.entry === undefined
|
|
90
90
|
? {}
|
|
91
|
-
: {
|
|
91
|
+
: {
|
|
92
|
+
entry: {
|
|
93
|
+
authorizeUrl: options.entry.authorizeUrl,
|
|
94
|
+
// Whether a clean address names anything, before a browser is sent away (P070).
|
|
95
|
+
named: async (label, path, request) => {
|
|
96
|
+
const scope = options.name?.scopeOf?.(label) ?? null;
|
|
97
|
+
const at = options.entry?.at;
|
|
98
|
+
return (scope !== null &&
|
|
99
|
+
at !== undefined &&
|
|
100
|
+
(await at(scope, path, null, request)) != null);
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
92
104
|
...(options.authorize === undefined
|
|
93
105
|
? {}
|
|
94
106
|
: {
|
|
@@ -173,6 +185,14 @@ export function createContentHost(options) {
|
|
|
173
185
|
? { evt: "porthole.content.healthy" }
|
|
174
186
|
: { evt: "porthole.content.unhealthy", reason });
|
|
175
187
|
}
|
|
188
|
+
if (before.state !== view.state) {
|
|
189
|
+
try {
|
|
190
|
+
gate.onChange?.(view);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// The app's listener failing changes nothing here.
|
|
194
|
+
}
|
|
195
|
+
}
|
|
176
196
|
running = null;
|
|
177
197
|
schedule();
|
|
178
198
|
return view;
|
|
@@ -356,18 +376,28 @@ export function createContentHost(options) {
|
|
|
356
376
|
try {
|
|
357
377
|
const instance = scope === null
|
|
358
378
|
? null
|
|
359
|
-
: await entry.at(scope, new URL(state.path, "https://x.invalid").pathname, viewer, request);
|
|
379
|
+
: ((await entry.at(scope, new URL(state.path, "https://x.invalid").pathname, viewer, request)) ?? null);
|
|
360
380
|
const grant = instance === null ? null : await options.access(instance, viewer, request);
|
|
381
|
+
// The address being opened, decoded, for the app's sign-in and refusal pages.
|
|
382
|
+
const context = {
|
|
383
|
+
label: state.label,
|
|
384
|
+
scope,
|
|
385
|
+
path: state.path,
|
|
386
|
+
url: clean,
|
|
387
|
+
instance,
|
|
388
|
+
framed,
|
|
389
|
+
};
|
|
361
390
|
if (instance === null || grant === null) {
|
|
362
391
|
if (viewer === null) {
|
|
363
392
|
// The app's sign-in never shows in a frame: the person opens a tab for it.
|
|
364
393
|
return framed
|
|
365
394
|
? appPage(401, "Open this in a new tab to sign in", "Signing in doesn't work inside a frame.", "sign-in", method)
|
|
366
|
-
: await entry.signIn(request.url, request);
|
|
395
|
+
: await entry.signIn(request.url, request, context);
|
|
367
396
|
}
|
|
368
397
|
const status = instance === null ? 404 : 403;
|
|
369
|
-
|
|
370
|
-
|
|
398
|
+
const own = framed ? null : await entry.refusal?.(status, request, context);
|
|
399
|
+
if (own !== null && own !== undefined) {
|
|
400
|
+
return own;
|
|
371
401
|
}
|
|
372
402
|
return status === 404
|
|
373
403
|
? appPage(404, "Not found", "There is nothing here.", "not-found", method)
|
package/dist/server/index.d.ts
CHANGED
|
@@ -33,10 +33,12 @@
|
|
|
33
33
|
* content without a valid ticket or cookie gets nothing of the content.
|
|
34
34
|
*/
|
|
35
35
|
export { createServer } from "./server.ts";
|
|
36
|
-
export { createContentHost, HEALTH_PATH, type ContentHealthView, type ContentHost, type ContentHostEvent, type ContentHostOptions, type EntryOptions, type ContentMeta, type HealthOptions, type HealthProbe, } from "./host.ts";
|
|
36
|
+
export { createContentHost, HEALTH_PATH, type ContentHealthView, type ContentHost, type ContentHostEvent, type ContentHostOptions, type EntryContext, type EntryOptions, type ContentMeta, type HealthOptions, type HealthProbe, } from "./host.ts";
|
|
37
37
|
export { clashOf, contentDomain, ContentDomainError, hostnameOf, type ContentDomainInfo, type ContentDomainProblem, } from "./domain.ts";
|
|
38
38
|
export { d1TicketSchema, d1TicketStore, memoryTicketStore, type D1Like, type D1StatementLike, } from "./tickets.ts";
|
|
39
39
|
export { FRAME_STATUS_METHOD } from "../isolation/types.ts";
|
|
40
40
|
export { isHostLabel, prefixedName, type Naming } from "./naming.ts";
|
|
41
|
+
export type { PortholeConfig } from "../config/schema.ts";
|
|
42
|
+
export type { FrameLocation, InstanceRef, ServedDocument } from "../isolation/types.ts";
|
|
41
43
|
export { proxyTo, type ProxyOptions } from "./proxy.ts";
|
|
42
44
|
export type { Access, ArtifactStore, AuthorizeAnswer, AuthorizeRequest, Authorizer, ContentDomain, EntryState, Grant, PortholeServer, ServerOptions, TicketRecord, TicketStore, Upstream, } from "./types.ts";
|
package/dist/server/pages.js
CHANGED
|
@@ -14,7 +14,21 @@ export const STATUS_SCRIPT_PATH = "/__porthole/status.js";
|
|
|
14
14
|
export const ENTER_SCRIPT_PATH = "/__porthole/enter.js";
|
|
15
15
|
/** Answers 204 when the request carries a good session for its origin's app, 401 otherwise. */
|
|
16
16
|
export const SESSION_PATH = "/__porthole/session";
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Tells the page that frames this one how loading went, at each web host origin. A desktop app's
|
|
19
|
+
* own scheme (`ferry://app`) is no target origin a browser takes (postMessage throws), so where
|
|
20
|
+
* the host origins hold one, and the parent isn't known to be a web host origin, the word goes to
|
|
21
|
+
* the parent as `*`: these pages' `frame-ancestors` already hold their parent to the host origins,
|
|
22
|
+
* and what they say is no secret (P070). One origin failing never stops the page.
|
|
23
|
+
*/
|
|
24
|
+
const say = () => `const say = (params) => {
|
|
25
|
+
if (window.parent === window) return;
|
|
26
|
+
const message = { jsonrpc: "2.0", method: ${JSON.stringify(FRAME_STATUS_METHOD)}, params };
|
|
27
|
+
const web = hosts.filter((origin) => origin.startsWith("https://") || origin.startsWith("http://"));
|
|
28
|
+
for (const origin of web) { try { window.parent.postMessage(message, origin); } catch (_) {} }
|
|
29
|
+
const parent = location.ancestorOrigins && location.ancestorOrigins.length > 0 ? location.ancestorOrigins[0] : null;
|
|
30
|
+
if (web.length < hosts.length && (parent === null || !web.includes(parent))) { try { window.parent.postMessage(message, "*"); } catch (_) {} }
|
|
31
|
+
};`;
|
|
18
32
|
/** Tells the host the page is blocked, and why (from the page's data attributes). */
|
|
19
33
|
const STATUS_SCRIPT = `(() => {
|
|
20
34
|
const main = document.querySelector("[data-porthole-status]");
|
package/dist/server/proxy.js
CHANGED
|
@@ -96,6 +96,8 @@ export function proxyTo(options = {}) {
|
|
|
96
96
|
method,
|
|
97
97
|
headers: upstreamRequestHeaders(request.headers, access.origin, target),
|
|
98
98
|
redirect: "manual",
|
|
99
|
+
// The browser went away (`nodeHost` aborts it): so does the relay (P070).
|
|
100
|
+
signal: request.signal,
|
|
99
101
|
...(hasBody ? { body: request.body, duplex: "half" } : {}),
|
|
100
102
|
});
|
|
101
103
|
return new Response(method === "HEAD" ? null : answer.body, {
|
package/dist/server/server.js
CHANGED
|
@@ -565,8 +565,15 @@ export function createServer(options) {
|
|
|
565
565
|
* signed state, the browser holding the state's nonce in a cookie of this origin. A path just
|
|
566
566
|
* entered whose cookie still didn't come back is a browser that keeps none here: no loop.
|
|
567
567
|
*/
|
|
568
|
-
function beginEntry(request, url, label, to) {
|
|
568
|
+
async function beginEntry(request, url, label, to) {
|
|
569
569
|
const method = request.method.toUpperCase();
|
|
570
|
+
// An address that names nothing is nothing here (P070): never a trip to the app's sign-in.
|
|
571
|
+
const scopeOf = content.name?.scopeOf;
|
|
572
|
+
const named = options.entry?.named;
|
|
573
|
+
if ((scopeOf !== undefined && scopeOf(label) === null) ||
|
|
574
|
+
(named !== undefined && !(await named(label, url.pathname, request)))) {
|
|
575
|
+
return page(404, "Nothing here", "This address names no content.", method);
|
|
576
|
+
}
|
|
570
577
|
if (method !== "GET" && method !== "HEAD") {
|
|
571
578
|
return page(401, "Open this from the app", "Open this address in a browser.", method, "session");
|
|
572
579
|
}
|
|
@@ -942,7 +949,7 @@ export function createServer(options) {
|
|
|
942
949
|
return await serveAccess(request, entered, null);
|
|
943
950
|
}
|
|
944
951
|
if (options.upstream === undefined || accessOf(request, label, null) === null) {
|
|
945
|
-
return beginEntry(request, url, label, authorizeUrl);
|
|
952
|
+
return await beginEntry(request, url, label, authorizeUrl);
|
|
946
953
|
}
|
|
947
954
|
}
|
|
948
955
|
// Without an upstream, only Porthole's own paths are served here: any other is nothing.
|
package/dist/server/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PortholeConfig } from "../config/schema.ts";
|
|
2
2
|
import type { FrameLocation, InstanceRef } from "../isolation/types.ts";
|
|
3
|
+
import type { Naming } from "./naming.ts";
|
|
3
4
|
export interface ContentDomain {
|
|
4
5
|
/**
|
|
5
6
|
* The registrable domain content is served under, used for nothing else
|
|
@@ -16,13 +17,14 @@ export interface ContentDomain {
|
|
|
16
17
|
* names like `a-<id>`) must return one lowercase DNS label, the same for the same scope, a
|
|
17
18
|
* different one for a different scope, and never a `reserved` one.
|
|
18
19
|
*/
|
|
19
|
-
readonly name?:
|
|
20
|
+
readonly name?: Naming;
|
|
20
21
|
/**
|
|
21
|
-
* Labels
|
|
22
|
-
* (`f` for `<instance>.f.<domain>`,
|
|
23
|
-
* `p
|
|
24
|
-
* whether a label is reserved. Requests for them, and for
|
|
25
|
-
* host's routes), so a host routes them without wrapping
|
|
22
|
+
* Labels something else serves on this domain, so this server never names them and hands their
|
|
23
|
+
* requests back: another server's namespace (`f` for `<instance>.f.<domain>`, where that server
|
|
24
|
+
* answers), or the host's own (`p-*`: its previews). Each entry is a whole label, or a prefix
|
|
25
|
+
* ending in `*`; or a function that says whether a label is reserved. Requests for them, and for
|
|
26
|
+
* the names under them, get null (the host's routes), so a host routes them without wrapping
|
|
27
|
+
* `handle`. Labels nothing serves need no entry: they get this server's 404 (P070).
|
|
26
28
|
*/
|
|
27
29
|
readonly reserved?: readonly string[] | ((label: string) => boolean);
|
|
28
30
|
}
|
|
@@ -161,6 +163,13 @@ export interface ServerOptions {
|
|
|
161
163
|
*/
|
|
162
164
|
readonly entry?: {
|
|
163
165
|
readonly authorizeUrl: string;
|
|
166
|
+
/**
|
|
167
|
+
* Whether a clean address names anything (P070): asked before sending a browser away, with
|
|
168
|
+
* the address's label and pathname. False: this server's 404, never the authorize route. A
|
|
169
|
+
* naming that says a label's scope (`Naming.scopeOf`) already answers 404 for a label it never
|
|
170
|
+
* makes.
|
|
171
|
+
*/
|
|
172
|
+
readonly named?: (label: string, path: string, request: Request) => boolean | Promise<boolean>;
|
|
164
173
|
};
|
|
165
174
|
}
|
|
166
175
|
/** A redirect entry's state, as the host's authorize route reads it (`PortholeServer.entryState`). */
|