diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7609414..b8309d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,14 @@ jobs: - name: Run tests run: npm test + - name: Run canonical Bluesky archive contract tests + run: npm run test:archive-contract + + - name: Check for a newer canonical Bluesky archive bundle + env: + GITHUB_TOKEN: ${{ github.token }} + run: python3 scripts/archive-contract/check_latest.py + build_ios: name: Build iOS runs-on: macos-26 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 049c5b9..720fc6c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,3 +29,11 @@ jobs: - name: Run tests run: npm test + + - name: Run canonical Bluesky archive contract tests + run: npm run test:archive-contract + + - name: Check for a newer canonical Bluesky archive bundle + env: + GITHUB_TOKEN: ${{ github.token }} + run: python3 scripts/archive-contract/check_latest.py diff --git a/CONTEXT.md b/CONTEXT.md index 4a81f7a..b621b38 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -9,62 +9,66 @@ Mobile and desktop Cyd offer the same Bluesky account-management capabilities an _Avoid_: UI parity, identical clients **Cyd archive**: -A complete, self-contained, versioned snapshot of an account's saved data and media that supported Cyd clients can exchange without losing its meaning. -_Avoid_: HTML export, database backup +An export of one social-platform account's saved data. Each platform defines its own archive contents, behavior, compatibility guarantees, and version history, so references to a particular archive must name the platform. +_Avoid_: archive, platform-neutral archive -**Interchange database**: -The canonical representation of structured account data inside a Cyd archive. It is independent of each client's private runtime storage. -_Avoid_: runtime database, mobile database +**Cyd Bluesky archive**: +A Cyd archive governed by the Bluesky archive contract. Version 2 is the first supported Bluesky archive format; the earlier unversioned mobile prototype is not a compatibility surface. +_Avoid_: Cyd archive v2, platform-neutral v2 archive + +**Bluesky interchange database**: +The canonical representation of structured Bluesky account data inside a Cyd Bluesky archive. It is independent of each client's private runtime storage. +_Avoid_: interchange database, runtime database, mobile database **Bluesky identity**: A Bluesky account identified durably by its DID, even when its handle changes or separate Cyd installations know it by different local identifiers. _Avoid_: handle, Cyd UUID -**Local account**: -A client's local representation of a Bluesky identity, identified within Cyd by a UUID and containing that client's settings and saved data. -_Avoid_: Bluesky identity +**Bluesky local account**: +A client's local representation of a Bluesky identity, identified within Cyd by a UUID and containing that client's Bluesky account settings and Bluesky saved data. +_Avoid_: local account, Bluesky identity -**Archive import**: -An idempotent recovery merge of a Cyd archive into a matching local account, preserving the union of saved data while collapsing records that share stable Bluesky identifiers. It may restore data previously removed through local deletion. -_Avoid_: replace, synchronize +**Bluesky archive import**: +An idempotent recovery merge of a Cyd Bluesky archive into a matching Bluesky local account, preserving the union of Bluesky saved data while collapsing records that share stable Bluesky identifiers. It may restore data previously removed through Bluesky local deletion. +_Avoid_: archive import, replace, synchronize -**Account settings**: -Local preferences governing how Cyd saves and manages a Bluesky identity. An archive can supply defaults for a new local account, but does not silently override an existing local account's preferences or schedules. -_Avoid_: account data, archive state +**Bluesky account settings**: +Local preferences governing how Cyd saves and manages a Bluesky identity. A Cyd Bluesky archive can supply defaults for a new Bluesky local account, but does not silently override an existing Bluesky local account's preferences or schedules. +_Avoid_: account settings, account data, archive state -**Scheduled reminder**: -A prompt to review and start due account-management work. It does not authorize Cyd to perform deletion unattended; clients may use platform-appropriate delivery such as local notifications or server-scheduled push. -_Avoid_: scheduled job, automatic deletion +**Bluesky scheduled reminder**: +A prompt to review and start due Bluesky account-management work. It does not authorize Cyd to perform deletion unattended; clients may use platform-appropriate delivery such as local notifications or server-scheduled push. +_Avoid_: scheduled reminder, scheduled job, automatic deletion -**Connection**: -A local installation's authorization to act on a Bluesky identity. Connections are established separately on each client and are never part of a Cyd archive. -Disconnecting removes authorization without removing the local account or its saved data. -_Avoid_: account data, imported session, local account +**Bluesky connection**: +A local installation's authorization to act on a Bluesky identity. Bluesky connections are established separately on each client and are never part of a Cyd Bluesky archive. +Disconnecting removes authorization without removing the Bluesky local account or its Bluesky saved data. +_Avoid_: connection, account data, imported session, Bluesky local account -**Saved data**: -The account records and complete media Cyd has preserved locally, including material that may later disappear from Bluesky. When a record is selected for saving, its full media is part of the saved data regardless of the record category. -_Avoid_: live feed, HTML export +**Bluesky saved data**: +The Bluesky records and complete media Cyd has preserved locally, including material that may later disappear from Bluesky. When a record is selected for saving, its full media is part of the Bluesky saved data regardless of the record category. +_Avoid_: saved data, live feed, HTML export -**Saved record**: +**Bluesky saved record**: The latest representation of a Bluesky record observed by Cyd at a stable AT URI, together with its observation timestamps and deletion state. It is not a history of every CID revision. -_Avoid_: record revision, live record +_Avoid_: saved record, record revision, live record -**Browse**: -Inspect saved data inside Cyd without requiring a Bluesky connection or network access. -_Avoid_: view on Bluesky, live feed +**Bluesky browse**: +Inspection of Bluesky saved data inside Cyd without requiring a Bluesky connection or network access. +_Avoid_: browse, view on Bluesky, live feed -**Context snapshot**: -The directly referenced author, reply parent, quoted record, external embed, and other captured information required to render a saved record faithfully without recursively preserving the surrounding social graph. -_Avoid_: full thread, live lookup +**Bluesky context snapshot**: +The directly referenced author, reply parent, quoted record, external embed, and other captured information required to render a Bluesky saved record faithfully without recursively preserving the surrounding social graph. +_Avoid_: context snapshot, full thread, live lookup -**Complete backup**: -A saved dataset or Cyd archive containing every expected asset for its selected records. Missing assets are explicit and make the backup incomplete without invalidating the data that was successfully preserved. -_Avoid_: valid archive, successful export +**Complete Bluesky backup**: +Bluesky saved data or a Cyd Bluesky archive containing every expected asset for its selected records. Missing assets are explicit and make the backup incomplete without invalidating the data that was successfully preserved. +_Avoid_: complete backup, valid archive, successful export -**Source deletion**: -Removal of a record or relationship from Bluesky while retaining Cyd's saved copy and deletion state. -_Avoid_: local deletion +**Bluesky source deletion**: +Removal of a record or relationship from Bluesky while retaining its Bluesky saved record and deletion state in Cyd. +_Avoid_: source deletion, Bluesky local deletion -**Local deletion**: -An explicit removal of saved data from Cyd, independent of whether the source still exists on Bluesky. -_Avoid_: source deletion, stop saving +**Bluesky local deletion**: +An explicit removal of Bluesky saved data from Cyd, independent of whether the source still exists on Bluesky. +_Avoid_: local deletion, Bluesky source deletion, stop saving diff --git a/app.config.ts b/app.config.ts index 98b081e..11fc3b1 100644 --- a/app.config.ts +++ b/app.config.ts @@ -113,7 +113,6 @@ export default ({ config }: ConfigContext): ExpoConfig => { "@react-native-community/datetimepicker", "expo-image", "expo-localization", - "expo-sharing", "expo-sqlite", "expo-web-browser", "./plugins/remove-android-permissions", diff --git a/app/account/[accountId].tsx b/app/account/[accountId].tsx index c070051..63ad276 100644 --- a/app/account/[accountId].tsx +++ b/app/account/[accountId].tsx @@ -211,19 +211,6 @@ export default function AccountScreen() { router.replace("/"); }, [account, handleSignOut, router]); - const handleExportArchive = useCallback(async () => { - if (!account) { - throw new Error( - "Account information is not available yet. Please try again in a moment.", - ); - } - - await runWithAccountController(account, async (controller) => { - await controller.initAgent(); - await controller.exportArchive(); - }); - }, [account]); - // Reset auth status when the account disappears (adjusting state during // render, per React docs, instead of a synchronous setState in an effect). const [previousAccountForAuth, setPreviousAccountForAuth] = @@ -574,7 +561,6 @@ export default function AccountScreen() { onReauthenticate={handleReauthenticate} onSignOut={handleSignOut} onRemoveAccount={handleRemoveAccount} - onExportArchive={handleExportArchive} /> ); diff --git a/app/account/components/AccountMenuSheet.tsx b/app/account/components/AccountMenuSheet.tsx index 6435f63..12977cc 100644 --- a/app/account/components/AccountMenuSheet.tsx +++ b/app/account/components/AccountMenuSheet.tsx @@ -1,6 +1,5 @@ import { useCallback, useMemo, useState } from "react"; import { - ActivityIndicator, Alert, Linking, Modal, @@ -23,7 +22,6 @@ export type AccountMenuSheetProps = { onSignOut?: () => Promise; onReauthenticate?: () => Promise; onRemoveAccount?: () => Promise; - onExportArchive?: () => Promise; authStatus: "authenticated" | "signed_out" | "unknown"; }; @@ -56,11 +54,9 @@ export function AccountMenuSheet({ onSignOut, onReauthenticate, onRemoveAccount, - onExportArchive, authStatus, }: AccountMenuSheetProps) { const [pendingAction, setPendingAction] = useState(null); - const [isExporting, setIsExporting] = useState(false); const [signInModalVisible, setSignInModalVisible] = useState(false); const { state: cydState, @@ -74,12 +70,6 @@ export function AccountMenuSheet({ key: "bluesky-handle", label: `Bluesky account: ${handle}`, }, - { - type: "action", - key: "export-archive", - label: "Export Bluesky archive", - log: `Export archive tapped for ${handle}`, - }, ...(authStatus !== "authenticated" ? [ { @@ -153,34 +143,6 @@ export function AccountMenuSheet({ return; } - if (item.key === "export-archive") { - if (!onExportArchive) { - Alert.alert( - "Export unavailable", - "This action is not supported here.", - ); - onClose(); - return; - } - - setPendingAction(item.key); - setIsExporting(true); - try { - await onExportArchive(); - onClose(); - } catch (err) { - const message = - err instanceof Error - ? err.message - : "Unable to export archive right now."; - Alert.alert("Export failed", message); - } finally { - setIsExporting(false); - setPendingAction(null); - } - return; - } - setPendingAction(item.key); if (item.key === "signout") { if (!onSignOut) { @@ -302,7 +264,6 @@ export function AccountMenuSheet({ }, [ onClose, - onExportArchive, onReauthenticate, onRemoveAccount, onSignOut, @@ -403,18 +364,6 @@ export function AccountMenuSheet({ })} - {isExporting && ( - - - - - Preparing archive... - - - - )} { setMenuVisible(true); @@ -83,69 +71,6 @@ export function CydAccountBar({ onShowOnboarding?.(); }, [onShowOnboarding]); - const handleImportBlueskyAccount = useCallback(async () => { - if (isImporting) return; - setMenuVisible(false); - - // Delay to allow the modal to fully close before opening the document picker - await new Promise((resolve) => setTimeout(resolve, 50)); - - try { - // Pick a zip file - const pickedFile = await pickArchiveFile(); - if (!pickedFile) { - return; // User cancelled - } - - // Validate the filename - const filenameValidation = validateArchiveFilename(pickedFile.filename); - if (!filenameValidation.valid) { - Alert.alert("Import Failed", filenameValidation.error); - return; - } - - setIsImporting(true); - - // Validate the archive - const validation = await validateArchive(pickedFile.uri); - - if (!validation.valid) { - if (validation.tempDir) { - cleanupTempDir(validation.tempDir); - } - Alert.alert("Import Failed", validation.error); - return; - } - - // Import the archive - const result = await importArchive( - validation.metadata, - validation.tempDir, - ); - - // Clean up temp directory - cleanupTempDir(validation.tempDir); - - if (!result.success) { - Alert.alert("Import Failed", result.error); - return; - } - - // Refresh the accounts list - await refresh(); - - Alert.alert( - "Import Successful", - `Successfully imported archive for @${validation.metadata.account.handle}.`, - ); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - Alert.alert("Import Failed", message); - } finally { - setIsImporting(false); - } - }, [isImporting, refresh]); - if (state.isLoading || hidden) { return null; } @@ -259,30 +184,6 @@ export function CydAccountBar({ Sign out of Cyd account - - void handleImportBlueskyAccount()} - style={({ pressed }) => [ - styles.sheetActionButton, - { - borderColor: palette.icon + "22", - backgroundColor: palette.background, - opacity: pressed ? 0.9 : 1, - }, - ]} - accessibilityRole="button" - > - - Import Bluesky archive - - ) : ( <> @@ -304,24 +205,6 @@ export function CydAccountBar({ Sign in to Cyd to access premium features - void handleImportBlueskyAccount()} - style={({ pressed }) => [ - styles.sheetActionButton, - { - borderColor: palette.icon + "22", - backgroundColor: palette.background, - opacity: pressed ? 0.9 : 1, - }, - ]} - accessibilityRole="button" - > - - Import Bluesky archive - - )} - {/* Importing Modal */} - - - - - - Importing archive... - - - - ); } @@ -445,20 +307,4 @@ const styles = StyleSheet.create({ borderTopWidth: StyleSheet.hairlineWidth, marginVertical: 4, }, - loadingOverlay: { - flex: 1, - backgroundColor: "rgba(0, 0, 0, 0.5)", - justifyContent: "center", - alignItems: "center", - }, - loadingContainer: { - padding: 24, - borderRadius: 16, - alignItems: "center", - gap: 16, - }, - loadingText: { - fontSize: 16, - fontWeight: "500", - }, }); diff --git a/components/__tests__/CydAccountBar.test.tsx b/components/__tests__/CydAccountBar.test.tsx index 8ae5d8d..b15dfce 100644 --- a/components/__tests__/CydAccountBar.test.tsx +++ b/components/__tests__/CydAccountBar.test.tsx @@ -18,7 +18,6 @@ import { useCydAccount } from "@/contexts/CydAccountProvider"; // Mock the hooks const mockSignOut = jest.fn(); const mockGetDashboardURL = jest.fn(() => "https://dash.cyd.social"); -const mockRefresh = jest.fn(); jest.mock("@/contexts/CydAccountProvider", () => ({ useCydAccount: jest.fn(() => ({ @@ -32,27 +31,10 @@ jest.mock("@/contexts/CydAccountProvider", () => ({ })), })); -jest.mock("@/hooks/use-accounts", () => ({ - useAccounts: () => ({ - accounts: [], - loading: false, - error: null, - refresh: mockRefresh, - }), -})); - jest.mock("react-native-safe-area-context", () => ({ useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }), })); -jest.mock("@/services/archive-import", () => ({ - cleanupTempDir: jest.fn(), - importArchive: jest.fn(), - pickArchiveFile: jest.fn(), - validateArchive: jest.fn(), - validateArchiveFilename: jest.fn(), -})); - jest.mock("@/hooks/use-color-scheme", () => ({ useColorScheme: () => "light", })); diff --git a/controllers/BlueskyAccountController.ts b/controllers/BlueskyAccountController.ts index e1117c5..2d24bcd 100644 --- a/controllers/BlueskyAccountController.ts +++ b/controllers/BlueskyAccountController.ts @@ -5,9 +5,7 @@ import { } from "@atproto/api"; import type { OAuthSession } from "@atproto/oauth-client"; import { Directory, File, Paths } from "expo-file-system"; -import * as Sharing from "expo-sharing"; import type { SQLiteDatabase } from "expo-sqlite"; -import { zip } from "react-native-zip-archive"; import { getDatabase } from "@/database"; import { resolveBlobDownloadUrl } from "./bluesky/blob-url"; @@ -1751,170 +1749,6 @@ export class BlueskyAccountController extends BaseAccountController { - const handle = this.handle ?? "unknown"; - const dateStr = this.formatDateForArchive(new Date()); - const archiveBaseName = `Cyd-archive_${dateStr}_Bluesky_${handle}`; - const zipFileName = `${archiveBaseName}.zip`; - - const accountPaths = buildAccountPaths( - this.getAccountType(), - this.getAccountUUID(), - ); - - // Create a temporary directory for the archive contents - const cacheBase = Paths.cache?.uri ?? accountPaths.base; - const tempDir = new Directory(cacheBase, archiveBaseName); - const tempZipPath = `${cacheBase}${zipFileName}`; - - try { - // Create the temp directory with the archive name - if (tempDir.exists) { - tempDir.delete(); - } - tempDir.create({ intermediates: true, idempotent: true }); - - // Copy files from account directory to temp directory - const accountDir = new Directory(accountPaths.accountDir); - if (accountDir.exists) { - const items = accountDir.list(); - for (const item of items) { - if (item instanceof File) { - item.copy(new File(tempDir, item.name)); - } else if (item instanceof Directory) { - // Copy directories recursively - this.copyDirectoryRecursive( - item, - new Directory(tempDir, item.name), - ); - } - } - } - - // Add exportTimestamp and account data to metadata.json - const metadataFile = new File(tempDir, "metadata.json"); - if (metadataFile.exists) { - const metadataContent = await metadataFile.text(); - const metadata = JSON.parse(metadataContent) as Record; - metadata.exportTimestamp = new Date().toISOString(); - - // Fetch account data from main.db (excluding sensitive fields) - const mainDb = await getDatabase(); - const accountData = await mainDb.getFirstAsync<{ - createdAt: number; - updatedAt: number; - accessedAt: number | null; - handle: string; - displayName: string | null; - postsCount: number; - settingSavePosts: number; - settingSaveLikes: number; - settingSaveBookmarks: number; - settingSaveChats: number; - settingDeletePosts: number; - settingDeletePostsDaysOldEnabled: number; - settingDeletePostsDaysOld: number; - settingDeletePostsLikesThresholdEnabled: number; - settingDeletePostsLikesThreshold: number; - settingDeletePostsRepostsThresholdEnabled: number; - settingDeletePostsRepostsThreshold: number; - settingDeletePostsPreserveThreads: number; - settingDeleteReposts: number; - settingDeleteRepostsDaysOldEnabled: number; - settingDeleteRepostsDaysOld: number; - settingDeleteLikes: number; - settingDeleteLikesDaysOldEnabled: number; - settingDeleteLikesDaysOld: number; - settingDeleteChats: number; - settingDeleteChatsDaysOldEnabled: number; - settingDeleteChatsDaysOld: number; - settingDeleteBookmarks: number; - settingDeleteUnfollowEveryone: number; - avatarUrl: string | null; - did: string | null; - lastSavedAt: number | null; - }>( - `SELECT - b.createdAt, b.updatedAt, b.accessedAt, b.handle, b.displayName, - b.postsCount, b.settingSavePosts, b.settingSaveLikes, b.settingSaveBookmarks, - b.settingSaveChats, b.settingDeletePosts, b.settingDeletePostsDaysOldEnabled, - b.settingDeletePostsDaysOld, b.settingDeletePostsLikesThresholdEnabled, - b.settingDeletePostsLikesThreshold, b.settingDeletePostsRepostsThresholdEnabled, - b.settingDeletePostsRepostsThreshold, b.settingDeletePostsPreserveThreads, - b.settingDeleteReposts, b.settingDeleteRepostsDaysOldEnabled, - b.settingDeleteRepostsDaysOld, b.settingDeleteLikes, b.settingDeleteLikesDaysOldEnabled, - b.settingDeleteLikesDaysOld, b.settingDeleteChats, b.settingDeleteChatsDaysOldEnabled, - b.settingDeleteChatsDaysOld, b.settingDeleteBookmarks, b.settingDeleteUnfollowEveryone, - b.avatarUrl, b.did, b.lastSavedAt - FROM account a - INNER JOIN bsky_account b ON b.id = a.bskyAccountID - WHERE a.id = ?;`, - [this.accountId], - ); - - if (accountData) { - metadata.account = accountData; - } - - metadataFile.write(JSON.stringify(metadata, null, 2)); - } - - // Create the zip archive - await zip(tempDir.uri, tempZipPath); - - // Share the zip file - if (await Sharing.isAvailableAsync()) { - await Sharing.shareAsync(tempZipPath, { - mimeType: "application/zip", - dialogTitle: `Export ${handle} archive`, - }); - } else { - throw new Error("Sharing is not available on this device"); - } - } finally { - // Clean up temporary files - if (tempDir.exists) { - tempDir.delete(); - } - const tempZipFile = new File(tempZipPath); - if (tempZipFile.exists) { - tempZipFile.delete(); - } - } - } - - /** - * Recursively copy a directory and its contents - */ - private copyDirectoryRecursive(source: Directory, dest: Directory): void { - if (!dest.exists) { - dest.create({ intermediates: true, idempotent: true }); - } - const items = source.list(); - for (const item of items) { - if (item instanceof File) { - item.copy(new File(dest, item.name)); - } else if (item instanceof Directory) { - this.copyDirectoryRecursive(item, new Directory(dest, item.name)); - } - } - } - /** * Clean up resources */ diff --git a/controllers/__tests__/BlueskyAccountController.test.ts b/controllers/__tests__/BlueskyAccountController.test.ts index d19365b..7d4f82e 100644 --- a/controllers/__tests__/BlueskyAccountController.test.ts +++ b/controllers/__tests__/BlueskyAccountController.test.ts @@ -831,23 +831,7 @@ describe("BlueskyAccountController", () => { }); }); - describe("export archive", () => { - it("should format date correctly for archive name", async () => { - const controller = new BlueskyAccountController(1); - - // Access the private method via reflection for testing - const formatDate = ( - controller as unknown as { - formatDateForArchive: (date: Date) => string; - } - ).formatDateForArchive; - - const testDate = new Date(2026, 0, 10); // January 10, 2026 - const result = formatDate.call(controller, testDate); - - expect(result).toBe("2026-01-10"); - }); - + describe("media download", () => { it("should use unknown as handle when handle is null", async () => { const controller = new BlueskyAccountController(1); // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/docs/adr/0001-adopt-the-shared-cyd-contract.md b/docs/adr/0001-adopt-the-shared-cyd-contract.md index 49ecb92..ec193ef 100644 --- a/docs/adr/0001-adopt-the-shared-cyd-contract.md +++ b/docs/adr/0001-adopt-the-shared-cyd-contract.md @@ -1,3 +1,3 @@ # Adopt the shared Cyd contract -Cyd Mobile adopts the cross-client Bluesky semantics, archive specification, version history, canonical fixtures, and compatibility expectations owned by the `cyd` repository rather than maintaining a duplicate normative contract. Mobile records only its implementation-specific decisions locally, so shared behavior has one source of truth while mobile constraints remain discoverable in this repository. +Cyd Mobile adopts the cross-client Cyd Bluesky archive specification, Bluesky version history, canonical fixtures, and compatibility expectations owned by the `cyd` repository rather than maintaining a duplicate normative contract. Other social platforms have independent archive contracts and version histories. Mobile records only its implementation-specific decisions locally, so shared behavior has one source of truth while mobile constraints remain discoverable in this repository. diff --git a/docs/adr/0002-translate-archives-through-private-runtime-storage.md b/docs/adr/0002-translate-archives-through-private-runtime-storage.md index 7c20ddc..0421d19 100644 --- a/docs/adr/0002-translate-archives-through-private-runtime-storage.md +++ b/docs/adr/0002-translate-archives-through-private-runtime-storage.md @@ -1,3 +1,3 @@ # Translate archives through private runtime storage -Cyd Mobile retains and migrates its existing private per-account runtime database rather than replacing it with the version 2 interchange schema. Dedicated version 2 adapters translate archive data at the boundary, allowing installed mobile data and mobile-specific persistence to evolve without coupling runtime migrations to the shared archive contract. +Cyd Mobile retains and migrates its existing private per-account runtime database rather than replacing it with the Bluesky version 2 interchange schema. Dedicated Bluesky version 2 adapters translate archive data at the boundary, allowing installed mobile data and mobile-specific persistence to evolve without coupling runtime migrations to the shared Bluesky archive contract. diff --git a/docs/adr/0004-gate-v2-writing-on-complete-local-media.md b/docs/adr/0004-gate-v2-writing-on-complete-local-media.md index 55650db..c534e17 100644 --- a/docs/adr/0004-gate-v2-writing-on-complete-local-media.md +++ b/docs/adr/0004-gate-v2-writing-on-complete-local-media.md @@ -1,3 +1,3 @@ -# Gate version 2 writing on complete local media +# Gate Bluesky version 2 writing on complete local media -Cyd Mobile may implement and release a version 2 archive reader before it can preserve all selected media locally, but it does not enable its version 2 writer or claim complete version 2 conformance until issue #90 covers full-size images and full video. Specification work and reader rollout remain unblocked; the gate prevents mobile from producing nominally portable backups that still depend on live Bluesky media. +Cyd Mobile may implement and release a Bluesky version 2 archive reader before it can preserve all selected media locally, but it does not enable its Bluesky version 2 writer or claim complete Bluesky version 2 conformance until issue #90 covers full-size images and full video. Specification work and reader rollout remain unblocked; the gate prevents mobile from producing nominally portable backups that still depend on live Bluesky media. diff --git a/docs/adr/0007-deduplicate-media-within-each-account.md b/docs/adr/0007-deduplicate-media-within-each-account.md index f8bfc0c..00ab76f 100644 --- a/docs/adr/0007-deduplicate-media-within-each-account.md +++ b/docs/adr/0007-deduplicate-media-within-each-account.md @@ -1,3 +1,3 @@ -# Deduplicate media within each local account +# Deduplicate media within each Bluesky local account -Cyd Mobile stores full-size images and video content-addressably within each UUID-keyed local account, and saved records reference those assets rather than owning duplicate files. It does not deduplicate across accounts, so deleting, importing, or exporting one account never depends on another account’s storage while version 2 archives can package each unique asset once. +Cyd Mobile stores full-size images and video content-addressably within each UUID-keyed Bluesky local account, and Bluesky saved records reference those assets rather than owning duplicate files. It does not deduplicate across Bluesky local accounts, so deleting, importing, or exporting one Bluesky local account never depends on another Bluesky local account’s storage while Cyd Bluesky archives at version 2 can package each unique asset once. diff --git a/docs/adr/0008-pin-versioned-archive-contract-bundles.md b/docs/adr/0008-pin-versioned-archive-contract-bundles.md index 56e58ed..564f155 100644 --- a/docs/adr/0008-pin-versioned-archive-contract-bundles.md +++ b/docs/adr/0008-pin-versioned-archive-contract-bundles.md @@ -1,3 +1,3 @@ # Pin versioned archive contract bundles -Cyd Mobile validates archive behavior against immutable, versioned contract-test bundles published from the `cyd` repository, containing the normative specification, canonical fixtures, and expected normalized results. CI pins an explicit bundle for reproducibility and reports newer available bundles; mobile does not depend on a sibling checkout, a moving branch, or shared production runtime code. +Cyd Mobile validates Cyd Bluesky archive behavior against immutable, versioned Bluesky contract-test bundles published from the `cyd` repository, containing the normative specification, canonical fixtures, and expected normalized results. CI pins an explicit bundle for reproducibility and reports newer available bundles; mobile does not depend on a sibling checkout, a moving branch, or shared production runtime code. diff --git a/docs/adr/0009-stream-and-defensively-validate-archive-imports.md b/docs/adr/0009-stream-and-defensively-validate-archive-imports.md index 88770b6..793d90c 100644 --- a/docs/adr/0009-stream-and-defensively-validate-archive-imports.md +++ b/docs/adr/0009-stream-and-defensively-validate-archive-imports.md @@ -1,3 +1,3 @@ # Stream and defensively validate archive imports -Cyd Mobile’s version 2 importer reads ZIP entries incrementally into an isolated staging root and validates normalized paths, entry types, counts, declared sizes, digests, expansion, and available storage before commit. An extract-all API is insufficient; mobile may keep its ZIP library only if it exposes these controls, otherwise it uses a narrow replacement or native adapter so traversal, symlinks, forged manifests, and resource-exhaustion archives are rejected safely. +Cyd Mobile’s Bluesky version 2 importer reads ZIP entries incrementally into an isolated staging root and validates normalized paths, entry types, counts, declared sizes, digests, expansion, and available storage before commit. An extract-all API is insufficient; mobile may keep its ZIP library only if it exposes these controls, otherwise it uses a narrow replacement or native adapter so traversal, symlinks, forged manifests, and resource-exhaustion archives are rejected safely. diff --git a/docs/adr/0012-abandon-the-unreleased-v1-archive.md b/docs/adr/0012-abandon-the-unreleased-v1-archive.md index 9722bab..e4a262e 100644 --- a/docs/adr/0012-abandon-the-unreleased-v1-archive.md +++ b/docs/adr/0012-abandon-the-unreleased-v1-archive.md @@ -1,3 +1,3 @@ # Abandon the unreleased version 1 archive -Cyd Mobile neither imports nor exports its unversioned prototype archive, retrospectively called version 1, because it has no users and exposes mobile’s private runtime schema rather than the canonical interchange contract. Version 2 is the first supported Cyd archive; removing v1 compatibility avoids maintaining an unsafe filename-dependent extract-and-copy path or constraining the cross-client design around unused data. +Cyd Mobile neither imports nor exports its unversioned Bluesky prototype archive, retrospectively called version 1, because it has no users and exposes mobile’s private runtime schema rather than the canonical interchange contract. Version 2 is the first supported Cyd Bluesky archive format; removing prototype compatibility avoids maintaining an unsafe filename-dependent extract-and-copy path or constraining the cross-client design around unused data. diff --git a/docs/adr/0013-encrypt-connections-not-saved-content.md b/docs/adr/0013-encrypt-connections-not-saved-content.md index 3cdde8d..7f5e40e 100644 --- a/docs/adr/0013-encrypt-connections-not-saved-content.md +++ b/docs/adr/0013-encrypt-connections-not-saved-content.md @@ -1,3 +1,3 @@ -# Encrypt connections, not saved content +# Encrypt Bluesky connections, not Bluesky saved data -Cyd Mobile does not add application-level encryption for runtime databases, chats, or media in the initial version 2 work; saved content relies on the mobile operating system’s app sandbox, access controls, and device encryption. OAuth connection material remains the protected-storage exception, while plaintext version 2 export explicitly warns that an archive may contain sensitive saved content; archive encryption requires a future format and recovery design. +Cyd Mobile does not add application-level encryption for its private runtime databases, chats, or media in the initial Bluesky version 2 work; Bluesky saved data relies on the mobile operating system’s app sandbox, access controls, and device encryption. Bluesky connection material remains the protected-storage exception, while plaintext Cyd Bluesky archive version 2 export explicitly warns that a Cyd Bluesky archive may contain sensitive Bluesky saved data; Cyd Bluesky archive encryption requires a future Bluesky format and recovery design. diff --git a/jest-setup.ts b/jest-setup.ts index 8488a09..e25cf88 100644 --- a/jest-setup.ts +++ b/jest-setup.ts @@ -321,18 +321,6 @@ jest.mock("react-native-svg", () => ({ Stop: "Stop", })); -// Mock expo-sharing -jest.mock("expo-sharing", () => ({ - isAvailableAsync: jest.fn(() => Promise.resolve(true)), - shareAsync: jest.fn(() => Promise.resolve()), -})); - -// Mock react-native-zip-archive -jest.mock("react-native-zip-archive", () => ({ - zip: jest.fn((source: string, target: string) => Promise.resolve(target)), - unzip: jest.fn((source: string, target: string) => Promise.resolve(target)), -})); - // Silence console warnings in tests global.console = { ...console, diff --git a/package-lock.json b/package-lock.json index 89abb0a..8bf4b1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "expo-crypto": "~57.0.0", "expo-dev-client": "~57.0.5", "expo-device": "~57.0.0", - "expo-document-picker": "~57.0.0", "expo-font": "~57.0.0", "expo-haptics": "~57.0.0", "expo-iap": "^4.3.6", @@ -38,7 +37,6 @@ "expo-localization": "~57.0.0", "expo-notifications": "~57.0.3", "expo-router": "~57.0.3", - "expo-sharing": "~57.0.2", "expo-splash-screen": "~57.0.2", "expo-sqlite": "~57.0.0", "expo-status-bar": "~57.0.0", @@ -58,7 +56,6 @@ "react-native-svg": "15.15.4", "react-native-web": "~0.21.2", "react-native-worklets": "0.10.0", - "react-native-zip-archive": "^7.0.2", "text-encoding": "^0.7.0" }, "devDependencies": { @@ -861,7 +858,7 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -874,7 +871,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -887,7 +884,7 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" @@ -900,7 +897,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -989,7 +986,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" @@ -1005,7 +1002,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -1018,7 +1015,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1046,7 +1043,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -1071,7 +1068,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -1084,7 +1081,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1097,7 +1094,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1122,7 +1119,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -1138,7 +1135,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -2396,7 +2393,7 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@craftzdog/react-native-buffer": { @@ -3445,7 +3442,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "camelcase": "^5.3.1", @@ -3462,7 +3459,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3472,7 +3469,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -3490,7 +3487,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -3503,7 +3500,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -3521,14 +3518,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@jest/console/node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -3546,7 +3543,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -3559,7 +3556,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -3607,7 +3604,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -3620,7 +3617,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -3647,7 +3644,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -3665,14 +3662,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@jest/core/node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -3689,7 +3686,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", @@ -3706,7 +3703,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -3732,7 +3729,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -3742,7 +3739,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -3760,7 +3757,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -3773,7 +3770,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -3797,7 +3794,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/fake-timers": "^29.7.0", @@ -3813,7 +3810,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -3826,7 +3823,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -3844,14 +3841,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@jest/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "expect": "^29.7.0", @@ -3865,7 +3862,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3" @@ -3878,7 +3875,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -3896,7 +3893,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -3909,7 +3906,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -3927,14 +3924,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@jest/fake-timers/node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -3952,7 +3949,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -3975,7 +3972,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -3991,7 +3988,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -4004,7 +4001,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -4022,7 +4019,7 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@jest/pattern": { @@ -4043,7 +4040,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -4087,7 +4084,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -4100,7 +4097,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -4127,7 +4124,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -4145,14 +4142,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@jest/reporters/node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -4169,7 +4166,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", @@ -4187,7 +4184,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -4208,7 +4205,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -4234,7 +4231,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4244,7 +4241,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -4262,7 +4259,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -4275,7 +4272,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -4302,7 +4299,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", @@ -4317,7 +4314,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -4333,7 +4330,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -4346,7 +4343,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -4364,14 +4361,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@jest/test-sequencer": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -4387,7 +4384,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -4400,7 +4397,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -4418,14 +4415,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@jest/test-sequencer/node_modules/jest-haste-map": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -4451,7 +4448,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4461,7 +4458,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -4479,7 +4476,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -5463,7 +5460,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" @@ -5473,7 +5470,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" @@ -5975,7 +5972,7 @@ "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -5989,7 +5986,7 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -5999,7 +5996,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -6010,7 +6007,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.2" @@ -6027,7 +6024,7 @@ "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -6119,7 +6116,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/strip-bom": { @@ -6558,9 +6555,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6575,9 +6569,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6592,9 +6583,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6609,9 +6597,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6626,9 +6611,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6643,9 +6625,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6660,9 +6639,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6677,9 +6653,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6694,9 +6667,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6711,9 +6681,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6938,7 +6905,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -6952,7 +6919,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -7326,7 +7293,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", @@ -7443,7 +7410,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -7528,7 +7495,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7686,7 +7653,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7696,7 +7663,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7751,7 +7718,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -7807,7 +7774,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/cli-cursor": { @@ -7867,7 +7834,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "iojs": ">= 1.0.0", @@ -7878,7 +7845,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/color": { @@ -7989,7 +7956,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/connect": { @@ -8113,7 +8080,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -8135,7 +8102,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -8148,7 +8115,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -8166,14 +8133,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/create-jest/node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -8191,7 +8158,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -8455,7 +8422,7 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -8570,7 +8537,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8586,7 +8553,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -8743,7 +8710,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -8777,7 +8744,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -9474,7 +9441,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -9561,7 +9528,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -9585,7 +9552,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "devOptional": true, + "dev": true, "engines": { "node": ">= 0.8.0" } @@ -9594,7 +9561,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.7.0", @@ -9611,7 +9578,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -9624,7 +9591,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -9642,14 +9609,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/expect/node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -9667,7 +9634,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -9904,15 +9871,6 @@ "expo": "*" } }, - "node_modules/expo-document-picker": { - "version": "57.0.0", - "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-57.0.0.tgz", - "integrity": "sha512-foT1pA3tD7KvcqXwjQBh4qEDjHT9U9MhZ6H2aF38VolCiHRhXDQLtKQqCeXjbHxJhSf8HC/YPV3IRxDxFTL2sA==", - "license": "MIT", - "peerDependencies": { - "expo": "*" - } - }, "node_modules/expo-file-system": { "version": "57.0.0", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.0.tgz", @@ -10195,31 +10153,15 @@ "node": ">=20.16.0" } }, - "node_modules/expo-sharing": { + "node_modules/expo-splash-screen": { "version": "57.0.2", - "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-57.0.2.tgz", - "integrity": "sha512-tMyTkAjQb4QFQjQlTYBvQgEGHa3rCxSUKLzd15x3DVVczUkoDbfvZowSfNgz45Lyc+J6FanuNBKA1ho3dJc78A==", + "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-57.0.2.tgz", + "integrity": "sha512-xyfl+Twl5EiFKxYLKlofJi/r7mvdxwr/w9olUWYL4viEEpYCPCvj2YzZQtPptr/wMFe4G+X97W42sRWq6zdSfA==", "license": "MIT", "dependencies": { - "@expo/config-plugins": "^57.0.2", - "@expo/config-types": "^57.0.1", - "@expo/plist": "^0.8.0" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo-splash-screen": { - "version": "57.0.2", - "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-57.0.2.tgz", - "integrity": "sha512-xyfl+Twl5EiFKxYLKlofJi/r7mvdxwr/w9olUWYL4viEEpYCPCvj2YzZQtPptr/wMFe4G+X97W42sRWq6zdSfA==", - "license": "MIT", - "dependencies": { - "@expo/config-plugins": "~57.0.2", - "@expo/image-utils": "^0.11.1", - "xml2js": "0.6.0" + "@expo/config-plugins": "~57.0.2", + "@expo/image-utils": "^0.11.1", + "xml2js": "0.6.0" }, "peerDependencies": { "expo": "*" @@ -10549,7 +10491,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -10754,7 +10696,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -10868,13 +10810,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -11022,7 +10965,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.0.0" @@ -11045,7 +10988,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -11458,7 +11401,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/http-errors": { @@ -11507,7 +11450,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -11594,7 +11537,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", @@ -11614,7 +11557,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -11634,7 +11577,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -11718,7 +11661,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -11929,7 +11872,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -12079,7 +12022,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12219,7 +12162,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=8" @@ -12229,7 +12172,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.23.9", @@ -12246,7 +12189,7 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -12259,7 +12202,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", @@ -12274,7 +12217,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", @@ -12289,7 +12232,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", @@ -12337,7 +12280,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", @@ -12364,7 +12307,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "execa": "^5.0.0", @@ -12379,7 +12322,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -12392,7 +12335,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -12410,14 +12353,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jest-changed-files/node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -12435,7 +12378,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -12448,7 +12391,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -12480,7 +12423,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -12493,7 +12436,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -12511,14 +12454,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jest-circus/node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -12536,7 +12479,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -12549,7 +12492,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", @@ -12583,7 +12526,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -12596,7 +12539,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -12614,14 +12557,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jest-cli/node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -12639,7 +12582,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -12652,7 +12595,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -12698,7 +12641,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -12711,7 +12654,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -12738,7 +12681,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -12756,14 +12699,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jest-config/node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/transform": "^29.7.0", @@ -12785,7 +12728,7 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -12802,7 +12745,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", @@ -12818,7 +12761,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", @@ -12836,7 +12779,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -12857,7 +12800,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", @@ -12874,7 +12817,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -12900,7 +12843,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -12910,7 +12853,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -12928,7 +12871,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -12941,7 +12884,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -12955,7 +12898,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -12971,7 +12914,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" @@ -12984,7 +12927,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -13001,7 +12944,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -13014,7 +12957,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -13032,14 +12975,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jest-each/node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -13057,7 +13000,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -13070,7 +13013,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -13088,7 +13031,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -13101,7 +13044,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -13119,14 +13062,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jest-environment-node/node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -13144,7 +13087,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -13224,7 +13167,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3", @@ -13238,7 +13181,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -13254,7 +13197,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", @@ -13275,7 +13218,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -13288,7 +13231,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -13306,14 +13249,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jest-mock": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -13328,7 +13271,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -13341,7 +13284,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -13359,14 +13302,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jest-mock/node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -13384,7 +13327,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -13397,7 +13340,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -13425,7 +13368,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -13446,7 +13389,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "^29.6.3", @@ -13460,7 +13403,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -13470,7 +13413,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -13483,7 +13426,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -13501,14 +13444,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jest-resolve/node_modules/jest-haste-map": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -13534,7 +13477,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -13544,7 +13487,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -13562,7 +13505,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -13575,7 +13518,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -13608,7 +13551,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -13621,7 +13564,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -13648,7 +13591,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -13666,14 +13609,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jest-runner/node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -13690,7 +13633,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", @@ -13707,7 +13650,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -13733,7 +13676,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -13743,7 +13686,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -13761,7 +13704,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -13774,7 +13717,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -13788,7 +13731,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -13822,7 +13765,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -13835,7 +13778,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -13862,7 +13805,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -13880,14 +13823,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jest-runtime/node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -13905,7 +13848,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -13926,7 +13869,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", @@ -13943,7 +13886,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -13969,7 +13912,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -13979,7 +13922,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -13997,7 +13940,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -14010,7 +13953,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -14024,7 +13967,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -14056,7 +13999,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -14069,7 +14012,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -14096,7 +14039,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -14114,14 +14057,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jest-snapshot/node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -14138,7 +14081,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", @@ -14155,7 +14098,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -14165,7 +14108,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -14191,7 +14134,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -14201,7 +14144,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -14219,7 +14162,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -14232,7 +14175,7 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -14245,7 +14188,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -14357,7 +14300,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -14377,7 +14320,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -14390,7 +14333,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -14408,14 +14351,14 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jest-watcher/node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -14433,7 +14376,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -14540,7 +14483,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -14553,7 +14496,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -14571,7 +14514,7 @@ "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/jimp-compact": { @@ -14599,7 +14542,7 @@ "version": "3.15.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -14648,7 +14591,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -14928,9 +14871,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -14951,9 +14891,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -14974,9 +14911,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -14997,9 +14931,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -15057,7 +14988,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/linkify-it": { @@ -15073,7 +15004,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -15226,7 +15157,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" @@ -15242,7 +15173,7 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -15724,7 +15655,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -15743,7 +15674,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -15839,7 +15770,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/negotiator": { @@ -15949,7 +15880,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15986,7 +15917,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" @@ -16172,7 +16103,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -16182,7 +16113,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -16359,7 +16290,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -16375,7 +16306,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -16388,7 +16319,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -16404,7 +16335,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -16434,7 +16365,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -16481,7 +16412,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16491,7 +16422,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16569,7 +16500,7 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -16579,7 +16510,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" @@ -16803,7 +16734,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "individual", @@ -17273,16 +17204,6 @@ "node": ">=10" } }, - "node_modules/react-native-zip-archive": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/react-native-zip-archive/-/react-native-zip-archive-7.1.1.tgz", - "integrity": "sha512-rlRn8iZCwURU6R1MlrukQT0W84d62UijBmhL8PF32so5J06auDpry+Tmb2dsBEpLKhXCtZgG60uBXukV3hqY3A==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.6", - "react-native": ">=0.60.0" - } - }, "node_modules/react-native/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -17572,7 +17493,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" @@ -17610,7 +17531,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -18169,7 +18090,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18217,7 +18138,7 @@ "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -18250,7 +18171,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" @@ -18263,7 +18184,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18356,7 +18277,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "char-regex": "^1.0.2", @@ -18525,7 +18446,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18535,7 +18456,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -18557,7 +18478,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18723,7 +18644,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", @@ -18739,7 +18660,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -18938,7 +18859,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -19375,7 +19296,7 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", @@ -19638,7 +19559,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -19794,7 +19715,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/package.json b/package.json index 0565c2b..b41eb9b 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "typecheck": "tsc --noEmit", "lint": "expo lint && tsc --noEmit", "test": "jest", + "test:archive-contract": "python3 scripts/archive-contract/test_bundle.py", "test:watch": "jest --watch", "test:coverage": "jest --coverage" }, @@ -41,7 +42,6 @@ "expo-crypto": "~57.0.0", "expo-dev-client": "~57.0.5", "expo-device": "~57.0.0", - "expo-document-picker": "~57.0.0", "expo-font": "~57.0.0", "expo-haptics": "~57.0.0", "expo-iap": "^4.3.6", @@ -51,7 +51,6 @@ "expo-localization": "~57.0.0", "expo-notifications": "~57.0.3", "expo-router": "~57.0.3", - "expo-sharing": "~57.0.2", "expo-splash-screen": "~57.0.2", "expo-sqlite": "~57.0.0", "expo-status-bar": "~57.0.0", @@ -71,7 +70,6 @@ "react-native-svg": "15.15.4", "react-native-web": "~0.21.2", "react-native-worklets": "0.10.0", - "react-native-zip-archive": "^7.0.2", "text-encoding": "^0.7.0" }, "devDependencies": { @@ -95,15 +93,6 @@ "typescript": "~6.0.3", "typescript-eslint": "^8.62.1" }, - "expo": { - "doctor": { - "reactNativeDirectoryCheck": { - "exclude": [ - "react-native-zip-archive" - ] - } - } - }, "private": true, "allowScripts": { "fsevents@2.3.3": true, diff --git a/scripts/archive-contract/check_latest.py b/scripts/archive-contract/check_latest.py new file mode 100644 index 0000000..31ae798 --- /dev/null +++ b/scripts/archive-contract/check_latest.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Report whether cyd has a newer canonical Bluesky contract bundle.""" + +from __future__ import annotations + +import json +import os +import urllib.parse +import urllib.request +from pathlib import Path + + +script_root = Path(__file__).resolve().parent +pin = json.loads((script_root / "pin.json").read_text(encoding="utf-8")) +query = urllib.parse.urlencode({"path": pin["path"], "per_page": 1}) +request = urllib.request.Request( + f"https://api.github.com/repos/{pin['repository']}/commits?{query}", + headers={ + "Accept": "application/vnd.github+json", + "User-Agent": "cyd-mobile-ci", + **( + {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"} + if os.environ.get("GITHUB_TOKEN") + else {} + ), + }, +) +with urllib.request.urlopen(request, timeout=30) as response: + latest = json.load(response)[0] + +if latest["sha"] == pin["commit"]: + print(f"Canonical Bluesky archive bundle is current at {pin['commit']}.") +else: + print( + "::warning title=New canonical Bluesky archive bundle available::" + f"Pinned {pin['commit']}; latest is {latest['sha']} ({latest['html_url']})." + ) diff --git a/scripts/archive-contract/pin.json b/scripts/archive-contract/pin.json new file mode 100644 index 0000000..d48adf2 --- /dev/null +++ b/scripts/archive-contract/pin.json @@ -0,0 +1,5 @@ +{ + "repository": "lockdown-systems/cyd", + "commit": "cbd261484d0f3cac4f1c997c423db7730d5d3009", + "path": "docs/archive/bluesky/v2" +} diff --git a/scripts/archive-contract/test_bundle.py b/scripts/archive-contract/test_bundle.py new file mode 100644 index 0000000..0675e45 --- /dev/null +++ b/scripts/archive-contract/test_bundle.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Verify Mobile against the pinned canonical Cyd Bluesky archive bundle.""" + +from __future__ import annotations + +import hashlib +import json +import os +import sqlite3 +import subprocess +import tempfile +import urllib.request +import zipfile +from pathlib import Path +from typing import Any + + +SCRIPT_ROOT = Path(__file__).resolve().parent +PIN = json.loads((SCRIPT_ROOT / "pin.json").read_text(encoding="utf-8")) +FILES = ( + "fixtures/complete.cyd", + "fixtures/incomplete.cyd", + "fixtures/semantic-expectations.json", +) + + +def download_bundle(destination: Path) -> None: + base_url = ( + f"https://raw.githubusercontent.com/{PIN['repository']}/" + f"{PIN['commit']}/{PIN['path']}" + ) + for relative_path in FILES: + output = destination / relative_path + output.parent.mkdir(parents=True, exist_ok=True) + request = urllib.request.Request( + f"{base_url}/{relative_path}", headers={"User-Agent": "cyd-mobile-ci"} + ) + with urllib.request.urlopen(request, timeout=30) as response: + output.write_bytes(response.read()) + + +def rows(database: sqlite3.Connection, query: str) -> list[dict[str, Any]]: + return [dict(row) for row in database.execute(query).fetchall()] + + +def raw_mobile_tables(database: sqlite3.Connection) -> dict[str, Any]: + queries = { + "archive": "SELECT * FROM archive", + "identity": "SELECT * FROM identity", + "profiles": "SELECT * FROM profiles ORDER BY id", + "records": "SELECT * FROM records ORDER BY uri", + "selections": "SELECT * FROM selections ORDER BY category", + "record_subjects": "SELECT * FROM record_subjects ORDER BY relationship_uri", + "record_context": "SELECT * FROM record_context ORDER BY record_uri, kind", + "conversations": "SELECT * FROM conversations ORDER BY id", + "conversation_members": ( + "SELECT * FROM conversation_members ORDER BY conversation_id, profile_id" + ), + "messages": "SELECT * FROM messages ORDER BY id", + "relationships": "SELECT * FROM relationships ORDER BY uri", + "record_assets": ( + "SELECT * FROM record_assets " + "ORDER BY owner_type, owner_id, role, position" + ), + "portable_settings": "SELECT * FROM portable_settings ORDER BY key", + "assets": "SELECT * FROM assets ORDER BY id", + } + return {table: rows(database, query) for table, query in queries.items()} + + +def prepare_fixture(bundle: Path, fixture_name: str) -> None: + archive_path = bundle / "fixtures" / fixture_name + with tempfile.TemporaryDirectory(prefix="cyd-bluesky-v2-") as extracted: + with zipfile.ZipFile(archive_path) as archive: + archive.extractall(extracted) + entries = { + info.filename: archive.read(info) + for info in archive.infolist() + if not info.is_dir() + } + + metadata = json.loads(entries["metadata.json"]) + manifest = json.loads(entries["manifest.json"]) + assert [payload["path"] for payload in manifest["payloads"]] == sorted( + path for path in entries if path != "manifest.json" + ) + for payload in manifest["payloads"]: + content = entries[payload["path"]] + assert len(content) == payload["bytes"] + assert hashlib.sha256(content).hexdigest() == payload["sha256"] + + database = sqlite3.connect(Path(extracted) / "data.db") + database.row_factory = sqlite3.Row + try: + mobile_input = bundle / "mobile-input" / f"{fixture_name}.json" + mobile_input.parent.mkdir(parents=True, exist_ok=True) + mobile_input.write_text( + json.dumps( + {"metadata": metadata, "tables": raw_mobile_tables(database)} + ), + encoding="utf-8", + ) + finally: + database.close() + + +def main() -> None: + with tempfile.TemporaryDirectory(prefix="cyd-contract-") as directory: + bundle = Path(directory) + download_bundle(bundle) + prepare_fixture(bundle, "complete.cyd") + prepare_fixture(bundle, "incomplete.cyd") + subprocess.run( + [ + "npm", + "test", + "--", + "--runInBand", + "services/__tests__/archive-contract-bundle.test.ts", + ], + check=True, + env={**os.environ, "CYD_BLUESKY_CONTRACT_ROOT": str(bundle)}, + ) + print(f"Canonical Bluesky archive semantics match {PIN['commit']}.") + + +if __name__ == "__main__": + main() diff --git a/services/__tests__/archive-contract-bundle.test.ts b/services/__tests__/archive-contract-bundle.test.ts new file mode 100644 index 0000000..5759b6b --- /dev/null +++ b/services/__tests__/archive-contract-bundle.test.ts @@ -0,0 +1,93 @@ +import fs from "fs"; +import path from "path"; + +import { classifyBlueskyArchiveMetadata } from "../archive-metadata"; +import { + type BlueskyArchiveTables, + normalizeBlueskyArchiveSemantics, +} from "../archive-semantics"; + +const contractRoot = process.env.CYD_BLUESKY_CONTRACT_ROOT; +const contractDescribe = contractRoot ? describe : describe.skip; + +type ContractInput = { + metadata: Record; + tables: BlueskyArchiveTables; +}; + +function readExpectations(root: string) { + return JSON.parse( + fs.readFileSync( + path.join(root, "fixtures/semantic-expectations.json"), + "utf8", + ), + ) as { + commonSemantics: Record; + fixtures: Record< + string, + { assets: Record[]; completeness: string } + >; + desktopBlueskyVersionBehavior: Record; + }; +} + +contractDescribe("pinned canonical Cyd Bluesky archive bundle", () => { + const root = contractRoot!; + + it.each(["complete.cyd", "incomplete.cyd"])( + "normalizes %s through Mobile semantics", + (fixtureName) => { + const expectations = readExpectations(root); + const input = JSON.parse( + fs.readFileSync( + path.join(root, "mobile-input", `${fixtureName}.json`), + "utf8", + ), + ) as ContractInput; + + expect(classifyBlueskyArchiveMetadata(input.metadata)).toEqual({ + supported: true, + metadata: input.metadata, + }); + + const normalized = normalizeBlueskyArchiveSemantics(input.tables); + expect(normalized.commonSemantics).toEqual(expectations.commonSemantics); + expect(normalized.assets).toEqual(expectations.fixtures[fixtureName].assets); + expect(normalized.completeness).toBe( + expectations.fixtures[fixtureName].completeness, + ); + }, + ); + + it("matches the canonical version rejection outcomes", () => { + const expectations = readExpectations(root); + const input = JSON.parse( + fs.readFileSync( + path.join(root, "mobile-input", "complete.cyd.json"), + "utf8", + ), + ) as ContractInput; + + expect(expectations.desktopBlueskyVersionBehavior).toMatchObject({ + unversionedV1: "reject_unsupported_legacy_format", + blueskyV2Import: "accept", + otherPlatform: "reject_unsupported_archive_platform", + newerVersion: "reject_unsupported_newer_version", + }); + + expect( + classifyBlueskyArchiveMetadata({ ...input.metadata, version: 3 }), + ).toMatchObject({ supported: false, reason: "newer-version" }); + expect( + classifyBlueskyArchiveMetadata({ ...input.metadata, platform: "x" }), + ).toMatchObject({ supported: false, reason: "platform" }); + expect( + classifyBlueskyArchiveMetadata({ + type: "bluesky", + uuid: input.metadata.accountUuid, + exportTimestamp: input.metadata.createdAt, + account: {}, + }), + ).toMatchObject({ supported: false, reason: "legacy" }); + }); +}); diff --git a/services/__tests__/archive-import.test.ts b/services/__tests__/archive-import.test.ts deleted file mode 100644 index 954c1d7..0000000 --- a/services/__tests__/archive-import.test.ts +++ /dev/null @@ -1,717 +0,0 @@ -/** - * @fileoverview Tests for archive-import service - */ - -import * as Crypto from "expo-crypto"; -import * as DocumentPicker from "expo-document-picker"; -import { unzip } from "react-native-zip-archive"; - -import { getDatabase } from "@/database"; -import { - accountExistsWithUuid, - ArchiveMetadata, - cleanupTempDir, - importArchive, - pickArchiveFile, - validateArchive, - validateArchiveFilename, -} from "../archive-import"; - -// Mock expo-crypto -jest.mock("expo-crypto", () => ({ - randomUUID: jest.fn(() => "test-random-uuid"), -})); - -// Mock expo-document-picker -jest.mock("expo-document-picker", () => ({ - getDocumentAsync: jest.fn(), -})); - -// Mock react-native-zip-archive -jest.mock("react-native-zip-archive", () => ({ - unzip: jest.fn(), -})); - -// Mock database -jest.mock("@/database", () => ({ - getDatabase: jest.fn(), -})); - -// Mock expo-file-system -const mockFileInstances = new Map< - string, - { exists: boolean; content?: string } ->(); -const mockDirInstances = new Map< - string, - { exists: boolean; items: { type: "file" | "dir"; name: string }[] } ->(); - -let mockFileWriteData: { path: string; content: string }[] = []; -let mockFileCopyData: { from: string; to: string }[] = []; -let mockDirCreateData: string[] = []; -let mockDirDeleteData: string[] = []; - -jest.mock("expo-file-system", () => { - return { - Paths: { - cache: { uri: "/cache/" }, - }, - Directory: jest - .fn() - .mockImplementation((path: string, subPath?: string) => { - const fullPath = subPath ? `${path}/${subPath}` : path; - return { - uri: fullPath, - get exists() { - const data = mockDirInstances.get(fullPath); - return data?.exists ?? false; - }, - list: (): unknown[] => { - const data = mockDirInstances.get(fullPath); - if (!data) return []; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const expoFs = jest.requireMock("expo-file-system"); - return data.items.map((item) => { - if (item.type === "file") { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - return new expoFs.File(fullPath, item.name) as unknown; - } else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - return new expoFs.Directory(fullPath, item.name) as unknown; - } - }); - }, - delete: () => { - mockDirDeleteData.push(fullPath); - }, - create: () => { - mockDirCreateData.push(fullPath); - }, - name: fullPath.split("/").pop() || "", - }; - }), - File: jest.fn().mockImplementation((dir: unknown, name?: string) => { - const dirUri = - typeof dir === "string" ? dir : (dir as { uri: string }).uri; - const fullPath = name ? `${dirUri}/${name}` : dirUri; - return { - uri: fullPath, - name: fullPath.split("/").pop() || "", - get exists() { - const data = mockFileInstances.get(fullPath); - return data?.exists ?? false; - }, - text: async () => { - const data = mockFileInstances.get(fullPath); - return data?.content ?? ""; - }, - write: (content: string) => { - mockFileWriteData.push({ path: fullPath, content }); - }, - copy: (destFile: { uri: string }) => { - mockFileCopyData.push({ from: fullPath, to: destFile.uri }); - }, - }; - }), - }; -}); - -describe("archive-import", () => { - beforeEach(() => { - jest.clearAllMocks(); - mockFileInstances.clear(); - mockDirInstances.clear(); - mockFileWriteData = []; - mockFileCopyData = []; - mockDirCreateData = []; - mockDirDeleteData = []; - }); - - describe("pickArchiveFile", () => { - it("should return null when user cancels", async () => { - (DocumentPicker.getDocumentAsync as jest.Mock).mockResolvedValue({ - canceled: true, - assets: [], - }); - - const result = await pickArchiveFile(); - expect(result).toBeNull(); - }); - - it("should return null when no assets selected", async () => { - (DocumentPicker.getDocumentAsync as jest.Mock).mockResolvedValue({ - canceled: false, - assets: [], - }); - - const result = await pickArchiveFile(); - expect(result).toBeNull(); - }); - - it("should return the file URI and filename when a file is selected", async () => { - (DocumentPicker.getDocumentAsync as jest.Mock).mockResolvedValue({ - canceled: false, - assets: [ - { - uri: "/path/to/archive.zip", - name: "Cyd-archive_2026-01-10_Bluesky_test.zip", - }, - ], - }); - - const result = await pickArchiveFile(); - expect(result).toEqual({ - uri: "/path/to/archive.zip", - filename: "Cyd-archive_2026-01-10_Bluesky_test.zip", - }); - }); - - it("should request zip files only", async () => { - (DocumentPicker.getDocumentAsync as jest.Mock).mockResolvedValue({ - canceled: true, - assets: [], - }); - - await pickArchiveFile(); - - expect(DocumentPicker.getDocumentAsync).toHaveBeenCalledWith({ - type: "application/zip", - copyToCacheDirectory: true, - }); - }); - }); - - describe("validateArchiveFilename", () => { - it("should accept valid filename with simple handle", () => { - const result = validateArchiveFilename( - "Cyd-archive_2026-01-10_Bluesky_test.bsky.social.zip" - ); - expect(result.valid).toBe(true); - if (result.valid) { - expect(result.filename).toBe( - "Cyd-archive_2026-01-10_Bluesky_test.bsky.social.zip" - ); - } - }); - - it("should accept valid filename with complex handle", () => { - const result = validateArchiveFilename( - "Cyd-archive_2025-12-31_Bluesky_my-user_name.bsky.social.zip" - ); - expect(result.valid).toBe(true); - }); - - it("should accept filename with hyphens and underscores in handle", () => { - const result = validateArchiveFilename( - "Cyd-archive_2026-01-10_Bluesky_nexamind-cyd.bsky.social.zip" - ); - expect(result.valid).toBe(true); - }); - - it("should reject filename without Cyd-archive prefix", () => { - const result = validateArchiveFilename( - "archive_2026-01-10_Bluesky_test.bsky.social.zip" - ); - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toContain("Invalid archive filename"); - } - }); - - it("should reject filename with wrong date format", () => { - const result = validateArchiveFilename( - "Cyd-archive_01-10-2026_Bluesky_test.bsky.social.zip" - ); - expect(result.valid).toBe(false); - }); - - it("should reject filename without Bluesky marker", () => { - const result = validateArchiveFilename( - "Cyd-archive_2026-01-10_Twitter_test.bsky.social.zip" - ); - expect(result.valid).toBe(false); - }); - - it("should reject filename without handle", () => { - const result = validateArchiveFilename( - "Cyd-archive_2026-01-10_Bluesky_.zip" - ); - expect(result.valid).toBe(false); - }); - - it("should reject filename without .zip extension", () => { - const result = validateArchiveFilename( - "Cyd-archive_2026-01-10_Bluesky_test.bsky.social" - ); - expect(result.valid).toBe(false); - }); - - it("should reject random zip filename", () => { - const result = validateArchiveFilename("my-backup.zip"); - expect(result.valid).toBe(false); - }); - - it("should return error when filename is empty", () => { - const result = validateArchiveFilename(""); - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toContain("Could not determine filename"); - } - }); - }); - - describe("cleanupTempDir", () => { - it("should delete the directory if it exists", () => { - mockDirInstances.set("/temp/test-dir", { exists: true, items: [] }); - - cleanupTempDir("/temp/test-dir"); - - expect(mockDirDeleteData).toContain("/temp/test-dir"); - }); - - it("should not throw if directory does not exist", () => { - mockDirInstances.set("/temp/test-dir", { exists: false, items: [] }); - - expect(() => cleanupTempDir("/temp/test-dir")).not.toThrow(); - }); - }); - - describe("validateArchive", () => { - const validMetadata = { - type: "bluesky", - uuid: "d628f5e2-44d2-465c-8b96-3529a614c2d1", - exportTimestamp: "2026-01-11T05:28:46.071Z", - account: { - createdAt: 1768107605109, - updatedAt: 1768107611531, - accessedAt: 1768109325856, - handle: "test.bsky.social", - displayName: "Test User", - postsCount: 100, - settingSavePosts: 1, - settingSaveLikes: 1, - settingSaveBookmarks: 1, - settingSaveChats: 1, - settingDeletePosts: 1, - settingDeletePostsDaysOldEnabled: 1, - settingDeletePostsDaysOld: 30, - settingDeletePostsLikesThresholdEnabled: 0, - settingDeletePostsLikesThreshold: 100, - settingDeletePostsRepostsThresholdEnabled: 0, - settingDeletePostsRepostsThreshold: 100, - settingDeletePostsPreserveThreads: 1, - settingDeleteReposts: 1, - settingDeleteRepostsDaysOldEnabled: 1, - settingDeleteRepostsDaysOld: 30, - settingDeleteLikes: 1, - settingDeleteLikesDaysOldEnabled: 1, - settingDeleteLikesDaysOld: 30, - settingDeleteChats: 1, - settingDeleteChatsDaysOldEnabled: 1, - settingDeleteChatsDaysOld: 14, - settingDeleteBookmarks: 0, - settingDeleteUnfollowEveryone: 0, - avatarUrl: null, - did: "did:plc:test123", - lastSavedAt: 1768107650382, - }, - }; - - beforeEach(() => { - (Crypto.randomUUID as jest.Mock).mockReturnValue("test-random-uuid"); - (unzip as jest.Mock).mockResolvedValue(undefined); - }); - - it("should return error if root directory does not exist after unzip", async () => { - mockDirInstances.set("/cache/archive-import-test-random-uuid", { - exists: false, - items: [], - }); - - const result = await validateArchive("/path/to/archive.zip"); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toBe("Failed to extract archive"); - } - }); - - it("should return error if metadata.json is missing", async () => { - const tempDir = "/cache/archive-import-test-random-uuid"; - mockDirInstances.set(tempDir, { exists: true, items: [] }); - mockFileInstances.set(`${tempDir}/metadata.json`, { exists: false }); - mockFileInstances.set(`${tempDir}/data.db`, { exists: true }); - - const result = await validateArchive("/path/to/archive.zip"); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toContain("missing metadata.json"); - } - }); - - it("should return error if data.db is missing", async () => { - const tempDir = "/cache/archive-import-test-random-uuid"; - mockDirInstances.set(tempDir, { exists: true, items: [] }); - mockFileInstances.set(`${tempDir}/metadata.json`, { - exists: true, - content: JSON.stringify(validMetadata), - }); - mockFileInstances.set(`${tempDir}/data.db`, { exists: false }); - - const result = await validateArchive("/path/to/archive.zip"); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toContain("missing data.db"); - } - }); - - it("should return error if metadata.json is not valid JSON", async () => { - const tempDir = "/cache/archive-import-test-random-uuid"; - mockDirInstances.set(tempDir, { exists: true, items: [] }); - mockFileInstances.set(`${tempDir}/metadata.json`, { - exists: true, - content: "not valid json {{{", - }); - mockFileInstances.set(`${tempDir}/data.db`, { exists: true }); - - const result = await validateArchive("/path/to/archive.zip"); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toContain("not valid JSON"); - } - }); - - it("should return error if type is not bluesky", async () => { - const tempDir = "/cache/archive-import-test-random-uuid"; - mockDirInstances.set(tempDir, { exists: true, items: [] }); - mockFileInstances.set(`${tempDir}/metadata.json`, { - exists: true, - content: JSON.stringify({ ...validMetadata, type: "twitter" }), - }); - mockFileInstances.set(`${tempDir}/data.db`, { exists: true }); - - const result = await validateArchive("/path/to/archive.zip"); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toContain("only supports importing Bluesky"); - } - }); - - it("should return error if uuid is invalid", async () => { - const tempDir = "/cache/archive-import-test-random-uuid"; - mockDirInstances.set(tempDir, { exists: true, items: [] }); - mockFileInstances.set(`${tempDir}/metadata.json`, { - exists: true, - content: JSON.stringify({ ...validMetadata, uuid: "not-a-uuid" }), - }); - mockFileInstances.set(`${tempDir}/data.db`, { exists: true }); - - const result = await validateArchive("/path/to/archive.zip"); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toContain("not a valid UUID"); - } - }); - - it("should return error if exportTimestamp is invalid", async () => { - const tempDir = "/cache/archive-import-test-random-uuid"; - mockDirInstances.set(tempDir, { exists: true, items: [] }); - mockFileInstances.set(`${tempDir}/metadata.json`, { - exists: true, - content: JSON.stringify({ - ...validMetadata, - exportTimestamp: "not-a-date", - }), - }); - mockFileInstances.set(`${tempDir}/data.db`, { exists: true }); - - const result = await validateArchive("/path/to/archive.zip"); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toContain("not a valid ISO timestamp"); - } - }); - - it("should return error if account object is missing", async () => { - const tempDir = "/cache/archive-import-test-random-uuid"; - const { account: _account, ...metadataWithoutAccount } = validMetadata; - mockDirInstances.set(tempDir, { exists: true, items: [] }); - mockFileInstances.set(`${tempDir}/metadata.json`, { - exists: true, - content: JSON.stringify(metadataWithoutAccount), - }); - mockFileInstances.set(`${tempDir}/data.db`, { exists: true }); - - const result = await validateArchive("/path/to/archive.zip"); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toContain("'account' object"); - } - }); - - it("should return error if required account field is missing", async () => { - const tempDir = "/cache/archive-import-test-random-uuid"; - const { handle: _handle, ...accountWithoutHandle } = - validMetadata.account; - mockDirInstances.set(tempDir, { exists: true, items: [] }); - mockFileInstances.set(`${tempDir}/metadata.json`, { - exists: true, - content: JSON.stringify({ - ...validMetadata, - account: accountWithoutHandle, - }), - }); - mockFileInstances.set(`${tempDir}/data.db`, { exists: true }); - - const result = await validateArchive("/path/to/archive.zip"); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toContain("missing required field 'handle'"); - } - }); - - it("should return valid result for valid metadata", async () => { - const tempDir = "/cache/archive-import-test-random-uuid"; - mockDirInstances.set(tempDir, { exists: true, items: [] }); - mockFileInstances.set(`${tempDir}/metadata.json`, { - exists: true, - content: JSON.stringify(validMetadata), - }); - mockFileInstances.set(`${tempDir}/data.db`, { exists: true }); - - const result = await validateArchive("/path/to/archive.zip"); - - expect(result.valid).toBe(true); - if (result.valid) { - expect(result.metadata.type).toBe("bluesky"); - expect(result.metadata.uuid).toBe( - "d628f5e2-44d2-465c-8b96-3529a614c2d1" - ); - expect(result.metadata.account.handle).toBe("test.bsky.social"); - } - }); - }); - - describe("accountExistsWithUuid", () => { - it("should return true if account exists", async () => { - const mockDb = { - getFirstAsync: jest.fn().mockResolvedValue({ count: 1 }), - }; - (getDatabase as jest.Mock).mockResolvedValue(mockDb); - - const result = await accountExistsWithUuid("test-uuid"); - - expect(result).toBe(true); - expect(mockDb.getFirstAsync).toHaveBeenCalledWith( - "SELECT COUNT(*) as count FROM account WHERE uuid = ?;", - ["test-uuid"] - ); - }); - - it("should return false if account does not exist", async () => { - const mockDb = { - getFirstAsync: jest.fn().mockResolvedValue({ count: 0 }), - }; - (getDatabase as jest.Mock).mockResolvedValue(mockDb); - - const result = await accountExistsWithUuid("test-uuid"); - - expect(result).toBe(false); - }); - - it("should return false if query returns null", async () => { - const mockDb = { - getFirstAsync: jest.fn().mockResolvedValue(null), - }; - (getDatabase as jest.Mock).mockResolvedValue(mockDb); - - const result = await accountExistsWithUuid("test-uuid"); - - expect(result).toBe(false); - }); - }); - - describe("importArchive", () => { - const validMetadata: ArchiveMetadata = { - type: "bluesky", - uuid: "d628f5e2-44d2-465c-8b96-3529a614c2d1", - exportTimestamp: "2026-01-11T05:28:46.071Z", - account: { - createdAt: 1768107605109, - updatedAt: 1768107611531, - accessedAt: 1768109325856, - handle: "test.bsky.social", - displayName: "Test User", - postsCount: 100, - settingSavePosts: 1, - settingSaveLikes: 1, - settingSaveBookmarks: 1, - settingSaveChats: 1, - settingDeletePosts: 1, - settingDeletePostsDaysOldEnabled: 1, - settingDeletePostsDaysOld: 30, - settingDeletePostsLikesThresholdEnabled: 0, - settingDeletePostsLikesThreshold: 100, - settingDeletePostsRepostsThresholdEnabled: 0, - settingDeletePostsRepostsThreshold: 100, - settingDeletePostsPreserveThreads: 1, - settingDeleteReposts: 1, - settingDeleteRepostsDaysOldEnabled: 1, - settingDeleteRepostsDaysOld: 30, - settingDeleteLikes: 1, - settingDeleteLikesDaysOldEnabled: 1, - settingDeleteLikesDaysOld: 30, - settingDeleteChats: 1, - settingDeleteChatsDaysOldEnabled: 1, - settingDeleteChatsDaysOld: 14, - settingDeleteBookmarks: 0, - settingDeleteUnfollowEveryone: 0, - avatarUrl: null, - did: "did:plc:test123", - lastSavedAt: 1768107650382, - }, - }; - - it("should return error if account with same UUID already exists", async () => { - const mockDb = { - getFirstAsync: jest.fn().mockResolvedValue({ count: 1 }), - }; - (getDatabase as jest.Mock).mockResolvedValue(mockDb); - - const result = await importArchive(validMetadata, "/temp/archive"); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error).toContain("already have this account"); - } - }); - - it("should import archive successfully", async () => { - const mockDb = { - getFirstAsync: jest - .fn() - .mockResolvedValueOnce({ count: 0 }) // accountExistsWithUuid check - .mockResolvedValueOnce({ nextOrder: 0 }), // sortOrder query - runAsync: jest - .fn() - .mockResolvedValueOnce({ lastInsertRowId: 123 }) // bsky_account insert - .mockResolvedValueOnce({}), // account insert - }; - (getDatabase as jest.Mock).mockResolvedValue(mockDb); - - // Setup mock directory and files - const tempDir = "/temp/archive"; - mockDirInstances.set(tempDir, { - exists: true, - items: [ - { type: "file", name: "data.db" }, - { type: "file", name: "metadata.json" }, - { type: "dir", name: "media" }, - ], - }); - mockDirInstances.set(`${tempDir}/media`, { - exists: true, - items: [{ type: "file", name: "image.jpg" }], - }); - - // Mock the account directory as not existing initially - const accountDir = `/accounts/bluesky-${validMetadata.uuid}`; - mockDirInstances.set(accountDir, { exists: false, items: [] }); - - const result = await importArchive(validMetadata, tempDir); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.accountUuid).toBe(validMetadata.uuid); - } - - // Verify bsky_account was inserted with correct values - expect(mockDb.runAsync).toHaveBeenCalledTimes(2); - const bskyInsertCall = mockDb.runAsync.mock.calls[0] as [ - string, - unknown[], - ]; - expect(bskyInsertCall[0]).toContain("INSERT INTO bsky_account"); - expect(bskyInsertCall[1]).toContain(validMetadata.account.handle); - - // Verify account was inserted with correct values - const accountInsertCall = mockDb.runAsync.mock.calls[1] as [ - string, - unknown[], - ]; - expect(accountInsertCall[0]).toContain("INSERT INTO account"); - expect(accountInsertCall[1]).toEqual([validMetadata.uuid, 0, 123]); - - // Verify metadata.json was written - expect( - mockFileWriteData.some((w) => w.path.includes("metadata.json")) - ).toBe(true); - }); - - it("should write fresh metadata.json without account data after import", async () => { - const mockDb = { - getFirstAsync: jest - .fn() - .mockResolvedValueOnce({ count: 0 }) - .mockResolvedValueOnce({ nextOrder: 0 }), - runAsync: jest - .fn() - .mockResolvedValueOnce({ lastInsertRowId: 123 }) - .mockResolvedValueOnce({}), - }; - (getDatabase as jest.Mock).mockResolvedValue(mockDb); - - const tempDir = "/temp/archive"; - mockDirInstances.set(tempDir, { - exists: true, - items: [], - }); - - const accountDir = `/accounts/bluesky-${validMetadata.uuid}`; - mockDirInstances.set(accountDir, { exists: false, items: [] }); - - await importArchive(validMetadata, tempDir); - - // Find the metadata.json write - const metadataWrite = mockFileWriteData.find((w) => - w.path.includes("metadata.json") - ); - expect(metadataWrite).toBeDefined(); - - // Verify it only contains type and uuid (not account data) - const parsedMetadata = JSON.parse(metadataWrite!.content) as { - type?: string; - uuid?: string; - account?: unknown; - exportTimestamp?: string; - }; - expect(parsedMetadata.type).toBe("bluesky"); - expect(parsedMetadata.uuid).toBe(validMetadata.uuid); - expect(parsedMetadata.account).toBeUndefined(); - expect(parsedMetadata.exportTimestamp).toBeUndefined(); - }); - - it("should handle database errors gracefully", async () => { - const mockDb = { - getFirstAsync: jest.fn().mockRejectedValue(new Error("Database error")), - }; - (getDatabase as jest.Mock).mockResolvedValue(mockDb); - - const result = await importArchive(validMetadata, "/temp/archive"); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error).toContain("Database error"); - } - }); - }); -}); diff --git a/services/__tests__/archive-metadata.test.ts b/services/__tests__/archive-metadata.test.ts new file mode 100644 index 0000000..1ded3da --- /dev/null +++ b/services/__tests__/archive-metadata.test.ts @@ -0,0 +1,92 @@ +import { classifyBlueskyArchiveMetadata } from "../archive-metadata"; + +const canonicalMetadata = { + format: "cyd-archive", + platform: "bluesky", + version: 2, + createdAt: "2026-01-15T12:00:00.000Z", + accountDid: "did:plc:canonicalalice", + accountUuid: "018d5f7a-9b3c-7d10-8a2e-1f4c6b8d0e12", + completeness: "complete", +}; + +describe("Cyd Bluesky archive metadata", () => { + it("recognizes canonical Bluesky v2 metadata from internal content", () => { + expect(classifyBlueskyArchiveMetadata(canonicalMetadata)).toEqual({ + supported: true, + metadata: canonicalMetadata, + }); + }); + + it("ignores reserved unknown fields", () => { + expect( + classifyBlueskyArchiveMetadata({ + ...canonicalMetadata, + type: "bluesky", + futureOptionalField: "ignored", + }), + ).toEqual({ supported: true, metadata: canonicalMetadata }); + }); + + it("rejects the unversioned mobile prototype explicitly", () => { + const result = classifyBlueskyArchiveMetadata({ + type: "bluesky", + uuid: "d628f5e2-44d2-465c-8b96-3529a614c2d1", + exportTimestamp: "2026-01-11T05:28:46.071Z", + account: { handle: "alice.test" }, + }); + + expect(result).toEqual({ + supported: false, + reason: "legacy", + error: "Unsupported legacy Bluesky archive format.", + }); + }); + + it("rejects newer Bluesky archive versions explicitly", () => { + expect( + classifyBlueskyArchiveMetadata({ ...canonicalMetadata, version: 3 }), + ).toEqual({ + supported: false, + reason: "newer-version", + error: "Unsupported newer Bluesky archive version 3.", + }); + }); + + it("rejects another platform explicitly", () => { + expect( + classifyBlueskyArchiveMetadata({ + ...canonicalMetadata, + platform: "mastodon", + }), + ).toEqual({ + supported: false, + reason: "platform", + error: "Unsupported archive platform: mastodon.", + }); + }); + + it.each([ + ["a missing discriminator", {}], + ["a malformed version", { ...canonicalMetadata, version: "2" }], + ["an invalid timestamp", { ...canonicalMetadata, createdAt: "today" }], + [ + "a nonexistent calendar timestamp", + { ...canonicalMetadata, createdAt: "2026-02-30T12:00:00.000Z" }, + ], + ["an invalid DID", { ...canonicalMetadata, accountDid: "alice.test" }], + ["an invalid UUID", { ...canonicalMetadata, accountUuid: "not-a-uuid" }], + [ + "an invalid completeness value", + { ...canonicalMetadata, completeness: "mostly-complete" }, + ], + ])("rejects %s as invalid or corrupt", (_description, metadata) => { + const result = classifyBlueskyArchiveMetadata(metadata); + + expect(result.supported).toBe(false); + if (!result.supported) { + expect(result.reason).toBe("invalid"); + expect(result.error).toMatch(/^Invalid or corrupt Bluesky archive:/); + } + }); +}); diff --git a/services/__tests__/archive-semantics.test.ts b/services/__tests__/archive-semantics.test.ts new file mode 100644 index 0000000..f0032bc --- /dev/null +++ b/services/__tests__/archive-semantics.test.ts @@ -0,0 +1,55 @@ +import { normalizeBlueskyArchiveSemantics } from "../archive-semantics"; + +describe("Bluesky archive semantic normalization", () => { + it("translates canonical wire names without exposing table layout to consumers", () => { + expect( + normalizeBlueskyArchiveSemantics({ + archive: [ + { + created_at: "2026-01-15T12:00:00.000Z", + account_did: "did:plc:alice", + account_uuid: "018d5f7a-9b3c-7d10-8a2e-1f4c6b8d0e12", + completeness: "complete", + }, + ], + identity: [ + { did: "did:plc:alice", current_profile_id: "profile-alice" }, + ], + profiles: [], + records: [], + selections: [], + record_subjects: [], + record_context: [], + conversations: [], + conversation_members: [], + messages: [], + relationships: [], + record_assets: [], + portable_settings: [], + assets: [], + }), + ).toEqual({ + commonSemantics: { + archive: { + createdAt: "2026-01-15T12:00:00.000Z", + accountDid: "did:plc:alice", + accountUuid: "018d5f7a-9b3c-7d10-8a2e-1f4c6b8d0e12", + }, + identity: { did: "did:plc:alice", currentProfileId: "profile-alice" }, + profiles: [], + records: [], + selections: [], + recordSubjects: [], + recordContext: [], + conversations: [], + conversationMembers: [], + messages: [], + relationships: [], + recordAssets: [], + portableSettings: [], + }, + assets: [], + completeness: "complete", + }); + }); +}); diff --git a/services/archive-import.ts b/services/archive-import.ts deleted file mode 100644 index c9f3b55..0000000 --- a/services/archive-import.ts +++ /dev/null @@ -1,548 +0,0 @@ -import * as Crypto from "expo-crypto"; -import * as DocumentPicker from "expo-document-picker"; -import { Directory, File, Paths } from "expo-file-system"; -import { unzip } from "react-native-zip-archive"; - -import { buildAccountPaths } from "@/controllers/BaseAccountController"; -import { getDatabase } from "@/database"; - -/** - * Expected structure of account data in metadata.json - */ -export type ArchiveAccountData = { - createdAt: number; - updatedAt: number; - accessedAt: number | null; - handle: string; - displayName: string | null; - postsCount: number; - settingSavePosts: number; - settingSaveLikes: number; - settingSaveBookmarks: number; - settingSaveChats: number; - settingDeletePosts: number; - settingDeletePostsDaysOldEnabled: number; - settingDeletePostsDaysOld: number; - settingDeletePostsLikesThresholdEnabled: number; - settingDeletePostsLikesThreshold: number; - settingDeletePostsRepostsThresholdEnabled: number; - settingDeletePostsRepostsThreshold: number; - settingDeletePostsPreserveThreads: number; - settingDeleteReposts: number; - settingDeleteRepostsDaysOldEnabled: number; - settingDeleteRepostsDaysOld: number; - settingDeleteLikes: number; - settingDeleteLikesDaysOldEnabled: number; - settingDeleteLikesDaysOld: number; - settingDeleteChats: number; - settingDeleteChatsDaysOldEnabled: number; - settingDeleteChatsDaysOld: number; - settingDeleteBookmarks: number; - settingDeleteUnfollowEveryone: number; - avatarUrl: string | null; - did: string | null; - lastSavedAt: number | null; -}; - -/** - * Expected structure of metadata.json in a Cyd archive - */ -export type ArchiveMetadata = { - type: string; - uuid: string; - exportTimestamp: string; - account: ArchiveAccountData; -}; - -/** - * Result of archive validation - */ -export type ArchiveValidationResult = - | { valid: true; metadata: ArchiveMetadata; tempDir: string } - | { valid: false; error: string; tempDir?: string }; - -/** - * Result of archive import - */ -export type ArchiveImportResult = - | { success: true; accountUuid: string } - | { success: false; error: string }; - -/** - * Required fields in account data - */ -const REQUIRED_ACCOUNT_FIELDS: (keyof ArchiveAccountData)[] = [ - "createdAt", - "updatedAt", - "handle", - "postsCount", - "settingSavePosts", - "settingSaveLikes", - "settingSaveBookmarks", - "settingSaveChats", - "settingDeletePosts", - "settingDeletePostsDaysOldEnabled", - "settingDeletePostsDaysOld", - "settingDeletePostsLikesThresholdEnabled", - "settingDeletePostsLikesThreshold", - "settingDeletePostsRepostsThresholdEnabled", - "settingDeletePostsRepostsThreshold", - "settingDeletePostsPreserveThreads", - "settingDeleteReposts", - "settingDeleteRepostsDaysOldEnabled", - "settingDeleteRepostsDaysOld", - "settingDeleteLikes", - "settingDeleteLikesDaysOldEnabled", - "settingDeleteLikesDaysOld", - "settingDeleteChats", - "settingDeleteChatsDaysOldEnabled", - "settingDeleteChatsDaysOld", - "settingDeleteBookmarks", - "settingDeleteUnfollowEveryone", -]; - -/** - * Result of filename validation - */ -export type FilenameValidationResult = - | { valid: true; filename: string } - | { valid: false; error: string }; - -/** - * Regex pattern for valid archive filenames - * Format: Cyd-archive_{YYYY-MM-DD}_Bluesky_{handle}.zip - */ -const ARCHIVE_FILENAME_PATTERN = - /^Cyd-archive_\d{4}-\d{2}-\d{2}_Bluesky_[a-zA-Z0-9._-]+\.zip$/; - -/** - * Validate the filename of an archive - */ -export function validateArchiveFilename( - filename: string -): FilenameValidationResult { - if (!filename) { - return { - valid: false, - error: "Could not determine filename from the selected file.", - }; - } - - if (!ARCHIVE_FILENAME_PATTERN.test(filename)) { - return { - valid: false, - error: - "Invalid archive filename. Expected format: Cyd-archive_YYYY-MM-DD_Bluesky_{handle}.zip", - }; - } - - return { valid: true, filename }; -} - -/** - * Result from picking an archive file - */ -export type PickArchiveResult = { - uri: string; - filename: string; -} | null; - -/** - * Pick a zip file using the document picker - */ -export async function pickArchiveFile(): Promise { - const result = await DocumentPicker.getDocumentAsync({ - type: "application/zip", - copyToCacheDirectory: true, - }); - - if (result.canceled || result.assets.length === 0) { - return null; - } - - const asset = result.assets[0]; - return { - uri: asset.uri, - filename: asset.name ?? "", - }; -} - -/** - * Clean up a temporary directory - */ -export function cleanupTempDir(tempDir: string): void { - try { - const dir = new Directory(tempDir); - if (dir.exists) { - dir.delete(); - } - } catch (err) { - console.warn("[ArchiveImport] Failed to cleanup temp directory", err); - } -} - -/** - * Validate an archive file and return the metadata if valid - */ -export async function validateArchive( - zipPath: string -): Promise { - const tempId = Crypto.randomUUID(); - const cacheBase = Paths.cache?.uri ?? ""; - const tempDir = `${cacheBase}archive-import-${tempId}`; - - try { - // Unzip the file - await unzip(zipPath, tempDir); - - // Find the actual content directory (zip might have a root folder) - const rootDir = new Directory(tempDir); - if (!rootDir.exists) { - return { valid: false, error: "Failed to extract archive", tempDir }; - } - - // Check if files are directly in tempDir or in a subdirectory - let contentDir = tempDir; - const items = rootDir.list(); - - // If there's exactly one directory and no files, use that as the content dir - if (items.length === 1 && items[0] instanceof Directory) { - contentDir = items[0].uri; - } - - // Check for required files - const metadataFile = new File(contentDir, "metadata.json"); - const dataDbFile = new File(contentDir, "data.db"); - - if (!metadataFile.exists) { - return { - valid: false, - error: - "Invalid archive: missing metadata.json. This does not appear to be a Cyd archive.", - tempDir, - }; - } - - if (!dataDbFile.exists) { - return { - valid: false, - error: - "Invalid archive: missing data.db. This does not appear to be a Cyd archive.", - tempDir, - }; - } - - // Parse and validate metadata - const metadataContent = await metadataFile.text(); - let metadata: unknown; - try { - metadata = JSON.parse(metadataContent); - } catch { - return { - valid: false, - error: "Invalid archive: metadata.json is not valid JSON.", - tempDir, - }; - } - - if (typeof metadata !== "object" || metadata === null) { - return { - valid: false, - error: "Invalid archive: metadata.json must be an object.", - tempDir, - }; - } - - const meta = metadata as Record; - - // Validate required top-level fields - if (typeof meta.type !== "string" || meta.type.length === 0) { - return { - valid: false, - error: "Invalid archive: metadata.json must have a valid 'type' field.", - tempDir, - }; - } - - if (meta.type !== "bluesky") { - return { - valid: false, - error: "This version of Cyd only supports importing Bluesky archives.", - tempDir, - }; - } - - if (typeof meta.uuid !== "string" || meta.uuid.length === 0) { - return { - valid: false, - error: "Invalid archive: metadata.json must have a valid 'uuid' field.", - tempDir, - }; - } - - // Validate UUID format (basic check) - const uuidRegex = - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - if (!uuidRegex.test(meta.uuid)) { - return { - valid: false, - error: "Invalid archive: 'uuid' is not a valid UUID format.", - tempDir, - }; - } - - if ( - typeof meta.exportTimestamp !== "string" || - meta.exportTimestamp.length === 0 - ) { - return { - valid: false, - error: - "Invalid archive: metadata.json must have a valid 'exportTimestamp' field.", - tempDir, - }; - } - - // Validate exportTimestamp is a valid ISO date - const timestamp = Date.parse(meta.exportTimestamp); - if (isNaN(timestamp)) { - return { - valid: false, - error: - "Invalid archive: 'exportTimestamp' is not a valid ISO timestamp.", - tempDir, - }; - } - - // Validate account object - if (typeof meta.account !== "object" || meta.account === null) { - return { - valid: false, - error: - "Invalid archive: metadata.json must have an 'account' object with account data.", - tempDir, - }; - } - - const account = meta.account as Record; - - // Validate required account fields - for (const field of REQUIRED_ACCOUNT_FIELDS) { - if (!(field in account)) { - return { - valid: false, - error: `Invalid archive: account data is missing required field '${field}'.`, - tempDir, - }; - } - } - - // Validate handle is a string - if (typeof account.handle !== "string" || account.handle.length === 0) { - return { - valid: false, - error: "Invalid archive: account 'handle' must be a non-empty string.", - tempDir, - }; - } - - return { - valid: true, - metadata: { - type: meta.type, - uuid: meta.uuid, - exportTimestamp: meta.exportTimestamp, - account: account as unknown as ArchiveAccountData, - }, - tempDir: contentDir, // Use the content directory, not the root temp dir - }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return { - valid: false, - error: `Failed to validate archive: ${message}`, - tempDir, - }; - } -} - -/** - * Check if an account with the given UUID already exists - */ -export async function accountExistsWithUuid(uuid: string): Promise { - const db = await getDatabase(); - const row = await db.getFirstAsync<{ count: number }>( - "SELECT COUNT(*) as count FROM account WHERE uuid = ?;", - [uuid] - ); - return (row?.count ?? 0) > 0; -} - -/** - * Import an archive into Cyd - */ -export async function importArchive( - metadata: ArchiveMetadata, - tempDir: string -): Promise { - const { uuid, account } = metadata; - - try { - // Check if account already exists - if (await accountExistsWithUuid(uuid)) { - return { - success: false, - error: - "You already have this account loaded into Cyd. If you want to restore the data from this archive, first delete the existing account.", - }; - } - - const db = await getDatabase(); - - // Get next sort order - const sortOrderRow = await db.getFirstAsync<{ nextOrder: number }>( - "SELECT COALESCE(MAX(sortOrder), -1) + 1 AS nextOrder FROM account;" - ); - const sortOrder = sortOrderRow?.nextOrder ?? 0; - - // Insert bsky_account record using prepared statements - const bskyResult = await db.runAsync( - `INSERT INTO bsky_account ( - createdAt, - updatedAt, - accessedAt, - handle, - displayName, - postsCount, - settingSavePosts, - settingSaveLikes, - settingSaveBookmarks, - settingSaveChats, - settingDeletePosts, - settingDeletePostsDaysOldEnabled, - settingDeletePostsDaysOld, - settingDeletePostsLikesThresholdEnabled, - settingDeletePostsLikesThreshold, - settingDeletePostsRepostsThresholdEnabled, - settingDeletePostsRepostsThreshold, - settingDeletePostsPreserveThreads, - settingDeleteReposts, - settingDeleteRepostsDaysOldEnabled, - settingDeleteRepostsDaysOld, - settingDeleteLikes, - settingDeleteLikesDaysOldEnabled, - settingDeleteLikesDaysOld, - settingDeleteChats, - settingDeleteChatsDaysOldEnabled, - settingDeleteChatsDaysOld, - settingDeleteBookmarks, - settingDeleteUnfollowEveryone, - avatarUrl, - did, - lastSavedAt - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`, - [ - account.createdAt, - account.updatedAt, - account.accessedAt, - account.handle, - account.displayName, - account.postsCount, - account.settingSavePosts, - account.settingSaveLikes, - account.settingSaveBookmarks, - account.settingSaveChats, - account.settingDeletePosts, - account.settingDeletePostsDaysOldEnabled, - account.settingDeletePostsDaysOld, - account.settingDeletePostsLikesThresholdEnabled, - account.settingDeletePostsLikesThreshold, - account.settingDeletePostsRepostsThresholdEnabled, - account.settingDeletePostsRepostsThreshold, - account.settingDeletePostsPreserveThreads, - account.settingDeleteReposts, - account.settingDeleteRepostsDaysOldEnabled, - account.settingDeleteRepostsDaysOld, - account.settingDeleteLikes, - account.settingDeleteLikesDaysOldEnabled, - account.settingDeleteLikesDaysOld, - account.settingDeleteChats, - account.settingDeleteChatsDaysOldEnabled, - account.settingDeleteChatsDaysOld, - account.settingDeleteBookmarks, - account.settingDeleteUnfollowEveryone, - account.avatarUrl, - account.did, - account.lastSavedAt, - ] - ); - - const bskyAccountID = bskyResult.lastInsertRowId; - - // Insert account record - await db.runAsync( - `INSERT INTO account (uuid, sortOrder, type, bskyAccountID) - VALUES (?, ?, 'bluesky', ?);`, - [uuid, sortOrder, bskyAccountID] - ); - - // Copy files to account directory - const accountPaths = buildAccountPaths("bluesky", uuid); - const accountDir = new Directory(accountPaths.accountDir); - - if (!accountDir.exists) { - accountDir.create({ intermediates: true, idempotent: true }); - } - - // Copy all files from temp directory to account directory - const tempDirObj = new Directory(tempDir); - const items = tempDirObj.list(); - - for (const item of items) { - if (item instanceof File) { - // Skip metadata.json - we'll write a fresh one - if (item.name === "metadata.json") { - continue; - } - item.copy(new File(accountDir, item.name)); - } else if (item instanceof Directory) { - copyDirectoryRecursive(item, new Directory(accountDir, item.name)); - } - } - - // Write a fresh metadata.json without the account data (matches what initDB creates) - const metadataFile = new File(accountDir, "metadata.json"); - metadataFile.write( - JSON.stringify( - { - type: "bluesky", - uuid: uuid, - }, - null, - 2 - ) - ); - - return { success: true, accountUuid: uuid }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return { success: false, error: `Failed to import archive: ${message}` }; - } -} - -/** - * Recursively copy a directory - */ -function copyDirectoryRecursive(source: Directory, dest: Directory): void { - if (!dest.exists) { - dest.create({ intermediates: true, idempotent: true }); - } - const items = source.list(); - for (const item of items) { - if (item instanceof File) { - item.copy(new File(dest, item.name)); - } else if (item instanceof Directory) { - copyDirectoryRecursive(item, new Directory(dest, item.name)); - } - } -} diff --git a/services/archive-metadata.ts b/services/archive-metadata.ts new file mode 100644 index 0000000..efa2721 --- /dev/null +++ b/services/archive-metadata.ts @@ -0,0 +1,137 @@ +export const SUPPORTED_BLUESKY_ARCHIVE_VERSION = 2; + +export type BlueskyArchiveMetadata = { + format: "cyd-archive"; + platform: "bluesky"; + version: 2; + createdAt: string; + accountDid: string; + accountUuid: string; + completeness: "complete" | "incomplete"; +}; + +export type BlueskyArchiveMetadataResult = + | { supported: true; metadata: BlueskyArchiveMetadata } + | { + supported: false; + reason: "legacy" | "newer-version" | "platform" | "invalid"; + error: string; + }; + +const UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; +const RFC3339_MILLISECONDS_PATTERN = + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + +function invalid(detail: string): BlueskyArchiveMetadataResult { + return { + supported: false, + reason: "invalid", + error: `Invalid or corrupt Bluesky archive: ${detail}`, + }; +} + +function isPrototypeMetadata(value: Record): boolean { + return ( + value.type === "bluesky" || + ("uuid" in value && "exportTimestamp" in value && "account" in value) + ); +} + +function isCanonicalTimestamp(value: unknown): value is string { + if (typeof value !== "string" || !RFC3339_MILLISECONDS_PATTERN.test(value)) { + return false; + } + const timestamp = Date.parse(value); + return !Number.isNaN(timestamp) && new Date(timestamp).toISOString() === value; +} + +export function classifyBlueskyArchiveMetadata( + value: unknown, +): BlueskyArchiveMetadataResult { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + return invalid("metadata.json must contain an object."); + } + + const metadata = value as Record; + + if (metadata.format !== "cyd-archive") { + if (isPrototypeMetadata(metadata)) { + return { + supported: false, + reason: "legacy", + error: "Unsupported legacy Bluesky archive format.", + }; + } + return invalid("the format discriminator is missing or invalid."); + } + + if (metadata.platform !== "bluesky") { + if (typeof metadata.platform === "string" && metadata.platform.length > 0) { + return { + supported: false, + reason: "platform", + error: `Unsupported archive platform: ${metadata.platform}.`, + }; + } + return invalid("the platform discriminator is missing or invalid."); + } + + if (!Number.isInteger(metadata.version)) { + return invalid("the Bluesky archive version is missing or invalid."); + } + + const version = metadata.version as number; + if (version > SUPPORTED_BLUESKY_ARCHIVE_VERSION) { + return { + supported: false, + reason: "newer-version", + error: `Unsupported newer Bluesky archive version ${version}.`, + }; + } + if (version < SUPPORTED_BLUESKY_ARCHIVE_VERSION) { + return { + supported: false, + reason: "legacy", + error: "Unsupported legacy Bluesky archive format.", + }; + } + + if (!isCanonicalTimestamp(metadata.createdAt)) { + return invalid( + "createdAt must be an RFC 3339 UTC timestamp with milliseconds.", + ); + } + if ( + typeof metadata.accountDid !== "string" || + !metadata.accountDid.startsWith("did:") || + metadata.accountDid.split(":").length < 3 + ) { + return invalid("accountDid must be a DID."); + } + if ( + typeof metadata.accountUuid !== "string" || + !UUID_PATTERN.test(metadata.accountUuid) + ) { + return invalid("accountUuid must be a UUID."); + } + if ( + metadata.completeness !== "complete" && + metadata.completeness !== "incomplete" + ) { + return invalid("completeness must be complete or incomplete."); + } + + return { + supported: true, + metadata: { + format: "cyd-archive", + platform: "bluesky", + version: SUPPORTED_BLUESKY_ARCHIVE_VERSION, + createdAt: metadata.createdAt, + accountDid: metadata.accountDid, + accountUuid: metadata.accountUuid, + completeness: metadata.completeness, + }, + }; +} diff --git a/services/archive-semantics.ts b/services/archive-semantics.ts new file mode 100644 index 0000000..e5c3515 --- /dev/null +++ b/services/archive-semantics.ts @@ -0,0 +1,162 @@ +type ArchiveRow = Record; + +export type BlueskyArchiveTables = { + archive: ArchiveRow[]; + identity: ArchiveRow[]; + profiles: ArchiveRow[]; + records: ArchiveRow[]; + selections: ArchiveRow[]; + record_subjects: ArchiveRow[]; + record_context: ArchiveRow[]; + conversations: ArchiveRow[]; + conversation_members: ArchiveRow[]; + messages: ArchiveRow[]; + relationships: ArchiveRow[]; + record_assets: ArchiveRow[]; + portable_settings: ArchiveRow[]; + assets: ArchiveRow[]; +}; + +const fields = ( + row: ArchiveRow, + mapping: Record, +): ArchiveRow => + Object.fromEntries( + Object.entries(mapping).map(([normalized, source]) => [ + normalized, + row[source], + ]), + ); + +function json(value: unknown): unknown { + return value === null ? null : JSON.parse(value as string); +} + +function mapRows( + rows: ArchiveRow[], + mapping: Record, +): ArchiveRow[] { + return rows.map((row) => fields(row, mapping)); +} + +export function normalizeBlueskyArchiveSemantics(tables: BlueskyArchiveTables) { + const archive = tables.archive[0]; + const identity = tables.identity[0]; + + return { + commonSemantics: { + archive: fields(archive, { + createdAt: "created_at", + accountDid: "account_did", + accountUuid: "account_uuid", + }), + identity: fields(identity, { + did: "did", + currentProfileId: "current_profile_id", + }), + profiles: mapRows(tables.profiles, { + id: "id", + did: "did", + handle: "handle", + displayName: "display_name", + description: "description", + avatarAssetId: "avatar_asset_id", + bannerAssetId: "banner_asset_id", + capturedAt: "captured_at", + }), + records: tables.records.map((row) => ({ + ...fields(row, { + uri: "uri", + cid: "cid", + recordType: "record_type", + authorProfileId: "author_profile_id", + indexedAt: "indexed_at", + createdAt: "created_at", + firstObservedAt: "first_observed_at", + observedAt: "observed_at", + sourceDeletedAt: "source_deleted_at", + text: "text", + }), + facets: json(row.facets_json), + payload: json(row.payload_json), + })), + selections: mapRows(tables.selections, { + category: "category", + subjectId: "subject_id", + selectedAt: "selected_at", + }), + recordSubjects: mapRows(tables.record_subjects, { + relationshipUri: "relationship_uri", + subjectRecordUri: "subject_record_uri", + }), + recordContext: tables.record_context.map((row) => ({ + ...fields(row, { + recordUri: "record_uri", + kind: "kind", + contextRecordUri: "context_record_uri", + contextProfileId: "context_profile_id", + }), + external: json(row.external_json), + })), + conversations: mapRows(tables.conversations, { + id: "id", + rev: "rev", + firstObservedAt: "first_observed_at", + observedAt: "observed_at", + sourceDeletedAt: "source_deleted_at", + }), + conversationMembers: mapRows(tables.conversation_members, { + conversationId: "conversation_id", + profileId: "profile_id", + }), + messages: tables.messages.map((row) => ({ + ...fields(row, { + id: "id", + conversationId: "conversation_id", + senderProfileId: "sender_profile_id", + sentAt: "sent_at", + observedAt: "observed_at", + sourceDeletedAt: "source_deleted_at", + text: "text", + }), + facets: json(row.facets_json), + payload: json(row.payload_json), + })), + relationships: mapRows(tables.relationships, { + uri: "uri", + kind: "kind", + actorDid: "actor_did", + subjectDid: "subject_did", + createdAt: "created_at", + observedAt: "observed_at", + sourceDeletedAt: "source_deleted_at", + }), + recordAssets: mapRows(tables.record_assets, { + ownerType: "owner_type", + ownerId: "owner_id", + assetId: "asset_id", + role: "role", + position: "position", + }), + portableSettings: tables.portable_settings.map((row) => ({ + key: row.key, + value: json(row.value_json), + })), + }, + assets: mapRows(tables.assets, { + id: "id", + kind: "kind", + mediaType: "media_type", + byteCount: "byte_count", + sha256: "sha256", + archivePath: "archive_path", + availability: "availability", + unavailableReason: "unavailable_reason", + sourceUrl: "source_url", + width: "width", + height: "height", + altText: "alt_text", + }), + completeness: archive.completeness, + }; +} diff --git a/testdata/Cyd-archive_2026-01-10_Bluesky_nexamind-cyd.bsky.social.zip b/testdata/Cyd-archive_2026-01-10_Bluesky_nexamind-cyd.bsky.social.zip deleted file mode 100644 index bf415f1..0000000 Binary files a/testdata/Cyd-archive_2026-01-10_Bluesky_nexamind-cyd.bsky.social.zip and /dev/null differ