feat(journal): federation outbox + push delivery to remote followers

social-federation tasks 5.1–5.6. Completes the inbound-federation
story: a Mastodon follower now receives a trails user's new public
activities in their home timeline.

Outbox (5.1/5.2):
- /users/:username/outbox — paginated OrderedCollection of public
  activities as Create(Note), newest first; unlisted/private never
  federate. Private-user 404 enforced at the route layer because
  Fedify builds collection-level responses from counter/cursors
  without consulting the page dispatcher.
- Note shape: HTML content (escaped name/description/stats + link to
  the activity page) with structured PropertyValue attachments
  (distance-m, elevation-gain-m, duration-s) — Mastodon renders the
  text, trails consumers read the structured fields. Resolves the
  design open question toward Create(Note).
- Authorized Fetch: signed and unsigned outbox fetches deliberately
  see the same (public-only) content until locked accounts exist.

Push delivery (5.3–5.6):
- createActivity / updateActivityVisibility(→public) enqueue one
  deliver-activity job per accepted remote follower; flips away from
  public and hard deletes enqueue Delete(Tombstone) retractions
  (enqueued before the row disappears).
- deliver-activity job: re-reads the row at delivery time (skips if
  gone or no longer public), resolves the recipient inbox via the
  remote_actors cache with actor-document fetch fallback (priming the
  cache), HTTP-signs via the owner's key, and POSTs. retryLimit 8 +
  exponential backoff at enqueue time; outbound paced at 1 req/s per
  remote host.
- Actor objects now advertise the outbox IRI.
- @js-temporal/polyfill added (same range Fedify uses) for published
  timestamps; Fedify's types want the global esnext.temporal namespace,
  bridged with a documented cast.

Tests: 9 unit tests for the AS mapping (escaping, stats, attachments,
published fallback, stable ids, tombstones), 4 outbox integration
tests (collection count, page shape/visibility filtering, private-404,
delivery audience query).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-06-06 15:32:52 +02:00
parent bec249f93f
commit bc233e03e5
16 changed files with 905 additions and 27 deletions

View file

@ -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<string, number>();
const MIN_INTERVAL_MS = 1000;
async function paceHost(host: string): Promise<void> {
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<void> {
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",
);
}

View file

@ -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<boolean> {
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;
}

View file

@ -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<string | null>;
send(queueName: string, data: unknown, options?: BossSendOptions): Promise<string | null>;
}
let _boss: BossLike | null = null;
@ -40,10 +46,11 @@ export async function enqueueOptional(
queue: string,
data: unknown,
ctx: Record<string, unknown> = {},
options?: BossSendOptions,
): Promise<void> {
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");
}

View file

@ -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<string[]> {
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<void> {
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<CachedRemoteActor | null> {
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<void> {
const db = getDb();
const { actorIri, ...rest } = fields;
await db
.insert(remoteActors)
.values({ actorIri, ...rest })
.onConflictDoUpdate({ target: remoteActors.actorIri, set: rest });
}

View file

@ -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 &lt;3");
expect(content).toContain("&quot;forest&quot; &amp; 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");
});
});

View file

@ -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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
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 = [
`<p>${escapeHtml(a.name)}</p>`,
a.description ? `<p>${escapeHtml(a.description)}</p>` : "",
stats ? `<p>${escapeHtml(stats)}</p>` : "",
`<p><a href="${objectIri}">${objectIri}</a></p>`,
].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,
});
}

View file

@ -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<Response> {
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"]);
});
});

View file

@ -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<FederatableActivity[]> {
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<number> {
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;
}

View file

@ -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<void> {
// 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<void> {
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<void> {
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<boolean> {
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

View file

@ -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"),

View file

@ -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);
}

View file

@ -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:",

View file

@ -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());

View file

@ -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

View file

@ -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

196
pnpm-lock.yaml generated
View file

@ -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