| author | |
| committer | |
| log | bce680926d38915f3590242c0d1a4309e7ca1b9a |
| tree | 6b1eb051adbe11289c82c2d57ffcb2aadbb6265f |
| parent | bd9ed2b4bbda3dd6e468df1cb06233e29c4a9f5c |
| signature | Signed by PGP key B5690EEEBB952194 |
11 files changed, 122 insertions(+), 11 deletions(-)
apps/mobile/package.json+1| ... | @@ -81,6 +81,7 @@ | ... | @@ -81,6 +81,7 @@ |
| 81 | "expo-constants": "~56.0.18", | 81 | "expo-constants": "~56.0.18", |
| 82 | "expo-crypto": "~56.0.4", | 82 | "expo-crypto": "~56.0.4", |
| 83 | "expo-dev-client": "~56.0.20", | 83 | "expo-dev-client": "~56.0.20", |
| 84 | "expo-device": "~56.0.4", | ||
| 84 | "expo-file-system": "~56.0.8", | 85 | "expo-file-system": "~56.0.8", |
| 85 | "expo-font": "~56.0.7", | 86 | "expo-font": "~56.0.7", |
| 86 | "expo-glass-effect": "~56.0.4", | 87 | "expo-glass-effect": "~56.0.4", |
apps/mobile/src/features/cloud/linkEnvironment.test.ts+5| ... | @@ -33,6 +33,11 @@ vi.mock("expo-constants", () => ({ | ... | @@ -33,6 +33,11 @@ vi.mock("expo-constants", () => ({ |
| 33 | }, | 33 | }, |
| 34 | })); | 34 | })); |
| 35 | 35 | ||
| 36 | vi.mock("expo-device", () => ({ | ||
| 37 | osVersion: "18.4.1", | ||
| 38 | modelName: "iPhone 15 Pro", | ||
| 39 | })); | ||
| 40 | |||
| 36 | vi.mock("react-native", () => ({ | 41 | vi.mock("react-native", () => ({ |
| 37 | Platform: { | 42 | Platform: { |
| 38 | OS: "ios", | 43 | OS: "ios", |
apps/mobile/src/lib/authClientMetadata.ts+6| ... | @@ -1,11 +1,17 @@ | ... | @@ -1,11 +1,17 @@ |
| 1 | import type { AuthClientPresentationMetadata } from "@t3tools/contracts"; | 1 | import type { AuthClientPresentationMetadata } from "@t3tools/contracts"; |
| 2 | import * as Device from "expo-device"; | ||
| 2 | import { Platform } from "react-native"; | 3 | import { Platform } from "react-native"; |
| 3 | 4 | ||
| 4 | export function authClientMetadata(appVersion?: string): AuthClientPresentationMetadata { | 5 | export function authClientMetadata(appVersion?: string): AuthClientPresentationMetadata { |
| 6 | const osMajorVersion = Number.parseInt(Device.osVersion?.split(".")[0] ?? "", 10); | ||
| 7 | const deviceModel = Device.modelName?.trim(); | ||
| 8 | |||
| 5 | return { | 9 | return { |
| 6 | label: "T3 Code Mobile", | 10 | label: "T3 Code Mobile", |
| 7 | deviceType: "mobile", | 11 | deviceType: "mobile", |
| 8 | ...(Platform.OS === "ios" ? { os: "iOS" } : Platform.OS === "android" ? { os: "Android" } : {}), | 12 | ...(Platform.OS === "ios" ? { os: "iOS" } : Platform.OS === "android" ? { os: "Android" } : {}), |
| 13 | ...(Number.isFinite(osMajorVersion) && osMajorVersion > 0 ? { osMajorVersion } : {}), | ||
| 14 | ...(deviceModel ? { deviceModel } : {}), | ||
| 9 | surface: "mobile", | 15 | surface: "mobile", |
| 10 | ...(appVersion ? { appVersion } : {}), | 16 | ...(appVersion ? { appVersion } : {}), |
| 11 | }; | 17 | }; |
apps/mobile/src/lib/connection.test.ts+31-5| ... | @@ -1,12 +1,18 @@ | ... | @@ -1,12 +1,18 @@ |
| 1 | import { describe, expect, it, vi } from "vite-plus/test"; | 1 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; |
| 2 | import { EnvironmentId } from "@t3tools/contracts"; | 2 | import { EnvironmentId } from "@t3tools/contracts"; |
| 3 | 3 | ||
| 4 | import { | 4 | import { |
| 5 | isRelayManagedConnection, | 5 | isRelayManagedConnection, |
| 6 | authClientMetadata, | ||
| 7 | redactPairingCredential, | 6 | redactPairingCredential, |
| 8 | toStableSavedRemoteConnection, | 7 | toStableSavedRemoteConnection, |
| 9 | } from "./connection"; | 8 | } from "./connection"; |
| 9 | import { authClientMetadata } from "./authClientMetadata"; | ||
| 10 | |||
| 11 | const mobilePlatform = vi.hoisted(() => ({ OS: "ios" as "ios" | "android" })); | ||
| 12 | const mobileDevice = vi.hoisted(() => ({ | ||
| 13 | osVersion: "18.4.1", | ||
| 14 | modelName: "iPhone 15 Pro", | ||
| 15 | })); | ||
| 10 | 16 | ||
| 11 | vi.mock("./runtime", () => ({ | 17 | vi.mock("./runtime", () => ({ |
| 12 | runtime: { | 18 | runtime: { |
| ... | @@ -15,21 +21,41 @@ vi.mock("./runtime", () => ({ | ... | @@ -15,21 +21,41 @@ vi.mock("./runtime", () => ({ |
| 15 | })); | 21 | })); |
| 16 | 22 | ||
| 17 | vi.mock("react-native", () => ({ | 23 | vi.mock("react-native", () => ({ |
| 18 | Platform: { | 24 | Platform: mobilePlatform, |
| 19 | OS: "ios", | ||
| 20 | }, | ||
| 21 | })); | 25 | })); |
| 22 | 26 | ||
| 27 | vi.mock("expo-device", () => mobileDevice); | ||
| 28 | |||
| 23 | describe("mobile remote connection records", () => { | 29 | describe("mobile remote connection records", () => { |
| 30 | afterEach(() => { | ||
| 31 | mobilePlatform.OS = "ios"; | ||
| 32 | mobileDevice.osVersion = "18.4.1"; | ||
| 33 | mobileDevice.modelName = "iPhone 15 Pro"; | ||
| 34 | }); | ||
| 35 | |||
| 24 | it("identifies mobile token exchanges for authorized-client presentation", () => { | 36 | it("identifies mobile token exchanges for authorized-client presentation", () => { |
| 25 | expect(authClientMetadata()).toEqual({ | 37 | expect(authClientMetadata()).toEqual({ |
| 26 | label: "T3 Code Mobile", | 38 | label: "T3 Code Mobile", |
| 27 | deviceType: "mobile", | 39 | deviceType: "mobile", |
| 28 | os: "iOS", | 40 | os: "iOS", |
| 41 | osMajorVersion: 18, | ||
| 42 | deviceModel: "iPhone 15 Pro", | ||
| 29 | surface: "mobile", | 43 | surface: "mobile", |
| 30 | }); | 44 | }); |
| 31 | }); | 45 | }); |
| 32 | 46 | ||
| 47 | it("includes only the Android major version and hardware model", () => { | ||
| 48 | mobilePlatform.OS = "android"; | ||
| 49 | mobileDevice.osVersion = "15.2.1"; | ||
| 50 | mobileDevice.modelName = "Pixel 9"; | ||
| 51 | |||
| 52 | expect(authClientMetadata()).toMatchObject({ | ||
| 53 | os: "Android", | ||
| 54 | osMajorVersion: 15, | ||
| 55 | deviceModel: "Pixel 9", | ||
| 56 | }); | ||
| 57 | }); | ||
| 58 | |||
| 33 | it("includes the mobile app version when the client provides it", () => { | 59 | it("includes the mobile app version when the client provides it", () => { |
| 34 | expect(authClientMetadata("1.2.3")).toMatchObject({ | 60 | expect(authClientMetadata("1.2.3")).toMatchObject({ |
| 35 | surface: "mobile", | 61 | surface: "mobile", |
apps/mobile/src/lib/connection.ts-2| ... | @@ -2,8 +2,6 @@ import { EnvironmentId } from "@t3tools/contracts"; | ... | @@ -2,8 +2,6 @@ import { EnvironmentId } from "@t3tools/contracts"; |
| 2 | import { stripPairingTokenFromUrl } from "@t3tools/shared/remote"; | 2 | import { stripPairingTokenFromUrl } from "@t3tools/shared/remote"; |
| 3 | import { type EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection"; | 3 | import { type EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection"; |
| 4 | 4 | ||
| 5 | export { authClientMetadata } from "./authClientMetadata"; | ||
| 6 | |||
| 7 | export interface SavedRemoteConnection { | 5 | export interface SavedRemoteConnection { |
| 8 | readonly environmentId: EnvironmentId; | 6 | readonly environmentId: EnvironmentId; |
| 9 | readonly environmentLabel: string; | 7 | readonly environmentLabel: string; |
apps/server/src/server.test.ts+10-2| ... | @@ -5209,7 +5209,9 @@ it.layer(NodeServices.layer)("server router seam", (it) => { | ... | @@ -5209,7 +5209,9 @@ it.layer(NodeServices.layer)("server router seam", (it) => { |
| 5209 | createdAt: "2026-01-01T00:00:00.000Z", | 5209 | createdAt: "2026-01-01T00:00:00.000Z", |
| 5210 | }) as const; | 5210 | }) as const; |
| 5211 | 5211 | ||
| 5212 | const wsUrl = yield* getWsServerUrl("/ws?clientSurface=mobile&clientAppVersion=1.2.3"); | 5212 | const wsUrl = yield* getWsServerUrl( |
| 5213 | "/ws?clientSurface=mobile&clientAppVersion=1.2.3&clientOs=iOS&clientOsMajorVersion=18&clientDeviceModel=iPhone+15+Pro", | ||
| 5214 | ); | ||
| 5213 | yield* Effect.scoped( | 5215 | yield* Effect.scoped( |
| 5214 | withWsRpcClient(wsUrl, (client) => | 5216 | withWsRpcClient(wsUrl, (client) => |
| 5215 | Effect.gen(function* () { | 5217 | Effect.gen(function* () { |
| ... | @@ -5242,7 +5244,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => { | ... | @@ -5242,7 +5244,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => { |
| 5242 | "analytics:client.thread.started", | 5244 | "analytics:client.thread.started", |
| 5243 | ]); | 5245 | ]); |
| 5244 | assert.deepEqual(analyticsProperties, [ | 5246 | assert.deepEqual(analyticsProperties, [ |
| 5245 | { surface: "mobile", appVersion: "1.2.3" }, | 5247 | { |
| 5248 | surface: "mobile", | ||
| 5249 | appVersion: "1.2.3", | ||
| 5250 | os: "iOS", | ||
| 5251 | osMajorVersion: 18, | ||
| 5252 | deviceModel: "iPhone 15 Pro", | ||
| 5253 | }, | ||
| 5246 | { surface: "mobile", appVersion: "1.2.3" }, | 5254 | { surface: "mobile", appVersion: "1.2.3" }, |
| 5247 | ]); | 5255 | ]); |
| 5248 | }).pipe(Effect.provide(NodeHttpServer.layerTest)), | 5256 | }).pipe(Effect.provide(NodeHttpServer.layerTest)), |
apps/server/src/ws.ts+27-1| ... | @@ -360,6 +360,7 @@ function toAuthAccessStreamEvent( | ... | @@ -360,6 +360,7 @@ function toAuthAccessStreamEvent( |
| 360 | 360 | ||
| 361 | const isClientSurface = Schema.is(ClientSurface); | 361 | const isClientSurface = Schema.is(ClientSurface); |
| 362 | const MAX_CLIENT_APP_VERSION_LENGTH = 64; | 362 | const MAX_CLIENT_APP_VERSION_LENGTH = 64; |
| 363 | const MAX_CLIENT_DEVICE_MODEL_LENGTH = 80; | ||
| 363 | 364 | ||
| 364 | // Optional client identity announced on the /ws upgrade URL next to wsTicket. | 365 | // Optional client identity announced on the /ws upgrade URL next to wsTicket. |
| 365 | // Lenient by design: absent or malformed values degrade to {} so a connection | 366 | // Lenient by design: absent or malformed values degrade to {} so a connection |
| ... | @@ -386,6 +387,28 @@ const clientOriginAnalyticsProps = (origin: OrchestrationClientOrigin) => ({ | ... | @@ -386,6 +387,28 @@ const clientOriginAnalyticsProps = (origin: OrchestrationClientOrigin) => ({ |
| 386 | ...(origin.appVersion !== undefined ? { appVersion: origin.appVersion } : {}), | 387 | ...(origin.appVersion !== undefined ? { appVersion: origin.appVersion } : {}), |
| 387 | }); | 388 | }); |
| 388 | 389 | ||
| 390 | function readMobileDeviceAnalyticsProps(request: HttpServerRequest.HttpServerRequest) { | ||
| 391 | const url = HttpServerRequest.toURL(request); | ||
| 392 | if (Option.isNone(url) || url.value.searchParams.get("clientSurface") !== "mobile") { | ||
| 393 | return {}; | ||
| 394 | } | ||
| 395 | |||
| 396 | const os = url.value.searchParams.get("clientOs"); | ||
| 397 | const rawOsMajorVersion = url.value.searchParams.get("clientOsMajorVersion") ?? ""; | ||
| 398 | const osMajorVersion = Number(rawOsMajorVersion); | ||
| 399 | const deviceModel = url.value.searchParams.get("clientDeviceModel")?.trim() ?? ""; | ||
| 400 | |||
| 401 | return { | ||
| 402 | ...(os === "iOS" || os === "Android" ? { os } : {}), | ||
| 403 | ...(rawOsMajorVersion !== "" && Number.isInteger(osMajorVersion) && osMajorVersion > 0 | ||
| 404 | ? { osMajorVersion } | ||
| 405 | : {}), | ||
| 406 | ...(deviceModel !== "" && deviceModel.length <= MAX_CLIENT_DEVICE_MODEL_LENGTH | ||
| 407 | ? { deviceModel } | ||
| 408 | : {}), | ||
| 409 | }; | ||
| 410 | } | ||
| 411 | |||
| 389 | const makeWsRpcLayer = ( | 412 | const makeWsRpcLayer = ( |
| 390 | currentSession: EnvironmentAuth.AuthenticatedSession, | 413 | currentSession: EnvironmentAuth.AuthenticatedSession, |
| 391 | clientOrigin: OrchestrationClientOrigin, | 414 | clientOrigin: OrchestrationClientOrigin, |
| ... | @@ -2434,7 +2457,10 @@ export const websocketRpcRouteLayer = Layer.unwrap( | ... | @@ -2434,7 +2457,10 @@ export const websocketRpcRouteLayer = Layer.unwrap( |
| 2434 | ); | 2457 | ); |
| 2435 | const clientOrigin = readClientConnectionOrigin(request); | 2458 | const clientOrigin = readClientConnectionOrigin(request); |
| 2436 | yield* sessions.recordClientConnection(session.sessionId, clientOrigin); | 2459 | yield* sessions.recordClientConnection(session.sessionId, clientOrigin); |
| 2437 | yield* analytics.record("client.connected", clientOriginAnalyticsProps(clientOrigin)); | 2460 | yield* analytics.record("client.connected", { |
| 2461 | ...clientOriginAnalyticsProps(clientOrigin), | ||
| 2462 | ...readMobileDeviceAnalyticsProps(request), | ||
| 2463 | }); | ||
| 2438 | const rpcWebSocketHttpEffect = yield* RpcServer.toHttpEffectWebsocket(WsRpcGroup, { | 2464 | const rpcWebSocketHttpEffect = yield* RpcServer.toHttpEffectWebsocket(WsRpcGroup, { |
| 2439 | disableTracing: true, | 2465 | disableTracing: true, |
| 2440 | }).pipe( | 2466 | }).pipe( |
packages/client-runtime/src/authorization/remote.test.ts+10-1| ... | @@ -467,9 +467,18 @@ describe("remote environment authorization", () => { | ... | @@ -467,9 +467,18 @@ describe("remote environment authorization", () => { |
| 467 | wsBaseUrl: "wss://remote.example.com/", | 467 | wsBaseUrl: "wss://remote.example.com/", |
| 468 | httpBaseUrl: "https://remote.example.com/", | 468 | httpBaseUrl: "https://remote.example.com/", |
| 469 | bearerToken: "bearer-token", | 469 | bearerToken: "bearer-token", |
| 470 | clientMetadata: { | ||
| 471 | surface: "mobile", | ||
| 472 | appVersion: "1.2.3", | ||
| 473 | os: "Android", | ||
| 474 | osMajorVersion: 15, | ||
| 475 | deviceModel: "Pixel 9", | ||
| 476 | }, | ||
| 470 | }).pipe(provideRemoteHttp(fetch.fetchFn)); | 477 | }).pipe(provideRemoteHttp(fetch.fetchFn)); |
| 471 | 478 | ||
| 472 | expect(url).toBe("wss://remote.example.com/ws?wsTicket=ws-ticket"); | 479 | expect(url).toBe( |
| 480 | "wss://remote.example.com/ws?wsTicket=ws-ticket&clientSurface=mobile&clientAppVersion=1.2.3&clientOs=Android&clientOsMajorVersion=15&clientDeviceModel=Pixel+9", | ||
| 481 | ); | ||
| 473 | }), | 482 | }), |
| 474 | ); | 483 | ); |
| 475 | }); | 484 | }); |
packages/client-runtime/src/authorization/remote.ts+11| ... | @@ -44,6 +44,17 @@ export const appendClientConnectionParams = ( | ... | @@ -44,6 +44,17 @@ export const appendClientConnectionParams = ( |
| 44 | if (clientMetadata?.appVersion) { | 44 | if (clientMetadata?.appVersion) { |
| 45 | url.searchParams.set("clientAppVersion", clientMetadata.appVersion); | 45 | url.searchParams.set("clientAppVersion", clientMetadata.appVersion); |
| 46 | } | 46 | } |
| 47 | if (clientMetadata?.surface === "mobile") { | ||
| 48 | if (clientMetadata.os) { | ||
| 49 | url.searchParams.set("clientOs", clientMetadata.os); | ||
| 50 | } | ||
| 51 | if (clientMetadata.osMajorVersion !== undefined) { | ||
| 52 | url.searchParams.set("clientOsMajorVersion", String(clientMetadata.osMajorVersion)); | ||
| 53 | } | ||
| 54 | if (clientMetadata.deviceModel) { | ||
| 55 | url.searchParams.set("clientDeviceModel", clientMetadata.deviceModel); | ||
| 56 | } | ||
| 57 | } | ||
| 47 | }; | 58 | }; |
| 48 | 59 | ||
| 49 | export const exchangeRemoteDpopAccessToken = Effect.fn( | 60 | export const exchangeRemoteDpopAccessToken = Effect.fn( |
packages/contracts/src/auth.ts+2| ... | @@ -169,6 +169,8 @@ export const AuthClientPresentationMetadata = Schema.Struct({ | ... | @@ -169,6 +169,8 @@ export const AuthClientPresentationMetadata = Schema.Struct({ |
| 169 | label: Schema.optionalKey(TrimmedNonEmptyString), | 169 | label: Schema.optionalKey(TrimmedNonEmptyString), |
| 170 | deviceType: Schema.optionalKey(AuthClientMetadataDeviceType), | 170 | deviceType: Schema.optionalKey(AuthClientMetadataDeviceType), |
| 171 | os: Schema.optionalKey(TrimmedNonEmptyString), | 171 | os: Schema.optionalKey(TrimmedNonEmptyString), |
| 172 | osMajorVersion: Schema.optionalKey(Schema.Int), | ||
| 173 | deviceModel: Schema.optionalKey(TrimmedNonEmptyString), | ||
| 172 | surface: Schema.optionalKey(ClientSurface), | 174 | surface: Schema.optionalKey(ClientSurface), |
| 173 | appVersion: Schema.optionalKey(TrimmedNonEmptyString), | 175 | appVersion: Schema.optionalKey(TrimmedNonEmptyString), |
| 174 | }); | 176 | }); |
pnpm-lock.yaml+19| ... | @@ -318,6 +318,9 @@ importers: | ... | @@ -318,6 +318,9 @@ importers: |
| 318 | expo-dev-client: | 318 | expo-dev-client: |
| 319 | specifier: ~56.0.20 | 319 | specifier: ~56.0.20 |
| 320 | version: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) | 320 | version: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) |
| 321 | expo-device: | ||
| 322 | specifier: ~56.0.4 | ||
| 323 | version: 56.0.4(expo@56.0.12) | ||
| 321 | expo-file-system: | 324 | expo-file-system: |
| 322 | specifier: ~56.0.8 | 325 | specifier: ~56.0.8 |
| 323 | version: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) | 326 | version: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) |
| ... | @@ -6586,6 +6589,11 @@ packages: | ... | @@ -6586,6 +6589,11 @@ packages: |
| 6586 | expo: '*' | 6589 | expo: '*' |
| 6587 | react-native: '*' | 6590 | react-native: '*' |
| 6588 | 6591 | ||
| 6592 | expo-device@56.0.4: | ||
| 6593 | resolution: {integrity: sha512-ucVcGPkvBrl2QHuy7XcYex2Y6BETvJ6TREutZrwLGUDnlvbpKS8KfQoNZOpvkyo5Nmm9RrasYQ0CrXmBHho2mg==} | ||
| 6594 | peerDependencies: | ||
| 6595 | expo: '*' | ||
| 6596 | |||
| 6589 | expo-eas-client@56.0.1: | 6597 | expo-eas-client@56.0.1: |
| 6590 | resolution: {integrity: sha512-r8h0ZIExacCrSRgY+ARfhMvFqosLHLJt1L7jyhvabfr1DN/ZDKDsYbovss2tzkpEUZGxZ3BPcB5epCwUsBBdOA==} | 6598 | resolution: {integrity: sha512-r8h0ZIExacCrSRgY+ARfhMvFqosLHLJt1L7jyhvabfr1DN/ZDKDsYbovss2tzkpEUZGxZ3BPcB5epCwUsBBdOA==} |
| 6591 | 6599 | ||
| ... | @@ -9857,6 +9865,10 @@ packages: | ... | @@ -9857,6 +9865,10 @@ packages: |
| 9857 | engines: {node: '>=14.17'} | 9865 | engines: {node: '>=14.17'} |
| 9858 | hasBin: true | 9866 | hasBin: true |
| 9859 | 9867 | ||
| 9868 | ua-parser-js@0.7.41: | ||
| 9869 | resolution: {integrity: sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==} | ||
| 9870 | hasBin: true | ||
| 9871 | |||
| 9860 | ufo@1.6.4: | 9872 | ufo@1.6.4: |
| 9861 | resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} | 9873 | resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} |
| 9862 | 9874 | ||
| ... | @@ -16867,6 +16879,11 @@ snapshots: | ... | @@ -16867,6 +16879,11 @@ snapshots: |
| 16867 | expo-dev-menu-interface: 56.0.1(expo@56.0.12) | 16879 | expo-dev-menu-interface: 56.0.1(expo@56.0.12) |
| 16868 | react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) | 16880 | react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) |
| 16869 | 16881 | ||
| 16882 | expo-device@56.0.4(expo@56.0.12): | ||
| 16883 | dependencies: | ||
| 16884 | expo: 56.0.12(8895228379997a2a064f9644cda56ed0) | ||
| 16885 | ua-parser-js: 0.7.41 | ||
| 16886 | |||
| 16870 | expo-eas-client@56.0.1: {} | 16887 | expo-eas-client@56.0.1: {} |
| 16871 | 16888 | ||
| 16872 | expo-file-system@56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): | 16889 | expo-file-system@56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): |
| ... | @@ -21104,6 +21121,8 @@ snapshots: | ... | @@ -21104,6 +21121,8 @@ snapshots: |
| 21104 | 21121 | ||
| 21105 | typescript@6.0.3: {} | 21122 | typescript@6.0.3: {} |
| 21106 | 21123 | ||
| 21124 | ua-parser-js@0.7.41: {} | ||
| 21125 | |||
| 21107 | ufo@1.6.4: {} | 21126 | ufo@1.6.4: {} |
| 21108 | 21127 | ||
| 21109 | ultrahtml@1.6.0: {} | 21128 | ultrahtml@1.6.0: {} |