@braidlabs/gateway 0.1.1

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.
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Registry & manifests: the registry is data, not code. Fragments register via manifest
3
+ * documents; deploying a fragment never redeploys the gateway.
4
+ */
5
+ /** How a fragment failure surfaces in a composed page. */
6
+ export type FragmentFallback = 'omit' | 'placeholder' | 'error-html';
7
+ /**
8
+ * `braid.manifest.json` — one per fragment.
9
+ */
10
+ import type { FragmentAppdMetadata, FragmentFdc3Metadata } from './appd.js';
11
+ export interface FragmentManifest {
12
+ /** Unique fragment id; addresses the fragment in the reserved namespace (`/__braid/frag/:id/*`). */
13
+ id: string;
14
+ /**
15
+ * The endpoint of the fragment application: a base URL, or a `fetch`-compatible function.
16
+ * Namespace requests are forwarded here with the namespace prefix stripped, so the endpoint
17
+ * sees the same paths it would serve standalone.
18
+ */
19
+ endpoint: string | typeof fetch;
20
+ /**
21
+ * The adapter the fragment runs under in the client. Defaults to `"compat"` — the contained
22
+ * web-fragments-style emulation layer — so legacy apps compose with zero app-code changes.
23
+ */
24
+ adapter?: string;
25
+ /** Contract version for contract-mode fragments (unused by the compat adapter). */
26
+ contractVersion?: string;
27
+ /** Module entry for contract-mode fragments (unused by the compat adapter). */
28
+ entry?: string;
29
+ /**
30
+ * For the `custom-element` adapter: the tag name the fragment's entry module defines.
31
+ *
32
+ * The fragment ships an ordinary custom element and knows nothing about Braid; this is how
33
+ * the gateway tells the client what to mount.
34
+ */
35
+ element?: string;
36
+ /**
37
+ * Route-pattern sugar: the page URLs whose server-rendered HTML this fragment is pierced
38
+ * into. Patterns are URLPattern pathname syntax (`/checkout/*`, `/orders/:id`).
39
+ *
40
+ * This is *only* the "which page URLs pierce which fragment" mapping — it is never the
41
+ * mechanism for asset or data routing, which is always exact and id-addressed.
42
+ */
43
+ pierce?: string[];
44
+ /**
45
+ * Whether this fragment renders the page's route.
46
+ *
47
+ * Defaults to `true`: a bound fragment is a screen, so it is fetched at the page's own path and
48
+ * participates in host navigation. `false` marks a widget — header chrome, a sidebar, a global
49
+ * search box — whose content lives at one fixed path regardless of which page it appears on.
50
+ */
51
+ bound?: boolean;
52
+ /**
53
+ * Where an unbound fragment's content lives, as a path on its own endpoint (`/panel`).
54
+ *
55
+ * Required in practice for `bound: false`, because the alternative is asking a notifications
56
+ * endpoint for `/billing/invoices` — a request that means nothing to it and that will, at best,
57
+ * 404 on every page the widget appears on.
58
+ */
59
+ src?: string;
60
+ /** Typed event surface for hosts. Reserved; not enforced by this build. */
61
+ events?: Record<string, {
62
+ detail: string;
63
+ }>;
64
+ /** Per-fragment budget for endpoint fetches, in milliseconds. */
65
+ timeoutMs?: number;
66
+ /**
67
+ * Opt this fragment out of concurrent-fetch coalescing. Defaults to `true` (coalesced).
68
+ *
69
+ * The gateway only shares a fetch between requests whose `cookie`, `authorization`, `user-agent`,
70
+ * and negotiation headers match, which covers how endpoints normally vary. Set this to `false`
71
+ * when yours varies on something the gateway cannot see — a tenant header, a feature-flag
72
+ * header, anything bespoke — because two such requests would otherwise look identical to it and
73
+ * share one render.
74
+ */
75
+ coalesce?: boolean;
76
+ /**
77
+ * What the gateway renders into the slot when this fragment can't be server-rendered.
78
+ *
79
+ * - `placeholder` (default) — the slot is left empty and marked `data-braid-fallback`, so the
80
+ * page can style a skeleton. The client runtime then fetches the fragment itself, which
81
+ * means a transient SSR failure self-heals instead of becoming a visible error.
82
+ * - `omit` — same, without the marker attribute.
83
+ * - `error-html` — the gateway renders an error into the slot. Choose this only when a
84
+ * missing section is worse than a visible failure.
85
+ */
86
+ fallback?: FragmentFallback;
87
+ /** Compat-specific options. */
88
+ compat?: {
89
+ fidelity?: 'documented';
90
+ warnOnUnaudited?: boolean;
91
+ };
92
+ /** Human-readable name for discovery listings. Defaults to the id. */
93
+ title?: string;
94
+ /** One-line description for discovery listings. */
95
+ description?: string;
96
+ /** Free-form labels for filtering and grouping in discovery listings. */
97
+ tags?: string[];
98
+ /**
99
+ * FDC3 metadata: the intents this app handles and raises, and the channels it uses.
100
+ *
101
+ * Projected into the App Directory listing (see `appd.ts`). The runtime members —
102
+ * `apiVersion`, `contexts` — are reserved for the FDC3 work and unused by the gateway.
103
+ */
104
+ fdc3?: FragmentFdc3Metadata;
105
+ /**
106
+ * Descriptive fields the App Directory carries that the registry does not otherwise need —
107
+ * publisher, contact addresses, icons. Purely for listings.
108
+ */
109
+ appd?: FragmentAppdMetadata;
110
+ /**
111
+ * Who may list this fragment, and who may load it. Declared at registration, so a
112
+ * fragment's own team decides its exposure rather than every host re-deciding it.
113
+ *
114
+ * **Both are public by default.** Omit `access` entirely and anyone can discover and load the
115
+ * fragment; add only the rule you need. The two are independent: a fragment can be openly
116
+ * listed but restricted to load (visible in a launcher, gated on click), or loadable by anyone
117
+ * holding a deep link but kept out of listings.
118
+ */
119
+ access?: FragmentAccess;
120
+ }
121
+ export interface FragmentAccess {
122
+ /** Who may see this fragment in the discovery registry. Public when omitted. */
123
+ list?: AccessRule;
124
+ /** Who may load this fragment through the namespace, or have it pierced in. Public when omitted. */
125
+ fetch?: AccessRule;
126
+ }
127
+ export interface AccessRule {
128
+ /** The caller must hold **at least one** of these roles. */
129
+ roles?: string[];
130
+ /** The caller must hold **all** of these scopes. */
131
+ scopes?: string[];
132
+ }
133
+ /** Who is asking, as resolved from a request by the gateway's host application. */
134
+ export interface Principal {
135
+ roles?: readonly string[];
136
+ scopes?: readonly string[];
137
+ }
138
+ /**
139
+ * Whether a principal satisfies an access rule.
140
+ *
141
+ * Note the deliberate asymmetry, which matches how these are normally used: **roles are any-of**
142
+ * (holding one of `["finance", "admin"]` is enough), while **scopes are all-of** (an operation
143
+ * requiring `["billing:read", "billing:list"]` needs both).
144
+ */
145
+ export declare function satisfies(rule: AccessRule | undefined, principal: Principal | undefined): boolean;
146
+ /** Whether this fragment appears in discovery listings for the given caller. */
147
+ export declare function canList(manifest: ResolvedFragmentManifest, principal: Principal | undefined): boolean;
148
+ /** Whether this caller may load the fragment — through the namespace or by piercing. */
149
+ export declare function canFetch(manifest: ResolvedFragmentManifest, principal: Principal | undefined): boolean;
150
+ /** Whether a manifest restricts anything at all, so the gateway can skip resolving a principal. */
151
+ export declare function hasAccessRules(manifest: ResolvedFragmentManifest): boolean;
152
+ /** A manifest with all defaults applied. */
153
+ export interface ResolvedFragmentManifest extends FragmentManifest {
154
+ adapter: string;
155
+ timeoutMs: number;
156
+ fallback: FragmentFallback;
157
+ bound: boolean;
158
+ }
159
+ /**
160
+ * The adapter used when a manifest doesn't declare one. This build ships only the compat
161
+ * adapter, and makes it the default.
162
+ */
163
+ export declare const DEFAULT_ADAPTER = "compat";
164
+ export declare const DEFAULT_TIMEOUT_MS = 1500;
165
+ /**
166
+ * A registry source: inline manifests, a URL to a JSON document (an array of manifests), or an
167
+ * async loader for anything else (file, KV, database).
168
+ */
169
+ export type RegistrySource = FragmentManifest[] | string | (() => Promise<FragmentManifest[]>);
170
+ export declare function normalizeManifest(manifest: FragmentManifest): ResolvedFragmentManifest;
171
+ export declare class Registry {
172
+ #private;
173
+ constructor(source: RegistrySource);
174
+ getFragment(fragmentId: string): Promise<ResolvedFragmentManifest | undefined>;
175
+ /**
176
+ * Returns every fragment whose `pierce` patterns cover this page path, in registration order.
177
+ *
178
+ * A page may compose several independently deployed fragments; each is pierced into the slot
179
+ * that names it.
180
+ */
181
+ matchPierceRoutes(pathname: string): Promise<ResolvedFragmentManifest[]>;
182
+ /** Every registered fragment, ordered by id so pagination is deterministic. */
183
+ listFragments(): Promise<ResolvedFragmentManifest[]>;
184
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Registry & manifests: the registry is data, not code. Fragments register via manifest
3
+ * documents; deploying a fragment never redeploys the gateway.
4
+ */
5
+ /**
6
+ * Whether a principal satisfies an access rule.
7
+ *
8
+ * Note the deliberate asymmetry, which matches how these are normally used: **roles are any-of**
9
+ * (holding one of `["finance", "admin"]` is enough), while **scopes are all-of** (an operation
10
+ * requiring `["billing:read", "billing:list"]` needs both).
11
+ */
12
+ export function satisfies(rule, principal) {
13
+ if (!rule)
14
+ return true;
15
+ const roles = new Set(principal?.roles ?? []);
16
+ const scopes = new Set(principal?.scopes ?? []);
17
+ if (rule.roles?.length && !rule.roles.some((role) => roles.has(role)))
18
+ return false;
19
+ if (rule.scopes?.length && !rule.scopes.every((scope) => scopes.has(scope)))
20
+ return false;
21
+ return true;
22
+ }
23
+ /** Whether this fragment appears in discovery listings for the given caller. */
24
+ export function canList(manifest, principal) {
25
+ return satisfies(manifest.access?.list, principal);
26
+ }
27
+ /** Whether this caller may load the fragment — through the namespace or by piercing. */
28
+ export function canFetch(manifest, principal) {
29
+ return satisfies(manifest.access?.fetch, principal);
30
+ }
31
+ /** Whether a manifest restricts anything at all, so the gateway can skip resolving a principal. */
32
+ export function hasAccessRules(manifest) {
33
+ return Boolean(manifest.access?.list || manifest.access?.fetch);
34
+ }
35
+ /**
36
+ * The adapter used when a manifest doesn't declare one. This build ships only the compat
37
+ * adapter, and makes it the default.
38
+ */
39
+ export const DEFAULT_ADAPTER = 'compat';
40
+ export const DEFAULT_TIMEOUT_MS = 1500;
41
+ export function normalizeManifest(manifest) {
42
+ if (!manifest.id || manifest.id.includes('/')) {
43
+ throw new Error(`braid-gateway: manifest id "${manifest.id}" is invalid — ids must be non-empty and must not contain "/"`);
44
+ }
45
+ if (!manifest.endpoint) {
46
+ throw new Error(`braid-gateway: manifest "${manifest.id}" is missing its endpoint`);
47
+ }
48
+ // Warned rather than thrown: the fragment still composes, at the page path, which is wrong in a
49
+ // way that shows up as an empty widget rather than as an error anyone can trace back to here.
50
+ if (manifest.bound === false && !manifest.src) {
51
+ console.warn(`braid-gateway: fragment "${manifest.id}" declares bound: false without a src, so it will be ` +
52
+ `fetched at each page's own path — declare the path its content lives at, e.g. src: "/panel"`);
53
+ }
54
+ return {
55
+ ...manifest,
56
+ adapter: manifest.adapter ?? DEFAULT_ADAPTER,
57
+ timeoutMs: manifest.timeoutMs ?? DEFAULT_TIMEOUT_MS,
58
+ fallback: manifest.fallback ?? 'placeholder',
59
+ bound: manifest.bound ?? true,
60
+ };
61
+ }
62
+ export class Registry {
63
+ #source;
64
+ #manifests;
65
+ #pierceRoutes;
66
+ constructor(source) {
67
+ this.#source = source;
68
+ if (Array.isArray(source)) {
69
+ // inline manifests fail fast at construction
70
+ this.#manifests = indexManifests(source);
71
+ this.#pierceRoutes = compilePierceRoutes(this.#manifests);
72
+ }
73
+ }
74
+ async getFragment(fragmentId) {
75
+ const manifests = await this.#load();
76
+ return manifests.get(fragmentId);
77
+ }
78
+ /**
79
+ * Returns every fragment whose `pierce` patterns cover this page path, in registration order.
80
+ *
81
+ * A page may compose several independently deployed fragments; each is pierced into the slot
82
+ * that names it.
83
+ */
84
+ async matchPierceRoutes(pathname) {
85
+ await this.#load();
86
+ // Deduplicated by fragment. Routes are compiled one per *pattern*, so a fragment declaring
87
+ // both `/billing` and `/billing/*` matches `/billing` twice — the second through the
88
+ // trailing-slash tolerance. Returning it twice would fetch that fragment twice per page load
89
+ // and pierce two copies of it into one slot; callers want the set of fragments that match,
90
+ // not the set of patterns that did.
91
+ const matched = new Map();
92
+ for (const route of this.#pierceRoutes) {
93
+ if (route.matches(pathname) && !matched.has(route.manifest.id)) {
94
+ matched.set(route.manifest.id, route.manifest);
95
+ }
96
+ }
97
+ return [...matched.values()];
98
+ }
99
+ /** Every registered fragment, ordered by id so pagination is deterministic. */
100
+ async listFragments() {
101
+ const manifests = await this.#load();
102
+ return [...manifests.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
103
+ }
104
+ async #load() {
105
+ if (this.#manifests)
106
+ return this.#manifests;
107
+ const source = this.#source;
108
+ let manifests;
109
+ if (typeof source === 'function') {
110
+ manifests = await source();
111
+ }
112
+ else if (typeof source === 'string') {
113
+ const response = await fetch(source);
114
+ if (!response.ok) {
115
+ throw new Error(`braid-gateway: loading the registry from "${source}" failed with HTTP ${response.status}`);
116
+ }
117
+ manifests = (await response.json());
118
+ }
119
+ else {
120
+ manifests = source;
121
+ }
122
+ this.#manifests = indexManifests(manifests);
123
+ this.#pierceRoutes = compilePierceRoutes(this.#manifests);
124
+ return this.#manifests;
125
+ }
126
+ }
127
+ /**
128
+ * Compiles every manifest's `pierce` patterns with the standard URLPattern API (global since
129
+ * Node 23.8, and native on Workers/Deno). An invalid pattern is a registration error, not a
130
+ * silently-never-matching route.
131
+ */
132
+ function compilePierceRoutes(manifests) {
133
+ const routes = [];
134
+ const piercing = [...manifests.values()].some((manifest) => (manifest.pierce ?? []).length > 0);
135
+ if (piercing)
136
+ assertURLPatternAvailable();
137
+ for (const manifest of manifests.values()) {
138
+ for (const pathnamePattern of manifest.pierce ?? []) {
139
+ let pattern;
140
+ try {
141
+ pattern = new URLPattern({ pathname: pathnamePattern });
142
+ }
143
+ catch (cause) {
144
+ throw new Error(`braid-gateway: fragment "${manifest.id}" declares an invalid pierce pattern "${pathnamePattern}" — ` +
145
+ `patterns use URLPattern pathname syntax (https://developer.mozilla.org/docs/Web/API/URL_Pattern_API)`, { cause });
146
+ }
147
+ routes.push({
148
+ manifest,
149
+ matches(pathname) {
150
+ if (pattern.test({ pathname }))
151
+ return true;
152
+ // URLPattern is strict about trailing slashes (`/checkout/*` does not match
153
+ // `/checkout`); page routes are conventionally slash-insensitive, so try both forms
154
+ const toggled = pathname.endsWith('/') ? pathname.slice(0, -1) : `${pathname}/`;
155
+ return toggled !== '' && pattern.test({ pathname: toggled });
156
+ },
157
+ });
158
+ }
159
+ }
160
+ return routes;
161
+ }
162
+ /**
163
+ * `URLPattern` is a global only from Node 23.8; on older runtimes the constructor throws a
164
+ * ReferenceError. Left to the per-pattern try/catch below that reads as "your pattern is invalid"
165
+ * for every pattern in the registry, which sends you debugging the manifest instead of the runtime.
166
+ */
167
+ function assertURLPatternAvailable() {
168
+ if (typeof URLPattern !== 'undefined')
169
+ return;
170
+ const node = typeof process !== 'undefined' && process.versions?.node ? ` (running Node ${process.versions.node})` : '';
171
+ throw new Error(`braid-gateway: this runtime has no global URLPattern${node}, which pierce patterns require — ` +
172
+ `use Node 24 or newer (URLPattern is global from Node 23.8), or a runtime that implements the ` +
173
+ `URL Pattern API (Workers, Deno, modern browsers)`);
174
+ }
175
+ function indexManifests(manifests) {
176
+ const indexed = new Map();
177
+ for (const manifest of manifests) {
178
+ const resolved = normalizeManifest(manifest);
179
+ if (indexed.has(resolved.id)) {
180
+ console.warn(`braid-gateway: duplicate manifest for fragment id "${resolved.id}" — ignoring the duplicate`);
181
+ continue;
182
+ }
183
+ indexed.set(resolved.id, resolved);
184
+ }
185
+ return indexed;
186
+ }
187
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../../libs/braid-gateway/src/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The owned streaming HTML rewriter (zero forked dependencies).
3
+ *
4
+ * Braid's streaming needs are narrow and known: rename three elements, neutralize scripts and
5
+ * script-preload links, inject content at slot elements, and interleave two HTML streams. That
6
+ * is a small enough surface to own outright rather than inherit a forked HTMLRewriter wasm
7
+ * build — and owning it is what makes the composition protocol testable as a spec.
8
+ *
9
+ * Properties this implementation guarantees:
10
+ *
11
+ * - **Streaming**: output is produced as input arrives. Only the current (partial) tag is
12
+ * buffered, so memory stays bounded regardless of document size.
13
+ * - **Chunk-boundary safe**: a tag, comment, doctype, or raw-text end sequence split across
14
+ * network chunks is reassembled before it is interpreted.
15
+ * - **Fidelity by default**: a tag no handler modifies is emitted byte-for-byte as it arrived.
16
+ * Only mutated tags are re-serialized.
17
+ * - **Stream injection**: injected content may itself be a `ReadableStream`, which is what makes
18
+ * piercing a true interleave (shell up to the slot, then the fragment's stream, then the rest
19
+ * of the shell) rather than a buffer-everything-then-concatenate.
20
+ *
21
+ * Scope note: this is a rewriter, not a parser. It does not build a tree, resolve implied tags,
22
+ * or handle malformed-markup recovery — it observes a linear token stream. That is sufficient
23
+ * for every transform in the composition protocol, and the conformance vectors in
24
+ * `html-rewrite-stream.spec.ts` are the oracle for it.
25
+ */
26
+ /** Content that can be injected: literal HTML, or a stream of it (interleaved as it arrives). */
27
+ export type Injection = string | ReadableStream<Uint8Array>;
28
+ export interface StartTag {
29
+ /** The tag name, lowercased. Assigning renames the element. */
30
+ tagName: string;
31
+ readonly selfClosing: boolean;
32
+ /** Lowercased names of the element's attributes, in source order. */
33
+ readonly attributeNames: string[];
34
+ getAttribute(name: string): string | null;
35
+ setAttribute(name: string, value: string): void;
36
+ removeAttribute(name: string): void;
37
+ /** Emits content immediately before this start tag. */
38
+ before(content: Injection): void;
39
+ /** Emits content immediately after this start tag (i.e. as the element's first content). */
40
+ prepend(content: Injection): void;
41
+ }
42
+ export interface EndTag {
43
+ readonly tagName: string;
44
+ /** Emits content immediately before this end tag. */
45
+ before(content: Injection): void;
46
+ }
47
+ export interface ElementHandler {
48
+ element?(tag: StartTag): void;
49
+ endTag?(tag: EndTag): void;
50
+ }
51
+ export interface RewriteOptions {
52
+ /**
53
+ * Handlers keyed by lowercased tag name. The key `*` matches every element and runs before
54
+ * the tag-specific handler — used for transforms that apply regardless of element, such as
55
+ * stripping inline event handlers.
56
+ */
57
+ handlers: Record<string, ElementHandler>;
58
+ /** Drops `<!doctype …>` tokens (nested doctypes make some parsers choke). */
59
+ stripDoctype?: boolean;
60
+ /**
61
+ * Emitted once the input ends. Used as the safety net for injections anchored to an end tag
62
+ * that never arrives — `</body>` is optional in HTML and frequently omitted.
63
+ */
64
+ onEnd?(): Injection | undefined;
65
+ }
66
+ export declare function rewriteHtmlStream(input: ReadableStream<Uint8Array>, options: RewriteOptions): ReadableStream<Uint8Array>;
67
+ /** Adapts an async generator of chunks into a ReadableStream. */
68
+ export declare function toReadableStream(generator: AsyncGenerator<Uint8Array>): ReadableStream<Uint8Array>;
69
+ /** Builds a stream from literal strings and other streams, concatenated in order. */
70
+ export declare function concatStreams(parts: Injection[]): ReadableStream<Uint8Array>;