Repository navigation
feat(bridge): stable installId and pushTokenChanged for device registration; document the no-Service-Worker rule - #167
Merged
Conversation
… (WIP) Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv Co-authored-by: Claude <noreply@anthropic.com>
…data candidate Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv Co-authored-by: Claude <noreply@anthropic.com>
The Console installs its shell-event listeners in the commit that posts ready, after the document's load end, so a token reported earlier was dropped. Keep the latest token and deliver it at ready; a new document start re-arms the hold. Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv Co-authored-by: Claude <noreply@anthropic.com>
…, no Service Worker in the shell Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv Co-authored-by: Claude <noreply@anthropic.com>
…s origin The bootstrap runs in every main-frame document, a trusted sign-in host's pages included. Host data only the Console should see is gated to the server origin (getPushToken, pushTokenChanged); installId now follows the same rule and reads null elsewhere. Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv Co-authored-by: Claude <noreply@anthropic.com>
Contributor
Author
|
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv Seat verdict: ACCEPT — Head-sha: 维护者速读
席位意见: 两处判断是席位定的,请你知道。
四条消融实测都能让对应用例转红(第四个用例候选、来源检查、token 暂存、bootstrap 门控),用例确实钉住了行为。建议合并。 席位核对
|
os-justin
pushed a commit
that referenced
this pull request
Nov 2, 2026
Reconciles #167 (installId, pushTokenChanged) with the load state machine: one source of readiness. The held push token moves into lib/shell/console-load.ts as state plus a sendPushToken effect, sent on entering `ready` (the event or the ready timeout) and re-armed by the shell's reload() and a re-created WebView. #167's consoleReadyRef, heldPushTokenRef and onLoadStart re-arm are dropped: on Android react-native-webview 13.16 fires onLoadStart from doUpdateVisitedHistory, i.e. for SPA history changes too. hostInfo and bootstrap stay exactly as #167 wrote them. Docs stack both sides. Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv Co-authored-by: Claude <noreply@anthropic.com>
os-justin
pushed a commit
that referenced
this pull request
Nov 2, 2026
Reconciles #170 (bridge scan(), per-WebView answer guard) with the load state machine. #170's documentRef and the onMessage answer guard stay; the counter is bumped in the machine's `recreate` effect, the one place a new WebView is created, and nowhere tied to onLoadStart (still unwired, as in the #167 reconcile). Two #170 tests drove a load start through the `onLoadStart` prop; they now deliver it the way react-native-webview's WebViewShared does (onLoadStart if wired, then onNavigationStateChange). Docs stack #170's Scanning codes and wire-format text beside Load states. Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #163
Clause-②: no
Metadata-protocol: none
维护者速读
installId(首次启动随机生成、存 SecureStore 的安装 ID,只给配置服务器源的页面,其他页面读到 null)和pushTokenChanged事件(APNs/FCM 换 token 时通知 Console);iOS 推送点击多读一个兜底位置userInfo.data;文档写明壳内不注册 Service Worker,并把ObjectStackShell/UA 标记定为契约。deviceId存在 WebView 存储里,存储一清就会注册出第二条设备记录;token 轮换时 Console 也无从得知。补上这两样后,Console 可以做到每个安装只占一条设备记录。installId已按服务器源门控,EXPO_PUBLIC_TRUSTED_HOSTS里的登录页只读到 null,子框架读不到。iOS 重装后 installId 可能保留。回滚直接 revert 本 PR 即可,没有数据迁移。components/shell/ConsoleWebView.tsx(token 在ready前暂存、ready时投递,onLoadStart重新开始暂存,与 feat(shell): act on the Console's ready and routeChanged signals; the shell owns the safe area #166 有交叠);lib/install-id.ts(同步读写 SecureStore,失败时返回 null,绝不发出没存住的 ID);lib/shell/bridge/scripts.ts的buildBootstrapScript与buildPushTokenChangedScript(installId与 token 都只给服务器源)。npx jest --ci __tests__/components/ConsoleWebView.test.tsx __tests__/lib/install-id.test.ts,应看到 2 个测试套件、全部用例通过。objectstack-native:pushTokenChanged;② 清掉 WebView 存储(Android 系统设置里清除 WebView 数据/缓存,或在 Console 里清站点数据)后,window.ObjectStackNative.installId保持不变。What changes
installIdon host info (lib/install-id.ts,BridgeHostInfo.installId, bootstrapwindow.ObjectStackNative.installId):uuid.v4()(UUID()on iOS,UUID.randomUUID()on Android). No new dependency. It is not a hardware id or an advertising id.objectstack_install_idwithAFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY. That makes it readable after the first unlock (a notification can start the app in the background) and keeps it out of backups restored onto another device.SecureStore.getItem/setItem), because the bootstrap is built synchronously when the WebView mounts. Doing this inConsoleWebViewavoids a loading gate in the region Shell: act on the Console'sreadyandrouteChangedsignals, and settle who pads the safe area #162 owns.nulland mints nothing. An id that could not be stored is never handed out. Either way, the next call tries again.pushTokenChanged(shell → Console event, announced as a capability):protocol.ts:SHELL_EVENTS = ["pushTokenChanged"].BRIDGE_CAPABILITIES= methods plus shell events, andPUSH_TOKEN_CHANGED_EVENT = "objectstack-native:pushTokenChanged".lib/push-notifications.ts:addPushTokenChangeListenerwrapsNotifications.addPushTokenListener, maps the token to{ token, vendor }, and never throws. Expo Go on Android throws there.hooks/usePushRouting.ts:usePushTokenChangesowns the listener lifecycle and subscribes once.scripts.ts:buildPushTokenChangedScriptdispatches only whenwindow.location.originis the configured server's origin. Those are the same pagesgetPushToken()answers, so a trusted IdP page in the main frame never receives the token.ConsoleWebView.tsxdelivers the event. A token reported before the Console'sready()is held (latest only) and delivered atready().onLoadStart(a new document) re-arms the hold. See the overlap note below.routeFromResponsereadsuserInfo.dataas a fourth candidate, aftercontent.data,userInfoanduserInfo.body.HYBRID-SHELL.mdcovers the install id, the shell event and how to add one, the UA marker contract, "Service Workers: none inside the shell", the Console-side integration list, and a Known-limits entry on where host info reaches.PUSH-NOTIFICATIONS.mdcoversdeviceId = installId, rotation, and the fourth candidate..changeset/163-install-id-token-rotation.md(patch).Decision: the
ObjectStackShell/user-agent marker becomes a documented contractYes. Reasons:
55e3e5e,ui/apps/console/src/main.tsx:218-233) readsgetNativeHost()after boot. Its own comment concedes that a bridge landing later "leaves that page load's worker in place". Android can inject the bridge after the page's first scripts (HYBRID-SHELL Known limits), and that window is exactly the masked-offline failure.__tests__/components/ConsoleWebView.test.tsxalready pins^ObjectStackShell\/.The doc says what it is not: it is not a capability signal (use
has()), and it is not a security signal.Consumers re-measured on objectstack
mainat5901871c(shallow clone of the current tip):git grep -n "ObjectStackShell"gives 0 hits, exit 1. Control leg on the same tree:git grep -c NATIVE_NAVIGATE_EVENT -- ui/packages/mobile/src/nativeHost.tsgives 1, exit 0. The #29326 head (55e3e5e) also has 0 hits. So nothing reads the marker today, and documenting it adds an option without breaking anyone.Exposure reading: where
installIdis readableMain frame only, and only on the configured server's origin. Sub-frames cannot read it. A trusted host's main-frame page reads
null.ConsoleWebView.tsx:350,354passes the bootstrap without the...ForMainFrameOnlyprops, so the defaults apply. react-native-webview 13.16:src/WebView.ios.tsx:61-62defaults both totrue.apple/RNCWebViewImpl.m:1714,1754installs the scripts asWKUserScript ... forMainFrameOnly:with that value.RNCWebView.java:309-316runsevaluateJavascriptfromRNCWebViewClient.java:85-90(onPageStarted), which is main frame only.src/WebViewTypes.ts:1283-1289says "mandatory for Android".handler.ts). A cross-origin sub-frame cannot read the main frame'swindow(same-origin policy).window.ReactNativeWebViewto every frame:RNCWebView.java:253-269, withSet.of("*")oraddJavascriptInterface. But that object only posts. Answers are injected into the main frame.navigation-policy.ts:68loadsEXPO_PUBLIC_TRUSTED_HOSTSin the top frame, so a trusted IdP sign-in page runs it too.buildBootstrapScript(info, serverUrl)(scripts.ts:39-48) exposesinstallIdonly whenwindow.location.originequals the configured server's origin, with the default port dropped. Every other page readsnull, and it stays synchronous. This is the same rulegetPushToken(handler.ts) andpushTokenChanged(scripts.ts:166) already follow.platform,appVersion,capabilities,safeArea), as before this PR.Overlap with #162 / #166 (
claude/issue-162-ready-route-safe-area)Measured: delivering on
loadedReffired before the Console's listeners existed.main7cb17d1,loadedRefflips atonLoadEnd(ConsoleWebView.tsx:146-151,320).ready(objectstack5901871cui/packages/mobile/src/useNativeHost.ts:115-133).consoleReadyRefplusheldPushTokenRef:readythrough the existingonEventhandler, as one added line besidemarkLoaded();onLoadStartprop.RNCWebViewImpl.m:778-783), never a start, so an in-app route change does not re-arm the hold.lib/shell/console-load.ts, and this branch is based only onmain. feat(shell): act on the Console's ready and routeChanged signals; the shell owns the safe area #166 is accepted and awaits the maintainer. Whichever PR lands second mergesmainand reconcilesConsoleWebView.tsx. If feat(shell): act on the Console's ready and routeChanged signals; the shell owns the safe area #166 is onmainby then, the hold/flush routes throughconsole-load.ts's ready phase andconsoleReadyRef/onDocumentStartare dropped.ConsoleWebView.tsx: thehostInfomemo (installId,capabilities: BRIDGE_CAPABILITIES) and thebootstrapmemo (now passesserverUrl). Nothing in the cover, held-route or last-route logic.Tests
Head
526102f, run in the worktree:npx tsc --noEmitnpx eslint . --ext .ts,.tsxnpx jest --ci --coverageTest Suites: 27 passed, 27 total·Tests: 324 passed, 324 total; thresholds met (install-id.ts95.45 % statements,scripts.ts/protocol.ts100 %)New or extended coverage:
install-id.test.ts: stable across calls and launches, regenerated when missing, an invalid stored value replaced,nullwithout minting on read failure, never handing out an unstored id,nullwithout a UUID generator.push-notifications.test.ts:userInfo.datafallback;content.data, top-level andbodywins over it);usePushRouting.test.ts: subscribes once, uses the latest callback, unsubscribes on unmount.bridge-scripts.test.ts:installIdexposed read-only, ornull;bridge-protocol.test.ts: capabilities are methods plus events; the event type name.ConsoleWebView.test.tsx:installIdandhas("pushTokenChanged");ready(cold start, after load end) and delivered once, latest only;onLoadStart;Ablation: four legs, each committed first, mutated with an anchor count, and restored with
git checkout HEAD --plus a hash compare.Ablation readings
lib/push-notifications.ts). Anchor count 1 → 0.push-notifications.test.tsexit 1:falls back to userInfo.data.routeandskips an invalid earlier candidate and still reads userInfo.datafail (2 failed, 30 passed). Restored byte-identical.lib/shell/bridge/scripts.ts). Anchor count 1 → 0. Exit 1:pushTokenChanged script › delivers nothing to a page on another originandConsoleWebView › pushTokenChanged › delivers a rotated token to a ready pagefail (2 failed, 43 passed). Restored byte-identical.components/shell/ConsoleWebView.tsx,else heldPushTokenRef.current = token;). Anchor count 1 → 0. Exit 1: the three hold tests fail (3 failed, 21 passed). Restored byte-identical.526102f), remove the bootstrap's origin gate (scripts.ts, theonServer ?arm). Anchor count 1 → 0. Exit 1:bootstrap script › exposes a null install id to a page on another origin (a trusted sign-in host)fails (1 failed, 23 passed). Restored byte-identical.Deviations
expo-secure-storeSecureStoreModule.kt:159-168removes it onBadPaddingExceptionand a fresh id is minted.docs/HYBRID-SHELL.md(Install id) states this.Acceptance notes
getPushToken()triggers (PushTokenModule.swiftdidRegister). Android replays the last known token to a new listener (FirebaseMessagingDelegate.ktaddTokenListener). The registry upsert is idempotent (feat(spec): push channel protocol face — device registration widening, stored device shape, device contract members, push route, APNs/FCM config by credentialRef objectstack#29218notification-service.ts"Refresh, not duplicate"), so the doc says to treat each event as "register this token". On Android this means a cold start delivers one event atready.deviceIdin #29218 isz.string()(protocol.zod.ts:3539), so a UUID fits.userInfo.datafallback is a lenient consumer read. The card rules it as belt-and-braces. It is documented as off-contract and read last.deviceId = installId, re-register onpushTokenChanged, optionally read the UA marker in the SW gate) is for the seat to file after acceptance. Not filed here.Verification
installIdsurviving a WebView storage clear. They are listed under 「需要你做的」.__tests__/**only addsaddPushTokenListenerthrough per-filejest.mockfactories. The shared__mocks__/expo-notifications.jsis untouched, because it is outside this card's surface.Generated by Claude Code