@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.
- package/package.json +2 -2
- package/src/adapters/adapter.d.ts +13 -0
- package/src/adapters/adapter.js.map +1 -1
- package/src/adapters/compat-adapter.d.ts +7 -0
- package/src/adapters/compat-adapter.js +24 -16
- package/src/adapters/compat-adapter.js.map +1 -1
- package/src/adapters/contract-adapter.d.ts +21 -0
- package/src/adapters/contract-adapter.js +71 -0
- package/src/adapters/contract-adapter.js.map +1 -0
- package/src/config.d.ts +29 -0
- package/src/config.js +16 -0
- package/src/config.js.map +1 -1
- package/src/context/context-bus.d.ts +63 -3
- package/src/context/context-bus.js +147 -61
- package/src/context/context-bus.js.map +1 -1
- package/src/elements/fragment-slot.d.ts +114 -1
- package/src/elements/fragment-slot.js +743 -60
- package/src/elements/fragment-slot.js.map +1 -1
- package/src/env/create-env.d.ts +20 -8
- package/src/env/create-env.js +48 -16
- package/src/env/create-env.js.map +1 -1
- package/src/env/define-fragment.d.ts +43 -0
- package/src/env/define-fragment.js +23 -0
- package/src/env/define-fragment.js.map +1 -0
- package/src/env/fragment-env.d.ts +20 -1
- package/src/env/fragment-env.js +0 -12
- package/src/env/fragment-env.js.map +1 -1
- package/src/errors.d.ts +1 -1
- package/src/errors.js.map +1 -1
- package/src/index.d.ts +14 -1
- package/src/index.js +7 -1
- package/src/index.js.map +1 -1
- package/src/protocol.d.ts +20 -4
- package/src/protocol.js +23 -7
- package/src/protocol.js.map +1 -1
- package/src/realm/realm-manager.d.ts +19 -2
- package/src/realm/realm-manager.js +32 -7
- package/src/realm/realm-manager.js.map +1 -1
- package/src/realm/sandbox-realm.d.ts +95 -0
- package/src/realm/sandbox-realm.js +194 -0
- package/src/realm/sandbox-realm.js.map +1 -0
- package/src/weave/capabilities.d.ts +71 -0
- package/src/weave/capabilities.js +61 -0
- package/src/weave/capabilities.js.map +1 -0
- package/src/weave/channel.d.ts +63 -0
- package/src/weave/channel.js +189 -0
- package/src/weave/channel.js.map +1 -0
- package/src/weave/closing.d.ts +99 -0
- package/src/weave/closing.js +130 -0
- package/src/weave/closing.js.map +1 -0
- package/src/weave/context-bridge.d.ts +81 -0
- package/src/weave/context-bridge.js +145 -0
- package/src/weave/context-bridge.js.map +1 -0
- package/src/weave/contract.d.ts +89 -0
- package/src/weave/contract.js +143 -0
- package/src/weave/contract.js.map +1 -0
- package/src/weave/envelope.d.ts +80 -0
- package/src/weave/envelope.js +49 -0
- package/src/weave/envelope.js.map +1 -0
- package/src/weave/guest.d.ts +59 -0
- package/src/weave/guest.js +120 -0
- package/src/weave/guest.js.map +1 -0
- package/src/weave/handshake.d.ts +124 -0
- package/src/weave/handshake.js +109 -0
- package/src/weave/handshake.js.map +1 -0
- package/src/weave/liveness.d.ts +90 -0
- package/src/weave/liveness.js +189 -0
- package/src/weave/liveness.js.map +1 -0
- package/src/weave/messages.d.ts +27 -0
- package/src/weave/messages.js +13 -0
- package/src/weave/messages.js.map +1 -0
- package/src/weave/port-backing.d.ts +19 -0
- package/src/weave/port-backing.js +54 -0
- package/src/weave/port-backing.js.map +1 -0
- package/src/weave/same-realm-backing.d.ts +21 -0
- package/src/weave/same-realm-backing.js +44 -0
- package/src/weave/same-realm-backing.js.map +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-bridge.js","sourceRoot":"","sources":["../../../../../libs/core/src/weave/context-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { VersionedSchema } from '@braidlabs/skew';
|
|
2
|
+
/**
|
|
3
|
+
* Contract negotiation at connect time.
|
|
4
|
+
*
|
|
5
|
+
* Skew is the strongest thing Braid has, and it answers the *second* question about version
|
|
6
|
+
* disagreement: given a mismatch, what can be salvaged? This module asks the first one — should
|
|
7
|
+
* these two have been connected at all — and it exists because Braid could previously answer only
|
|
8
|
+
* by failing later, inside whichever interaction happened to touch the missing term.
|
|
9
|
+
*
|
|
10
|
+
* Three outcomes, and the third is the one only Braid can offer:
|
|
11
|
+
*
|
|
12
|
+
* 1. **compatible** — terms agree; open.
|
|
13
|
+
* 2. **incompatible** — refuse at mount, naming both versions and the failing term. Failing here is
|
|
14
|
+
* strictly better than failing at first interaction: an empty slot with a fallback beats a
|
|
15
|
+
* rendered UI that throws when someone clicks it.
|
|
16
|
+
* 3. **bridged** — the terms disagree but a registered Skew migration spans the gap. Open, and
|
|
17
|
+
* report what will be derived or discarded. Detecting skew and reconciling it are not the same
|
|
18
|
+
* capability, and this is where the difference shows up.
|
|
19
|
+
*/
|
|
20
|
+
export interface ContractProvisions {
|
|
21
|
+
events?: string[];
|
|
22
|
+
actions?: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface ContractRequirements {
|
|
25
|
+
/** Range the fragment requires of the host's contract version. See `satisfiesRange`. */
|
|
26
|
+
host?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Skew chain version per context key, not a semver range.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately the same integers `contextVersions` already uses rather than a parallel
|
|
31
|
+
* string-range vocabulary: a second way to say "which shape of `cart` do you speak" would need
|
|
32
|
+
* its own mapping onto the migration chain, and the two would disagree the first time anyone
|
|
33
|
+
* edited one of them.
|
|
34
|
+
*/
|
|
35
|
+
context?: Record<string, number>;
|
|
36
|
+
}
|
|
37
|
+
export interface FragmentContract {
|
|
38
|
+
version: string;
|
|
39
|
+
requires?: ContractRequirements;
|
|
40
|
+
provides?: ContractProvisions;
|
|
41
|
+
}
|
|
42
|
+
export interface HostContract {
|
|
43
|
+
version: string;
|
|
44
|
+
provides?: ContractProvisions;
|
|
45
|
+
}
|
|
46
|
+
/** One context key the fragment will receive at a version other than the published one. */
|
|
47
|
+
export interface ContextBridge {
|
|
48
|
+
key: string;
|
|
49
|
+
/** The version the host publishes. */
|
|
50
|
+
from: number;
|
|
51
|
+
/** The version the fragment declared it speaks. */
|
|
52
|
+
to: number;
|
|
53
|
+
/** Fields the projection discards on the way down. Empty when the bridge is lossless. */
|
|
54
|
+
discards: string[];
|
|
55
|
+
}
|
|
56
|
+
export type NegotiationOutcome = 'compatible' | 'bridged' | 'incompatible';
|
|
57
|
+
export interface Negotiation {
|
|
58
|
+
outcome: NegotiationOutcome;
|
|
59
|
+
bridges: ContextBridge[];
|
|
60
|
+
/** Set when incompatible: what disagreed, in terms a developer can act on. */
|
|
61
|
+
reason?: string;
|
|
62
|
+
fixHint?: string;
|
|
63
|
+
}
|
|
64
|
+
export interface NegotiateOptions {
|
|
65
|
+
host: HostContract | undefined;
|
|
66
|
+
fragment: FragmentContract | undefined;
|
|
67
|
+
/** Resolves a context key's registered schema. Injected — see `ContextBusLike`. */
|
|
68
|
+
schemaFor(key: string): VersionedSchema<unknown> | undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Compares what a fragment requires against what the host offers.
|
|
72
|
+
*
|
|
73
|
+
* A fragment that declares no contract is compatible with everything, and must stay that way:
|
|
74
|
+
* compat mode's whole promise is that being composed requires no change to the app, and a
|
|
75
|
+
* negotiation that refused undeclared fragments would revoke it.
|
|
76
|
+
*/
|
|
77
|
+
export declare function negotiateContract(options: NegotiateOptions): Negotiation;
|
|
78
|
+
/**
|
|
79
|
+
* A deliberately narrow subset of semver range syntax.
|
|
80
|
+
*
|
|
81
|
+
* `semver` is not a dependency of `@braidlabs/core` and should not become one — it is a browser
|
|
82
|
+
* runtime bundle, and the ranges that appear in a fragment manifest are the simple ones. The subset
|
|
83
|
+
* is documented, and anything outside it returns `'unparseable'` rather than a guess, so a range
|
|
84
|
+
* this build cannot read fails loudly at mount instead of quietly never being enforced.
|
|
85
|
+
*
|
|
86
|
+
* Supported: `*`, `1.x` / `1.*`, `1.2.3`, `>=`, `>`, `<=`, `<`, `^`, `~`.
|
|
87
|
+
* Not supported: hyphen ranges, `||` unions, pre-release precedence.
|
|
88
|
+
*/
|
|
89
|
+
export declare function satisfiesRange(version: string, range: string): boolean | 'unparseable';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compares what a fragment requires against what the host offers.
|
|
3
|
+
*
|
|
4
|
+
* A fragment that declares no contract is compatible with everything, and must stay that way:
|
|
5
|
+
* compat mode's whole promise is that being composed requires no change to the app, and a
|
|
6
|
+
* negotiation that refused undeclared fragments would revoke it.
|
|
7
|
+
*/
|
|
8
|
+
export function negotiateContract(options) {
|
|
9
|
+
const { host, fragment, schemaFor } = options;
|
|
10
|
+
if (!fragment)
|
|
11
|
+
return { outcome: 'compatible', bridges: [] };
|
|
12
|
+
const requiredHost = fragment.requires?.host;
|
|
13
|
+
if (requiredHost !== undefined) {
|
|
14
|
+
if (!host) {
|
|
15
|
+
return {
|
|
16
|
+
outcome: 'incompatible',
|
|
17
|
+
bridges: [],
|
|
18
|
+
reason: `the fragment requires a host contract matching "${requiredHost}", and this host declares none`,
|
|
19
|
+
fixHint: 'declare the host contract: initBraid({ contract: { version: "1.0.0" } })',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const satisfied = satisfiesRange(host.version, requiredHost);
|
|
23
|
+
if (satisfied === 'unparseable') {
|
|
24
|
+
// Refused rather than waved through. A range nobody can read is a requirement nobody is
|
|
25
|
+
// checking, and the failure mode of silently passing it is a fragment that mounts against a
|
|
26
|
+
// host it was never meant to run on.
|
|
27
|
+
return {
|
|
28
|
+
outcome: 'incompatible',
|
|
29
|
+
bridges: [],
|
|
30
|
+
reason: `the fragment requires host "${requiredHost}", which is not a range this build understands`,
|
|
31
|
+
fixHint: `use one of: "1.2.3", ">=1.2.3", ">1.2.3", "<=1.2.3", "<1.2.3", "^1.2.3", "~1.2.3", "1.x", "*"`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (!satisfied) {
|
|
35
|
+
return {
|
|
36
|
+
outcome: 'incompatible',
|
|
37
|
+
bridges: [],
|
|
38
|
+
reason: `the fragment requires host "${requiredHost}", and this host is "${host.version}"`,
|
|
39
|
+
fixHint: 'deploy a host that satisfies the range, or relax the fragment’s requires.host',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const bridges = [];
|
|
44
|
+
for (const [key, declared] of Object.entries(fragment.requires?.context ?? {})) {
|
|
45
|
+
const schema = schemaFor(key);
|
|
46
|
+
// No schema registered means no projection happens and none is needed — the value crosses as
|
|
47
|
+
// published, exactly as an untyped context always has.
|
|
48
|
+
if (!schema || declared === schema.version)
|
|
49
|
+
continue;
|
|
50
|
+
if (declared > schema.version) {
|
|
51
|
+
return {
|
|
52
|
+
outcome: 'incompatible',
|
|
53
|
+
bridges: [],
|
|
54
|
+
reason: `the fragment reads context "${key}" at v${declared}, and this host publishes v${schema.version}`,
|
|
55
|
+
fixHint: 'the fragment is newer than the host — deploy the host, or pin the fragment to a version it can produce',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const spanned = schema.steps.filter((step) => step.to > declared && step.to <= schema.version);
|
|
59
|
+
const missing = spanned.filter((step) => !step.down);
|
|
60
|
+
if (missing.length > 0) {
|
|
61
|
+
return {
|
|
62
|
+
outcome: 'incompatible',
|
|
63
|
+
bridges: [],
|
|
64
|
+
reason: `context "${key}" cannot be bridged from v${schema.version} down to v${declared}: ` +
|
|
65
|
+
`${missing.map((step) => `v${step.to} (${step.description})`).join(', ')} ` +
|
|
66
|
+
`${missing.length === 1 ? 'declares' : 'declare'} no down migration`,
|
|
67
|
+
fixHint: `add a down migration to the step that introduced v${missing[0]?.to}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
bridges.push({
|
|
71
|
+
key,
|
|
72
|
+
from: schema.version,
|
|
73
|
+
to: declared,
|
|
74
|
+
// What the fragment will not see. Reported rather than merely permitted: a bridge that
|
|
75
|
+
// silently drops a field is how a fragment ends up rendering a confidently wrong screen.
|
|
76
|
+
discards: [...new Set(spanned.flatMap((step) => step.lossy ?? []))],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { outcome: bridges.length > 0 ? 'bridged' : 'compatible', bridges };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A deliberately narrow subset of semver range syntax.
|
|
83
|
+
*
|
|
84
|
+
* `semver` is not a dependency of `@braidlabs/core` and should not become one — it is a browser
|
|
85
|
+
* runtime bundle, and the ranges that appear in a fragment manifest are the simple ones. The subset
|
|
86
|
+
* is documented, and anything outside it returns `'unparseable'` rather than a guess, so a range
|
|
87
|
+
* this build cannot read fails loudly at mount instead of quietly never being enforced.
|
|
88
|
+
*
|
|
89
|
+
* Supported: `*`, `1.x` / `1.*`, `1.2.3`, `>=`, `>`, `<=`, `<`, `^`, `~`.
|
|
90
|
+
* Not supported: hyphen ranges, `||` unions, pre-release precedence.
|
|
91
|
+
*/
|
|
92
|
+
export function satisfiesRange(version, range) {
|
|
93
|
+
const trimmed = range.trim();
|
|
94
|
+
if (trimmed === '*' || trimmed === '')
|
|
95
|
+
return true;
|
|
96
|
+
const target = parseVersion(version);
|
|
97
|
+
if (!target)
|
|
98
|
+
return 'unparseable';
|
|
99
|
+
const wildcard = /^(\d+)\.(x|\*)$/.exec(trimmed);
|
|
100
|
+
if (wildcard)
|
|
101
|
+
return target[0] === Number(wildcard[1]);
|
|
102
|
+
const operatorMatch = /^(>=|<=|>|<|\^|~)?\s*(\d+\.\d+\.\d+)$/.exec(trimmed);
|
|
103
|
+
if (!operatorMatch)
|
|
104
|
+
return 'unparseable';
|
|
105
|
+
const bound = parseVersion(operatorMatch[2]);
|
|
106
|
+
if (!bound)
|
|
107
|
+
return 'unparseable';
|
|
108
|
+
const order = compareVersions(target, bound);
|
|
109
|
+
switch (operatorMatch[1]) {
|
|
110
|
+
case undefined:
|
|
111
|
+
return order === 0;
|
|
112
|
+
case '>=':
|
|
113
|
+
return order >= 0;
|
|
114
|
+
case '>':
|
|
115
|
+
return order > 0;
|
|
116
|
+
case '<=':
|
|
117
|
+
return order <= 0;
|
|
118
|
+
case '<':
|
|
119
|
+
return order < 0;
|
|
120
|
+
case '^':
|
|
121
|
+
// Caret: same major, and not older. Major 0 is treated as every minor being breaking, which
|
|
122
|
+
// is the convention pre-1.0 packages actually rely on.
|
|
123
|
+
return order >= 0 && target[0] === bound[0] && (bound[0] !== 0 || target[1] === bound[1]);
|
|
124
|
+
case '~':
|
|
125
|
+
return order >= 0 && target[0] === bound[0] && target[1] === bound[1];
|
|
126
|
+
default:
|
|
127
|
+
return 'unparseable';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function parseVersion(value) {
|
|
131
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value.trim());
|
|
132
|
+
if (!match)
|
|
133
|
+
return undefined;
|
|
134
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
135
|
+
}
|
|
136
|
+
function compareVersions(a, b) {
|
|
137
|
+
for (let i = 0; i < 3; i++) {
|
|
138
|
+
if (a[i] !== b[i])
|
|
139
|
+
return a[i] < b[i] ? -1 : 1;
|
|
140
|
+
}
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../../../libs/core/src/weave/contract.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Weave envelope — the unit that crosses a fragment boundary.
|
|
3
|
+
*
|
|
4
|
+
* Braid already had a *bus*: `braidContext` publishes with structured cloning, per-subscriber
|
|
5
|
+
* Skew projection and subscriber isolation. What it did not have was a **wire**. A fragment
|
|
6
|
+
* reached the bus because the host built `env.context` as closures over `braidContext` and handed
|
|
7
|
+
* that object into the realm — a synchronous cross-realm call on a shared object graph. That works
|
|
8
|
+
* for a same-origin realm and for nothing else: a closure cannot be handed to another origin, which
|
|
9
|
+
* is the real reason the untrusted tier throws rather than degrades.
|
|
10
|
+
*
|
|
11
|
+
* This module is the addressing layer that was missing underneath. The bus's semantics are
|
|
12
|
+
* unchanged and are not re-implemented here — they remain the specification this format serves.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Version of the Weave envelope format.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately distinct from `BRAID_PROTOCOL_VERSION`, which versions the *client ↔ gateway*
|
|
18
|
+
* composition protocol. The two move for unrelated reasons: a change to how the gateway namespaces
|
|
19
|
+
* fragment assets has nothing to do with how a mounted fragment talks to its host, and a single
|
|
20
|
+
* number covering both would force one side to break for the other's benefit.
|
|
21
|
+
*/
|
|
22
|
+
export declare const WEAVE_VERSION = 1;
|
|
23
|
+
/** A failure delivered as a reply, rather than thrown into a realm that cannot catch it. */
|
|
24
|
+
export interface WeaveErrorPayload {
|
|
25
|
+
message: string;
|
|
26
|
+
/** Mirrors `BraidErrorStage` where the failure had one; absent for transport-level faults. */
|
|
27
|
+
stage?: string;
|
|
28
|
+
fixHint?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface Envelope {
|
|
31
|
+
v: typeof WEAVE_VERSION;
|
|
32
|
+
/** Unique per message. Correlates a reply to its request; also useful in a trace. */
|
|
33
|
+
id: string;
|
|
34
|
+
/**
|
|
35
|
+
* Set only on a reply, to the `id` being answered.
|
|
36
|
+
*
|
|
37
|
+
* A reply is not a message type — it is any message answering another. Keeping that in a field
|
|
38
|
+
* rather than in `type` means a handler for `ctx/get` never has to know whether the value it
|
|
39
|
+
* returns will be delivered as a reply or dropped; the channel decides from `reply` below.
|
|
40
|
+
*/
|
|
41
|
+
re?: string;
|
|
42
|
+
/** Set on a request to mean "a reply to this id is expected". */
|
|
43
|
+
reply?: true;
|
|
44
|
+
type: string;
|
|
45
|
+
fragmentId: string;
|
|
46
|
+
/**
|
|
47
|
+
* Document-instance id: unique to one boot of one fragment, surviving nothing — not a reload,
|
|
48
|
+
* not a re-mount of the same fragment into the same slot.
|
|
49
|
+
*
|
|
50
|
+
* Cheap to carry now and expensive to add later, because it is the only thing that distinguishes
|
|
51
|
+
* "the fragment reloaded" from "this is a different fragment". Liveness (Phase 2) needs it to
|
|
52
|
+
* avoid crediting a dead instance's beats to its replacement, and negotiated teardown (Phase 3)
|
|
53
|
+
* needs it to avoid ACKing a close on behalf of an instance that never received it.
|
|
54
|
+
*/
|
|
55
|
+
instance: string;
|
|
56
|
+
/**
|
|
57
|
+
* Which bus this addresses. `undefined` means the host bus — which is every message today.
|
|
58
|
+
*
|
|
59
|
+
* Reserved, unused, and deliberately not removed: the fragment-local bus is planned last, and
|
|
60
|
+
* without this field a context message addresses a *key* but not a *bus*. Introducing a second
|
|
61
|
+
* bus later would then mean either bumping `WEAVE_VERSION` or smuggling the scope into the key
|
|
62
|
+
* string, and both are worse than one optional field nothing currently sets.
|
|
63
|
+
*/
|
|
64
|
+
scope?: string;
|
|
65
|
+
payload?: unknown;
|
|
66
|
+
/** Set on a reply that failed. Mutually exclusive with a meaningful `payload`. */
|
|
67
|
+
error?: WeaveErrorPayload;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Identity generator for message and instance ids.
|
|
71
|
+
*
|
|
72
|
+
* `crypto.randomUUID` is required to be present in a secure context and is available in every
|
|
73
|
+
* environment Braid targets, but it is absent over plain http on a LAN address — a configuration
|
|
74
|
+
* developers hit constantly when testing on a phone — so the fallback exists to keep the dev loop
|
|
75
|
+
* working rather than to be cryptographically interesting. Ids here are correlation handles, never
|
|
76
|
+
* capabilities: nothing is authorized by holding one.
|
|
77
|
+
*/
|
|
78
|
+
export declare function weaveId(): string;
|
|
79
|
+
/** Narrows an unknown message — anything can arrive on a port — to a well-formed envelope. */
|
|
80
|
+
export declare function isEnvelope(value: unknown): value is Envelope;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Weave envelope — the unit that crosses a fragment boundary.
|
|
3
|
+
*
|
|
4
|
+
* Braid already had a *bus*: `braidContext` publishes with structured cloning, per-subscriber
|
|
5
|
+
* Skew projection and subscriber isolation. What it did not have was a **wire**. A fragment
|
|
6
|
+
* reached the bus because the host built `env.context` as closures over `braidContext` and handed
|
|
7
|
+
* that object into the realm — a synchronous cross-realm call on a shared object graph. That works
|
|
8
|
+
* for a same-origin realm and for nothing else: a closure cannot be handed to another origin, which
|
|
9
|
+
* is the real reason the untrusted tier throws rather than degrades.
|
|
10
|
+
*
|
|
11
|
+
* This module is the addressing layer that was missing underneath. The bus's semantics are
|
|
12
|
+
* unchanged and are not re-implemented here — they remain the specification this format serves.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Version of the Weave envelope format.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately distinct from `BRAID_PROTOCOL_VERSION`, which versions the *client ↔ gateway*
|
|
18
|
+
* composition protocol. The two move for unrelated reasons: a change to how the gateway namespaces
|
|
19
|
+
* fragment assets has nothing to do with how a mounted fragment talks to its host, and a single
|
|
20
|
+
* number covering both would force one side to break for the other's benefit.
|
|
21
|
+
*/
|
|
22
|
+
export const WEAVE_VERSION = 1;
|
|
23
|
+
/**
|
|
24
|
+
* Identity generator for message and instance ids.
|
|
25
|
+
*
|
|
26
|
+
* `crypto.randomUUID` is required to be present in a secure context and is available in every
|
|
27
|
+
* environment Braid targets, but it is absent over plain http on a LAN address — a configuration
|
|
28
|
+
* developers hit constantly when testing on a phone — so the fallback exists to keep the dev loop
|
|
29
|
+
* working rather than to be cryptographically interesting. Ids here are correlation handles, never
|
|
30
|
+
* capabilities: nothing is authorized by holding one.
|
|
31
|
+
*/
|
|
32
|
+
export function weaveId() {
|
|
33
|
+
const cryptoApi = globalThis.crypto;
|
|
34
|
+
if (cryptoApi && typeof cryptoApi.randomUUID === 'function')
|
|
35
|
+
return cryptoApi.randomUUID();
|
|
36
|
+
return `w-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
37
|
+
}
|
|
38
|
+
/** Narrows an unknown message — anything can arrive on a port — to a well-formed envelope. */
|
|
39
|
+
export function isEnvelope(value) {
|
|
40
|
+
if (typeof value !== 'object' || value === null)
|
|
41
|
+
return false;
|
|
42
|
+
const candidate = value;
|
|
43
|
+
return (candidate.v === WEAVE_VERSION &&
|
|
44
|
+
typeof candidate.id === 'string' &&
|
|
45
|
+
typeof candidate.type === 'string' &&
|
|
46
|
+
typeof candidate.fragmentId === 'string' &&
|
|
47
|
+
typeof candidate.instance === 'string');
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../../../../libs/core/src/weave/envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC;AAkD/B;;;;;;;;GAQG;AACH,MAAM,UAAU,OAAO;IACrB,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC;IACpC,IAAI,SAAS,IAAI,OAAO,SAAS,CAAC,UAAU,KAAK,UAAU;QAAE,OAAO,SAAS,CAAC,UAAU,EAAE,CAAC;IAC3F,OAAO,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AACnF,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,SAAS,GAAG,KAA0B,CAAC;IAC7C,OAAO,CACL,SAAS,CAAC,CAAC,KAAK,aAAa;QAC7B,OAAO,SAAS,CAAC,EAAE,KAAK,QAAQ;QAChC,OAAO,SAAS,CAAC,IAAI,KAAK,QAAQ;QAClC,OAAO,SAAS,CAAC,UAAU,KAAK,QAAQ;QACxC,OAAO,SAAS,CAAC,QAAQ,KAAK,QAAQ,CACvC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { BoundaryChannel } from './channel.js';
|
|
2
|
+
import { ClosingCoordinator } from './closing.js';
|
|
3
|
+
import { FragmentContract } from './contract.js';
|
|
4
|
+
/**
|
|
5
|
+
* The guest half of the untrusted tier: what a cross-origin fragment runs to join a host page.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const session = await connectToBraidHost({ hostOrigin: 'https://portal.example.com' });
|
|
9
|
+
* session.context.subscribe('user', renderUser);
|
|
10
|
+
* session.onClosing(() => flushOutbox());
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Note what a session **does not** have, compared with `FragmentEnv`: no `root`, no `document`, no
|
|
14
|
+
* `location`, no `history`. A cross-origin fragment already owns all four — it is a whole document
|
|
15
|
+
* with its own URL — and handing it host-shaped substitutes would be re-creating exactly the
|
|
16
|
+
* illusion this tier exists to refuse. What crosses is state and lifecycle, nothing more.
|
|
17
|
+
*
|
|
18
|
+
* This is intentionally small. The framework adapters that make it ergonomic are Phase 7's job; this
|
|
19
|
+
* is the primitive they will be written against, shipped now because the untrusted tier is not
|
|
20
|
+
* testable without a guest and a tier with no guest is a tier nobody can use.
|
|
21
|
+
*/
|
|
22
|
+
export interface GuestSession {
|
|
23
|
+
readonly channel: BoundaryChannel;
|
|
24
|
+
readonly context: {
|
|
25
|
+
get(key: string): unknown;
|
|
26
|
+
subscribe(key: string, listener: (value: unknown) => void, options?: {
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}): () => void;
|
|
29
|
+
};
|
|
30
|
+
readonly props: Readonly<Record<string, unknown>>;
|
|
31
|
+
onPropsChanged(listener: (props: Readonly<Record<string, unknown>>) => void): () => void;
|
|
32
|
+
/** Fragment → host. Surfaced on the slot element as `braid:event`. */
|
|
33
|
+
emit(type: string, detail?: unknown): void;
|
|
34
|
+
onClosing: ClosingCoordinator['onClosing'];
|
|
35
|
+
setDirty: ClosingCoordinator['setDirty'];
|
|
36
|
+
/** Aborts when the host closes the session. */
|
|
37
|
+
readonly signal: AbortSignal;
|
|
38
|
+
disconnect(): void;
|
|
39
|
+
}
|
|
40
|
+
export interface ConnectOptions {
|
|
41
|
+
/**
|
|
42
|
+
* The exact origin of the host page this fragment agrees to be embedded by.
|
|
43
|
+
*
|
|
44
|
+
* Required, and deliberately not defaulted to "whoever framed me". A fragment that accepted a
|
|
45
|
+
* channel from any parent could be framed by an attacker's page, handed a plausible-looking
|
|
46
|
+
* context, and induced to render a user's data inside a document that can screenshot it. The
|
|
47
|
+
* fragment decides who may embed it, and this is where it says so.
|
|
48
|
+
*/
|
|
49
|
+
hostOrigin: string;
|
|
50
|
+
/** Skew chain versions this fragment can parse, per context key. */
|
|
51
|
+
contextVersions?: Readonly<Record<string, number>>;
|
|
52
|
+
contract?: FragmentContract;
|
|
53
|
+
/** How long to wait for a host to offer a connection. */
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
/** Beat interval; defaults to the shared two seconds. */
|
|
56
|
+
beatIntervalMs?: number;
|
|
57
|
+
}
|
|
58
|
+
/** Waits for a host to offer a port, then completes the Weave handshake over it. */
|
|
59
|
+
export declare function connectToBraidHost(options: ConnectOptions): Promise<GuestSession>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { BraidError } from '../errors.js';
|
|
2
|
+
import { SANDBOX_CONNECT, SANDBOX_READY, } from '../realm/sandbox-realm.js';
|
|
3
|
+
import { createBoundaryChannel } from './channel.js';
|
|
4
|
+
import { createPortBacking } from './port-backing.js';
|
|
5
|
+
import { createContextMirror } from './context-bridge.js';
|
|
6
|
+
import { createClosingCoordinator } from './closing.js';
|
|
7
|
+
import { answerHandshake } from './handshake.js';
|
|
8
|
+
import { startFragmentBeats } from './liveness.js';
|
|
9
|
+
import { FRAGMENT_EVENT, PROPS_CHANGED } from './messages.js';
|
|
10
|
+
const CONNECT_TIMEOUT_MS = 15_000;
|
|
11
|
+
/** Waits for a host to offer a port, then completes the Weave handshake over it. */
|
|
12
|
+
export async function connectToBraidHost(options) {
|
|
13
|
+
const { hostOrigin } = options;
|
|
14
|
+
const { port, fragmentId, instance } = await acceptConnection(hostOrigin, options.timeoutMs ?? CONNECT_TIMEOUT_MS);
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const channel = createBoundaryChannel({
|
|
17
|
+
backing: createPortBacking(port),
|
|
18
|
+
fragmentId,
|
|
19
|
+
instance,
|
|
20
|
+
signal: controller.signal,
|
|
21
|
+
});
|
|
22
|
+
const context = createContextMirror(channel);
|
|
23
|
+
const closing = createClosingCoordinator(channel);
|
|
24
|
+
let props = {};
|
|
25
|
+
const propsListeners = new Set();
|
|
26
|
+
const announceProps = () => {
|
|
27
|
+
for (const listener of [...propsListeners]) {
|
|
28
|
+
try {
|
|
29
|
+
listener(props);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error('braid: a props listener threw', error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
channel.on(PROPS_CHANGED, (payload) => {
|
|
37
|
+
props = (payload ?? {}).props ?? {};
|
|
38
|
+
announceProps();
|
|
39
|
+
});
|
|
40
|
+
const opened = answerHandshake({
|
|
41
|
+
channel,
|
|
42
|
+
...(options.contextVersions === undefined ? {} : { contextVersions: options.contextVersions }),
|
|
43
|
+
...(options.contract === undefined ? {} : { contract: options.contract }),
|
|
44
|
+
onOpen(open) {
|
|
45
|
+
context.seed(open);
|
|
46
|
+
props = open.props ?? {};
|
|
47
|
+
announceProps();
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
await opened;
|
|
51
|
+
/**
|
|
52
|
+
* Beats run on this document's own timers, which is what makes them meaningful here — more so
|
|
53
|
+
* than on the trusted tier, where the "realm" is a hidden frame. A cross-origin fragment that
|
|
54
|
+
* hangs is a whole application hanging, and its beats stop because its event loop stopped.
|
|
55
|
+
*/
|
|
56
|
+
startFragmentBeats({
|
|
57
|
+
channel,
|
|
58
|
+
scheduler: window,
|
|
59
|
+
signal: controller.signal,
|
|
60
|
+
...(options.beatIntervalMs === undefined ? {} : { beatIntervalMs: options.beatIntervalMs }),
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
channel,
|
|
64
|
+
context: { get: (key) => context.get(key), subscribe: (key, listener, subscribeOptions) => context.subscribe(key, listener, { signal: subscribeOptions?.signal ?? controller.signal }) },
|
|
65
|
+
get props() {
|
|
66
|
+
return props;
|
|
67
|
+
},
|
|
68
|
+
onPropsChanged(listener) {
|
|
69
|
+
propsListeners.add(listener);
|
|
70
|
+
return () => void propsListeners.delete(listener);
|
|
71
|
+
},
|
|
72
|
+
emit: (type, detail) => channel.send(FRAGMENT_EVENT, { type, detail }),
|
|
73
|
+
onClosing: closing.onClosing,
|
|
74
|
+
setDirty: closing.setDirty,
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
disconnect: () => controller.abort(),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Announces readiness, then takes the first port a pinned host offers. */
|
|
80
|
+
function acceptConnection(hostOrigin, timeoutMs) {
|
|
81
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
82
|
+
const onMessage = (event) => {
|
|
83
|
+
// Origin pinning, the whole point. `event.origin` is set by the browser and cannot be forged by
|
|
84
|
+
// the sender, which is what makes it worth checking and `event.data` worth distrusting.
|
|
85
|
+
if (event.origin !== hostOrigin)
|
|
86
|
+
return;
|
|
87
|
+
const data = event.data;
|
|
88
|
+
if (data?.braid !== SANDBOX_CONNECT)
|
|
89
|
+
return;
|
|
90
|
+
const port = event.ports[0];
|
|
91
|
+
if (!port)
|
|
92
|
+
return;
|
|
93
|
+
cleanup();
|
|
94
|
+
resolve({ port, fragmentId: String(data.fragmentId ?? 'fragment'), instance: String(data.instance ?? 'instance') });
|
|
95
|
+
};
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
cleanup();
|
|
98
|
+
reject(new BraidError(`no braid host offered a connection within ${timeoutMs}ms`, {
|
|
99
|
+
fragmentId: '<unconnected>',
|
|
100
|
+
stage: 'handshake',
|
|
101
|
+
fixHint: `check that this page is framed by "${hostOrigin}" and that the host mounts it with trust="untrusted"`,
|
|
102
|
+
}));
|
|
103
|
+
}, timeoutMs);
|
|
104
|
+
function cleanup() {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
window.removeEventListener('message', onMessage);
|
|
107
|
+
}
|
|
108
|
+
window.addEventListener('message', onMessage);
|
|
109
|
+
/**
|
|
110
|
+
* Announced *after* the listener is attached, so the two orderings converge.
|
|
111
|
+
*
|
|
112
|
+
* The host posts on the frame's `load` event, which may fire before this module has even been
|
|
113
|
+
* evaluated. Announcing tells a host that already gave up waiting to post again; attaching first
|
|
114
|
+
* means a host that posted immediately is not missed either.
|
|
115
|
+
*/
|
|
116
|
+
const ready = { braid: SANDBOX_READY, v: 1 };
|
|
117
|
+
window.parent?.postMessage(ready, hostOrigin);
|
|
118
|
+
return promise;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=guest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guest.js","sourceRoot":"","sources":["../../../../../libs/core/src/weave/guest.ts"],"names":[],"mappings":"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"}
|