@braidlabs/core 0.1.1 → 0.3.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.
Files changed (77) hide show
  1. package/package.json +2 -2
  2. package/src/adapters/adapter.d.ts +13 -0
  3. package/src/adapters/adapter.js.map +1 -1
  4. package/src/adapters/compat-adapter.d.ts +7 -0
  5. package/src/adapters/compat-adapter.js +24 -16
  6. package/src/adapters/compat-adapter.js.map +1 -1
  7. package/src/adapters/contract-adapter.d.ts +21 -0
  8. package/src/adapters/contract-adapter.js +71 -0
  9. package/src/adapters/contract-adapter.js.map +1 -0
  10. package/src/config.d.ts +29 -0
  11. package/src/config.js +16 -0
  12. package/src/config.js.map +1 -1
  13. package/src/context/context-bus.d.ts +63 -3
  14. package/src/context/context-bus.js +147 -61
  15. package/src/context/context-bus.js.map +1 -1
  16. package/src/elements/fragment-slot.d.ts +114 -1
  17. package/src/elements/fragment-slot.js +743 -60
  18. package/src/elements/fragment-slot.js.map +1 -1
  19. package/src/env/create-env.d.ts +20 -8
  20. package/src/env/create-env.js +48 -16
  21. package/src/env/create-env.js.map +1 -1
  22. package/src/env/define-fragment.d.ts +43 -0
  23. package/src/env/define-fragment.js +23 -0
  24. package/src/env/define-fragment.js.map +1 -0
  25. package/src/env/fragment-env.d.ts +20 -1
  26. package/src/env/fragment-env.js +0 -12
  27. package/src/env/fragment-env.js.map +1 -1
  28. package/src/errors.d.ts +1 -1
  29. package/src/errors.js.map +1 -1
  30. package/src/index.d.ts +14 -1
  31. package/src/index.js +7 -1
  32. package/src/index.js.map +1 -1
  33. package/src/protocol.d.ts +20 -4
  34. package/src/protocol.js +23 -7
  35. package/src/protocol.js.map +1 -1
  36. package/src/realm/realm-manager.d.ts +19 -2
  37. package/src/realm/realm-manager.js +32 -7
  38. package/src/realm/realm-manager.js.map +1 -1
  39. package/src/realm/sandbox-realm.d.ts +95 -0
  40. package/src/realm/sandbox-realm.js +194 -0
  41. package/src/realm/sandbox-realm.js.map +1 -0
  42. package/src/weave/capabilities.d.ts +71 -0
  43. package/src/weave/capabilities.js +61 -0
  44. package/src/weave/capabilities.js.map +1 -0
  45. package/src/weave/channel.d.ts +63 -0
  46. package/src/weave/channel.js +189 -0
  47. package/src/weave/channel.js.map +1 -0
  48. package/src/weave/closing.d.ts +99 -0
  49. package/src/weave/closing.js +130 -0
  50. package/src/weave/closing.js.map +1 -0
  51. package/src/weave/context-bridge.d.ts +81 -0
  52. package/src/weave/context-bridge.js +145 -0
  53. package/src/weave/context-bridge.js.map +1 -0
  54. package/src/weave/contract.d.ts +89 -0
  55. package/src/weave/contract.js +143 -0
  56. package/src/weave/contract.js.map +1 -0
  57. package/src/weave/envelope.d.ts +80 -0
  58. package/src/weave/envelope.js +49 -0
  59. package/src/weave/envelope.js.map +1 -0
  60. package/src/weave/guest.d.ts +59 -0
  61. package/src/weave/guest.js +120 -0
  62. package/src/weave/guest.js.map +1 -0
  63. package/src/weave/handshake.d.ts +124 -0
  64. package/src/weave/handshake.js +109 -0
  65. package/src/weave/handshake.js.map +1 -0
  66. package/src/weave/liveness.d.ts +90 -0
  67. package/src/weave/liveness.js +189 -0
  68. package/src/weave/liveness.js.map +1 -0
  69. package/src/weave/messages.d.ts +27 -0
  70. package/src/weave/messages.js +13 -0
  71. package/src/weave/messages.js.map +1 -0
  72. package/src/weave/port-backing.d.ts +19 -0
  73. package/src/weave/port-backing.js +54 -0
  74. package/src/weave/port-backing.js.map +1 -0
  75. package/src/weave/same-realm-backing.d.ts +21 -0
  76. package/src/weave/same-realm-backing.js +44 -0
  77. package/src/weave/same-realm-backing.js.map +1 -0
