@heybox/hb-sdk 0.7.0-alpha.5 → 0.7.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +17 -16
  3. package/dist/cli-chunks/{build-35mI_IwM.cjs → build-CsXk21Yi.cjs} +2 -2
  4. package/dist/cli-chunks/{context-DysC5qrh.cjs → context-ClUhQYbA.cjs} +1 -1
  5. package/dist/cli-chunks/{create-BJSQzUeZ.cjs → create-vadbYtVj.cjs} +1 -1
  6. package/dist/cli-chunks/{dev-DiGtxt4q.cjs → dev-Dkk4qHsv.cjs} +5 -5
  7. package/dist/cli-chunks/{doctor-DWWVwcd7.cjs → doctor-DzgWYWKU.cjs} +1 -1
  8. package/dist/cli-chunks/{index-k5wJ6TMJ.cjs → index-BMddcNGi.cjs} +1 -1
  9. package/dist/cli-chunks/{index-CgCp6jGG.cjs → index-EYRMHb10.cjs} +14 -14
  10. package/dist/cli-chunks/{login-CzkMdcXM.cjs → login-5TM5_o8q.cjs} +2 -2
  11. package/dist/cli-chunks/{project-vite-TBMlJw8y.cjs → project-vite-eJ89lO8_.cjs} +1 -1
  12. package/dist/cli-chunks/{remote-DE9DIxqw.cjs → remote-DxiHjrI9.cjs} +4 -4
  13. package/dist/cli-chunks/{session--DdaUkcq.cjs → session-DYEm6ALx.cjs} +1 -1
  14. package/dist/cli.cjs +1 -1
  15. package/dist/devtools/mock-host/main.js +43 -23
  16. package/dist/index.cjs.js +108 -68
  17. package/dist/index.esm.js +107 -68
  18. package/dist/protocol.cjs.js +19 -0
  19. package/dist/protocol.esm.js +19 -1
  20. package/dist/templates/vue3-vite-ts/src/App.vue +29 -19
  21. package/dist/templates/vue3-vite-ts/src/__tests__/App.spec.ts +82 -9
  22. package/dist/vite.cjs.js +1 -1
  23. package/dist/vite.esm.js +1 -1
  24. package/package.json +1 -1
  25. package/skill/SKILL.md +8 -7
  26. package/skill/references/api-protocol.md +6 -2
  27. package/skill/references/api-root.md +65 -19
  28. package/skill/references/examples.md +30 -9
  29. package/skill/references/recipes.md +48 -26
  30. package/skill/references/safety-boundaries.md +3 -1
  31. package/skill/references/smoke-evaluation.md +3 -1
  32. package/skill/scripts/sync-references.mjs +31 -9
  33. package/skill/skill.json +4 -4
  34. package/types/core/client.d.ts +15 -9
  35. package/types/core/handshake-state.d.ts +24 -0
  36. package/types/core/sdk.d.ts +6 -17
  37. package/types/core/singleton.d.ts +5 -7
  38. package/types/index.d.ts +5 -3
  39. package/types/protocol/trusted-user-gesture.d.ts +6 -0
  40. package/types/protocol/types.d.ts +1 -1
  41. package/types/protocol.d.ts +1 -0
@@ -30,6 +30,24 @@ function isMiniProgramBridgeMessage(value) {
30
30
  typeof message.type === 'string');
31
31
  }
32
32
 
33
+ function readBrowserEnvironment() {
34
+ try {
35
+ return {
36
+ activeElement: globalThis.document?.activeElement ?? null,
37
+ userActivation: globalThis.navigator?.userActivation,
38
+ };
39
+ }
40
+ catch {
41
+ return { activeElement: null };
42
+ }
43
+ }
44
+ /** 按当前浏览器消息、用户激活和目标 iframe 焦点生成请求级可信手势快照。 */
45
+ function computeTrustedIframeUserGesture(event, iframe, environment = readBrowserEnvironment()) {
46
+ return (event.isTrusted === true
47
+ && environment.userActivation?.isActive === true
48
+ && environment.activeElement === iframe);
49
+ }
50
+
33
51
  const MANAGED_RUNTIME_PERMISSION_KEYS = new Set(['network.request']);
34
52
  /**
35
53
  * 判断 permission key 是否由 Runtime 权限快照管理。
@@ -433,4 +451,4 @@ const MINI_PROGRAM_PROTOCOL_CAPABILITIES = [
433
451
  },
434
452
  ];
435
453
 
436
- export { AUTH_LOGIN_METHOD, CLOUD_LEADERBOARD_DELETE_CURRENT_USER_ENTRY_METHOD, CLOUD_LEADERBOARD_GET_CURRENT_USER_ENTRY_METHOD, CLOUD_LEADERBOARD_GET_INFO_METHOD, CLOUD_LEADERBOARD_GET_LIST_METHOD, CLOUD_LEADERBOARD_SUBMIT_METHOD, DEVICE_SET_CLIPBOARD_METHOD, DEVICE_VIBRATE_METHOD, MINI_PROGRAM_BRIDGE_NONCE_PARAM, MINI_PROGRAM_MESSAGE_NAMESPACE, MINI_PROGRAM_MESSAGE_VERSION, MINI_PROGRAM_NETWORK_PROTECTED_PATHS, MINI_PROGRAM_PROTOCOL_CAPABILITIES, NAVIGATION_CLOSE_METHOD, NAVIGATION_OPEN_APP_PAGE_METHOD, NAVIGATION_RELOAD_METHOD, NETWORK_REQUEST_METHOD, RUNTIME_LOCATION_PROBE_METHOD, SDK_CSP_VIOLATION_METHOD, SDK_HANDSHAKE_METHOD, SDK_LOCATION_REPORT_METHOD, SHARE_SCREENSHOT_METHOD, SHARE_SHOW_SHARE_MENU_METHOD, STORAGE_GET_STORAGE_METHOD, STORAGE_SET_STORAGE_METHOD, UI_HIDE_LOADING_METHOD, UI_SHOW_LOADING_METHOD, UI_SHOW_TOAST_METHOD, USER_GET_INFO_METHOD, USER_GET_LOCAL_IDENTITY_METHOD, USER_GET_STEAM_GAME_LIST_METHOD, USER_REVOKE_AUTHORIZATION_METHOD, VIEWPORT_GET_WINDOW_INFO_METHOD, VIEWPORT_SET_NAVIGATION_BAR_STYLE_METHOD, isManagedMiniProgramRuntimePermissionKey, isMiniProgramBridgeMessage, isOfficialMiniProgramNetworkUrl, isProtectedMiniProgramNetworkPath, normalizeMiniProgramNetworkPathname, parseMiniProgramRuntimePermissions };
454
+ export { AUTH_LOGIN_METHOD, CLOUD_LEADERBOARD_DELETE_CURRENT_USER_ENTRY_METHOD, CLOUD_LEADERBOARD_GET_CURRENT_USER_ENTRY_METHOD, CLOUD_LEADERBOARD_GET_INFO_METHOD, CLOUD_LEADERBOARD_GET_LIST_METHOD, CLOUD_LEADERBOARD_SUBMIT_METHOD, DEVICE_SET_CLIPBOARD_METHOD, DEVICE_VIBRATE_METHOD, MINI_PROGRAM_BRIDGE_NONCE_PARAM, MINI_PROGRAM_MESSAGE_NAMESPACE, MINI_PROGRAM_MESSAGE_VERSION, MINI_PROGRAM_NETWORK_PROTECTED_PATHS, MINI_PROGRAM_PROTOCOL_CAPABILITIES, NAVIGATION_CLOSE_METHOD, NAVIGATION_OPEN_APP_PAGE_METHOD, NAVIGATION_RELOAD_METHOD, NETWORK_REQUEST_METHOD, RUNTIME_LOCATION_PROBE_METHOD, SDK_CSP_VIOLATION_METHOD, SDK_HANDSHAKE_METHOD, SDK_LOCATION_REPORT_METHOD, SHARE_SCREENSHOT_METHOD, SHARE_SHOW_SHARE_MENU_METHOD, STORAGE_GET_STORAGE_METHOD, STORAGE_SET_STORAGE_METHOD, UI_HIDE_LOADING_METHOD, UI_SHOW_LOADING_METHOD, UI_SHOW_TOAST_METHOD, USER_GET_INFO_METHOD, USER_GET_LOCAL_IDENTITY_METHOD, USER_GET_STEAM_GAME_LIST_METHOD, USER_REVOKE_AUTHORIZATION_METHOD, VIEWPORT_GET_WINDOW_INFO_METHOD, VIEWPORT_SET_NAVIGATION_BAR_STYLE_METHOD, computeTrustedIframeUserGesture, isManagedMiniProgramRuntimePermissionKey, isMiniProgramBridgeMessage, isOfficialMiniProgramNetworkUrl, isProtectedMiniProgramNetworkPath, normalizeMiniProgramNetworkPathname, parseMiniProgramRuntimePermissions };
@@ -3,15 +3,11 @@
3
3
  <section class="card">
4
4
  <p class="eyebrow">Heybox Mini Program</p>
5
5
  <h1><%= projectName %></h1>
6
- <p class="intro">
7
- 这个模板只保留最小接入示例:等待 SDK ready,并由用户主动选择受支持的操作。
8
- </p>
6
+ <p class="intro">这个模板只保留最小接入示例:SDK 会自动完成初始化,由用户主动选择受支持的操作。</p>
9
7
 
10
8
  <p class="status" data-testid="status">{{ status }}</p>
11
9
 
12
- <button type="button" :disabled="loading" data-testid="login" @click="login">
13
- 获取授权码
14
- </button>
10
+ <button type="button" :disabled="loading || !sdkReady" data-testid="login" @click="login">获取授权码</button>
15
11
 
16
12
  <pre data-testid="output">{{ output }}</pre>
17
13
  </section>
@@ -20,28 +16,37 @@
20
16
 
21
17
  <script setup lang="ts">
22
18
  import hbSDK from '@heybox/hb-sdk';
23
- import { onMounted, ref } from 'vue';
19
+ import type { MiniProgramSDKHandshakeState } from '@heybox/hb-sdk';
20
+ import { onBeforeUnmount, ref } from 'vue';
24
21
  import { loginAndExchangeAuthorizationCode } from './auth-handoff';
25
22
 
26
23
  const loading = ref(false);
24
+ const sdkReady = ref(false);
27
25
  const status = ref('正在连接 SDK...');
28
- const output = ref('连接后请选择操作。');
26
+ const output = ref('请选择操作。');
29
27
 
30
- onMounted(() => {
31
- void connectSDK();
32
- });
28
+ function updateHandshakeState(state: MiniProgramSDKHandshakeState) {
29
+ if (state.status === 'connecting') {
30
+ sdkReady.value = false;
31
+ status.value = '正在连接 SDK...';
32
+ return;
33
+ }
33
34
 
34
- async function connectSDK() {
35
- try {
36
- await hbSDK.ready();
37
- status.value = 'SDK 已连接';
38
- output.value = '请选择操作。';
39
- } catch (error) {
35
+ if (state.status === 'failed') {
36
+ sdkReady.value = false;
40
37
  status.value = 'SDK 连接失败';
41
- output.value = readErrorMessage(error);
38
+ output.value = readErrorMessage(state.error);
39
+ return;
42
40
  }
41
+
42
+ sdkReady.value = true;
43
+ status.value = 'SDK 已连接';
44
+ output.value = '请选择操作。';
43
45
  }
44
46
 
47
+ const stopHandshakeState = hbSDK.onHandshakeStateChange(updateHandshakeState);
48
+ onBeforeUnmount(stopHandshakeState);
49
+
45
50
  async function login() {
46
51
  loading.value = true;
47
52
  status.value = 'auth.login 调用中...';
@@ -63,6 +68,11 @@ async function login() {
63
68
  }
64
69
 
65
70
  function readErrorMessage(error: unknown) {
66
- return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
71
+ if (!(error instanceof Error)) {
72
+ return String(error);
73
+ }
74
+
75
+ const code = 'code' in error && typeof error.code === 'string' ? `\ncode: ${error.code}` : '';
76
+ return `${error.name}: ${error.message}${code}`;
67
77
  }
68
78
  </script>
@@ -3,9 +3,37 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
3
  import App from '../App.vue';
4
4
 
5
5
  const sdkMock = vi.hoisted(() => ({
6
+ ...(() => {
7
+ type HandshakeState = { status: 'connecting' } | { status: 'ready' } | { status: 'failed'; error: Error & { code: string } };
8
+
9
+ let state: HandshakeState = { status: 'connecting' };
10
+ const handlers = new Set<(nextState: HandshakeState) => void>();
11
+ const unsubscribe = vi.fn();
12
+
13
+ return {
14
+ getHandshakeState: vi.fn(() => state),
15
+ onHandshakeStateChange: vi.fn((handler: (nextState: HandshakeState) => void) => {
16
+ handlers.add(handler);
17
+ handler(state);
18
+ return () => {
19
+ unsubscribe();
20
+ handlers.delete(handler);
21
+ };
22
+ }),
23
+ reset(nextState: HandshakeState = { status: 'connecting' }) {
24
+ state = nextState;
25
+ handlers.clear();
26
+ unsubscribe.mockReset();
27
+ },
28
+ setHandshakeState(nextState: HandshakeState) {
29
+ state = nextState;
30
+ handlers.forEach((handler) => handler(state));
31
+ },
32
+ unsubscribe,
33
+ };
34
+ })(),
6
35
  login: vi.fn(),
7
36
  networkRequest: vi.fn(),
8
- ready: vi.fn(),
9
37
  }));
10
38
 
11
39
  vi.mock('@heybox/hb-sdk', () => ({
@@ -16,34 +44,79 @@ vi.mock('@heybox/hb-sdk', () => ({
16
44
  network: {
17
45
  request: sdkMock.networkRequest,
18
46
  },
19
- ready: sdkMock.ready,
47
+ getHandshakeState: sdkMock.getHandshakeState,
48
+ onHandshakeStateChange: sdkMock.onHandshakeStateChange,
20
49
  },
21
50
  }));
22
51
 
23
52
  describe('App', () => {
24
53
  beforeEach(() => {
25
- vi.resetAllMocks();
54
+ vi.clearAllMocks();
26
55
  vi.unstubAllEnvs();
27
56
 
28
- sdkMock.ready.mockResolvedValue(undefined);
57
+ sdkMock.reset();
29
58
  sdkMock.login.mockResolvedValue({ code: 'authorization_code', expiresIn: 300, scopes: ['identity'] });
30
59
  sdkMock.networkRequest.mockResolvedValue({ status: 200 });
31
60
  });
32
61
 
33
- it('only waits for SDK readiness during startup', async () => {
62
+ it('keeps actions disabled while the SDK is connecting', () => {
63
+ const wrapper = mount(App);
64
+
65
+ expect(sdkMock.getHandshakeState).not.toHaveBeenCalled();
66
+ expect(wrapper.get('[data-testid="status"]').text()).toBe('正在连接 SDK...');
67
+ expect(wrapper.get<HTMLButtonElement>('[data-testid="login"]').element.disabled).toBe(true);
68
+ });
69
+
70
+ it('enables actions when the handshake becomes ready', async () => {
34
71
  const wrapper = mount(App);
72
+
73
+ sdkMock.setHandshakeState({ status: 'ready' });
35
74
  await flushPromises();
36
75
 
37
- expect(sdkMock.ready).toHaveBeenCalledTimes(1);
38
- expect(sdkMock.login).not.toHaveBeenCalled();
39
76
  expect(wrapper.get('[data-testid="status"]').text()).toBe('SDK 已连接');
40
77
  expect(wrapper.get('[data-testid="output"]').text()).toContain('请选择操作');
78
+ expect(wrapper.get<HTMLButtonElement>('[data-testid="login"]').element.disabled).toBe(false);
79
+ });
80
+
81
+ describe('late mount', () => {
82
+ it('renders an already-ready handshake immediately', () => {
83
+ sdkMock.reset({ status: 'ready' });
84
+
85
+ const wrapper = mount(App);
86
+
87
+ expect(wrapper.get('[data-testid="status"]').text()).toBe('SDK 已连接');
88
+ expect(wrapper.get<HTMLButtonElement>('[data-testid="login"]').element.disabled).toBe(false);
89
+ });
90
+
91
+ it('renders the original failed handshake details immediately', () => {
92
+ const error = Object.assign(new Error('Host 未响应'), {
93
+ code: 'HANDSHAKE_TIMEOUT',
94
+ name: 'HbMiniProgramSDKError',
95
+ });
96
+ sdkMock.reset({ status: 'failed', error });
97
+
98
+ const wrapper = mount(App);
99
+
100
+ expect(wrapper.get('[data-testid="status"]').text()).toBe('SDK 连接失败');
101
+ expect(wrapper.get<HTMLButtonElement>('[data-testid="login"]').element.disabled).toBe(true);
102
+ expect(wrapper.get('[data-testid="output"]').text()).toContain('HbMiniProgramSDKError');
103
+ expect(wrapper.get('[data-testid="output"]').text()).toContain('Host 未响应');
104
+ expect(wrapper.get('[data-testid="output"]').text()).toContain('HANDSHAKE_TIMEOUT');
105
+ });
106
+ });
107
+
108
+ it('stops observing handshake state after unmount', () => {
109
+ const wrapper = mount(App);
110
+
111
+ wrapper.unmount();
112
+
113
+ expect(sdkMock.unsubscribe).toHaveBeenCalledTimes(1);
41
114
  });
42
115
 
43
116
  it('hands the authorization code to the developer server without rendering it', async () => {
44
117
  vi.stubEnv('VITE_HEYBOX_AUTH_EXCHANGE_URL', 'https://developer.example.com/api/heybox/exchange');
118
+ sdkMock.reset({ status: 'ready' });
45
119
  const wrapper = mount(App);
46
- await flushPromises();
47
120
 
48
121
  await wrapper.get('[data-testid="login"]').trigger('click');
49
122
  await flushPromises();
@@ -61,8 +134,8 @@ describe('App', () => {
61
134
  });
62
135
 
63
136
  it('does not request a code when the developer server is not configured', async () => {
137
+ sdkMock.reset({ status: 'ready' });
64
138
  const wrapper = mount(App);
65
- await flushPromises();
66
139
 
67
140
  await wrapper.get('[data-testid="login"]').trigger('click');
68
141
  await flushPromises();
package/dist/vite.cjs.js CHANGED
@@ -8,7 +8,7 @@ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentS
8
8
  /** 构建时替换为当前发布包的实际版本。 */
