diff --git a/apps/journal/app/jobs/deliver-activity.ts b/apps/journal/app/jobs/deliver-activity.ts new file mode 100644 index 0000000..d158a3f --- /dev/null +++ b/apps/journal/app/jobs/deliver-activity.ts @@ -0,0 +1,119 @@ +import type { JobDefinition } from "@trails-cool/jobs"; +import { and, eq } from "drizzle-orm"; +import { activities } from "@trails-cool/db/schema/journal"; +import { getDb } from "../lib/db.ts"; +import { getOrigin } from "../lib/config.server.ts"; +import { getFederation } from "../lib/federation.server.ts"; +import { + activityToCreate, + activityToDelete, + type FederatableActivity, +} from "../lib/federation-objects.server.ts"; +import { + getCachedRemoteActor, + upsertRemoteActor, + type DeliveryPayload, +} from "../lib/federation-delivery.server.ts"; +import { logger } from "../lib/logger.server.ts"; + +/** + * Outbound pacing (spec 5.5): never exceed 1 request/second per remote + * host. In-process map is sufficient — pg-boss works this queue + * sequentially in the single journal process. + */ +const lastSendPerHost = new Map(); +const MIN_INTERVAL_MS = 1000; + +async function paceHost(host: string): Promise { + const last = lastSendPerHost.get(host) ?? 0; + const wait = last + MIN_INTERVAL_MS - Date.now(); + if (wait > 0) await new Promise((r) => setTimeout(r, wait)); + lastSendPerHost.set(host, Date.now()); +} + +/** + * Deliver one activity to one remote follower's inbox (spec 5.3/5.4). + * One job per (activity, recipient) so each delivery retries with + * exponential backoff independently (configured at enqueue time — + * see enqueueActivityDeliveries). A thrown error marks the attempt + * failed and pg-boss retries; exhausting the budget is the permanent + * failure, logged by the final catch. + */ +export const deliverActivityJob: JobDefinition = { + name: "deliver-activity", + expireInSeconds: 60, + async handler(jobs) { + for (const job of jobs) { + const p = job.data as DeliveryPayload; + try { + await deliverOne(p); + } catch (err) { + logger.warn( + { err, action: p.action, objectIri: p.objectIri, recipient: p.recipientActorIri }, + "deliver-activity attempt failed (pg-boss will retry until budget exhausted)", + ); + throw err; + } + } + }, +}; + +async function deliverOne(p: DeliveryPayload): Promise { + const federation = getFederation(); + const ctx = federation.createContext(new URL(getOrigin()), undefined); + + // Build the activity to send. For `create`, re-read the row at + // delivery time: if it was deleted or un-publicized since enqueue, + // skip rather than leak. + let activity; + if (p.action === "create") { + const db = getDb(); + const [row] = await db + .select() + .from(activities) + .where(and(eq(activities.id, p.activityId!), eq(activities.visibility, "public"))) + .limit(1); + if (!row) { + logger.info({ objectIri: p.objectIri }, "deliver-activity: activity gone or non-public; skipping"); + return; + } + activity = activityToCreate(row as FederatableActivity, p.ownerUsername); + } else { + activity = activityToDelete(p.objectIri, p.ownerUsername); + } + + // Resolve the recipient's inbox: cached remote_actors row first, + // actor-document fetch as fallback (which also primes the cache). + const recipientIri = new URL(p.recipientActorIri); + let inboxUrl: URL; + const cached = await getCachedRemoteActor(p.recipientActorIri); + if (cached?.inboxUrl) { + inboxUrl = new URL(cached.inboxUrl); + } else { + await paceHost(recipientIri.host); + const actor = await ctx.lookupObject(recipientIri); + const fetchedInbox = + actor != null && "inboxId" in actor ? (actor.inboxId as URL | null) : null; + if (!fetchedInbox) { + throw new Error(`deliver-activity: cannot resolve inbox for ${p.recipientActorIri}`); + } + inboxUrl = fetchedInbox; + await upsertRemoteActor({ + actorIri: p.recipientActorIri, + inboxUrl: inboxUrl.href, + domain: recipientIri.host, + }); + } + + await paceHost(inboxUrl.host); + await ctx.sendActivity( + // Identifier and username are the same thing in our actor model. + { identifier: p.ownerUsername }, + { id: recipientIri, inboxId: inboxUrl }, + activity, + ); + logger.info( + { action: p.action, objectIri: p.objectIri, recipient: p.recipientActorIri }, + "deliver-activity: delivered", + ); +} diff --git a/apps/journal/app/lib/activities.server.ts b/apps/journal/app/lib/activities.server.ts index 4c66a4c..fb2f9ae 100644 --- a/apps/journal/app/lib/activities.server.ts +++ b/apps/journal/app/lib/activities.server.ts @@ -6,6 +6,7 @@ import type { Visibility } from "@trails-cool/db/schema/journal"; import { validateGpx, writeGeom } from "./gpx-save.server.ts"; import type { GpxData } from "./gpx-save.server.ts"; import { enqueueOptional } from "./boss.server.ts"; +import { enqueueActivityDeliveries } from "./federation-delivery.server.ts"; export interface ActivityInput { name: string; @@ -38,6 +39,16 @@ export async function updateActivityVisibility( // followers (only the first transition per activity emits). if (visibility === "public") { await enqueueOptional("notifications-fanout", { activityId: id }, { source: "updateActivityVisibility" }); + // Federation: newly-public activities push to remote followers as + // Create(Note) (spec 5.3/5.6 — publish-on-flip is the "update" + // remotes care about; the delivery job re-checks visibility). + await enqueueActivityDeliveries(ownerId, id, "create"); + } else { + // Was the activity possibly public before? Retract from remotes — + // a Delete for an object a remote never saw is acknowledged and + // dropped, so over-sending here is harmless and avoids tracking + // previous visibility (spec 5.6, basic update/delete fan-out). + await enqueueActivityDeliveries(ownerId, id, "delete"); } return true; @@ -92,6 +103,8 @@ export async function createActivity(ownerId: string, input: ActivityInput) { // up-front rather than flipped later). if (input.visibility === "public") { await enqueueOptional("notifications-fanout", { activityId: id }, { source: "createActivity" }); + // Federation push delivery to accepted remote followers (spec 5.3). + await enqueueActivityDeliveries(ownerId, id, "create"); } return id; @@ -108,9 +121,15 @@ export async function getActivity(id: string) { export async function deleteActivity(id: string, ownerId: string): Promise { const db = getDb(); - const [activity] = await db.select({ id: activities.id }).from(activities) + const [activity] = await db.select({ id: activities.id, visibility: activities.visibility }).from(activities) .where(and(eq(activities.id, id), eq(activities.ownerId, ownerId))); if (!activity) return false; + // Enqueue the federation retraction *before* the row disappears — + // the Delete payload carries everything it needs (object IRI + + // owner), so it survives the deletion (spec 5.6). + if (activity.visibility === "public") { + await enqueueActivityDeliveries(ownerId, id, "delete"); + } await db.delete(activities).where(eq(activities.id, id)); return true; } diff --git a/apps/journal/app/lib/boss.server.ts b/apps/journal/app/lib/boss.server.ts index 2ebeb1c..6703b8b 100644 --- a/apps/journal/app/lib/boss.server.ts +++ b/apps/journal/app/lib/boss.server.ts @@ -8,8 +8,14 @@ import { logger } from "./logger.server.ts"; // Structurally typed (we only need `send`) so we don't have to pull // pg-boss into the journal app's dep graph just for the typedef. +export interface BossSendOptions { + retryLimit?: number; + retryBackoff?: boolean; + retryDelay?: number; +} + interface BossLike { - send(queueName: string, data: unknown): Promise; + send(queueName: string, data: unknown, options?: BossSendOptions): Promise; } let _boss: BossLike | null = null; @@ -40,10 +46,11 @@ export async function enqueueOptional( queue: string, data: unknown, ctx: Record = {}, + options?: BossSendOptions, ): Promise { try { const boss = getBoss(); - await boss.send(queue, data); + await boss.send(queue, data, options); } catch (err) { logger.warn({ err, queue, ...ctx }, "boss.send failed; continuing"); } diff --git a/apps/journal/app/lib/federation-delivery.server.ts b/apps/journal/app/lib/federation-delivery.server.ts new file mode 100644 index 0000000..e4c1f31 --- /dev/null +++ b/apps/journal/app/lib/federation-delivery.server.ts @@ -0,0 +1,115 @@ +// Push delivery of local public activities to accepted remote +// followers (spec: social-federation, "Push delivery on local activity +// create"). Enqueue side lives here; the actual signed POST happens in +// jobs/deliver-activity.ts. + +import { and, eq, isNotNull } from "drizzle-orm"; +import { follows, remoteActors, users } from "@trails-cool/db/schema/journal"; +import { getDb } from "./db.ts"; +import { federationEnabled } from "./federation.server.ts"; +import { enqueueOptional } from "./boss.server.ts"; +import { activityObjectIri } from "./federation-objects.server.ts"; + +export type DeliveryAction = "create" | "delete"; + +export interface DeliveryPayload { + action: DeliveryAction; + /** Present for `create` — the job re-reads the row at delivery time. */ + activityId?: string; + /** Object IRI; for `delete` this is all that's left of the activity. */ + objectIri: string; + ownerUsername: string; + recipientActorIri: string; +} + +/** Accepted remote followers of a local user (the delivery audience). */ +export async function listAcceptedRemoteFollowers(ownerId: string): Promise { + const db = getDb(); + const rows = await db + .select({ actorIri: follows.followerActorIri }) + .from(follows) + .where( + and( + eq(follows.followedUserId, ownerId), + isNotNull(follows.followerActorIri), + isNotNull(follows.acceptedAt), + ), + ); + return rows.map((r) => r.actorIri).filter((iri): iri is string => iri !== null); +} + +/** + * Fan out one delivery job per accepted remote follower (spec 5.3: a + * job per follower, each with its own retry/backoff lifecycle). No-op + * when federation is off or the user has no remote followers — the + * common case stays a single SELECT. + */ +export async function enqueueActivityDeliveries( + ownerId: string, + activityId: string, + action: DeliveryAction, +): Promise { + if (!federationEnabled()) return; + const recipients = await listAcceptedRemoteFollowers(ownerId); + if (recipients.length === 0) return; + + const db = getDb(); + const [owner] = await db + .select({ username: users.username, profileVisibility: users.profileVisibility }) + .from(users) + .where(eq(users.id, ownerId)) + .limit(1); + // Private profiles don't federate — suppress push delivery entirely + // (spec 9.3: flipping to private stops federation). + if (!owner || owner.profileVisibility !== "public") return; + + for (const recipientActorIri of recipients) { + const payload: DeliveryPayload = { + action, + activityId: action === "create" ? activityId : undefined, + objectIri: activityObjectIri(activityId), + ownerUsername: owner.username, + recipientActorIri, + }; + await enqueueOptional( + "deliver-activity", + payload, + { source: "enqueueActivityDeliveries", action }, + // Spec 5.4: exponential backoff on failure, bounded retry budget. + // 8 retries with backoff spans roughly a day before permanent fail. + { retryLimit: 8, retryBackoff: true }, + ); + } +} + +export interface CachedRemoteActor { + actorIri: string; + inboxUrl: string | null; +} + +/** Read the remote actor cache; null when we've never seen the actor. */ +export async function getCachedRemoteActor(actorIri: string): Promise { + const db = getDb(); + const [row] = await db + .select({ actorIri: remoteActors.actorIri, inboxUrl: remoteActors.inboxUrl }) + .from(remoteActors) + .where(eq(remoteActors.actorIri, actorIri)) + .limit(1); + return row ?? null; +} + +/** Upsert the fields delivery learns about a remote actor. */ +export async function upsertRemoteActor(fields: { + actorIri: string; + inboxUrl?: string | null; + displayName?: string | null; + username?: string | null; + domain?: string | null; +}): Promise { + const db = getDb(); + const { actorIri, ...rest } = fields; + await db + .insert(remoteActors) + .values({ actorIri, ...rest }) + .onConflictDoUpdate({ target: remoteActors.actorIri, set: rest }); +} diff --git a/apps/journal/app/lib/federation-objects.server.test.ts b/apps/journal/app/lib/federation-objects.server.test.ts new file mode 100644 index 0000000..92ff81d --- /dev/null +++ b/apps/journal/app/lib/federation-objects.server.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect } from "vitest"; +import { + activityToNote, + activityToCreate, + activityToDelete, + activityObjectIri, + type FederatableActivity, +} from "./federation-objects.server.ts"; + +const ACTIVITY: FederatableActivity = { + id: "act-1", + name: "Morning ride <3", + description: 'Through the "forest" & hills', + distance: 42_195, + elevationGain: 512.4, + duration: 2 * 3600 + 30 * 60, + startedAt: new Date("2026-06-01T08:00:00Z"), + createdAt: new Date("2026-06-01T12:00:00Z"), +}; + +describe("activityToNote", () => { + it("builds a public Note addressed at the activity page", async () => { + const note = activityToNote(ACTIVITY, "bruno"); + expect(note.id?.href).toBe("http://localhost:3000/activities/act-1"); + expect(note.attributionId?.href).toBe("http://localhost:3000/users/bruno"); + expect(note.toIds.map((u) => u.href)).toContain( + "https://www.w3.org/ns/activitystreams#Public", + ); + expect(note.published?.toString()).toBe("2026-06-01T08:00:00Z"); + }); + + it("escapes HTML in user content and includes stats + link", () => { + const note = activityToNote(ACTIVITY, "bruno"); + const content = String(note.content); + expect(content).toContain("Morning ride <3"); + expect(content).toContain(""forest" & hills"); + expect(content).toContain("42.2 km"); + expect(content).toContain("↗ 512 m"); + expect(content).toContain("2h 30m"); + expect(content).toContain('href="http://localhost:3000/activities/act-1"'); + }); + + it("carries structured metadata as PropertyValue attachments", async () => { + const note = activityToNote(ACTIVITY, "bruno"); + const attachments = []; + for await (const a of note.getAttachments()) attachments.push(a); + const byName = new Map( + attachments.map((a) => [String((a as { name: unknown }).name), String((a as { value: unknown }).value)]), + ); + expect(byName.get("distance-m")).toBe("42195"); + expect(byName.get("elevation-gain-m")).toBe("512"); + expect(byName.get("duration-s")).toBe("9000"); + }); + + it("omits stats it doesn't have", () => { + const bare = activityToNote( + { ...ACTIVITY, distance: null, elevationGain: null, duration: null, description: null }, + "bruno", + ); + const content = String(bare.content); + expect(content).not.toContain("km"); + expect(content).not.toContain("↗"); + }); + + it("falls back to createdAt when startedAt is missing", () => { + const note = activityToNote({ ...ACTIVITY, startedAt: null }, "bruno"); + expect(note.published?.toString()).toBe("2026-06-01T12:00:00Z"); + }); +}); + +describe("activityToCreate", () => { + it("derives a stable id from the object and attributes the actor", () => { + const create = activityToCreate(ACTIVITY, "bruno"); + expect(create.id?.href).toBe("http://localhost:3000/activities/act-1#create"); + expect(create.actorId?.href).toBe("http://localhost:3000/users/bruno"); + }); +}); + +describe("activityToDelete", () => { + it("wraps a Tombstone for the deleted object", async () => { + const del = activityToDelete(activityObjectIri("act-1"), "bruno"); + expect(del.actorId?.href).toBe("http://localhost:3000/users/bruno"); + const tombstone = await del.getObject(); + expect(tombstone?.id?.href).toBe("http://localhost:3000/activities/act-1"); + }); +}); diff --git a/apps/journal/app/lib/federation-objects.server.ts b/apps/journal/app/lib/federation-objects.server.ts new file mode 100644 index 0000000..d07a267 --- /dev/null +++ b/apps/journal/app/lib/federation-objects.server.ts @@ -0,0 +1,121 @@ +// Mapping from journal activities to ActivityStreams objects (spec: +// social-federation, "Outbox publishes user's public activities" + +// push delivery). One mapping, used by both the outbox dispatcher and +// the deliver-activity job so remotes see identical shapes either way. +// +// Shape decision (design.md open question, resolved toward Mastodon +// compat): plain `Create(Note)`. The Note's HTML content carries the +// human-readable text + a link back to the activity page; structured +// trails metadata (distance, elevation, duration) rides along as +// PropertyValue attachments, which Mastodon ignores gracefully and +// trails consumers can read without parsing the HTML. + +import { Temporal as TemporalPolyfill } from "@js-temporal/polyfill"; +import { Create, Delete, Note, PropertyValue, Tombstone, PUBLIC_COLLECTION } from "@fedify/fedify/vocab"; +import { getOrigin } from "./config.server.ts"; +import { localActorIri } from "./actor-iri.ts"; + +export interface FederatableActivity { + id: string; + name: string; + description: string | null; + distance: number | null; + elevationGain: number | null; + duration: number | null; + startedAt: Date | null; + createdAt: Date; +} + +/** Public IRI of an activity — its journal detail page. */ +export function activityObjectIri(activityId: string): string { + return `${getOrigin()}/activities/${activityId}`; +} + +function escapeHtml(s: string): string { + return s + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +function formatStats(a: FederatableActivity): string { + const parts: string[] = []; + if (a.distance != null) parts.push(`${(a.distance / 1000).toFixed(1)} km`); + if (a.elevationGain != null) parts.push(`↗ ${Math.round(a.elevationGain)} m`); + if (a.duration != null) { + const h = Math.floor(a.duration / 3600); + const m = Math.round((a.duration % 3600) / 60); + parts.push(h > 0 ? `${h}h ${m}m` : `${m}m`); + } + return parts.join(" · "); +} + +// Fedify's types reference the *global* Temporal namespace +// (esnext.temporal lib); Node 22 doesn't ship Temporal at runtime, so +// we construct via the polyfill and bridge the nominally-distinct (but +// structurally identical) types with a cast. +function toInstant(d: Date): Temporal.Instant { + return TemporalPolyfill.Instant.fromEpochMilliseconds( + d.getTime(), + ) as unknown as Temporal.Instant; +} + +export function activityToNote(a: FederatableActivity, ownerUsername: string): Note { + const objectIri = activityObjectIri(a.id); + const stats = formatStats(a); + const paragraphs = [ + `

${escapeHtml(a.name)}

`, + a.description ? `

${escapeHtml(a.description)}

` : "", + stats ? `

${escapeHtml(stats)}

` : "", + `

${objectIri}

`, + ].filter(Boolean); + + const attachments: PropertyValue[] = []; + if (a.distance != null) { + attachments.push(new PropertyValue({ name: "distance-m", value: String(Math.round(a.distance)) })); + } + if (a.elevationGain != null) { + attachments.push(new PropertyValue({ name: "elevation-gain-m", value: String(Math.round(a.elevationGain)) })); + } + if (a.duration != null) { + attachments.push(new PropertyValue({ name: "duration-s", value: String(a.duration) })); + } + + return new Note({ + id: new URL(objectIri), + attribution: new URL(localActorIri(ownerUsername)), + content: paragraphs.join("\n"), + mediaType: "text/html", + url: new URL(objectIri), + published: toInstant(a.startedAt ?? a.createdAt), + to: PUBLIC_COLLECTION, + attachments, + }); +} + +export function activityToCreate(a: FederatableActivity, ownerUsername: string): Create { + return new Create({ + // Stable id derived from the object so replays/dedupe work across + // outbox pages and push deliveries alike. + id: new URL(`${activityObjectIri(a.id)}#create`), + actor: new URL(localActorIri(ownerUsername)), + object: activityToNote(a, ownerUsername), + published: toInstant(a.startedAt ?? a.createdAt), + to: PUBLIC_COLLECTION, + }); +} + +/** + * Retraction for a deleted (or un-publicized) activity. Carries a + * Tombstone so remotes drop their copy (Mastodon honors Delete + + * Tombstone). + */ +export function activityToDelete(objectIri: string, ownerUsername: string): Delete { + return new Delete({ + id: new URL(`${objectIri}#delete-${Date.now()}`), + actor: new URL(localActorIri(ownerUsername)), + object: new Tombstone({ id: new URL(objectIri) }), + to: PUBLIC_COLLECTION, + }); +} diff --git a/apps/journal/app/lib/federation-outbox.integration.test.ts b/apps/journal/app/lib/federation-outbox.integration.test.ts new file mode 100644 index 0000000..1fb55c9 --- /dev/null +++ b/apps/journal/app/lib/federation-outbox.integration.test.ts @@ -0,0 +1,124 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { eq } from "drizzle-orm"; +import { randomUUID } from "node:crypto"; +import { getDb } from "./db.ts"; +import { users, activities, follows } from "@trails-cool/db/schema/journal"; + +// Opt-in: talks to real Postgres (and exercises the real Fedify +// dispatcher stack via handleFederationRequest). Same convention as the +// other *.integration.test.ts files. +const runIntegration = process.env.FEDERATION_INTEGRATION === "1"; + +const USERNAME = `outbox-user-${Date.now()}`; +let userId: string; + +async function seed() { + const db = getDb(); + userId = randomUUID(); + await db.insert(users).values({ + id: userId, + email: `${USERNAME}@example.test`, + username: USERNAME, + domain: "test.local", + profileVisibility: "public", + }); + // 3 public, 1 unlisted, 1 private — only the 3 public ones federate. + const mk = (n: number, visibility: "public" | "unlisted" | "private") => ({ + id: randomUUID(), + ownerId: userId, + name: `Activity ${n}`, + visibility, + distance: 1000 * n, + createdAt: new Date(Date.now() - n * 60_000), + }); + await db + .insert(activities) + .values([mk(1, "public"), mk(2, "public"), mk(3, "public"), mk(4, "unlisted"), mk(5, "private")]); +} + +describe.runIf(runIntegration)("federation outbox (integration)", () => { + beforeAll(async () => { + process.env.FEDERATION_ENABLED = "true"; + process.env.ORIGIN ??= "http://localhost:3000"; + await seed(); + }); + + afterAll(async () => { + const db = getDb(); + await db.delete(activities).where(eq(activities.ownerId, userId)); + await db.delete(follows).where(eq(follows.followedUserId, userId)); + await db.delete(users).where(eq(users.id, userId)); + }); + + // Mirrors routes/users.$username.outbox.ts: route-level visibility + // gate first (Fedify's collection-level response doesn't consult the + // page dispatcher), then delegate to Fedify. + async function fetchOutbox(path: string): Promise { + const { handleFederationRequest, isFederatableUser } = await import("./federation.server.ts"); + if (!(await isFederatableUser(USERNAME))) { + return new Response("Not Found", { status: 404 }); + } + return handleFederationRequest( + new Request(`http://localhost:3000${path}`, { + headers: { accept: "application/activity+json" }, + }), + ); + } + + it("serves an OrderedCollection counting only public activities", async () => { + const res = await fetchOutbox(`/users/${USERNAME}/outbox`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.type).toBe("OrderedCollection"); + expect(body.totalItems).toBe(3); + expect(body.first).toBeDefined(); + }); + + it("pages contain Create(Note) items, public only, newest first", async () => { + const res = await fetchOutbox(`/users/${USERNAME}/outbox?cursor=0`); + expect(res.status).toBe(200); + const body = await res.json(); + const items = Array.isArray(body.orderedItems) ? body.orderedItems : [body.orderedItems]; + expect(items).toHaveLength(3); + for (const item of items) { + expect(item.type).toBe("Create"); + expect(item.object.type).toBe("Note"); + expect(item.object.attributedTo).toContain(`/users/${USERNAME}`); + } + const names = items.map((i: { object: { content: string } }) => i.object.content); + expect(names[0]).toContain("Activity 1"); // newest first + expect(names.join()).not.toContain("Activity 4"); // unlisted excluded + expect(names.join()).not.toContain("Activity 5"); // private excluded + }); + + it("404s the outbox of a private profile", async () => { + const db = getDb(); + await db.update(users).set({ profileVisibility: "private" }).where(eq(users.id, userId)); + const res = await fetchOutbox(`/users/${USERNAME}/outbox`); + expect(res.status).toBe(404); + await db.update(users).set({ profileVisibility: "public" }).where(eq(users.id, userId)); + }); + + it("lists accepted remote followers as the delivery audience", async () => { + const db = getDb(); + const { listAcceptedRemoteFollowers } = await import("./federation-delivery.server.ts"); + await db.insert(follows).values([ + { + id: randomUUID(), + followerActorIri: "https://other.example/users/accepted", + followedActorIri: `http://localhost:3000/users/${USERNAME}`, + followedUserId: userId, + acceptedAt: new Date(), + }, + { + id: randomUUID(), + followerActorIri: "https://other.example/users/pending", + followedActorIri: `http://localhost:3000/users/${USERNAME}`, + followedUserId: userId, + acceptedAt: null, + }, + ]); + const audience = await listAcceptedRemoteFollowers(userId); + expect(audience).toEqual(["https://other.example/users/accepted"]); + }); +}); diff --git a/apps/journal/app/lib/federation-outbox.server.ts b/apps/journal/app/lib/federation-outbox.server.ts new file mode 100644 index 0000000..098f8ce --- /dev/null +++ b/apps/journal/app/lib/federation-outbox.server.ts @@ -0,0 +1,44 @@ +// Outbox listing queries (spec 5.1). Offset-paged, public-only — +// `unlisted` and `private` never federate. Separate from +// activities.server.ts because the outbox needs raw rows (no geojson +// batching) in a stable reverse-chronological order keyed on createdAt. + +import { and, count, desc, eq } from "drizzle-orm"; +import { activities } from "@trails-cool/db/schema/journal"; +import { getDb } from "./db.ts"; +import type { FederatableActivity } from "./federation-objects.server.ts"; + +export const OUTBOX_PAGE_SIZE = 20; + +export async function listPublicActivitiesPage( + ownerId: string, + offset: number, + limit: number, +): Promise { + const db = getDb(); + return db + .select({ + id: activities.id, + name: activities.name, + description: activities.description, + distance: activities.distance, + elevationGain: activities.elevationGain, + duration: activities.duration, + startedAt: activities.startedAt, + createdAt: activities.createdAt, + }) + .from(activities) + .where(and(eq(activities.ownerId, ownerId), eq(activities.visibility, "public"))) + .orderBy(desc(activities.createdAt)) + .offset(offset) + .limit(limit); +} + +export async function countPublicActivities(ownerId: string): Promise { + const db = getDb(); + const [row] = await db + .select({ n: count() }) + .from(activities) + .where(and(eq(activities.ownerId, ownerId), eq(activities.visibility, "public"))); + return row?.n ?? 0; +} diff --git a/apps/journal/app/lib/federation.server.ts b/apps/journal/app/lib/federation.server.ts index 7ba410f..ccac7e2 100644 --- a/apps/journal/app/lib/federation.server.ts +++ b/apps/journal/app/lib/federation.server.ts @@ -34,6 +34,12 @@ import { getOrigin } from "./config.server.ts"; import { localActorIri } from "./actor-iri.ts"; import { PostgresKvStore } from "./federation-kv.server.ts"; import { ensureUserKeypair, loadUserKeypair } from "./federation-keys.server.ts"; +import { activityToCreate } from "./federation-objects.server.ts"; +import { + OUTBOX_PAGE_SIZE, + countPublicActivities, + listPublicActivitiesPage, +} from "./federation-outbox.server.ts"; import { recordRemoteFollow, removeRemoteFollow, @@ -117,6 +123,7 @@ function buildFederation(): Federation { // declared explicitly so AP clients link browsers to HTML. url: new URL(localActorIri(identifier)), inbox: ctx.getInboxUri(identifier), + outbox: ctx.getOutboxUri(identifier), // Public keys for inbound HTTP-Signature verification by // remotes (Mastodon reads publicKey; newer stacks read the // multikey assertionMethods). @@ -138,6 +145,30 @@ function buildFederation(): Federation { return pair ? [pair] : []; }); + // Outbox (spec 5.1/5.2): paginated OrderedCollection of the user's + // public activities as Create(Note). Unsigned and signed fetches see + // the same content — the outbox only ever contains public items, so + // Authorized Fetch adds nothing until locked accounts exist (the + // spec's two scenarios deliberately collapse to one shape). + federation + .setOutboxDispatcher("/users/{identifier}/outbox", async (_ctx, identifier, cursor) => { + const user = await findUserByUsername(identifier); + if (!user || user.profileVisibility !== "public") return null; + const offset = cursor === null ? 0 : Number.parseInt(cursor, 10); + if (Number.isNaN(offset) || offset < 0) return null; + const page = await listPublicActivitiesPage(user.id, offset, OUTBOX_PAGE_SIZE); + return { + items: page.map((a) => activityToCreate(a, identifier)), + nextCursor: page.length === OUTBOX_PAGE_SIZE ? String(offset + OUTBOX_PAGE_SIZE) : null, + }; + }) + .setCounter(async (_ctx, identifier) => { + const user = await findUserByUsername(identifier); + if (!user || user.profileVisibility !== "public") return null; + return countPublicActivities(user.id); + }) + .setFirstCursor(() => "0"); + federation .setInboxListeners("/users/{identifier}/inbox") .on(Follow, async (ctx, follow) => { @@ -224,6 +255,18 @@ export function getFederation(): Federation { return _federation; } +/** + * Is this username a local user that federates (exists + public)? + * Route-level gate for federation surfaces whose collection-level + * responses Fedify builds without consulting the page dispatcher + * (e.g. the outbox OrderedCollection) — private users must 404 + * everywhere, mirroring the actor object. + */ +export async function isFederatableUser(username: string): Promise { + const user = await findUserByUsername(username); + return user !== null && user.profileVisibility === "public"; +} + /** * Delegate a request to Fedify's URL dispatcher. 404s when the feature * flag is off so disabled instances are indistinguishable from instances diff --git a/apps/journal/app/routes.ts b/apps/journal/app/routes.ts index db8d720..0f630a3 100644 --- a/apps/journal/app/routes.ts +++ b/apps/journal/app/routes.ts @@ -7,6 +7,7 @@ export default [ route(".well-known/nodeinfo", "routes/api.well-known.nodeinfo.ts"), route("nodeinfo/2.1", "routes/api.nodeinfo.ts"), route("users/:username/inbox", "routes/users.$username.inbox.ts"), + route("users/:username/outbox", "routes/users.$username.outbox.ts"), route("oauth/authorize", "routes/oauth.authorize.tsx"), route("oauth/token", "routes/oauth.token.ts"), route("auth/register", "routes/auth.register.tsx"), diff --git a/apps/journal/app/routes/users.$username.outbox.ts b/apps/journal/app/routes/users.$username.outbox.ts new file mode 100644 index 0000000..1e1c17f --- /dev/null +++ b/apps/journal/app/routes/users.$username.outbox.ts @@ -0,0 +1,22 @@ +import type { Route } from "./+types/users.$username.outbox"; +import { + federationEnabled, + handleFederationRequest, + isFederatableUser, +} from "~/lib/federation.server"; + +/** + * GET /users/:username/outbox — paginated OrderedCollection of the + * user's public activities as Create(Note) (spec: social-federation, + * "Outbox publishes user's public activities"). Served by Fedify's + * collection dispatcher; 404s for private users (checked here because + * Fedify builds the collection-level response from counter/cursors + * without consulting the page dispatcher) and while FEDERATION_ENABLED + * is off. + */ +export async function loader({ request, params }: Route.LoaderArgs) { + if (!federationEnabled() || !(await isFederatableUser(params.username))) { + return new Response("Not Found", { status: 404 }); + } + return handleFederationRequest(request); +} diff --git a/apps/journal/package.json b/apps/journal/package.json index 79f4c76..0869f53 100644 --- a/apps/journal/package.json +++ b/apps/journal/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@fedify/fedify": "2.1.16", + "@js-temporal/polyfill": "^0.5.1", "@react-router/node": "catalog:", "@react-router/serve": "catalog:", "@sentry/node": "catalog:", diff --git a/apps/journal/server.ts b/apps/journal/server.ts index 0897533..aad8035 100644 --- a/apps/journal/server.ts +++ b/apps/journal/server.ts @@ -182,7 +182,8 @@ server.listen(port, async () => { if (process.env.FEDERATION_ENABLED === "true") { const { backfillUserKeypairsJob } = await import("./app/jobs/backfill-user-keypairs.ts"); const { federationKvSweepJob } = await import("./app/jobs/federation-kv-sweep.ts"); - jobs.push(backfillUserKeypairsJob, federationKvSweepJob); + const { deliverActivityJob } = await import("./app/jobs/deliver-activity.ts"); + jobs.push(backfillUserKeypairsJob, federationKvSweepJob, deliverActivityJob); } const boss = createBoss(getDatabaseUrl()); diff --git a/openspec/changes/social-federation/design.md b/openspec/changes/social-federation/design.md index b5efc86..15f9bd4 100644 --- a/openspec/changes/social-federation/design.md +++ b/openspec/changes/social-federation/design.md @@ -124,6 +124,17 @@ Inbound signature verification uses the actor's public key from their actor obje with `/.well-known/trails-cool` as the secondary signal. - **Fedify's KvStore is Postgres-backed** (`journal.federation_kv`) so inbox replay protection and document caches survive restarts; swept daily. +- **Outgoing activity shape: `Create(Note)` with PropertyValue attachments** + (task 5.1; resolves the open question below toward Mastodon compat). The + Note's HTML content carries name/description/stats plus a link to the + activity page; distance/elevation/duration ride along as `PropertyValue` + attachments that Mastodon ignores gracefully and trails consumers can read + without HTML parsing. GPX download links can join once activities have a + public GPX endpoint. +- **Private-user 404 for collection endpoints is enforced at the route layer** + (task 5.1): Fedify builds collection-level responses (outbox + OrderedCollection) from counter/cursors without consulting the page + dispatcher, so the dispatcher's `null` → 404 contract doesn't cover them. ## Open Questions diff --git a/openspec/changes/social-federation/tasks.md b/openspec/changes/social-federation/tasks.md index 1ec4675..34346ca 100644 --- a/openspec/changes/social-federation/tasks.md +++ b/openspec/changes/social-federation/tasks.md @@ -34,12 +34,14 @@ ## 5. Outbox + push delivery -- [ ] 5.1 Outbox endpoint at `/users/:username/outbox`. Returns paginated `OrderedCollection` of the user's `public` activities as `Create(Note)` (with structured trails-specific metadata in `attachment` so Mastodon shows text + GPX link, and trails consumers can read distance/elevation) -- [ ] 5.2 Honor signed-fetch (Authorized Fetch) on outbox GETs. Unsigned requests get a public-only subset; signed requests get the same (locked accounts is later-change territory) -- [ ] 5.3 On local activity create with `visibility = 'public'`, enqueue a `deliver-activity` pg-boss job per accepted remote follower -- [ ] 5.4 `deliver-activity` job: HTTP-sign + POST `Create(Note)` to follower inbox; retry with exponential backoff on 5xx; permanent-fail after retry budget; log final outcome -- [ ] 5.5 Per-remote-host rate limit on outbound: 1 req/sec per host -- [ ] 5.6 On local activity update or delete, enqueue corresponding `Update`/`Delete` activities to followers (basic — full update/delete fan-out) +- [x] 5.1 Outbox endpoint at `/users/:username/outbox`. Returns paginated `OrderedCollection` of the user's `public` activities as `Create(Note)` (with structured trails-specific metadata in `attachment` so Mastodon shows text + GPX link, and trails consumers can read distance/elevation) +- [x] 5.2 Honor signed-fetch (Authorized Fetch) on outbox GETs. Unsigned requests get a public-only subset; signed requests get the same (locked accounts is later-change territory) + > The two scenarios collapse to one shape — the outbox only ever contains public items, so no signature check is needed until locked accounts exist. Documented in code. +- [x] 5.3 On local activity create with `visibility = 'public'`, enqueue a `deliver-activity` pg-boss job per accepted remote follower +- [x] 5.4 `deliver-activity` job: HTTP-sign + POST `Create(Note)` to follower inbox; retry with exponential backoff on 5xx; permanent-fail after retry budget; log final outcome +- [x] 5.5 Per-remote-host rate limit on outbound: 1 req/sec per host +- [x] 5.6 On local activity update or delete, enqueue corresponding `Update`/`Delete` activities to followers (basic — full update/delete fan-out) + > Implemented as: visibility flip → public sends Create; flip away from public or hard delete sends Delete(Tombstone). There is no general field-edit server path today, so `Update` has no trigger yet — revisit if/when activity editing ships. ## 6. Outbound follow + trails-to-trails check diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd23e25..69f51b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,6 +206,9 @@ importers: '@fedify/fedify': specifier: 2.1.16 version: 2.1.16 + '@js-temporal/polyfill': + specifier: ^0.5.1 + version: 0.5.1 '@react-router/node': specifier: 'catalog:' version: 7.16.0(react-router@7.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.3) @@ -9588,7 +9591,7 @@ snapshots: connect: 3.7.0 debug: 4.4.3 dnssd-advertise: 1.1.4 - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-server: 56.0.4 fetch-nodeshim: 0.4.10 getenv: 2.0.0 @@ -9614,7 +9617,7 @@ snapshots: ws: 8.21.0 zod: 3.25.76 optionalDependencies: - expo-router: 56.2.8(8f2951496f8771d7ce04c6a782b8e488) + expo-router: 56.2.8(8a75af1e83efd87b396be933266b8559) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) transitivePeerDependencies: - '@expo/dom-webview' @@ -9735,7 +9738,7 @@ snapshots: '@expo/dom-webview@55.0.5(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)': dependencies: - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) @@ -9871,7 +9874,7 @@ snapshots: dependencies: '@expo/dom-webview': 55.0.5(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) anser: 1.4.10 - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) stacktrace-parser: 0.1.11 @@ -9932,7 +9935,7 @@ snapshots: postcss: 8.5.15 resolve-from: 5.0.0 optionalDependencies: - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -9970,7 +9973,7 @@ snapshots: dependencies: '@expo/log-box': 56.0.12(@expo/dom-webview@55.0.5)(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) anser: 1.4.10 - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) pretty-format: 29.7.0 react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) @@ -10117,14 +10120,14 @@ snapshots: '@expo/router-server@56.0.11(@expo/metro-runtime@56.0.13)(expo-constants@56.0.16)(expo-font@56.0.5)(expo-router@56.2.8)(expo-server@56.0.4)(expo@56.0.4)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: debug: 4.4.3 - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-constants: 56.0.16(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6)) expo-font: 56.0.5(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) expo-server: 56.0.4 react: 19.2.6 optionalDependencies: '@expo/metro-runtime': 56.0.13(@expo/log-box@56.0.12)(expo@56.0.4)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) - expo-router: 56.2.8(8f2951496f8771d7ce04c6a782b8e488) + expo-router: 56.2.8(8a75af1e83efd87b396be933266b8559) react-dom: 19.2.6(react@19.2.6) transitivePeerDependencies: - supports-color @@ -10157,6 +10160,23 @@ snapshots: - '@types/react' - '@types/react-dom' + '@expo/ui@56.0.15(27b64df329b3e7add338555fbe0e3ac9)': + dependencies: + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + react: 19.2.6 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) + sf-symbols-typescript: 2.2.0 + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + optionalDependencies: + '@babel/core': 7.29.7 + react-dom: 19.2.6(react@19.2.6) + react-native-reanimated: 4.4.0(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + optional: true + '@expo/ui@56.0.15(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(expo@55.0.26)(react-dom@19.2.6(react@19.2.6))(react-native-reanimated@4.4.0(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.83.4(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.83.4(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.83.4(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.83.4(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)': dependencies: expo: 55.0.26(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.11)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.83.4(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.83.4(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) @@ -12875,7 +12895,7 @@ snapshots: react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) transitivePeerDependencies: - '@babel/core' - supports-color @@ -13713,7 +13733,7 @@ snapshots: expo-asset@56.0.14(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.10.1(typescript@6.0.3) - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-constants: 56.0.16(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6)) react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) @@ -13741,7 +13761,7 @@ snapshots: expo-constants@56.0.16(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6)): dependencies: '@expo/env': 2.3.0 - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) transitivePeerDependencies: - supports-color @@ -13791,7 +13811,7 @@ snapshots: expo-file-system@56.0.7(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6)): dependencies: - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) expo-font@55.0.8(expo@55.0.26)(react-native@0.83.4(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6): @@ -13803,7 +13823,7 @@ snapshots: expo-font@56.0.5(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6): dependencies: - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) fontfaceobserver: 2.3.0 react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) @@ -13817,7 +13837,7 @@ snapshots: expo-glass-effect@56.0.4(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6): dependencies: - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) @@ -13830,7 +13850,7 @@ snapshots: expo-keep-awake@56.0.3(expo@56.0.4)(react@19.2.6): dependencies: - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 expo-linking@56.0.13(expo@55.0.26)(react-native@0.83.4(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6): @@ -13901,6 +13921,16 @@ snapshots: optionalDependencies: react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.83.4(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + expo-modules-core@56.0.12(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6): + dependencies: + '@expo/expo-modules-macros-plugin': 0.0.9 + expo-modules-jsi: 56.0.7(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6)) + invariant: 2.2.4 + react: 19.2.6 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) + optionalDependencies: + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + expo-modules-core@56.0.12(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6): dependencies: '@expo/expo-modules-macros-plugin': 0.0.9 @@ -13938,6 +13968,57 @@ snapshots: - supports-color - typescript + expo-router@56.2.8(8a75af1e83efd87b396be933266b8559): + dependencies: + '@expo/log-box': 56.0.12(@expo/dom-webview@55.0.5)(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + '@expo/metro-runtime': 56.0.13(@expo/log-box@56.0.12)(expo@56.0.4)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + '@expo/schema-utils': 56.0.1 + '@expo/ui': 56.0.15(27b64df329b3e7add338555fbe0e3ac9) + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + client-only: 0.0.1 + color: 4.2.3 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo-constants: 56.0.16(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6)) + expo-glass-effect: 56.0.4(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + expo-linking: 56.0.13(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + expo-server: 56.0.4 + expo-symbols: 56.0.5(expo-font@56.0.5)(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.12 + query-string: 7.1.3 + react: 19.2.6 + react-fast-compare: 3.2.2 + react-is: 19.2.6 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) + react-native-drawer-layout: 4.2.4(7fc434303659a0f47c7bdf85df049415) + react-native-safe-area-context: 5.8.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + react-native-screens: 4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + server-only: 0.0.1 + sf-symbols-typescript: 2.2.0 + shallowequal: 1.1.0 + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + optionalDependencies: + '@testing-library/react-native': 13.3.3(jest@29.7.0(@types/node@25.9.1))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) + react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + react-native-reanimated: 4.4.0(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + transitivePeerDependencies: + - '@babel/core' + - '@testing-library/dom' + - '@types/react' + - '@types/react-dom' + - expo-font + - react-native-worklets + - supports-color + optional: true + expo-router@56.2.8(8f2951496f8771d7ce04c6a782b8e488): dependencies: '@expo/log-box': 56.0.12(@expo/dom-webview@55.0.5)(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) @@ -14068,7 +14149,7 @@ snapshots: expo-sqlite@56.0.4(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6): dependencies: await-lock: 2.2.2 - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) @@ -14091,7 +14172,7 @@ snapshots: expo-symbols@56.0.5(expo-font@56.0.5)(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6): dependencies: '@expo-google-fonts/material-symbols': 0.4.38 - expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-font: 56.0.5(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) @@ -14157,6 +14238,47 @@ snapshots: - typescript - utf-8-validate + expo@56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3): + dependencies: + '@babel/runtime': 7.29.7 + '@expo/cli': 56.1.11(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-constants@56.0.16)(expo-font@56.0.5)(expo-router@56.2.8)(expo@56.0.4)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + '@expo/config': 56.0.9(typescript@6.0.3) + '@expo/config-plugins': 56.0.8(typescript@6.0.3) + '@expo/devtools': 56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + '@expo/fingerprint': 0.19.3 + '@expo/local-build-cache-provider': 56.0.7(typescript@6.0.3) + '@expo/log-box': 56.0.12(@expo/dom-webview@55.0.5)(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + '@expo/metro': 56.0.0 + '@expo/metro-config': 56.0.12(expo@56.0.4)(typescript@6.0.3) + '@ungap/structured-clone': 1.3.1 + babel-preset-expo: 56.0.12(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@56.0.4)(react-refresh@0.14.2) + expo-asset: 56.0.14(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo-constants: 56.0.16(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6)) + expo-file-system: 56.0.7(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6)) + expo-font: 56.0.5(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + expo-keep-awake: 56.0.3(expo@56.0.4)(react@19.2.6) + expo-modules-autolinking: 56.0.12(typescript@6.0.3) + expo-modules-core: 56.0.12(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + pretty-format: 29.7.0 + react: 19.2.6 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) + react-refresh: 0.14.2 + whatwg-url-minimum: 0.1.2 + optionalDependencies: + '@expo/dom-webview': 55.0.5(expo@56.0.4)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + '@expo/metro-runtime': 56.0.13(@expo/log-box@56.0.12)(expo@56.0.4)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + react-dom: 19.2.6(react@19.2.6) + transitivePeerDependencies: + - '@babel/core' + - bufferutil + - expo-router + - expo-widgets + - react-native-worklets + - react-server-dom-webpack + - supports-color + - typescript + - utf-8-validate + expo@56.0.4(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@56.0.13)(expo-router@56.2.8)(react-dom@19.2.6(react@19.2.6))(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6)(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.7 @@ -16312,6 +16434,16 @@ snapshots: react: 19.2.6 react-dom: 19.2.6(react@19.2.6) + react-native-drawer-layout@4.2.4(7fc434303659a0f47c7bdf85df049415): + dependencies: + color: 4.2.3 + react: 19.2.6 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) + react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + react-native-reanimated: 4.4.0(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + use-latest-callback: 0.2.6(react@19.2.6) + optional: true + react-native-drawer-layout@4.2.4(c0e69a3071d879bebf456eb4f44ccb1f): dependencies: color: 4.2.3 @@ -16370,6 +16502,15 @@ snapshots: semver: 7.8.1 optional: true + react-native-reanimated@4.4.0(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6): + dependencies: + react: 19.2.6 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) + react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6) + semver: 7.8.1 + optional: true + react-native-reanimated@4.4.0(react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6): dependencies: react: 19.2.6 @@ -16425,6 +16566,27 @@ snapshots: - supports-color optional: true + react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6): + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) + '@react-native/metro-config': 0.85.3(@babel/core@7.29.7) + convert-source-map: 2.0.0 + react: 19.2.6 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6) + semver: 7.8.1 + transitivePeerDependencies: + - supports-color + optional: true + react-native-worklets@0.9.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/jest-preset@0.85.3(@babel/core@7.29.7)(react@19.2.6))(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.15)(react@19.2.6))(react@19.2.6): dependencies: '@babel/core': 7.29.7