@lingxia/bridge 0.15.0 → 0.17.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.
@@ -0,0 +1,215 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
12
+ var __rest = (this && this.__rest) || function (s, e) {
13
+ var t = {};
14
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
15
+ t[p] = s[p];
16
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
17
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
18
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
19
+ t[p[i]] = s[p[i]];
20
+ }
21
+ return t;
22
+ };
23
+ /** Document-side framing for host-attested RequiredV3 documents. */
24
+ export var V3_PROTOCOL = 3;
25
+ export var DEFAULT_MAX_V3_FRAME_BYTES = 64 * 1024;
26
+ var documentToNativeKinds = new Set([
27
+ "hello",
28
+ "req",
29
+ "res",
30
+ "notify",
31
+ "cancel",
32
+ "ch.open",
33
+ "ch.data",
34
+ "ch.close",
35
+ "state.ack",
36
+ "console",
37
+ ]);
38
+ var nativeToDocumentKinds = new Set([
39
+ "helloAck",
40
+ "ready",
41
+ "req",
42
+ "res",
43
+ "event",
44
+ "state.snapshot",
45
+ "state.patch",
46
+ "ch.ack",
47
+ "ch.data",
48
+ "ch.close",
49
+ ]);
50
+ function isRecord(value) {
51
+ return value !== null && typeof value === "object" && !Array.isArray(value);
52
+ }
53
+ function hasDuplicateTopLevelSecurityKey(frame) {
54
+ var _a;
55
+ var securityKeys = new Set(["v", "kind", "sessionId", "secret"]);
56
+ var seen = new Set();
57
+ var depth = 0;
58
+ for (var index = 0; index < frame.length; index++) {
59
+ var char = frame[index];
60
+ if (char === "{" || char === "[") {
61
+ depth++;
62
+ continue;
63
+ }
64
+ if (char === "}" || char === "]") {
65
+ depth--;
66
+ continue;
67
+ }
68
+ if (char !== '"')
69
+ continue;
70
+ var start = index;
71
+ index++;
72
+ while (index < frame.length) {
73
+ if (frame[index] === "\\") {
74
+ index += 2;
75
+ continue;
76
+ }
77
+ if (frame[index] === '"')
78
+ break;
79
+ index++;
80
+ }
81
+ if (index >= frame.length)
82
+ return false;
83
+ if (depth !== 1)
84
+ continue;
85
+ var next = index + 1;
86
+ while (/\s/.test((_a = frame[next]) !== null && _a !== void 0 ? _a : ""))
87
+ next++;
88
+ if (frame[next] !== ":")
89
+ continue;
90
+ var key = void 0;
91
+ try {
92
+ key = JSON.parse(frame.slice(start, index + 1));
93
+ }
94
+ catch (_b) {
95
+ return false;
96
+ }
97
+ if (typeof key === "string" && securityKeys.has(key)) {
98
+ if (seen.has(key))
99
+ return true;
100
+ seen.add(key);
101
+ }
102
+ }
103
+ return false;
104
+ }
105
+ /**
106
+ * Captures the document secret in a closure. Neither the returned codec nor
107
+ * parsed native messages expose it.
108
+ */
109
+ export function createV3DocumentCodec(binding) {
110
+ if (!isRecord(binding) ||
111
+ typeof binding.sessionId !== "string" ||
112
+ binding.sessionId.length === 0 ||
113
+ typeof binding.secret !== "string" ||
114
+ binding.secret.length === 0) {
115
+ return { ok: false, error: "INVALID_DOCUMENT_BINDING" };
116
+ }
117
+ var sessionId = binding.sessionId, secret = binding.secret;
118
+ return {
119
+ ok: true,
120
+ value: {
121
+ encode: function (kind, payload) {
122
+ if (!documentToNativeKinds.has(kind) || !isRecord(payload)) {
123
+ return { ok: false, error: "INVALID_DOCUMENT_PAYLOAD" };
124
+ }
125
+ if (["v", "kind", "sessionId", "secret"].some(function (field) { return field in payload; })) {
126
+ return { ok: false, error: "SECURITY_FIELD_IN_PAYLOAD" };
127
+ }
128
+ return {
129
+ ok: true,
130
+ value: __assign(__assign({}, payload), { v: V3_PROTOCOL, kind: kind, sessionId: sessionId, secret: secret }),
131
+ };
132
+ },
133
+ parse: function (frame, maxFrameBytes) {
134
+ if (maxFrameBytes === void 0) { maxFrameBytes = DEFAULT_MAX_V3_FRAME_BYTES; }
135
+ // Envelope checks deliberately precede typed payload handling.
136
+ if (new TextEncoder().encode(frame).byteLength > maxFrameBytes) {
137
+ return { ok: false, error: "FRAME_TOO_LARGE" };
138
+ }
139
+ if (hasDuplicateTopLevelSecurityKey(frame)) {
140
+ return { ok: false, error: "MALFORMED_ENVELOPE" };
141
+ }
142
+ var candidate;
143
+ try {
144
+ candidate = JSON.parse(frame);
145
+ }
146
+ catch (_a) {
147
+ return { ok: false, error: "MALFORMED_ENVELOPE" };
148
+ }
149
+ if (!isRecord(candidate))
150
+ return { ok: false, error: "MALFORMED_ENVELOPE" };
151
+ if (candidate.v !== V3_PROTOCOL) {
152
+ return { ok: false, error: "UNSUPPORTED_VERSION" };
153
+ }
154
+ if (typeof candidate.kind !== "string" ||
155
+ !nativeToDocumentKinds.has(candidate.kind)) {
156
+ return { ok: false, error: "UNSUPPORTED_NATIVE_KIND" };
157
+ }
158
+ if (typeof candidate.sessionId !== "string" || candidate.sessionId.length === 0) {
159
+ return { ok: false, error: "MALFORMED_ENVELOPE" };
160
+ }
161
+ if (candidate.sessionId !== sessionId)
162
+ return { ok: false, error: "SESSION_MISMATCH" };
163
+ if (Object.prototype.hasOwnProperty.call(candidate, "secret")) {
164
+ return { ok: false, error: "UNEXPECTED_SECRET" };
165
+ }
166
+ var _v = candidate.v, _kind = candidate.kind, _sessionId = candidate.sessionId, payload = __rest(candidate, ["v", "kind", "sessionId"]);
167
+ return {
168
+ ok: true,
169
+ value: { kind: candidate.kind, payload: payload },
170
+ };
171
+ },
172
+ },
173
+ };
174
+ }
175
+ /**
176
+ * Atomically takes the host-installed one-shot handoff. The secret remains in
177
+ * the returned codec closure and is never copied into bridge globals/config.
178
+ */
179
+ export function consumeV3Bootstrap() {
180
+ if (typeof window === "undefined")
181
+ return { kind: "absent" };
182
+ var take = window.__LingXiaTakeControlBootstrap;
183
+ if (typeof take !== "function")
184
+ return { kind: "absent" };
185
+ try {
186
+ var bootstrap = take();
187
+ if (!bootstrap ||
188
+ bootstrap.requiredProtocol !== V3_PROTOCOL ||
189
+ typeof bootstrap.publicSessionId !== "string" ||
190
+ bootstrap.publicSessionId.length === 0 ||
191
+ typeof bootstrap.secret !== "string" ||
192
+ bootstrap.secret.length === 0) {
193
+ return { kind: "blocked" };
194
+ }
195
+ var codec = createV3DocumentCodec({
196
+ sessionId: bootstrap.publicSessionId,
197
+ secret: bootstrap.secret,
198
+ });
199
+ return codec.ok
200
+ ? { kind: "required", codec: codec.value }
201
+ : { kind: "blocked" };
202
+ }
203
+ catch (_a) {
204
+ return { kind: "blocked" };
205
+ }
206
+ finally {
207
+ // A malformed replacement must not leave a callable handoff for later.
208
+ try {
209
+ delete window.__LingXiaTakeControlBootstrap;
210
+ }
211
+ catch (_b) {
212
+ // A hostile non-configurable property has no attested secret to retain.
213
+ }
214
+ }
215
+ }
@@ -83,7 +83,7 @@ function applyDisplayLanguage(next) {
83
83
  try {
84
84
  for (var _b = __values(__spreadArray([], __read(current.listeners), false)), _c = _b.next(); !_c.done; _c = _b.next()) {
85
85
  var listener = _c.value;
86
- listener();
86
+ listener(normalized);
87
87
  }
88
88
  }
89
89
  catch (e_1_1) { e_1 = { error: e_1_1 }; }
@@ -104,7 +104,14 @@ if (typeof window !== 'undefined' && !window.__lingxiaApplyDisplayLanguage) {
104
104
  export function getDisplayLanguage() {
105
105
  return store().value;
106
106
  }
107
- /** Subscribe to host display-language changes. Returns an unsubscribe. */
107
+ /**
108
+ * Subscribe to host display-language changes. Returns an unsubscribe.
109
+ *
110
+ * Change-only, so that this composes with `useSyncExternalStore`: the listener
111
+ * runs when the language changes, never on subscribe. Read the current value
112
+ * with `getDisplayLanguage()`. Logic's `lx.app.displayLanguage.watch` differs
113
+ * deliberately — it has no render loop to feed, so it delivers immediately.
114
+ */
108
115
  export function subscribeDisplayLanguage(listener) {
109
116
  var listeners = store().listeners;
110
117
  listeners.add(listener);
package/dist/es5/types.js CHANGED
@@ -13,4 +13,5 @@ export var BRIDGE_ERROR = {
13
13
  OUTBOX_FULL: 'BRIDGE_OUTBOX_FULL',
14
14
  STREAM_OVERFLOW: 'BRIDGE_STREAM_OVERFLOW',
15
15
  STREAM_CLOSED: 'BRIDGE_STREAM_CLOSED',
16
+ MESSAGE_TOO_LARGE: 'BRIDGE_MESSAGE_TOO_LARGE',
16
17
  };
@@ -0,0 +1,43 @@
1
+ /** Document-side framing for host-attested RequiredV3 documents. */
2
+ export declare const V3_PROTOCOL: 3;
3
+ export declare const DEFAULT_MAX_V3_FRAME_BYTES: number;
4
+ export type V3DocumentToNativeKind = "hello" | "req" | "res" | "notify" | "cancel" | "ch.open" | "ch.data" | "ch.close" | "state.ack" | "console";
5
+ export type V3NativeToDocumentKind = "helloAck" | "ready" | "req" | "res" | "event" | "state.snapshot" | "state.patch" | "ch.ack" | "ch.data" | "ch.close";
6
+ export type V3CodecError = "FRAME_TOO_LARGE" | "MALFORMED_ENVELOPE" | "UNSUPPORTED_VERSION" | "UNSUPPORTED_NATIVE_KIND" | "INVALID_DOCUMENT_BINDING" | "INVALID_DOCUMENT_PAYLOAD" | "SECURITY_FIELD_IN_PAYLOAD" | "SESSION_MISMATCH" | "UNEXPECTED_SECRET";
7
+ export type V3CodecResult<T> = {
8
+ readonly ok: true;
9
+ readonly value: T;
10
+ } | {
11
+ readonly ok: false;
12
+ readonly error: V3CodecError;
13
+ };
14
+ export type V3DocumentBinding = {
15
+ readonly sessionId: string;
16
+ readonly secret: string;
17
+ };
18
+ export type V3NativeEnvelope = {
19
+ readonly kind: V3NativeToDocumentKind;
20
+ readonly payload: Readonly<Record<string, unknown>>;
21
+ };
22
+ export type V3DocumentCodec = {
23
+ encode(kind: V3DocumentToNativeKind, payload: Record<string, unknown>): V3CodecResult<Record<string, unknown>>;
24
+ parse(frame: string, maxFrameBytes?: number): V3CodecResult<V3NativeEnvelope>;
25
+ };
26
+ export type V3BootstrapActivation = {
27
+ readonly kind: "absent";
28
+ } | {
29
+ readonly kind: "required";
30
+ readonly codec: V3DocumentCodec;
31
+ } | {
32
+ readonly kind: "blocked";
33
+ };
34
+ /**
35
+ * Captures the document secret in a closure. Neither the returned codec nor
36
+ * parsed native messages expose it.
37
+ */
38
+ export declare function createV3DocumentCodec(binding: V3DocumentBinding): V3CodecResult<V3DocumentCodec>;
39
+ /**
40
+ * Atomically takes the host-installed one-shot handoff. The secret remains in
41
+ * the returned codec closure and is never copied into bridge globals/config.
42
+ */
43
+ export declare function consumeV3Bootstrap(): V3BootstrapActivation;
@@ -3,8 +3,15 @@ export type CommunicationMethod = 'messageport' | 'jsinterface' | 'webmessage' |
3
3
  export type PlatformOS = NonNullable<BridgeConfig['os']> | 'unknown';
4
4
  export declare const BRIDGE_CONFIG: BridgeConfig;
5
5
  export declare function getDisplayLanguage(): string;
6
- /** Subscribe to host display-language changes. Returns an unsubscribe. */
7
- export declare function subscribeDisplayLanguage(listener: () => void): () => void;
6
+ /**
7
+ * Subscribe to host display-language changes. Returns an unsubscribe.
8
+ *
9
+ * Change-only, so that this composes with `useSyncExternalStore`: the listener
10
+ * runs when the language changes, never on subscribe. Read the current value
11
+ * with `getDisplayLanguage()`. Logic's `lx.app.displayLanguage.watch` differs
12
+ * deliberately — it has no render loop to feed, so it delivers immediately.
13
+ */
14
+ export declare function subscribeDisplayLanguage(listener: (language: string) => void): () => void;
8
15
  export declare function getPlatformOS(): PlatformOS;
9
16
  export declare function isHarmony(): boolean;
10
17
  export declare function isIOS(): boolean;
package/dist/types.d.ts CHANGED
@@ -21,6 +21,18 @@ export interface ErrorInfo {
21
21
  reason?: string;
22
22
  code?: number;
23
23
  }
24
+ /**
25
+ * Private, one-shot handoff installed only in host-attested internal HTML.
26
+ *
27
+ * This is intentionally not part of the public bridge API and V2 never reads
28
+ * it. A future V3 bootstrap consumes the handoff before protocol activation.
29
+ */
30
+ interface ControlBootstrap {
31
+ readonly requiredProtocol: 3;
32
+ readonly publicSessionId: string;
33
+ readonly secret: string;
34
+ }
35
+ type TakeControlBootstrap = () => ControlBootstrap | undefined;
24
36
  export declare const BRIDGE_ERROR: {
25
37
  readonly NOT_READY: "BRIDGE_NOT_READY";
26
38
  readonly TIMEOUT: "BRIDGE_TIMEOUT";
@@ -35,6 +47,7 @@ export declare const BRIDGE_ERROR: {
35
47
  readonly OUTBOX_FULL: "BRIDGE_OUTBOX_FULL";
36
48
  readonly STREAM_OVERFLOW: "BRIDGE_STREAM_OVERFLOW";
37
49
  readonly STREAM_CLOSED: "BRIDGE_STREAM_CLOSED";
50
+ readonly MESSAGE_TOO_LARGE: "BRIDGE_MESSAGE_TOO_LARGE";
38
51
  };
39
52
  export type BridgeErrorCode = (typeof BRIDGE_ERROR)[keyof typeof BRIDGE_ERROR];
40
53
  export interface LxBridgeError {
@@ -132,16 +145,24 @@ declare global {
132
145
  };
133
146
  __LX_BRIDGE_INIT_STATE?: 'initializing' | 'initialized';
134
147
  __LX_RUNTIME_CONFIG?: RuntimeConfig;
148
+ /**
149
+ * Host-attested, one-shot V3 bootstrap handoff. It is absent for legacy
150
+ * scheme loads, assets, and all untrusted documents.
151
+ */
152
+ __LingXiaTakeControlBootstrap?: TakeControlBootstrap;
135
153
  __pageBridge?: {
136
154
  __names: string[];
137
155
  [key: string]: unknown;
138
156
  };
139
157
  __LingXiaRecvMessage?: (message: string) => void;
158
+ /** WebView2 inject script queues inbound payloads here until the bridge binds `__LingXiaRecvMessage`. */
159
+ __LingXiaEarlyNativeMessages?: string[];
140
160
  LingXiaBridge?: LingXiaBridgeInterface;
141
161
  LingXiaProxy?: {
142
162
  supportsMessagePort: () => boolean;
143
163
  getPort: (name: string) => string;
144
164
  postMessage: (message: string) => void;
165
+ nativeComponentUpdate?: ((payload: string) => void) | ((componentId: string, payload: string) => void);
145
166
  };
146
167
  NativeComponentBridge?: {
147
168
  postMessage: (message: string) => void;
@@ -171,6 +192,11 @@ export interface LingXiaBridgeInterface {
171
192
  proto: boolean;
172
193
  all: boolean;
173
194
  };
195
+ displayLanguage: {
196
+ get(): string;
197
+ /** Change-only; read the current value with `get()`. */
198
+ subscribe(listener: (language: string) => void): () => void;
199
+ };
174
200
  platform: {
175
201
  isHarmony(): boolean;
176
202
  isIOS(): boolean;
@@ -211,3 +237,4 @@ export interface StreamOptions extends StreamCallOptions {
211
237
  }
212
238
  export interface ChannelOptions extends ChannelOpenOptions {
213
239
  }
240
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingxia/bridge",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "private": false,
5
5
  "description": "LingXia Web Runtime - Bridge, boot, and error handling for WebView",
6
6
  "repository": {
@@ -39,6 +39,8 @@
39
39
  "build:modules:es2020": "tsc -p tsconfig.modules.json",
40
40
  "build:modules:es5": "tsc -p tsconfig.modules.legacy.json",
41
41
  "build:types": "tsc -p tsconfig.json --emitDeclarationOnly",
42
+ "build:v3-test": "tsc -p tsconfig.v3-test.json",
43
+ "test": "npm run build:es2020 && npm run build:es5 && npm run build:v3-test && node ./test-support/test-protocol-v3.mjs && node ./test-support/test-bridge-required-v3.mjs && node ./test-support/test-control-bootstrap.mjs",
42
44
  "dev": "rolldown -c -w",
43
45
  "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
44
46
  "prepublishOnly": "npm run build"