From ec67bdb4ac500487a8a837e96916bbaf0721b117 Mon Sep 17 00:00:00 2001 From: Sawyer Hood Date: Wed, 19 Aug 2026 17:25:59 -0700 Subject: [PATCH 1/4] Prepare core, cloud, and test surfaces for the bb mobile app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Small server/SDK/cloud/test changes the native client needs, with no mobile code: - Request-side `mobile` app surface (`x-bb-app-surface`): `@bb/config` `parseRequestAppSurface`, server request context + telemetry. - `@bb/sdk`: `SdkResponseLike` transport/response contract, `projects.sidebarBootstrap()`. - New `@bb/plugin-interaction-contracts` package with the ask-user-question and secrets pending-interaction schemas; both plugins re-export it. - Fake provider `approve:` script step for approval fixtures. - Integration harness `serverPort` / `bindHost` options, the long-lived mobile e2e backend and the TLS bb connect stub (`e2e:mobile-backend`, `e2e:mobile-connect-stub`). - bb connect mobile pairing: `@bb/connect-client` machine-code pairing helpers, Settings → Remote access "Add mobile device" QR, the `bb connect machine-code` CLI command, guide/skill/docs updates. - `/.well-known/apple-app-site-association` and `assetlinks.json` served by the connect gate worker and the apex web routes (`@bb/connect-db` `app-links`). Co-Authored-By: Claude --- apps/connect/src/tunnel-do.ts | 6 + apps/connect/src/worker.test.ts | 101 +++ apps/connect/src/worker.ts | 16 +- apps/connect/wrangler.jsonc | 3 + apps/server/src/request-context.ts | 8 +- .../skills/builtin-skills/bb-cli/SKILL.md | 11 +- apps/server/src/services/system/telemetry.ts | 6 +- apps/web/src/routeTree.gen.ts | 45 + ...]well-known.apple-app-site-association.tsx | 19 + .../[.]well-known.assetlinks[.]json.tsx | 15 + apps/web/src/server/env.ts | 5 + docs/configuration.md | 20 + docs/multiple-devices.md | 25 + .../src/runtime.fake-approvals.test.ts | 185 ++++ .../agent-runtime/src/test/fake-adapter.ts | 165 +++- .../src/test/fake-provider-script.ts | 116 +++ packages/config/src/app-surface.ts | 25 + packages/config/test/app-surface.test.ts | 19 + packages/connect-client/package.json | 1 + packages/connect-client/src/credential.ts | 27 - packages/connect-client/src/index.ts | 12 +- packages/connect-client/src/list-servers.ts | 7 +- packages/connect-client/src/mobile-pairing.ts | 104 +++ packages/connect-client/src/urls.ts | 30 + .../test/connect-client.test.ts | 77 ++ packages/connect-db/src/app-links.ts | 116 +++ packages/connect-db/src/index.ts | 1 + packages/connect-db/test/app-links.test.ts | 93 ++ .../plugin-interaction-contracts/package.json | 28 + .../src/ask-user-question.ts | 76 ++ .../plugin-interaction-contracts/src/index.ts | 33 + .../src/secret-request.ts | 44 + .../test/contracts.test.ts | 99 +++ .../tsconfig.json | 10 + .../vitest.config.ts | 10 + packages/sdk/src/areas/projects.ts | 22 + packages/sdk/src/response.ts | 22 +- packages/sdk/src/transport.ts | 7 +- packages/sdk/test/public-types.test.ts | 7 +- .../src/templates/bb-guide-environments.md | 16 +- plugins/ask-user-question/package.json | 1 + plugins/ask-user-question/src/contracts.ts | 93 +- plugins/connect/app.test.tsx | 102 +++ plugins/connect/app.tsx | 255 +++++- plugins/connect/src/cli.ts | 73 +- plugins/connect/src/connect.test.ts | 104 +++ plugins/secrets/package.json | 1 + plugins/secrets/src/contracts.ts | 43 +- pnpm-lock.yaml | 28 + tests/integration/helpers/harness.ts | 15 +- tests/integration/mobile-e2e/backend.ts | 296 +++++++ tests/integration/mobile-e2e/connect-stub.ts | 812 ++++++++++++++++++ tests/integration/package.json | 4 +- turbo.json | 19 + 54 files changed, 3296 insertions(+), 182 deletions(-) create mode 100644 apps/web/src/routes/[.]well-known.apple-app-site-association.tsx create mode 100644 apps/web/src/routes/[.]well-known.assetlinks[.]json.tsx create mode 100644 packages/agent-runtime/src/runtime.fake-approvals.test.ts create mode 100644 packages/config/test/app-surface.test.ts create mode 100644 packages/connect-client/src/mobile-pairing.ts create mode 100644 packages/connect-client/src/urls.ts create mode 100644 packages/connect-db/src/app-links.ts create mode 100644 packages/connect-db/test/app-links.test.ts create mode 100644 packages/plugin-interaction-contracts/package.json create mode 100644 packages/plugin-interaction-contracts/src/ask-user-question.ts create mode 100644 packages/plugin-interaction-contracts/src/index.ts create mode 100644 packages/plugin-interaction-contracts/src/secret-request.ts create mode 100644 packages/plugin-interaction-contracts/test/contracts.test.ts create mode 100644 packages/plugin-interaction-contracts/tsconfig.json create mode 100644 packages/plugin-interaction-contracts/vitest.config.ts create mode 100644 tests/integration/mobile-e2e/backend.ts create mode 100644 tests/integration/mobile-e2e/connect-stub.ts diff --git a/apps/connect/src/tunnel-do.ts b/apps/connect/src/tunnel-do.ts index 7eca8777af..895c329864 100644 --- a/apps/connect/src/tunnel-do.ts +++ b/apps/connect/src/tunnel-do.ts @@ -20,6 +20,12 @@ export interface Env { BETTER_AUTH_SECRET: string; ACCOUNT_APP_URL?: string; CLOUD_DEV?: string; + /** + * Android signing-cert SHA-256 fingerprints for `/.well-known/assetlinks.json` + * (comma-separated). Unset → the file serves an empty list (iOS universal + * links are unaffected). + */ + ASSETLINKS_SHA256_FINGERPRINTS?: string; } const TUNNEL_TAG = "tunnel"; diff --git a/apps/connect/src/worker.test.ts b/apps/connect/src/worker.test.ts index e6a9bdd078..c0f1974d05 100644 --- a/apps/connect/src/worker.test.ts +++ b/apps/connect/src/worker.test.ts @@ -685,6 +685,107 @@ describe("machine gate auth", () => { ); }); +describe("bb mobile app-link association files", () => { + beforeEach(() => { + vi.clearAllMocks(); + // No cookie, no machine header: these must never reach the session gate. + mockParseCookie.mockReturnValue(null); + mockResolveLabel.mockResolvedValue(resolvedServer()); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it.each([ + "/.well-known/apple-app-site-association", + "/.well-known/assetlinks.json", + ])( + "serves %s on a bare label without a session and without proxying", + async (path) => { + const { env, ctx, captured } = makeEnv(() => new Response("origin")); + const response = await worker.fetch( + visitorRequest("sawyer.getbb.app", path), + env as never, + ctx, + ); + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("application/json"); + expect(captured).toHaveLength(0); + expect(mockResolveLabel).not.toHaveBeenCalled(); + expect(mockVerifySession).not.toHaveBeenCalled(); + }, + ); + + it("serves the AASA on share hosts and labels that do not resolve (Apple fetches anonymously)", async () => { + mockResolveLabel.mockResolvedValue(null); + const { env, ctx, captured } = makeEnv(() => new Response("origin")); + const share = await worker.fetch( + visitorRequest( + "sawyer--8000.getbb.app", + "/.well-known/apple-app-site-association", + ), + env as never, + ctx, + ); + expect(share.status).toBe(200); + const body = (await share.json()) as { + applinks: { details: { appIDs: string[] }[] }; + }; + expect(body.applinks.details[0]?.appIDs).toEqual([ + "9QCU24SXK5.app.getbb.mobile", + ]); + const unknown = await worker.fetch( + visitorRequest( + "nobody-here.getbb.app", + "/.well-known/apple-app-site-association", + ), + env as never, + ctx, + ); + expect(unknown.status).toBe(200); + expect(captured).toHaveLength(0); + }); + + it("reads Android fingerprints from the env and serves an empty list otherwise", async () => { + const { env, ctx } = makeEnv(() => new Response("origin")); + const empty = await worker.fetch( + visitorRequest("sawyer.getbb.app", "/.well-known/assetlinks.json"), + env as never, + ctx, + ); + const emptyBody = (await empty.json()) as { + target: { sha256_cert_fingerprints: string[] }; + }[]; + expect(emptyBody[0]?.target.sha256_cert_fingerprints).toEqual([]); + + const withEnv = await worker.fetch( + visitorRequest("sawyer.getbb.app", "/.well-known/assetlinks.json"), + { ...env, ASSETLINKS_SHA256_FINGERPRINTS: "aa:bb,cc:dd" } as never, + ctx, + ); + const withEnvBody = (await withEnv.json()) as { + target: { package_name: string; sha256_cert_fingerprints: string[] }; + }[]; + expect(withEnvBody[0]?.target.package_name).toBe("app.getbb.mobile"); + expect(withEnvBody[0]?.target.sha256_cert_fingerprints).toEqual([ + "AA:BB", + "CC:DD", + ]); + }); + + it("leaves other .well-known paths to the session gate", async () => { + const { env, ctx, captured } = makeEnv(() => new Response("origin")); + const response = await worker.fetch( + visitorRequest("sawyer.getbb.app", "/.well-known/openid-configuration"), + env as never, + ctx, + ); + expect(response.status).toBe(401); + expect(captured).toHaveLength(0); + }); +}); + describe("gate worker share hosts", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/apps/connect/src/worker.ts b/apps/connect/src/worker.ts index 9935622663..6065a683ee 100644 --- a/apps/connect/src/worker.ts +++ b/apps/connect/src/worker.ts @@ -1,5 +1,10 @@ import { drizzle } from "drizzle-orm/d1"; -import { RESERVED_HANDLES, parseVisitorHost, schema } from "@bb/connect-db"; +import { + RESERVED_HANDLES, + handleAppLinkAssociationRequest, + parseVisitorHost, + schema, +} from "@bb/connect-db"; import { TUNNEL_OFFLINE_HEADER, TunnelDO, type Env } from "./tunnel-do.js"; import { parseCookie, @@ -290,6 +295,15 @@ export default { if (url.pathname === "/api/connect/machine-label") { return handleAssignMachineLabel(request, env); } + // bb mobile universal / app links: Apple's CDN and Android fetch the + // association files anonymously from `https://