diff --git a/desktop-connector/src/admissions.mjs b/desktop-connector/src/admissions.mjs new file mode 100644 index 0000000..aaf36f2 --- /dev/null +++ b/desktop-connector/src/admissions.mjs @@ -0,0 +1,69 @@ +import { readFile, writeFile, rename, chmod } from "node:fs/promises" +import { randomUUID } from "node:crypto" + +const record = value => value !== null && typeof value === "object" && !Array.isArray(value) +const invalid = message => Object.assign(new Error(message), { code: "INVALID_ADMISSIONS", mobileStorage: true }) + +async function recoveryBefore(path) { + try { + const value = JSON.parse(await readFile(path + ".recovery", "utf8")) + if (!record(value) || !Number.isSafeInteger(value.before) || value.before < 0) + throw invalid("Invalid mobile admission recovery boundary") + return value.before + } catch (error) { + if (error.code === "ENOENT") return undefined + if (error instanceof SyntaxError) throw invalid("Invalid mobile admission recovery boundary") + error.mobileStorage = true + throw error + } +} + +async function journal(path) { + try { + const value = JSON.parse(await readFile(path, "utf8")) + if (!record(value) || !record(value.admissions) || !record(value.selections) || + Object.entries(value.admissions).some(([key, item]) => + !/^[A-Za-z0-9_-]+\.msg_[A-Za-z0-9_-]+$/.test(key) || !record(item) || + !/^[a-f0-9]{64}$/.test(item.hash) || !["forwarding", "admitted"].includes(item.state)) || + Object.values(value.selections).some(item => !record(item)) || + (value.recoverySessions !== undefined && (!Array.isArray(value.recoverySessions) || + value.recoverySessions.some(id => typeof id !== "string" || !/^ses_[a-f0-9]{32}$/.test(id))))) + throw invalid("Invalid mobile admission journal") + return value + } catch (error) { + if (error.code === "ENOENT") return { admissions: {}, selections: {} } + if (error instanceof SyntaxError) throw invalid("Invalid mobile admission journal") + error.mobileStorage = true + throw error + } +} + +export async function openAdmissions(path) { + const before = await recoveryBefore(path) + return { state: await journal(path), recovered: before !== undefined } +} + +// Reset never discards a healthy journal: it may contain an uncertain delivery. +// Persist the fence BEFORE quarantine so a crash cannot reopen an empty journal +// without its duplicate-delivery protection. Healthy resets leave it unchanged; +// another corrupt journal must fence deliveries made since the previous recovery. +export async function resetAdmissions(path) { + const before = await recoveryBefore(path) + try { + await journal(path) + return + } catch (error) { + if (error.code !== "INVALID_ADMISSIONS") throw error + } + try { + const temporary = path + ".recovery." + randomUUID() + await writeFile(temporary, JSON.stringify({ before: Math.max(before ?? 0, Date.now()) }), { flag: "wx", mode: 0o600 }) + await rename(temporary, path + ".recovery") + // A damaged file can contain previous selections; keep its backup private. + await chmod(path, 0o600) + await rename(path, path + ".corrupt-" + randomUUID()) + } catch (error) { + error.mobileStorage = true + throw error + } +} diff --git a/desktop-connector/src/desktop-access.mjs b/desktop-connector/src/desktop-access.mjs index d1fd02f..7fbc22c 100644 --- a/desktop-connector/src/desktop-access.mjs +++ b/desktop-connector/src/desktop-access.mjs @@ -4,6 +4,7 @@ import QRCode from "qrcode" import { createGateway } from "./gateway.mjs" import { connectTunnel, removeTunnel } from "./tunnel.mjs" import { openDevices, resetDevices } from "./devices.mjs" +import { resetAdmissions } from "./admissions.mjs" export async function createMobileAccess(options) { const root = join(options.userDataPath, "mobile-access") @@ -136,7 +137,7 @@ export async function createMobileAccess(options) { } catch (error) { if (token === epoch) { status = "error" - errorKey ??= "mobile.error.connection" + errorKey ??= error.mobileStorage ? "mobile.error.storage" : "mobile.error.connection" } throw error } finally { @@ -190,7 +191,15 @@ export async function createMobileAccess(options) { await shutdown(true) } else if (input.type === "reset") { await shutdown(true) - devices = (await resetDevices(storePath)).list() + try { + await resetAdmissions(join(root, "admissions.json")) + devices = (await resetDevices(storePath)).list() + } catch (error) { + status = "error" + errorKey = "mobile.error.storage" + emit() + throw error + } await (options.removeTunnel ?? removeTunnel)({ storePath: join(root, "tunnel-bharatcode-ai") }) } else if (input.type === "approve") { if (!gateway) throw new Error("Remote access disabled") diff --git a/desktop-connector/src/desktop-api.mjs b/desktop-connector/src/desktop-api.mjs index d18bf68..68228c2 100644 --- a/desktop-connector/src/desktop-api.mjs +++ b/desktop-connector/src/desktop-api.mjs @@ -1,6 +1,7 @@ -import { readFile, writeFile, rename } from 'node:fs/promises' -import { createHash } from 'node:crypto' +import { writeFile, rename } from 'node:fs/promises' +import { createHash, randomUUID } from 'node:crypto' import { setTimeout as delay } from 'node:timers/promises' +import { openAdmissions } from './admissions.mjs' const fail = (status, message) => Object.assign(new Error(message), { status }) const normalizedPermission = item => ({ ...item, action: item.permission, resources: item.patterns ?? [] }) @@ -16,8 +17,7 @@ export function desktopMessage(item) { // Desktop's default runtime serves native session metadata but continues chats // through its compatibility engine. Keep that same engine and message tables. export async function createDesktopAPI({ request, storePath }) { - let state = { admissions: {}, selections: {} } - try { state = { ...state, ...JSON.parse(await readFile(storePath,'utf8')) } } catch (error) { if(error.code !== 'ENOENT') throw error } + const { state, recovered } = await openAdmissions(storePath) let writes = Promise.resolve() const save = () => { const json = JSON.stringify(state) @@ -43,6 +43,20 @@ export async function createDesktopAPI({ request, storePath }) { } async function handle(method, raw, body, signal) { const url = new URL(raw,'http://localhost'), path = url.pathname + if (method === 'POST' && path === '/api/session' && recovered) { + // Session creation can adopt a caller-supplied identity. During recovery + // only our own fresh random identity proves that a session has no lost + // admissions. Wall-clock timestamps cannot prove this after clock changes. + if (!body || typeof body !== 'object' || Array.isArray(body) || body.id !== undefined) + throw fail(400,'A fresh conversation identity is required after mobile history recovery') + if ((state.recoverySessions?.length ?? 0) >= 10000) throw fail(507,'Mobile recovery conversation history is full') + const id = 'ses_' + randomUUID().replaceAll('-','') + const result = await request(raw,'POST',{...body,id},signal) + if (result.data?.data?.id !== id) throw fail(502,'Desktop did not create the requested conversation') + state.recoverySessions = [...(state.recoverySessions ?? []),id] + await save() + return result + } const match = /^\/api\/session\/([A-Za-z0-9_-]+)\/(message|prompt|interrupt|permission|question|model|agent)$/.exec(path) if (method==='GET' && path==='/api/session') { const result = await request(raw,'GET',undefined,signal) @@ -116,6 +130,16 @@ export async function createDesktopAPI({ request, storePath }) { } throw fail(409,'Desktop delivery is unconfirmed. Check the conversation on your computer before continuing.') } + if (recovered && !state.recoverySessions?.includes(id)) { + // With the old journal lost, absence in Desktop is not proof that an + // old request was never forwarded. Only our durably recorded fresh + // conversation identities can submit without that lost history. + if (await stored(id,body,info.directory,signal)) { + state.admissions[key]={hash,state:'admitted'}; await save() + return {data:{data:{id:body.id}}} + } + throw fail(409,'Mobile delivery history was recovered. Check this conversation on your computer; start a new conversation on your phone to send safely.') + } if(Object.keys(state.admissions).length>=10000) throw fail(507,'Mobile admission history is full') const operation=(async()=>{ const selection=state.selections[id], model=selection?.model ?? info.model diff --git a/desktop-connector/test/desktop-access.test.mjs b/desktop-connector/test/desktop-access.test.mjs index be9a848..49eb578 100644 --- a/desktop-connector/test/desktop-access.test.mjs +++ b/desktop-connector/test/desktop-access.test.mjs @@ -1,12 +1,90 @@ import { test } from "node:test" import assert from "node:assert/strict" -import { mkdtemp, rm, mkdir, writeFile, readFile, readdir } from "node:fs/promises" +import { mkdtemp, rm, mkdir, writeFile, readFile, readdir, stat } from "node:fs/promises" import { join } from "node:path" import { tmpdir } from "node:os" import { createMobileAccess } from "../src/desktop-access.mjs" import QRCode from "qrcode" import { PNG } from "pngjs" +for (const damaged of ['{"admissions":', '{"admissions":null,"selections":{}}', '{"admissions":{},"selections":[]}']) { + test(`explicit reset quarantines invalid admissions and allows pairing: ${damaged}`, async (t) => { + const directory = await mkdtemp(join(tmpdir(), "bc-admissions-reset-")) + t.after(() => rm(directory, { recursive: true, force: true })) + const root = join(directory, "mobile-access") + await mkdir(root) + await writeFile(join(root, "admissions.json"), damaged) + const options = { + userDataPath: directory, + getConnection: () => ({ url: "http://127.0.0.1:4096", password: "private" }), + removeTunnel: async () => {}, + tunnelFactory: async () => ({ origin: "https://pc.tunnel.bharatcode.ai", events: (async function* () {})(), close: async () => {} }), + } + const access = await createMobileAccess(options) + t.after(() => access.close()) + await assert.rejects(access.action({ type: "pair" })) + assert.equal(access.snapshot().errorKey, "mobile.error.storage") + assert.equal(await readFile(join(root, "admissions.json"), "utf8"), damaged) + await access.action({ type: "reset" }) + const backups = (await readdir(root)).filter(name => name.startsWith("admissions.json.corrupt-")) + assert.equal(backups.length, 1) + assert.equal(await readFile(join(root, backups[0]), "utf8"), damaged) + if (process.platform !== "win32") assert.equal((await stat(join(root, backups[0]))).mode & 0o777, 0o600) + const fence = JSON.parse(await readFile(join(root, "admissions.json.recovery"), "utf8")) + assert.ok(Number.isSafeInteger(fence.before)) + await access.action({ type: "pair" }) + assert.equal(access.snapshot().status, "connected") + await access.action({ type: "reset" }) + assert.deepEqual(JSON.parse(await readFile(join(root, "admissions.json.recovery"), "utf8")), fence) + await access.close() + const reopened = await createMobileAccess(options) + t.after(() => reopened.close()) + await reopened.action({ type: "pair" }) + assert.equal(reopened.snapshot().status, "connected") + }) +} + +test("reset preserves a healthy forwarding journal byte-for-byte", async (t) => { + const directory = await mkdtemp(join(tmpdir(), "bc-admissions-healthy-")) + t.after(() => rm(directory, { recursive: true, force: true })) + const root = join(directory, "mobile-access") + await mkdir(root) + const original = JSON.stringify({ admissions: { "ses_one.msg_one": { hash: "a".repeat(64), state: "forwarding" } }, selections: {} }) + await writeFile(join(root, "admissions.json"), original) + const access = await createMobileAccess({ userDataPath: directory, getConnection: () => undefined, removeTunnel: async () => {} }) + t.after(() => access.close()) + await access.action({ type: "reset" }) + assert.equal(await readFile(join(root, "admissions.json"), "utf8"), original) + assert.equal((await readdir(root)).some(name => name.includes(".corrupt-")), false) +}) + +test("reset reports storage failure instead of success when admissions cannot be read", async (t) => { + const directory = await mkdtemp(join(tmpdir(), "bc-admissions-unreadable-")) + t.after(() => rm(directory, { recursive: true, force: true })) + const root = join(directory, "mobile-access") + await mkdir(join(root, "admissions.json"), { recursive: true }) + const access = await createMobileAccess({ userDataPath: directory, getConnection: () => undefined, removeTunnel: async () => {} }) + t.after(() => access.close()) + await assert.rejects(access.action({ type: "reset" })) + assert.equal(access.snapshot().status, "error") + assert.equal(access.snapshot().errorKey, "mobile.error.storage") + assert.equal((await readdir(root)).some(name => name.includes(".corrupt-")), false) +}) + +test("a second damaged journal advances the recovery fence to cover newer uncertain deliveries", async (t) => { + const directory = await mkdtemp(join(tmpdir(), "bc-admissions-second-reset-")) + t.after(() => rm(directory, { recursive: true, force: true })) + const root = join(directory, "mobile-access") + await mkdir(root) + await writeFile(join(root, "admissions.json.recovery"), '{"before":1000}') + await writeFile(join(root, "admissions.json"), '{"admissions":') + const access = await createMobileAccess({ userDataPath: directory, getConnection: () => undefined, removeTunnel: async () => {} }) + t.after(() => access.close()) + const started = Date.now() + await access.action({ type: "reset" }) + assert.ok(JSON.parse(await readFile(join(root, "admissions.json.recovery"), "utf8")).before >= started) +}) + test("pairing QR preserves a four-module clear border for physical scanners", async (t) => { const directory = await mkdtemp(join(tmpdir(), "bc-access-qr-margin-")) t.after(() => rm(directory, { recursive: true, force: true })) diff --git a/desktop-connector/test/desktop-api.test.mjs b/desktop-connector/test/desktop-api.test.mjs index 2e2f325..3fdf9b4 100644 --- a/desktop-connector/test/desktop-api.test.mjs +++ b/desktop-connector/test/desktop-api.test.mjs @@ -1,10 +1,64 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { mkdtemp, rm } from 'node:fs/promises' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { tmpdir } from 'node:os' import { createDesktopAPI } from '../src/desktop-api.mjs' +for (const created of [undefined, 999, 1000, Date.now() + 3_600_000]) { + test(`recovery fence blocks unknown old delivery across restart, created=${created}`, async t => { + const root=await mkdtemp(join(tmpdir(),'bc-recovery-fence-'));t.after(()=>rm(root,{recursive:true,force:true})) + const storePath=join(root,'admissions.json') + // Simulate a crash after quarantine but before the replacement journal was written. + await writeFile(storePath+'.recovery',JSON.stringify({before:1000})) + let submissions=0 + const request=async path=>{ + if(path==='/session/ses_one')return {data:{directory:'/work',time:{created}}} + if(path.includes('/prompt_async')){submissions++;return {data:undefined}} + throw Object.assign(new Error('not stored'),{status:404}) + } + for(let i=0;i<2;i++){ + const api=await createDesktopAPI({request,storePath}) + await assert.rejects(api.handle('POST','/api/session/ses_one/prompt',{id:'msg_one',prompt:{text:'Do not duplicate'}}),/recovered.*new conversation/i) + } + assert.equal(submissions,0) + }) +} + +test('recovered history acknowledges stored messages and permits only connector-created conversations',async t=>{ + const root=await mkdtemp(join(tmpdir(),'bc-recovery-proof-'));t.after(()=>rm(root,{recursive:true,force:true})) + const storePath=join(root,'admissions.json') + await writeFile(storePath+'.recovery',JSON.stringify({before:1000})) + let submissions=0, fresh + const request=async(path,method,body)=>{ + if(path==='/api/session' && method==='POST'){fresh=body.id;return {data:{data:{id:fresh}}}} + if(path==='/session/ses_old')return {data:{directory:'/work',time:{created:900}}} + if(path==='/session/'+fresh)return {data:{directory:'/work',time:{created:0}}} + if(path.includes('/prompt_async')){submissions++;return {data:undefined}} + return {data:{info:{id:'msg_one',role:'user'},parts:[{type:'text',text:'Only once'}]}} + } + const api=await createDesktopAPI({request,storePath}) + const prompt={id:'msg_one',prompt:{text:'Only once'}} + await api.handle('POST','/api/session/ses_old/prompt',prompt) + assert.equal(submissions,0) + await assert.rejects(api.handle('POST','/api/session/ses_old/prompt',{...prompt,prompt:{text:'Different'}}),/identity/) + await assert.rejects(api.handle('POST','/api/session',{id:'ses_old'}),/identity/i) + const created=await api.handle('POST','/api/session',{location:{directory:'/work'}}) + assert.match(created.data.data.id,/^ses_[a-f0-9]{32}$/) + await api.handle('POST',`/api/session/${fresh}/prompt`,prompt) + assert.equal(submissions,1) + const reopened=await createDesktopAPI({request,storePath}) + await reopened.handle('POST',`/api/session/${fresh}/prompt`,prompt) + assert.equal(submissions,1) +}) + +test('malformed recovery fence fails closed instead of forgetting prior quarantine',async t=>{ + const root=await mkdtemp(join(tmpdir(),'bc-recovery-invalid-'));t.after(()=>rm(root,{recursive:true,force:true})) + const storePath=join(root,'admissions.json') + await writeFile(storePath+'.recovery','{"before":null}') + await assert.rejects(createDesktopAPI({request:()=>{throw new Error('must not contact sidecar')},storePath}),/recovery/i) +}) + test('stored admission is acknowledged while the upstream async HTTP acknowledgement is still pending', async t => { const root = await mkdtemp(join(tmpdir(),'bc-admission-early-')) t.after(()=>rm(root,{recursive:true,force:true})) diff --git a/desktop-connector/test/recovery.test.mjs b/desktop-connector/test/recovery.test.mjs new file mode 100644 index 0000000..7a05958 --- /dev/null +++ b/desktop-connector/test/recovery.test.mjs @@ -0,0 +1,98 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { createServer } from "node:http" +import { once } from "node:events" +import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { createMobileAccess } from "../src/desktop-access.mjs" + +test("corrupt journal recovery pairs a phone and safely handles real HTTP prompts through restart", async t => { + const directory = await mkdtemp(join(tmpdir(), "bc-recovery-http-")) + t.after(() => rm(directory, { recursive: true, force: true })) + const root = join(directory, "mobile-access") + await mkdir(root) + await writeFile(join(root, "admissions.json"), '{"admissions":') + let fresh, forwarded = 0, target + const messages = new Map() + const sidecar = createServer(async (req, res) => { + assert.equal(req.headers.authorization, "Basic " + Buffer.from("opencode:private").toString("base64")) + res.setHeader("content-type", "application/json") + const path = new URL(req.url, "http://localhost").pathname + if (path === "/api/session" && req.method === "POST") { + let raw = "" + for await (const chunk of req) raw += chunk + fresh = JSON.parse(raw).id + res.end(JSON.stringify({ data: { id: fresh } })) + return + } + if (path === "/session/ses_old" || path === "/session/" + fresh) { + res.end(JSON.stringify({ directory: "/fixture", time: { created: Date.now() + 3_600_000 } })) + return + } + if (path.endsWith("/prompt_async")) { + let raw = "" + for await (const chunk of req) raw += chunk + const body = JSON.parse(raw) + forwarded++ + messages.set(path.split("/")[2] + "." + body.messageID, body.parts) + res.writeHead(204).end() + return + } + const match = /^\/session\/(ses_\w+)\/message\/(msg_\w+)$/.exec(path) + const parts = match && messages.get(match[1] + "." + match[2]) + if (parts) res.end(JSON.stringify({ info: { id: match[2], role: "user" }, parts })) + else res.writeHead(404).end("{}") + }) + sidecar.listen(0, "127.0.0.1") + await once(sidecar, "listening") + t.after(() => { sidecar.closeAllConnections(); return new Promise(resolve => sidecar.close(resolve)) }) + const options = { + userDataPath: directory, + getConnection: () => ({ url: `http://127.0.0.1:${sidecar.address().port}`, password: "private" }), + removeTunnel: async () => {}, + tunnelFactory: async options => { + target = options.target + return { origin: "https://pc.tunnel.bharatcode.ai", events: (async function* () {})(), close: async () => {} } + }, + } + let access = await createMobileAccess(options) + t.after(() => access.close()) + await assert.rejects(access.action({ type: "pair" })) + await access.action({ type: "reset" }) + const pair = async () => { + await access.action({ type: "pair" }) + const link = new URL(access.snapshot().pairing) + const post = (path, body) => fetch(target + path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }) + const claim = await (await post("/mobile/v1/claim", { desktopID: link.searchParams.get("desktopID"), deviceName: "Fixture phone" })).json() + await access.action({ type: "approve", id: claim.claimID }) + return (await (await post("/mobile/v1/claim/status", claim)).json()).token + } + let token = await pair() + const send = (session, text = "Only once") => fetch(target + `/api/session/${session}/prompt`, { + method: "POST", headers: { authorization: "Bearer " + token, "content-type": "application/json" }, + body: JSON.stringify({ id: "msg_one", prompt: { text } }), + }) + const blocked = await send("ses_old") + assert.equal(blocked.status, 409) + assert.match(await blocked.text(), /recovered.*new conversation/) + assert.equal(forwarded, 0) + const created = await fetch(target + "/api/session", { + method: "POST", headers: { authorization: "Bearer " + token, "content-type": "application/json" }, + body: JSON.stringify({ location: { directory: "/fixture" } }), + }) + assert.equal(created.status, 200) + fresh = (await created.json()).data.id + const accepted = await send(fresh) + assert.equal(accepted.status, 200, await accepted.text()) + assert.equal(forwarded, 1) + // Reset must not forget the healthy admission, even after restarting access. + await access.action({ type: "reset" }) + await access.close() + access = await createMobileAccess(options) + token = await pair() + assert.equal((await send(fresh)).status, 200) + assert.equal((await send(fresh, "Changed prompt")).status, 409) + assert.equal((await send("ses_old")).status, 409) + assert.equal(forwarded, 1) +}) diff --git a/desktop-integration/test_apply.py b/desktop-integration/test_apply.py index aebcfff..a7297dc 100644 --- a/desktop-integration/test_apply.py +++ b/desktop-integration/test_apply.py @@ -45,6 +45,13 @@ def test_tunnel_is_main_owned_and_ipc_is_trusted(self): self.assertIn('window.api.mobileAccess(input)', renderer) self.assertNotIn('@opentunnel/client', renderer) + def test_generated_connector_matches_the_mobile_source(self): + for folder in ("src", "test"): + for source in (ROOT / "desktop-connector" / folder).rglob("*"): + if source.is_file(): + target = self.target / "packages/mobile-connector" / folder / source.relative_to(ROOT / "desktop-connector" / folder) + self.assertEqual(target.read_bytes(), source.read_bytes(), str(source)) + if __name__ == "__main__": unittest.main() diff --git a/docs/superpowers/plans/2026-10-30-review-followups.md b/docs/superpowers/plans/2026-10-30-review-followups.md new file mode 100644 index 0000000..4e0597c --- /dev/null +++ b/docs/superpowers/plans/2026-10-30-review-followups.md @@ -0,0 +1,32 @@ +# BharatCode review follow-ups + +**Goal:** Address Shrey's Desktop #60/#59 and Mobile #1 feedback without publishing or replying before user approval. + +**Architecture:** Mobile owns the connector source; synchronize the Desktop copy and verify the pinned generator. Explicit reset quarantines invalid admissions state and persists a recovery marker before moving any journal. Existing conversations cannot forward unknown prompts after recovery; matching stored messages can reconcile. New conversations created through the connector use random identities recorded durably in the replacement journal. Healthy admission journals survive reset. Support #59 remains the sole exporter, with adversarial fixtures adapted from #67/#68 and attribution. + +**Constraints:** Existing Desktop sidecar and hosted OpenTunnel only. No new relay or connection flow. Source verification is separate from packaged/native/physical verification. No PR, push, or posted reply before approval of exact proposed text. + +## Recovery +- [x] Reproduce invalid JSON and invalid-shaped journals; confirm storage reporting and failed pairing before reset. +- [x] Add regressions for quarantine, restart, crash between cutoff and replacement, reset failure, and healthy ambiguous-delivery retention. +- [x] Implement strict journal validation and explicit recovery in desktop-connector/src/admissions.mjs and desktop-api.mjs; wire reset/reporting in desktop-access.mjs. +- [x] Verify exact stored retry, blocked unknown/mismatched old prompts, normal new-session submission, and repeated reset/restart. +- [x] Synchronize Desktop source/test trees; prove parity, verify desktop-integration/apply.py on its documented pin, and run connector/Desktop/settings checks on current dev. + +## Support bundle +- [x] Inspect latest #67 code and fixtures; document differences from #59 (the latest #67 still exports raw redacted logs and sanitized config). +- [x] Reuse relevant privacy cases as actual-archive tests for #59, preserving Krish6115/prashilthul attribution. +- [x] Validate OPENCODE_DB, live WAL immutability, both Settings layouts, and both legacy/new IPC routes on current dev. +- [x] Run affected package tests/typechecks; reproduce and report any baseline failures. + +## Review and handoff +- [x] Obtain a fresh independent source review, resolve material findings, and record verification limits. +- [x] Prepare exact PR/update/reply drafts with evidence; wait for user approval before external writes. + +## Review decision + +The fresh independent source review reproduced a backward-clock replay risk in the initial timestamp-based design. The fix rejects caller-selected identities during recovery and durably records connector-generated random session identities. Old sessions are readable, and exact stored retries reconcile without forwarding; fresh sends require a new conversation created on the phone. This is intentionally conservative when the delivery journal is unrecoverable. The regression failed before the fix and passes afterward. No user approval is needed for local implementation; external PRs, pushes, and replies remain pending approval. + +## Verification record + +See `docs/verification/admissions-recovery-2026-10-30.md` and Desktop `docs/support-review-2026-10-30.md`. Full app suites retain only the independently reproduced current-dev Arabic parity failure. Publication remains awaiting user approval of exact drafts; no PR, push, reply, installation, or deployment has occurred. In response to the request for stronger verification, a delegated gpt-6-luna/max worker built an isolated combined Linux package and verified actual export and Reset IPC. See the verification document for exact scope and remaining platform/device limits. diff --git a/docs/verification/admissions-recovery-2026-10-30.md b/docs/verification/admissions-recovery-2026-10-30.md new file mode 100644 index 0000000..68ec8b9 --- /dev/null +++ b/docs/verification/admissions-recovery-2026-10-30.md @@ -0,0 +1,49 @@ +# Admissions recovery review — 2026-10-30 + +Addresses Shrey's Desktop #60 review and the follow-up on merged Mobile #1. + +## Behavior + +- Invalid JSON and invalid journal structures fail closed and report mobile storage failure. Pairing failure preserves the original file. +- Explicit Reset stops access, privately quarantines invalid admissions bytes, revokes device access, and removes tunnel identity. A durable recovery marker is written before quarantine, so a process crash cannot silently reopen without lost-history protection. A failed storage operation rejects Reset and reports an error. +- Healthy journals are never removed by Reset; unresolved forwarding records remain unresolved. +- After corruption recovery, old conversations remain readable. An exact matching stored user message can reconcile without sending. Unknown or mismatched old prompts are rejected with instructions to inspect Desktop and start a new conversation on the phone. +- New conversations created through the connector use a fresh random identity, reject caller-selected IDs, and are recorded durably before they can send. This avoids relying on wall clocks to distinguish old/new conversations. Restart and healthy Reset retain that proof; recovering a newly corrupt journal loses the allowlist and blocks those now-uncertain sessions too. +- The Mobile source and Desktop generated source/tests match byte-for-byte. The pinned integration generator is tested independently on its documented source pin; the Desktop PR follow-up is also merged locally with current dev `f8c7feacae8a8cf89d9b9cb05a780e306f845d7d`. + +## Evidence + +The original tests failed before the recovery implementation. A fresh source reviewer then reproduced a backward-clock duplicate-delivery risk in the first timestamp design. The clock regression failed before replacing that design with durable fresh session identities, then passed afterward. + +| Check | Result | +| --- | --- | +| Mobile connector suite | 65 passed | +| Mirrored Desktop connector suite | 65 passed | +| Real local HTTP pairing/recovery/prompt/reset/restart regression | Passed inside both connector suites | +| Mobile Jest / security / TypeScript / lint | 76 / 15 passed; typecheck and lint passed | +| Desktop source suite on recovery branch | 170 passed | +| Recovery-branch server suite | 324 passed; 2 existing skips | +| Actual Desktop HTTP routes/database + local test-model recovery | Passed; two intended prompts, two model calls, two upstream submissions; retries did not add submissions | +| App unit suite on recovery branch | 748 passed; 1 baseline Arabic UI parity failure | +| App browser-condition suite | 55 passed | +| Mobile Settings Chromium cases | 5 passed | +| Integration generator / host browser helper | 3 / 3 passed | +| Desktop/app/e2e/server typechecks | Passed | + +The app unit failure is `i18n parity > non-English locales have every English key and required plural variants`: four missing `ui.message.outputLimit*` keys in Arabic. It was reproduced from the current dev dictionaries/test in an isolated temporary directory. This change does not alter the parity test or UI locale dictionaries. + +These checks establish source and local fixture behavior. They do not establish installed Electron, physical Android/iOS, native Windows/macOS, store signing, or release acceptance. No installation, deployment, push, PR, or review reply was made for this follow-up. Targeted packaged Linux checks are recorded below. + +## Additional actual-server verification + +The Desktop `packages/opencode/test/server/httpapi-session.test.ts` regression uses the actual HTTP routes and database with the mirrored connector and a local deterministic model server. It checks an original prompt/reply, corrupt journal rejection, explicit recovery, reconciliation of the original stored retry, 409 rejection of unknown old delivery, connector-generated session creation, the new conversation reply, and Reset/reopen retry. Both upstream submission counts and model-call counts remain exactly two for the two intended prompts. This complements the gateway pairing fixture; it does not substitute for an installed Electron/phone test. The complete recovery-branch server suite passed 324 tests with two existing skips. + +## Targeted packaged Linux qualification + +A delegated gpt-6-luna/max worker built a combined temporary tree from support `c76b871` plus an uncommitted, conflict-free merge of recovery `22f91ba`. Later recovery `4d6ac75` adds a server test/documentation only; recovery product source is unchanged. The unpacked Linux package is `/tmp/bc-desktop-qualification.F13MBy/packages/desktop/dist/linux-unpacked`. Its `resources/app.asar` SHA-256 is `d7a49b14cc36e6e029db41cbdcb8b409b2862e17c4e6b27a006d1579208b15b6` (independently checked by the primary agent). The bundled CLI version smoke test passed. + +The packaged Electron 42.3.3 app launched with `isPackaged=true`, version 1.15.46, inside isolated user/network namespaces and Xvfb with a temporary home/profile and no provider credentials. From the top-level renderer, the shipped preload's `exportDebugLogs()` called the registered main IPC and produced a mode-0600 ZIP (705–707 bytes across repeated runs) containing only `support-bundle.json`. Its nine allowlisted top-level keys were `formatVersion`, `generated`, `version`, `os`, `channel`, `auth`, `config`, `database`, and `errors`; auth was signed out. The Settings screen was behind sign-in, so this verifies the packaged export IPC path alongside the separate Chromium Settings UI tests. + +A synthetic corrupt journal was then written under isolated userData. The renderer's shipped `mobileAccess({type:'reset'})` IPC preserved the exact damaged bytes in one `.corrupt-*` backup, removed the active damaged journal, persisted a `.recovery` marker, and returned off/disabled without an error. Backup and marker modes were 0600. No tunnel was enabled, and the namespace had loopback only. These checks do not establish real hosted-tunnel connectivity from the package, physical-phone pairing, native Windows/macOS behavior, signing, or release acceptance. The package was not installed or published. + +Detailed delegated report and retained harness: `/tmp/bc-desktop-qualification.F13MBy/LOCAL_LINUX_QUALIFICATION.md`. The combined staged source tree was `39464cee6416c16391412040016f27ee55bc77ef`. diff --git a/package-lock.json b/package-lock.json index a03bab8..2102f98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,26 +9,26 @@ "version": "0.2.0", "dependencies": { "@expo/vector-icons": "^15.0.2", - "expo": "58.0.21", + "expo": "58.0.22", "expo-asset": "~58.0.19", "expo-camera": "~58.0.14", "expo-clipboard": "~58.0.4", "expo-constants": "~58.0.14", "expo-crypto": "~58.0.7", - "expo-dev-client": "~58.0.17", + "expo-dev-client": "~58.0.18", "expo-font": "~58.0.11", "expo-linking": "~58.0.15", - "expo-router": "~58.0.28", + "expo-router": "~58.0.29", "expo-secure-store": "~58.0.2", - "expo-sqlite": "~58.0.14", + "expo-sqlite": "~58.0.15", "expo-status-bar": "~58.0.4", "expo-system-ui": "~58.0.7", "react": "19.3.0", "react-dom": "19.3.0", - "react-native": "0.88.0", + "react-native": "0.88.1", "react-native-markdown-display": "^7.0.2", "react-native-safe-area-context": "~5.10.1", - "react-native-screens": "~4.30.0", + "react-native-screens": "~4.30.1", "react-native-svg": "15.16.0", "react-native-web": "^0.21.4" }, @@ -1777,9 +1777,9 @@ } }, "node_modules/@expo/config": { - "version": "58.0.9", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-58.0.9.tgz", - "integrity": "sha512-rN21dSqt6jfzpnVaBwCZVIvRFxoWEit+11AY+dSyaVkkwFWnVga0KOkFTQydtYgIZmP/vdm4QJhKeQkoUf4oMQ==", + "version": "58.0.10", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-58.0.10.tgz", + "integrity": "sha512-EC2BKXf9nIVrsvpwMOIUw8fyJPLU4moeFFiWhQivpVX9Ta9EUfXjpeoJHhv8Nt7+lx9RfRfZy9sgMsSUVMuRBw==", "license": "MIT", "dependencies": { "@expo/config-plugins": "~58.0.11", @@ -1787,7 +1787,6 @@ "@expo/json-file": "^11.2.1", "@expo/platforms": "^58.0.1", "@expo/require-utils": "^58.0.5", - "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-workspace-root": "^2.0.0", @@ -1867,9 +1866,9 @@ } }, "node_modules/@expo/fingerprint": { - "version": "0.21.7", - "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.21.7.tgz", - "integrity": "sha512-rE9QN4TXGZxS3PC1yW77D1B6JNnuv3n7hXVUZxFEDDndPKFnQPC0XkqFyIz21oXbtrvThROCbV9CDVJiycCntg==", + "version": "0.21.8", + "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.21.8.tgz", + "integrity": "sha512-ufFLmY4Lyf8POxZgaWsZGq3p16fWJUCrkkm9E0HM9mw74vGQkRu5sKUIPde+ROGXv2cWLsrudJkUZLU5l6Clvw==", "license": "MIT", "dependencies": { "@expo/env": "^2.5.3", @@ -1923,19 +1922,19 @@ } }, "node_modules/@expo/local-build-cache-provider": { - "version": "58.0.9", - "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-58.0.9.tgz", - "integrity": "sha512-C1QZ4GDljraOtARoZPvImxA9gKLd9kb/w1fRpc+x2sCZ1qXxWmqoVh+ISuwe6niB1BvzBEDA8KH0QQJj3r9nLQ==", + "version": "58.0.10", + "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-58.0.10.tgz", + "integrity": "sha512-0O0EF9V2rEgIPYJI+4vYLEKQn3xUt2js4L2L6DLE9PrElozL7cvHJKPkfEpYSxo5NNJhyGfVxBdazd0vE4Xg3w==", "license": "MIT", "dependencies": { - "@expo/config": "~58.0.9", + "@expo/config": "~58.0.10", "chalk": "^4.1.2" } }, "node_modules/@expo/log-box": { - "version": "58.0.13", - "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-58.0.13.tgz", - "integrity": "sha512-Cl9qc77nWjQVivUI5/OCPSEP8s20EtbK3Ivy5ayy+uHO0Cf+XatatWfTvKiHO38VOpT2ywlROzZb4XZyxZUZ1Q==", + "version": "58.0.14", + "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-58.0.14.tgz", + "integrity": "sha512-0aXQIWgTBvtzlzzqS7+oDx+DeGyX961Xxr53HzWWiyxNM9PkK7QXVV2gVeuvAYL0RIKUL1hNC0ZCz7sSkYyfww==", "license": "MIT", "dependencies": { "@expo/dom-webview": "^58.0.2", @@ -1984,15 +1983,15 @@ } }, "node_modules/@expo/metro-config": { - "version": "58.0.19", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-58.0.19.tgz", - "integrity": "sha512-z8tOnyiY5jg2sbrCAD60N+G9AX6sZQyMrfrhL4DK4pkwyt5TODkxk61G0Y0k9RidXpvjguqftB7v/w8UK5wCGg==", + "version": "58.0.20", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-58.0.20.tgz", + "integrity": "sha512-M8O9hcpHTYXZjuUO0UzHInbpqKiWVXW/If8ZD3tdg1l01vDjoS6ol5lzP0IJhGY4VeJNJI3U5MsdS7dxx0AlLw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", - "@expo/config": "~58.0.9", + "@expo/config": "~58.0.10", "@expo/env": "~2.5.3", "@expo/json-file": "~11.2.1", "@expo/metro": "~58.0.0", @@ -2027,15 +2026,6 @@ } } }, - "node_modules/@expo/metro-config/node_modules/@react-native/js-polyfills": { - "version": "0.88.1", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.88.1.tgz", - "integrity": "sha512-B62O6zVMYW7kK8dxIuCwpdJ5RYcIvET6ItScm0sfsTibJs+llS0krUVSAOHnEUn3lbFMfw+o+sNdWREOAc6FfQ==", - "license": "MIT", - "engines": { - "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" - } - }, "node_modules/@expo/metro-config/node_modules/picomatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", @@ -2063,19 +2053,19 @@ } }, "node_modules/@expo/metro-runtime": { - "version": "58.0.15", - "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-58.0.15.tgz", - "integrity": "sha512-csALn54hZUkHFM3FofWlU5MQcJB62deCt+JtHOhQogRT+k/VTtTuhLQCrCUOtO2MAUsyhrcRRa6Nh79e1bkvfg==", + "version": "58.0.16", + "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-58.0.16.tgz", + "integrity": "sha512-gE9Vx3NuqWCXcKnk4/fbJdfpkf5ABpdk+76QlzJFZ31R0ke85C7C48xKSXc4fxl+kFY0lAQYICz26107NVroNg==", "license": "MIT", "dependencies": { - "@expo/log-box": "^58.0.13", + "@expo/log-box": "^58.0.14", "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { - "@expo/log-box": "^58.0.13", + "@expo/log-box": "^58.0.14", "expo": "*", "react": "*", "react-dom": "*", @@ -2189,12 +2179,12 @@ } }, "node_modules/@expo/prebuild-config": { - "version": "58.0.17", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-58.0.17.tgz", - "integrity": "sha512-QSMM9YiHQnRENUIgP7wgZt71WKPLUMLz5QFm4KWA2aa20a808F6b+HV/ZzfX47MWWt39RjyT9pUcLv/dgEp0UA==", + "version": "58.0.18", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-58.0.18.tgz", + "integrity": "sha512-xTfSvhBrD2UYwRmpnQCPxY06j37ZMobXiZsdRPsW07vVMHe0ox4OpMbs47qv3h6RG/gfV+uu1RgMqi8d6cxurg==", "license": "MIT", "dependencies": { - "@expo/config": "~58.0.9", + "@expo/config": "~58.0.10", "@expo/config-plugins": "~58.0.11", "@expo/config-types": "^58.0.3", "@expo/image-utils": "^0.12.6", @@ -2206,12 +2196,6 @@ "semver": "^7.7.4" } }, - "node_modules/@expo/prebuild-config/node_modules/@react-native/normalize-colors": { - "version": "0.88.1", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.88.1.tgz", - "integrity": "sha512-2SAKiLyZRf+peBLkhhJWGY5vC26w1Uj9QBgUZvjAnOxjgeEKUGBE5riPS0R7+YoqSrRL0w8ucwZB4M+vvy9HMg==", - "license": "MIT" - }, "node_modules/@expo/require-utils": { "version": "58.0.5", "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-58.0.5.tgz", @@ -2232,9 +2216,9 @@ } }, "node_modules/@expo/schema-utils": { - "version": "58.0.2", - "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-58.0.2.tgz", - "integrity": "sha512-w28y8SG+5ZTdDdmH2ZnCQk9wHVwGzI9NqX5V4urJUFEnxbm7I129fJRkb8RIAJmMA1WKU5h3ONmranV473uaXQ==", + "version": "58.0.3", + "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-58.0.3.tgz", + "integrity": "sha512-DwoMEpT3H+r1lU+YUEP10UcUW7sDKmfvgE7oA+zQeuGyi0XxDSDgRZUUZ5nOGp+lymmTicoIUKkoN7rByuI04g==", "license": "MIT" }, "node_modules/@expo/sdk-runtime-versions": { @@ -5157,32 +5141,31 @@ } }, "node_modules/@react-native/asset-utils": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/asset-utils/-/asset-utils-0.88.0.tgz", - "integrity": "sha512-Bj1DxQt5f4OrjdlgSVkZT37Z3hx5hUIN5AUDSqL/e99vgtTcqj5vnm067epZL1EF/QiqsniZO8Q1GSkItOakDg==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/@react-native/asset-utils/-/asset-utils-0.88.1.tgz", + "integrity": "sha512-BLb5FF7NgggGYiCsA7KKkK2qFhn0K+6iGXOI+4Ucd+G7Robzw9Gt4WsWwxLt+Od+G+/hyqfz8ASokyWFZ6VcLw==", "license": "MIT", "engines": { "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" } }, "node_modules/@react-native/babel-plugin-codegen": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.88.0.tgz", - "integrity": "sha512-leGEnCcS7Q0hTxvzvhNtr8zGQpI1gfWqXjzC4UedsFmNMv6kIo4s70o0XbtM30ZXbpfWe4BcSC8NKWuak5sI4A==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.88.1.tgz", + "integrity": "sha512-2r7aeE3dXu3hawHNYHgazWQl9MJNjcnA/gxLApFK2gPMxBCLiQOHVUSaemDQz4mKPs5svulbhtDehxo6LzTzpQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/traverse": "^7.29.0", - "@react-native/codegen": "0.88.0" + "@react-native/codegen": "0.88.1" }, "engines": { "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" } }, "node_modules/@react-native/babel-preset": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.88.0.tgz", - "integrity": "sha512-iMfAHhFGNegnReSY6CYaWHncvyDwHq8olRs0zp9nRRApMAoCiLyfo1dwL/f39qt/MreD6xIqUs9SrTPYWobY6g==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.88.1.tgz", + "integrity": "sha512-tnacL6tECmtFyBf59tI1GuadSvpm/XCcRQ1vMkL3qoQECpgMVdCfhw79A6l7iZdKM78TcZ0xKV98kZlERydwCg==", "license": "MIT", "peer": true, "dependencies": { @@ -5215,7 +5198,7 @@ "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@react-native/babel-plugin-codegen": "0.88.0", + "@react-native/babel-plugin-codegen": "0.88.1", "babel-plugin-transform-flow-enums": "^0.0.2", "flow-parser": "0.327.0", "react-refresh": "^0.14.0" @@ -5251,9 +5234,9 @@ } }, "node_modules/@react-native/codegen": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.88.0.tgz", - "integrity": "sha512-SAGg+XfzsrPiYTscbKa9bo2/H2ei2TjyxW8RTMq2Ps661kRK3AnmpMGVfrEFshEMr3FGEbuQIvj86X98kaXlRA==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.88.1.tgz", + "integrity": "sha512-URPgPhnsxqeQSLyTRVzhZZ8iNXtZdR1Jop6lqaQacymvl3byghXisNIRR1YyG6PbXUfzPan5Jmz/kPhUUsZ2Yw==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -5293,13 +5276,13 @@ } }, "node_modules/@react-native/community-cli-plugin": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.88.0.tgz", - "integrity": "sha512-WrQdXbjVt1gwAay8ZzYtpOi9ly99ksJ0VMYFdVcMe6nq3vtiRgqKRpHgCdN6ySIbtr5o+aJ+Vr7OTDY98ec/Ow==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.88.1.tgz", + "integrity": "sha512-L2CQvzsOMz4oof6AVLSzesFL1Q92nrtXpK0uQH2VwdOmzzgkiHql8WFQSo9VLvMFGJDg8+G6enrHH573ruqqWA==", "license": "MIT", "dependencies": { - "@react-native/asset-utils": "0.88.0", - "@react-native/dev-middleware": "0.88.0", + "@react-native/asset-utils": "0.88.1", + "@react-native/dev-middleware": "0.88.1", "commander": "^12.0.0", "debug": "^4.4.0", "invariant": "^2.2.4", @@ -5311,7 +5294,7 @@ }, "peerDependencies": { "@react-native-community/cli": "*", - "@react-native/metro-config": "0.88.0" + "@react-native/metro-config": "0.88.1" }, "peerDependenciesMeta": { "@react-native-community/cli": { @@ -5332,18 +5315,18 @@ } }, "node_modules/@react-native/debugger-frontend": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.88.0.tgz", - "integrity": "sha512-v5cnBaaWhDRPcQv8EC20zj53jOAKH7u2wnzzXS4JTNu+diPC+cA2+kC7rq3eKI5vGhlAcG2kDPoViRuC0AY0oA==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.88.1.tgz", + "integrity": "sha512-4ZjH2093hZWWgsYs5ExqjzJX5uCS4R5yZa17eRHsQClUc6RjlV61kLdIk722sQReLL+NYtBn2RJtdcQtLCwiug==", "license": "BSD-3-Clause", "engines": { "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" } }, "node_modules/@react-native/debugger-shell": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.88.0.tgz", - "integrity": "sha512-p82zT8ZKNx7nIm/8zf8pi55sxm5KQBm8td47kzoJysTdqHLyPsRMUvDXtvCJIWadHSv7RJ6t/yiCc+IoImLBtw==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.88.1.tgz", + "integrity": "sha512-vrOLlxkp/417qcSUiS3x3gmdoQHYA/26oEKIWPB4EekJG0uJxKXsMR0tHIDiJWO0JH/2BO3M+507DjR1U74jQg==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6", @@ -5355,14 +5338,14 @@ } }, "node_modules/@react-native/dev-middleware": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.88.0.tgz", - "integrity": "sha512-sHLxVHOwfkF672ZzMepNh4d3IKbrZy3qXDKWSwN5LC5VJkzzZ5FdmSPP4N/CCn2pcKJ4oSqvojXwN4De3Xk92A==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.88.1.tgz", + "integrity": "sha512-0smOWE3NCdEYUwPiEdiwVXjTdvXcwSejviCM0YZB0Dj6HaQ+uu4hpSrvgERDevr5dvtSaqr8N4TdYZCLXiVgFw==", "license": "MIT", "dependencies": { "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.88.0", - "@react-native/debugger-shell": "0.88.0", + "@react-native/debugger-frontend": "0.88.1", + "@react-native/debugger-shell": "0.88.1", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", @@ -5376,9 +5359,9 @@ } }, "node_modules/@react-native/gradle-plugin": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.88.0.tgz", - "integrity": "sha512-4jPSKslOfJj8NKY/jv9qmWkxZYmqPyamoS3Vs0JOFxw9t4PM6L/ea0fUsjNV0LxC9zJlvWilPJOCTOf7h8wlyw==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.88.1.tgz", + "integrity": "sha512-zalxoJJ/m5kxLnBy9I1zYrx4jaPxPXdys8DeROvrSOa3pSQkTwVVXRZ4FkXCdGodVA9KR2WitplDo/URNjyvFA==", "license": "MIT", "engines": { "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" @@ -5405,36 +5388,24 @@ "react": "^19.2.3" } }, - "node_modules/@react-native/jest-preset/node_modules/@react-native/js-polyfills": { + "node_modules/@react-native/js-polyfills": { "version": "0.88.1", "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.88.1.tgz", "integrity": "sha512-B62O6zVMYW7kK8dxIuCwpdJ5RYcIvET6ItScm0sfsTibJs+llS0krUVSAOHnEUn3lbFMfw+o+sNdWREOAc6FfQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" - } - }, - "node_modules/@react-native/js-polyfills": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.88.0.tgz", - "integrity": "sha512-Iq6v4+EWdOLv0M1+BpADQJBVfInl6FkABXCIBSi0FbvHahEYT+Y4UJ7Cm96p18gbEN1VKzqz3nbBbwLA6bx0rQ==", "license": "MIT", - "peer": true, "engines": { "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" } }, "node_modules/@react-native/metro-babel-transformer": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.88.0.tgz", - "integrity": "sha512-xsUfzXrqKXeD7sBja7hkyBG32AGIwWJ3IEZ+UcNMRTR/sucnXXZ6Do40qXoeY52YC8b9//J+p51JIp2CC0DZKA==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.88.1.tgz", + "integrity": "sha512-74bvSM4lVZcv+M777/uYW2Nzs6Ga5/F0Ncn9dmeXyUPFMkJO3Ys5v9YlhI//AQ2+0nxojYYFVS/rlAjOkVjtHA==", "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.25.2", - "@react-native/babel-preset": "0.88.0", + "@react-native/babel-preset": "0.88.1", "flow-parser": "0.327.0", "nullthrows": "^1.1.1" }, @@ -5469,14 +5440,14 @@ } }, "node_modules/@react-native/metro-config": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.88.0.tgz", - "integrity": "sha512-iPV2HNXlPs7xQDBJJGIjkH0x2psZwJeRIEpfSqxgqLaO5eEfE2Ccicfz2OBHBcIC5jKGGa4IoI/e26zMNDuTMQ==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.88.1.tgz", + "integrity": "sha512-Q0rKoC55HSOIc4j55lCOHpjIt/JkGQ66u2KsorfTC99LaRIez9UYJ/mhMwhjkZqGHq5Z4iBT6PCd4eQf35zdgQ==", "license": "MIT", "peer": true, "dependencies": { - "@react-native/js-polyfills": "0.88.0", - "@react-native/metro-babel-transformer": "0.88.0", + "@react-native/js-polyfills": "0.88.1", + "@react-native/metro-babel-transformer": "0.88.1", "metro-config": "^0.87.1", "metro-runtime": "^0.87.1" }, @@ -5485,15 +5456,15 @@ } }, "node_modules/@react-native/normalize-colors": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.88.0.tgz", - "integrity": "sha512-eZCu3tGAQCfvMCPQJVY4dmoMX8ZZYBvqIGHX7lwOYyzNK+Dl8e5ky7ZoXub1Rq75oSrQXNNmSP2CDjNNXXZpFQ==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.88.1.tgz", + "integrity": "sha512-2SAKiLyZRf+peBLkhhJWGY5vC26w1Uj9QBgUZvjAnOxjgeEKUGBE5riPS0R7+YoqSrRL0w8ucwZB4M+vvy9HMg==", "license": "MIT" }, "node_modules/@react-native/virtualized-lists": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.88.0.tgz", - "integrity": "sha512-zpZIOAbobdS93edwHkNUdGDzvElNeQJqukRERRPns3/ucPvuMd2G4fYpmVJSVjhxtGCcLLB1PFndLYYfwaYoBQ==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.88.1.tgz", + "integrity": "sha512-Zt+OGvJE6VgmzgemZL8TSA0VomNWpHMNcG0OvLkkBQEGrzY8OdDHRfjJh7S+f+UFJF77kicRC6o6mt3A7ER9lQ==", "license": "MIT", "dependencies": { "invariant": "^2.2.4", @@ -5505,7 +5476,7 @@ "peerDependencies": { "@types/react": "^19.2.0", "react": "*", - "react-native": "0.88.0" + "react-native": "0.88.1" }, "peerDependenciesMeta": { "@types/react": { @@ -6983,9 +6954,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "58.0.22", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-58.0.22.tgz", - "integrity": "sha512-2/pbzxDh4wd4MrBU4lFqFWEfCQooCVjnh6JJZnPLmWisOy5+r4qaOItcH+zaiO4hP2CyYwnyotg1QKhgsmAMEw==", + "version": "58.0.23", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-58.0.23.tgz", + "integrity": "sha512-7wdPu1QSvG7BwZ1UCKFMedJp/I4hLFvVsOjchu78YooHyxoJ/6aU8QzW8uiwZR3ov5Z9OFkRWTTcYyDP9u113w==", "license": "MIT", "dependencies": { "@babel/generator": "^7.20.5", @@ -7034,7 +7005,7 @@ "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", - "expo-widgets": "^58.0.25", + "expo-widgets": "^58.0.26", "react-refresh": ">=0.14.0 <1.0.0" }, "peerDependenciesMeta": { @@ -7049,61 +7020,6 @@ } } }, - "node_modules/babel-preset-expo/node_modules/@react-native/babel-plugin-codegen": { - "version": "0.88.1", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.88.1.tgz", - "integrity": "sha512-2r7aeE3dXu3hawHNYHgazWQl9MJNjcnA/gxLApFK2gPMxBCLiQOHVUSaemDQz4mKPs5svulbhtDehxo6LzTzpQ==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.0", - "@react-native/codegen": "0.88.1" - }, - "engines": { - "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" - } - }, - "node_modules/babel-preset-expo/node_modules/@react-native/codegen": { - "version": "0.88.1", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.88.1.tgz", - "integrity": "sha512-URPgPhnsxqeQSLyTRVzhZZ8iNXtZdR1Jop6lqaQacymvl3byghXisNIRR1YyG6PbXUfzPan5Jmz/kPhUUsZ2Yw==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/parser": "^7.29.0", - "flow-parser": "0.327.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "tinyglobby": "^0.2.15", - "yargs": "^17.6.2" - }, - "engines": { - "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "node_modules/babel-preset-expo/node_modules/flow-estree": { - "version": "0.327.0", - "resolved": "https://registry.npmjs.org/flow-estree/-/flow-estree-0.327.0.tgz", - "integrity": "sha512-f9vK751YUzD0qaHSpKc5/qQM6gqOlgGS+4ZRqTNcZOpVqn3mC55yEQ6dUG2K1eaVm5FbKkz9CFWe6XNvrkQFQg==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/babel-preset-expo/node_modules/flow-parser": { - "version": "0.327.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.327.0.tgz", - "integrity": "sha512-/MYZ5hXWf5ozpbdfUDmazMgQXviLxWEiRLH3MppEGDdW0itRVqmB5UaU7QX+SBCDNiq+gEesSVn8nlxmPDyDkw==", - "license": "MIT", - "dependencies": { - "flow-estree": "0.327.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/babel-preset-jest": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", @@ -7967,6 +7883,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9312,25 +9229,25 @@ } }, "node_modules/expo": { - "version": "58.0.21", - "resolved": "https://registry.npmjs.org/expo/-/expo-58.0.21.tgz", - "integrity": "sha512-2Q8jp0fKLrDMou4aKWZ5V2cCVfcwOScuV8GBMkogiXIv7bCwwN4CIkUIq5QsX7DHXJxbruXLTXEgrp6J+iF4YA==", + "version": "58.0.22", + "resolved": "https://registry.npmjs.org/expo/-/expo-58.0.22.tgz", + "integrity": "sha512-9DmnUN+UxIRRmPLe+t7oOVwdnFzpq/9+1evMVXdFjiC4iwEF7iHCFU0AEm4o2A6HP6A7nTtCox7ADsobYvB/Lw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "^58.1.17", - "@expo/config": "~58.0.9", + "@expo/cli": "^58.1.18", + "@expo/config": "~58.0.10", "@expo/config-plugins": "~58.0.11", "@expo/devtools": "~58.0.2", "@expo/dom-webview": "~58.0.2", - "@expo/fingerprint": "^0.21.7", - "@expo/local-build-cache-provider": "^58.0.9", - "@expo/log-box": "^58.0.13", + "@expo/fingerprint": "^0.21.8", + "@expo/local-build-cache-provider": "^58.0.10", + "@expo/log-box": "^58.0.14", "@expo/log-box-utils": "^58.0.2", "@expo/metro": "~58.0.0", - "@expo/metro-config": "~58.0.19", + "@expo/metro-config": "~58.0.20", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~58.0.22", + "babel-preset-expo": "~58.0.23", "expo-asset": "~58.0.19", "expo-constants": "~58.0.14", "expo-file-system": "~58.0.13", @@ -9338,7 +9255,7 @@ "expo-keep-awake": "~58.0.3", "expo-modules-autolinking": "~58.0.16", "expo-modules-cli": "~0.4.0", - "expo-modules-core": "~58.0.28", + "expo-modules-core": "~58.0.29", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.2.0" @@ -9449,12 +9366,12 @@ } }, "node_modules/expo-dev-client": { - "version": "58.0.17", - "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-58.0.17.tgz", - "integrity": "sha512-9kGl91OWp4oxNuxAxg3C6UxgLFC78JpK4mRmNNo3B3K6OB40rZ5m6PqtqlxcZ1BWvGV8SGGoALxaYrxP68uw5A==", + "version": "58.0.18", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-58.0.18.tgz", + "integrity": "sha512-1oTxhWiZAdFK1zXDWE047cjSw0S3xOATMso8fxXnjK9YvpaG1mGQpCMUVanXvXmiwmqDCHlVXW0myAKBSRzATQ==", "license": "MIT", "dependencies": { - "expo-dev-launcher": "~58.0.17", + "expo-dev-launcher": "~58.0.18", "expo-dev-menu": "~58.0.12", "expo-dev-menu-interface": "~58.0.1", "expo-manifests": "~58.0.1", @@ -9465,12 +9382,12 @@ } }, "node_modules/expo-dev-launcher": { - "version": "58.0.17", - "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-58.0.17.tgz", - "integrity": "sha512-z2/F90do1q7Jgs6hnsY6Xjc1Sd7vKPMvw7Pp5Iv6M2qGHWprfydMCaUcwOkAQFoXqE2rLvd5hC1aSDFbH4oqRQ==", + "version": "58.0.18", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-58.0.18.tgz", + "integrity": "sha512-WjcuYTGn86bwz0qCpPgi/EXHfo9Pj/Z3ynHTaX5h9yaOhe+3p20bm50XVrDIOC+GAbuke8LK8bglVkpkQrIOrg==", "license": "MIT", "dependencies": { - "@expo/schema-utils": "^58.0.2", + "@expo/schema-utils": "^58.0.3", "expo-dev-menu": "~58.0.12", "expo-manifests": "~58.0.1" }, @@ -9599,9 +9516,9 @@ } }, "node_modules/expo-modules-core": { - "version": "58.0.28", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-58.0.28.tgz", - "integrity": "sha512-2FCnT8mIubdQAHVtX9fVMN9qajF7hGDC3CjxxBG0HlyK7NEsxUDKkukiNVzqQpwiI+jJx7ysG0YvATwKKlu1sw==", + "version": "58.0.29", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-58.0.29.tgz", + "integrity": "sha512-S3L38WVxcn1sQfUr9fiUgKri2n4YKdtuToF3r45V7dVIUmHN69Fc7nZPF5BiMtOl/CgPWACicqUZr7jawBxXFg==", "license": "MIT", "dependencies": { "expo-modules-jsi": "~58.0.21", @@ -9641,14 +9558,14 @@ } }, "node_modules/expo-router": { - "version": "58.0.28", - "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-58.0.28.tgz", - "integrity": "sha512-figS9IpRn1h3iYUii1PY7ka/6XSIQ0EBKR5HqKcDl8gadN4hGfS90WFU9gjpquIdM72lS3Dxm6CT2vGQmsiQSg==", + "version": "58.0.29", + "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-58.0.29.tgz", + "integrity": "sha512-WF/40FrYoiFR3AAsbZorbn3CDURb7ne1BFlNZp+v8wE3fzHCIGwSue64Xu3NGrmnZ8/4I22fiYbHSxBFg3pgGQ==", "license": "MIT", "dependencies": { - "@expo/log-box": "^58.0.13", - "@expo/metro-runtime": "^58.0.15", - "@expo/schema-utils": "^58.0.2", + "@expo/log-box": "^58.0.14", + "@expo/metro-runtime": "^58.0.16", + "@expo/schema-utils": "^58.0.3", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-native-masked-view/masked-view": "^0.3.2", @@ -9661,15 +9578,15 @@ "react-fast-compare": "^3.2.2", "react-is": "^19.1.0", "react-native-drawer-layout": "^4.2.12", - "react-native-screens": "^4.30.0 || ^5.0.0-alpha.3", + "react-native-screens": "^4.30.1 || ^5.0.0-alpha.3", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "standard-navigation": "^0.0.5" }, "peerDependencies": { - "@expo/log-box": "^58.0.13", - "@expo/metro-runtime": "^58.0.15", - "@expo/ui": "^58.0.25", + "@expo/log-box": "^58.0.14", + "@expo/metro-runtime": "^58.0.16", + "@expo/ui": "^58.0.26", "@testing-library/react-native": ">= 14.0.0", "expo": "*", "expo-constants": "^58.0.14", @@ -9681,7 +9598,7 @@ "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", - "react-native-screens": "^4.30.0 || ^5.0.0-alpha.3", + "react-native-screens": "^4.30.1 || ^5.0.0-alpha.3", "react-native-web": "*", "react-native-worklets": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4 || ~19.3.0" @@ -9716,12 +9633,6 @@ } } }, - "node_modules/expo-router/node_modules/@react-native/normalize-colors": { - "version": "0.88.1", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.88.1.tgz", - "integrity": "sha512-2SAKiLyZRf+peBLkhhJWGY5vC26w1Uj9QBgUZvjAnOxjgeEKUGBE5riPS0R7+YoqSrRL0w8ucwZB4M+vvy9HMg==", - "license": "MIT" - }, "node_modules/expo-router/node_modules/react-is": { "version": "19.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.1.tgz", @@ -9747,9 +9658,9 @@ } }, "node_modules/expo-sqlite": { - "version": "58.0.14", - "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-58.0.14.tgz", - "integrity": "sha512-OEZsNXYYOzWDkMORprUYRblJVfhO/TKloMWHm5bLzIhYMgs8UcdQZ/RFIMLQFciiaMoYZDsCZbveG47OKcDRXg==", + "version": "58.0.15", + "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-58.0.15.tgz", + "integrity": "sha512-jokbgUHPD1zvpFT/LeJaCoOTsd0QXXWzbthVPCMyMMn+/IZAMqQu79OxYzck5G5wlc+t+vTuEESGMY8r1n3VTA==", "license": "MIT", "dependencies": { "await-lock": "^2.2.2" @@ -9791,12 +9702,6 @@ } } }, - "node_modules/expo-system-ui/node_modules/@react-native/normalize-colors": { - "version": "0.88.1", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.88.1.tgz", - "integrity": "sha512-2SAKiLyZRf+peBLkhhJWGY5vC26w1Uj9QBgUZvjAnOxjgeEKUGBE5riPS0R7+YoqSrRL0w8ucwZB4M+vvy9HMg==", - "license": "MIT" - }, "node_modules/expo-updates-interface": { "version": "58.0.1", "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-58.0.1.tgz", @@ -9807,32 +9712,32 @@ } }, "node_modules/expo/node_modules/@expo/cli": { - "version": "58.1.17", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-58.1.17.tgz", - "integrity": "sha512-2WKvwrq9Sr0jj4vFVt7dPtbHoxFzaSp4s5Bsk1FDmCQjZhQlWZjb7j6igBYCs0nBzZZ4UyBdSMY2hwmDhRmPQA==", + "version": "58.1.18", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-58.1.18.tgz", + "integrity": "sha512-voZUcir9mEwvLKBNqQM6Jy2Yp67z8ONmcINXb6ZrLMayb7yxzwvEnN0eeH8ADzUABKL288pv1eJ1WRqtYxXQ/Q==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.7", - "@expo/config": "~58.0.9", + "@expo/config": "~58.0.10", "@expo/config-plugins": "~58.0.11", "@expo/devcert": "^1.2.2", "@expo/env": "~2.5.3", "@expo/image-utils": "^0.12.6", "@expo/inline-modules": "^0.2.8", "@expo/json-file": "^11.2.1", - "@expo/log-box": "^58.0.13", + "@expo/log-box": "^58.0.14", "@expo/log-box-utils": "^58.0.2", "@expo/metro": "~58.0.0", - "@expo/metro-config": "58.0.19", + "@expo/metro-config": "58.0.20", "@expo/metro-file-map": "^58.0.5", "@expo/osascript": "^2.8.1", "@expo/package-manager": "^1.14.2", "@expo/platforms": "^58.0.1", "@expo/plist": "^0.10.1", - "@expo/prebuild-config": "^58.0.17", + "@expo/prebuild-config": "^58.0.18", "@expo/require-utils": "^58.0.5", - "@expo/router-server": "^58.0.19", - "@expo/schema-utils": "^58.0.2", + "@expo/router-server": "^58.0.20", + "@expo/schema-utils": "^58.0.3", "@expo/spawn-async": "^1.8.0", "@expo/ws-tunnel": "^2.0.1", "@expo/xcpretty": "^4.4.6", @@ -9873,18 +9778,21 @@ "terminal-link": "^2.1.1", "toqr": "^0.1.1", "wrap-ansi": "^7.0.0", - "ws": "^8.12.1", - "zod": "^3.25.76" + "ws": "^8.12.1" }, "bin": { "expo-internal": "bin/cli.js" }, "peerDependencies": { "expo": "*", + "expo-mcp": "*", "expo-router": "*", "react-native": "*" }, "peerDependenciesMeta": { + "expo-mcp": { + "optional": true + }, "expo-router": { "optional": true }, @@ -9894,15 +9802,15 @@ } }, "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { - "version": "58.0.19", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-58.0.19.tgz", - "integrity": "sha512-yyqiU830tiKybH6Vr0BqXbcSCdCj5+lK/MR6l/DG5Jbv3s4jXKqa8vb4QuWRO2mMvk0xUeJO7RNyWEtGF9TYEA==", + "version": "58.0.20", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-58.0.20.tgz", + "integrity": "sha512-EqrJBFWlouh6YywiKpCVr4vjRWYon3NeRTsvULuTEVpWD0EB/8eXDxcRTfUvAtxcc6Zlf5mCEXpzE8Hf8ulA1Q==", "license": "MIT", "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { - "@expo/metro-runtime": "^58.0.15", + "@expo/metro-runtime": "^58.0.16", "expo": "*", "expo-constants": "^58.0.14", "expo-font": "^58.0.11", @@ -9961,89 +9869,6 @@ "ws": "^8.0.0" } }, - "node_modules/expo/node_modules/@react-native/asset-utils": { - "version": "0.88.1", - "resolved": "https://registry.npmjs.org/@react-native/asset-utils/-/asset-utils-0.88.1.tgz", - "integrity": "sha512-BLb5FF7NgggGYiCsA7KKkK2qFhn0K+6iGXOI+4Ucd+G7Robzw9Gt4WsWwxLt+Od+G+/hyqfz8ASokyWFZ6VcLw==", - "license": "MIT", - "engines": { - "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" - } - }, - "node_modules/expo/node_modules/@react-native/debugger-frontend": { - "version": "0.88.1", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.88.1.tgz", - "integrity": "sha512-4ZjH2093hZWWgsYs5ExqjzJX5uCS4R5yZa17eRHsQClUc6RjlV61kLdIk722sQReLL+NYtBn2RJtdcQtLCwiug==", - "license": "BSD-3-Clause", - "engines": { - "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" - } - }, - "node_modules/expo/node_modules/@react-native/debugger-shell": { - "version": "0.88.1", - "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.88.1.tgz", - "integrity": "sha512-vrOLlxkp/417qcSUiS3x3gmdoQHYA/26oEKIWPB4EekJG0uJxKXsMR0tHIDiJWO0JH/2BO3M+507DjR1U74jQg==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.6", - "debug": "^4.4.0", - "fb-dotslash": "0.5.8" - }, - "engines": { - "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" - } - }, - "node_modules/expo/node_modules/@react-native/dev-middleware": { - "version": "0.88.1", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.88.1.tgz", - "integrity": "sha512-0smOWE3NCdEYUwPiEdiwVXjTdvXcwSejviCM0YZB0Dj6HaQ+uu4hpSrvgERDevr5dvtSaqr8N4TdYZCLXiVgFw==", - "license": "MIT", - "dependencies": { - "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.88.1", - "@react-native/debugger-shell": "0.88.1", - "connect": "^3.6.5", - "debug": "^4.4.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "open": "^8.4.2", - "serve-static": "^1.16.2", - "ws": "^7.5.10" - }, - "engines": { - "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" - } - }, - "node_modules/expo/node_modules/@react-native/dev-middleware/node_modules/ws": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", - "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/@react-native/js-polyfills": { - "version": "0.88.1", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.88.1.tgz", - "integrity": "sha512-B62O6zVMYW7kK8dxIuCwpdJ5RYcIvET6ItScm0sfsTibJs+llS0krUVSAOHnEUn3lbFMfw+o+sNdWREOAc6FfQ==", - "license": "MIT", - "engines": { - "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" - } - }, "node_modules/expo/node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -10110,9 +9935,9 @@ } }, "node_modules/expo/node_modules/ws": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", - "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "version": "8.22.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.1.tgz", + "integrity": "sha512-oyL6OPYme/CmiFvNuGzeoxCJ4nOrZE2O1FwM9MWM1AsGri6nLXB2pg+lNA8aLeXMSav0J6eIVS1j6vUPzUtK9g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -10758,9 +10583,9 @@ } }, "node_modules/hermes-compiler": { - "version": "260318099.0.4", - "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-260318099.0.4.tgz", - "integrity": "sha512-8odcPFkPC82HlqCl0C5QI9mBN4rEAT+IAo55sJhzwKw//K3qZloiv3ex6J2O8CuBbhdmkJozfidNsset2KH25w==", + "version": "260318099.0.5", + "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-260318099.0.5.tgz", + "integrity": "sha512-4Gze+pGTGTUBRmnv6LtdOMo6ZzMMh3hEkvDiWzvuNL5LM0EO5YGhr8xh3fRjFaEqReUwEGjJGcGgEPFe9+E6Gw==", "license": "MIT" }, "node_modules/hermes-estree": { @@ -18021,23 +17846,23 @@ "license": "MIT" }, "node_modules/react-native": { - "version": "0.88.0", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.88.0.tgz", - "integrity": "sha512-v/wee0nYWcammMYBnSat/e3WY/5iaf9YQOMcBrfKT/wPApPPlHp94hgZovBtwhMuthR01yc/8cGn0FpFTE/C4A==", + "version": "0.88.1", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.88.1.tgz", + "integrity": "sha512-wajWcLZ93GCN7k4Uwh4CS4L2Ah8ohcvkOXnvIYmh4Yvuqho5j3APZ9qozK+nQrHWSgauq4h8D3HN5DKK84SrHA==", "license": "MIT", "dependencies": { - "@react-native/asset-utils": "0.88.0", - "@react-native/codegen": "0.88.0", - "@react-native/community-cli-plugin": "0.88.0", - "@react-native/gradle-plugin": "0.88.0", - "@react-native/normalize-colors": "0.88.0", - "@react-native/virtualized-lists": "0.88.0", + "@react-native/asset-utils": "0.88.1", + "@react-native/codegen": "0.88.1", + "@react-native/community-cli-plugin": "0.88.1", + "@react-native/gradle-plugin": "0.88.1", + "@react-native/normalize-colors": "0.88.1", + "@react-native/virtualized-lists": "0.88.1", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "base64-js": "^1.5.1", "flow-enums-runtime": "^0.0.6", "flow-parser": "0.327.0", - "hermes-compiler": "260318099.0.4", + "hermes-compiler": "260318099.0.5", "invariant": "^2.2.4", "memoize-one": "^5.0.0", "metro-runtime": "^0.87.1", @@ -18166,9 +17991,9 @@ } }, "node_modules/react-native-screens": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.30.0.tgz", - "integrity": "sha512-FrZG/Q5gqoDu0SgP4d2Ly0qdio5DR1KdGPEYImhkz7tmrhpML4C39m6RYLGd5TVLHZwcCh8NPvW9rylevjD0uQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.30.1.tgz", + "integrity": "sha512-wLzZSQpvMx3FxIlDnb6raDJjsv2x45tFLGFz8RFUvxHzsKU9dWOkZYv9fy2siWtFf3eWGcRh66hL+o+dHhRJLQ==", "license": "MIT", "dependencies": { "react-freeze": "^1.0.0", @@ -20528,6 +20353,7 @@ "version": "4.6.7", "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 406388e..1fc3194 100644 --- a/package.json +++ b/package.json @@ -17,26 +17,26 @@ }, "dependencies": { "@expo/vector-icons": "^15.0.2", - "expo": "58.0.21", + "expo": "58.0.22", "expo-asset": "~58.0.19", "expo-camera": "~58.0.14", "expo-clipboard": "~58.0.4", "expo-constants": "~58.0.14", "expo-crypto": "~58.0.7", - "expo-dev-client": "~58.0.17", + "expo-dev-client": "~58.0.18", "expo-font": "~58.0.11", "expo-linking": "~58.0.15", - "expo-router": "~58.0.28", + "expo-router": "~58.0.29", "expo-secure-store": "~58.0.2", - "expo-sqlite": "~58.0.14", + "expo-sqlite": "~58.0.15", "expo-status-bar": "~58.0.4", "expo-system-ui": "~58.0.7", "react": "19.3.0", "react-dom": "19.3.0", - "react-native": "0.88.0", + "react-native": "0.88.1", "react-native-markdown-display": "^7.0.2", "react-native-safe-area-context": "~5.10.1", - "react-native-screens": "~4.30.0", + "react-native-screens": "~4.30.1", "react-native-svg": "15.16.0", "react-native-web": "^0.21.4" },