Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Code: `src/ecosystem/types.ts`
}
],
"permissions": ["wallet:read", "wallet:write"],
"permissionsPolicy": ["clipboard-write", "camera"],
"splash_screen": {
"timeout": 3000
}
Expand All @@ -32,8 +33,11 @@ Code: `src/ecosystem/types.ts`

- `display`: 控制窗口模式。`standalone` (隐藏浏览器UI), `minimal-ui`, `browser`。
- `permissions`: 申请的 BioBridge 权限。
- `permissionsPolicy`: 申请的 Web Permissions Policy 指令列表(如 `clipboard-write`, `camera`)。KeyApp 会根据该字段为 miniapp iframe 注入 `allow`,用于跨域权限委派。
- `splash_screen`: 自定义启动闪屏的行为。

> 注意:跨域 miniapp 需要宿主页面的 `Permissions-Policy` 响应头允许委派,否则 `allow` 不生效。

## 样式规范 (CSS Guidelines)

Miniapp 运行在 KeyApp 的容器中(iframe 或 Wujie 沙箱),需要遵循以下样式规范以确保正确渲染。
Expand Down
26 changes: 26 additions & 0 deletions openspec/changes/add-miniapp-policy-permissions/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Design: Miniapp Permissions Policy

## Goals
- Manifest-driven Permissions Policy for cross-origin miniapps.
- Full directive coverage aligned with Web standard.
- Immediate manifest refresh for installed and running apps.

## Manifest Field
- `permissionsPolicy?: PermissionsPolicyDirective[]`
- Directive names match Permissions-Policy tokens (e.g. `clipboard-write`, `camera`).
- Validation uses a compile-time list of known directives.

## Policy Enforcement
- Top-level document must allow delegation for all directives (header `Permissions-Policy: <directive>=*`), while per-miniapp restriction is enforced via iframe/wujie `allow` attributes.
- Allow string format: `directive1; directive2; ...` (deduped, stable order).

## Runtime Sync
- Registry refresh triggers manifest refresh for installed/running apps.
- If a running app's manifest changes, update:
- stored manifest
- iframe allow attribute
- active bridge permissions (when active)

## Risk/Compatibility
- No behavior change for miniapps without `permissionsPolicy`.
- Headers are additive in dev; production requires CDN/host config.
15 changes: 15 additions & 0 deletions openspec/changes/add-miniapp-policy-permissions/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Change: Add manifest-driven Permissions Policy for miniapps

## Why
Miniapps run cross-origin inside KeyApp. Without a manifest-driven Permissions Policy layer, Web APIs like clipboard and camera fail at runtime, and updates to miniapp manifests do not propagate to running apps.

## What Changes
- Introduce a manifest field for Permissions Policy directives (full Web standard list).
- Map manifest directives to iframe/wujie `allow` attributes and update policies on source refresh.
- Ensure installed miniapps refresh their manifests immediately when sources update.
- Add runtime integration to keep allowlists in sync for running apps.
- Update docs and example miniapps to declare required directives.

## Impact
- Affected specs: miniapp-runtime (new)
- Affected code: ecosystem registry, miniapp runtime container, manifest schema/types, docs, i18n, Vite dev headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## ADDED Requirements

### Requirement: Manifest-driven Permissions Policy
The system SHALL allow miniapps to declare Permissions Policy directives in their manifest and enforce them at runtime.

#### Scenario: Miniapp declares clipboard write
- **GIVEN** a miniapp manifest includes `permissionsPolicy: ["clipboard-write"]`
- **WHEN** the miniapp is launched
- **THEN** the runtime injects `allow="clipboard-write"` into the miniapp iframe

### Requirement: Permissions Policy directive validation
The system SHALL validate declared Permissions Policy directives against the supported directive list.

#### Scenario: Invalid directive
- **GIVEN** a manifest includes an unknown directive
- **WHEN** the registry parses the manifest
- **THEN** the manifest is rejected or the invalid directive is ignored with diagnostics

### Requirement: Manifest refresh updates running apps
The system SHALL update running miniapp manifests and their iframe allowlists when source data is refreshed.

#### Scenario: Source update changes permissions
- **GIVEN** a running miniapp with `permissionsPolicy: ["camera"]`
- **WHEN** the source refresh updates the manifest to include `"clipboard-write"`
- **THEN** the runtime updates the iframe allowlist to include `clipboard-write`
24 changes: 24 additions & 0 deletions openspec/changes/add-miniapp-policy-permissions/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## 1. Design
- [ ] Confirm Permissions Policy directive list source and mapping strategy
- [ ] Define manifest field name and validation rules

## 2. Schema + Types
- [ ] Add Permissions Policy directive type + list
- [ ] Extend MiniappManifest schema/type with policy directives