9
9
  const HB_SDK_VERSION = typeof undefined === 'string'
10
10
  ? undefined
11
- : '0.7.0-alpha.5';
11
+ : '0.7.0';
12
12
 
13
13
  var re = {exports: {}};
14
14
 
package/dist/vite.esm.js CHANGED
@@ -5,7 +5,7 @@ import { AsyncLocalStorage } from 'node:async_hooks';
5
5
  /** 构建时替换为当前发布包的实际版本。 */
6
6
  const HB_SDK_VERSION = typeof undefined === 'string'
7
7
  ? undefined
8
- : '0.7.0-alpha.5';
8
+ : '0.7.0';
9
9
 
10
10
  var re = {exports: {}};
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heybox/hb-sdk",
3
- "version": "0.7.0-alpha.5",
3
+ "version": "0.7.0",
4
4
  "sideEffects": [
5
5
  "./src/index.ts",
6
6
  "./src/core/singleton.ts",
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: hb-sdk
3
- description: Uses @heybox/hb-sdk and its hb-sdk CLI to build, debug, review, and publish workshop mini-programs for the Heybox App. Use for public SDK APIs, lifecycle events, hb-sdk create/dev/login/doctor, browser Mock and device debugging, or publishing checks. Don't use for private credentials, internal Heybox client protocols, private package paths, or non-Heybox SDKs.
3
+ description: Uses @heybox/hb-sdk and its hb-sdk CLI to build, debug, review, and publish workshop mini-programs for the Heybox App. Use for public SDK APIs, persistent handshake state, lifecycle events, hb-sdk create/dev/login/doctor, browser Mock and device debugging, or publishing checks. Don't use for private credentials, internal Heybox client protocols, private package paths, or non-Heybox SDKs.
4
4
  ---
5
5
 
6
6
  # hb-sdk Agent Procedure
@@ -38,17 +38,18 @@ Apply these instructions when writing, reviewing, or debugging code that consume
38
38
 
39
39
  ## Step 4: Implement workshop mini-program code
40
40
 
41
- 1. Call `await ready()` or `await hbSDK.ready()` near page startup when the page uses SDK capabilities.
41
+ 1. Import the root package eagerly; the SDK starts its handshake automatically and capability calls wait for it internally.
42
42
  2. For a network-enabled mini-program, use `auth.login({ scopes? })` to obtain `{ code, expiresIn: 300, scopes }`. Identity is always implicit; request only optional scopes the business actually needs.
43
43
  3. Send the code only to the developer's backend. That backend follows the backend OpenAPI documentation to exchange it for a token; mini-program code must never exchange application credentials itself.
44
44
  4. For a network-disabled mini-program, call `user.getLocalIdentity()` from a trusted user action to obtain `{ appUserId }`, because first login or identity consent may require UI. It cannot obtain a code or call OpenAPI.
45
45
  5. Handle `HbMiniProgramSDKError` for SDK initialization and capability failures.
46
46
  6. Handle `HbMiniProgramNetworkError` separately when HTTP completed but `validateStatus` rejected the status.
47
- 7. Cancel lifecycle/event subscriptions returned by `on()` when the page or component unmounts.
48
- 8. Use the default SDK instance exposed by the root package.
49
- 9. When authorization UI is required, call `auth.login()` or `user.getLocalIdentity()` from a trusted user action. Missing gesture returns `USER_GESTURE_REQUIRED`; cancelling, rejecting, or closing returns `AUTHORIZATION_CANCELLED` and does not confirm or change authorization. An already-authorized `auth.login()` request may return a new code silently.
50
- 10. `user.getSteamGameList()` is available only to network-disabled mini-programs. In a network-enabled mini-program it returns `SERVER_API_REQUIRED`, and there is no Steam library authorization scope or OpenAPI resource. Treat `user.getInfo()` and `user.getLocalIdentity()` returning `SERVER_API_REQUIRED` as a server-boundary signal, not as logged-out state.
51
- 11. Use `share.showShareMenu({ post })` or `share.screenshot({ post })` to preset editable community destinations and topics. Pass partition IDs through `topicIds` and topic text without surrounding `#` through `topics`; do not construct the underlying client post protocol.
47
+ 7. Use `getHandshakeState()` for the current persistent handshake state and `onHandshakeStateChange()` for an immediate replay plus future changes. Cancel the returned subscription when the page or component unmounts.
48
+ 8. Treat the non-replayed `ready` lifecycle event only as an edge notification. After a duplicate handshake it may repeat, so never use it as state or as the source for enabling a late-mounted control.
49
+ 9. Use the default SDK instance exposed by the root package. For gesture-gated controls, initialize from the current handshake state, subscribe to changes, and enable the control only when `status === 'ready'`. Keep calls such as `auth.login()` in the original user-action callback; do not wait for handshake inside that callback.
50
+ 10. When authorization UI is required, call `auth.login()` or `user.getLocalIdentity()` from a trusted user action. Missing gesture returns `USER_GESTURE_REQUIRED`; cancelling, rejecting, or closing returns `AUTHORIZATION_CANCELLED` and does not confirm or change authorization. An already-authorized `auth.login()` request may return a new code silently.
51
+ 11. `user.getSteamGameList()` is available only to network-disabled mini-programs. In a network-enabled mini-program it returns `SERVER_API_REQUIRED`, and there is no Steam library authorization scope or OpenAPI resource. Treat `user.getInfo()` and `user.getLocalIdentity()` returning `SERVER_API_REQUIRED` as a server-boundary signal, not as logged-out state.
52
+ 12. Use `share.showShareMenu({ post })` or `share.screenshot({ post })` to preset editable community destinations and topics. Pass partition IDs through `topicIds` and topic text without surrounding `#` through `topics`; do not construct the underlying client post protocol.
52
53
 
53
54
  ## Step 5: Use CLI workflows
54
55
 
@@ -26,6 +26,10 @@ export {
26
26
  SDK_HANDSHAKE_METHOD,
27
27
  } from './protocol/constants';
28
28
  export { isMiniProgramBridgeMessage } from './protocol/guards';
29
+ export {
30
+ computeTrustedIframeUserGesture,
31
+ type TrustedIframeUserGestureEnvironment,
32
+ } from './protocol/trusted-user-gesture';
29
33
  export { isManagedMiniProgramRuntimePermissionKey, parseMiniProgramRuntimePermissions } from './protocol/runtime-permissions';
30
34
  export {
31
35
  isOfficialMiniProgramNetworkUrl,
@@ -207,7 +211,7 @@ Reference 由 `@heybox/hb-sdk` 的公开导出与源码注释自动生成,不
207
211
 
208
212
  | 导出面 | Classes | Functions | Interfaces | Types | Constants |
209
213
  | --- | ---: | ---: | ---: | ---: | ---: |
210
- | Root API | 2 | 3 | 51 | 52 | 0 |
211
- | Protocol API | 0 | 6 | 46 | 65 | 31 |
214
+ | Root API | 2 | 4 | 51 | 54 | 0 |
215
+ | Protocol API | 0 | 7 | 47 | 65 | 31 |
212
216
  | Miniapp Publish API | 0 | 5 | 2 | 0 | 0 |
213
217
  | Vite API | 0 | 1 | 0 | 0 | 0 |
@@ -24,7 +24,7 @@
24
24
  ## Package metadata
25
25
 
26
26
  - Package: `@heybox/hb-sdk`
27
- - Version at generation time: `0.7.0-alpha.2`
27
+ - Version at generation time: `0.7.0`
28
28
  - Public root export: `@heybox/hb-sdk`
29
29
  - Protocol export: `@heybox/hb-sdk/protocol`
30
30
  - Vite plugin export: `@heybox/hb-sdk/vite`
@@ -33,7 +33,23 @@
33
33
 
34
34
  ```ts
35
35
  export { HbMiniProgramSDKError, HbMiniProgramNetworkError } from './core/errors';
36
- export { ready, on, off, auth, user, share, viewport, storage, network, ui, device, navigation, cloud } from './core/singleton';
36
+ export {
37
+ getHandshakeState,
38
+ onHandshakeStateChange,
39
+ on,
40
+ off,
41
+ auth,
42
+ user,
43
+ share,
44
+ viewport,
45
+ storage,
46
+ network,
47
+ ui,
48
+ device,
49
+ navigation,
50
+ cloud,
51
+ } from './core/singleton';
52
+ export type { MiniProgramSDKHandshakeState, MiniProgramSDKHandshakeStateHandler } from './core/handshake-state';
37
53
  export type { MiniProgramEventHandler, MiniProgramEventName, MiniProgramEventPayloadMap } from './protocol/types';
38
54
  export type { LoginOptions, LoginPayload, LoginResult, LoginScope, MiniProgramAuthModule } from './modules/auth';
39
55
  export type {
@@ -145,10 +161,26 @@ export type {
145
161
  ReloadResult,
146
162
  } from './modules/navigation';
147
163
 
148
- import { auth, cloud, device, navigation, network, off, on, ready, share, storage, ui, viewport, user } from './core/singleton';
164
+ import {
165
+ auth,
166
+ cloud,
167
+ device,
168
+ getHandshakeState,
169
+ navigation,
170
+ network,
171
+ off,
172
+ on,
173
+ onHandshakeStateChange,
174
+ share,
175
+ storage,
176
+ ui,
177
+ viewport,
178
+ user,
179
+ } from './core/singleton';
149
180
 
150
181
  const hbSDK = {
151
- ready,
182
+ getHandshakeState,
183
+ onHandshakeStateChange,
152
184
  on,
153
185
  off,
154
186
  auth,
@@ -360,7 +392,26 @@ hb-sdk build [--env <name>] [--verbose]
360
392
 
361
393
  # 快速开始
362
394
 
363
- 如果页面不需要小黑盒开放能力,可以不接入 SDK。需要开放能力时,先等待 `ready()`,再根据小程序是否开通网络权限选择身份流程。
395
+ 如果页面不需要小黑盒开放能力,可以不接入 SDK。SDK 导入时会自动启动握手,能力调用会自动等待;业务只需根据小程序是否开通网络权限选择身份流程。
396
+
397
+ ## 交互控件与握手状态
398
+
399
+ `auth.login()`、`user.getLocalIdentity()` 等可能展示授权 UI 的能力必须保留原始可信用户手势。不要在点击回调中等待握手;页面加载时读取并订阅持久握手状态,只在 `status === 'ready'` 时启用按钮:
400
+
401
+ ```ts
402
+ import hbSDK, { type MiniProgramSDKHandshakeState } from '@heybox/hb-sdk'
403
+ import { computed, onUnmounted, ref } from 'vue'
404
+
405
+ const handshakeState = ref<MiniProgramSDKHandshakeState>(hbSDK.getHandshakeState())
406
+ const stopHandshakeState = hbSDK.onHandshakeStateChange((state) => {
407
+ handshakeState.value = state
408
+ })
409
+ const sdkReady = computed(() => handshakeState.value.status === 'ready')
410
+
411
+ onUnmounted(stopHandshakeState)
412
+ ```
413
+
414
+ 把 `sdkReady` 用作按钮的 `disabled` 条件。订阅会立即回放当前状态,因此晚挂载组件也能得到 `ready` 或 `failed` 终态。`ready` 生命周期事件只在握手成功瞬间派发且不会重放,不能作为当前状态来源。
364
415
 
365
416
  ## 未开通网络权限
366
417
 
@@ -370,8 +421,6 @@ hb-sdk build [--env <name>] [--verbose]
370
421
  import hbSDK, { HbMiniProgramSDKError } from '@heybox/hb-sdk'
371
422
 
372
423
  async function getLocalIdentityFromUserAction() {
373
- await hbSDK.ready()
374
-
375
424
  try {
376
425
  const { appUserId } = await hbSDK.user.getLocalIdentity()
377
426
  return appUserId
@@ -390,11 +439,11 @@ async function getLocalIdentityFromUserAction() {
390
439
 
391
440
  有网络小程序通过 `auth.login()` 获取短期授权码。登录可能展示 UI,因此应绑定到按钮点击等明确的用户操作:
392
441
 
442
+ SDK 会自动完成握手。登录按钮应按上面的持久握手状态启用;点击回调直接调用 `auth.login()`,不要先等待其他异步任务,以免首次操作丢失可信手势。
443
+
393
444
  ```ts
394
445
  import hbSDK, { HbMiniProgramSDKError } from '@heybox/hb-sdk'
395
446
 
396
- await hbSDK.ready()
397
-
398
447
  async function loginFromUserAction() {
399
448
  try {
400
449
  const result = await hbSDK.auth.login({ scopes: ['profile'] })
@@ -413,11 +462,7 @@ async function loginFromUserAction() {
413
462
  }
414
463
  ```
415
464
 
416
- 返回值与边界以[用户身份与登录](/guide/auth)为准。页面只把 `code` 提交给自己的服务端。不要在页面初始化阶段自动调用 `auth.login()`。
417
-
418
- ## ready 的含义
419
-
420
- `ready()` 只表示 SDK 已就绪,可以安全调用开放能力。它不表示已经取得用户授权,也不返回用户身份。
465
+ 返回值与边界以[用户身份与登录](https://docs.xiaoheihe.cn/hb_sdk/guide/auth)为准。页面只把 `code` 提交给自己的服务端。不要在页面初始化阶段自动调用 `auth.login()`。
421
466
 
422
467
  ## 默认单例
423
468
 
@@ -475,7 +520,6 @@ type LoginViewState =
475
520
 
476
521
  async function loginFromUserAction(): Promise<LoginViewState> {
477
522
  try {
478
- await hbSDK.ready()
479
523
  const result = await hbSDK.auth.login()
480
524
  await exchangeCodeWithDeveloperServer(result.code)
481
525
  return { status: 'ready' }
@@ -511,7 +555,7 @@ function reportSDKError(errorCode: string, message: string, data?: unknown) {
511
555
 
512
556
  ## 能力概览
513
557
 
514
- `ready()` 外,SDK 还提供 `on()` `off()` 处理前后台、登录态等生命周期事件。
558
+ SDK 还提供 `getHandshakeState()` / `onHandshakeStateChange()` 管理持久握手状态,并通过 `on()` / `off()` 处理前后台、登录态等生命周期事件。
515
559
 
516
560
  | 模块 | 用途 |
517
561
  | ------------ | ------------------------------------------------------------ |
@@ -561,7 +605,7 @@ onUnmounted(stopLifecycleEvents)
561
605
  | 事件 | 触发时机 | 典型用途 |
562
606
  | ------------------------- | ------------------------ | -------------------------------- |
563
607
  | `launch` | 小程序首次启动 | 初始化一次性数据 |
564
- | `ready` | SDK 可安全调用开放能力 | 标记 SDK 可用 |
608
+ | `ready` | SDK 握手成功瞬间 | 不可重放的边沿通知 |
565
609
  | `show` | 小程序页面展示 | 刷新可见态数据 |
566
610
  | `hide` | 小程序页面隐藏 | 暂停轮询、暂停播放 |
567
611
  | `unload` | 当前小程序运行环境终止 | 清理资源并停止请求 |
@@ -575,11 +619,13 @@ onUnmounted(stopLifecycleEvents)
575
619
 
576
620
  ## 生命周期建议
577
621
 
578
- - 初始化开放能力前先 `await ready()`。
622
+ - SDK 导入时自动启动握手,能力调用会自动等待;不要在用户手势回调中额外等待握手。
623
+ - 当前握手状态只通过 `getHandshakeState()` 查询、通过 `onHandshakeStateChange()` 订阅;订阅会立即回放当前值并返回取消函数。
624
+ - `ready` 事件只在握手成功瞬间派发,不会向晚订阅者重放,不能用于驱动按钮可用状态或替代握手状态 API。
579
625
  - UI 可见性相关逻辑放在 `show`、`hide`。
580
626
  - 使用 Host current-user APIs 的无网络小程序,可以在 `heybox_app_login_change` 后刷新本地状态。已开通网络权限的小程序使用 `auth.login()` 获取 code,并由开发者服务端维护业务会话。
581
627
  - 组件或页面销毁时清理 `on` 注册的监听,避免重复响应。
582
- - 事件只派发给注册当时存在的监听器,不会重放;一次性 `launch`/`ready` 状态应以 `ready()` Promise 为准。
628
+ - 生命周期事件只派发给注册当时存在的监听器,不会重放;一次性 `launch` 信息应在页面启动阶段通过业务能力调用处理。
583
629
  - 收到 `unload` 后,当前 SDK 上下文不可恢复,未完成请求会失败;不要在同一页面上下文继续重试能力调用。
584
630
 
585
631
  ## 云端排行榜
@@ -9,14 +9,37 @@
9
9
  - apps/docs/hb-sdk/recipes/**
10
10
  ## Positive examples
11
11
 
12
+ ### Gesture-gated control handshake state
13
+
14
+ ```ts
15
+ import hbSDK, { type MiniProgramSDKHandshakeState } from '@heybox/hb-sdk';
16
+
17
+ let handshakeState: MiniProgramSDKHandshakeState = hbSDK.getHandshakeState();
18
+ const stopHandshakeState = hbSDK.onHandshakeStateChange((state) => {
19
+ handshakeState = state;
20
+ setLoginButtonDisabled(state.status !== 'ready');
21
+ });
22
+
23
+ export function disposePage() {
24
+ stopHandshakeState();
25
+ }
26
+
27
+ export async function loginFromUserAction() {
28
+ if (handshakeState.status !== 'ready') return;
29
+ return hbSDK.auth.login();
30
+ }
31
+
32
+ function setLoginButtonDisabled(disabled: boolean) {
33
+ document.querySelector('button')?.toggleAttribute('disabled', disabled);
34
+ }
35
+ ```
36
+
12
37
  ### Local identity for a network-disabled mini-program
13
38
 
14
39
  ```ts
15
- import { ready, user, HbMiniProgramSDKError } from '@heybox/hb-sdk';
40
+ import { user, HbMiniProgramSDKError } from '@heybox/hb-sdk';
16
41
 
17
42
  export async function getLocalIdentityFromUserAction() {
18
- await ready();
19
-
20
43
  try {
21
44
  return await user.getLocalIdentity();
22
45
  } catch (error) {
@@ -31,11 +54,9 @@ export async function getLocalIdentityFromUserAction() {
31
54
  ### Developer server session for a network-enabled mini-program
32
55
 
33
56
  ```ts
34
- import { auth, network, ready, HbMiniProgramSDKError } from '@heybox/hb-sdk';
57
+ import { auth, network, HbMiniProgramSDKError } from '@heybox/hb-sdk';
35
58
 
36
59
  export async function createServerSessionFromUserAction() {
37
- await ready();
38
-
39
60
  try {
40
61
  const result = await auth.login({ scopes: ['profile'] });
41
62
  await exchangeCodeWithDeveloperServer(result.code);
@@ -60,11 +81,9 @@ async function exchangeCodeWithDeveloperServer(code: string) {
60
81
  ### Network request
61
82
 
62
83
  ```ts
63
- import { ready, network, HbMiniProgramNetworkError, HbMiniProgramSDKError } from '@heybox/hb-sdk';
84
+ import { network, HbMiniProgramNetworkError, HbMiniProgramSDKError } from '@heybox/hb-sdk';
64
85
 
65
86
  try {
66
- await ready();
67
-
68
87
  const response = await network.request<{ ok: boolean }>({
69
88
  url: 'https://api.example.com/demo',
70
89
  method: 'GET',
@@ -138,6 +157,8 @@ export default defineConfig({
138
157
  ## Negative examples
139
158
 
140
159
  - Do not import from internal hb-sdk implementation paths.
160
+ - The non-replayed `ready` lifecycle event is only an edge notification. After a duplicate handshake it may repeat; never treat it as current state, and query or subscribe to the persistent state APIs instead.
161
+ - Do not wait for handshake inside a gesture-gated click handler; keep the control disabled until the persistent state is `ready`.
141
162
  - Do not read cookies, tokens, phone numbers, or private credentials.
142
163
  - Do not create raw `postMessage` bridge envelopes in business pages.
143
164
  - Do not call unsupported storage delete/clear/info operations.