@mearl/browser-core 3.0.0-beta.4 → 3.0.0-beta.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  浏览器与 Node 可共享的页面控制逻辑和协议:DOM/AX 快照、动作观察、输入、滚动、导航、请求序列化、捕获游标、设备模拟、通用设备外观、Cookie/域名权限和插件任务、页面、状态合同。模块没有浏览器扩展或本机进程的安装副作用。
4
4
 
5
- `APP_PROFILE_NAMES` 只列出飞猪、淘宝、微信、支付宝四个基础身份;动态档案 ID 由宿主的已激活能力目录校验。App 档案 schema、显式选项、revision 和页面运行时绑定是数据合同,完整桥实现由独立能力包提供。
5
+ 核心不预置 App 身份;档案 ID 由宿主的已激活能力目录校验。App 档案 schema、显式选项、revision 和页面运行时绑定是数据合同,完整桥实现由独立能力包提供。
6
6
 
7
7
  `@mearl/browser-core/appProfileRuntime` 的内置安装器只同步 UA、navigator.appVersion、navigator.platform 和触摸点,不安装 App 桥、业务全局变量、容器栈或授权处理。有 pageRuntime 绑定时,宿主必须先从已安装的能力包准备确切代码,再通过页面运行时派发器执行;不能调用基础安装器代替。资源准备和派发见 [能力包](../../docs/browser-bundles.md#页面运行时)。
8
8
 
@@ -2,7 +2,7 @@ import type { AppProfileBinding, AppProfilePolicies, AppProfileSetting } from '.
2
2
  import type { BrowserDevicePresetName } from '../browserActionProtocol.js';
3
3
  import type { DeviceCutout } from '../deviceEmulation.js';
4
4
  /** 桥调用风格,与页面适配器使用的端内调用约定一一对应。 */
5
- export type AppBridgeStyle = 'windvane' | 'fliggy' | 'alipay' | 'callback' | 'btrip' | 'invoke' | 'callHandler' | 'amap' | 'didi' | 'easybikeNative' | 'mega' | 'wechat' | 'wechatMiniProgram';
5
+ export type AppBridgeStyle = string;
6
6
  export interface AppBridgeSpec {
7
7
  global: string;
8
8
  style: AppBridgeStyle;
@@ -10,7 +10,7 @@ export interface AppBridgeSpec {
10
10
  persistent?: boolean;
11
11
  }
12
12
  export type AppBridgeBehavior = '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';
13
- /** 桥方法名到标准化动作的映射。WindVane 风格使用 `Class.method`,其余使用 api 名。 */
13
+ /** 桥方法名到标准化动作的映射。命名空间桥使用 `Class.method`,其余使用 api 名。 */
14
14
  export interface BridgeActionMap {
15
15
  open: string[];
16
16
  close: string[];
@@ -68,14 +68,14 @@ export interface AppMiniProgramMessageRule {
68
68
  }
69
69
  export type AppMiniProgramNativeAction = 'navigateMiniProgram' | 'subscribeMessage';
70
70
  export interface AppMiniProgramNativeRoute {
71
- /** 承接微信原生能力的小程序页面 pathname。 */
71
+ /** 承接原生能力的小程序页面 pathname。 */
72
72
  pathname: string;
73
73
  /** 该页面允许模拟的固定原生动作,动作名读取 URL 的 action 参数。 */
74
74
  actions: AppMiniProgramNativeAction[];
75
75
  /** 限制业务根域,避免其他小程序的同名页面被接管。 */
76
76
  hostSuffixes?: string[];
77
77
  }
78
- /** 微信小程序容器相关规则统一收口,避免为每种业务页增加顶层配置。 */
78
+ /** 小程序容器相关规则统一收口,避免为每种业务页增加顶层配置。 */
79
79
  export interface AppMiniProgramConfig {
80
80
  paymentRoutes?: AppMiniProgramPaymentRoute[];
81
81
  nativeRoutes?: AppMiniProgramNativeRoute[];
@@ -187,6 +187,8 @@ export interface InjectedAppProfileConfig {
187
187
  binding?: AppProfileBinding;
188
188
  policies?: AppProfilePolicies;
189
189
  settings?: AppProfileSetting[];
190
+ settingsUi?: 'runtime';
191
+ settingsGroup?: string;
190
192
  profile: string;
191
193
  label: string;
192
194
  globals: Record<string, string>;
@@ -1,9 +1,9 @@
1
1
  import type { BrowserDevicePresetName, AppBridgeConfig } from './browserActionProtocol.js';
2
2
  import type { AppBridgeSpec, AppImagePickerConfig, AppLayerUrlProxyConfig, AppMiniProgramConfig, AppNativeBackConfig, AppOverlayConfig, AppSchemeRule, AppTitleBarHostStyle, BridgeActionMap } from './app-profile-runtime/types.js';
3
3
  export interface AppProfilePolicies {
4
- titleBar?: 'default' | 'taobao' | 'fliggy' | 'alipay' | 'wechat';
4
+ titleBar?: string;
5
5
  navigatorLanguage?: boolean;
6
- networkResult?: 'default' | 'alipay';
6
+ networkResult?: string;
7
7
  languages?: string[];
8
8
  defaultLanguage?: string;
9
9
  }
@@ -56,28 +56,49 @@ export interface AppProfileCatalogEntry {
56
56
  revision: string;
57
57
  definition: AppProfileDefinition;
58
58
  }
59
+ /** Short, author-supplied guidance; discovery does not load Skills or follow links. */
60
+ export interface AppProfileUsage {
61
+ description?: string;
62
+ limitations?: string[];
63
+ skills?: string[];
64
+ documentation?: Array<{
65
+ title: string;
66
+ url: string;
67
+ }>;
68
+ }
59
69
  /** 单容器档案(纯数据) */
60
70
  export interface AppProfileDefinition {
61
71
  name: string;
72
+ usage?: AppProfileUsage;
62
73
  /** Content-addressed installer supplied by the selected browser capability package. */
63
74
  pageRuntime?: {
64
75
  id: string;
65
76
  sha256: string;
66
77
  };
67
78
  label: string;
68
- /** 容器的应用标识,如 LX / TB / AP / WeChat */
79
+ actionIcon?: string;
80
+ labels?: {
81
+ en: string;
82
+ zh_CN?: string;
83
+ };
84
+ /** 容器的应用标识,由能力包定义 */
69
85
  appName: string;
70
86
  defaultAppVersion: string;
71
- /** UA 主体格式;默认使用 AliApp(appName/version) */
72
- uaFamily?: 'aliapp' | 'wechatMiniProgram';
87
+ /** UA 主体格式;未提供时使用标准浏览器 UA */
88
+ uaFamily?: string;
73
89
  uaTemplate?: {
74
90
  ios: string;
75
91
  android: string;
76
92
  };
77
93
  policies?: AppProfilePolicies;
78
94
  settings?: AppProfileSetting[];
79
- settingsGroup?: 'fliggy' | 'alipay';
80
- /** AliApp UA 主体之后追加的 token,{v} 占位会被替换为 appVersion */
95
+ authorization?: {
96
+ hook: string;
97
+ method: string;
98
+ };
99
+ settingsUi?: 'runtime';
100
+ settingsGroup?: string;
101
+ /** UA 主体之后追加的 token,{v} 占位会被替换为 appVersion */
81
102
  uaTokens: string[];
82
103
  /** 额外注入到 window 的变量(公共变量由 resolve 统一生成) */
83
104
  globals: Record<string, string>;
@@ -1,18 +1,3 @@
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
1
  export const APP_BRIDGE_BEHAVIORS = [
17
2
  'navigate',
18
3
  'closeAll',
@@ -133,12 +118,17 @@ export function validateAppProfileDefinition(value) {
133
118
  'schemes',
134
119
  ];
135
120
  const optional = [
121
+ 'usage',
122
+ 'actionIcon',
123
+ 'labels',
136
124
  'pageRuntime',
137
125
  'uaFamily',
138
126
  'uaTemplate',
139
127
  'policies',
140
128
  'settings',
141
129
  'settingsGroup',
130
+ 'settingsUi',
131
+ 'authorization',
142
132
  'miniProgram',
143
133
  'imagePicker',
144
134
  'geolocation',
@@ -160,6 +150,37 @@ export function validateAppProfileDefinition(value) {
160
150
  invalid(key);
161
151
  if (!/^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)?$/.test(value.name))
162
152
  invalid('name');
153
+ if (value.usage !== undefined) {
154
+ const usage = value.usage;
155
+ fields(usage, [], ['description', 'limitations', 'skills', 'documentation'], 'usage');
156
+ if (new TextEncoder().encode(JSON.stringify(usage)).byteLength > 4096)
157
+ invalid('usage exceeds 4 KiB');
158
+ if (usage.description !== undefined && (!text(usage.description) || usage.description.length > 512))
159
+ invalid('usage.description');
160
+ for (const key of ['limitations', 'skills']) {
161
+ const items = usage[key];
162
+ if (items !== undefined && (!Array.isArray(items) || items.length > 8 || items.some(item => !text(item) || !item || item.length > 256)))
163
+ invalid(`usage.${key}`);
164
+ }
165
+ if (usage.documentation !== undefined) {
166
+ if (!Array.isArray(usage.documentation) || usage.documentation.length > 8)
167
+ invalid('usage.documentation');
168
+ for (const link of usage.documentation) {
169
+ fields(link, ['title', 'url'], [], 'usage.documentation');
170
+ if (!text(link.title) || !link.title || link.title.length > 128 || !text(link.url) || link.url.length > 1024)
171
+ invalid('usage.documentation');
172
+ let url;
173
+ try {
174
+ url = new URL(link.url);
175
+ }
176
+ catch {
177
+ invalid('usage.documentation.url');
178
+ }
179
+ if (url.protocol !== 'https:' || url.username || url.password)
180
+ invalid('usage.documentation.url');
181
+ }
182
+ }
183
+ }
163
184
  if (value.pageRuntime !== undefined) {
164
185
  fields(value.pageRuntime, ['id', 'sha256'], [], 'pageRuntime');
165
186
  const runtime = value.pageRuntime;
@@ -191,17 +212,28 @@ export function validateAppProfileDefinition(value) {
191
212
  .split('.')
192
213
  .some(part => ['__proto__', 'prototype', 'constructor'].includes(part)) ||
193
214
  globals.has(bridge.global) ||
194
- !BRIDGE_STYLES.has(bridge.style) ||
215
+ (typeof bridge.style !== 'string' || !/^[a-zA-Z][a-zA-Z0-9-]{0,63}$/.test(bridge.style)) ||
195
216
  (bridge.persistent !== undefined && typeof bridge.persistent !== 'boolean'))
196
217
  invalid('bridge');
197
218
  globals.add(bridge.global);
198
219
  }
199
220
  validateActions(value.actions);
221
+ if (value.labels !== undefined) {
222
+ fields(value.labels, ['en'], ['zh_CN'], 'labels');
223
+ if (!text(value.labels.en) || !value.labels.en || value.labels.en.length > 120 || value.labels.zh_CN !== undefined && (!text(value.labels.zh_CN) || value.labels.zh_CN.length > 120))
224
+ invalid('labels');
225
+ }
226
+ if (value.actionIcon !== undefined && (typeof value.actionIcon !== 'string' || value.actionIcon.length > 32768 || !/^data:image\/png;base64,iVBORw0KGgo[A-Za-z0-9+/]*={0,2}$/.test(value.actionIcon)))
227
+ invalid('actionIcon');
228
+ if (value.authorization !== undefined && (!record(value.authorization) || Object.keys(value.authorization).sort().join(',') !== 'hook,method' || typeof value.authorization.hook !== 'string' || !/^[a-z][a-zA-Z0-9_.-]{0,95}$/.test(value.authorization.hook) || typeof value.authorization.method !== 'string' || !value.authorization.method || value.authorization.method.length > 200))
229
+ invalid('authorization');
230
+ if (value.settingsUi !== undefined && value.settingsUi !== 'runtime')
231
+ throw new Error('Invalid App settings UI');
200
232
  if (value.settingsGroup !== undefined &&
201
- !['fliggy', 'alipay'].includes(value.settingsGroup))
233
+ (typeof value.settingsGroup !== 'string' || value.settingsGroup.length > 64))
202
234
  invalid('settingsGroup');
203
235
  if (value.uaFamily !== undefined &&
204
- !['aliapp', 'wechatMiniProgram'].includes(value.uaFamily))
236
+ (typeof value.uaFamily !== 'string' || value.uaFamily.length > 64))
205
237
  invalid('uaFamily');
206
238
  if (value.uaTemplate !== undefined) {
207
239
  fields(value.uaTemplate, ['ios', 'android'], [], 'uaTemplate');
@@ -212,9 +244,9 @@ export function validateAppProfileDefinition(value) {
212
244
  fields(value.policies, [], ['titleBar', 'navigatorLanguage', 'networkResult', 'languages', 'defaultLanguage'], 'policies');
213
245
  const p = value.policies;
214
246
  if (p.titleBar !== undefined &&
215
- !['default', 'taobao', 'fliggy', 'alipay', 'wechat'].includes(p.titleBar))
247
+ (typeof p.titleBar !== 'string' || p.titleBar.length > 64))
216
248
  invalid('policies.titleBar');
217
- if (p.networkResult !== undefined && !['default', 'alipay'].includes(p.networkResult))
249
+ if (p.networkResult !== undefined && (typeof p.networkResult !== 'string' || p.networkResult.length > 64))
218
250
  invalid('policies.networkResult');
219
251
  if (p.navigatorLanguage !== undefined && typeof p.navigatorLanguage !== 'boolean')
220
252
  invalid('policies.navigatorLanguage');
@@ -1,3 +1,8 @@
1
+ import type { BrowserCapabilityCatalog } from './browserCapabilities.js';
2
+ export interface GetCapabilitiesParams {
3
+ /** Include method and setting details for this installed App ID. */
4
+ app?: string;
5
+ }
1
6
  export interface GetRequestsParams {
2
7
  tabId: number;
3
8
  count?: number;
@@ -486,8 +491,8 @@ export interface TabOpenParams {
486
491
  emulation?: BrowserEmulationOptions;
487
492
  group?: string;
488
493
  /**
489
- * 客户端容器模拟档案名(如 'fliggy')。只注入容器 UA、环境变量、JSBridge 与桥协议;
490
- * 设备视口和 iframe 系统外壳由 emulation.deviceShell 独立控制。可用档案见 APP_PROFILE_NAMES。
494
+ * 客户端容器模拟档案名(由已安装能力目录提供)。只注入容器 UA、环境变量、JSBridge 与桥协议;
495
+ * 设备视口和 iframe 系统外壳由 emulation.deviceShell 独立控制。可用档案通过 get_capabilities 查询。
491
496
  */
492
497
  appProfile?: AppProfileName;
493
498
  /** 与 appProfile 配合使用;容器系统,默认 ios */
@@ -504,16 +509,9 @@ export type TabGroupParams = {
504
509
  ungroup: true;
505
510
  title?: never;
506
511
  };
507
- /**
508
- * 客户端容器模拟可用档案名。
509
- *
510
- * 档案数据(UA / 桥对象 / 方法映射)在 @mearl/app-profiles 维护,但名单属于协议:
511
- * 它是 set_app_profile.profile 与 tab_open.appProfile 的取值域,CLI 帮助与 MCP 工具
512
- * schema 的 enum 也由它生成。app-profiles 反向引用本清单约束自己的档案表,
513
- * 因此新增档案时只需改两处(名单 + 档案数据),且漏改会在类型检查时暴露。
514
- */
515
- export declare const APP_PROFILE_NAMES: readonly ["fliggy", "taobao", "wechat", "alipay"];
516
- export type BuiltinAppProfileName = (typeof APP_PROFILE_NAMES)[number];
512
+ /** App identities are supplied by installed capability catalogs. */
513
+ export declare const APP_PROFILE_NAMES: readonly string[];
514
+ export type BuiltinAppProfileName = string;
517
515
  /** Hosts validate dynamic profile IDs against the active capability catalog. */
518
516
  export type AppProfileName = string;
519
517
  export interface SetAppProfileParams {
@@ -765,7 +763,7 @@ export interface SetCookieResult {
765
763
  export interface RequestDomainPermissionParams {
766
764
  domain: string;
767
765
  }
768
- export type RunActionName = Exclude<ExtensionBrowserAction, 'run_actions' | 'browser_release'>;
766
+ export type RunActionName = Exclude<ExtensionBrowserAction, 'run_actions' | 'browser_release' | 'get_capabilities'>;
769
767
  export interface ActionStep {
770
768
  action: RunActionName;
771
769
  data?: Record<string, unknown>;
@@ -775,6 +773,10 @@ export interface RunActionsParams {
775
773
  tabId: number;
776
774
  }
777
775
  export interface BrowserActionMap {
776
+ get_capabilities: {
777
+ request: GetCapabilitiesParams;
778
+ response: BrowserCapabilityCatalog;
779
+ };
778
780
  tab_checkpoint: {
779
781
  request: CaptureCheckpointParams;
780
782
  response: CaptureCheckpointResult;
@@ -979,6 +981,7 @@ export declare const BROWSER_ACTIONS: {
979
981
  readonly get_mocks: true;
980
982
  readonly set_rule: true;
981
983
  readonly get_rules: true;
984
+ readonly get_capabilities: true;
982
985
  readonly send_request: true;
983
986
  readonly send_mtop_request: true;
984
987
  readonly browser_list: true;
@@ -1025,6 +1028,7 @@ export declare const BROWSER_COMMAND_ACTIONS: {
1025
1028
  readonly get_mocks: true;
1026
1029
  readonly set_rule: true;
1027
1030
  readonly get_rules: true;
1031
+ readonly get_capabilities: true;
1028
1032
  readonly send_request: true;
1029
1033
  readonly send_mtop_request: true;
1030
1034
  readonly browser_list: true;
@@ -1070,6 +1074,7 @@ export declare const EXTENSION_BROWSER_ACTIONS: {
1070
1074
  readonly get_mocks: true;
1071
1075
  readonly set_rule: true;
1072
1076
  readonly get_rules: true;
1077
+ readonly get_capabilities: true;
1073
1078
  readonly send_request: true;
1074
1079
  readonly send_mtop_request: true;
1075
1080
  readonly browser_release: true;
@@ -1111,6 +1116,7 @@ export declare const BROWSER_CAPABILITY_ACTIONS: {
1111
1116
  readonly get_mocks: true;
1112
1117
  readonly set_rule: true;
1113
1118
  readonly get_rules: true;
1119
+ readonly get_capabilities: true;
1114
1120
  readonly send_request: true;
1115
1121
  readonly send_mtop_request: true;
1116
1122
  readonly browser_release: true;
@@ -20,15 +20,8 @@ export const BROWSER_DEVICE_PRESET_NAMES = [
20
20
  'ipad-mini',
21
21
  'galaxy-s23',
22
22
  ];
23
- /**
24
- * 客户端容器模拟可用档案名。
25
- *
26
- * 档案数据(UA / 桥对象 / 方法映射)在 @mearl/app-profiles 维护,但名单属于协议:
27
- * 它是 set_app_profile.profile 与 tab_open.appProfile 的取值域,CLI 帮助与 MCP 工具
28
- * schema 的 enum 也由它生成。app-profiles 反向引用本清单约束自己的档案表,
29
- * 因此新增档案时只需改两处(名单 + 档案数据),且漏改会在类型检查时暴露。
30
- */
31
- export const APP_PROFILE_NAMES = ['fliggy', 'taobao', 'wechat', 'alipay'];
23
+ /** App identities are supplied by installed capability catalogs. */
24
+ export const APP_PROFILE_NAMES = [];
32
25
  export function resolvePageEvalExpression(params) {
33
26
  if (typeof params.expression !== 'string' || !params.expression.trim())
34
27
  throw new Error('Invalid expression');
@@ -83,6 +76,10 @@ export function actionRequiresTabId(action) {
83
76
  return typeof action === 'string' && TAB_ID_REQUIRED_ACTION_SET.has(action);
84
77
  }
85
78
  export function validateRequiredActionTabId(action, data) {
79
+ if (action === 'get_capabilities' && data && typeof data === 'object' &&
80
+ ['tabId', 'tabIds', 'openerTabId'].some(key => Object.hasOwn(data, key))) {
81
+ throw new Error('get_capabilities does not accept page targets');
82
+ }
86
83
  const cookieAction = action === 'get_cookie' || action === 'set_cookie';
87
84
  if (!actionRequiresTabId(action) && !cookieAction)
88
85
  return;
@@ -123,6 +120,7 @@ export const BROWSER_ACTIONS = {
123
120
  get_mocks: true,
124
121
  set_rule: true,
125
122
  get_rules: true,
123
+ get_capabilities: true,
126
124
  send_request: true,
127
125
  send_mtop_request: true,
128
126
  browser_list: true,
@@ -169,6 +167,7 @@ export const BROWSER_COMMAND_ACTIONS = {
169
167
  get_mocks: true,
170
168
  set_rule: true,
171
169
  get_rules: true,
170
+ get_capabilities: true,
172
171
  send_request: true,
173
172
  send_mtop_request: true,
174
173
  browser_list: true,
@@ -214,6 +213,7 @@ export const EXTENSION_BROWSER_ACTIONS = {
214
213
  get_mocks: true,
215
214
  set_rule: true,
216
215
  get_rules: true,
216
+ get_capabilities: true,
217
217
  send_request: true,
218
218
  send_mtop_request: true,
219
219
  browser_release: true,
@@ -0,0 +1,45 @@
1
+ import type { AppProfileCatalogEntry, AppProfileSetting, AppProfileUsage } from './appProfileDefinition.js';
2
+ import type { GetCapabilitiesParams } from './browserActionProtocol.js';
3
+ export interface AppCapability {
4
+ id: string;
5
+ label: string;
6
+ source: AppProfileCatalogEntry['source'];
7
+ plugin?: string;
8
+ available: boolean;
9
+ features: string[];
10
+ usage?: AppProfileUsage;
11
+ details?: {
12
+ defaultAppVersion: string;
13
+ bridges: string[];
14
+ methods: Record<string, string[]>;
15
+ behaviors: Record<string, string>;
16
+ settings: AppProfileSetting[];
17
+ };
18
+ }
19
+ export interface BrowserCapabilityCatalog {
20
+ /** Supported operations, not a grant of page permissions or business authorization. */
21
+ actions: string[];
22
+ devicePresets: string[];
23
+ apps: {
24
+ status: 'available' | 'partial' | 'unsupported';
25
+ entries: AppCapability[];
26
+ issues: Array<{
27
+ plugin: string;
28
+ error: string;
29
+ }>;
30
+ };
31
+ limitations: string[];
32
+ help: {
33
+ skill: string;
34
+ commands: string[];
35
+ };
36
+ }
37
+ /** Project validated installed metadata without scripts, global values or Mock response bodies. */
38
+ export declare function describeBrowserCapabilities(actions: readonly string[], catalog: {
39
+ profiles: AppProfileCatalogEntry[];
40
+ errors: Array<{
41
+ plugin: string;
42
+ error: string;
43
+ }>;
44
+ offline?: boolean;
45
+ } | undefined, params?: GetCapabilitiesParams): BrowserCapabilityCatalog;
@@ -0,0 +1,49 @@
1
+ import { BROWSER_DEVICE_PRESET_NAMES } from './browserActionProtocol.js';
2
+ /** Project validated installed metadata without scripts, global values or Mock response bodies. */
3
+ export function describeBrowserCapabilities(actions, catalog, params = {}) {
4
+ if (!params || typeof params !== 'object' || Array.isArray(params))
5
+ throw new Error('Invalid capability query');
6
+ if (params.app !== undefined && (typeof params.app !== 'string' || !params.app || params.app.length > 161))
7
+ throw new Error('Invalid App capability ID');
8
+ if (params.app && catalog && !catalog.offline && !catalog.errors.length && !catalog.profiles.some(entry => entry.id === params.app))
9
+ throw new Error(`App profile is not installed: ${params.app}`);
10
+ const supportsApps = actions.includes('set_app_profile');
11
+ const profiles = supportsApps ? catalog?.profiles ?? [] : [];
12
+ return {
13
+ actions: [...new Set(actions)],
14
+ devicePresets: actions.includes('set_device_emulation') ? [...BROWSER_DEVICE_PRESET_NAMES] : [],
15
+ apps: {
16
+ status: !supportsApps || !catalog ? 'unsupported' : catalog.offline || catalog.errors.length || profiles.some(entry => entry.available === false) ? 'partial' : 'available',
17
+ entries: profiles.filter(entry => !params.app || entry.id === params.app).map(entry => {
18
+ const definition = entry.definition;
19
+ const methods = {};
20
+ for (const [name, values] of Object.entries(definition.actions)) {
21
+ const names = Array.isArray(values) ? values : Object.keys(values ?? {});
22
+ if (names.length)
23
+ methods[name] = [...names];
24
+ }
25
+ return {
26
+ id: entry.id, label: entry.label, source: entry.source, plugin: entry.plugin,
27
+ available: entry.available !== false,
28
+ features: ['userAgent', ...(definition.bridges.length ? ['jsBridge'] : []), ...Object.keys(methods), ...(definition.schemes.length ? ['schemes'] : []), ...(definition.settings?.length ? ['settings'] : [])],
29
+ ...(definition.usage ? { usage: structuredClone(definition.usage) } : {}),
30
+ ...(params.app ? { details: {
31
+ defaultAppVersion: definition.defaultAppVersion,
32
+ bridges: definition.bridges.map(bridge => bridge.global),
33
+ methods,
34
+ behaviors: { ...definition.actions.behaviors },
35
+ settings: structuredClone(definition.settings ?? []),
36
+ } } : {}),
37
+ };
38
+ }),
39
+ issues: catalog?.errors.map(issue => ({ ...issue })) ?? [],
40
+ },
41
+ limitations: [
42
+ 'Actions describe support, not current domain permissions, login state or device connectivity.',
43
+ 'App profiles simulate a WebView. Declared bridge methods, behaviors and response stubs do not guarantee real native authorization, payment or other business effects.',
44
+ 'Package usage and Skill names are guidance from the package author; referenced Skills may need separate installation. Read the relevant help only when needed.',
45
+ ...(!catalog && supportsApps ? ['This Provider does not expose an installed App catalog.'] : []),
46
+ ],
47
+ help: { skill: 'mearl', commands: ['mearl get_capabilities --help', 'mearl set_device_emulation --help', 'mearl set_app_profile --help', 'mearl tab_open --help'] },
48
+ };
49
+ }
package/dist/cookie.d.ts CHANGED
@@ -25,6 +25,12 @@ export interface CookieCandidate {
25
25
  partitionKey?: unknown;
26
26
  partitionKeyOpaque?: boolean;
27
27
  }
28
+ export type RequestCookie = Pick<CookieCandidate, 'name' | 'value'>;
29
+ export declare function serializeRequestCookies(cookies: readonly RequestCookie[]): string;
30
+ /** Token lookup follows the first (most specific path) occurrence in the Cookie header. */
31
+ export declare function requestCookiesToMap(cookies: readonly RequestCookie[]): Map<string, string>;
32
+ /** Chromium also treats localhost and loopback URLs as trustworthy for Secure cookies. */
33
+ export declare function isSecureCookieUrl(url: URL): boolean;
28
34
  /** Filter browser-context storage results to the cookies applicable to the requested URL. */
29
35
  export declare function filterCookiesForUrl<T extends CookieCandidate>(cookies: readonly T[], cookieUrl: string): T[];
30
36
  /** Select one readable, non-partitioned named cookie from URL-filtered backend results. */
package/dist/cookie.js CHANGED
@@ -1,3 +1,15 @@
1
+ export function serializeRequestCookies(cookies) {
2
+ return cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ');
3
+ }
4
+ /** Token lookup follows the first (most specific path) occurrence in the Cookie header. */
5
+ export function requestCookiesToMap(cookies) {
6
+ const values = new Map();
7
+ for (const cookie of cookies) {
8
+ if (!values.has(cookie.name))
9
+ values.set(cookie.name, cookie.value);
10
+ }
11
+ return values;
12
+ }
1
13
  function normalizeCookieDomain(input, action) {
2
14
  const value = input.trim();
3
15
  if (!value)
@@ -14,21 +26,24 @@ function normalizeCookieDomain(input, action) {
14
26
  throw new Error(`${action}: invalid domain "${input}"`);
15
27
  return hostname;
16
28
  }
17
- /** Filter browser-context storage results to the cookies applicable to the requested URL. */
18
- export function filterCookiesForUrl(cookies, cookieUrl) {
19
- const url = new URL(cookieUrl);
29
+ /** Chromium also treats localhost and loopback URLs as trustworthy for Secure cookies. */
30
+ export function isSecureCookieUrl(url) {
20
31
  const hostname = url.hostname.replace(/\.$/, '');
21
32
  const isLocalhost = hostname === 'localhost' ||
22
33
  hostname.endsWith('.localhost') ||
23
34
  hostname === '[::1]' ||
24
35
  /^127\.\d+\.\d+\.\d+$/.test(hostname);
36
+ return url.protocol === 'https:' || isLocalhost;
37
+ }
38
+ /** Filter browser-context storage results to the cookies applicable to the requested URL. */
39
+ export function filterCookiesForUrl(cookies, cookieUrl) {
40
+ const url = new URL(cookieUrl);
25
41
  return cookies.filter(item => {
26
42
  const domain = item.domain.replace(/^\./, '').toLowerCase();
27
43
  const hostOnly = item.hostOnly ?? !item.domain.startsWith('.');
28
44
  if (url.hostname !== domain && (hostOnly || !url.hostname.endsWith(`.${domain}`)))
29
45
  return false;
30
- // Chromium treats localhost and loopback URLs as trustworthy for Secure cookies.
31
- if (item.secure && url.protocol !== 'https:' && !isLocalhost)
46
+ if (item.secure && !isSecureCookieUrl(url))
32
47
  return false;
33
48
  return (url.pathname === item.path ||
34
49
  (url.pathname.startsWith(item.path) &&
@@ -0,0 +1,8 @@
1
+ export interface StructureOptions {
2
+ maxDepth?: number;
3
+ maxKeys?: number;
4
+ arraySamples?: number;
5
+ maxNodes?: number;
6
+ }
7
+ /** Describes a bounded sample of a value without invoking accessors or exposing leaf values. */
8
+ export declare function getStructure(data: unknown, options?: StructureOptions): unknown;
@@ -0,0 +1,58 @@
1
+ /** Describes a bounded sample of a value without invoking accessors or exposing leaf values. */
2
+ export function getStructure(data, options = {}) {
3
+ const { maxDepth = 5, maxKeys = 50, arraySamples = 1, maxNodes = 2000 } = options;
4
+ for (const [name, value, limit] of [
5
+ ['maxDepth', maxDepth, 20],
6
+ ['maxKeys', maxKeys, 1000],
7
+ ['arraySamples', arraySamples, 20],
8
+ ['maxNodes', maxNodes, 10000],
9
+ ]) {
10
+ if (!Number.isSafeInteger(value) || value < 1 || value > limit)
11
+ throw new Error(`${name} must be an integer between 1 and ${limit}`);
12
+ }
13
+ const ancestors = new WeakSet();
14
+ let nodes = 0;
15
+ const visit = (value, depth) => {
16
+ if (++nodes > maxNodes)
17
+ return '<max nodes reached>';
18
+ if (depth >= maxDepth)
19
+ return '<max depth reached>';
20
+ if (value === null)
21
+ return 'null';
22
+ if (typeof value !== 'object')
23
+ return typeof value;
24
+ if (ancestors.has(value))
25
+ return '<circular>';
26
+ ancestors.add(value);
27
+ try {
28
+ if (Array.isArray(value)) {
29
+ if (!value.length)
30
+ return 'array[0]';
31
+ const samples = Array.from({ length: Math.min(value.length, arraySamples) }, (_, index) => {
32
+ const descriptor = Object.getOwnPropertyDescriptor(value, index);
33
+ return descriptor && 'value' in descriptor
34
+ ? visit(descriptor.value, depth + 1)
35
+ : descriptor
36
+ ? '<accessor>'
37
+ : 'undefined';
38
+ });
39
+ return {
40
+ _type: `array[${value.length}]`,
41
+ ...(arraySamples === 1 ? { _sample: samples[0] } : { _samples: samples }),
42
+ };
43
+ }
44
+ const keys = Object.keys(value);
45
+ const entries = keys.slice(0, maxKeys).map(key => {
46
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
47
+ return [key, 'value' in descriptor ? visit(descriptor.value, depth + 1) : '<accessor>'];
48
+ });
49
+ if (keys.length > maxKeys)
50
+ entries.push(['<truncated keys>', keys.length - maxKeys]);
51
+ return Object.fromEntries(entries);
52
+ }
53
+ finally {
54
+ ancestors.delete(value);
55
+ }
56
+ };
57
+ return visit(data, 0);
58
+ }
@@ -365,9 +365,22 @@ export function clickElementViaJS(expectedText) {
365
365
  let hitTarget = null;
366
366
  try {
367
367
  hitTarget = document.elementFromPoint(cx, cy);
368
+ const visited = new Set();
369
+ while (hitTarget?.shadowRoot?.elementFromPoint && !visited.has(hitTarget)) {
370
+ visited.add(hitTarget);
371
+ const inner = hitTarget.shadowRoot.elementFromPoint(cx, cy);
372
+ if (!inner || inner === hitTarget)
373
+ break;
374
+ hitTarget = inner;
375
+ }
368
376
  }
369
377
  catch { }
378
+ let composedHit = hitTarget;
379
+ while (composedHit && composedHit !== target) {
380
+ composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
381
+ }
370
382
  if (hitTarget &&
383
+ !composedHit &&
371
384
  hitTarget !== target &&
372
385
  !target.contains?.(hitTarget) &&
373
386
  !hitTarget.contains?.(target)) {
@@ -680,9 +693,22 @@ export function clickElementBySelector(selector, invalidSelectorHint) {
680
693
  let hitTarget = null;
681
694
  try {
682
695
  hitTarget = document.elementFromPoint(cx, cy);
696
+ const visited = new Set();
697
+ while (hitTarget?.shadowRoot?.elementFromPoint && !visited.has(hitTarget)) {
698
+ visited.add(hitTarget);
699
+ const inner = hitTarget.shadowRoot.elementFromPoint(cx, cy);
700
+ if (!inner || inner === hitTarget)
701
+ break;
702
+ hitTarget = inner;
703
+ }
683
704
  }
684
705
  catch { }
706
+ let composedHit = hitTarget;
707
+ while (composedHit && composedHit !== target) {
708
+ composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
709
+ }
685
710
  if (hitTarget &&
711
+ !composedHit &&
686
712
  hitTarget !== target &&
687
713
  !target.contains(hitTarget) &&
688
714
  !hitTarget.contains(target)) {
@@ -1001,9 +1027,22 @@ export function getClickCoordsFromSelector(selector, invalidSelectorHint) {
1001
1027
  let hitTarget = null;
1002
1028
  try {
1003
1029
  hitTarget = document.elementFromPoint(x, y);
1030
+ const visited = new Set();
1031
+ while (hitTarget?.shadowRoot?.elementFromPoint && !visited.has(hitTarget)) {
1032
+ visited.add(hitTarget);
1033
+ const inner = hitTarget.shadowRoot.elementFromPoint(x, y);
1034
+ if (!inner || inner === hitTarget)
1035
+ break;
1036
+ hitTarget = inner;
1037
+ }
1004
1038
  }
1005
1039
  catch { }
1040
+ let composedHit = hitTarget;
1041
+ while (composedHit && composedHit !== target) {
1042
+ composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
1043
+ }
1006
1044
  if (hitTarget &&
1045
+ !composedHit &&
1007
1046
  hitTarget !== target &&
1008
1047
  !target.contains(hitTarget) &&
1009
1048
  !hitTarget.contains(target)) {
@@ -1295,9 +1334,22 @@ export function getClickCoordsFromNode(expectedText) {
1295
1334
  let hitTarget = null;
1296
1335
  try {
1297
1336
  hitTarget = document.elementFromPoint(x, y);
1337
+ const visited = new Set();
1338
+ while (hitTarget?.shadowRoot?.elementFromPoint && !visited.has(hitTarget)) {
1339
+ visited.add(hitTarget);
1340
+ const inner = hitTarget.shadowRoot.elementFromPoint(x, y);
1341
+ if (!inner || inner === hitTarget)
1342
+ break;
1343
+ hitTarget = inner;
1344
+ }
1298
1345
  }
1299
1346
  catch { }
1347
+ let composedHit = hitTarget;
1348
+ while (composedHit && composedHit !== target) {
1349
+ composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
1350
+ }
1300
1351
  if (hitTarget &&
1352
+ !composedHit &&
1301
1353
  hitTarget !== target &&
1302
1354
  !target.contains?.(hitTarget) &&
1303
1355
  !hitTarget.contains?.(target)) {
@@ -211,9 +211,21 @@ export function inspectElementsInPage(selector, filter, styles, maxResults, inva
211
211
  let hit = null;
212
212
  try {
213
213
  hit = visible ? (documentRef.elementFromPoint?.(x, y) ?? null) : null;
214
+ const visited = new Set();
215
+ while (hit?.shadowRoot?.elementFromPoint && !visited.has(hit)) {
216
+ visited.add(hit);
217
+ const inner = hit.shadowRoot.elementFromPoint(x, y);
218
+ if (!inner || inner === hit)
219
+ break;
220
+ hit = inner;
221
+ }
214
222
  }
215
223
  catch { }
216
- const topmost = !!hit && (hit === element || element.contains?.(hit));
224
+ let composedHit = hit;
225
+ while (composedHit && composedHit !== element) {
226
+ composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
227
+ }
228
+ const topmost = !!hit && (!!composedHit || element.contains?.(hit));
217
229
  const target = hit
218
230
  ? {
219
231
  selector: selectorFor(hit),
@@ -206,6 +206,7 @@ export interface HostToExtensionActionMap {
206
206
  __control_cleanup: {
207
207
  request: BrowserReleaseParams & {
208
208
  tabIds: number[];
209
+ returningToAgent?: boolean;
209
210
  };
210
211
  response: BrowserReleaseResult;
211
212
  };
package/dist/index.d.ts CHANGED
@@ -37,6 +37,7 @@ export * from './pluginPages.js';
37
37
  export * from './pluginPageRuntime.js';
38
38
  export * from './pluginPageHost.js';
39
39
  export * from './browserActionProtocol.js';
40
+ export * from './browserCapabilities.js';
40
41
  export * from './runActions.js';
41
42
  export * from './sendRequestFiles.js';
42
43
  export * from './sendRequestHeaders.js';
@@ -57,3 +58,4 @@ export * from './consumeSseResponse.js';
57
58
  export { parseQueryString } from './queryString.js';
58
59
  export * from './extensionVersion.js';
59
60
  export { MEARL_NATIVE_HOST_NAMES, type NativeHostIntegrationMode } from './nativeHostNames.js';
61
+ export * from './dataStructure.js';
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ export * from './pluginPages.js';
36
36
  export * from './pluginPageRuntime.js';
37
37
  export * from './pluginPageHost.js';
38
38
  export * from './browserActionProtocol.js';
39
+ export * from './browserCapabilities.js';
39
40
  export * from './runActions.js';
40
41
  export * from './sendRequestFiles.js';
41
42
  export * from './sendRequestHeaders.js';
@@ -56,3 +57,4 @@ export * from './consumeSseResponse.js';
56
57
  export { parseQueryString } from './queryString.js';
57
58
  export * from './extensionVersion.js';
58
59
  export { MEARL_NATIVE_HOST_NAMES } from './nativeHostNames.js';
60
+ export * from './dataStructure.js';
@@ -85,6 +85,7 @@ export function isDomainAuthorized(domain, allowedOrigins) {
85
85
  export const ACTIONS_SKIP_DOMAIN_CHECK = new Set([
86
86
  'request_domain_permission',
87
87
  'get_browser_info',
88
+ 'get_capabilities',
88
89
  '__export_cookies',
89
90
  'send_mtop_request',
90
91
  'run_actions',
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Request serialization utilities — pure computation, no environment dependencies.
3
3
  */
4
+ import { getStructure } from './dataStructure.js';
4
5
  // ─── Path utilities ───────────────────────────────────────────────────────────
5
6
  function parsePath(path) {
6
7
  if (path.length > 1000)
@@ -67,35 +68,6 @@ export function setValueByPath(obj, path, value) {
67
68
  current[keys[keys.length - 1]] = value;
68
69
  }
69
70
  // ─── Structure helpers ────────────────────────────────────────────────────────
70
- function getStructure(data, maxDepth = 5, currentDepth = 0) {
71
- if (currentDepth >= maxDepth)
72
- return '<max depth reached>';
73
- if (data === null)
74
- return 'null';
75
- if (data === undefined)
76
- return 'undefined';
77
- const type = typeof data;
78
- if (type === 'string' || type === 'number' || type === 'boolean')
79
- return type;
80
- if (Array.isArray(data)) {
81
- if (data.length === 0)
82
- return 'array[0]';
83
- return {
84
- _type: `array[${data.length}]`,
85
- _sample: getStructure(data[0], maxDepth, currentDepth + 1),
86
- };
87
- }
88
- if (type === 'object') {
89
- const structure = {};
90
- for (const key in data) {
91
- if (Object.prototype.hasOwnProperty.call(data, key)) {
92
- structure[key] = getStructure(data[key], maxDepth, currentDepth + 1);
93
- }
94
- }
95
- return structure;
96
- }
97
- return type;
98
- }
99
71
  function truncateData(data, maxSize) {
100
72
  const str = JSON.stringify(data);
101
73
  if (str.length <= maxSize)
@@ -22,7 +22,7 @@ export async function runActionBatch(data, execute, options = {}) {
22
22
  validationError = `unsupported action "${action}"`;
23
23
  else if (action === 'run_actions')
24
24
  validationError = `composite action "${action}" is not allowed in run_actions`;
25
- else if (action === 'browser_release')
25
+ else if (action === 'browser_release' || action === 'get_capabilities')
26
26
  validationError = `browser-level action "${action}" is not allowed in run_actions`;
27
27
  if (validationError)
28
28
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/browser-core",
3
- "version": "3.0.0-beta.4",
3
+ "version": "3.0.0-beta.5",
4
4
  "description": "Shared pure-logic utilities for Mearl — usable in both browser extension and Node.js environments",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",