@braidlabs/gateway 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -17
- package/package.json +2 -2
- package/src/gateway.d.ts +46 -15
- package/src/gateway.js +260 -44
- package/src/gateway.js.map +1 -1
- package/src/protocol.d.ts +18 -2
- package/src/protocol.js +19 -3
- package/src/protocol.js.map +1 -1
- package/src/redirect.d.ts +70 -0
- package/src/redirect.js +172 -0
- package/src/redirect.js.map +1 -0
- package/src/registry.d.ts +73 -0
- package/src/registry.js +16 -0
- package/src/registry.js.map +1 -1
- package/src/rewriter/html-rewrite-stream.d.ts +8 -3
- package/src/rewriter/html-rewrite-stream.js +25 -7
- package/src/rewriter/html-rewrite-stream.js.map +1 -1
- package/src/rewriter/transforms.d.ts +60 -10
- package/src/rewriter/transforms.js +192 -49
- package/src/rewriter/transforms.js.map +1 -1
- package/src/single-flight.d.ts +7 -1
- package/src/single-flight.js +26 -6
- package/src/single-flight.js.map +1 -1
package/src/protocol.d.ts
CHANGED
|
@@ -49,6 +49,17 @@ export declare const BRAID_ADAPTER_META = "braid-adapter";
|
|
|
49
49
|
export declare const BRAID_ADAPTER_OPTIONS_META = "braid-adapter-options";
|
|
50
50
|
/** Response/request header carrying a fragment id for diagnostics. */
|
|
51
51
|
export declare const BRAID_FRAGMENT_ID_HEADER = "x-braid-fragment-id";
|
|
52
|
+
/**
|
|
53
|
+
* Response header on a `409` from the document namespace: where the fragment tried to send the
|
|
54
|
+
* request. A browser hides the target of a redirect it was told not to follow, so the gateway
|
|
55
|
+
* carries it here instead — only for fragments that opt in with `redirect: 'navigate'`.
|
|
56
|
+
*/
|
|
57
|
+
export declare const BRAID_REDIRECT_LOCATION_HEADER = "x-braid-redirect-location";
|
|
58
|
+
/**
|
|
59
|
+
* Request header on a document fetch: the page the fragment is being shown on. The gateway uses it
|
|
60
|
+
* to point a login's return URL at that page instead of at the fragment's own document URL.
|
|
61
|
+
*/
|
|
62
|
+
export declare const BRAID_RETURN_URL_HEADER = "x-braid-return-url";
|
|
52
63
|
/** What a braid-namespaced URL addresses. */
|
|
53
64
|
export type BraidRouteKind =
|
|
54
65
|
/** The fragment's own endpoint: assets, data, anything it serves. Forwarded verbatim. */
|
|
@@ -75,5 +86,10 @@ export declare function parseNamespacePathname(pathname: string): {
|
|
|
75
86
|
fragmentId: string;
|
|
76
87
|
pathname: string;
|
|
77
88
|
} | null;
|
|
78
|
-
/**
|
|
79
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Builds a URL in the fragment's asset namespace.
|
|
91
|
+
*
|
|
92
|
+
* `basePath` is the gateway's mount (`/manage`), already normalized: empty, or a path with no
|
|
93
|
+
* trailing slash.
|
|
94
|
+
*/
|
|
95
|
+
export declare function braidFragmentUrl(fragmentId: string, pathname: string, search?: string, basePath?: string): string;
|
package/src/protocol.js
CHANGED
|
@@ -49,6 +49,17 @@ export const BRAID_ADAPTER_META = 'braid-adapter';
|
|
|
49
49
|
export const BRAID_ADAPTER_OPTIONS_META = 'braid-adapter-options';
|
|
50
50
|
/** Response/request header carrying a fragment id for diagnostics. */
|
|
51
51
|
export const BRAID_FRAGMENT_ID_HEADER = 'x-braid-fragment-id';
|
|
52
|
+
/**
|
|
53
|
+
* Response header on a `409` from the document namespace: where the fragment tried to send the
|
|
54
|
+
* request. A browser hides the target of a redirect it was told not to follow, so the gateway
|
|
55
|
+
* carries it here instead — only for fragments that opt in with `redirect: 'navigate'`.
|
|
56
|
+
*/
|
|
57
|
+
export const BRAID_REDIRECT_LOCATION_HEADER = 'x-braid-redirect-location';
|
|
58
|
+
/**
|
|
59
|
+
* Request header on a document fetch: the page the fragment is being shown on. The gateway uses it
|
|
60
|
+
* to point a login's return URL at that page instead of at the fragment's own document URL.
|
|
61
|
+
*/
|
|
62
|
+
export const BRAID_RETURN_URL_HEADER = 'x-braid-return-url';
|
|
52
63
|
const PREFIXES = [
|
|
53
64
|
[BRAID_FRAGMENT_PREFIX, 'fragment'],
|
|
54
65
|
[BRAID_REALM_PREFIX, 'realm'],
|
|
@@ -82,8 +93,13 @@ export function parseNamespacePathname(pathname) {
|
|
|
82
93
|
const route = parseBraidPathname(pathname);
|
|
83
94
|
return route && route.kind === 'fragment' ? { fragmentId: route.fragmentId, pathname: route.pathname } : null;
|
|
84
95
|
}
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Builds a URL in the fragment's asset namespace.
|
|
98
|
+
*
|
|
99
|
+
* `basePath` is the gateway's mount (`/manage`), already normalized: empty, or a path with no
|
|
100
|
+
* trailing slash.
|
|
101
|
+
*/
|
|
102
|
+
export function braidFragmentUrl(fragmentId, pathname, search = '', basePath = '') {
|
|
103
|
+
return `${basePath}${BRAID_FRAGMENT_PREFIX}${encodeURIComponent(fragmentId)}${pathname}${search}`;
|
|
88
104
|
}
|
|
89
105
|
//# sourceMappingURL=protocol.js.map
|
package/src/protocol.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../../libs/braid-gateway/src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,gBAAgB,CAAC;AACtD,MAAM,CAAC,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AACpD,MAAM,CAAC,MAAM,qBAAqB,GAAG,eAAe,CAAC;AAErD,4FAA4F;AAC5F,MAAM,CAAC,MAAM,sBAAsB,GAAG,qBAAqB,CAAC;AAE5D;;;;;;GAMG;AACH,4EAA4E;AAC5E,MAAM,CAAC,MAAM,yBAAyB,GAAG,gBAAgB,CAAC;AAE1D,gFAAgF;AAChF,MAAM,CAAC,MAAM,wBAAwB,GAAG,oBAAoB,CAAC;AAC7D,+CAA+C;AAC/C,MAAM,CAAC,MAAM,wBAAwB,GAAG,iBAAiB,CAAC;AAE1D,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAE1C,6FAA6F;AAC7F,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AAEpD,wGAAwG;AACxG,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAElD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,uBAAuB,CAAC;AAElE,sEAAsE;AACtE,MAAM,CAAC,MAAM,wBAAwB,GAAG,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../../libs/braid-gateway/src/protocol.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a fragment's redirect into something a browser can act on.
|
|
3
|
+
*
|
|
4
|
+
* A browser that was told not to follow a redirect hides where it was going, so for fragments that
|
|
5
|
+
* opt in (`redirect: 'navigate'`) the gateway answers the document request with a `409` carrying the
|
|
6
|
+
* target in a header. Everything here is pure: what the target may be, and how a login's return URL
|
|
7
|
+
* is pointed back at the page the user was on.
|
|
8
|
+
*
|
|
9
|
+
* The target is chosen by the fragment's endpoint, and it ends up as a top-level navigation, so it
|
|
10
|
+
* is only ever one of three things: a path on the host's own origin, a path inside the fragment's
|
|
11
|
+
* own endpoint (mapped back to the host), or an origin the host operator listed.
|
|
12
|
+
*/
|
|
13
|
+
/** An origin the host trusts as a redirect destination — `https://login.example.com`, or `https://*.example.com`. */
|
|
14
|
+
export interface OriginRule {
|
|
15
|
+
protocol: string;
|
|
16
|
+
/** The exact hostname, or the suffix after `*.` when `wildcard` is set. */
|
|
17
|
+
host: string;
|
|
18
|
+
wildcard: boolean;
|
|
19
|
+
port: string;
|
|
20
|
+
}
|
|
21
|
+
/** Parses `redirectOrigins`, failing at construction so a typo is not a silent refusal at 2am. */
|
|
22
|
+
export declare function parseOriginRules(origins: readonly string[]): OriginRule[];
|
|
23
|
+
export interface RedirectContext {
|
|
24
|
+
/** The raw `Location` the fragment answered with. */
|
|
25
|
+
location: string;
|
|
26
|
+
/** The URL the fragment's endpoint was asked for — what a relative `Location` is relative to. */
|
|
27
|
+
fragmentRequestUrl: URL;
|
|
28
|
+
/** The fragment's endpoint, when it is a URL (a function endpoint has no origin of its own). */
|
|
29
|
+
endpoint: string | undefined;
|
|
30
|
+
/** The request that reached the gateway: the host's origin. */
|
|
31
|
+
requestUrl: URL;
|
|
32
|
+
allowedOrigins: readonly OriginRule[];
|
|
33
|
+
}
|
|
34
|
+
export type RedirectResolution = {
|
|
35
|
+
ok: true;
|
|
36
|
+
target: string;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
reason: string;
|
|
40
|
+
};
|
|
41
|
+
/** `origin + path` only. A login's query string is where its tokens and return URLs live. */
|
|
42
|
+
export declare function redactUrl(url: URL): string;
|
|
43
|
+
/**
|
|
44
|
+
* A path that can only mean a path on this origin.
|
|
45
|
+
*
|
|
46
|
+
* `//evil.net` is a valid *path* to a URL parser (`https://host//evil.net`, or `/\evil.net`, whose
|
|
47
|
+
* backslash becomes a slash) and a valid *host* to whoever reads it back as a relative URL — the
|
|
48
|
+
* client does, with `new URL(target, location.href)`. Every leading slash or backslash collapses to
|
|
49
|
+
* one, so nothing this module emits as a path can be read as anything else.
|
|
50
|
+
*/
|
|
51
|
+
export declare function hostPath(pathname: string, search?: string, hash?: string): string;
|
|
52
|
+
export declare function resolveRedirectTarget(context: RedirectContext): RedirectResolution;
|
|
53
|
+
/**
|
|
54
|
+
* The page the user is on, if the request says so and it is one the gateway may send them back to.
|
|
55
|
+
*
|
|
56
|
+
* Same origin as the gateway only: this value is client-supplied and ends up inside a redirect, so
|
|
57
|
+
* it must not be able to name anywhere else. A braid URL is not a page.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveReturnUrl(requestUrl: URL, headerValue: string | null, referer: string | null): URL | null;
|
|
60
|
+
/**
|
|
61
|
+
* Points a login's return-URL parameters at the page instead of at the fragment.
|
|
62
|
+
*
|
|
63
|
+
* A fragment builds `?next=` from the request it saw — its own URL, which means nothing to the
|
|
64
|
+
* user. Any query parameter whose value is that URL (same path, on the fragment's origin or the
|
|
65
|
+
* host's) is replaced with the page the user was on, in the same form (absolute stays absolute, a
|
|
66
|
+
* path stays a path). Anything else is left exactly as it was.
|
|
67
|
+
*/
|
|
68
|
+
export declare function rewriteReturnUrls(target: string, returnUrl: URL | null, context: Pick<RedirectContext, 'fragmentRequestUrl' | 'requestUrl'> & {
|
|
69
|
+
strippedUrl: URL;
|
|
70
|
+
}): string;
|
package/src/redirect.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a fragment's redirect into something a browser can act on.
|
|
3
|
+
*
|
|
4
|
+
* A browser that was told not to follow a redirect hides where it was going, so for fragments that
|
|
5
|
+
* opt in (`redirect: 'navigate'`) the gateway answers the document request with a `409` carrying the
|
|
6
|
+
* target in a header. Everything here is pure: what the target may be, and how a login's return URL
|
|
7
|
+
* is pointed back at the page the user was on.
|
|
8
|
+
*
|
|
9
|
+
* The target is chosen by the fragment's endpoint, and it ends up as a top-level navigation, so it
|
|
10
|
+
* is only ever one of three things: a path on the host's own origin, a path inside the fragment's
|
|
11
|
+
* own endpoint (mapped back to the host), or an origin the host operator listed.
|
|
12
|
+
*/
|
|
13
|
+
/** Parses `redirectOrigins`, failing at construction so a typo is not a silent refusal at 2am. */
|
|
14
|
+
export function parseOriginRules(origins) {
|
|
15
|
+
return origins.map((origin) => {
|
|
16
|
+
const wildcard = /^[a-z][a-z0-9+.-]*:\/\/\*\./i.test(origin);
|
|
17
|
+
let url;
|
|
18
|
+
try {
|
|
19
|
+
url = new URL(wildcard ? origin.replace('*.', '') : origin);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error(`braid-gateway: redirectOrigins entry "${origin}" is not a valid origin`);
|
|
23
|
+
}
|
|
24
|
+
if ((url.protocol !== 'https:' && url.protocol !== 'http:') || url.pathname !== '/' || url.search || url.hash) {
|
|
25
|
+
throw new Error(`braid-gateway: redirectOrigins entry "${origin}" must be a bare http(s) origin such as ` +
|
|
26
|
+
`"https://login.example.com" or "https://*.example.com"`);
|
|
27
|
+
}
|
|
28
|
+
return { protocol: url.protocol, host: url.hostname, wildcard, port: url.port };
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function originAllowed(url, rules) {
|
|
32
|
+
return rules.some((rule) => rule.protocol === url.protocol &&
|
|
33
|
+
rule.port === url.port &&
|
|
34
|
+
// a wildcard is subdomains only — the apex is a different thing and has to be listed
|
|
35
|
+
(rule.wildcard ? url.hostname.endsWith(`.${rule.host}`) : url.hostname === rule.host));
|
|
36
|
+
}
|
|
37
|
+
/** `origin + path` only. A login's query string is where its tokens and return URLs live. */
|
|
38
|
+
export function redactUrl(url) {
|
|
39
|
+
return `${url.origin}${url.pathname}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A path that can only mean a path on this origin.
|
|
43
|
+
*
|
|
44
|
+
* `//evil.net` is a valid *path* to a URL parser (`https://host//evil.net`, or `/\evil.net`, whose
|
|
45
|
+
* backslash becomes a slash) and a valid *host* to whoever reads it back as a relative URL — the
|
|
46
|
+
* client does, with `new URL(target, location.href)`. Every leading slash or backslash collapses to
|
|
47
|
+
* one, so nothing this module emits as a path can be read as anything else.
|
|
48
|
+
*/
|
|
49
|
+
export function hostPath(pathname, search = '', hash = '') {
|
|
50
|
+
return `/${pathname.replace(/^[\\/]+/, '')}${search}${hash}`;
|
|
51
|
+
}
|
|
52
|
+
export function resolveRedirectTarget(context) {
|
|
53
|
+
let target;
|
|
54
|
+
try {
|
|
55
|
+
target = new URL(context.location, context.fragmentRequestUrl);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { ok: false, reason: 'its Location is not a valid URL' };
|
|
59
|
+
}
|
|
60
|
+
// `javascript:` and `data:` would run in the page that follows them
|
|
61
|
+
if (target.protocol !== 'https:' && target.protocol !== 'http:') {
|
|
62
|
+
return { ok: false, reason: `its Location uses the "${target.protocol}" scheme` };
|
|
63
|
+
}
|
|
64
|
+
// credentials in a URL are for phishing, and nothing a login sends the user to needs them
|
|
65
|
+
if (target.username || target.password) {
|
|
66
|
+
return { ok: false, reason: 'its Location carries credentials in the URL' };
|
|
67
|
+
}
|
|
68
|
+
// Inside the fragment's own endpoint: the same path on the host. The mirror image of how the
|
|
69
|
+
// gateway forwards requests, which is what makes `/apps/billing/login` mean `/login` to a user.
|
|
70
|
+
if (context.endpoint) {
|
|
71
|
+
const endpointUrl = new URL(context.endpoint);
|
|
72
|
+
const basePath = endpointUrl.pathname.endsWith('/') ? endpointUrl.pathname.slice(0, -1) : endpointUrl.pathname;
|
|
73
|
+
if (target.origin === endpointUrl.origin &&
|
|
74
|
+
(!basePath || target.pathname === basePath || target.pathname.startsWith(`${basePath}/`))) {
|
|
75
|
+
return { ok: true, target: hostPath(target.pathname.slice(basePath.length), target.search, target.hash) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// The host's own origin — including a path under /__braid/, which is how a login that is itself
|
|
79
|
+
// a fragment is reached.
|
|
80
|
+
if (target.origin === context.requestUrl.origin) {
|
|
81
|
+
return { ok: true, target: hostPath(target.pathname, target.search, target.hash) };
|
|
82
|
+
}
|
|
83
|
+
if (originAllowed(target, context.allowedOrigins))
|
|
84
|
+
return { ok: true, target: target.href };
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason: `its Location (${redactUrl(target)}) is not on this host, inside the fragment's endpoint, or in ` +
|
|
88
|
+
`redirectOrigins`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The page the user is on, if the request says so and it is one the gateway may send them back to.
|
|
93
|
+
*
|
|
94
|
+
* Same origin as the gateway only: this value is client-supplied and ends up inside a redirect, so
|
|
95
|
+
* it must not be able to name anywhere else. A braid URL is not a page.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveReturnUrl(requestUrl, headerValue, referer) {
|
|
98
|
+
for (const candidate of [headerValue, referer]) {
|
|
99
|
+
if (!candidate)
|
|
100
|
+
continue;
|
|
101
|
+
try {
|
|
102
|
+
const url = new URL(candidate);
|
|
103
|
+
// a braid URL is not a page — wherever the gateway is mounted (`/manage/__braid/…` with a basePath)
|
|
104
|
+
if (url.origin === requestUrl.origin && !/(^|\/)__braid\//.test(url.pathname))
|
|
105
|
+
return url;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// try the next one
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Points a login's return-URL parameters at the page instead of at the fragment.
|
|
115
|
+
*
|
|
116
|
+
* A fragment builds `?next=` from the request it saw — its own URL, which means nothing to the
|
|
117
|
+
* user. Any query parameter whose value is that URL (same path, on the fragment's origin or the
|
|
118
|
+
* host's) is replaced with the page the user was on, in the same form (absolute stays absolute, a
|
|
119
|
+
* path stays a path). Anything else is left exactly as it was.
|
|
120
|
+
*/
|
|
121
|
+
export function rewriteReturnUrls(target, returnUrl, context) {
|
|
122
|
+
if (!returnUrl)
|
|
123
|
+
return target;
|
|
124
|
+
const url = new URL(target, context.requestUrl);
|
|
125
|
+
if (!url.search)
|
|
126
|
+
return target;
|
|
127
|
+
const origins = new Set([context.fragmentRequestUrl.origin, context.requestUrl.origin]);
|
|
128
|
+
const paths = new Set([
|
|
129
|
+
context.fragmentRequestUrl.pathname,
|
|
130
|
+
context.strippedUrl.pathname,
|
|
131
|
+
context.requestUrl.pathname,
|
|
132
|
+
]);
|
|
133
|
+
const pagePath = hostPath(returnUrl.pathname, returnUrl.search, returnUrl.hash);
|
|
134
|
+
let rewritten = false;
|
|
135
|
+
// Pair by pair on the raw query, so a parameter that is not being rewritten keeps its exact bytes
|
|
136
|
+
// — re-serializing the whole query would change `~`, bare flags and invalid UTF-8 for an IdP that
|
|
137
|
+
// never asked for it.
|
|
138
|
+
const pairs = url.search.slice(1).split('&').map((pair) => {
|
|
139
|
+
const eq = pair.indexOf('=');
|
|
140
|
+
if (eq < 0)
|
|
141
|
+
return pair;
|
|
142
|
+
let value;
|
|
143
|
+
try {
|
|
144
|
+
value = decodeURIComponent(pair.slice(eq + 1).replace(/\+/g, ' '));
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return pair;
|
|
148
|
+
}
|
|
149
|
+
const isPath = value.startsWith('/') && !value.startsWith('//');
|
|
150
|
+
if (!isPath && !/^https?:\/\//i.test(value))
|
|
151
|
+
return pair;
|
|
152
|
+
let candidate;
|
|
153
|
+
try {
|
|
154
|
+
candidate = new URL(value, context.fragmentRequestUrl);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return pair;
|
|
158
|
+
}
|
|
159
|
+
// A fragment served at `/` would match every `redirect_uri=<its root>`, which is an OAuth
|
|
160
|
+
// callback registration and not a place to return to.
|
|
161
|
+
if (candidate.pathname === '/' || !origins.has(candidate.origin) || !paths.has(candidate.pathname))
|
|
162
|
+
return pair;
|
|
163
|
+
rewritten = true;
|
|
164
|
+
return `${pair.slice(0, eq + 1)}${encodeURIComponent(isPath ? pagePath : returnUrl.href)}`;
|
|
165
|
+
});
|
|
166
|
+
if (!rewritten)
|
|
167
|
+
return target;
|
|
168
|
+
url.search = `?${pairs.join('&')}`;
|
|
169
|
+
// keep the form the target arrived in: host-relative stays host-relative
|
|
170
|
+
return /^https?:\/\//i.test(target) ? url.href : hostPath(url.pathname, url.search, url.hash);
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=redirect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect.js","sourceRoot":"","sources":["../../../../libs/braid-gateway/src/redirect.ts"],"names":[],"mappings":"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"}
|
package/src/registry.d.ts
CHANGED
|
@@ -24,6 +24,54 @@ export interface FragmentManifest {
|
|
|
24
24
|
adapter?: string;
|
|
25
25
|
/** Contract version for contract-mode fragments (unused by the compat adapter). */
|
|
26
26
|
contractVersion?: string;
|
|
27
|
+
/**
|
|
28
|
+
* What this fragment requires of a host, and what it provides back.
|
|
29
|
+
*
|
|
30
|
+
* Compared at the boundary handshake, before any state crosses. Recorded here rather than taken
|
|
31
|
+
* only from the fragment's own ACCEPT because the registry is host-side configuration: on the
|
|
32
|
+
* untrusted tier a fragment stating its own requirements could state them away.
|
|
33
|
+
*
|
|
34
|
+
* ```jsonc
|
|
35
|
+
* "contract": {
|
|
36
|
+
* "version": "2.1.0",
|
|
37
|
+
* "requires": { "host": ">=1.4.0", "context": { "cart": 2 } },
|
|
38
|
+
* "provides": { "events": ["checkout:complete"] }
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
contract?: {
|
|
43
|
+
version: string;
|
|
44
|
+
requires?: {
|
|
45
|
+
host?: string;
|
|
46
|
+
context?: Record<string, number>;
|
|
47
|
+
};
|
|
48
|
+
provides?: {
|
|
49
|
+
events?: string[];
|
|
50
|
+
actions?: string[];
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* What this fragment is allowed to do. Host-owned: never describable by the fragment itself,
|
|
55
|
+
* because a grant a fragment can widen is not a grant.
|
|
56
|
+
*
|
|
57
|
+
* ```jsonc
|
|
58
|
+
* "capabilities": {
|
|
59
|
+
* "context": { "read": ["user", "cart"] },
|
|
60
|
+
* "sandbox": ["allow-popups"],
|
|
61
|
+
* "permissions": ["clipboard-write"]
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
capabilities?: {
|
|
66
|
+
context?: {
|
|
67
|
+
read?: string[];
|
|
68
|
+
write?: string[];
|
|
69
|
+
};
|
|
70
|
+
sandbox?: string[];
|
|
71
|
+
permissions?: string[];
|
|
72
|
+
network?: string[];
|
|
73
|
+
storage?: 'partitioned' | 'shared';
|
|
74
|
+
};
|
|
27
75
|
/** Module entry for contract-mode fragments (unused by the compat adapter). */
|
|
28
76
|
entry?: string;
|
|
29
77
|
/**
|
|
@@ -57,6 +105,15 @@ export interface FragmentManifest {
|
|
|
57
105
|
* 404 on every page the widget appears on.
|
|
58
106
|
*/
|
|
59
107
|
src?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Where a bound fragment's document lives, as a path on its own endpoint (`/index.html`).
|
|
110
|
+
*
|
|
111
|
+
* For a single-page app on a static origin — a bucket behind a CDN — that has one document and
|
|
112
|
+
* 404s every route. Without it, a deep link asks that origin for `/accounts/123`. The fragment's
|
|
113
|
+
* router still sees the page's real route; only the request for its document changes. Assets
|
|
114
|
+
* are unaffected and are always fetched at their own paths.
|
|
115
|
+
*/
|
|
116
|
+
documentPath?: string;
|
|
60
117
|
/** Typed event surface for hosts. Reserved; not enforced by this build. */
|
|
61
118
|
events?: Record<string, {
|
|
62
119
|
detail: string;
|
|
@@ -117,6 +174,22 @@ export interface FragmentManifest {
|
|
|
117
174
|
* holding a deep link but kept out of listings.
|
|
118
175
|
*/
|
|
119
176
|
access?: FragmentAccess;
|
|
177
|
+
/**
|
|
178
|
+
* What happens when the fragment answers its document request with a redirect — almost always an
|
|
179
|
+
* unauthenticated request being sent to a login page.
|
|
180
|
+
*
|
|
181
|
+
* - `'error'` (default) — the client reports a named `fragment-fetch` error. A browser hides where
|
|
182
|
+
* a redirect it was told not to follow was going, so nothing more is possible without an opt-in.
|
|
183
|
+
* - `'navigate'` — the gateway hands the target to the client, which navigates the whole page
|
|
184
|
+
* there, exactly as the shell would have done had the redirect hit the top-level request.
|
|
185
|
+
*
|
|
186
|
+
* Opt-in because it lets the fragment's endpoint move the top-level page. The target must be on
|
|
187
|
+
* the host's origin, inside the fragment's own endpoint path, or on an origin listed in the
|
|
188
|
+
* gateway's `redirectOrigins`; anything else is refused. It only works end to end once the
|
|
189
|
+
* fragment can see who the user is (`forwardCredentials` or `additionalHeaders`) — otherwise it
|
|
190
|
+
* redirects again on the way back, and the client stops after one round trip.
|
|
191
|
+
*/
|
|
192
|
+
redirect?: 'error' | 'navigate';
|
|
120
193
|
}
|
|
121
194
|
export interface FragmentAccess {
|
|
122
195
|
/** Who may see this fragment in the discovery registry. Public when omitted. */
|
package/src/registry.js
CHANGED
|
@@ -45,12 +45,21 @@ export function normalizeManifest(manifest) {
|
|
|
45
45
|
if (!manifest.endpoint) {
|
|
46
46
|
throw new Error(`braid-gateway: manifest "${manifest.id}" is missing its endpoint`);
|
|
47
47
|
}
|
|
48
|
+
if (manifest.documentPath !== undefined && !isPlainPath(manifest.documentPath)) {
|
|
49
|
+
throw new Error(`braid-gateway: manifest "${manifest.id}" has documentPath "${manifest.documentPath}" — it must be ` +
|
|
50
|
+
`a plain path on the fragment's endpoint, e.g. documentPath: "/index.html" (no "//", "\\", ` +
|
|
51
|
+
`dot segments, query, or fragment)`);
|
|
52
|
+
}
|
|
48
53
|
// Warned rather than thrown: the fragment still composes, at the page path, which is wrong in a
|
|
49
54
|
// way that shows up as an empty widget rather than as an error anyone can trace back to here.
|
|
50
55
|
if (manifest.bound === false && !manifest.src) {
|
|
51
56
|
console.warn(`braid-gateway: fragment "${manifest.id}" declares bound: false without a src, so it will be ` +
|
|
52
57
|
`fetched at each page's own path — declare the path its content lives at, e.g. src: "/panel"`);
|
|
53
58
|
}
|
|
59
|
+
if (manifest.redirect !== undefined && manifest.redirect !== 'error' && manifest.redirect !== 'navigate') {
|
|
60
|
+
throw new Error(`braid-gateway: manifest "${manifest.id}" declares redirect: "${String(manifest.redirect)}" — ` +
|
|
61
|
+
`use "error" or "navigate"`);
|
|
62
|
+
}
|
|
54
63
|
return {
|
|
55
64
|
...manifest,
|
|
56
65
|
adapter: manifest.adapter ?? DEFAULT_ADAPTER,
|
|
@@ -59,6 +68,13 @@ export function normalizeManifest(manifest) {
|
|
|
59
68
|
bound: manifest.bound ?? true,
|
|
60
69
|
};
|
|
61
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* An absolute path that means the same thing however it is joined onto an endpoint: one leading
|
|
73
|
+
* slash, no backslashes (which URL parsers treat as slashes), no dot segments, no query or hash.
|
|
74
|
+
*/
|
|
75
|
+
function isPlainPath(path) {
|
|
76
|
+
return /^\/(?!\/)[^?#\\]*$/.test(path) && !path.split('/').some((segment) => segment === '.' || segment === '..');
|
|
77
|
+
}
|
|
62
78
|
export class Registry {
|
|
63
79
|
#source;
|
|
64
80
|
#manifests;
|
package/src/registry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../../libs/braid-gateway/src/registry.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../../libs/braid-gateway/src/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -44,9 +44,14 @@ export interface EndTag {
|
|
|
44
44
|
/** Emits content immediately before this end tag. */
|
|
45
45
|
before(content: Injection): void;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A handler may return a promise, and the stream waits on it at that tag — everything before the
|
|
49
|
+
* tag has already been emitted. That is how a pierced slot waits for its fragment without holding
|
|
50
|
+
* back the shell ahead of it. Handlers that return nothing cost nothing extra.
|
|
51
|
+
*/
|
|
47
52
|
export interface ElementHandler {
|
|
48
|
-
element?(tag: StartTag): void
|
|
49
|
-
endTag?(tag: EndTag): void
|
|
53
|
+
element?(tag: StartTag): void | Promise<void>;
|
|
54
|
+
endTag?(tag: EndTag): void | Promise<void>;
|
|
50
55
|
}
|
|
51
56
|
export interface RewriteOptions {
|
|
52
57
|
/**
|
|
@@ -61,7 +66,7 @@ export interface RewriteOptions {
|
|
|
61
66
|
* Emitted once the input ends. Used as the safety net for injections anchored to an end tag
|
|
62
67
|
* that never arrives — `</body>` is optional in HTML and frequently omitted.
|
|
63
68
|
*/
|
|
64
|
-
onEnd?(): Injection | undefined
|
|
69
|
+
onEnd?(): Injection | undefined | Promise<Injection | undefined>;
|
|
65
70
|
}
|
|
66
71
|
export declare function rewriteHtmlStream(input: ReadableStream<Uint8Array>, options: RewriteOptions): ReadableStream<Uint8Array>;
|
|
67
72
|
/** Adapts an async generator of chunks into a ReadableStream. */
|
|
@@ -74,7 +74,7 @@ async function* rewrite(input, options) {
|
|
|
74
74
|
finally {
|
|
75
75
|
reader.releaseLock();
|
|
76
76
|
}
|
|
77
|
-
const trailing = options.onEnd?.();
|
|
77
|
+
const trailing = await options.onEnd?.();
|
|
78
78
|
if (trailing !== undefined) {
|
|
79
79
|
yield* emit(trailing);
|
|
80
80
|
}
|
|
@@ -206,7 +206,11 @@ async function* rewrite(input, options) {
|
|
|
206
206
|
},
|
|
207
207
|
};
|
|
208
208
|
for (const element of elementHandlers) {
|
|
209
|
-
element
|
|
209
|
+
// awaited only when it is a promise: this runs for every element of every document, and an
|
|
210
|
+
// unconditional await would be a microtask per tag
|
|
211
|
+
const pending = element(tag);
|
|
212
|
+
if (pending)
|
|
213
|
+
await pending;
|
|
210
214
|
}
|
|
211
215
|
if (name !== originalName && !token.selfClosing && !VOID_ELEMENTS.has(originalName)) {
|
|
212
216
|
openRenames.push({ original: originalName, renamed: name });
|
|
@@ -234,12 +238,14 @@ async function* rewrite(input, options) {
|
|
|
234
238
|
return;
|
|
235
239
|
}
|
|
236
240
|
const before = [];
|
|
237
|
-
handler.endTag({
|
|
241
|
+
const pending = handler.endTag({
|
|
238
242
|
tagName: token.name,
|
|
239
243
|
before(content) {
|
|
240
244
|
before.push(content);
|
|
241
245
|
},
|
|
242
246
|
});
|
|
247
|
+
if (pending)
|
|
248
|
+
await pending;
|
|
243
249
|
for (const content of before) {
|
|
244
250
|
yield* emit(content);
|
|
245
251
|
}
|
|
@@ -330,10 +336,17 @@ function scanToken(text, start) {
|
|
|
330
336
|
index++;
|
|
331
337
|
continue;
|
|
332
338
|
}
|
|
339
|
+
// Attribute names end where the HTML tokenizer ends them — at `/` too, which separates
|
|
340
|
+
// attributes — and a leading `=` is the name's first character, not a separator. A name split
|
|
341
|
+
// differently here than in the browser is an attribute the transforms never see.
|
|
333
342
|
const attributeNameStart = index;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
343
|
+
if (text[index] === '=')
|
|
344
|
+
index++;
|
|
345
|
+
while (index < length &&
|
|
346
|
+
!isWhitespace(text[index]) &&
|
|
347
|
+
text[index] !== '=' &&
|
|
348
|
+
text[index] !== '>' &&
|
|
349
|
+
text[index] !== '/') {
|
|
337
350
|
index++;
|
|
338
351
|
}
|
|
339
352
|
if (index >= length)
|
|
@@ -374,7 +387,12 @@ function scanToken(text, start) {
|
|
|
374
387
|
}
|
|
375
388
|
index = cursor;
|
|
376
389
|
}
|
|
377
|
-
|
|
390
|
+
// The browser keeps the first of a repeated attribute and drops the rest, so the same goes
|
|
391
|
+
// here: otherwise removing the first would promote a second the transforms never looked at.
|
|
392
|
+
const attributeName = rawName.toLowerCase();
|
|
393
|
+
if (!attributes.some((attribute) => attribute.name === attributeName)) {
|
|
394
|
+
attributes.push({ name: attributeName, rawName, value });
|
|
395
|
+
}
|
|
378
396
|
}
|
|
379
397
|
}
|
|
380
398
|
/** Finds the end of a markup declaration, honoring quoted strings (doctypes may contain `>`). */
|