From f0594135f580296ff72ff9b4341362fb48a118b1 Mon Sep 17 00:00:00 2001 From: Can Vardar Date: Fri, 19 Jun 2026 14:47:24 +0300 Subject: [PATCH 1/8] fix(relay): buffer pre-auth frames to prevent socket close race --- apps/relay/src/index.ts | 42 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/apps/relay/src/index.ts b/apps/relay/src/index.ts index be648b8..5b8e5ff 100644 --- a/apps/relay/src/index.ts +++ b/apps/relay/src/index.ts @@ -21,8 +21,14 @@ type WsData = { sessionId?: string; role?: RelayRole; userId?: string; + authorized: boolean; + pending: Array; }; +// Cap frames buffered before authorization completes so a misbehaving or +// malicious client cannot use the pre-auth window to exhaust memory. +const MAX_PENDING_FRAMES = 32; + const log = createLogger("relay"); const app = new Hono(); const convexClient = new ConvexHttpClient(resolveConvexUrl()); @@ -55,7 +61,7 @@ const server = Bun.serve({ if (url.pathname === "/ws") { const ticket = url.searchParams.get("ticket"); const upgraded = serverInstance.upgrade(req, { - data: { ticket }, + data: { ticket, authorized: false, pending: [] }, }); if (upgraded) return undefined; return new Response("Expected websocket upgrade", { status: 426 }); @@ -69,13 +75,28 @@ const server = Bun.serve({ void authorizeSocket(ws); }, message(ws, raw) { - if (typeof raw === "string") { - hub.routeInbound(ws, raw); + if ( + typeof raw !== "string" && + !(raw instanceof ArrayBuffer) && + !(raw instanceof Uint8Array) + ) { return; } - if (raw instanceof ArrayBuffer || raw instanceof Uint8Array) { - hub.routeInbound(ws, raw); + + // Authorization is an async Convex round-trip. Frames can arrive before + // it completes (the host bridge sends `session.opened` immediately on + // open), so buffer them and flush in order once the socket is bound, + // instead of dropping the socket as "unbound". + if (!ws.data.authorized) { + if (ws.data.pending.length >= MAX_PENDING_FRAMES) { + ws.close(4003, "too many pre-auth messages"); + return; + } + ws.data.pending.push(raw); + return; } + + hub.routeInbound(ws, raw); }, close(ws) { sockets.delete(ws); @@ -107,6 +128,8 @@ async function authorizeSocket(ws: ServerWebSocket): Promise { try { const consumed = await convexClient.mutation(consumeTicketRef, { ticket }); + // The socket may have closed while the ticket round-trip was in flight. + if (ws.readyState !== WebSocket.OPEN) return; ws.data.role = consumed.role; ws.data.sessionId = consumed.sessionId; ws.data.userId = consumed.userId; @@ -115,13 +138,22 @@ async function authorizeSocket(ws: ServerWebSocket): Promise { role: consumed.role, sessionId: consumed.sessionId, }); + ws.data.authorized = true; + // Flush any frames buffered during authorization, in arrival order. + const pending = ws.data.pending; + ws.data.pending = []; + for (const frame of pending) { + hub.routeInbound(ws, frame); + } log.debug("socket authorized", { role: consumed.role, sessionId: consumed.sessionId, + flushed: pending.length, }); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); log.warn("ticket rejected", { error: err.message }); + ws.data.pending = []; ws.close(4003, "unauthorized"); } } From 8d37cb4bbcbab5d7776b526a94b5638df4992185 Mon Sep 17 00:00:00 2001 From: Can Vardar Date: Fri, 19 Jun 2026 14:47:24 +0300 Subject: [PATCH 2/8] fix(cli): handle pty spawn failure without hanging shell-host --- apps/cli/commands/shell-host.ts | 14 ++++++++++++++ apps/cli/pty/session.ts | 10 ++++++++-- apps/cli/tests/pty-session.test.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 apps/cli/tests/pty-session.test.ts diff --git a/apps/cli/commands/shell-host.ts b/apps/cli/commands/shell-host.ts index 2aba5c7..301e067 100644 --- a/apps/cli/commands/shell-host.ts +++ b/apps/cli/commands/shell-host.ts @@ -116,6 +116,14 @@ export async function runShellHost(opts: ShellHostOptions = {}): Promise { trackError("shell-host", err, { scope: "pty" }); }); + // PtySession reports spawn failure synchronously via state. Bail out before + // any further setup so a failed spawn cannot hang the host process. + if (session.status === "closed") { + log.error("failed to start shell session", { sessionId }); + process.stderr.write("wrapper: failed to start shell session\n"); + process.exit(1); + } + let server: LocalServerHandle; try { server = startLocalServer({ @@ -381,6 +389,12 @@ export async function runShellHost(opts: ShellHostOptions = {}): Promise { }); const exitCode = await new Promise((resolve) => { + // Guard against a session that already exited during setup, otherwise the + // late `once("exit")` listener would never fire and the host would hang. + if (session.status === "closed") { + resolve(session.lastExitCode); + return; + } session.once("exit", (code) => resolve(code)); }); diff --git a/apps/cli/pty/session.ts b/apps/cli/pty/session.ts index d341bad..d195c7a 100644 --- a/apps/cli/pty/session.ts +++ b/apps/cli/pty/session.ts @@ -146,8 +146,14 @@ export class PtySession extends EventEmitter { }); } catch (err) { this.state = "closed"; - this.emit("error", asError(err, "pty:spawn")); - this.emit("exit", null); + // Defer emits to a microtask: the constructor returns first so callers + // can attach `error`/`exit` listeners. Emitting synchronously here would + // throw on the unlistened `error` event and lose `exit`, hanging the host. + // Callers can also detect failure synchronously via `status === "closed"`. + queueMicrotask(() => { + this.emit("error", asError(err, "pty:spawn")); + this.emit("exit", null); + }); return; } diff --git a/apps/cli/tests/pty-session.test.ts b/apps/cli/tests/pty-session.test.ts new file mode 100644 index 0000000..5c04cea --- /dev/null +++ b/apps/cli/tests/pty-session.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "bun:test"; +import { PtySession } from "../pty/session"; + +/** + * Regression guard for the spawn-failure lifecycle (Finding 5): an `exit` + * listener attached *after* construction must still observe termination, and + * the session must never leave a caller awaiting an event that already fired. + * A nonexistent shell makes the PTY helper fail `execvp` and exit non-zero. + */ +describe("PtySession failure lifecycle", () => { + test("late exit listener still observes termination of a failing shell", async () => { + const session = new PtySession({ shell: "/nonexistent/wrapper-bad-shell" }); + // Never let an emitted error event throw and crash the test runner. + session.on("error", () => {}); + + const exitCode = await new Promise((resolve) => { + if (session.status === "closed") { + resolve(session.lastExitCode); + return; + } + session.once("exit", (code) => resolve(code)); + }); + + expect(session.status).toBe("closed"); + expect(exitCode === null || typeof exitCode === "number").toBe(true); + }); +}); From 8d219bf3cacfe8344dd7c28b1c8644f2fe166bef Mon Sep 17 00:00:00 2001 From: Can Vardar Date: Fri, 19 Jun 2026 14:47:25 +0300 Subject: [PATCH 3/8] fix(backend): avoid known default better-auth secret outside dev --- packages/backend/convex/auth.ts | 38 +++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/backend/convex/auth.ts b/packages/backend/convex/auth.ts index 4a99358..c0904dd 100644 --- a/packages/backend/convex/auth.ts +++ b/packages/backend/convex/auth.ts @@ -8,10 +8,38 @@ import authConfig from "./auth.config"; import { deviceAuthorization, lastLoginMethod } from "better-auth/plugins"; import authSchema from "./betterAuth/schema"; +const isDevEnvironment = process.env.ENVIRONMENT === "development"; + const siteUrl = - process.env.SITE_URL || - (process.env.ENVIRONMENT === "development" ? "http://localhost:3000" : "https://wrapper.sh"); -const betterAuthSecret = process.env.BETTER_AUTH_SECRET ?? "wrapper-local-dev-secret-change-me"; + process.env.SITE_URL || (isDevEnvironment ? "http://localhost:3000" : "https://wrapper.sh"); + +const DEV_AUTH_SECRET = "wrapper-local-dev-secret-change-me"; + +function resolveBetterAuthSecret(): string { + const secret = process.env.BETTER_AUTH_SECRET; + if (secret) return secret; + + if (isDevEnvironment) { + console.warn( + "[auth] BETTER_AUTH_SECRET is not set; using a stable development-only default. " + + "Never run production without a real secret.", + ); + return DEV_AUTH_SECRET; + } + + // Outside development we must never fall back to a publicly known constant — + // that would let anyone forge auth/session tokens and impersonate accounts. + // A module-load throw is not viable: Convex's push/analyze phase evaluates + // this module without deployment env vars, which would break codegen/deploy. + // Instead, fail *closed* with an ephemeral per-instance secret: a + // misconfigured deployment can't validate any session (forcing the operator + // to notice and set the real secret) and is never forgeable with a known key. + console.error( + "[auth] BETTER_AUTH_SECRET is not set outside development. Generating an ephemeral " + + "secret; sessions will not persist or validate until BETTER_AUTH_SECRET is configured.", + ); + return `ephemeral-${crypto.randomUUID()}-${crypto.randomUUID()}`; +} // The component client has methods needed for integrating Convex with Better Auth, // as well as helper methods for general use. @@ -39,7 +67,9 @@ export const createAuth = (ctx: GenericCtx) => { return betterAuth({ baseURL: siteUrl, - secret: betterAuthSecret, + // Resolved lazily (not at module load) so Convex's push/analyze phase + // never evaluates it without the deployment env present. + secret: resolveBetterAuthSecret(), database: authComponent.adapter(ctx), socialProviders, plugins: [ From 9ad6927753dec7c50ec153669726ede9a1c857a8 Mon Sep 17 00:00:00 2001 From: Can Vardar Date: Fri, 19 Jun 2026 14:47:25 +0300 Subject: [PATCH 4/8] feat(backend): rate-limit unauthenticated device-auth endpoints --- packages/backend/convex/deviceAuth.ts | 19 +++++++++ packages/backend/convex/lib/rateLimit.ts | 51 ++++++++++++++++++++++++ packages/backend/convex/schema.ts | 5 +++ 3 files changed, 75 insertions(+) create mode 100644 packages/backend/convex/lib/rateLimit.ts diff --git a/packages/backend/convex/deviceAuth.ts b/packages/backend/convex/deviceAuth.ts index 500ad40..b9d54aa 100644 --- a/packages/backend/convex/deviceAuth.ts +++ b/packages/backend/convex/deviceAuth.ts @@ -2,6 +2,7 @@ import { v } from "convex/values"; import { components } from "./_generated/api"; import { mutation, query } from "./_generated/server"; import { protectedMutation } from "./lib/middleware"; +import { enforceRateLimit } from "./lib/rateLimit"; const siteUrl = process.env.SITE_URL || @@ -13,6 +14,17 @@ export const requestDeviceCode = mutation({ scope: v.optional(v.string()), }, handler: async (ctx, args) => { + // Throttle unauthenticated issuance to prevent storage/abuse flooding: + // a per-client window plus a global ceiling. Keys stay low-cardinality. + await enforceRateLimit(ctx, `requestDeviceCode:client:${args.clientId ?? "anon"}`, { + limit: 10, + windowMs: 60_000, + }); + await enforceRateLimit(ctx, "requestDeviceCode:global", { + limit: 120, + windowMs: 60_000, + }); + const result = await ctx.runMutation(components.betterAuth.deviceAuth.requestDeviceCode, { clientId: args.clientId, scope: args.scope, @@ -36,6 +48,13 @@ export const pollDeviceToken = mutation({ device_code: v.string(), }, handler: async (ctx, args) => { + // Global poll ceiling caps flooding; per-code pacing is enforced by the + // Better Auth `interval`/`slow_down` contract the CLI already honours. + await enforceRateLimit(ctx, "pollDeviceToken:global", { + limit: 600, + windowMs: 60_000, + }); + let result: { expires_in: number; session_token: string; diff --git a/packages/backend/convex/lib/rateLimit.ts b/packages/backend/convex/lib/rateLimit.ts new file mode 100644 index 0000000..b5faee0 --- /dev/null +++ b/packages/backend/convex/lib/rateLimit.ts @@ -0,0 +1,51 @@ +import type { MutationCtx } from "../_generated/server.js"; +import { createError, ErrorCode } from "./errors.ts"; +import { ErrorSeverity } from "./types.ts"; + +export type RateLimitOptions = { + /** Maximum allowed calls per window. */ + limit: number; + /** Window length in milliseconds. */ + windowMs: number; +}; + +/** + * Fixed-window rate limiter for unauthenticated endpoints. + * + * Keys should be low-cardinality (e.g. `requestDeviceCode:global` or + * per-clientId) so the backing table stays small. Throws + * `RATE_LIMIT_EXCEEDED` with a `retryAfterMs` hint when the window is full. + */ +export async function enforceRateLimit( + ctx: MutationCtx, + key: string, + options: RateLimitOptions, +): Promise { + const now = Date.now(); + const existing = await ctx.db + .query("rateLimit") + .withIndex("by_key", (q) => q.eq("key", key)) + .first(); + + if (!existing) { + await ctx.db.insert("rateLimit", { key, count: 1, resetAt: now + options.windowMs }); + return; + } + + if (existing.resetAt <= now) { + await ctx.db.patch(existing._id, { count: 1, resetAt: now + options.windowMs }); + return; + } + + if (existing.count >= options.limit) { + const retryAfterMs = existing.resetAt - now; + throw createError({ + code: ErrorCode.RATE_LIMIT_EXCEEDED, + message: `Rate limit exceeded. Retry in ${Math.ceil(retryAfterMs / 1000)}s`, + severity: ErrorSeverity.Low, + metadata: { retryAfterMs }, + }); + } + + await ctx.db.patch(existing._id, { count: existing.count + 1 }); +} diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index 135be69..8bedcb5 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -55,4 +55,9 @@ export default defineSchema({ .index("by_tokenHash", ["tokenHash"]) .index("by_session_role", ["sessionId", "role"]) .index("by_expiresAt", ["expiresAt"]), + rateLimit: defineTable({ + key: v.string(), + count: v.number(), + resetAt: v.number(), + }).index("by_key", ["key"]), }); From 9af72a0eb4e1292ed9e000d1cddc156931ceaedd Mon Sep 17 00:00:00 2001 From: Can Vardar Date: Fri, 19 Jun 2026 14:47:25 +0300 Subject: [PATCH 5/8] fix(cli): verify release archive checksum during install --- .github/workflows/release-cli.yml | 7 +++++++ apps/cli/scripts/install.sh | 33 ++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 80233fa..81ccd6b 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -129,6 +129,12 @@ jobs: echo "sha_darwin_x64=$(sha256sum dist/wrapper-darwin-x86_64.tar.gz | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" echo "sha_linux_x64=$(sha256sum dist/wrapper-linux-x86_64.tar.gz | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" + - name: Write checksums file + run: | + cd dist + sha256sum wrapper-darwin-arm64.tar.gz wrapper-darwin-x86_64.tar.gz wrapper-linux-x86_64.tar.gz > checksums.txt + cat checksums.txt + - name: Create or update release uses: softprops/action-gh-release@v2 with: @@ -139,6 +145,7 @@ jobs: dist/wrapper-darwin-arm64.tar.gz dist/wrapper-darwin-x86_64.tar.gz dist/wrapper-linux-x86_64.tar.gz + dist/checksums.txt update-homebrew: name: Update Homebrew Tap diff --git a/apps/cli/scripts/install.sh b/apps/cli/scripts/install.sh index d8071d3..7dd4bbd 100644 --- a/apps/cli/scripts/install.sh +++ b/apps/cli/scripts/install.sh @@ -37,12 +37,39 @@ if [ "$VERSION" = "latest" ]; then fi archive="wrapper-${platform}.tar.gz" -url="https://github.com/${REPO}/releases/download/${VERSION}/${archive}" +base="https://github.com/${REPO}/releases/download/${VERSION}" tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT -echo "Downloading ${url}" -curl -fsSL "$url" -o "${tmp_dir}/${archive}" +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d' ' -f1 + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | cut -d' ' -f1 + else + echo "Neither sha256sum nor shasum is available for integrity verification" >&2 + exit 1 + fi +} + +echo "Downloading ${base}/${archive}" +curl -fsSL "${base}/${archive}" -o "${tmp_dir}/${archive}" + +echo "Verifying checksum" +curl -fsSL "${base}/checksums.txt" -o "${tmp_dir}/checksums.txt" +expected="$(grep " ${archive}\$" "${tmp_dir}/checksums.txt" | cut -d' ' -f1)" +if [ -z "$expected" ]; then + echo "No checksum found for ${archive} in checksums.txt; aborting" >&2 + exit 1 +fi +actual="$(sha256_of "${tmp_dir}/${archive}")" +if [ "$expected" != "$actual" ]; then + echo "Checksum mismatch for ${archive}" >&2 + echo " expected: ${expected}" >&2 + echo " actual: ${actual}" >&2 + exit 1 +fi + mkdir -p "$INSTALL_DIR" tar -xzf "${tmp_dir}/${archive}" -C "$INSTALL_DIR" chmod +x "${INSTALL_DIR}/bin/wrapper" || true From f5fb843fb938279b9f8f8502b6333e3677623330 Mon Sep 17 00:00:00 2001 From: Can Vardar Date: Fri, 19 Jun 2026 14:47:25 +0300 Subject: [PATCH 6/8] test(relay): guard protocol copy against repo protocol drift --- apps/relay/package.json | 1 + apps/relay/src/protocol.ts | 10 ++++++ apps/relay/tests/protocol-parity.test.ts | 46 ++++++++++++++++++++++++ bun.lock | 11 ++++++ 4 files changed, 68 insertions(+) create mode 100644 apps/relay/tests/protocol-parity.test.ts diff --git a/apps/relay/package.json b/apps/relay/package.json index 45de881..1b042a5 100644 --- a/apps/relay/package.json +++ b/apps/relay/package.json @@ -21,6 +21,7 @@ "hono": "^4.10.6" }, "devDependencies": { + "@repo/protocol": "*", "@types/bun": "catalog:", "oxfmt": "catalog:", "oxlint": "catalog:" diff --git a/apps/relay/src/protocol.ts b/apps/relay/src/protocol.ts index 36cb0fc..f4f19ef 100644 --- a/apps/relay/src/protocol.ts +++ b/apps/relay/src/protocol.ts @@ -1,3 +1,13 @@ +/* + * Deploy-time copy of `@repo/protocol`. + * + * The relay Docker image installs only its own package (no monorepo + * workspace context), so it cannot resolve `@repo/protocol` at runtime. + * This hand-rolled copy keeps the relay self-contained. Drift is guarded by + * `tests/protocol-parity.test.ts`, which fails CI if this diverges from + * `@repo/protocol`. Keep both in sync when the wire protocol changes. + */ + export type RawWireData = string | ArrayBuffer | Uint8Array; type SessionId = string; diff --git a/apps/relay/tests/protocol-parity.test.ts b/apps/relay/tests/protocol-parity.test.ts new file mode 100644 index 0000000..6a671b0 --- /dev/null +++ b/apps/relay/tests/protocol-parity.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from "bun:test"; +import { + WrapperMessageSchema, + encodeMessage as repoEncode, + parseMessage as repoParse, + type WrapperMessage as RepoMessage, +} from "@repo/protocol"; +import { encodeMessage as relayEncode, parseMessage as relayParse } from "../src/protocol"; + +/** + * The relay ships a deploy-time copy of the wire protocol (see + * `src/protocol.ts`). This test fails if that copy drifts from + * `@repo/protocol` — either a new message type is added upstream, or the + * encode/parse behaviour diverges for an existing one. + */ + +const samples: RepoMessage[] = [ + { type: "attach", sessionId: "s1" }, + { type: "detach", sessionId: "s1" }, + { type: "input", sessionId: "s1", data: "ls\n" }, + { type: "resize", sessionId: "s1", size: { cols: 80, rows: 24 } }, + { type: "session.opened", sessionId: "s1", size: { cols: 80, rows: 24 } }, + { type: "session.closed", sessionId: "s1", exitCode: 0 }, + { type: "output", sessionId: "s1", data: "hi" }, + { type: "error", sessionId: "s1", code: "internal", message: "boom" }, +]; + +describe("relay protocol parity with @repo/protocol", () => { + test("relay handles every message type @repo/protocol defines", () => { + const repoTypes = new Set( + WrapperMessageSchema.options.map((option) => option.shape.type.value as string), + ); + const sampleTypes = new Set(samples.map((message) => message.type)); + expect(sampleTypes).toEqual(repoTypes); + }); + + test("messages round-trip identically across both implementations", () => { + for (const message of samples) { + const repoWire = repoEncode(message); + const relayWire = relayEncode(message); + expect(relayWire).toBe(repoWire); + expect(relayParse(repoWire)).toEqual(message); + expect(repoParse(relayWire)).toEqual(message); + } + }); +}); diff --git a/bun.lock b/bun.lock index 8392534..426d473 100644 --- a/bun.lock +++ b/bun.lock @@ -58,6 +58,7 @@ "hono": "^4.10.6", }, "devDependencies": { + "@repo/protocol": "*", "@types/bun": "catalog:", "oxfmt": "catalog:", "oxlint": "catalog:", @@ -67,7 +68,9 @@ "name": "web", "version": "0.1.0", "dependencies": { + "@convex-dev/better-auth": "^0.12.2", "@opennextjs/cloudflare": "^1.18.0", + "better-auth": "^1.6.9", "convex": "^1.37.0", "next": "catalog:", "react": "catalog:", @@ -2838,6 +2841,10 @@ "@puppeteer/browsers/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "@repo/protocol/@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@repo/relay/@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@shikijs/core/hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "@stoplight/better-ajv-errors/leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], @@ -3256,6 +3263,10 @@ "@puppeteer/browsers/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "@repo/protocol/@types/bun/bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "@repo/relay/@types/bun/bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "@shikijs/core/hast-util-to-html/property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "@stoplight/spectral-core/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], From dec34e706c7a58f6e64e83939eb8a519dd0c3822 Mon Sep 17 00:00:00 2001 From: Can Vardar Date: Fri, 19 Jun 2026 14:47:26 +0300 Subject: [PATCH 7/8] chore(backend): refresh generated convex bindings and widen fmt ignore --- .oxfmtignore | 2 +- packages/backend/convex/_generated/api.d.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.oxfmtignore b/.oxfmtignore index fa72f34..ace43b5 100644 --- a/.oxfmtignore +++ b/.oxfmtignore @@ -8,5 +8,5 @@ out build coverage bun.lock -**/convex/_generated/** +**/_generated/** **/CHANGELOG.md diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index 1dc0311..6840416 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -15,9 +15,12 @@ import type * as http from "../http.js"; import type * as lib_errors from "../lib/errors.js"; import type * as lib_logger from "../lib/logger.js"; import type * as lib_middleware from "../lib/middleware.js"; +import type * as lib_onboarding from "../lib/onboarding.js"; +import type * as lib_rateLimit from "../lib/rateLimit.js"; import type * as lib_relayTicket from "../lib/relayTicket.js"; import type * as lib_sessionConfig from "../lib/sessionConfig.js"; import type * as lib_types from "../lib/types.js"; +import type * as onboarding from "../onboarding.js"; import type * as relay from "../relay.js"; import type * as session from "../session.js"; @@ -31,9 +34,12 @@ declare const fullApi: ApiFromModules<{ "lib/errors": typeof lib_errors; "lib/logger": typeof lib_logger; "lib/middleware": typeof lib_middleware; + "lib/onboarding": typeof lib_onboarding; + "lib/rateLimit": typeof lib_rateLimit; "lib/relayTicket": typeof lib_relayTicket; "lib/sessionConfig": typeof lib_sessionConfig; "lib/types": typeof lib_types; + onboarding: typeof onboarding; relay: typeof relay; session: typeof session; }>; From 3521fa73b0ac9cb314e4a1edb6299a80fb3b3253 Mon Sep 17 00:00:00 2001 From: Can Vardar Date: Fri, 19 Jun 2026 15:07:41 +0300 Subject: [PATCH 8/8] chore(backend): regenerate convex bindings after dev merge