@spopc/wechat-miniprogram 0.0.0-stage → 0.1.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/README.md CHANGED
@@ -1,3 +1,148 @@
1
- # Temporary Holding Version
1
+ # @spopc/wechat-miniprogram
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ H5 调用微信小程序原生能力的 TypeScript SDK。使用 URL 协议和一个自动执行的原生页面,零 Taro / React / Vue 运行时依赖。
4
+
5
+ ## 安装
6
+
7
+ ```sh
8
+ npm install @spopc/wechat-miniprogram
9
+ ```
10
+
11
+ 包提供 ESM 和 TypeScript 类型声明,供浏览器 H5 项目使用;在客户端创建实例,SSR 项目需放在挂载后的客户端代码中。普通浏览器可直接使用 mock,真实微信能力还需要小程序宿主实现本文的 URL 协议。仅安装 H5 SDK 不会自动添加原生小程序页面。
12
+
13
+ 源码仓库内使用 npm workspace 开发,无需先发布到 npm。
14
+
15
+ ## 快速接入
16
+
17
+ 默认自动识别环境:小程序 WebView 使用原生桥接,普通浏览器使用 mock。真实小程序调用需要网页加载微信 JSSDK:`https://res.wx.qq.com/open/js/jweixin-1.6.0.js`;浏览器 mock 无需加载它。每个页面创建一个 SDK 实例。
18
+
19
+ ```ts
20
+ import { createWechatBridge } from '@spopc/wechat-miniprogram'
21
+
22
+ const bridge = createWechatBridge({
23
+ mode: 'auto', // 默认值;也可强制 'native' 或 'mock'
24
+ onResult({ result, delivery, source }) {
25
+ // 唯一业务结果入口:live 与文档重建后的 recovered 都经过这里。
26
+ // 若同时消费 Promise,请避免对同一调用重复执行业务副作用。
27
+ console.log(result.method, result.status, delivery, source)
28
+ }
29
+ })
30
+
31
+ // 由 H5 上的用户操作触发;原生页自动执行,无需再点“执行”和“返回”。
32
+ const scan = await bridge.scanCode({ scanType: ['qrCode', 'barCode'] })
33
+ const position = await bridge.getLocation({ type: 'gcj02' })
34
+ const { code } = await bridge.login()
35
+ // 将 code 提交给自己的业务后端,换取业务登录态;不要把 AppSecret 放在 H5。
36
+ ```
37
+
38
+ 上述调用是顺序示例;根据实际按钮分别触发。页面卸载 / 组件卸载时调用 `bridge.dispose()`。它移除监听并拒绝当前 Promise,原生模式保留请求记录以支持文档重建。
39
+
40
+ ## 自动识别与 Mock
41
+
42
+ ```ts
43
+ const bridge = createWechatBridge() // 等同于 { mode: 'auto' }
44
+ const environment = await bridge.ready() // 'native' | 'mock'
45
+ console.log(bridge.environment, bridge.isMock)
46
+ const location = await bridge.getLocation() // 调用本身也会等待识别,无需手动 ready
47
+ ```
48
+
49
+ `auto` 结合小程序标记、User-Agent、`wx.miniProgram.getEnv` 和 `WeixinJSBridgeReady` 识别。普通浏览器直接使用 mock,即使网页加载了微信 JSSDK 也不会被误判为小程序。微信容器仍在初始化时,默认最多等待 2000 ms;可用 `environmentTimeoutMs` 调整为 100–30000 ms。无法确认时抛出 `ENVIRONMENT_TIMEOUT`,加载完成后可重试 `ready()` / 调用,不把未知环境当成模拟成功。
50
+
51
+ 自动识别完成后,本实例固定使用选定模式。真实调用的权限拒绝、导航失败、超时等不会退回 mock。已识别为小程序但缺少 JSSDK 时抛出 `JSSDK_NOT_READY`。`mode: 'native'` 可以强制真实调用,浏览器中会报 `NOT_IN_MINIPROGRAM`;`mode: 'mock'` 可以在任何环境强制模拟。`isInMiniProgram()` 检测实际环境,不会因强制 mock 返回假值。检测未完成时 `environment` 为 undefined,应等待 `ready()` 再读取 `isMock`。
52
+
53
+ 8 项能力均内置成功数据:固定模拟坐标、门店码、无效的 MOCK 登录 code、虚构地址及成功标记。可覆盖任意方法,其余保持默认:
54
+
55
+ ```ts
56
+ import { createWechatBridge, BridgeError } from '@spopc/wechat-miniprogram'
57
+
58
+ const bridge = createWechatBridge({
59
+ mode: 'auto',
60
+ mock: {
61
+ delayMs: 500, // 默认 300;0–600000 ms,计入调用超时
62
+ handlers: {
63
+ getLocation: params => ({
64
+ latitude: 22.5431, longitude: 114.0579,
65
+ accuracy: 10, type: params.type ?? 'gcj02'
66
+ }),
67
+ scanCode: async (_params, { signal, requestId }) => {
68
+ // 可按业务场景返回自定义扫码数据;signal 在超时/销毁/完成时终止。
69
+ return { result: 'tea-store://demo?table=08', scanType: 'QR_CODE' }
70
+ },
71
+ chooseAddress: () => { throw new BridgeError('CANCELLED', '模拟用户取消') },
72
+ checkSession: () => { throw new BridgeError('SESSION_EXPIRED', '模拟登录过期') }
73
+ }
74
+ }
75
+ })
76
+ ```
77
+
78
+ mock 与真实调用共用参数/结果校验、Promise 类型、单次并发限制和调用超时。`CANCELLED` 产生 cancel 事件;其他 `BridgeError` 保留错误码;普通异常变成 `MOCK_FAILED`,不合法/超大的返回值变成 `INVALID_MOCK_RESULT`。超时或 dispose 后的迟到结果被丢弃,自定义异步工作可响应 `signal` 提前结束。
79
+
80
+ mock 的 `onResult.source`、`trigger` 均为 `mock`,`delivery` 固定为 `live`;只在当前内存中返回数据,不跳页面、不读写原生 pending 记录、不消费或修改 URL。即使传入调用参数 `mode: 'query'`,也不模拟重载,因此不能用来验证 WebView 的 Hash / Query 传输和文档重建。模拟登录 code 不能交给微信换登录态,模拟支付不会产生订单或真实支付;业务后端联调需配套测试数据。
81
+
82
+ 测试页直接打开 `http://127.0.0.1:13334/` 即自动进入 mock,可选成功/取消/失败。`?mode=native`、`?mode=mock` 强制模式,旧 `?mock=1` 也可用。这些 URL 开关只属于测试页面,SDK 本身不读取它们。
83
+
84
+ ## 方法与参数
85
+
86
+ 所有快捷方法接受 `mode?: 'hash' | 'query'`、`timeoutMs?: number`。这里的 mode 指返回 URL 格式,与创建 SDK 时的运行模式不同。默认 Hash、120000 毫秒;超时可设 1000–600000 毫秒。`login`、`chooseAddress`、`requestPayment` 只允许 Hash。
87
+
88
+ | 方法 | 原生参数 | 成功结果 |
89
+ | --- | --- | --- |
90
+ | `getLocation(options?)` | type: gcj02(默认)或 wgs84 | latitude、longitude、accuracy、type |
91
+ | `scanCode(options?)` | onlyFromCamera?: boolean;scanType?: qrCode / barCode / datamatrix / pdf417 数组 | result、scanType;可选 charSet、path、rawData |
92
+ | `login(options?)` | timeout?: 1000–60000,微信接口自身超时 | code |
93
+ | `checkSession(options?)` | 无 | valid: true |
94
+ | `openLocation(options)` | GCJ-02 latitude、longitude;可选 name、address、scale(5–18) | ok: true |
95
+ | `chooseAddress(options?)` | 无 | userName、telNumber、provinceName、cityName、countyName、detailInfo,以及平台返回的可选地址字段 |
96
+ | `makePhoneCall(options)` | phoneNumber | ok: true |
97
+ | `requestPayment(options)` | timeStamp、nonceStr、package、signType、paySign | ok: true |
98
+
99
+ 底层通用入口也有方法与参数、结果的类型关联:
100
+
101
+ ```ts
102
+ const result = await bridge.invoke('scanCode', { onlyFromCamera: true }, { mode: 'hash' })
103
+ await bridge.openLocation({ latitude: 31.2, longitude: 121.4, name: '门店', scale: 16 })
104
+ // 实际支付参数必须由业务后端签发:
105
+ // await bridge.requestPayment(paymentParamsFromYourServer)
106
+ ```
107
+
108
+ `requestPayment.package` 使用 `prepay_id=...` 格式,`signType` 明确传 RSA / MD5 / HMAC-SHA256。支付没有自动重试;最终订单状态以业务后端的支付通知或查单结果为准。`makePhoneCall` 成功只表示原生接口调用成功,不表示电话已接通。
109
+
110
+ ## 返回与错误
111
+
112
+ 同一网页上下文存活时,成功 resolve 对应能力的数据,失败或取消 reject `BridgeError`。`onResult` 同时收到结构化事件:
113
+
114
+ ```ts
115
+ {
116
+ result: {
117
+ v: 1, id, method, mode, completedAt,
118
+ status: 'success', data // 或 status: 'cancel' | 'error', error: { code, message }
119
+ },
120
+ delivery: 'live', // 或 'recovered'
121
+ source: 'native', // 或 'mock'
122
+ trigger: 'hashchange' // 或 startup / pageshow / visible / mock
123
+ }
124
+ ```
125
+
126
+ 网页重建时旧 Promise 无法继续,必须在新页面重新创建 SDK 并使用 `onResult` 恢复。回传按 id、method、mode 同时关联,消费后清理 URL 和 sessionStorage,一次调用最多交付一次。
127
+
128
+ 常见错误:`CANCELLED`、`PERMISSION_DENIED`、`NATIVE_FAILED`、`SESSION_EXPIRED`、`UNSUPPORTED`、`TIMEOUT`、`RESULT_TOO_LARGE`、`INVALID_NATIVE_RESULT`。H5 侧还会产生 `BUSY`、`NOT_IN_MINIPROGRAM`、`ENVIRONMENT_TIMEOUT`、`JSSDK_NOT_READY`、`INVALID_PARAMS`、`STORAGE_UNAVAILABLE`、`NAVIGATE_FAILED`、`DISPOSED`、`MOCK_FAILED`、`INVALID_MOCK_RESULT`。微信错误原文保留在 message,不依赖它处理分支。H5 本地校验、环境识别、导航失败和本地等待超时通过 Promise 拒绝;它们没有原生回传,因此不产生 `onResult`。
129
+
130
+ `checkSession` 仅检查微信 session_key,不验证业务服务自己的 token。`login` 不获取手机号、头像或昵称;手机号授权需要原生按钮交互,不属于本版静默桥接范围。
131
+
132
+ ## 宿主契约与扩展
133
+
134
+ 默认原生路由 `/native/invoke`,通过 `request` URL 参数携带 JSON:v、id、method、params、mode、returnUrl、createdAt、expiresAt。可用 `nativePath` 更改路由,原生宿主须实现相同协议。旧的 `/native/location` 在示例宿主保留兼容。
135
+
136
+ 原生页解析请求后,在页面 ready 时调用注册的 handler;成功、取消、失败或超时后自动 navigateBack。宿主把结果添加到 WebView src 的 `__mp_bridge` 字段。JS 协议模块同时导出于 `@spopc/wechat-miniprogram/protocol`,可供小程序端使用,不依赖 DOM、URL、Node API。
137
+
138
+ 当前支持的方法采用明确注册表,拒绝未知方法和额外参数,不开放任意 wx 方法执行。扩展能力时同步修改协议类型与校验、宿主 handler、SDK 快捷方法和测试。
139
+
140
+ 默认只适配示例中允许的业务域名。请求与结果 JSON 的应用上限为 16,384 字符,宿主/微信版本可能有更低 URL 限制;大内容应走业务后端引用。Hash 路由需兼容临时的 `__mp_bridge` 字段。超时后原生操作本身不一定可撤销,尤其支付不得仅因超时直接重试。
141
+
142
+ 中间页无需用户操作,但扫码、地图、收货地址、拨号、支付和权限提示由微信展示,不能承诺无感或没有路由动画。扫码 UI 及支付参数约定见 [Taro 扫码文档](https://docs.taro.zone/docs/apis/device/scan/scanCode)、[Taro 支付文档](https://docs.taro.zone/docs/apis/payment/requestPayment)。
143
+
144
+ ## 已验证范围
145
+
146
+ 2026-10-13,Windows 微信开发者工具 Stable 2.02.2608070 / 基础库 3.17.4:自动 login、getLocation、checkSession、scanCode 成功及扫码取消均完成往返,通过 Hash 在同一页面收到 live 结果。扫码使用开发者工具选择测试二维码图片,不代表真机相机扫描已测试。初版定位页另验证过 Query 重载与 recovered 恢复。iOS / Android 真机、真实支付、拨号、地图、个人地址尚未实测。
147
+
148
+ 构建:`npm run build`。打包检查:`npm pack --dry-run`。包只包含 dist、README 和 package.json。
@@ -0,0 +1,19 @@
1
+ export interface WechatWindow extends Window {
2
+ __wxjs_environment?: string;
3
+ WeixinJSBridge?: unknown;
4
+ wx?: {
5
+ miniProgram?: {
6
+ getEnv(callback: (env: {
7
+ miniprogram: boolean;
8
+ }) => void): void;
9
+ navigateTo(options: {
10
+ url: string;
11
+ fail?: (error: {
12
+ errMsg?: string;
13
+ }) => void;
14
+ }): void;
15
+ };
16
+ };
17
+ }
18
+ /** A negative result must identify a browser, not an unfinished WeChat initialization. */
19
+ export declare function detectMiniProgram(browser: WechatWindow, signal: AbortSignal, timeoutMs: number): Promise<boolean>;
@@ -0,0 +1,59 @@
1
+ import { BridgeError } from './errors.js';
2
+ /** A negative result must identify a browser, not an unfinished WeChat initialization. */
3
+ export function detectMiniProgram(browser, signal, timeoutMs) {
4
+ if (signal.aborted)
5
+ return Promise.reject(new BridgeError('DISPOSED', 'SDK 已销毁'));
6
+ const userAgent = browser.navigator?.userAgent ?? '';
7
+ const marked = () => browser.__wxjs_environment === 'miniprogram' || /\bminiProgram\b/i.test(userAgent);
8
+ if (marked())
9
+ return Promise.resolve(true);
10
+ // Loading jweixin.js in Chrome also creates wx.miniProgram, but getEnv may never
11
+ // call back there. Its presence alone is not evidence of a WeChat container.
12
+ if (!/MicroMessenger|wechatdevtools/i.test(userAgent) && !browser.WeixinJSBridge)
13
+ return Promise.resolve(false);
14
+ return new Promise((resolve, reject) => {
15
+ let done = false;
16
+ let queried;
17
+ let poll;
18
+ const finish = (inside, error) => {
19
+ if (done)
20
+ return;
21
+ done = true;
22
+ clearTimeout(deadline);
23
+ clearTimeout(poll);
24
+ browser.document.removeEventListener('WeixinJSBridgeReady', check);
25
+ signal.removeEventListener('abort', aborted);
26
+ if (error)
27
+ reject(error);
28
+ else
29
+ resolve(!!inside);
30
+ };
31
+ const aborted = () => finish(undefined, new BridgeError('DISPOSED', 'SDK 已销毁'));
32
+ const deadline = setTimeout(() => finish(undefined, new BridgeError('ENVIRONMENT_TIMEOUT', '微信环境尚未就绪,请在 JSSDK 加载完成后重试')), timeoutMs);
33
+ function check() {
34
+ if (done)
35
+ return;
36
+ clearTimeout(poll);
37
+ if (marked()) {
38
+ finish(true);
39
+ return;
40
+ }
41
+ const api = browser.wx?.miniProgram;
42
+ if (api && api !== queried && typeof api.getEnv === 'function') {
43
+ queried = api;
44
+ try {
45
+ api.getEnv(env => { if (typeof env?.miniprogram === 'boolean')
46
+ finish(marked() || env.miniprogram); });
47
+ }
48
+ catch {
49
+ queried = undefined; /* Retry after bridge readiness. */
50
+ }
51
+ }
52
+ if (!done)
53
+ poll = setTimeout(check, 50);
54
+ }
55
+ browser.document.addEventListener('WeixinJSBridgeReady', check);
56
+ signal.addEventListener('abort', aborted, { once: true });
57
+ check();
58
+ });
59
+ }
@@ -0,0 +1,4 @@
1
+ export declare class BridgeError extends Error {
2
+ code: string;
3
+ constructor(code: string, message: string);
4
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,7 @@
1
+ export class BridgeError extends Error {
2
+ constructor(code, message) {
3
+ super(message);
4
+ this.code = code;
5
+ this.name = 'BridgeError';
6
+ }
7
+ }
@@ -0,0 +1,54 @@
1
+ import { type BridgeResult, type BridgeMethod, type ParamsOf, type DataOf, type ReturnMode } from './protocol.js';
2
+ import { type MockOptions } from './mock.js';
3
+ export * from './protocol.js';
4
+ export { BridgeError } from './errors.js';
5
+ export { defaultMockHandlers, type MockOptions, type MockHandlers, type MockHandler, type MockContext } from './mock.js';
6
+ export type BridgeMode = 'auto' | 'native' | 'mock';
7
+ export type BridgeEnvironment = 'native' | 'mock';
8
+ export interface ResultEvent {
9
+ result: BridgeResult;
10
+ source: 'native' | 'mock';
11
+ delivery: 'live' | 'recovered';
12
+ trigger: string;
13
+ }
14
+ export interface BridgeOptions {
15
+ /** Default: auto. WeChat mini-programs use native APIs; other browsers use mock. */
16
+ mode?: BridgeMode;
17
+ mock?: MockOptions;
18
+ /** Maximum wait for an unfinished WeChat environment. Default: 2000 ms. */
19
+ environmentTimeoutMs?: number;
20
+ timeoutMs?: number;
21
+ nativePath?: string;
22
+ onResult?: (event: ResultEvent) => void;
23
+ onDiagnostic?: (message: string) => void;
24
+ }
25
+ export interface CallOptions {
26
+ mode?: ReturnMode;
27
+ timeoutMs?: number;
28
+ }
29
+ export declare function createWechatBridge(options?: BridgeOptions): {
30
+ invoke: <M extends BridgeMethod>(method: M, params: ParamsOf<M>, callOptions?: CallOptions) => Promise<DataOf<M>>;
31
+ isInMiniProgram: () => Promise<boolean>;
32
+ ready: () => Promise<BridgeEnvironment>;
33
+ readonly environment: BridgeEnvironment | undefined;
34
+ readonly isMock: boolean;
35
+ getLocation: (input?: ParamsOf<"getLocation"> & CallOptions) => Promise<import("./protocol.js").LocationData>;
36
+ scanCode: (input?: ParamsOf<"scanCode"> & CallOptions) => Promise<import("./protocol.js").ScanCodeData>;
37
+ login: (input?: ParamsOf<"login"> & CallOptions) => Promise<{
38
+ code: string;
39
+ }>;
40
+ checkSession: (input?: CallOptions) => Promise<{
41
+ valid: true;
42
+ }>;
43
+ chooseAddress: (input?: CallOptions) => Promise<import("./protocol.js").AddressData>;
44
+ openLocation: (input: ParamsOf<"openLocation"> & CallOptions) => Promise<{
45
+ ok: true;
46
+ }>;
47
+ makePhoneCall: (input: ParamsOf<"makePhoneCall"> & CallOptions) => Promise<{
48
+ ok: true;
49
+ }>;
50
+ requestPayment: (input: ParamsOf<"requestPayment"> & CallOptions) => Promise<{
51
+ ok: true;
52
+ }>;
53
+ dispose(): void;
54
+ };
package/dist/index.js ADDED
@@ -0,0 +1,272 @@
1
+ import { buildNativeUrl, readReturnUrl, normalizeParams, parseRequest, DEFAULT_TIMEOUT_MS } from './protocol.js';
2
+ import { BridgeError } from './errors.js';
3
+ import { createMockTransport } from './mock.js';
4
+ import { detectMiniProgram } from './environment.js';
5
+ export * from './protocol.js';
6
+ export { BridgeError } from './errors.js';
7
+ export { defaultMockHandlers } from './mock.js';
8
+ // Keep the existing key so a package rename cannot orphan an in-flight native request.
9
+ const STORAGE_KEY = 'brazosd.bridge.pending.v1';
10
+ export function createWechatBridge(options = {}) {
11
+ const browser = window;
12
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
13
+ const mode = options.mode ?? 'auto';
14
+ if (!['auto', 'native', 'mock'].includes(mode))
15
+ throw new BridgeError('INVALID_PARAMS', 'Invalid SDK mode');
16
+ const environmentTimeoutMs = options.environmentTimeoutMs ?? 2000;
17
+ if (!Number.isFinite(environmentTimeoutMs) || environmentTimeoutMs < 100 || environmentTimeoutMs > 30000)
18
+ throw new BridgeError('INVALID_PARAMS', 'environmentTimeoutMs must be between 100 and 30000');
19
+ const simulate = createMockTransport(options.mock ?? {});
20
+ let mockTransport = mode === 'mock' ? simulate : undefined;
21
+ let environment = mode === 'auto' ? undefined : mode;
22
+ let readiness;
23
+ let listening = false;
24
+ const environmentController = new AbortController();
25
+ let disposed = false;
26
+ let launching = false;
27
+ let active;
28
+ const diagnostic = (message) => { try {
29
+ options.onDiagnostic?.(message);
30
+ }
31
+ catch { /* Consumer logging cannot break transport. */ } };
32
+ function loadPending() {
33
+ if (mockTransport)
34
+ return;
35
+ try {
36
+ const value = JSON.parse(browser.sessionStorage.getItem(STORAGE_KEY) || 'null');
37
+ if (!value || !Number.isFinite(value.deadline))
38
+ return;
39
+ return { request: parseRequest(JSON.stringify(value.request)), deadline: value.deadline };
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ function removePending() { if (!mockTransport) {
46
+ try {
47
+ browser.sessionStorage.removeItem(STORAGE_KEY);
48
+ }
49
+ catch { /* Memory delivery still works. */ }
50
+ } }
51
+ function fail(code, message) {
52
+ const waiting = active;
53
+ active = undefined;
54
+ if (waiting) {
55
+ clearTimeout(waiting.timer);
56
+ waiting.reject(new BridgeError(code, message));
57
+ waiting.cleanup?.();
58
+ }
59
+ removePending();
60
+ }
61
+ function publish(result, delivery, trigger) {
62
+ try {
63
+ options.onResult?.({ result, delivery, trigger, source: mockTransport ? 'mock' : 'native' });
64
+ }
65
+ catch {
66
+ diagnostic('onResult 回调抛出异常,SDK 已完成结果交付');
67
+ }
68
+ }
69
+ function consume(trigger) {
70
+ if (disposed || environment !== 'native')
71
+ return;
72
+ try {
73
+ const { result, cleanUrl } = readReturnUrl(browser.location.href);
74
+ if (!result)
75
+ return;
76
+ const saved = loadPending();
77
+ if ((!active || active.request.id !== result.id) && saved?.request.id !== result.id) {
78
+ diagnostic('忽略未关联或已消费的回传结果');
79
+ browser.history.replaceState(browser.history.state, '', cleanUrl);
80
+ return;
81
+ }
82
+ if (saved && saved.deadline < Date.now()) {
83
+ fail('TIMEOUT', '原生调用已超时');
84
+ browser.history.replaceState(browser.history.state, '', cleanUrl);
85
+ return;
86
+ }
87
+ const expected = active?.request.id === result.id ? active.request : saved?.request;
88
+ if (expected?.mode !== result.mode || expected.method !== result.method)
89
+ throw new Error('Return method/mode mismatch');
90
+ const waiting = active?.request.id === result.id ? active : undefined;
91
+ if (waiting)
92
+ clearTimeout(waiting.timer);
93
+ active = undefined;
94
+ removePending();
95
+ browser.history.replaceState(browser.history.state, '', cleanUrl);
96
+ if (waiting) {
97
+ if (result.status === 'success')
98
+ waiting.resolve(result.data);
99
+ else
100
+ waiting.reject(new BridgeError(result.error.code, result.error.message));
101
+ }
102
+ publish(result, waiting ? 'live' : 'recovered', trigger);
103
+ }
104
+ catch (error) {
105
+ diagnostic(`回传解析失败:${error instanceof Error ? error.message : String(error)}`);
106
+ }
107
+ }
108
+ const hashListener = () => consume('hashchange');
109
+ const showListener = () => consume('pageshow');
110
+ const visibleListener = () => { if (document.visibilityState === 'visible')
111
+ consume('visible'); };
112
+ function listen() {
113
+ if (listening || disposed || environment !== 'native')
114
+ return;
115
+ listening = true;
116
+ browser.addEventListener('hashchange', hashListener);
117
+ browser.addEventListener('pageshow', showListener);
118
+ document.addEventListener('visibilitychange', visibleListener);
119
+ Promise.resolve().then(() => consume('startup'));
120
+ }
121
+ function isInMiniProgram() {
122
+ return detectMiniProgram(browser, environmentController.signal, environmentTimeoutMs);
123
+ }
124
+ async function ready() {
125
+ if (disposed)
126
+ throw new BridgeError('DISPOSED', 'SDK 已销毁');
127
+ if (environment) {
128
+ listen();
129
+ return environment;
130
+ }
131
+ if (!readiness)
132
+ readiness = isInMiniProgram().then(inside => {
133
+ if (disposed)
134
+ throw new BridgeError('DISPOSED', 'SDK 已销毁');
135
+ environment = inside ? 'native' : 'mock';
136
+ mockTransport = inside ? undefined : simulate;
137
+ listen();
138
+ diagnostic(`运行环境:${environment}(自动识别)`);
139
+ return environment;
140
+ }).catch(error => { readiness = undefined; throw error; });
141
+ return readiness;
142
+ }
143
+ // Start recovery without requiring a first API call, and consume initialization failures.
144
+ void ready().catch(error => diagnostic(error instanceof Error ? error.message : String(error)));
145
+ async function invoke(method, params, callOptions = {}) {
146
+ if (disposed)
147
+ throw new BridgeError('DISPOSED', 'SDK 已销毁');
148
+ if (active || launching)
149
+ throw new BridgeError('BUSY', '已有一次调用正在进行');
150
+ launching = true;
151
+ try {
152
+ await ready();
153
+ }
154
+ finally {
155
+ launching = false;
156
+ }
157
+ if (disposed)
158
+ throw new BridgeError('DISPOSED', 'SDK 已销毁');
159
+ const saved = loadPending();
160
+ if (saved && saved.deadline > Date.now())
161
+ throw new BridgeError('BUSY', '有尚未完成的调用,请等待结果或超时');
162
+ let nativeParams;
163
+ const duration = callOptions.timeoutMs ?? timeoutMs;
164
+ const mode = callOptions.mode ?? 'hash';
165
+ try {
166
+ nativeParams = normalizeParams(method, params);
167
+ if (!Number.isFinite(duration) || duration < 1000 || duration > 600000)
168
+ throw new Error('timeoutMs must be between 1000 and 600000');
169
+ if (mode !== 'hash' && mode !== 'query')
170
+ throw new Error('Invalid return mode');
171
+ // Credentials and address details must not enter HTTP query logs.
172
+ if (mode === 'query' && ['login', 'chooseAddress', 'requestPayment'].includes(method))
173
+ throw new Error(`${method} requires hash mode`);
174
+ }
175
+ catch (error) {
176
+ throw new BridgeError('INVALID_PARAMS', error instanceof Error ? error.message : String(error));
177
+ }
178
+ if (!mockTransport) {
179
+ launching = true;
180
+ let inside = false;
181
+ try {
182
+ inside = await isInMiniProgram();
183
+ }
184
+ finally {
185
+ launching = false;
186
+ }
187
+ if (disposed)
188
+ throw new BridgeError('DISPOSED', 'SDK 已销毁');
189
+ if (!inside)
190
+ throw new BridgeError('NOT_IN_MINIPROGRAM', '请在微信小程序 WebView 中运行');
191
+ if (typeof browser.wx?.miniProgram?.navigateTo !== 'function')
192
+ throw new BridgeError('JSSDK_NOT_READY', '请在微信 JSSDK 加载完成后重试');
193
+ }
194
+ const createdAt = Date.now();
195
+ const request = parseRequest(JSON.stringify({
196
+ v: 1, id: browser.crypto.randomUUID(), method, mode,
197
+ returnUrl: mockTransport ? browser.location.href : readReturnUrl(browser.location.href).cleanUrl, createdAt,
198
+ expiresAt: createdAt + duration, params: nativeParams
199
+ }));
200
+ // Validate the route before reserving storage, so invalid configuration cannot leave a BUSY slot.
201
+ const url = mockTransport ? undefined : buildNativeUrl(request, options.nativePath);
202
+ if (!mockTransport) {
203
+ try {
204
+ browser.sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ request, deadline: request.expiresAt }));
205
+ }
206
+ catch {
207
+ throw new BridgeError('STORAGE_UNAVAILABLE', '无法保存调用信息,不能可靠恢复 URL 回传');
208
+ }
209
+ }
210
+ return new Promise((resolve, reject) => {
211
+ const timer = setTimeout(() => fail('TIMEOUT', mockTransport ? '等待模拟结果超时' : '等待原生页面回传超时'), duration);
212
+ active = { request, resolve, reject, timer };
213
+ if (mockTransport) {
214
+ active.cleanup = mockTransport(request, result => {
215
+ const waiting = active;
216
+ if (!waiting || waiting.request.id !== request.id || disposed)
217
+ return;
218
+ active = undefined;
219
+ clearTimeout(waiting.timer);
220
+ waiting.cleanup?.();
221
+ if (result.status === 'success')
222
+ waiting.resolve(result.data);
223
+ else
224
+ waiting.reject(new BridgeError(result.error.code, result.error.message));
225
+ publish(result, 'live', 'mock');
226
+ });
227
+ diagnostic(`模拟 ${method} · ${request.id}`);
228
+ return;
229
+ }
230
+ diagnostic(`发起 ${method} · ${request.mode} · ${request.id}`);
231
+ try {
232
+ browser.wx.miniProgram.navigateTo({ url: url, fail: error => fail('NAVIGATE_FAILED', error.errMsg || '原生页面跳转失败') });
233
+ }
234
+ catch (error) {
235
+ fail('NAVIGATE_FAILED', String(error));
236
+ }
237
+ });
238
+ }
239
+ function call(method, input) {
240
+ const { mode, timeoutMs, ...params } = input;
241
+ return invoke(method, params, { mode, timeoutMs });
242
+ }
243
+ return {
244
+ invoke, isInMiniProgram, ready,
245
+ get environment() { return environment; },
246
+ get isMock() { return environment === 'mock'; },
247
+ getLocation: (input = {}) => call('getLocation', input),
248
+ scanCode: (input = {}) => call('scanCode', input),
249
+ login: (input = {}) => call('login', input),
250
+ checkSession: (input = {}) => invoke('checkSession', {}, input),
251
+ chooseAddress: (input = {}) => invoke('chooseAddress', {}, input),
252
+ openLocation: (input) => call('openLocation', input),
253
+ makePhoneCall: (input) => call('makePhoneCall', input),
254
+ requestPayment: (input) => call('requestPayment', input),
255
+ dispose() {
256
+ disposed = true;
257
+ environmentController.abort();
258
+ if (listening) {
259
+ browser.removeEventListener('hashchange', hashListener);
260
+ browser.removeEventListener('pageshow', showListener);
261
+ document.removeEventListener('visibilitychange', visibleListener);
262
+ }
263
+ const waiting = active;
264
+ active = undefined;
265
+ if (waiting) {
266
+ clearTimeout(waiting.timer);
267
+ waiting.reject(new BridgeError('DISPOSED', 'SDK 已销毁'));
268
+ waiting.cleanup?.();
269
+ }
270
+ }
271
+ };
272
+ }
package/dist/mock.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { type BridgeMethod, type BridgeRequest, type BridgeResult, type ParamsOf, type DataOf } from './protocol.js';
2
+ export interface MockContext {
3
+ requestId: string;
4
+ /** Aborted on timeout, disposal, or completion. Check this before any delayed work. */
5
+ signal: AbortSignal;
6
+ }
7
+ export type MockHandler<M extends BridgeMethod> = (params: ParamsOf<M>, context: MockContext) => DataOf<M> | Promise<DataOf<M>>;
8
+ export type MockHandlers = {
9
+ [M in BridgeMethod]?: MockHandler<M>;
10
+ };
11
+ export interface MockOptions {
12
+ /** Delay before calling the handler, in milliseconds. Default: 300; range: 0–600000. */
13
+ delayMs?: number;
14
+ /** Override selected methods; the others use the built-in successful fixtures. */
15
+ handlers?: MockHandlers;
16
+ }
17
+ /** Fictional fixtures only. No WeChat APIs, backend requests, or real transactions. */
18
+ export declare const defaultMockHandlers: Readonly<{
19
+ getLocation: MockHandler<"getLocation">;
20
+ scanCode: MockHandler<"scanCode">;
21
+ login: MockHandler<"login">;
22
+ checkSession: MockHandler<"checkSession">;
23
+ openLocation: MockHandler<"openLocation">;
24
+ chooseAddress: MockHandler<"chooseAddress">;
25
+ makePhoneCall: MockHandler<"makePhoneCall">;
26
+ requestPayment: MockHandler<"requestPayment">;
27
+ }>;
28
+ export declare function createMockTransport(options: MockOptions): (request: BridgeRequest, complete: (result: BridgeResult) => void) => (() => void);
package/dist/mock.js ADDED
@@ -0,0 +1,62 @@
1
+ import { BridgeError } from './errors.js';
2
+ import { BRIDGE_METHODS, parseResult } from './protocol.js';
3
+ /** Fictional fixtures only. No WeChat APIs, backend requests, or real transactions. */
4
+ export const defaultMockHandlers = Object.freeze({
5
+ getLocation: params => ({ latitude: 31.2304, longitude: 121.4737, accuracy: 10, type: params.type ?? 'gcj02' }),
6
+ scanCode: params => ({
7
+ result: 'tea-store://mock?store=demo&table=08',
8
+ scanType: { qrCode: 'QR_CODE', barCode: 'CODE_128', datamatrix: 'DATA_MATRIX', pdf417: 'PDF_417' }[params.scanType?.[0] ?? 'qrCode'],
9
+ charSet: 'UTF-8'
10
+ }),
11
+ login: () => ({ code: 'MOCK_LOGIN_CODE_NOT_VALID_FOR_WECHAT' }),
12
+ checkSession: () => ({ valid: true }),
13
+ openLocation: () => ({ ok: true }),
14
+ chooseAddress: () => ({
15
+ userName: '模拟收货人', telNumber: '00000000000', provinceName: '测试省',
16
+ cityName: '测试市', countyName: '测试区', detailInfo: '模拟门店路 1 号(测试数据)'
17
+ }),
18
+ makePhoneCall: () => ({ ok: true }),
19
+ requestPayment: () => ({ ok: true })
20
+ });
21
+ export function createMockTransport(options) {
22
+ const delayMs = options.delayMs ?? 300;
23
+ if (!Number.isFinite(delayMs) || delayMs < 0 || delayMs > 600000)
24
+ throw new BridgeError('INVALID_PARAMS', 'mock.delayMs must be between 0 and 600000');
25
+ const handlers = { ...options.handlers };
26
+ for (const [method, handler] of Object.entries(handlers)) {
27
+ if (!BRIDGE_METHODS.includes(method) || typeof handler !== 'function')
28
+ throw new BridgeError('INVALID_PARAMS', `Invalid mock handler: ${method}`);
29
+ }
30
+ return (request, complete) => {
31
+ const controller = new AbortController();
32
+ const envelope = () => ({ v: 1, id: request.id, method: request.method, mode: request.mode, completedAt: Date.now() });
33
+ function failure(code, message) {
34
+ // Bound user-provided errors so all mock results obey the same URL payload limit.
35
+ return parseResult(JSON.stringify({ ...envelope(), status: code === 'CANCELLED' ? 'cancel' : 'error', error: { code: code.slice(0, 128), message: message.slice(0, 1024) } }));
36
+ }
37
+ const timer = setTimeout(async () => {
38
+ let result;
39
+ try {
40
+ // The method and its parameters have already been validated together by invoke().
41
+ const handler = (handlers[request.method] ?? defaultMockHandlers[request.method]);
42
+ const data = await handler(request.params, { requestId: request.id, signal: controller.signal });
43
+ if (controller.signal.aborted)
44
+ return;
45
+ try {
46
+ result = parseResult(JSON.stringify({ ...envelope(), status: 'success', data }));
47
+ }
48
+ catch {
49
+ result = failure('INVALID_MOCK_RESULT', `Invalid mock result for ${request.method}`);
50
+ }
51
+ }
52
+ catch (error) {
53
+ if (controller.signal.aborted)
54
+ return;
55
+ result = failure(error instanceof BridgeError ? error.code : 'MOCK_FAILED', error instanceof Error ? error.message : String(error));
56
+ }
57
+ if (!controller.signal.aborted)
58
+ complete(result);
59
+ }, delayMs);
60
+ return () => { clearTimeout(timer); controller.abort(); };
61
+ };
62
+ }
@@ -0,0 +1,145 @@
1
+ export declare const PROTOCOL_VERSION: 1;
2
+ export declare const RESULT_KEY = "__mp_bridge";
3
+ export declare const MAX_PAYLOAD_LENGTH = 16384;
4
+ export declare const DEFAULT_TIMEOUT_MS = 120000;
5
+ export type ReturnMode = 'hash' | 'query';
6
+ export interface LocationData {
7
+ latitude: number;
8
+ longitude: number;
9
+ accuracy: number;
10
+ type: 'wgs84' | 'gcj02';
11
+ }
12
+ export interface ScanCodeParams {
13
+ onlyFromCamera?: boolean;
14
+ scanType?: Array<'barCode' | 'qrCode' | 'datamatrix' | 'pdf417'>;
15
+ }
16
+ export interface ScanCodeData {
17
+ result: string;
18
+ scanType: string;
19
+ charSet?: string;
20
+ path?: string;
21
+ rawData?: string;
22
+ }
23
+ export interface OpenLocationParams {
24
+ latitude: number;
25
+ longitude: number;
26
+ name?: string;
27
+ address?: string;
28
+ scale?: number;
29
+ }
30
+ export interface PaymentParams {
31
+ timeStamp: string;
32
+ nonceStr: string;
33
+ package: string;
34
+ signType: 'RSA' | 'MD5' | 'HMAC-SHA256';
35
+ paySign: string;
36
+ }
37
+ export interface AddressData {
38
+ userName: string;
39
+ telNumber: string;
40
+ provinceName: string;
41
+ cityName: string;
42
+ countyName: string;
43
+ detailInfo: string;
44
+ postalCode?: string;
45
+ nationalCode?: string;
46
+ streetName?: string;
47
+ detailInfoNew?: string;
48
+ }
49
+ export interface BridgeMethods {
50
+ getLocation: {
51
+ params: {
52
+ type?: 'wgs84' | 'gcj02';
53
+ };
54
+ data: LocationData;
55
+ };
56
+ scanCode: {
57
+ params: ScanCodeParams;
58
+ data: ScanCodeData;
59
+ };
60
+ login: {
61
+ params: {
62
+ timeout?: number;
63
+ };
64
+ data: {
65
+ code: string;
66
+ };
67
+ };
68
+ checkSession: {
69
+ params: Record<string, never>;
70
+ data: {
71
+ valid: true;
72
+ };
73
+ };
74
+ openLocation: {
75
+ params: OpenLocationParams;
76
+ data: {
77
+ ok: true;
78
+ };
79
+ };
80
+ chooseAddress: {
81
+ params: Record<string, never>;
82
+ data: AddressData;
83
+ };
84
+ makePhoneCall: {
85
+ params: {
86
+ phoneNumber: string;
87
+ };
88
+ data: {
89
+ ok: true;
90
+ };
91
+ };
92
+ requestPayment: {
93
+ params: PaymentParams;
94
+ data: {
95
+ ok: true;
96
+ };
97
+ };
98
+ }
99
+ export type BridgeMethod = keyof BridgeMethods;
100
+ export type ParamsOf<M extends BridgeMethod> = BridgeMethods[M]['params'];
101
+ export type DataOf<M extends BridgeMethod> = BridgeMethods[M]['data'];
102
+ export declare const BRIDGE_METHODS: readonly ["getLocation", "scanCode", "login", "checkSession", "openLocation", "chooseAddress", "makePhoneCall", "requestPayment"];
103
+ type Envelope = {
104
+ v: 1;
105
+ id: string;
106
+ mode: ReturnMode;
107
+ };
108
+ export type BridgeRequest<M extends BridgeMethod = BridgeMethod> = {
109
+ [K in M]: Envelope & {
110
+ method: K;
111
+ params: ParamsOf<K>;
112
+ returnUrl: string;
113
+ createdAt: number;
114
+ expiresAt?: number;
115
+ };
116
+ }[M];
117
+ export type BridgeResult<M extends BridgeMethod = BridgeMethod> = {
118
+ [K in M]: Envelope & {
119
+ method: K;
120
+ completedAt: number;
121
+ } & ({
122
+ status: 'success';
123
+ data: DataOf<K>;
124
+ error?: never;
125
+ } | {
126
+ status: 'cancel' | 'error';
127
+ error: {
128
+ code: string;
129
+ message: string;
130
+ };
131
+ data?: never;
132
+ });
133
+ }[M];
134
+ /** Validate on both sides of the bridge; never forward arbitrary wx options or callbacks. */
135
+ export declare function normalizeParams<M extends BridgeMethod>(method: M, input: unknown): ParamsOf<M>;
136
+ export declare function parseRequest(raw: string): BridgeRequest;
137
+ export declare function parseResult(raw: string): BridgeResult;
138
+ export declare function buildNativeUrl(request: BridgeRequest, path?: string): string;
139
+ export declare function readReturnUrl(url: string): {
140
+ cleanUrl: string;
141
+ result?: BridgeResult;
142
+ };
143
+ export declare function buildReturnUrl(returnUrl: string, result: BridgeResult): string;
144
+ export declare function isAllowedWebUrl(url: string, origins: readonly string[]): boolean;
145
+ export {};
@@ -0,0 +1,173 @@
1
+ export const PROTOCOL_VERSION = 1;
2
+ export const RESULT_KEY = '__mp_bridge';
3
+ export const MAX_PAYLOAD_LENGTH = 16384;
4
+ export const DEFAULT_TIMEOUT_MS = 120000;
5
+ export const BRIDGE_METHODS = ['getLocation', 'scanCode', 'login', 'checkSession', 'openLocation', 'chooseAddress', 'makePhoneCall', 'requestPayment'];
6
+ function record(value) {
7
+ return !!value && typeof value === 'object' && !Array.isArray(value);
8
+ }
9
+ function validId(id) {
10
+ return typeof id === 'string' && /^[a-zA-Z0-9_-]{8,100}$/.test(id);
11
+ }
12
+ function common(value) {
13
+ return record(value) && value.v === 1 && validId(value.id) &&
14
+ typeof value.method === 'string' && BRIDGE_METHODS.includes(value.method) &&
15
+ (value.mode === 'hash' || value.mode === 'query');
16
+ }
17
+ const finite = (value) => typeof value === 'number' && Number.isFinite(value);
18
+ const text = (value, max = 2048) => typeof value === 'string' && value.length <= max;
19
+ const coordinate = (value, max) => finite(value) && Math.abs(value) <= max;
20
+ function requireValid(condition, message) {
21
+ if (!condition)
22
+ throw new Error(message);
23
+ }
24
+ function keys(value, allowed) {
25
+ requireValid(Object.keys(value).every(key => allowed.includes(key)), 'Unknown native parameter');
26
+ }
27
+ /** Validate on both sides of the bridge; never forward arbitrary wx options or callbacks. */
28
+ export function normalizeParams(method, input) {
29
+ requireValid(record(input), 'Invalid native parameters');
30
+ const value = { ...input };
31
+ switch (method) {
32
+ case 'getLocation':
33
+ keys(value, ['type']);
34
+ value.type ?? (value.type = 'gcj02');
35
+ requireValid(value.type === 'wgs84' || value.type === 'gcj02', 'Invalid coordinate type');
36
+ break;
37
+ case 'scanCode':
38
+ keys(value, ['onlyFromCamera', 'scanType']);
39
+ requireValid(value.onlyFromCamera === undefined || typeof value.onlyFromCamera === 'boolean', 'Invalid camera option');
40
+ requireValid(value.scanType === undefined || (Array.isArray(value.scanType) && value.scanType.length > 0 && value.scanType.length <= 4 && value.scanType.every(type => ['barCode', 'qrCode', 'datamatrix', 'pdf417'].includes(type))), 'Invalid scan types');
41
+ break;
42
+ case 'login':
43
+ keys(value, ['timeout']);
44
+ requireValid(value.timeout === undefined || (finite(value.timeout) && value.timeout >= 1000 && value.timeout <= 60000), 'Invalid login timeout');
45
+ break;
46
+ case 'checkSession':
47
+ case 'chooseAddress':
48
+ keys(value, []);
49
+ break;
50
+ case 'openLocation':
51
+ keys(value, ['latitude', 'longitude', 'name', 'address', 'scale']);
52
+ requireValid(coordinate(value.latitude, 90) && coordinate(value.longitude, 180), 'Invalid coordinates');
53
+ requireValid(['name', 'address'].every(key => value[key] === undefined || text(value[key], 512)), 'Invalid map label');
54
+ requireValid(value.scale === undefined || (finite(value.scale) && value.scale >= 5 && value.scale <= 18), 'Invalid map scale');
55
+ break;
56
+ case 'makePhoneCall':
57
+ keys(value, ['phoneNumber']);
58
+ requireValid(typeof value.phoneNumber === 'string' && /^\+?[0-9][0-9 -]{2,30}$/.test(value.phoneNumber), 'Invalid phone number');
59
+ break;
60
+ case 'requestPayment':
61
+ keys(value, ['timeStamp', 'nonceStr', 'package', 'signType', 'paySign']);
62
+ requireValid(typeof value.timeStamp === 'string' && /^\d{1,16}$/.test(value.timeStamp), 'Invalid payment timestamp');
63
+ requireValid(text(value.nonceStr, 32) && value.nonceStr.length > 0 && text(value.paySign, 2048) && value.paySign.length > 0, 'Invalid payment signature');
64
+ requireValid(text(value.package, 512) && /^prepay_id=\S+$/.test(value.package), 'Invalid prepay package');
65
+ requireValid(['RSA', 'MD5', 'HMAC-SHA256'].includes(String(value.signType)), 'Invalid payment sign type');
66
+ break;
67
+ default: throw new Error('Unsupported native method');
68
+ }
69
+ return value;
70
+ }
71
+ export function parseRequest(raw) {
72
+ if (raw.length > MAX_PAYLOAD_LENGTH * 6)
73
+ throw new Error('Bridge request too large');
74
+ // WeChat versions may provide an already-decoded query. Never decode valid JSON twice.
75
+ let value;
76
+ try {
77
+ value = JSON.parse(raw);
78
+ }
79
+ catch {
80
+ value = JSON.parse(decodeURIComponent(raw));
81
+ }
82
+ if (!common(value) || typeof value.returnUrl !== 'string' || value.returnUrl.length > 4096 ||
83
+ !finite(value.createdAt) || (value.expiresAt !== undefined &&
84
+ (!finite(value.expiresAt) || value.expiresAt <= value.createdAt || value.expiresAt - value.createdAt > 600000))) {
85
+ throw new Error('Invalid bridge request');
86
+ }
87
+ value.params = normalizeParams(value.method, value.params);
88
+ requireValid(JSON.stringify(value).length <= MAX_PAYLOAD_LENGTH, 'Bridge request too large');
89
+ return value;
90
+ }
91
+ export function parseResult(raw) {
92
+ if (raw.length > MAX_PAYLOAD_LENGTH)
93
+ throw new Error('Bridge result too large');
94
+ const value = JSON.parse(raw);
95
+ if (!common(value) || !['success', 'cancel', 'error'].includes(String(value.status)) ||
96
+ typeof value.completedAt !== 'number' || !Number.isFinite(value.completedAt)) {
97
+ throw new Error('Invalid bridge result');
98
+ }
99
+ if (value.status === 'success') {
100
+ const data = value.data;
101
+ requireValid(record(data), 'Missing native result');
102
+ switch (value.method) {
103
+ case 'getLocation':
104
+ requireValid(coordinate(data.latitude, 90) && coordinate(data.longitude, 180) && finite(data.accuracy) && data.accuracy >= 0 && ['wgs84', 'gcj02'].includes(String(data.type)), 'Invalid location result');
105
+ break;
106
+ case 'scanCode':
107
+ requireValid(text(data.result, MAX_PAYLOAD_LENGTH) && text(data.scanType, 100) && ['charSet', 'path', 'rawData'].every(key => data[key] === undefined || text(data[key], MAX_PAYLOAD_LENGTH)), 'Invalid scan result');
108
+ break;
109
+ case 'login':
110
+ requireValid(text(data.code, 1024) && data.code.length > 0, 'Invalid login code');
111
+ break;
112
+ case 'checkSession':
113
+ requireValid(data.valid === true, 'Invalid session result');
114
+ break;
115
+ case 'chooseAddress':
116
+ requireValid(['userName', 'telNumber', 'provinceName', 'cityName', 'countyName', 'detailInfo'].every(key => text(data[key])) && ['postalCode', 'nationalCode', 'streetName', 'detailInfoNew'].every(key => data[key] === undefined || text(data[key])), 'Invalid address result');
117
+ break;
118
+ default: requireValid(data.ok === true, 'Invalid native acknowledgment');
119
+ }
120
+ }
121
+ else if (!record(value.error) || typeof value.error.code !== 'string' ||
122
+ typeof value.error.message !== 'string')
123
+ throw new Error('Missing bridge error');
124
+ return value;
125
+ }
126
+ export function buildNativeUrl(request, path = '/native/invoke') {
127
+ if (!/^\/[a-zA-Z0-9/_-]+$/.test(path))
128
+ throw new Error('Invalid native page path');
129
+ return `${path}?request=${encodeURIComponent(JSON.stringify(request))}`;
130
+ }
131
+ // Keep this module independent of DOM, URL, Buffer and Node APIs: it also runs in WeChat.
132
+ function splitUrl(url) {
133
+ const hashAt = url.indexOf('#');
134
+ const base = hashAt < 0 ? url : url.slice(0, hashAt);
135
+ const hash = hashAt < 0 ? '' : url.slice(hashAt + 1);
136
+ const queryAt = base.indexOf('?');
137
+ return { path: queryAt < 0 ? base : base.slice(0, queryAt),
138
+ query: queryAt < 0 ? '' : base.slice(queryAt + 1), hash };
139
+ }
140
+ function extract(part) {
141
+ const values = [];
142
+ const clean = part.split('&').filter(piece => {
143
+ if (!piece.startsWith(`${RESULT_KEY}=`))
144
+ return true;
145
+ values.push(piece.slice(RESULT_KEY.length + 1));
146
+ return false;
147
+ }).join('&');
148
+ return { clean, values };
149
+ }
150
+ export function readReturnUrl(url) {
151
+ const { path, query, hash } = splitUrl(url);
152
+ const q = extract(query), h = extract(hash);
153
+ const values = [...q.values, ...h.values];
154
+ if (values.length > 1)
155
+ throw new Error('Ambiguous bridge result');
156
+ return {
157
+ cleanUrl: path + (q.clean ? `?${q.clean}` : '') + (h.clean ? `#${h.clean}` : ''),
158
+ result: values.length ? parseResult(decodeURIComponent(values[0])) : undefined
159
+ };
160
+ }
161
+ export function buildReturnUrl(returnUrl, result) {
162
+ const { path, query, hash } = splitUrl(readReturnUrl(returnUrl).cleanUrl);
163
+ const payload = `${RESULT_KEY}=${encodeURIComponent(JSON.stringify(result))}`;
164
+ if (result.mode === 'hash')
165
+ return path + (query ? `?${query}` : '') + '#' + (hash ? `${hash}&` : '') + payload;
166
+ return path + '?' + (query ? `${query}&` : '') + payload + (hash ? `#${hash}` : '');
167
+ }
168
+ export function isAllowedWebUrl(url, origins) {
169
+ if (/[\s\\\u0000-\u001f]/.test(url))
170
+ return false;
171
+ const match = /^(https:\/\/[^/?#]+)(?:[/?#]|$)/.exec(url);
172
+ return !!match && origins.includes(match[1]);
173
+ }
package/package.json CHANGED
@@ -1,6 +1,23 @@
1
1
  {
2
2
  "name": "@spopc/wechat-miniprogram",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Web SDK for the WeChat Mini Program URL bridge",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/cloudcarver/miniprogram-shell.git",
8
+ "directory": "sdks/wechat-miniprogram"
9
+ },
10
+ "homepage": "https://www.npmjs.com/package/@spopc/wechat-miniprogram",
11
+ "keywords": ["wechat", "miniprogram", "webview", "bridge", "mock", "typescript"],
12
+ "type": "module",
13
+ "sideEffects": false,
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "default": "./dist/index.js" },
18
+ "./protocol": { "types": "./dist/protocol.d.ts", "import": "./dist/protocol.js", "default": "./dist/protocol.js" }
19
+ },
20
+ "files": ["dist", "README.md"],
21
+ "scripts": { "build": "tsc -p tsconfig.json", "prepack": "npm run build" },
22
+ "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" }
23
+ }