## 3. Runtime Integration
- [ ] Build allowlist helper (dedupe + validation + string format)
- [ ] Inject allow attribute into iframe and wujie containers
- [ ] Update running miniapps when sources refresh (manifest + allow)

## 4. Permissions Policy Headers
- [ ] Add dev server Permissions-Policy header template
- [ ] Document production header requirements

## 5. Docs + Tests
- [ ] Update manifest docs with new field
- [ ] Add unit tests for allowlist generation + runtime updates

## 6. Miniapp Manifests
- [ ] Update bfm-rwa-hub-app manifest
- [ ] Update bfm-rwa-org-app manifest
16 changes: 15 additions & 1 deletion src/i18n/locales/ar/ecosystem.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,20 @@
"exchange": "تبادل",
"other": "أخرى"
},
"tagLabel": "#{{tag}}"
"tagLabel": "#{{tag}}",
"permissions": "أذونات التطبيق"
},
"permissionsPolicy": {
"title": "قدرات المتصفح",
"hint": "قد يطلب هذا التطبيق الوصول إلى الميزات التالية في المتصفح",
"defaultDescription": "قد يصل هذا التطبيق إلى هذه الميزة في المتصفح",
"clipboard-read": {
"name": "قراءة الحافظة",
"description": "قراءة نص من الحافظة"
},
"clipboard-write": {
"name": "الكتابة إلى الحافظة",
"description": "كتابة نص إلى الحافظة"
}
}
}
16 changes: 16 additions & 0 deletions src/i18n/locales/en-US/ecosystem.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,21 @@
"bio_signMessage": "Sign message",
"bio_signTypedData": "Sign typed data",
"bio_sendTransaction": "Send transaction"
},
"permissionsPolicy": {
"title": "Browser Capabilities",
"hint": "This app also requests access to the following browser features",
"defaultDescription": "This app may access this browser capability",
"clipboard-read": {
"name": "Read Clipboard",
"description": "Read text from your clipboard"
},
"clipboard-write": {
"name": "Write Clipboard",
"description": "Write text to your clipboard"
}
},
"detail": {
"permissions": "App Permissions"
}
}
16 changes: 15 additions & 1 deletion src/i18n/locales/en/ecosystem.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,20 @@
"exchange": "Exchange",
"other": "Other"
},
"tagLabel": "#{{tag}}"
"tagLabel": "#{{tag}}",
"permissions": "App Permissions"
},
"permissionsPolicy": {
"title": "Browser Capabilities",
"hint": "This app also requests access to the following browser features",
"defaultDescription": "This app may access this browser capability",
"clipboard-read": {
"name": "Read Clipboard",
"description": "Read text from your clipboard"
},
"clipboard-write": {
"name": "Write Clipboard",
"description": "Write text to your clipboard"
}
}
}
16 changes: 15 additions & 1 deletion src/i18n/locales/zh-CN/ecosystem.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,20 @@
"exchange": "交易所",
"other": "其他"
},
"tagLabel": "#{{tag}}"
"tagLabel": "#{{tag}}",
"permissions": "应用权限"
},
"permissionsPolicy": {
"title": "浏览器能力",
"hint": "此应用还会请求以下浏览器能力",
"defaultDescription": "此应用可能访问此浏览器能力",
"clipboard-read": {
"name": "读取剪贴板",
"description": "读取剪贴板中的文本内容"
},
"clipboard-write": {
"name": "写入剪贴板",
"description": "将文本写入剪贴板"
}
}
}
16 changes: 15 additions & 1 deletion src/i18n/locales/zh-TW/ecosystem.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,20 @@
"exchange": "交易所",
"other": "其他"
},
"tagLabel": "#{{tag}}"
"tagLabel": "#{{tag}}",
"permissions": "應用權限"
},
"permissionsPolicy": {
"title": "瀏覽器能力",
"hint": "此應用還會請求以下瀏覽器能力",
"defaultDescription": "此應用可能存取此瀏覽器能力",
"clipboard-read": {
"name": "讀取剪貼簿",
"description": "讀取剪貼簿中的文字內容"
},
"clipboard-write": {
"name": "寫入剪貼簿",
"description": "將文字寫入剪貼簿"
}
}
}
1 change: 1 addition & 0 deletions src/services/ecosystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './provider';
export * from './registry';
export * from './my-apps';
export * from './permissions';
export * from './permissions-policy';
93 changes: 93 additions & 0 deletions src/services/ecosystem/permissions-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Permissions Policy directives for miniapps.
*
* Source: MDN Permissions-Policy directive list + clipboard directives.
*/