@@ -0,0 +1,124 @@
1
+ import { BoundaryChannel } from './channel.js';
2
+ import { ContextBridge, FragmentContract, HostContract } from './contract.js';
3
+ /**
4
+ * The connect-time handshake: HELLO → ACCEPT → OPEN.
5
+ *
6
+ * Braid has always verified a version at boot — the gateway stamps `BRAID_PROTOCOL_VERSION` onto
7
+ * the realm stub and the client checks the `<meta>`. That check answers "did these two ship
8
+ * together?" and nothing else. It cannot answer the questions the phases above this one ask:
9
+ * *which instance* am I talking to, *what* does it declare it can parse, and *when* did it become
10
+ * ready to hear from me.
11
+ *
12
+ * Three steps rather than two, because ACCEPT and OPEN carry different things in different
13
+ * directions and collapsing them would force the host to send the context snapshot before it has
14
+ * seen what the fragment can parse — which is precisely the information that decides how the
15
+ * snapshot must be projected.
16
+ */
17
+ export declare const HELLO = "weave/hello";
18
+ export declare const OPEN = "weave/open";
19
+ /** Host → fragment. What the host is, and what it is prepared to speak. */
20
+ export interface HelloPayload {
21
+ weave: number;
22
+ /** The client ↔ gateway composition protocol version, echoed for diagnosis only. */
23
+ protocol: string;
24
+ fragmentId: string;
25
+ instance: string;
26
+ /** What the host offers, so a fragment can refuse from its own side too. */
27
+ contract?: HostContract;
28
+ }
29
+ /** Fragment → host, as the reply to HELLO. What the fragment is, and what it can parse. */
30
+ export interface AcceptPayload {
31
+ weave: number;
32
+ /**
33
+ * Per-key context contract versions this fragment speaks.
34
+ *
35
+ * A fragment built months ago reads a context published today, so it declares what it can parse
36
+ * and every delivery is projected down to it. The registry manifest is the authority where it has
37
+ * an opinion — see `resolveContextVersions` — because a compromised or merely stale fragment
38
+ * should not be able to talk its way into a projection the host did not sanction.
39
+ */
40
+ contextVersions?: Record<string, number>;
41
+ /**
42
+ * What this fragment requires and provides.
43
+ *
44
+ * The registry manifest overrides it where it has an opinion, for the same reason it overrides
45
+ * `contextVersions` — on the untrusted tier this half of the handshake is attacker-controlled,
46
+ * and a fragment that could state its own requirements could state them away.
47
+ */
48
+ contract?: FragmentContract;
49
+ }
50
+ /** Host → fragment. The opening state, sent once terms are agreed. */
51
+ export interface OpenPayload {
52
+ /** Every context key, already projected to this fragment's declared versions. */
53
+ context: Record<string, unknown>;
54
+ props: Record<string, unknown>;
55
+ }
56
+ export interface HostHandshakeOptions {
57
+ channel: BoundaryChannel;
58
+ fragmentId: string;
59
+ instance: string;
60
+ /**
61
+ * Context versions from the fragment's registry manifest, where the registry has an opinion.
62
+ * Merged over whatever the fragment declares in ACCEPT.
63
+ */
64
+ manifestContextVersions?: Readonly<Record<string, number>>;
65
+ /** What this host offers. Absent when the host declares no contract. */
66
+ hostContract?: HostContract;
67
+ /** The fragment's contract from its registry manifest, overriding what it declares in ACCEPT. */
68
+ manifestContract?: FragmentContract;
69
+ /**
70
+ * Compares the two contracts. Injected so the handshake carries no contract logic of its own —
71
+ * negotiation is a policy question and this module is a protocol.
72
+ */
73
+ negotiate?(host: HostContract | undefined, fragment: FragmentContract | undefined): {
74
+ outcome: 'compatible' | 'bridged' | 'incompatible';
75
+ bridges: ContextBridge[];
76
+ reason?: string;
77
+ fixHint?: string;
78
+ };
79
+ /** Reports a connection opened across a version gap, with what it costs. */
80
+ onBridged?(bridges: ContextBridge[]): void;
81
+ /** Builds the opening state once terms are agreed and versions are known to be reachable. */
82
+ openWith(contextVersions: Readonly<Record<string, number>>): OpenPayload;
83
+ /**
84
+ * Raises if a declared version cannot be projected to from what the host publishes.
85
+ *
86
+ * Checked here, at the handshake, rather than at first delivery. That is where the context bus
87
+ * already argues it belongs — "refusing the subscription makes it a start-up error with a
88
+ * fragment's name on it" — and this moves it one step earlier still, to before the fragment has
89
+ * been handed any state at all.
90
+ */
91
+ assertReachable(contextVersions: Readonly<Record<string, number>>): void;
92
+ timeoutMs?: number;
93
+ }
94
+ export interface HandshakeResult {
95
+ contextVersions: Readonly<Record<string, number>>;
96
+ bridges: ContextBridge[];
97
+ }
98
+ /** Drives the host end. Resolves once the fragment has been sent its opening state. */
99
+ export declare function performHostHandshake(options: HostHandshakeOptions): Promise<HandshakeResult>;
100
+ export interface FragmentHandshakeOptions {
101
+ channel: BoundaryChannel;
102
+ /** What this fragment declares it can parse, per context key. */
103
+ contextVersions?: Readonly<Record<string, number>>;
104
+ /** What this fragment requires and provides. */
105
+ contract?: FragmentContract;
106
+ /** Called once the host sends the opening state. */
107
+ onOpen(payload: OpenPayload): void;
108
+ }
109
+ /**
110
+ * Answers the host end. Registers its handlers synchronously and returns a promise that resolves
111
+ * at OPEN, so a caller may either await readiness or ignore it — the handlers are live either way,
112
+ * which matters because HELLO can arrive in the same turn this is called.
113
+ */
114
+ export declare function answerHandshake(options: FragmentHandshakeOptions): Promise<OpenPayload>;
115
+ /**
116
+ * Merges the fragment's declaration with the registry's, with the registry winning.
117
+ *
118
+ * Not an arbitrary tie-break. The manifest is host-side configuration describing a fragment; the
119
+ * ACCEPT payload is the fragment describing itself. On the untrusted tier the second of those is
120
+ * attacker-controlled, and a fragment that could name its own context version could ask to be
121
+ * served a shape the host never intended to project. Letting the registry win costs nothing when
122
+ * the two agree, which is every honest case.
123
+ */
124
+ export declare function resolveContextVersions(declared: Readonly<Record<string, number>> | undefined, fromManifest: Readonly<Record<string, number>> | undefined): Readonly<Record<string, number>>;
@@ -0,0 +1,109 @@
1
+ import { BraidError } from '../errors.js';
2
+ import { BRAID_PROTOCOL_VERSION } from '../protocol.js';
3
+ import { WEAVE_VERSION } from './envelope.js';
4
+ /**
5
+ * The connect-time handshake: HELLO → ACCEPT → OPEN.
6
+ *
7
+ * Braid has always verified a version at boot — the gateway stamps `BRAID_PROTOCOL_VERSION` onto
8
+ * the realm stub and the client checks the `<meta>`. That check answers "did these two ship
9
+ * together?" and nothing else. It cannot answer the questions the phases above this one ask:
10
+ * *which instance* am I talking to, *what* does it declare it can parse, and *when* did it become
11
+ * ready to hear from me.
12
+ *
13
+ * Three steps rather than two, because ACCEPT and OPEN carry different things in different
14
+ * directions and collapsing them would force the host to send the context snapshot before it has
15
+ * seen what the fragment can parse — which is precisely the information that decides how the
16
+ * snapshot must be projected.
17
+ */
18
+ export const HELLO = 'weave/hello';
19
+ export const OPEN = 'weave/open';
20
+ /**
21
+ * Ten seconds, and deliberately not the channel's default five.
22
+ *
23
+ * HELLO is the one request sent while the fragment is still booting — its realm may be parsing a
24
+ * framework bundle on a cold cache over a slow connection. Every other request happens against a
25
+ * fragment that has already proven it can answer, which is why they get a tighter deadline.
26
+ */
27
+ const HANDSHAKE_TIMEOUT_MS = 10_000;
28
+ /** Drives the host end. Resolves once the fragment has been sent its opening state. */
29
+ export async function performHostHandshake(options) {
30
+ const { channel, fragmentId, instance } = options;
31
+ const hello = {
32
+ weave: WEAVE_VERSION,
33
+ protocol: BRAID_PROTOCOL_VERSION,
34
+ fragmentId,
35
+ instance,
36
+ ...(options.hostContract === undefined ? {} : { contract: options.hostContract }),
37
+ };
38
+ const accept = await channel.request(HELLO, hello, {
39
+ timeoutMs: options.timeoutMs ?? HANDSHAKE_TIMEOUT_MS,
40
+ });
41
+ if (accept?.weave !== WEAVE_VERSION) {
42
+ throw new BraidError(`the fragment answered with weave protocol v${String(accept?.weave)}, and this host speaks v${WEAVE_VERSION}`, {
43
+ fragmentId,
44
+ stage: 'handshake',
45
+ fixHint: 'the host and the fragment are running different major versions of @braidlabs/core — align them',
46
+ });
47
+ }
48
+ /**
49
+ * Terms before state.
50
+ *
51
+ * The negotiation runs between ACCEPT and OPEN because that is the only point where both halves
52
+ * are known and nothing has crossed yet. A fragment refused here has been handed no context, no
53
+ * props, and no reason to have started rendering.
54
+ */
55
+ const fragmentContract = options.manifestContract ?? accept.contract;
56
+ const negotiation = options.negotiate?.(options.hostContract, fragmentContract) ?? {
57
+ outcome: 'compatible',
58
+ bridges: [],
59
+ };
60
+ if (negotiation.outcome === 'incompatible') {
61
+ throw new BraidError(negotiation.reason ?? 'the fragment’s contract is not compatible with this host', {
62
+ fragmentId,
63
+ stage: 'contract',
64
+ ...(negotiation.fixHint === undefined ? {} : { fixHint: negotiation.fixHint }),
65
+ });
66
+ }
67
+ if (negotiation.bridges.length > 0)
68
+ options.onBridged?.(negotiation.bridges);
69
+ const contextVersions = resolveContextVersions(accept.contextVersions, options.manifestContextVersions);
70
+ options.assertReachable(contextVersions);
71
+ // Fire and forget: OPEN carries state, not a question. Making it a request would mean the host
72
+ // blocks its own mount on a fragment acknowledging state it has already been given, and a
73
+ // fragment slow to acknowledge would look identical to one that never got it.
74
+ channel.send(OPEN, options.openWith(contextVersions));
75
+ return { contextVersions, bridges: negotiation.bridges };
76
+ }
77
+ /**
78
+ * Answers the host end. Registers its handlers synchronously and returns a promise that resolves
79
+ * at OPEN, so a caller may either await readiness or ignore it — the handlers are live either way,
80
+ * which matters because HELLO can arrive in the same turn this is called.
81
+ */
82
+ export function answerHandshake(options) {
83
+ const { channel } = options;
84
+ const { promise, resolve } = Promise.withResolvers();
85
+ channel.on(HELLO, () => ({
86
+ weave: WEAVE_VERSION,
87
+ ...(options.contextVersions === undefined ? {} : { contextVersions: { ...options.contextVersions } }),
88
+ ...(options.contract === undefined ? {} : { contract: options.contract }),
89
+ }));
90
+ channel.on(OPEN, (payload) => {
91
+ const open = (payload ?? { context: {}, props: {} });
92
+ options.onOpen(open);
93
+ resolve(open);
94
+ });
95
+ return promise;
96
+ }
97
+ /**
98
+ * Merges the fragment's declaration with the registry's, with the registry winning.
99
+ *
100
+ * Not an arbitrary tie-break. The manifest is host-side configuration describing a fragment; the
101
+ * ACCEPT payload is the fragment describing itself. On the untrusted tier the second of those is
102
+ * attacker-controlled, and a fragment that could name its own context version could ask to be
103
+ * served a shape the host never intended to project. Letting the registry win costs nothing when
104
+ * the two agree, which is every honest case.
105
+ */
106
+ export function resolveContextVersions(declared, fromManifest) {
107
+ return { ...(declared ?? {}), ...(fromManifest ?? {}) };
108
+ }
109
+ //# sourceMappingURL=handshake.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handshake.js","sourceRoot":"","sources":["../../../../../libs/core/src/weave/handshake.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import { BoundaryChannel } from './channel.js';
2
+ /**
3
+ * Liveness — the fragment as a session rather than a mount.
4
+ *
5
+ * Before this, `<fragment-slot>` had four states and `ready` was terminal: a fragment whose realm
6
+ * wedged in a `while (true)` reported `ready` forever, and the only symptom was a region of the page
7
+ * that had quietly stopped being an application. The host had no way to tell "finished rendering"
8
+ * from "stopped existing".
9
+ *
10
+ * ```
11
+ * idle → connecting → healthy ⇄ unobservable
12
+ * ↓ ↓
13
+ * suspect → gone
14
+ * ```
15
+ *
16
+ * The state that makes this correct rather than merely present is **`unobservable`**. Browsers
17
+ * throttle timers in hidden tabs — to once a second, then far less after a few minutes — so a naive
18
+ * heartbeat reports mass death the moment a user switches tabs, and a team that has seen that once
19
+ * will turn the whole feature off. `unobservable` is not a fault: it is the host saying it has
20
+ * stopped being able to tell, which is a different claim from the fragment being unwell.
21
+ */
22
+ export declare const BEAT = "weave/beat";
23
+ export type LivenessState = 'connecting' | 'healthy' | 'unobservable' | 'suspect' | 'gone';
24
+ /**
25
+ * Where "can the fragment be observed right now?" is answered from.
26
+ *
27
+ * An interface rather than a direct `document.visibilityState` read so the two ends can be driven
28
+ * from the same source in a test. Both ends must agree — the fragment stops beating and the host
29
+ * suspends its deadline — and they agree by consulting the same document, which holds on the
30
+ * trusted tier (one document) and on the untrusted one (an iframe's visibility follows its top
31
+ * level).
32
+ */
33
+ export interface VisibilitySource {
34
+ isHidden(): boolean;
35
+ onChange(listener: () => void): () => void;
36
+ }
37
+ export declare function documentVisibility(): VisibilitySource;
38
+ /**
39
+ * The timer source a fragment's beats are scheduled on.
40
+ *
41
+ * This is the whole point of the fragment half, and the easiest thing to get wrong: scheduling
42
+ * beats on the *host's* `setInterval` would produce a perfectly steady heartbeat from a realm that
43
+ * had stopped executing. The beat has to be scheduled on the realm's own event loop — pass
44
+ * `realm.window` — so that a fragment spinning in a synchronous loop stops beating, because its
45
+ * timers stop firing. A beat proves the realm's task queue is turning; nothing else here does.
46
+ */
47
+ export interface BeatScheduler {
48
+ setInterval(handler: () => void, ms: number): number;
49
+ clearInterval(id: number): void;
50
+ /** Used for the opening beat, which must be evidence on the same terms as every other beat. */
51
+ setTimeout(handler: () => void, ms: number): number;
52
+ }
53
+ export interface HostLivenessOptions {
54
+ channel: BoundaryChannel;
55
+ /** The instance whose beats count. Beats stamped with any other instance are ignored. */
56
+ instance: string;
57
+ signal: AbortSignal;
58
+ onState(state: LivenessState, previous: LivenessState): void;
59
+ beatIntervalMs?: number;
60
+ graceBeats?: number;
61
+ goneAfterMs?: number;
62
+ visibility?: VisibilitySource;
63
+ /**
64
+ * Listen now, judge later: the beat handler is registered immediately, and the first deadline is
65
+ * armed only once this settles.
66
+ *
67
+ * The two have to be separable. Registering late loses the opening beat — the guest schedules it
68
+ * as soon as it is open, and on a cross-origin port nothing orders that against the host finishing
69
+ * its own mount — which leaves a perfectly healthy fragment sitting in `connecting` for a full
70
+ * interval. Arming early is the opposite mistake: it puts a deadline on a fragment that is still
71
+ * evaluating its framework bundle, and the honest name for that state is `loading`.
72
+ */
73
+ armAfter?: Promise<unknown>;
74
+ }
75
+ export interface HostLiveness {
76
+ readonly state: LivenessState;
77
+ stop(): void;
78
+ }
79
+ /** Host side: watches for beats and reports what it can and cannot conclude from their absence. */
80
+ export declare function startHostLiveness(options: HostLivenessOptions): HostLiveness;
81
+ export interface FragmentBeatOptions {
82
+ channel: BoundaryChannel;
83
+ /** The realm's timer source — see `BeatScheduler`. */
84
+ scheduler: BeatScheduler;
85
+ signal: AbortSignal;
86
+ beatIntervalMs?: number;
87
+ visibility?: VisibilitySource;
88
+ }
89
+ /** Fragment side: beats while it can be observed, and stays quiet while it cannot. */
90
+ export declare function startFragmentBeats(options: FragmentBeatOptions): () => void;
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Liveness — the fragment as a session rather than a mount.
3
+ *
4
+ * Before this, `<fragment-slot>` had four states and `ready` was terminal: a fragment whose realm
5
+ * wedged in a `while (true)` reported `ready` forever, and the only symptom was a region of the page
6
+ * that had quietly stopped being an application. The host had no way to tell "finished rendering"
7
+ * from "stopped existing".
8
+ *
9
+ * ```
10
+ * idle → connecting → healthy ⇄ unobservable
11
+ * ↓ ↓
12
+ * suspect → gone
13
+ * ```
14
+ *
15
+ * The state that makes this correct rather than merely present is **`unobservable`**. Browsers
16
+ * throttle timers in hidden tabs — to once a second, then far less after a few minutes — so a naive
17
+ * heartbeat reports mass death the moment a user switches tabs, and a team that has seen that once
18
+ * will turn the whole feature off. `unobservable` is not a fault: it is the host saying it has
19
+ * stopped being able to tell, which is a different claim from the fragment being unwell.
20
+ */
21
+ export const BEAT = 'weave/beat';
22
+ export function documentVisibility() {
23
+ return {
24
+ isHidden: () => document.visibilityState === 'hidden',
25
+ onChange(listener) {
26
+ document.addEventListener('visibilitychange', listener);
27
+ return () => document.removeEventListener('visibilitychange', listener);
28
+ },
29
+ };
30
+ }
31
+ /** Two seconds: three missed beats is six, comfortably under a person's patience for a dead panel. */
32
+ const BEAT_INTERVAL_MS = 2_000;
33
+ /** Three, so one dropped beat under load is not a diagnosis. */
34
+ const GRACE_BEATS = 3;
35
+ /** Fifteen seconds of silence from a fragment that should be running is not a slow fragment. */
36
+ const GONE_AFTER_MS = 15_000;
37
+ /** Host side: watches for beats and reports what it can and cannot conclude from their absence. */
38
+ export function startHostLiveness(options) {
39
+ const { channel, instance, signal } = options;
40
+ const beatIntervalMs = options.beatIntervalMs ?? BEAT_INTERVAL_MS;
41
+ const graceBeats = options.graceBeats ?? GRACE_BEATS;
42
+ const goneAfterMs = options.goneAfterMs ?? GONE_AFTER_MS;
43
+ const visibility = options.visibility ?? documentVisibility();
44
+ let state = 'connecting';
45
+ let deadline;
46
+ let goneTimer;
47
+ let stopped = false;
48
+ const transition = (next) => {
49
+ if (stopped || next === state)
50
+ return;
51
+ const previous = state;
52
+ state = next;
53
+ options.onState(next, previous);
54
+ };
55
+ const clearDeadline = () => {
56
+ if (deadline !== undefined)
57
+ clearTimeout(deadline);
58
+ deadline = undefined;
59
+ };
60
+ const clearGone = () => {
61
+ if (goneTimer !== undefined)
62
+ clearTimeout(goneTimer);
63
+ goneTimer = undefined;
64
+ };
65
+ const armDeadline = () => {
66
+ clearDeadline();
67
+ if (stopped || visibility.isHidden())
68
+ return;
69
+ deadline = setTimeout(() => {
70
+ transition('suspect');
71
+ // Suspicion is reported but not acted on. A fragment doing a long synchronous render is
72
+ // indistinguishable from one that has died, right up until it finishes — so the host says
73
+ // what it observes and waits before it says what it concludes.
74
+ clearGone();
75
+ goneTimer = setTimeout(() => transition('gone'), goneAfterMs);
76
+ }, beatIntervalMs * graceBeats);
77
+ };
78
+ channel.on(BEAT, (_payload, meta) => {
79
+ /**
80
+ * A beat only counts for the instance it came from.
81
+ *
82
+ * Without this, a fragment torn down mid-flight could have its last in-flight beat credited to
83
+ * the instance that replaced it, and a replacement that never booted would look healthy for one
84
+ * interval. (Today both ends of a trusted channel are stamped with the same instance by the
85
+ * slot; Phase 5 binds this to the instance the fragment reports in ACCEPT.)
86
+ */
87
+ if (meta.instance !== instance)
88
+ return;
89
+ if (stopped)
90
+ return;
91
+ clearGone();
92
+ transition(visibility.isHidden() ? 'unobservable' : 'healthy');
93
+ armDeadline();
94
+ });
95
+ const detachVisibility = visibility.onChange(() => {
96
+ if (stopped)
97
+ return;
98
+ if (visibility.isHidden()) {
99
+ clearDeadline();
100
+ clearGone();
101
+ // Only a running fragment becomes unobservable. One already suspect or gone stays that way:
102
+ // hiding the tab is not evidence of recovery.
103
+ if (state === 'healthy' || state === 'connecting')
104
+ transition('unobservable');
105
+ return;
106
+ }
107
+ // Back in view: the deadline restarts from now rather than resuming, because the fragment has
108
+ // only just been told it may beat again and has not had a chance to.
109
+ armDeadline();
110
+ });
111
+ /**
112
+ * Starting state depends on whether the fragment can be observed *right now*.
113
+ *
114
+ * `armDeadline()` alone is not enough, and the gap is easy to miss: it declines to arm while
115
+ * hidden but transitions nothing, so a fragment that mounts into an already-hidden tab sits in
116
+ * `connecting` indefinitely — indistinguishable, to a host, from one that never booted. Only the
117
+ * `visibilitychange` handler below said `unobservable`, and a tab that was hidden before the
118
+ * fragment existed never fires one.
119
+ *
120
+ * This is exactly the case a test that toggles visibility cannot reach, because toggling starts
121
+ * from visible. It took a real browser — a backgrounded tab — to surface it.
122
+ */
123
+ const begin = () => {
124
+ if (visibility.isHidden())
125
+ transition('unobservable');
126
+ else
127
+ armDeadline();
128
+ };
129
+ if (options.armAfter) {
130
+ // Settled either way: a mount that failed still stops this from arming, because the slot tears
131
+ // the instance down and the abort below runs.
132
+ void options.armAfter.then(begin, begin);
133
+ }
134
+ else {
135
+ begin();
136
+ }
137
+ const stop = () => {
138
+ if (stopped)
139
+ return;
140
+ stopped = true;
141
+ clearDeadline();
142
+ clearGone();
143
+ detachVisibility();
144
+ };
145
+ signal.addEventListener('abort', stop, { once: true });
146
+ return {
147
+ get state() {
148
+ return state;
149
+ },
150
+ stop,
151
+ };
152
+ }
153
+ /** Fragment side: beats while it can be observed, and stays quiet while it cannot. */
154
+ export function startFragmentBeats(options) {
155
+ const { channel, scheduler, signal } = options;
156
+ const beatIntervalMs = options.beatIntervalMs ?? BEAT_INTERVAL_MS;
157
+ const visibility = options.visibility ?? documentVisibility();
158
+ const beat = () => {
159
+ // Suppressed rather than merely ignored by the host: a hidden tab's throttled beats would
160
+ // arrive at unpredictable intervals, and a host that had to distinguish "throttled" from
161
+ // "dying" from timing alone would guess wrong in both directions.
162
+ if (visibility.isHidden())
163
+ return;
164
+ channel.send(BEAT);
165
+ };
166
+ const timer = scheduler.setInterval(beat, beatIntervalMs);
167
+ /**
168
+ * An opening beat as well as the interval, so `connecting → healthy` costs one message rather
169
+ * than one interval — a fragment that appears unwell for its first two seconds reads as a slow
170
+ * fragment, and that impression is very hard to undo.
171
+ *
172
+ * Scheduled on the realm rather than called directly, even though calling it would be simpler and
173
+ * would look identical in every passing test. A beat sent from host code is not evidence about
174
+ * the realm, and one exception to that rule is all it takes for a realm that wedged during mount
175
+ * to report `healthy` on the strength of a beat it never produced.
176
+ */
177
+ scheduler.setTimeout(beat, 0);
178
+ const detachVisibility = visibility.onChange(() => {
179
+ if (!visibility.isHidden())
180
+ scheduler.setTimeout(beat, 0);
181
+ });
182
+ const stop = () => {
183
+ scheduler.clearInterval(timer);
184
+ detachVisibility();
185
+ };
186
+ signal.addEventListener('abort', stop, { once: true });
187
+ return stop;
188
+ }
189
+ //# sourceMappingURL=liveness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"liveness.js","sourceRoot":"","sources":["../../../../../libs/core/src/weave/liveness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Message types that are not the handshake and not context.
3
+ *
4
+ * Both of these crossed the boundary before Weave existed, and both crossed it *unevenly*: the
5
+ * context bus cloned, while props and events handed live host-realm objects to fragment code. That
6
+ * asymmetry was invisible on the trusted tier and is unshippable on the untrusted one, so both are
7
+ * corrected here rather than carried forward.
8
+ */
9
+ /** Host → fragment. A complete props snapshot, cloned. Never a patch — see below. */
10
+ export declare const PROPS_CHANGED = "props/changed";
11
+ /** Fragment → host. Surfaced on the slot element as `braid:event`. */
12
+ export declare const FRAGMENT_EVENT = "frag/event";
13
+ export interface PropsChangedPayload {
14
+ /**
15
+ * The whole props object, every time.
16
+ *
17
+ * A diff would be smaller and would also require both ends to agree on how to apply one — which
18
+ * is a second protocol, with its own skew problem, protecting a payload that is a handful of
19
+ * fields on every real page. Sending the whole thing keeps "what does this fragment think its
20
+ * props are" answerable by looking at one message.
21
+ */
22
+ props: Record<string, unknown>;
23
+ }
24
+ export interface FragmentEventPayload {
25
+ type: string;
26
+ detail?: unknown;
27
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Message types that are not the handshake and not context.
3
+ *
4
+ * Both of these crossed the boundary before Weave existed, and both crossed it *unevenly*: the
5
+ * context bus cloned, while props and events handed live host-realm objects to fragment code. That
6
+ * asymmetry was invisible on the trusted tier and is unshippable on the untrusted one, so both are
7
+ * corrected here rather than carried forward.
8
+ */
9
+ /** Host → fragment. A complete props snapshot, cloned. Never a patch — see below. */
10
+ export const PROPS_CHANGED = 'props/changed';
11
+ /** Fragment → host. Surfaced on the slot element as `braid:event`. */
12
+ export const FRAGMENT_EVENT = 'frag/event';
13
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../../../../libs/core/src/weave/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,qFAAqF;AACrF,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAE7C,sEAAsE;AACtE,MAAM,CAAC,MAAM,cAAc,GAAG,YAAY,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { ChannelBacking } from './channel.js';
2
+ /**
3
+ * The cross-origin transport: one `MessagePort`, structured clone, nothing else.
4
+ *
5
+ * Nothing here is used by the trusted tier — it exists now, and is tested now, so that Phase 5 is
6
+ * the act of *choosing* this backing rather than the act of inventing it. A transport written after
7
+ * the protocol has shipped is a transport that inherits every same-origin assumption the protocol
8
+ * accumulated in the meantime.
9
+ *
10
+ * Two rules this backing enforces that the same-realm one gets for free:
11
+ *
12
+ * 1. **Everything inbound is untrusted.** A port is reachable by whatever holds the other end.
13
+ * Malformed messages are dropped silently — an untrusted peer must not be able to fill the
14
+ * host's console by posting garbage in a loop.
15
+ * 2. **Nothing unclonable goes out.** `postMessage` throws `DataCloneError` on a function or a DOM
16
+ * node, and the throw surfaces at the *sender*, far from whoever put the value in the payload.
17
+ * Converting it here names the offending message type while that context still exists.
18
+ */
19
+ export declare function createPortBacking(port: MessagePort): ChannelBacking;
@@ -0,0 +1,54 @@
1
+ import { isEnvelope } from './envelope.js';
2
+ /**
3
+ * The cross-origin transport: one `MessagePort`, structured clone, nothing else.
4
+ *
5
+ * Nothing here is used by the trusted tier — it exists now, and is tested now, so that Phase 5 is
6
+ * the act of *choosing* this backing rather than the act of inventing it. A transport written after
7
+ * the protocol has shipped is a transport that inherits every same-origin assumption the protocol
8
+ * accumulated in the meantime.
9
+ *
10
+ * Two rules this backing enforces that the same-realm one gets for free:
11
+ *
12
+ * 1. **Everything inbound is untrusted.** A port is reachable by whatever holds the other end.
13
+ * Malformed messages are dropped silently — an untrusted peer must not be able to fill the
14
+ * host's console by posting garbage in a loop.
15
+ * 2. **Nothing unclonable goes out.** `postMessage` throws `DataCloneError` on a function or a DOM
16
+ * node, and the throw surfaces at the *sender*, far from whoever put the value in the payload.
17
+ * Converting it here names the offending message type while that context still exists.
18
+ */
19
+ export function createPortBacking(port) {
20
+ let closed = false;
21
+ return {
22
+ post(envelope) {
23
+ if (closed)
24
+ return;
25
+ try {
26
+ port.postMessage(envelope);
27
+ }
28
+ catch (error) {
29
+ throw new Error(`braid: a "${envelope.type}" payload could not cross the boundary — ` +
30
+ `values sent to a cross-origin fragment must be structured-cloneable ` +
31
+ `(no functions, DOM nodes, or class instances with methods)`, { cause: error });
32
+ }
33
+ },
34
+ receive(handler) {
35
+ const listener = (event) => {
36
+ if (closed)
37
+ return;
38
+ if (!isEnvelope(event.data))
39
+ return;
40
+ handler(event.data);
41
+ };
42
+ port.addEventListener('message', listener);
43
+ port.start();
44
+ return () => port.removeEventListener('message', listener);
45
+ },
46
+ close() {
47
+ if (closed)
48
+ return;
49
+ closed = true;
50
+ port.close();
51
+ },
52
+ };
53
+ }
54
+ //# sourceMappingURL=port-backing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port-backing.js","sourceRoot":"","sources":["../../../../../libs/core/src/weave/port-backing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,UAAU,EAAE,MAAM,eAAe,CAAC;AAGrD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAiB;IACjD,IAAI,MAAM,GAAG,KAAK,CAAC;IAEnB,OAAO;QACL,IAAI,CAAC,QAAkB;YACrB,IAAI,MAAM;gBAAE,OAAO;YACnB,IAAI,CAAC;gBACH,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;YAC7B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CACb,aAAa,QAAQ,CAAC,IAAI,2CAA2C;oBACnE,sEAAsE;oBACtE,4DAA4D,EAC9D,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;YACJ,CAAC;QACH,CAAC;QAED,OAAO,CAAC,OAAO;YACb,MAAM,QAAQ,GAAG,CAAC,KAAmB,EAAE,EAAE;gBACvC,IAAI,MAAM;oBAAE,OAAO;gBACnB,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC;oBAAE,OAAO;gBACpC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACtB,CAAC,CAAC;YACF,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;YAC3C,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAC7D,CAAC;QAED,KAAK;YACH,IAAI,MAAM;gBAAE,OAAO;YACnB,MAAM,GAAG,IAAI,CAAC;YACd,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC;KACF,CAAC;AACJ,CAAC"}