Skip to content

feat!: rebuild as a hybrid shell around the web Console (v2.0) - #144

Merged
os-zhuang merged 3 commits into
mainfrom
claude/cool-volta-azu3gs
Nov 2, 2026
Merged

os-zhuang merged 3 commits into
mainfrom
claude/cool-volta-azu3gs

Conversation

@hotlong

@hotlong hotlong commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Why

The 1.x native metadata renderer could not keep pace with the platform:

  • it is on @objectstack/* 9.0.1 while the platform ships 18.2.0;
  • push registration calls client.notifications.registerDevice, which the framework client removed (the /notifications/devices routes were never built);
  • page kinds such as kind:'react' (ADR-0081) run real React in the browser and cannot be rendered natively. ADR-0078 also requires every spec property to be live in each renderer, so a second renderer doubles that burden.

2.0 turns the app into a native shell around the web Console. Business UI is the Console served by the configured server. The shell does what the web cannot do well. The 1.x renderer is preserved on legacy/native-renderer.

What's in this PR (Phase 1 — shell skeleton)

  • Server picker. Accepts a bare host, a full URL or a pasted Console link. Public hosts must use https.
  • Console WebView (components/shell/ConsoleWebView.tsx):
    • one long-lived page using the cookie session, so the shell holds no API credential;
    • a loading cover, plus native offline and 5xx pages;
    • automatic retry when the network comes back, and recovery when the web content process dies;
    • routes requested before the page has loaded are queued until it has.
  • Navigation policy (lib/shell/navigation-policy.ts):
    • other sites open in the in-app browser;
    • mailto: / tel: / app schemes go to the OS;
    • javascript: / file: / intent: / top-frame data: are blocked;
    • hosts in EXPO_PUBLIC_TRUSTED_HOSTS (e.g. the SSO identity provider) may load in the WebView.
  • Route ownership (lib/shell/route-owner.ts): every route is web today. A route can later move to a native screen with one rule. A rule that names a screen the build doesn't have falls back to web.
  • Bridge v1 (window.ObjectStackNative):
    • methods: getPushToken, haptic, openExternal, navigate, resetServer;
    • events: ready, routeChanged;
    • safe-area insets published as CSS variables;
    • calls are answered only for pages on the configured server's origin, and arguments are validated per method.
  • Push: the shell gets the native APNs/FCM device token, not an Expo token, and tapping a notification opens its Console route.
  • App links: objectstack://open?route=… and objectstack://console/… open the Console at that route.
  • Removed:
    • all renderers and data hooks, the SQLite offline store, the integration server, the apps/docs site and the Maestro / E2E suites;
    • @objectstack/client*, better-auth, TanStack Query and the other dependencies only they used.
  • Kept for later phases: the components/ui primitives, design tokens, haptics, and the biometric / app-lock libraries.
  • Docs: new docs/HYBRID-SHELL.md covering the architecture, navigation policy, route-ownership rules and the bridge contract. README, ROADMAP, push, device testing, deployment and contributing docs are rewritten.
  • CI: lint-typecheck.yml now runs on pull requests; its docs-site step is gone.

Verification

  • pnpm run lint (tsc + ESLint): clean.
  • npx jest --ci: 26 suites, 233 tests pass. lib/shell has about 90% line coverage. The bridge bootstrap script is executed against a fake window.
  • npx expo export --platform ios: the Hermes bundle builds.
  • Not run on a device or simulator yet. The manual checklist is in docs/DEVICE-TESTING.md.

Not in this PR (follow-ups)

  • Phase 2 (framework repo): @object-ui/mobile useNativeHost() and shell mode, and a mobile-width audit of the Console.
  • Phase 3: server device registry and APNs/FCM channel, plus Console-side token registration.
  • Phase 4: biometric lock, plus scan / pickImage / openFile / share, and external IdP sign-in via ASWebAuthenticationSession.
  • Phase 5: device verification and the App Store review build.

The archive tag native-renderer-final could not be pushed from this environment (the git proxy refused tag pushes), so the legacy/native-renderer branch serves as the archive.


Generated by Claude Code

Business UI moves to the Console served by the configured ObjectStack
server; the app becomes a native shell that hosts it. The native metadata
renderer, its 90+ data hooks, offline store and the @objectstack/client
dependency are removed (preserved on the legacy/native-renderer branch).

Shell skeleton:
- server picker (host / URL / pasted Console link, https for public hosts)
- long-lived Console WebView with cookie session, loading cover,
  offline + server-error pages, auto-retry on reconnect, process recovery
- navigation policy: in-app browser for other sites, OS for mailto/tel,
  dangerous schemes blocked, trusted IdP hosts
- Android back / iOS swipe-back through Console history
- route ownership seam + native-screen registry (all routes web today)
- bridge v1 (window.ObjectStackNative): getPushToken, haptic,
  openExternal, navigate, resetServer, ready/routeChanged events,
  safe-area CSS variables; origin-checked and argument-validated
- native device push token (APNs/FCM) + tap routing to Console routes
- app links: objectstack://open?route=… and objectstack://console/…
- Sentry init, shell i18n (en/zh/ar), lint CI on pull requests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4EjjRHkJgnuLkU9vVgHHR
…bView by remount

Review round on the hybrid shell:

- One route gate, safeConsoleRoute (lib/shell/console-url.ts), now used by
  app links, push routes, bridge navigate, routeFromConsoleUrl and
  consoleUrl. It refuses ./.. segments (raw, percent-encoded in any case,
  double-encoded), encoded separators, backslashes, control characters and
  a leading //, so no entry point can build <server>/_console/../../api/...
- parseUrl drops the scheme's default port, so normalizeServerUrl, originOf,
  routeFromConsoleUrl, the navigation policy and the bridge's origin check
  agree with the URLs WebViews report. The server picker adopts the origin
  a redirected probe ended on (apex -> www, http -> https) via
  resolveServerUrl / serverUrlAfterRedirect.
- routeFromResponse reads content.data, then the iOS trigger.payload and its
  body: expo-notifications exposes only userInfo["body"] as content.data on
  iOS. PUSH-NOTIFICATIONS.md states the APNs and FCM payload contract.
- ConsoleWebView: a failed load (onError, then onLoadEnd) no longer marks
  the page loaded or flushes the pending route; routes requested while the
  failure page is up are queued; retry re-creates the WebView at the pending
  route, else the last Console route. A dead render/content process
  re-creates the WebView (new key) instead of reload() on a dead instance.
  The idempotent bootstrap is also passed as injectedJavaScript.
- The bridge injects nothing for a call from an untrusted origin; the
  forbidden_origin code is gone.
- redirectIncomingPath adds a nonce so the same link opened twice navigates.
- HYBRID-SHELL.md: correct the Android injection description, and record
  the Android isTopFrame and old-WebView iframe message-origin limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4EjjRHkJgnuLkU9vVgHHR
…n trusted hosts

- buildNavigateScript(serverUrl, route) now dispatches a cancelable
  `objectstack-native:navigate` CustomEvent ({ detail: { route } }) on
  window. A Console that handles it with its own router calls
  preventDefault() and the bundle is not reloaded; otherwise the script
  falls back to window.location.assign(consoleUrl(route)). The route goes
  through safeConsoleRoute (an escaping route yields no script) and is
  spliced as escaped JSON. Documented under "Shell -> Console events" and
  in the Console-side integration list.
- parseHostList drops a default port (:443 / :80), matching parseUrl, so a
  trusted host written as idp.example.com:443 matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4EjjRHkJgnuLkU9vVgHHR

hotlong commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

维护者速读

  • 改了什么:mobile 由原生元数据渲染器重建为宿主 Console 的混合壳,包括服务器选择、承载 Console 的 WebView、导航策略、桥接 v1、原生推送 token 和 App 链接;同时删除原生渲染器和 @objectstack/client 依赖,约 −77k / +4k 行。
  • 为什么改:原生渲染器落后平台 9 个大版本,推送注册调用的是已被删除的方法,kind:'react' 页面也无法原生渲染。改成壳以后,平台发版不再需要移动端跟版。
  • 风险与回滚:
    • 还没有在真机或模拟器上跑过。
    • 合并到 main 时,如果仓库配置了 EXPO_TOKEN,会触发 eas-build 和 eas-update,相当于一次发布。
    • 回滚用 revert;旧实现保留在 legacy/native-renderer 分支。
  • 先看:lib/shell/console-url.ts(safeConsoleRoute,路由逃逸防护)、lib/shell/navigation-policy.ts、lib/shell/bridge/handler.ts、components/shell/ConsoleWebView.tsx。
  • 两分钟验证:pnpm install && pnpm test(283 通过)与 pnpm run lint;真机检查按 docs/DEVICE-TESTING.md 的清单。
  • 需要你做的:
    • 审阅后合并:本 PR 超过 3000 行,只能由你批准。
    • 合并前确认 EXPO_TOKEN 是否已配置:已配置的话,合并即触发一次 EAS 构建和 OTA。
    • 真机清单需要你或测试同学执行。

席位意见:ACCEPT。独立审核的 8 条应修问题已全部修复并带测试;补丁轮把壳发起的跳转改成先交给 Console 的前端路由,避免整页重载。唯一不能代你判断的是真机行为。

席位核对

Head-sha: 12ec76923be9c798adc9ba7b08c9f152284c2e12

  • Form: draft against main. No card: this is the session-driven rebuild, and its follow-ups are Biometric app lock: cover the Console with a native lock screen after N minutes in the background #145–Route ownership rules served by the server (Hotwire-style path configuration) instead of compiled into the app #149 plus @object-ui/mobile: shell mode — detect window.ObjectStackNative and adapt the Console when the mobile shell hosts it objectstack#29039–#29042. +4074 / −77303 lines across 560 files, so it lands only through maintainer approval.

  • Premise measured before the rebuild:

    • mobile was on @objectstack/* 9.0.1 while the platform ships 18.2.0;
    • client.notifications.registerDevice had been removed from the framework client, because the server routes were never built.
  • Gates on head, pinned to 12ec769: Code Quality success and Lint & Type Check success. The developer reported lint clean and 283 tests passing. The seat re-ran the shell suites itself: 108 tests, plus 16 for the navigate script and config, all passing.

  • Independent review. A reviewer separate from the author found 8 should-fix items, all reproduced and fixed in 443207c with tests:

    • .. and percent-encoded traversal escaping the Console, now one validator, safeConsoleRoute, used at every entry point;
    • default-port origin mismatch, plus server redirects;
    • the iOS APNs payload not reaching content.data;
    • a route queued during a failed load being lost;
    • WebView crash recovery, now done by remounting the WebView;
    • the Android injection-timing docs;
    • untrusted bridge calls getting a reply;
    • repeated app links doing nothing.

    The patch round in 12ec769 added Console-router navigation through the cancelable objectstack-native:navigate event, falling back to location.assign, and default-port normalization for trusted hosts. Seat spot checks: the validator is wired into incoming links, push routes, the bridge navigate call, consoleUrl and routeFromConsoleUrl; the navigate script refuses an escaping route and splices values as escaped JSON.

  • Accepted deviations:

    • no device or simulator run;
    • the archive tag native-renderer-final could not be pushed (the git proxy refused tag pushes), so the branch legacy/native-renderer is the archive;
    • GitHub writes went through the user's account, because no fleet credentials exist in this session;
    • Prettier reformatted a docs table in touched files.
  • Out of scope, recorded as known limits in docs/HYBRID-SHELL.md or acceptance notes:

  • Skills-docs: n/a, no metadata-protocol change.

  • Landing:


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants