@mearl/client 2.20.4 → 2.20.5

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/dist/check.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { CloudConnectorInfo } from '@mearl/cloud-types';
2
+ import { type NativeHostDiagnostic } from './nativeHost.js';
2
3
  import type { BrowserCapabilities, BrowserProviderIdentity, BrowserProviderOperation } from './generated-browser-action-protocol.js';
3
4
  import { type UnifiedClientOptions, type UnifiedInvokeOptions } from './unified.js';
4
5
  export interface UnifiedCheckVersions {
@@ -96,6 +97,7 @@ export interface UnifiedCheckDependencies {
96
97
  writeJson?: (json: string) => void;
97
98
  writeLine?: (line: string) => void;
98
99
  fetchLatestVersion?: (component: string) => Promise<string>;
100
+ inspectNativeHostInstallation?: () => NativeHostDiagnostic[];
99
101
  }
100
102
  export declare function parseReleaseVersion(value: unknown): number[] | null;
101
103
  export declare function isNewerRelease(latest: string, installed: string): boolean;
package/dist/check.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { inspectNativeHostInstallation } from './nativeHost.js';
1
2
  import { UnifiedClient, normalizeBrowserSelector, } from './unified.js';
2
3
  const UPDATE_SOURCES = {
3
4
  client: 'https://registry.npmjs.org/@mearl%2Fclient/latest',
@@ -221,6 +222,12 @@ export async function runUnifiedCheck(options, dependencies = {}) {
221
222
  }
222
223
  const connected = (browserList?.browsers ?? []).filter(browser => browser.status === 'connected' &&
223
224
  (options.transport !== 'cdp' || browser.transport === 'cdp'));
225
+ if ((options.local || !options.serverUrl) &&
226
+ options.transport !== 'cdp' &&
227
+ !connected.some(browser => browser.transport === 'extension')) {
228
+ const diagnostics = (dependencies.inspectNativeHostInstallation ?? inspectNativeHostInstallation)();
229
+ environment.push(...diagnostics.map(item => ({ status: 'warning', ...item })));
230
+ }
224
231
  let targets = connected;
225
232
  if (options.browser) {
226
233
  const matches = findBrowsers(connected, options.browser);
@@ -1056,11 +1056,19 @@ export declare const COMMANDS: readonly [{
1056
1056
  }, {
1057
1057
  readonly name: "profile";
1058
1058
  readonly type: "string";
1059
- readonly description: `\u5BB9\u5668\u6863\u6848\u540D\uFF08enabled=true \u65F6\u5FC5\u586B\uFF09\uFF1A${string}`;
1059
+ readonly description: `\u5BB9\u5668\u6863\u6848\u540D\uFF08enabled=true \u65F6\u5FC5\u586B\uFF09\uFF1B\u5185\u7F6E ${string}`;
1060
1060
  }, {
1061
1061
  readonly name: "os";
1062
1062
  readonly type: "\"ios\" | \"android\"";
1063
1063
  readonly description: "容器系统,默认 ios";
1064
+ }, {
1065
+ readonly name: "refreshDefinition";
1066
+ readonly type: "boolean";
1067
+ readonly description: "立即采用最新档案定义,保留用户显式设置";
1068
+ }, {
1069
+ readonly name: "lockDefinition";
1070
+ readonly type: "boolean";
1071
+ readonly description: "固定当前定义;默认在下次刷新或文档导航时自动更新";
1064
1072
  }, {
1065
1073
  readonly name: "appVersion";
1066
1074
  readonly type: "string";
package/dist/commands.js CHANGED
@@ -1153,9 +1153,19 @@ export const COMMANDS = [
1153
1153
  {
1154
1154
  name: 'profile',
1155
1155
  type: 'string',
1156
- description: `容器档案名(enabled=true 时必填):${APP_PROFILE_NAME_LIST}`,
1156
+ description: `容器档案名(enabled=true 时必填);内置 ${APP_PROFILE_NAME_LIST}`,
1157
1157
  },
1158
1158
  { name: 'os', type: '"ios" | "android"', description: '容器系统,默认 ios' },
1159
+ {
1160
+ name: 'refreshDefinition',
1161
+ type: 'boolean',
1162
+ description: '立即采用最新档案定义,保留用户显式设置',
1163
+ },
1164
+ {
1165
+ name: 'lockDefinition',
1166
+ type: 'boolean',
1167
+ description: '固定当前定义;默认在下次刷新或文档导航时自动更新',
1168
+ },
1159
1169
  { name: 'appVersion', type: 'string', description: '覆盖档案默认 App 版本号' },
1160
1170
  {
1161
1171
  name: 'deviceModel',
@@ -0,0 +1,130 @@
1
+ import type { BrowserDevicePresetName, AppBridgeConfig } from './generated-browser-action-protocol.js';
2
+ import type { AppBridgeSpec, AppImagePickerConfig, AppLayerUrlProxyConfig, AppMiniProgramConfig, AppNativeBackConfig, AppOverlayConfig, AppSchemeRule, AppTitleBarHostStyle, BridgeActionMap } from './generated-app-profile-runtime-types.js';
3
+ export interface AppProfilePolicies {
4
+ titleBar?: 'default' | 'taobao' | 'fliggy' | 'alipay' | 'wechat';
5
+ navigatorLanguage?: boolean;
6
+ networkResult?: 'default' | 'alipay';
7
+ languages?: string[];
8
+ defaultLanguage?: string;
9
+ }
10
+ export interface AppProfileSetting {
11
+ id: string;
12
+ label: string;
13
+ type: 'select' | 'boolean' | 'number' | 'text' | 'json';
14
+ target: {
15
+ option: 'language';
16
+ } | {
17
+ method: string;
18
+ field?: string;
19
+ };
20
+ choices?: Array<{
21
+ label: string;
22
+ value: string;
23
+ }>;
24
+ minimum?: number;
25
+ maximum?: number;
26
+ }
27
+ export interface AppProfileOptions {
28
+ /** Keep this definition across document navigations until explicitly refreshed. */
29
+ lockDefinition?: boolean;
30
+ os?: 'ios' | 'android';
31
+ appVersion?: string;
32
+ language?: string;
33
+ deviceModel?: string;
34
+ devicePreset?: BrowserDevicePresetName;
35
+ orientation?: 'portrait' | 'landscape';
36
+ bridgeConfig?: AppBridgeConfig;
37
+ }
38
+ export interface AppProfileBinding {
39
+ version: 1;
40
+ revision: string;
41
+ definition: AppProfileDefinition;
42
+ options: AppProfileOptions;
43
+ /** Explicit scalar choices; method overrides are always explicit and live in options.bridgeConfig. */
44
+ explicitOptions?: Omit<AppProfileOptions, 'bridgeConfig'>;
45
+ }
46
+ export interface AppProfileContribution {
47
+ version: 1;
48
+ profiles: AppProfileDefinition[];
49
+ }
50
+ export interface AppProfileCatalogEntry {
51
+ id: string;
52
+ label: string;
53
+ source: 'builtin' | 'plugin';
54
+ available?: boolean;
55
+ plugin?: string;
56
+ revision: string;
57
+ definition: AppProfileDefinition;
58
+ }
59
+ /** 单容器档案(纯数据) */
60
+ export interface AppProfileDefinition {
61
+ name: string;
62
+ label: string;
63
+ /** 容器的应用标识,如 LX / TB / AP / WeChat */
64
+ appName: string;
65
+ defaultAppVersion: string;
66
+ /** UA 主体格式;默认使用 AliApp(appName/version) */
67
+ uaFamily?: 'aliapp' | 'wechatMiniProgram';
68
+ uaTemplate?: {
69
+ ios: string;
70
+ android: string;
71
+ };
72
+ policies?: AppProfilePolicies;
73
+ settings?: AppProfileSetting[];
74
+ settingsGroup?: 'fliggy' | 'alipay';
75
+ /** AliApp UA 主体之后追加的 token,{v} 占位会被替换为 appVersion */
76
+ uaTokens: string[];
77
+ /** 额外注入到 window 的变量(公共变量由 resolve 统一生成) */
78
+ globals: Record<string, string>;
79
+ bridges: AppBridgeSpec[];
80
+ /** 页面回到前台时分发到 document 的事件名(如 WV.Event.APP.Active) */
81
+ activeEvents: string[];
82
+ actions: BridgeActionMap;
83
+ schemes: AppSchemeRule[];
84
+ /** 小程序原生页面与 WebView 通信规则。 */
85
+ miniProgram?: AppMiniProgramConfig;
86
+ /** 原生图片页 scheme 的浏览器选图与上传配置 */
87
+ imagePicker?: AppImagePickerConfig;
88
+ geolocation?: {
89
+ reverseGeocodeApi: string;
90
+ version: string;
91
+ };
92
+ /**
93
+ * 地址栏借位同步的可选 host 白名单:容器栈顶 URL 的 host 命中时,地址栏借 top
94
+ * 域名同步其 path+query+hash(pushState 同源约束下保持域名不变只改 path);
95
+ * 不配置则同步所有 HTTP(S) 业务层 URL。
96
+ */
97
+ urlSyncHostSuffixes?: string[];
98
+ /** URL / pageParams 浮层协议 */
99
+ overlay?: AppOverlayConfig;
100
+ /** URL 参数为真时以新 WebView 替换当前业务层。 */
101
+ popBeforeOpenParam?: string;
102
+ /** 跨 WebView 消息抵达 opener 时分发的 document 事件。 */
103
+ messageEvent?: string;
104
+ /** 原生返回键的页面接管协议。 */
105
+ nativeBack?: AppNativeBackConfig;
106
+ /** 开发代理 origin 下的业务层 URL 映射 */
107
+ layerUrlProxy?: AppLayerUrlProxyConfig;
108
+ /** App 框架的根 WebView 与自绘标题栏协议。 */
109
+ rootContainer?: {
110
+ statusBarHeightCookie?: {
111
+ name: string;
112
+ domain?: string;
113
+ path?: string;
114
+ };
115
+ };
116
+ /** 页面自行绘制导航区域时,不渲染模拟标题栏。 */
117
+ titleBarHiddenPathnames?: string[];
118
+ /** 页面级状态栏背景色,key 为业务 pathname。 */
119
+ statusBarBackgroundColors?: Record<string, string>;
120
+ /** 按业务根域配置标题栏与状态栏样式。 */
121
+ titleBarHostStyles?: AppTitleBarHostStyle[];
122
+ /** 桥存在时仍将 MTOP 传输固定在浏览器 H5 通道 */
123
+ forceH5Mtop?: boolean;
124
+ }
125
+ export declare const APP_BRIDGE_BEHAVIORS: readonly ["navigate", "closeAll", "closeBackData", "postMessage", "notifySubscribe", "notifyUnsubscribe", "notifyPost", "sharedFileRead", "sharedFileWrite", "sharedDataGet", "sharedDataSet", "legacyBroadcastCreate", "legacyBroadcastPost", "legacyBroadcastClose", "memoryGet", "memorySet", "memoryDelete", "availabilityMap", "userInfo", "authCode", "networkInfo", "geolocation", "permission", "imagePicker", "prefetch", "fptTrack", "mtopStream", "previewFile", "toast", "unsupportedNativeInput", "payment", "userTrack", "calendar", "networkQualityMonitor", "beaconRequest", "voiceRecognition"];
126
+ /** Validate plain data before registration or compiling a document-start configuration. */
127
+ export declare function validateAppProfileDefinition(value: unknown): AppProfileDefinition;
128
+ export declare function validateAppProfileContribution(value: unknown): AppProfileContribution | undefined;
129
+ /** Deterministic identity for immutable definitions; this identifier grants no host permissions. */
130
+ export declare function appProfileRevision(definition: AppProfileDefinition): string;
@@ -0,0 +1,448 @@
1
+ const BRIDGE_STYLES = new Set([
2
+ 'windvane',
3
+ 'fliggy',
4
+ 'alipay',
5
+ 'callback',
6
+ 'btrip',
7
+ 'invoke',
8
+ 'callHandler',
9
+ 'amap',
10
+ 'didi',
11
+ 'easybikeNative',
12
+ 'mega',
13
+ 'wechat',
14
+ 'wechatMiniProgram',
15
+ ]);
16
+ export const APP_BRIDGE_BEHAVIORS = [
17
+ 'navigate',
18
+ 'closeAll',
19
+ 'closeBackData',
20
+ 'postMessage',
21
+ 'notifySubscribe',
22
+ 'notifyUnsubscribe',
23
+ 'notifyPost',
24
+ 'sharedFileRead',
25
+ 'sharedFileWrite',
26
+ 'sharedDataGet',
27
+ 'sharedDataSet',
28
+ 'legacyBroadcastCreate',
29
+ 'legacyBroadcastPost',
30
+ 'legacyBroadcastClose',
31
+ 'memoryGet',
32
+ 'memorySet',
33
+ 'memoryDelete',
34
+ 'availabilityMap',
35
+ 'userInfo',
36
+ 'authCode',
37
+ 'networkInfo',
38
+ 'geolocation',
39
+ 'permission',
40
+ 'imagePicker',
41
+ 'prefetch',
42
+ 'fptTrack',
43
+ 'mtopStream',
44
+ 'previewFile',
45
+ 'toast',
46
+ 'unsupportedNativeInput',
47
+ 'payment',
48
+ 'userTrack',
49
+ 'calendar',
50
+ 'networkQualityMonitor',
51
+ 'beaconRequest',
52
+ 'voiceRecognition',
53
+ ];
54
+ const BEHAVIORS = new Set(APP_BRIDGE_BEHAVIORS);
55
+ function record(value) {
56
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
57
+ }
58
+ function invalid(path) {
59
+ throw new Error(`Invalid app profile: ${path}`);
60
+ }
61
+ function text(value) {
62
+ return (typeof value === 'string' &&
63
+ value.length <= 4096 &&
64
+ Array.from(value).every(character => character.charCodeAt(0) > 8));
65
+ }
66
+ function strings(value) {
67
+ return Array.isArray(value) && value.length <= 200 && value.every(text);
68
+ }
69
+ function fields(value, required, optional, path) {
70
+ if (!record(value) ||
71
+ required.some(key => !Object.hasOwn(value, key)) ||
72
+ Object.keys(value).some(key => !required.includes(key) && !optional.includes(key)))
73
+ invalid(path);
74
+ }
75
+ function stringMap(value) {
76
+ return record(value) && Object.keys(value).length <= 200 && Object.values(value).every(text);
77
+ }
78
+ function json(value, depth = 0) {
79
+ if (depth > 25)
80
+ return false;
81
+ if (value === null || typeof value === 'boolean' || text(value))
82
+ return true;
83
+ if (typeof value === 'number')
84
+ return Number.isFinite(value);
85
+ if (Array.isArray(value))
86
+ return value.length <= 1000 && value.every(item => json(item, depth + 1));
87
+ return (record(value) &&
88
+ Object.entries(value).every(([key, item]) => !['__proto__', 'prototype', 'constructor'].includes(key) && json(item, depth + 1)));
89
+ }
90
+ function validateActions(value) {
91
+ const arrays = [
92
+ 'open',
93
+ 'close',
94
+ 'title',
95
+ 'navigationBar',
96
+ 'statusBar',
97
+ 'systemInfo',
98
+ 'mtop',
99
+ 'kvGet',
100
+ 'kvSet',
101
+ 'kvDelete',
102
+ 'pageContext',
103
+ 'noop',
104
+ 'silent',
105
+ ];
106
+ fields(value, ['open', 'close', 'title', 'systemInfo', 'mtop', 'noop'], [...arrays, 'responses', 'behaviors'], 'actions');
107
+ for (const key of arrays)
108
+ if (value[key] !== undefined && !strings(value[key]))
109
+ invalid(`actions.${key}`);
110
+ if (value.responses !== undefined && !record(value.responses))
111
+ invalid('actions.responses');
112
+ if (value.behaviors !== undefined &&
113
+ (!stringMap(value.behaviors) ||
114
+ !Object.values(value.behaviors).every(item => BEHAVIORS.has(item))))
115
+ invalid('actions.behaviors');
116
+ }
117
+ /** Validate plain data before registration or compiling a document-start configuration. */
118
+ export function validateAppProfileDefinition(value) {
119
+ if (!json(value))
120
+ invalid('JSON');
121
+ if (JSON.stringify(value).length > 65_536)
122
+ invalid('definition exceeds 64 KiB');
123
+ const required = [
124
+ 'name',
125
+ 'label',
126
+ 'appName',
127
+ 'defaultAppVersion',
128
+ 'uaTokens',
129
+ 'globals',
130
+ 'bridges',
131
+ 'activeEvents',
132
+ 'actions',
133
+ 'schemes',
134
+ ];
135
+ const optional = [
136
+ 'uaFamily',
137
+ 'uaTemplate',
138
+ 'policies',
139
+ 'settings',
140
+ 'settingsGroup',
141
+ 'miniProgram',
142
+ 'imagePicker',
143
+ 'geolocation',
144
+ 'urlSyncHostSuffixes',
145
+ 'overlay',
146
+ 'popBeforeOpenParam',
147
+ 'messageEvent',
148
+ 'nativeBack',
149
+ 'layerUrlProxy',
150
+ 'rootContainer',
151
+ 'titleBarHiddenPathnames',
152
+ 'statusBarBackgroundColors',
153
+ 'titleBarHostStyles',
154
+ 'forceH5Mtop',
155
+ ];
156
+ fields(value, required, optional, 'definition');
157
+ for (const key of ['name', 'label', 'appName', 'defaultAppVersion'])
158
+ if (!text(value[key]) || !value[key])
159
+ invalid(key);
160
+ if (!/^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)?$/.test(value.name))
161
+ invalid('name');
162
+ for (const key of [
163
+ 'uaTokens',
164
+ 'activeEvents',
165
+ 'urlSyncHostSuffixes',
166
+ 'titleBarHiddenPathnames',
167
+ ]) {
168
+ if (value[key] !== undefined && !strings(value[key]))
169
+ invalid(key);
170
+ }
171
+ if (!stringMap(value.globals))
172
+ invalid('globals');
173
+ if (!Array.isArray(value.bridges) || value.bridges.length > 20)
174
+ invalid('bridges');
175
+ const globals = new Set();
176
+ for (const bridge of value.bridges) {
177
+ fields(bridge, ['global', 'style'], ['persistent'], 'bridges');
178
+ if (!text(bridge.global) ||
179
+ !/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(bridge.global) ||
180
+ bridge.global
181
+ .split('.')
182
+ .some(part => ['__proto__', 'prototype', 'constructor'].includes(part)) ||
183
+ globals.has(bridge.global) ||
184
+ !BRIDGE_STYLES.has(bridge.style) ||
185
+ (bridge.persistent !== undefined && typeof bridge.persistent !== 'boolean'))
186
+ invalid('bridge');
187
+ globals.add(bridge.global);
188
+ }
189
+ validateActions(value.actions);
190
+ if (value.settingsGroup !== undefined &&
191
+ !['fliggy', 'alipay'].includes(value.settingsGroup))
192
+ invalid('settingsGroup');
193
+ if (value.uaFamily !== undefined &&
194
+ !['aliapp', 'wechatMiniProgram'].includes(value.uaFamily))
195
+ invalid('uaFamily');
196
+ if (value.uaTemplate !== undefined) {
197
+ fields(value.uaTemplate, ['ios', 'android'], [], 'uaTemplate');
198
+ if (!text(value.uaTemplate.ios) || !text(value.uaTemplate.android))
199
+ invalid('uaTemplate');
200
+ }
201
+ if (value.policies !== undefined) {
202
+ fields(value.policies, [], ['titleBar', 'navigatorLanguage', 'networkResult', 'languages', 'defaultLanguage'], 'policies');
203
+ const p = value.policies;
204
+ if (p.titleBar !== undefined &&
205
+ !['default', 'taobao', 'fliggy', 'alipay', 'wechat'].includes(p.titleBar))
206
+ invalid('policies.titleBar');
207
+ if (p.networkResult !== undefined && !['default', 'alipay'].includes(p.networkResult))
208
+ invalid('policies.networkResult');
209
+ if (p.navigatorLanguage !== undefined && typeof p.navigatorLanguage !== 'boolean')
210
+ invalid('policies.navigatorLanguage');
211
+ if (p.languages !== undefined && (!strings(p.languages) || !p.languages.length))
212
+ invalid('policies.languages');
213
+ if (p.defaultLanguage !== undefined &&
214
+ (!text(p.defaultLanguage) ||
215
+ (p.languages && !p.languages.includes(p.defaultLanguage))))
216
+ invalid('policies.defaultLanguage');
217
+ }
218
+ if (!Array.isArray(value.schemes) || value.schemes.length > 100)
219
+ invalid('schemes');
220
+ for (const rule of value.schemes) {
221
+ fields(rule, ['match'], [
222
+ 'hostSuffixes',
223
+ 'param',
224
+ 'useCurrentOrigin',
225
+ 'targetPath',
226
+ 'targetHostPrefix',
227
+ 'targetParams',
228
+ ], 'scheme');
229
+ for (const key of ['match', 'param', 'targetPath', 'targetHostPrefix'])
230
+ if (rule[key] !== undefined && !text(rule[key]))
231
+ invalid(`scheme.${key}`);
232
+ if (rule.hostSuffixes !== undefined && !strings(rule.hostSuffixes))
233
+ invalid('scheme.hostSuffixes');
234
+ if (rule.useCurrentOrigin !== undefined && typeof rule.useCurrentOrigin !== 'boolean')
235
+ invalid('scheme.useCurrentOrigin');
236
+ if (rule.targetParams !== undefined && !stringMap(rule.targetParams))
237
+ invalid('scheme.targetParams');
238
+ }
239
+ if (value.settings !== undefined) {
240
+ if (!Array.isArray(value.settings) || value.settings.length > 30)
241
+ invalid('settings');
242
+ const ids = new Set();
243
+ for (const setting of value.settings) {
244
+ fields(setting, ['id', 'label', 'type', 'target'], ['choices', 'minimum', 'maximum'], 'setting');
245
+ if (!text(setting.id) ||
246
+ !/^[a-z][a-zA-Z0-9-]*$/.test(setting.id) ||
247
+ ids.has(setting.id) ||
248
+ !text(setting.label) ||
249
+ !['select', 'boolean', 'number', 'text', 'json'].includes(setting.type))
250
+ invalid('setting');
251
+ ids.add(setting.id);
252
+ if (record(setting.target) && setting.target.option !== undefined) {
253
+ fields(setting.target, ['option'], [], 'setting.target');
254
+ if (setting.target.option !== 'language')
255
+ invalid('setting.target.option');
256
+ }
257
+ else {
258
+ fields(setting.target, ['method'], ['field'], 'setting.target');
259
+ if (!text(setting.target.method) ||
260
+ !setting.target.method ||
261
+ ['__proto__', 'prototype', 'constructor'].includes(setting.target.method) ||
262
+ (setting.target.field !== undefined &&
263
+ (!text(setting.target.field) ||
264
+ ['__proto__', 'prototype', 'constructor'].includes(setting.target.field))))
265
+ invalid('setting.target.method');
266
+ const method = setting.target.method;
267
+ if ([
268
+ 'open',
269
+ 'close',
270
+ 'title',
271
+ 'navigationBar',
272
+ 'statusBar',
273
+ 'systemInfo',
274
+ 'mtop',
275
+ 'kvGet',
276
+ 'kvSet',
277
+ 'kvDelete',
278
+ 'pageContext',
279
+ ].some(key => value.actions[key]?.includes(method)))
280
+ invalid('setting targets a structural method');
281
+ }
282
+ if (setting.choices !== undefined &&
283
+ (!Array.isArray(setting.choices) ||
284
+ !setting.choices.length ||
285
+ setting.choices.some(choice => !record(choice) || !text(choice.label) || !text(choice.value))))
286
+ invalid('setting.choices');
287
+ for (const key of ['minimum', 'maximum'])
288
+ if (setting[key] !== undefined && typeof setting[key] !== 'number')
289
+ invalid(`setting.${key}`);
290
+ if (setting.type === 'select' && !Array.isArray(setting.choices))
291
+ invalid('setting.choices required');
292
+ if (setting.minimum !== undefined &&
293
+ setting.maximum !== undefined &&
294
+ setting.minimum > setting.maximum)
295
+ invalid('setting range');
296
+ if ('option' in setting.target && !['select', 'text'].includes(setting.type))
297
+ invalid('setting.option type');
298
+ }
299
+ }
300
+ for (const key of ['forceH5Mtop'])
301
+ if (value[key] !== undefined && typeof value[key] !== 'boolean')
302
+ invalid(key);
303
+ for (const key of ['popBeforeOpenParam', 'messageEvent'])
304
+ if (value[key] !== undefined && !text(value[key]))
305
+ invalid(key);
306
+ if (value.statusBarBackgroundColors !== undefined && !stringMap(value.statusBarBackgroundColors))
307
+ invalid('statusBarBackgroundColors');
308
+ validateStructuredOptions(value);
309
+ return JSON.parse(JSON.stringify(value));
310
+ }
311
+ function validateStructuredOptions(value) {
312
+ const shapes = {
313
+ nativeBack: [[], ['event', 'controlFunction', 'handler']],
314
+ geolocation: [['reverseGeocodeApi', 'version'], []],
315
+ layerUrlProxy: [
316
+ ['sourceHostPrefix', 'pathPrefix', 'targetHostSuffixes'],
317
+ ['defaultTargetHost'],
318
+ ],
319
+ imagePicker: [
320
+ ['schemes', 'api', 'version', 'fcId', 'fcGroup', 'uploadUrlFunction', 'prefix', 'region'],
321
+ [],
322
+ ],
323
+ overlay: [
324
+ [
325
+ 'enabledParam',
326
+ 'topRatioParam',
327
+ 'radiusParam',
328
+ 'maskAlphaParam',
329
+ 'tapCloseParam',
330
+ 'defaultTopRatio',
331
+ 'defaultRadius',
332
+ 'defaultMaskAlpha',
333
+ 'defaultTapClose',
334
+ ],
335
+ ['heightRatioParam', 'tapCloseSelectors', 'silentTapClose'],
336
+ ],
337
+ };
338
+ for (const [key, [required, optional]] of Object.entries(shapes)) {
339
+ if (value[key] === undefined)
340
+ continue;
341
+ fields(value[key], required, optional, key);
342
+ for (const [field, item] of Object.entries(value[key])) {
343
+ const valid = field.startsWith('default') && field !== 'defaultTargetHost'
344
+ ? field === 'defaultTapClose'
345
+ ? typeof item === 'boolean'
346
+ : typeof item === 'number'
347
+ : field === 'silentTapClose'
348
+ ? typeof item === 'boolean'
349
+ : ['schemes', 'targetHostSuffixes', 'tapCloseSelectors'].includes(field)
350
+ ? strings(item)
351
+ : text(item);
352
+ if (!valid)
353
+ invalid(`${key}.${field}`);
354
+ }
355
+ }
356
+ if (value.rootContainer !== undefined) {
357
+ fields(value.rootContainer, [], ['statusBarHeightCookie'], 'rootContainer');
358
+ if (value.rootContainer.statusBarHeightCookie !== undefined) {
359
+ fields(value.rootContainer.statusBarHeightCookie, ['name'], ['domain', 'path'], 'rootContainer.statusBarHeightCookie');
360
+ if (!Object.values(value.rootContainer.statusBarHeightCookie).every(text))
361
+ invalid('rootContainer.statusBarHeightCookie');
362
+ }
363
+ }
364
+ if (value.titleBarHostStyles !== undefined &&
365
+ (!Array.isArray(value.titleBarHostStyles) ||
366
+ value.titleBarHostStyles.some(rule => {
367
+ fields(rule, ['hostSuffix'], ['titleBarBackgroundColor', 'statusBarBackgroundColor'], 'titleBarHostStyle');
368
+ return !Object.values(rule).every(text);
369
+ })))
370
+ invalid('titleBarHostStyles');
371
+ if (value.miniProgram !== undefined) {
372
+ fields(value.miniProgram, [], ['paymentRoutes', 'nativeRoutes', 'messageRules'], 'miniProgram');
373
+ const shapes = {
374
+ paymentRoutes: [
375
+ ['pathname', 'paymentParamsParam', 'successUrlParams', 'failureUrlParams'],
376
+ ['hostSuffixes', 'successResultParam', 'forwardParams', 'useCurrentOrigin'],
377
+ ],
378
+ nativeRoutes: [['pathname', 'actions'], ['hostSuffixes']],
379
+ messageRules: [['type', 'action'], ['hostSuffixes']],
380
+ };
381
+ for (const [key, list] of Object.entries(value.miniProgram)) {
382
+ if (!Array.isArray(list) || list.length > 100)
383
+ invalid(`miniProgram.${key}`);
384
+ for (const rule of list) {
385
+ fields(rule, ...shapes[key], `miniProgram.${key}`);
386
+ for (const [field, item] of Object.entries(rule)) {
387
+ if ([
388
+ 'hostSuffixes',
389
+ 'successUrlParams',
390
+ 'failureUrlParams',
391
+ 'forwardParams',
392
+ 'actions',
393
+ ].includes(field)
394
+ ? !strings(item)
395
+ : field === 'useCurrentOrigin'
396
+ ? typeof item !== 'boolean'
397
+ : !text(item))
398
+ invalid(`miniProgram.${key}.${field}`);
399
+ }
400
+ if (key === 'messageRules' &&
401
+ !['stageBackData', 'closeAll'].includes(rule.action))
402
+ invalid('miniProgram.messageRules.action');
403
+ if (key === 'nativeRoutes' &&
404
+ !rule.actions.every(action => ['navigateMiniProgram', 'subscribeMessage'].includes(action)))
405
+ invalid('miniProgram.nativeRoutes.actions');
406
+ }
407
+ }
408
+ }
409
+ }
410
+ export function validateAppProfileContribution(value) {
411
+ if (value === undefined)
412
+ return undefined;
413
+ fields(value, ['version', 'profiles'], [], 'contribution');
414
+ if (value.version !== 1 ||
415
+ !Array.isArray(value.profiles) ||
416
+ !value.profiles.length ||
417
+ value.profiles.length > 20)
418
+ invalid('contribution');
419
+ const profiles = value.profiles.map(validateAppProfileDefinition);
420
+ if (JSON.stringify(profiles).length > 256_000)
421
+ invalid('contribution exceeds 256 KiB');
422
+ if (profiles.some(profile => profile.name.includes('/')))
423
+ invalid('contribution names must be local identifiers');
424
+ if (new Set(profiles.map(profile => profile.name)).size !== profiles.length)
425
+ invalid('duplicate profile name');
426
+ return { version: 1, profiles };
427
+ }
428
+ /** Deterministic identity for immutable definitions; this identifier grants no host permissions. */
429
+ export function appProfileRevision(definition) {
430
+ const canonical = (value) => {
431
+ if (Array.isArray(value))
432
+ return `[${value.map(canonical).join(',')}]`;
433
+ if (record(value))
434
+ return `{${Object.keys(value)
435
+ .sort()
436
+ .map(key => `${JSON.stringify(key)}:${canonical(value[key])}`)
437
+ .join(',')}}`;
438
+ return JSON.stringify(value);
439
+ };
440
+ const source = canonical(definition);
441
+ let left = 2166136261;
442
+ let right = 5381;
443
+ for (let i = 0; i < source.length; i++) {
444
+ left = Math.imul(left ^ source.charCodeAt(i), 16777619);
445
+ right = Math.imul(right, 33) ^ source.charCodeAt(i);
446
+ }
447
+ return `v1-${(left >>> 0).toString(16).padStart(8, '0')}${(right >>> 0).toString(16).padStart(8, '0')}`;
448
+ }