@twinsearth/w2m-dsh-plugin 0.5.5 → 0.5.7
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/CHANGELOG.md +92 -0
- package/CONTRIBUTING.md +123 -0
- package/README.md +3 -1
- package/SECURITY.md +162 -0
- package/TROUBLESHOOTING.md +787 -0
- package/lib/client.js +139 -14
- package/package.json +8 -5
- package/src/agent/mdns-wire.mjs +1062 -0
- package/src/agent/mdns.mjs +1311 -0
- package/src/plugin/mdns-discover.mjs +577 -0
- package/src/plugin/tools.mjs +37 -2
package/lib/client.js
CHANGED
|
@@ -8,7 +8,25 @@
|
|
|
8
8
|
* web shell's loader consumes (`window.__ModuleLoader__.load({ id, factory })`, a lazy CJS table
|
|
9
9
|
* whose factory runs at materialization). `package.json` declares it with
|
|
10
10
|
* `dsh.client: { platform: 'web' }` and `exports['./client'] = './lib/client.js'`, which is what the
|
|
11
|
-
* host scans for
|
|
11
|
+
* host scans for. There is no second copy to keep in sync: this file **is** the source, and
|
|
12
|
+
* `test/client-bundle.test.mjs` is where its entry shape is read.
|
|
13
|
+
*
|
|
14
|
+
* # A failed client entry is fatal, and silence is not a neutral failure
|
|
15
|
+
*
|
|
16
|
+
* Measured twice in production: when this entry's `apply` throws, the page does not degrade — the
|
|
17
|
+
* shell audits every entry after activation and refuses to boot with
|
|
18
|
+
* `web boot: 1 entry did not activate / @twinsearth/w2m-dsh-plugin: failed`, which in the desktop
|
|
19
|
+
* app is forwarded to the main process and written as a `web-boot` crash: **the app does not start**.
|
|
20
|
+
* The audit prints the entry's *name* and its fiber state and nothing else, so the cause has to be
|
|
21
|
+
* carried by the failing code itself; {@link apply} therefore reports its own error to
|
|
22
|
+
* `console.error`, which is the error-level renderer console the crash log already captures.
|
|
23
|
+
*
|
|
24
|
+
* The failure that cost those two outages was a single property read. Cordis resolves `ctx.<name>`
|
|
25
|
+
* through the fiber's `inject` list: a service the fiber never declared **throws**
|
|
26
|
+
* (`cannot get property "locale" without inject`) whether or not some plugin provides it, and every
|
|
27
|
+
* name that *is* declared becomes a required dependency the entry waits for. Both halves of that rule
|
|
28
|
+
* are load-bearing here — see {@link inject} for what this bundle declares, and how the optional
|
|
29
|
+
* locale carrier is read instead.
|
|
12
30
|
*
|
|
13
31
|
* Two consequences, both deliberate:
|
|
14
32
|
*
|
|
@@ -109,12 +127,18 @@ window.__ModuleLoader__.load({
|
|
|
109
127
|
* `{name}` substitution is deliberately the only templating: these strings have one variable
|
|
110
128
|
* each, and a message format would be a dependency this package does not have.
|
|
111
129
|
*
|
|
112
|
-
* @param
|
|
130
|
+
* @param localeOf - the shell's locale service, or a function returning it (read at lookup time).
|
|
131
|
+
* Passed in rather than read from `ctx` here: the service is optional, and reading an optional
|
|
132
|
+
* service off the context is the one operation in this file that can kill the page (see
|
|
133
|
+
* {@link readOptionalService}). A function is accepted because the service may arrive *after*
|
|
134
|
+
* this entry activates — the shell's `bind()` is live, so a deferred read still localises.
|
|
113
135
|
* @returns {(key: string, values?: object) => string}
|
|
114
136
|
*/
|
|
115
|
-
function makeTranslator(
|
|
116
|
-
const
|
|
137
|
+
function makeTranslator(localeOf) {
|
|
138
|
+
const resolveLocale = () => (typeof localeOf === 'function' ? localeOf() : localeOf);
|
|
117
139
|
return (key, values) => {
|
|
140
|
+
const locale = resolveLocale();
|
|
141
|
+
const bound = typeof locale?.bind === 'function' ? locale.bind(NS) : null;
|
|
118
142
|
const template = bound === null
|
|
119
143
|
? (DICT.zh[key] ?? DICT.en[key] ?? key)
|
|
120
144
|
: bound(key, values);
|
|
@@ -124,6 +148,50 @@ window.__ModuleLoader__.load({
|
|
|
124
148
|
};
|
|
125
149
|
}
|
|
126
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Read a service the composition may or may not provide, without the inject requirement.
|
|
153
|
+
*
|
|
154
|
+
* `ctx.get(name)` is Cordis's inject-free lookup, and it is the only correct way to read an
|
|
155
|
+
* *optional* service from a plugin: `ctx[name]` throws for anything the fiber did not declare as
|
|
156
|
+
* a dependency, and declaring it would make the service *required* — an entry whose declared
|
|
157
|
+
* dependency never arrives never activates either, which is fatal in exactly the same way. So a
|
|
158
|
+
* carrier that must not be able to block the control (`locale`) is read here, and a service the
|
|
159
|
+
* control cannot work without (`slots`) is declared in {@link inject}.
|
|
160
|
+
*
|
|
161
|
+
* @param ctx - the client plugin context.
|
|
162
|
+
* @param name - the service name.
|
|
163
|
+
* @returns the service, or undefined when the composition has none (or this is not a Cordis
|
|
164
|
+
* context at all, which is how `test/client-bundle.test.mjs` applies this bundle).
|
|
165
|
+
*/
|
|
166
|
+
function readOptionalService(ctx, name) {
|
|
167
|
+
return typeof ctx?.get === 'function' ? ctx.get(name) : undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Register {@link DICT} with the shell's locale service whenever that service exists.
|
|
172
|
+
*
|
|
173
|
+
* Two paths, one outcome. When the service is already up, the registration is an effect of this
|
|
174
|
+
* entry and goes away with it. When it is not up yet, the registration rides a **child fiber**
|
|
175
|
+
* (`ctx.inject`) that waits for `locale` without holding this entry back: declaring `locale` in
|
|
176
|
+
* {@link inject} instead would make it required, and a composition without a locale service
|
|
177
|
+
* would then report this entry as `pending` — which the shell's audit treats exactly like
|
|
178
|
+
* `failed`, i.e. the page does not boot. A missing localisation service must never cost an
|
|
179
|
+
* operator the control, and must never cost them the app.
|
|
180
|
+
*
|
|
181
|
+
* @param ctx - the client plugin context.
|
|
182
|
+
* @param locale - the shell's locale service, or undefined when the composition has none.
|
|
183
|
+
*/
|
|
184
|
+
function registerDictionaries(ctx, locale) {
|
|
185
|
+
if (typeof locale?.register === 'function') {
|
|
186
|
+
ctx.effect(() => locale.register(NS, DICT), 'w2m: composer target control dictionaries');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (typeof ctx?.inject !== 'function') return;
|
|
190
|
+
ctx.inject(['locale'], (localeCtx) => {
|
|
191
|
+
localeCtx.effect(() => localeCtx.locale.register(NS, DICT), 'w2m: composer target control dictionaries');
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
127
195
|
/** One JSON call to the host half, folded into a value: a refusal is never an exception here. */
|
|
128
196
|
async function callHost(options) {
|
|
129
197
|
let response;
|
|
@@ -463,7 +531,33 @@ window.__ModuleLoader__.load({
|
|
|
463
531
|
}
|
|
464
532
|
|
|
465
533
|
/**
|
|
466
|
-
* Client plugin body.
|
|
534
|
+
* Client plugin body: install the stylesheet, register the dictionaries, take the seat.
|
|
535
|
+
*
|
|
536
|
+
* @param ctx - the client root context.
|
|
537
|
+
*/
|
|
538
|
+
function apply(ctx) {
|
|
539
|
+
try {
|
|
540
|
+
mount(ctx);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
// The audit that decides whether the app boots reports the entry's name and the word
|
|
543
|
+
// "failed" and nothing else, so this is where the cause has to be said out loud. The
|
|
544
|
+
// renderer console at error level is the channel the desktop crash log already carries
|
|
545
|
+
// (`--- renderer console (error level, oldest first) ---`), which makes this the one place a
|
|
546
|
+
// browser half can put its own diagnosis next to the failure it caused.
|
|
547
|
+
console.error(
|
|
548
|
+
'@twinsearth/w2m-dsh-plugin: the client entry failed to activate, so this page will not boot: ' +
|
|
549
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
550
|
+
error,
|
|
551
|
+
);
|
|
552
|
+
// Deliberately rethrown. A swallowed failure would boot a page that looks healthy with no
|
|
553
|
+
// target control in it — the silent-half-arrival failure this package has already shipped
|
|
554
|
+
// once (v0.5.4) and refuses to ship again.
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* The body of {@link apply}, wrapped so no failure can escape without a diagnostic.
|
|
467
561
|
*
|
|
468
562
|
* `slots.inject` (not a bare `register`) is what the slot's declaration requires of an entry
|
|
469
563
|
* from a package that does not own the seat: the parent declared `conversation.input.left`, and
|
|
@@ -477,17 +571,30 @@ window.__ModuleLoader__.load({
|
|
|
477
571
|
*
|
|
478
572
|
* @param ctx - the client root context.
|
|
479
573
|
*/
|
|
480
|
-
function
|
|
574
|
+
function mount(ctx) {
|
|
575
|
+
// `ctx.slots` is legal to read because {@link inject} declares it — and declaring it is also
|
|
576
|
+
// what makes this entry wait for the seat instead of racing it. The shape check is for callers
|
|
577
|
+
// that are not a Cordis fiber at all (`test/client-bundle.test.mjs`): a missing or foreign
|
|
578
|
+
// registry is a control that cannot be mounted, and a control that cannot be mounted must not
|
|
579
|
+
// be allowed to take the whole page down with it.
|
|
580
|
+
const slots = ctx?.slots;
|
|
581
|
+
if (typeof slots?.inject !== 'function' || typeof slots?.register !== 'function') {
|
|
582
|
+
console.error(
|
|
583
|
+
'@twinsearth/w2m-dsh-plugin: the client half found no slot registry on this context, so the ' +
|
|
584
|
+
'dispatch-target control is not mounted; the rest of the page is unaffected',
|
|
585
|
+
);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
481
588
|
const styleDisposer = injectStyle(typeof document === 'undefined' ? null : document);
|
|
482
589
|
if (styleDisposer !== null && typeof ctx.effect === 'function') {
|
|
483
590
|
ctx.effect(() => styleDisposer, 'w2m: composer target control styles');
|
|
484
591
|
}
|
|
485
|
-
// Dictionaries
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const t = makeTranslator(ctx);
|
|
490
|
-
|
|
592
|
+
// Dictionaries as early as the composition allows: a namespace registered after the UI has
|
|
593
|
+
// mounted is still picked up, and the service is optional, so nothing here may block.
|
|
594
|
+
const locale = readOptionalService(ctx, 'locale');
|
|
595
|
+
registerDictionaries(ctx, locale);
|
|
596
|
+
const t = makeTranslator(() => readOptionalService(ctx, 'locale'));
|
|
597
|
+
slots.inject('conversation.input.left', () => slots.register({
|
|
491
598
|
name: 'conversation.input.left',
|
|
492
599
|
id: 'w2m-target',
|
|
493
600
|
// After the shipped permission control and the plan control, which sit earlier in the
|
|
@@ -502,8 +609,26 @@ window.__ModuleLoader__.load({
|
|
|
502
609
|
}, TargetSelect));
|
|
503
610
|
}
|
|
504
611
|
|
|
505
|
-
/**
|
|
506
|
-
|
|
612
|
+
/**
|
|
613
|
+
* Cordis services this entry depends on, read by the fiber that mounts this bundle.
|
|
614
|
+
*
|
|
615
|
+
* `slots` is the seat: the control is registered into `conversation.input.left` through the
|
|
616
|
+
* slot registry, and a fiber that cannot see `slots` has nowhere to register. Declaring it is
|
|
617
|
+
* not bookkeeping. Cordis resolves `ctx.<name>` against this list, so reading an undeclared
|
|
618
|
+
* service **throws** (`cannot get property "slots" without inject`) even when the service is
|
|
619
|
+
* provided — measured in production: this bundle shipped with `inject = []` while reading
|
|
620
|
+
* `ctx.locale` and `ctx.slots`, the first read threw, and the app refused to boot.
|
|
621
|
+
*
|
|
622
|
+
* `locale` is deliberately absent, and that is the other half of the same rule: **every**
|
|
623
|
+
* declared name is a required dependency, so an entry that declares a service the composition
|
|
624
|
+
* does not provide never reaches `active` either — the audit then reports it as
|
|
625
|
+
* `pending (waiting for service: locale)`, which is just as fatal. Optional carriers are read
|
|
626
|
+
* with {@link readOptionalService} instead, and waited for by a child fiber when waiting is
|
|
627
|
+
* useful ({@link registerDictionaries}). Keep those two decisions in step with the reads in
|
|
628
|
+
* {@link mount}: `test/client-bundle.test.mjs` mounts this bundle behind a context that throws
|
|
629
|
+
* on any undeclared service read, exactly as Cordis does.
|
|
630
|
+
*/
|
|
631
|
+
const inject = ['slots'];
|
|
507
632
|
|
|
508
633
|
exports.apply = apply;
|
|
509
634
|
exports.inject = inject;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "DSH: Windows2MacOS — drive the same project on online Windows and macOS machines from one DeepSeek account, across networks and regions. P2P hole punching is the default path (relay + shared server as rendezvous, ledger and fallback), with a bundled STUN responder, Tailscale/public-VPS/tunnel shapes, Rabbit relay + per-machine Localside + eight DSH tools (replicate / split), nine aggregate verdicts and signed requests.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
"PROTOCOL-v0.1.2.md",
|
|
33
33
|
"PROTOCOL-v0.3.0.md",
|
|
34
34
|
"PROTOCOL-v0.3.9.md",
|
|
35
|
-
"PROTOCOL-v0.4.0.md"
|
|
35
|
+
"PROTOCOL-v0.4.0.md",
|
|
36
|
+
"TROUBLESHOOTING.md",
|
|
37
|
+
"SECURITY.md",
|
|
38
|
+
"CONTRIBUTING.md"
|
|
36
39
|
],
|
|
37
40
|
"dsh": {
|
|
38
41
|
"plugin": true,
|
|
@@ -96,19 +99,19 @@
|
|
|
96
99
|
}
|
|
97
100
|
},
|
|
98
101
|
"scripts": {
|
|
99
|
-
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs
|
|
102
|
+
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/claims.test.mjstest/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs",
|
|
100
103
|
"test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs",
|
|
101
104
|
"test:relay": "node --test test/relay.test.mjs",
|
|
102
105
|
"test:agent": "node --test test/agent.test.mjs",
|
|
103
106
|
"test:tools": "node --test test/tools.test.mjs",
|
|
104
107
|
"test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
|
|
105
|
-
"test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs
|
|
108
|
+
"test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/claims.test.mjstest/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs",
|
|
106
109
|
"pack": "node scripts/pack.mjs",
|
|
107
110
|
"lint": "eslint .",
|
|
108
111
|
"lint:fix": "eslint . --fix",
|
|
109
112
|
"format": "prettier --write .",
|
|
110
113
|
"format:check": "prettier --check .",
|
|
111
|
-
"verify": "node scripts/check-entrypoints.mjs && node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/p2p-node.test.mjs test/p2p-agent.test.mjs",
|
|
114
|
+
"verify": "node scripts/check-entrypoints.mjs && node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/p2p-node.test.mjs test/p2p-agent.test.mjs test/client-bundle.test.mjs test/loader-entry.test.mjs",
|
|
112
115
|
"test:p2p": "node --test --test-force-exit test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/stun-server.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs"
|
|
113
116
|
},
|
|
114
117
|
"keywords": [
|