export const PERMISSIONS_POLICY_DIRECTIVES = [
'accelerometer',
'ambient-light-sensor',
'aria-notify',
'attribution-reporting',
'autoplay',
'bluetooth',
'browsing-topics',
'camera',
'captured-surface-control',
'ch-ua-high-entropy-values',
'compute-pressure',
'cross-origin-isolated',
'deferred-fetch',
'deferred-fetch-minimal',
'display-capture',
'encrypted-media',
'fullscreen',
'gamepad',
'geolocation',
'gyroscope',
'hid',
'identity-credentials-get',
'idle-detection',
'language-detector',
'local-fonts',
'magnetometer',
'microphone',
'midi',
'on-device-speech-recognition',
'otp-credentials',
'payment',
'picture-in-picture',
'private-state-token-issuance',
'private-state-token-redemption',
'publickey-credentials-create',
'publickey-credentials-get',
'screen-wake-lock',
'serial',
'speaker-selection',
'storage-access',
'translator',
'summarizer',
'usb',
'web-share',
'window-management',
'xr-spatial-tracking',
'clipboard-read',
'clipboard-write',
] as const;

export type PermissionsPolicyDirective = (typeof PERMISSIONS_POLICY_DIRECTIVES)[number];

const PERMISSIONS_POLICY_DIRECTIVE_SET = new Set<PermissionsPolicyDirective>(PERMISSIONS_POLICY_DIRECTIVES);

export function isPermissionsPolicyDirective(value: string): value is PermissionsPolicyDirective {
return PERMISSIONS_POLICY_DIRECTIVE_SET.has(value as PermissionsPolicyDirective);
}

export function normalizePermissionsPolicy(
directives?: readonly string[] | null,
): PermissionsPolicyDirective[] {
if (!directives || directives.length === 0) return [];

const allowed = new Set<PermissionsPolicyDirective>();
for (const entry of directives) {
if (isPermissionsPolicyDirective(entry)) {
allowed.add(entry);
}
}

if (allowed.size === 0) return [];
return PERMISSIONS_POLICY_DIRECTIVES.filter((directive) => allowed.has(directive));
}

export function buildPermissionsPolicyAllow(
directives?: readonly PermissionsPolicyDirective[] | null,
): string | undefined {
if (!directives || directives.length === 0) return undefined;
return directives.join('; ');
}

export function buildPermissionsPolicyHeaderValue(
directives: readonly PermissionsPolicyDirective[] = PERMISSIONS_POLICY_DIRECTIVES,
): string {
return directives.map((directive) => `${directive}=*`).join(', ');
}
10 changes: 10 additions & 0 deletions src/services/ecosystem/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { EcosystemSource, MiniappManifest, SourceRecord } from './types';
import { EcosystemSearchResponseSchema, EcosystemSourceSchema } from './schema';
import { computeFeaturedScore } from './scoring';
import { createResolver } from '@/lib/url-resolver';
import { isPermissionsPolicyDirective, normalizePermissionsPolicy } from './permissions-policy';

const SUPPORTED_SEARCH_RESPONSE_VERSIONS = new Set(['1', '1.0.0']);

Expand Down Expand Up @@ -367,8 +368,17 @@ function normalizeAppFromSource(

const resolve = createResolver(source.url);

const permissionsPolicy = normalizePermissionsPolicy(app.permissionsPolicy ?? []);
if ((app.permissionsPolicy?.length ?? 0) > 0) {
const unknown = (app.permissionsPolicy ?? []).filter((entry) => !isPermissionsPolicyDirective(entry));
if (unknown.length > 0) {
debugLog('unknown permissions policy directives', { appId: app.id, directives: unknown });
}
}

return {
...app,
permissionsPolicy,
icon: resolve(app.icon),
url: resolve(app.url),
screenshots: app.screenshots?.map(resolve),
Expand Down
2 changes: 1 addition & 1 deletion src/services/ecosystem/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const MiniappManifestSchema = z
category: MiniappCategorySchema.optional(),
tags: z.array(z.string()).optional(),
permissions: z.array(z.string()).optional(),
permissionsPolicy: z.array(z.string()).optional(),
chains: z.array(z.string()).optional(),
screenshots: z.array(z.string()).optional(),
minWalletVersion: z.string().optional(),
Expand Down Expand Up @@ -74,4 +75,3 @@ export const EcosystemSearchResponseSchema = z
data: z.array(MiniappManifestSchema),
})
.passthrough()

4 changes: 4 additions & 0 deletions src/services/ecosystem/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* 统一从 chain-adapter 导入交易相关类型
*/

import type { PermissionsPolicyDirective } from './permissions-policy';

// ===== 从 chain-adapter 导入核心类型 =====
export type {
// 交易意图
Expand Down Expand Up @@ -158,6 +160,8 @@ export interface MiniappManifest {
tags?: string[];
/** 请求的权限列表 */
permissions?: string[];
/** Permissions Policy directives */
permissionsPolicy?: PermissionsPolicyDirective[];
/** 支持的链 */
chains?: string[];
/** 截图 URL 列表 */
Expand Down
Loading