From f70d22ca4dccc7caad23cb4355ddbd92a9039acd Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 24 May 2026 16:16:01 +0100 Subject: [PATCH 1/9] =?UTF-8?q?feat(check):=20add=20pdscheck=20=E2=80=94?= =?UTF-8?q?=20a=20pure-client-side=20PDS=20conformance=20verifier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New app at apps/check/ (deployed to check.cirrus.earth). Solid + Vite + Tailwind v4, served as static assets via Wrangler. Tests AT Protocol PDS conformance across three modes: - Read-only: identity resolution, server endpoints, repo reads, sync 1.1 firehose, blobs, OAuth discovery - Write tests: lifecycle of createRecord / applyWrites / uploadBlob / putRecord / deleteRecord (OAuth-authenticated, ephemeral session) - OAuth conformance: full PAR + DPoP + PKCE + iss + revocation flow, with isolated probes for unregistered redirect_uri rejection, permission-set resolution (bogus, advertised, and published site.standard.authFull), and post-token scope/boundary enforcement Firehose sampling uses cursor=0 historical replay with cap (200) / timeout (8s) / inactivity (1.5s) / diversity-aware exit (creates + updates/deletes seen). Reports termination reason so users can tell "hit the cap" from "PDS went idle". Validates every response against atcute lexicon schemas and links every check to its spec section. Designed to be a one-stop conformance check for anyone running a PDS implementation. --- apps/check/.gitignore | 10 + apps/check/index.html | 18 + .../earth/cirrus/check/testrecord.json | 45 + apps/check/package.json | 45 + apps/check/public/client-metadata.json | 15 + apps/check/src/App.tsx | 638 +++++ apps/check/src/app.css | 38 + apps/check/src/checks/account.ts | 340 +++ apps/check/src/checks/blobs.ts | 178 ++ apps/check/src/checks/firehose.ts | 758 +++++ apps/check/src/checks/identity.ts | 340 +++ apps/check/src/checks/index.ts | 29 + apps/check/src/checks/oauth-discovery.ts | 666 +++++ apps/check/src/checks/repo-read.ts | 641 +++++ apps/check/src/checks/repo-write.ts | 515 ++++ apps/check/src/checks/server.ts | 236 ++ apps/check/src/checks/sync.ts | 322 +++ apps/check/src/components/CheckRow.tsx | 149 + apps/check/src/components/OAuthFlowView.tsx | 429 +++ apps/check/src/components/RecentRuns.tsx | 38 + apps/check/src/components/RunView.tsx | 536 ++++ apps/check/src/components/StatusGlyph.tsx | 53 + apps/check/src/lib/oauth-flow.ts | 1984 +++++++++++++ apps/check/src/lib/oauth.ts | 88 + apps/check/src/lib/recent.ts | 72 + apps/check/src/lib/resolvers.ts | 31 + apps/check/src/lib/spec-urls.ts | 141 + apps/check/src/lib/xrpc.ts | 82 + apps/check/src/main.tsx | 7 + apps/check/src/runner.ts | 105 + apps/check/src/types.ts | 100 + apps/check/src/vite-env.d.ts | 1 + apps/check/tsconfig.json | 11 + apps/check/vite.config.ts | 10 + apps/check/wrangler.jsonc | 18 + pnpm-lock.yaml | 2446 ++++++++++++++++- pnpm-workspace.yaml | 1 + 37 files changed, 11052 insertions(+), 84 deletions(-) create mode 100644 apps/check/.gitignore create mode 100644 apps/check/index.html create mode 100644 apps/check/lexicons/earth/cirrus/check/testrecord.json create mode 100644 apps/check/package.json create mode 100644 apps/check/public/client-metadata.json create mode 100644 apps/check/src/App.tsx create mode 100644 apps/check/src/app.css create mode 100644 apps/check/src/checks/account.ts create mode 100644 apps/check/src/checks/blobs.ts create mode 100644 apps/check/src/checks/firehose.ts create mode 100644 apps/check/src/checks/identity.ts create mode 100644 apps/check/src/checks/index.ts create mode 100644 apps/check/src/checks/oauth-discovery.ts create mode 100644 apps/check/src/checks/repo-read.ts create mode 100644 apps/check/src/checks/repo-write.ts create mode 100644 apps/check/src/checks/server.ts create mode 100644 apps/check/src/checks/sync.ts create mode 100644 apps/check/src/components/CheckRow.tsx create mode 100644 apps/check/src/components/OAuthFlowView.tsx create mode 100644 apps/check/src/components/RecentRuns.tsx create mode 100644 apps/check/src/components/RunView.tsx create mode 100644 apps/check/src/components/StatusGlyph.tsx create mode 100644 apps/check/src/lib/oauth-flow.ts create mode 100644 apps/check/src/lib/oauth.ts create mode 100644 apps/check/src/lib/recent.ts create mode 100644 apps/check/src/lib/resolvers.ts create mode 100644 apps/check/src/lib/spec-urls.ts create mode 100644 apps/check/src/lib/xrpc.ts create mode 100644 apps/check/src/main.tsx create mode 100644 apps/check/src/runner.ts create mode 100644 apps/check/src/types.ts create mode 100644 apps/check/src/vite-env.d.ts create mode 100644 apps/check/tsconfig.json create mode 100644 apps/check/vite.config.ts create mode 100644 apps/check/wrangler.jsonc diff --git a/apps/check/.gitignore b/apps/check/.gitignore new file mode 100644 index 00000000..93336d56 --- /dev/null +++ b/apps/check/.gitignore @@ -0,0 +1,10 @@ +node_modules +dist +.wrangler +.dev.vars* +!.dev.vars.example +.env* +!.env.example +*.tsbuildinfo +.DS_Store +*.log diff --git a/apps/check/index.html b/apps/check/index.html new file mode 100644 index 00000000..da867657 --- /dev/null +++ b/apps/check/index.html @@ -0,0 +1,18 @@ + + + + + + + + ☁️ check + + + +
+ + + diff --git a/apps/check/lexicons/earth/cirrus/check/testrecord.json b/apps/check/lexicons/earth/cirrus/check/testrecord.json new file mode 100644 index 00000000..a412bb80 --- /dev/null +++ b/apps/check/lexicons/earth/cirrus/check/testrecord.json @@ -0,0 +1,45 @@ +{ + "lexicon": 1, + "id": "earth.cirrus.check.testrecord", + "description": "Ephemeral test record created by pdscheck during PDS write verification. Records of this type are created and deleted within a single verification run; any leftover record can be safely deleted by hand. The namespace does not federate to app.bsky.* services.", + "defs": { + "main": { + "type": "record", + "description": "A disposable verification artifact.", + "key": "tid", + "record": { + "type": "object", + "required": ["createdAt"], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime", + "description": "When this record was created." + }, + "message": { + "type": "string", + "description": "Human-readable identifier — typically a fixed string flagging that the record is safe to delete.", + "maxLength": 256 + }, + "verifier": { + "type": "string", + "format": "uri", + "description": "Origin of the pdscheck instance that created the record, for tracing leftovers back to a specific verifier deployment.", + "maxLength": 256 + }, + "runId": { + "type": "string", + "description": "Identifier of the verification run that created the record. Useful for correlating leftovers with a specific test execution.", + "maxLength": 64 + }, + "blob": { + "type": "blob", + "description": "Optional blob attachment used to exercise uploadBlob and blob references.", + "accept": ["*/*"], + "maxSize": 1048576 + } + } + } + } + } +} diff --git a/apps/check/package.json b/apps/check/package.json new file mode 100644 index 00000000..8e5b9d5c --- /dev/null +++ b/apps/check/package.json @@ -0,0 +1,45 @@ +{ + "name": "@getcirrus/check", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Web-based PDS verifier for AT Protocol", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "wrangler:dev": "wrangler dev", + "deploy": "vite build && wrangler deploy", + "check": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@atcute/atproto": "^4.0.0", + "@atcute/bluesky": "^4.0.3", + "@atcute/cbor": "^2.3.3", + "@atcute/cid": "^2.4.1", + "@atcute/client": "^5.0.0", + "@atcute/identity": "^2.0.0", + "@atcute/identity-resolver": "^2.0.0", + "@atcute/lexicons": "^2.0.0", + "@atcute/oauth-browser-client": "^4.0.0", + "@atcute/tid": "^1.1.2", + "@ipld/car": "^5.4.6", + "@kobalte/core": "^0.13.11", + "@solid-primitives/storage": "^4.3.4", + "idb-keyval": "^6.2.4", + "jose": "^6.2.3", + "multiformats": "^14.0.0", + "oauth4webapi": "^3.8.6", + "solid-js": "^1.9.13" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.0", + "tailwindcss": "^4.3.0", + "typescript": "^6.0.3", + "vite": "^8.0.14", + "vite-plugin-solid": "^2.11.12", + "vitest": "4.1.7", + "wrangler": "^4.94.0" + } +} diff --git a/apps/check/public/client-metadata.json b/apps/check/public/client-metadata.json new file mode 100644 index 00000000..975c3f5d --- /dev/null +++ b/apps/check/public/client-metadata.json @@ -0,0 +1,15 @@ +{ + "client_id": "https://check.cirrus.earth/client-metadata.json", + "client_name": "check · a PDS validator", + "client_uri": "https://check.cirrus.earth", + "redirect_uris": [ + "https://check.cirrus.earth/oauth/callback", + "https://check.cirrus.earth/oauth/flow-callback" + ], + "scope": "atproto transition:generic repo:earth.cirrus.check.testrecord include:site.standard.authFull", + "grant_types": ["authorization_code", "refresh_token"], + "response_types": ["code"], + "application_type": "web", + "token_endpoint_auth_method": "none", + "dpop_bound_access_tokens": true +} diff --git a/apps/check/src/App.tsx b/apps/check/src/App.tsx new file mode 100644 index 00000000..89cd84b8 --- /dev/null +++ b/apps/check/src/App.tsx @@ -0,0 +1,638 @@ +import { createEffect, createSignal, onCleanup, onMount, Show } from "solid-js"; +import { anonymousChecks, writeChecks } from "./checks"; +import { OAuthFlowView } from "./components/OAuthFlowView"; +import { RecentRuns } from "./components/RecentRuns"; +import { RunView } from "./components/RunView"; +import { + completeCallback, + getAgent, + isCallbackPath, + signOut, + signedInDid, + startLogin, +} from "./lib/oauth"; +import { + abandonFlow, + isFlowCallback, + runPostCallback, + startPreRedirectFlow, + type FlowRun, + type FlowState, +} from "./lib/oauth-flow"; +import { recordRun } from "./lib/recent"; +import { startRun, type RunStore } from "./runner"; +import type { CheckContext } from "./types"; + +const PLACEHOLDER = "jay.bsky.team"; + +const INTENT_KEY = "pdscheck.post-login-intent"; +const TARGET_KEY = "pdscheck.post-login-target"; + +function initialTarget(): string { + const params = new URLSearchParams(location.search); + return ( + params.get("target") ?? params.get("pds") ?? params.get("handle") ?? "" + ); +} + +function withViewTransition(fn: () => void) { + if ( + typeof document !== "undefined" && + "startViewTransition" in document && + typeof document.startViewTransition === "function" + ) { + document.startViewTransition(fn); + } else { + fn(); + } +} + +type BootState = + | { kind: "ready" } + | { kind: "callback" } + | { kind: "callback-error"; message: string } + | { kind: "flow-callback"; state: FlowState }; + +type Mode = "landing" | "confirm-writes"; + +export function App() { + // Both callback paths show the "completing sign-in" loading view first, + // then transition to ready (atcute callback) or flow-callback (with state) once the + // async handlers resolve. + const initialBoot: BootState = + isFlowCallback() || isCallbackPath() + ? { kind: "callback" } + : { kind: "ready" }; + + const [boot, setBoot] = createSignal(initialBoot); + const [flow, setFlow] = createSignal(null); + const [flowRun, setFlowRun] = createSignal(null); + const [mode, setMode] = createSignal("landing"); + const [target, setTarget] = createSignal(initialTarget()); + const [downloadCar, setDownloadCar] = createSignal(false); + const [run, setRun] = createSignal(null); + const [runMode, setRunMode] = createSignal<"verify" | "writes">("verify"); + const [authError, setAuthError] = createSignal(null); + + onMount(() => { + if (isFlowCallback()) { + runPostCallback() + .then(({ state }) => { + setBoot({ kind: "flow-callback", state }); + setFlow(state); + }) + .catch((error: unknown) => { + setBoot({ + kind: "callback-error", + message: error instanceof Error ? error.message : String(error), + }); + }); + } else if (isCallbackPath()) { + completeCallback() + .then(async () => { + const intent = sessionStorage.getItem(INTENT_KEY); + const stashedTarget = sessionStorage.getItem(TARGET_KEY); + sessionStorage.removeItem(INTENT_KEY); + sessionStorage.removeItem(TARGET_KEY); + if (stashedTarget) setTarget(stashedTarget); + + // User passed the confirmation step before signing in, so kick off + // the run BEFORE transitioning out of the loading view — that way + // they go straight from "COMPLETING SIGN IN" to RunView without a + // landing-page flash. + if (intent === "writes" && stashedTarget) { + history.replaceState( + null, + "", + `/?target=${encodeURIComponent(stashedTarget)}`, + ); + await beginRun(stashedTarget, writeChecks, "writes"); + } else { + history.replaceState( + null, + "", + stashedTarget + ? `/?target=${encodeURIComponent(stashedTarget)}` + : "/", + ); + } + setBoot({ kind: "ready" }); + }) + .catch((error: unknown) => { + setBoot({ + kind: "callback-error", + message: error instanceof Error ? error.message : String(error), + }); + }); + } + + const onPop = () => { + if (!new URLSearchParams(location.search).get("target")) { + withViewTransition(() => { + run()?.cancel(); + setRun(null); + }); + } + }; + window.addEventListener("popstate", onPop); + onCleanup(() => { + window.removeEventListener("popstate", onPop); + }); + }); + + createEffect(() => { + const current = run(); + if (current?.endedAt) { + recordRun(current); + // Sessions are ephemeral — sign out at the end of any authenticated + // run so the next test starts fresh (and possibly with a different + // target account). + if (signedInDid()) void signOut(); + } + }); + + function selectRecent(value: string) { + setTarget(value); + void beginRun(value, anonymousChecks); + } + + async function beginRun( + value: string, + checks: readonly (typeof anonymousChecks)[number][], + mode: "verify" | "writes" = "verify", + ) { + const params = new URLSearchParams(location.search); + params.set("target", value); + history.pushState(null, "", `?${params.toString()}`); + const agent = await getAgent(); + const initial: Partial = { + downloadCar: downloadCar(), + ...(agent ? { signedIn: true, agent } : {}), + }; + setRunMode(mode); + withViewTransition(() => { + setRun(startRun(value, checks, initial)); + }); + } + + function cancelRun() { + run()?.cancel(); + history.pushState(null, "", "/"); + withViewTransition(() => setRun(null)); + } + + function runReadChecks() { + const value = target().trim(); + if (!value) return; + void beginRun(value, anonymousChecks, "verify"); + } + + function startWriteTests() { + const value = target().trim(); + if (!value) { + setAuthError("Enter a handle first"); + return; + } + setAuthError(null); + // Confirm BEFORE the redirect so the user sees what they're authorizing + // before authenticating. Post-callback skips straight to the run. + setMode("confirm-writes"); + } + + async function confirmWrites() { + const value = target().trim(); + if (!value) { + setAuthError("Enter a handle first"); + setMode("landing"); + return; + } + + // Already signed in (rare — could happen on a same-target re-run before + // auto-sign-out fired). Just start the run. + if (signedInDid()) { + setMode("landing"); + void beginRun(value, writeChecks, "writes"); + return; + } + + // Not signed in — stash intent + target, redirect to PDS for auth. + // Post-callback (in onMount) will read the intent and start the run. + sessionStorage.setItem(INTENT_KEY, "writes"); + sessionStorage.setItem(TARGET_KEY, value); + try { + await startLogin(value); + } catch (error) { + if (error instanceof Error && error.message === "redirecting") return; + sessionStorage.removeItem(INTENT_KEY); + sessionStorage.removeItem(TARGET_KEY); + setAuthError(error instanceof Error ? error.message : String(error)); + setMode("landing"); + } + } + + function cancelWrites() { + setMode("landing"); + } + + function startOAuthConformance() { + const value = target().trim(); + if (!value) { + setAuthError("Enter a handle first"); + return; + } + setAuthError(null); + const run = startPreRedirectFlow(value); + setFlow(run.state); + setFlowRun(run); + } + + function continueFlowRedirect() { + flowRun()?.redirect(); + } + + function exitFlow() { + abandonFlow(); + setFlow(null); + setFlowRun(null); + setBoot({ kind: "ready" }); + history.replaceState(null, "", "/"); + } + + async function onSignOut() { + await signOut(); + } + + return ( + }> + }> + {(state) => ( + + )} + + + ); + + function Surface() { + return ( + }> + {(activeRun) => ( + { + const v = target().trim(); + if (v) void beginRun(v, anonymousChecks, "verify"); + }} + onWriteTests={() => void startWriteTests()} + onOAuthConformance={startOAuthConformance} + /> + )} + + ); + } + + function ModeSurface() { + return ( + }> + + + ); + } + + function BootView(props: { state: BootState }) { + return ( + ) + : null + } + fallback={} + > + {(flowBoot) => ( + + )} + + ); + } + + function Landing() { + return ( +
+
+ + ☁️ + PDS CHECK + +
+ + {(did) => ( + + + {did()} + + + )} + + + ascorbic/cirrus + +
+
+ +
+
+

+ ☁️ PDS CHECK +

+ +
event.preventDefault()} class="mt-10"> + + setTarget(event.currentTarget.value)} + placeholder={PLACEHOLDER} + class="w-full border-2 border-ink bg-paper px-4 py-3 text-base placeholder:text-faint" + /> +

+ handle (alice.bsky.social), DID ( + did:plc:…), or PDS URL ( + https://pds.example.com — + server-only, no user context) +

+
+ +
+
+ + +
+ + + + {(message) => ( +
+ {message()} +
+ )} +
+
+ + +
+
+ + +
+ ); + } +} + +function ConfirmWritesView(props: { + target: string; + signedInDid: string | null; + onConfirm: () => void; + onCancel: () => void; +}) { + return ( +
+
+ + ☁️ + CHECK + + +
+ +
+
+
+ Confirm write tests +
+

{props.target}

+ + + you'll be redirected to your PDS to authorize before tests run +
+ } + > + {(did) => ( +
+ signed in as {did()} +
+ )} + + +
+

These tests will make real changes to your PDS. Specifically:

+
    +
  • + · Create test records in + collection{" "} + + earth.cirrus.check.testrecord + +
  • +
  • + · Round-trip read each created + record (getRecord, listRecords) +
  • +
  • + · Test{" "} + applyWrites with an + atomic create + delete +
  • +
  • + · Upload a 67-byte test blob (1×1 + transparent PNG) via{" "} + uploadBlob +
  • +
  • + · Reference the blob in a second + record +
  • +
  • + · Delete every record created, + leaving the collection empty +
  • +
+

+ The earth.cirrus.check{" "} + namespace is isolated from{" "} + app.bsky.* — records + won't appear in Bluesky feeds, won't federate to the AppView, and + can be safely deleted from your repo manually if any cleanup step + fails. The session is signed out automatically when the run + finishes. +

+
+ +
+ + +
+
+ + + ); +} + +function CallbackView(props: { state: BootState }) { + return ( +
+
+
+ ☁️ +
+ +

+ COMPLETING SIGN IN +

+

+ exchanging authorization code for a session… +

+ + } + > +

+ SIGN IN FAILED +

+

+ {props.state.kind === "callback-error" && props.state.message} +

+ + return home → + +
+
+
+ ); +} diff --git a/apps/check/src/app.css b/apps/check/src/app.css new file mode 100644 index 00000000..a1f7335d --- /dev/null +++ b/apps/check/src/app.css @@ -0,0 +1,38 @@ +@import "tailwindcss"; + +@theme { + --color-paper: oklch(0.95 0 0); + --color-ink: oklch(0 0 0); + --color-muted: oklch(0.5 0 0); + --color-faint: oklch(0.7 0 0); + --color-line: oklch(0.85 0 0); + + --color-pass: oklch(0.55 0.13 145); + --color-fail: oklch(0.55 0.16 25); + --color-warn: oklch(0.7 0.14 75); + + --font-mono: + ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, + "DejaVu Sans Mono", monospace; +} + +@layer base { + html { + color-scheme: light; + background: var(--color-paper); + color: var(--color-ink); + font-family: var(--font-mono); + } + + body { + min-height: 100dvh; + margin: 0; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + } + + :focus-visible { + outline: 2px solid var(--color-ink); + outline-offset: 2px; + } +} diff --git a/apps/check/src/checks/account.ts b/apps/check/src/checks/account.ts new file mode 100644 index 00000000..0db9a6d2 --- /dev/null +++ b/apps/check/src/checks/account.ts @@ -0,0 +1,340 @@ +import "@atcute/atproto"; +import { + ComAtprotoIdentityGetRecommendedDidCredentials, + ComAtprotoServerCheckAccountStatus, + ComAtprotoServerGetAccountInviteCodes, + ComAtprotoServerGetServiceAuth, + ComAtprotoServerGetSession, + ComAtprotoServerListAppPasswords, +} from "@atcute/atproto"; +import type { Did, Nsid } from "@atcute/lexicons/syntax"; +import { authedClient, validateLexicon } from "../lib/xrpc"; +import type { Check, CheckContext, CheckOutcome } from "../types"; + +let getSessionResponse: ComAtprotoServerGetSession.$output | undefined; +let checkAccountStatusResponse: + | ComAtprotoServerCheckAccountStatus.$output + | undefined; +let listAppPasswordsResponse: + | ComAtprotoServerListAppPasswords.$output + | undefined; +let getAccountInviteCodesResponse: + | ComAtprotoServerGetAccountInviteCodes.$output + | undefined; +let getServiceAuthResponse: ComAtprotoServerGetServiceAuth.$output | undefined; +let getRecommendedDidCredentialsResponse: + | ComAtprotoIdentityGetRecommendedDidCredentials.$output + | undefined; + +function reset() { + getSessionResponse = undefined; + checkAccountStatusResponse = undefined; + listAppPasswordsResponse = undefined; + getAccountInviteCodesResponse = undefined; + getServiceAuthResponse = undefined; + getRecommendedDidCredentialsResponse = undefined; +} + +function sessionMismatch(ctx: CheckContext): CheckOutcome | null { + if (!ctx.agent) return { status: "skip", message: "No active session" }; + if (ctx.agent.sub !== ctx.did) { + return { + status: "skip", + message: `Session is for ${ctx.agent.sub}, target is ${ctx.did}`, + }; + } + return null; +} + +const getSession: Check = { + id: "account.get-session", + category: "account", + label: "getSession", + requires: ["pds", "session"], + run: async (ctx): Promise => { + reset(); + const guard = sessionMismatch(ctx); + if (guard) return guard; + const client = authedClient(ctx.agent!); + const res = await client.get("com.atproto.server.getSession", {}); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + getSessionResponse = res.data; + return { + status: "pass", + message: `did ${res.data.did}, handle ${res.data.handle}`, + evidence: { response: { status: res.status, body: res.data } }, + }; + }, +}; + +const getSessionValidates: Check = { + id: "account.get-session.validates", + category: "account", + label: "getSession response matches lexicon", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + if (!getSessionResponse) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoServerGetSession.mainSchema.output.schema, + getSessionResponse, + ); + }, +}; + +const checkAccountStatus: Check = { + id: "account.check-account-status", + category: "account", + label: "checkAccountStatus", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + const client = authedClient(ctx.agent!); + const res = await client.get("com.atproto.server.checkAccountStatus", {}); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + checkAccountStatusResponse = res.data; + return { + status: "pass", + message: `activated=${res.data.activated}, valid=${res.data.validDid}`, + evidence: { response: { status: res.status, body: res.data } }, + }; + }, +}; + +const checkAccountStatusValidates: Check = { + id: "account.check-account-status.validates", + category: "account", + label: "checkAccountStatus response matches lexicon", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + if (!checkAccountStatusResponse) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoServerCheckAccountStatus.mainSchema.output.schema, + checkAccountStatusResponse, + ); + }, +}; + +const listAppPasswords: Check = { + id: "account.list-app-passwords", + category: "account", + label: "listAppPasswords", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + const client = authedClient(ctx.agent!); + const res = await client.get("com.atproto.server.listAppPasswords", {}); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + listAppPasswordsResponse = res.data; + return { + status: "pass", + message: `${res.data.passwords.length} app password(s)`, + evidence: { response: { status: res.status, body: res.data } }, + }; + }, +}; + +const listAppPasswordsValidates: Check = { + id: "account.list-app-passwords.validates", + category: "account", + label: "listAppPasswords response matches lexicon", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + if (!listAppPasswordsResponse) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoServerListAppPasswords.mainSchema.output.schema, + listAppPasswordsResponse, + ); + }, +}; + +const getAccountInviteCodes: Check = { + id: "account.get-account-invite-codes", + category: "account", + label: "getAccountInviteCodes", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + const client = authedClient(ctx.agent!); + const res = await client.get( + "com.atproto.server.getAccountInviteCodes", + { params: {} }, + ); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + getAccountInviteCodesResponse = res.data; + return { + status: "pass", + message: `${res.data.codes.length} invite code(s)`, + evidence: { response: { status: res.status, body: res.data } }, + }; + }, +}; + +const getAccountInviteCodesValidates: Check = { + id: "account.get-account-invite-codes.validates", + category: "account", + label: "getAccountInviteCodes response matches lexicon", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + if (!getAccountInviteCodesResponse) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoServerGetAccountInviteCodes.mainSchema.output.schema, + getAccountInviteCodesResponse, + ); + }, +}; + +const getServiceAuth: Check = { + id: "account.get-service-auth", + category: "account", + label: "getServiceAuth", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + const client = authedClient(ctx.agent!); + const res = await client.get("com.atproto.server.getServiceAuth", { + params: { + aud: "did:web:api.bsky.app" as Did, + lxm: "app.bsky.actor.getProfile" as Nsid, + }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + getServiceAuthResponse = res.data; + return { + status: "pass", + message: `token issued (${res.data.token.length} chars)`, + evidence: { response: { status: res.status, body: res.data } }, + }; + }, +}; + +const getServiceAuthValidates: Check = { + id: "account.get-service-auth.validates", + category: "account", + label: "getServiceAuth response matches lexicon", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + if (!getServiceAuthResponse) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoServerGetServiceAuth.mainSchema.output.schema, + getServiceAuthResponse, + ); + }, +}; + +const getRecommendedDidCredentials: Check = { + id: "account.get-recommended-did-credentials", + category: "account", + label: "getRecommendedDidCredentials", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + const client = authedClient(ctx.agent!); + const res = await client.get( + "com.atproto.identity.getRecommendedDidCredentials", + {}, + ); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + getRecommendedDidCredentialsResponse = res.data; + const aka = res.data.alsoKnownAs?.length ?? 0; + const rks = res.data.rotationKeys?.length ?? 0; + return { + status: "pass", + message: `${aka} alsoKnownAs, ${rks} rotation key(s)`, + evidence: { response: { status: res.status, body: res.data } }, + }; + }, +}; + +const getRecommendedDidCredentialsValidates: Check = { + id: "account.get-recommended-did-credentials.validates", + category: "account", + label: "getRecommendedDidCredentials response matches lexicon", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + if (!getRecommendedDidCredentialsResponse) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoIdentityGetRecommendedDidCredentials.mainSchema.output.schema, + getRecommendedDidCredentialsResponse, + ); + }, +}; + +export const accountChecks: Check[] = [ + getSession, + getSessionValidates, + checkAccountStatus, + checkAccountStatusValidates, + listAppPasswords, + listAppPasswordsValidates, + getAccountInviteCodes, + getAccountInviteCodesValidates, + getServiceAuth, + getServiceAuthValidates, + getRecommendedDidCredentials, + getRecommendedDidCredentialsValidates, +]; diff --git a/apps/check/src/checks/blobs.ts b/apps/check/src/checks/blobs.ts new file mode 100644 index 00000000..c18f39f3 --- /dev/null +++ b/apps/check/src/checks/blobs.ts @@ -0,0 +1,178 @@ +import { ComAtprotoSyncListBlobs } from "@atcute/atproto"; +import type { Did } from "@atcute/lexicons/syntax"; +import { publicClient, validateLexicon } from "../lib/xrpc"; +import type { Check, CheckOutcome } from "../types"; + +let listBlobsResponse: ComAtprotoSyncListBlobs.$output | undefined; +let firstBlobCid: string | undefined; +let getBlobResponse: { contentType: string | null; byteLength: number } | undefined; + +function reset() { + listBlobsResponse = undefined; + firstBlobCid = undefined; + getBlobResponse = undefined; +} + +function xrpcUrl(pds: string, nsid: string, params: Record): string { + const qs = new URLSearchParams(params).toString(); + return `${pds}/xrpc/${nsid}${qs ? `?${qs}` : ""}`; +} + +const listBlobs: Check = { + id: "blobs.list-blobs", + category: "blobs", + label: "listBlobs", + requires: ["pds", "did"], + run: async (ctx): Promise => { + reset(); + const pds = ctx.pds!; + const did = ctx.did!; + const url = xrpcUrl(pds, "com.atproto.sync.listBlobs", { did, limit: "5" }); + try { + const res = await publicClient(pds).get("com.atproto.sync.listBlobs", { + params: { did: did as Did, limit: 5 }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } + listBlobsResponse = res.data; + firstBlobCid = res.data.cids[0]; + return { + status: "pass", + message: `${res.data.cids.length} blob CIDs`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { + request: { method: "GET", url }, + error: String(error), + }, + }; + } + }, +}; + +const listBlobsValidates: Check = { + id: "blobs.list-blobs.validates", + category: "blobs", + label: "listBlobs response matches lexicon", + requires: ["pds", "did"], + run: async (): Promise => { + if (!listBlobsResponse) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoSyncListBlobs.mainSchema.output.schema, + listBlobsResponse, + ); + }, +}; + +const getBlob: Check = { + id: "blobs.get-blob", + category: "blobs", + label: "getBlob", + requires: ["pds", "did"], + run: async (ctx): Promise => { + if (listBlobsResponse && listBlobsResponse.cids.length === 0) { + return { status: "skip", message: "no blobs in repo" }; + } + if (!firstBlobCid) { + return { status: "skip", message: "listBlobs did not succeed" }; + } + const pds = ctx.pds!; + const did = ctx.did!; + const cid = firstBlobCid; + const url = xrpcUrl(pds, "com.atproto.sync.getBlob", { did, cid }); + try { + const res = await publicClient(pds).get("com.atproto.sync.getBlob", { + params: { did: did as Did, cid }, + as: "bytes", + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } + const bytes = res.data; + const contentType = res.headers.get("content-type"); + getBlobResponse = { contentType, byteLength: bytes.byteLength }; + if (bytes.byteLength === 0) { + return { + status: "fail", + message: "blob response was empty", + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: getBlobResponse }, + }, + }; + } + if (contentType && /^text\/html\b/i.test(contentType)) { + return { + status: "fail", + message: `unexpected content-type: ${contentType}`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: getBlobResponse }, + actual: contentType, + }, + }; + } + return { + status: "pass", + message: `${bytes.byteLength.toLocaleString()} bytes${contentType ? `, ${contentType}` : ""}`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: getBlobResponse }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { + request: { method: "GET", url }, + error: String(error), + }, + }; + } + }, +}; + +const getBlobValidates: Check = { + id: "blobs.get-blob.validates", + category: "blobs", + label: "getBlob response matches lexicon", + requires: ["pds", "did"], + run: async (): Promise => { + return { + status: "skip", + message: "binary response — content not lexicon-validated", + }; + }, +}; + +export const blobsChecks: Check[] = [ + listBlobs, + listBlobsValidates, + getBlob, + getBlobValidates, +]; diff --git a/apps/check/src/checks/firehose.ts b/apps/check/src/checks/firehose.ts new file mode 100644 index 00000000..9eebfd25 --- /dev/null +++ b/apps/check/src/checks/firehose.ts @@ -0,0 +1,758 @@ +import { decodeFirst, fromBytes, isBytes } from "@atcute/cbor"; +import { isCidLink } from "@atcute/cid"; +import { CarReader } from "@ipld/car"; +import type { Check, CheckOutcome } from "../types"; + +interface FrameHeader { + op: number; + t?: string; +} + +interface Frame { + header: FrameHeader; + body: Record; + raw: Uint8Array; +} + +interface DecodeFailure { + index: number; + error: string; +} + +let collectedFrames: Frame[] = []; +let decodeFailures: DecodeFailure[] = []; +let collectionAttempted = false; +let collectionTerminationReason = ""; +let collectionElapsedMs = 0; + +const FRAME_TARGET = 200; +const COLLECT_TIMEOUT_MS = 8000; +const CONNECT_TIMEOUT_MS = 5000; +const INACTIVITY_TIMEOUT_MS = 1500; +const MIN_FRAMES_BEFORE_DIVERSITY_EXIT = 50; + +function wsUrlFor(pds: string): string { + const url = new URL(pds); + url.protocol = url.protocol === "http:" ? "ws:" : "wss:"; + url.pathname = "/xrpc/com.atproto.sync.subscribeRepos"; + url.search = "?cursor=0"; + return url.toString(); +} + +function countHeaderTypes(frames: Frame[]): Record { + const counts: Record = {}; + for (const frame of frames) { + const key = frame.header.t ?? `op:${frame.header.op}`; + counts[key] = (counts[key] ?? 0) + 1; + } + return counts; +} + +function commitFrames(): Frame[] { + return collectedFrames.filter((f) => f.header.t === "#commit"); +} + +const connect: Check = { + id: "firehose.connect", + category: "firehose", + label: "Connect to firehose WebSocket", + description: + "Open wss:///xrpc/com.atproto.sync.subscribeRepos and verify the upgrade succeeds.", + requires: ["pds"], + run: async (ctx): Promise => { + collectedFrames = []; + decodeFailures = []; + collectionAttempted = false; + collectionTerminationReason = ""; + collectionElapsedMs = 0; + + if (!ctx.pds) { + return { status: "skip", message: "No PDS endpoint" }; + } + + const url = wsUrlFor(ctx.pds); + let ws: WebSocket; + try { + ws = new WebSocket(url); + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { request: { method: "WS", url }, error: String(error) }, + }; + } + + ws.binaryType = "arraybuffer"; + + try { + await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error(`Connection timed out after ${CONNECT_TIMEOUT_MS}ms`)); + }, CONNECT_TIMEOUT_MS); + ws.addEventListener( + "open", + () => { + clearTimeout(timer); + resolve(); + }, + { once: true }, + ); + ws.addEventListener( + "error", + () => { + clearTimeout(timer); + reject(new Error("WebSocket error before open")); + }, + { once: true }, + ); + ws.addEventListener( + "close", + (ev) => { + clearTimeout(timer); + reject( + new Error( + `WebSocket closed before open: code=${ev.code} reason=${ev.reason || "(none)"}`, + ), + ); + }, + { once: true }, + ); + }); + } catch (error) { + try { + ws.close(); + } catch { + // ignore + } + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { request: { method: "WS", url }, error: String(error) }, + }; + } + + const frames: Frame[] = []; + const failures: DecodeFailure[] = []; + let index = 0; + const collectionStartedAt = Date.now(); + let lastFrameAt = collectionStartedAt; + let sawCommitCreate = false; + let sawCommitMutate = false; + let terminationReason = "timeout"; + + await new Promise((resolve) => { + let done = false; + const finish = (reason: string) => { + if (done) return; + done = true; + terminationReason = reason; + clearTimeout(deadline); + clearInterval(inactivity); + try { + ws.close(1000, "collected"); + } catch { + // ignore + } + resolve(); + }; + + const deadline = setTimeout(() => finish("timeout"), COLLECT_TIMEOUT_MS); + + // Inactivity exit: PDS finished replaying historical events and is now + // idle on the live tip. No point waiting longer. + const inactivity = setInterval(() => { + if ( + frames.length + failures.length > 0 && + Date.now() - lastFrameAt > INACTIVITY_TIMEOUT_MS + ) { + finish("inactivity"); + } + }, 250); + + ws.addEventListener("message", (event) => { + const data = event.data; + if (!(data instanceof ArrayBuffer)) return; + const bytes = new Uint8Array(data); + const i = index++; + lastFrameAt = Date.now(); + try { + const [header, rest] = decodeFirst(bytes); + const [body] = decodeFirst(rest); + const frame: Frame = { + header: header as FrameHeader, + body: (body ?? {}) as Record, + raw: bytes, + }; + frames.push(frame); + if (frame.header.t === "#commit") { + const ops = (frame.body as { ops?: unknown }).ops; + if (Array.isArray(ops)) { + for (const op of ops) { + const action = (op as { action?: string }).action; + if (action === "create") sawCommitCreate = true; + else if (action === "update" || action === "delete") + sawCommitMutate = true; + } + } + } + } catch (error) { + failures.push({ + index: i, + error: error instanceof Error ? error.message : String(error), + }); + } + const total = frames.length + failures.length; + if (total >= FRAME_TARGET) { + finish("cap"); + } else if ( + total >= MIN_FRAMES_BEFORE_DIVERSITY_EXIT && + sawCommitCreate && + sawCommitMutate + ) { + finish("diversity"); + } + }); + ws.addEventListener("close", () => finish("server-close"), { once: true }); + ws.addEventListener("error", () => finish("ws-error"), { once: true }); + }); + + collectedFrames = frames; + decodeFailures = failures; + collectionAttempted = true; + collectionTerminationReason = terminationReason; + collectionElapsedMs = Date.now() - collectionStartedAt; + + return { + status: "pass", + message: `Connected to ${url}`, + evidence: { request: { method: "WS", url } }, + }; + }, +}; + +const collectFrames: Check = { + id: "firehose.collect-frames", + category: "firehose", + label: "Receive firehose frames", + description: `Sample from the historical replay (cursor=0): stop at ${FRAME_TARGET} frames, ${COLLECT_TIMEOUT_MS}ms, after ${INACTIVITY_TIMEOUT_MS}ms inactivity, or as soon as the sample includes both creates and updates/deletes (≥${MIN_FRAMES_BEFORE_DIVERSITY_EXIT} frames).`, + requires: ["pds"], + run: async (): Promise => { + if (!collectionAttempted) { + return { status: "skip", message: "Firehose was not connected" }; + } + const total = collectedFrames.length + decodeFailures.length; + const types = countHeaderTypes(collectedFrames); + if (total === 0) { + return { + status: "warn", + message: `No frames received in ${collectionElapsedMs}ms — relay may be idle (terminated: ${collectionTerminationReason})`, + evidence: { + expected: ">=1 frame", + actual: { frames: 0, terminatedBy: collectionTerminationReason, elapsedMs: collectionElapsedMs }, + }, + }; + } + return { + status: "pass", + message: `Received ${total} frame${total === 1 ? "" : "s"} in ${collectionElapsedMs}ms (terminated: ${collectionTerminationReason})`, + evidence: { + actual: { + frames: total, + decoded: collectedFrames.length, + types, + terminatedBy: collectionTerminationReason, + elapsedMs: collectionElapsedMs, + }, + }, + }; + }, +}; + +const frameDecodes: Check = { + id: "firehose.frame-decodes", + category: "firehose", + label: "Frames decode as DAG-CBOR header + body", + description: + "Each frame must be two concatenated DAG-CBOR objects with op:1 (event) or op:-1 (error).", + requires: ["pds"], + run: async (): Promise => { + if (!collectionAttempted) { + return { status: "skip", message: "Firehose was not connected" }; + } + if (collectedFrames.length === 0 && decodeFailures.length === 0) { + return { status: "skip", message: "No frames to validate" }; + } + if (decodeFailures.length > 0) { + return { + status: "fail", + message: `${decodeFailures.length} frame${ + decodeFailures.length === 1 ? "" : "s" + } failed to decode`, + evidence: { actual: decodeFailures }, + }; + } + const badOp = collectedFrames.find( + (f) => f.header.op !== 1 && f.header.op !== -1, + ); + if (badOp) { + return { + status: "fail", + message: `Frame header op=${badOp.header.op} is neither 1 nor -1`, + evidence: { actual: badOp.header }, + }; + } + return { + status: "pass", + message: `All ${collectedFrames.length} frame${ + collectedFrames.length === 1 ? "" : "s" + } decoded cleanly`, + evidence: { actual: { types: countHeaderTypes(collectedFrames) } }, + }; + }, +}; + +const commitHasPrevData: Check = { + id: "firehose.commit-has-prevdata", + category: "firehose", + label: "#commit frames include prevData", + description: + "Every #commit event must carry the previous MST root CID as prevData (atproto Sync 1.1).", + requires: ["pds"], + run: async (): Promise => { + if (!collectionAttempted) { + return { status: "skip", message: "Firehose was not connected" }; + } + const commits = commitFrames(); + if (commits.length === 0) { + return { status: "skip", message: "No #commit frames observed" }; + } + const missing = commits.filter((f) => f.body.prevData === undefined); + if (missing.length > 0) { + const offending = missing[0]!; + return { + status: "fail", + message: `${missing.length}/${commits.length} #commit frame${ + commits.length === 1 ? "" : "s" + } missing prevData — required by atproto Sync 1.1`, + evidence: { + expected: "body.prevData present on every #commit", + actual: { + header: offending.header, + bodyKeys: Object.keys(offending.body), + }, + }, + }; + } + return { + status: "pass", + message: `All ${commits.length} #commit frame${ + commits.length === 1 ? "" : "s" + } include prevData`, + }; + }, +}; + +const commitBlocksIsValidCar: Check = { + id: "firehose.commit-blocks-is-car", + category: "firehose", + label: "#commit body.blocks parses as a CAR", + description: + "The blocks field on a #commit is a CAR slice. It must parse cleanly and contain the commit block referenced by body.commit.", + requires: ["pds"], + run: async (): Promise => { + if (!collectionAttempted) { + return { status: "skip", message: "Firehose was not connected" }; + } + const commits = commitFrames(); + if (commits.length === 0) { + return { status: "skip", message: "No #commit frames observed" }; + } + const failures: Array<{ seq: unknown; error: string }> = []; + let withCommitInBlocks = 0; + for (const f of commits) { + const blocks = f.body.blocks; + // @atcute/cbor wraps CBOR byte strings as BytesWrapper; raw Uint8Array + // is also valid (e.g. if the firehose used a different decoder). + let bytes: Uint8Array | undefined; + if (blocks instanceof Uint8Array) { + bytes = blocks; + } else if (isBytes(blocks)) { + bytes = fromBytes(blocks); + } + if (!bytes) { + failures.push({ + seq: f.body.seq, + error: `body.blocks is not bytes (got ${typeof blocks})`, + }); + continue; + } + try { + const reader = await CarReader.fromBytes(bytes); + const roots = await reader.getRoots(); + if (roots.length === 0) { + failures.push({ + seq: f.body.seq, + error: "CAR has no roots", + }); + continue; + } + // commit field on the frame is a CidLinkWrapper (from @atcute/cbor's + // DAG-CBOR decode). Read `.$link` for the canonical CID string — + // `.toString()` returns the default "[object Object]". + const commitCid = f.body.commit; + const commitCidStr = isCidLink(commitCid) + ? (commitCid as { $link: string }).$link + : undefined; + const cidsInCar = new Set(); + for await (const blk of reader.blocks()) { + cidsInCar.add(blk.cid.toString()); + } + if (commitCidStr && cidsInCar.has(commitCidStr)) { + withCommitInBlocks++; + } else if (commitCidStr) { + failures.push({ + seq: f.body.seq, + error: `commit CID ${commitCidStr.slice(0, 24)}… not present in body.blocks CAR`, + }); + } + } catch (error) { + failures.push({ + seq: f.body.seq, + error: + error instanceof Error ? error.message : String(error), + }); + } + } + if (failures.length > 0) { + return { + status: "fail", + message: `${failures.length}/${commits.length} #commit frame${ + commits.length === 1 ? "" : "s" + } failed CAR validation`, + evidence: { + expected: + "body.blocks parses as a CAR and contains the commit block", + error: failures + .map((f) => `seq=${f.seq}: ${f.error}`) + .join("\n"), + }, + }; + } + return { + status: "pass", + message: `All ${commits.length} #commit frame${ + commits.length === 1 ? "" : "s" + } parse as valid CARs; ${withCommitInBlocks} include the commit block`, + }; + }, +}; + +const commitDeprecatedTooBig: Check = { + id: "firehose.commit-deprecated-toobig", + category: "firehose", + label: "#commit tooBig field is not set to true", + description: + "tooBig is deprecated on #commit frames. Producers should leave it false; consumers ignore it.", + requires: ["pds"], + run: async (): Promise => { + if (!collectionAttempted) { + return { status: "skip", message: "Firehose was not connected" }; + } + const commits = commitFrames(); + if (commits.length === 0) { + return { status: "skip", message: "No #commit frames observed" }; + } + const offenders = commits.filter((f) => f.body.tooBig === true); + if (offenders.length === 0) { + return { + status: "pass", + message: `All ${commits.length} #commit frame${ + commits.length === 1 ? "" : "s" + } have tooBig=false`, + }; + } + return { + status: "warn", + message: `${offenders.length}/${commits.length} #commit frame${ + commits.length === 1 ? "" : "s" + } have tooBig=true (deprecated)`, + evidence: { + expected: "tooBig should not be true on any #commit", + actual: offenders.map((f) => ({ seq: f.body.seq })), + }, + }; + }, +}; + +const commitDeprecatedBlobs: Check = { + id: "firehose.commit-deprecated-blobs", + category: "firehose", + label: "#commit blobs array is empty", + description: + "The blobs array on #commit is deprecated. Producers should emit an empty array.", + requires: ["pds"], + run: async (): Promise => { + if (!collectionAttempted) { + return { status: "skip", message: "Firehose was not connected" }; + } + const commits = commitFrames(); + if (commits.length === 0) { + return { status: "skip", message: "No #commit frames observed" }; + } + const offenders = commits.filter( + (f) => Array.isArray(f.body.blobs) && (f.body.blobs as unknown[]).length > 0, + ); + if (offenders.length === 0) { + return { + status: "pass", + message: `All ${commits.length} #commit frame${ + commits.length === 1 ? "" : "s" + } have empty blobs array`, + }; + } + return { + status: "warn", + message: `${offenders.length}/${commits.length} #commit frame${ + commits.length === 1 ? "" : "s" + } include legacy blobs entries (deprecated)`, + evidence: { + expected: "blobs array should be empty on #commit", + actual: offenders.map((f) => ({ + seq: f.body.seq, + blobCount: (f.body.blobs as unknown[]).length, + })), + }, + }; + }, +}; + +const commitDeprecatedRebase: Check = { + id: "firehose.commit-deprecated-rebase", + category: "firehose", + label: "#commit rebase field is not set", + description: + "rebase on #commit is deprecated; rebases are now signaled via #sync events.", + requires: ["pds"], + run: async (): Promise => { + if (!collectionAttempted) { + return { status: "skip", message: "Firehose was not connected" }; + } + const commits = commitFrames(); + if (commits.length === 0) { + return { status: "skip", message: "No #commit frames observed" }; + } + const offenders = commits.filter((f) => f.body.rebase === true); + if (offenders.length === 0) { + return { + status: "pass", + message: `No #commit frames flagged as rebase`, + }; + } + return { + status: "warn", + message: `${offenders.length}/${commits.length} #commit frame${ + commits.length === 1 ? "" : "s" + } carry rebase=true (deprecated — should be a #sync event)`, + evidence: { + actual: offenders.map((f) => ({ seq: f.body.seq })), + }, + }; + }, +}; + +const ACCOUNT_STATUS_VALUES = new Set([ + "takendown", + "suspended", + "deleted", + "deactivated", +]); + +const accountEventShape: Check = { + id: "firehose.account-event-shape", + category: "firehose", + label: "#account events carry required fields", + description: + "When emitted, #account events require seq, did, time, active. status is optional but must be one of: takendown, suspended, deleted, deactivated.", + requires: ["pds"], + run: async (): Promise => { + if (!collectionAttempted) { + return { status: "skip", message: "Firehose was not connected" }; + } + const events = collectedFrames.filter((f) => f.header.t === "#account"); + if (events.length === 0) { + return { + status: "skip", + message: "No #account frames observed in sample", + }; + } + const issues: string[] = []; + for (const f of events) { + const body = f.body; + if (typeof body.seq !== "number" && typeof body.seq !== "bigint") + issues.push(`#account missing/invalid seq`); + if (typeof body.did !== "string") + issues.push(`#account missing/invalid did`); + if (typeof body.time !== "string") + issues.push(`#account missing/invalid time`); + if (typeof body.active !== "boolean") + issues.push(`#account missing/invalid active`); + if ( + body.status !== undefined && + (typeof body.status !== "string" || + !ACCOUNT_STATUS_VALUES.has(body.status as string)) + ) { + issues.push( + `#account status=${JSON.stringify(body.status)} is not one of ${[...ACCOUNT_STATUS_VALUES].join(", ")}`, + ); + } + } + if (issues.length === 0) { + return { + status: "pass", + message: `${events.length} #account frame${events.length === 1 ? "" : "s"} all conformant`, + }; + } + return { + status: "fail", + message: + issues.length === 1 ? issues[0]! : `${issues.length} field issues`, + evidence: { error: issues.join("\n") }, + }; + }, +}; + +const commitOpsHavePrev: Check = { + id: "firehose.commit-ops-have-prev", + category: "firehose", + label: "#commit update/delete ops include prev", + description: + "Per the subscribeRepos lexicon, #repoOp.prev holds the previous record CID and is required for update/delete actions (inductive firehose). Create ops do not carry prev.", + requires: ["pds"], + run: async (): Promise => { + if (!collectionAttempted) { + return { status: "skip", message: "Firehose was not connected" }; + } + const commits = commitFrames(); + if (commits.length === 0) { + return { status: "skip", message: "No #commit frames observed" }; + } + let updateDeleteOps = 0; + let missingPrev = 0; + let firstOffending: { header: FrameHeader; op: unknown } | undefined; + for (const frame of commits) { + const ops = frame.body.ops; + if (!Array.isArray(ops)) continue; + for (const op of ops) { + if (!op || typeof op !== "object") continue; + const action = (op as { action?: unknown }).action; + if (action !== "update" && action !== "delete") continue; + updateDeleteOps++; + if (!Object.prototype.hasOwnProperty.call(op, "prev")) { + missingPrev++; + if (!firstOffending) { + firstOffending = { header: frame.header, op }; + } + } + } + } + if (updateDeleteOps === 0) { + return { + status: "skip", + message: "No update/delete ops in sampled commits — only creates", + }; + } + if (missingPrev > 0) { + return { + status: "fail", + message: `${missingPrev}/${updateDeleteOps} update/delete op${ + updateDeleteOps === 1 ? "" : "s" + } missing prev — required for inductive firehose (Sync 1.1)`, + evidence: { + expected: + "every #repoOp with action=update|delete carries prev (CID of prior record state)", + actual: firstOffending, + }, + }; + } + return { + status: "pass", + message: `All ${updateDeleteOps} update/delete op${updateDeleteOps === 1 ? "" : "s"} carry prev`, + }; + }, +}; + +function eventPresenceCheck( + id: string, + label: string, + description: string, + eventType: string, +): Check { + return { + id, + category: "firehose", + label, + description, + requires: ["pds"], + run: async (): Promise => { + if (!collectionAttempted) { + return { status: "skip", message: "Firehose was not connected" }; + } + if (collectedFrames.length === 0) { + return { status: "skip", message: "No frames to inspect" }; + } + const matches = collectedFrames.filter((f) => f.header.t === eventType); + if (matches.length === 0) { + return { + status: "warn", + message: `No ${eventType} frames seen in ${collectedFrames.length} sampled frame${ + collectedFrames.length === 1 ? "" : "s" + } — absence does not prove non-support`, + evidence: { actual: countHeaderTypes(collectedFrames) }, + }; + } + return { + status: "pass", + message: `Observed ${matches.length} ${eventType} frame${ + matches.length === 1 ? "" : "s" + }`, + }; + }, + }; +} + +const emitsSyncEvents = eventPresenceCheck( + "firehose.emits-sync-events", + "#sync events present", + "#sync events signal rebases and migrations (atproto Sync 1.1). Absence in a short sample is informational only.", + "#sync", +); + +const emitsAccountEvents = eventPresenceCheck( + "firehose.emits-account-events", + "#account events present", + "#account events are emitted on activation/deactivation state changes (atproto Sync 1.1).", + "#account", +); + +const emitsIdentityEvents = eventPresenceCheck( + "firehose.emits-identity-events", + "#identity events present", + "#identity events are emitted on handle/DID document changes (atproto Sync 1.1).", + "#identity", +); + +export const firehoseChecks: Check[] = [ + connect, + collectFrames, + frameDecodes, + commitHasPrevData, + commitBlocksIsValidCar, + commitOpsHavePrev, + commitDeprecatedTooBig, + commitDeprecatedBlobs, + commitDeprecatedRebase, + emitsSyncEvents, + emitsAccountEvents, + accountEventShape, + emitsIdentityEvents, +]; diff --git a/apps/check/src/checks/identity.ts b/apps/check/src/checks/identity.ts new file mode 100644 index 00000000..baef6873 --- /dev/null +++ b/apps/check/src/checks/identity.ts @@ -0,0 +1,340 @@ +import { + ComAtprotoIdentityResolveDid, + ComAtprotoIdentityResolveHandle, + ComAtprotoIdentityResolveIdentity, +} from "@atcute/atproto"; +import { getPdsEndpoint } from "@atcute/identity"; +import { isDid, isHandle, type Did, type Handle } from "@atcute/lexicons/syntax"; +import { didDocResolver, handleResolver } from "../lib/resolvers"; +import { publicClient, validateLexicon } from "../lib/xrpc"; +import type { Check, CheckOutcome } from "../types"; + +let pdsResolveHandleBody: ComAtprotoIdentityResolveHandle.$output | undefined; +let pdsResolveDidBody: ComAtprotoIdentityResolveDid.$output | undefined; +let pdsResolveIdentityBody: ComAtprotoIdentityResolveIdentity.$output | undefined; + +const parseInput: Check = { + id: "identity.parse-input", + category: "identity", + label: "Recognize input", + run: async (ctx): Promise => { + const raw = ctx.target.trim().replace(/^@/, ""); + if (!raw) { + return { status: "fail", message: "Empty input" }; + } + // http(s):// prefix → "PDS URL" mode: skip identity resolution and run + // PDS-only checks (server.*, sync.listRepos, OAuth discovery, etc.). + // Checks requiring ctx.did will skip cleanly. + if (/^https?:\/\//i.test(raw)) { + try { + const url = new URL(raw); + const origin = url.origin; + return { + status: "pass", + message: `PDS-URL mode: testing ${origin} directly — identity/repo/sync checks that need a user DID will skip`, + context: { pds: origin }, + }; + } catch { + return { + status: "fail", + message: `Not a parseable URL: ${raw}`, + evidence: { actual: raw }, + }; + } + } + if (isDid(raw)) { + return { + status: "pass", + message: `${raw} is a DID`, + context: { did: raw }, + }; + } + if (isHandle(raw)) { + return { + status: "pass", + message: `${raw} is a handle`, + context: { handle: raw }, + }; + } + return { + status: "fail", + message: `Not a recognizable handle or DID. Looks like a hostname? Prefix with https:// to test it as a PDS URL (limited check surface — most checks require a user context).`, + evidence: { actual: raw }, + }; + }, +}; + +const resolveHandle: Check = { + id: "identity.resolve-handle", + category: "identity", + label: "Resolve handle to DID", + run: async (ctx): Promise => { + if (ctx.did) { + return { status: "skip", message: "Input was already a DID" }; + } + if (!ctx.handle) { + return { status: "skip", message: "No handle to resolve" }; + } + try { + const did = await handleResolver.resolve(ctx.handle as Handle); + return { + status: "pass", + message: did, + context: { did }, + }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + // If the input is a multi-level hostname that doesn't resolve as a + // handle, it's most likely a PDS host, not a user account. Probe + // /xrpc/_health to confirm and, if so, auto-switch to PDS-URL mode + // so the rest of the run is useful. + const looksLikeHostname = /\./.test(ctx.handle) && !ctx.handle.endsWith("."); + if (looksLikeHostname) { + const probeUrl = `https://${ctx.handle}/xrpc/_health`; + try { + const res = await fetch(probeUrl, { + method: "GET", + headers: { accept: "application/json" }, + signal: AbortSignal.timeout(3000), + }); + if (res.ok) { + // It's a PDS — switch into PDS-URL mode automatically. + return { + status: "warn", + message: `not a user handle, but ${ctx.handle} responds to /xrpc/_health — auto-switching to PDS-URL mode. Identity/repo/sync checks needing a user DID will skip.`, + evidence: { + expected: "DNS TXT _atproto. or /.well-known/atproto-did to return a DID", + actual: `handle resolution failed; ${probeUrl} returned ${res.status}`, + error: errMsg, + }, + context: { pds: `https://${ctx.handle}`, handle: undefined }, + }; + } + } catch { + // probe failed — fall through to the regular fail path with helpful message + } + } + const hint = looksLikeHostname + ? ` (looks like a hostname — if this is a PDS rather than a user account, re-run with https://${ctx.handle})` + : ""; + return { + status: "fail", + message: `${errMsg}${hint}`, + evidence: { error: errMsg }, + }; + } + }, +}; + +const fetchDidDocument: Check = { + id: "identity.fetch-did-document", + category: "identity", + label: "Fetch DID document", + requires: ["did"], + run: async (ctx): Promise => { + const did = ctx.did as Did<"plc" | "web">; + try { + const doc = await didDocResolver.resolve(did); + return { + status: "pass", + message: `${doc.service?.length ?? 0} service entries`, + evidence: { response: { body: doc } }, + context: { didDoc: doc }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { error: String(error) }, + }; + } + }, +}; + +const extractPdsEndpoint: Check = { + id: "identity.extract-pds", + category: "identity", + label: "Extract PDS endpoint", + requires: ["did"], + run: async (ctx): Promise => { + if (!ctx.didDoc) { + return { status: "skip", message: "DID document unavailable" }; + } + const pds = getPdsEndpoint(ctx.didDoc); + if (!pds) { + return { + status: "fail", + message: "No #atproto_pds service entry in DID document", + evidence: { actual: ctx.didDoc.service }, + }; + } + return { + status: "pass", + message: pds, + context: { pds }, + }; + }, +}; + +const pdsResolveHandle: Check = { + id: "identity.pds-resolve-handle", + category: "identity", + label: "PDS resolves handle to DID", + requires: ["pds"], + run: async (ctx): Promise => { + pdsResolveHandleBody = undefined; + if (!ctx.handle) { + return { status: "skip", message: "no handle in context" }; + } + const client = publicClient(ctx.pds!); + const res = await client.get("com.atproto.identity.resolveHandle", { + params: { handle: ctx.handle as Handle }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + pdsResolveHandleBody = res.data; + if (ctx.did && res.data.did !== ctx.did) { + return { + status: "fail", + message: `PDS returned ${res.data.did}, expected ${ctx.did}`, + evidence: { expected: ctx.did, actual: res.data.did }, + }; + } + return { + status: "pass", + message: res.data.did, + evidence: { response: { body: res.data } }, + }; + }, +}; + +const pdsResolveHandleValidates: Check = { + id: "identity.pds-resolve-handle.validates", + category: "identity", + label: "PDS resolveHandle response matches lexicon", + requires: ["pds"], + run: async (): Promise => { + if (!pdsResolveHandleBody) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoIdentityResolveHandle.mainSchema.output.schema, + pdsResolveHandleBody, + ); + }, +}; + +const pdsResolveDid: Check = { + id: "identity.pds-resolve-did", + category: "identity", + label: "PDS resolves DID to document", + requires: ["pds", "did"], + run: async (ctx): Promise => { + pdsResolveDidBody = undefined; + const client = publicClient(ctx.pds!); + const res = await client.get("com.atproto.identity.resolveDid", { + params: { did: ctx.did as Did }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + pdsResolveDidBody = res.data; + return { + status: "pass", + message: `didDoc with ${res.data.didDoc ? "didDoc" : "no didDoc"}`, + evidence: { response: { body: res.data } }, + }; + }, +}; + +const pdsResolveDidValidates: Check = { + id: "identity.pds-resolve-did.validates", + category: "identity", + label: "PDS resolveDid response matches lexicon", + requires: ["pds", "did"], + run: async (): Promise => { + if (!pdsResolveDidBody) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoIdentityResolveDid.mainSchema.output.schema, + pdsResolveDidBody, + ); + }, +}; + +const pdsResolveIdentity: Check = { + id: "identity.pds-resolve-identity", + category: "identity", + label: "PDS resolves identity (combined)", + requires: ["pds"], + run: async (ctx): Promise => { + pdsResolveIdentityBody = undefined; + const identifier = ctx.handle ?? ctx.did; + if (!identifier) { + return { status: "skip", message: "no handle or DID in context" }; + } + const client = publicClient(ctx.pds!); + const res = await client.get("com.atproto.identity.resolveIdentity", { + params: { identifier: identifier as Handle | Did }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + pdsResolveIdentityBody = res.data; + if (ctx.did && res.data.did !== ctx.did) { + return { + status: "fail", + message: `did mismatch: expected ${ctx.did}, got ${res.data.did}`, + evidence: { expected: ctx.did, actual: res.data.did }, + }; + } + return { + status: "pass", + message: `${res.data.did}`, + evidence: { response: { body: res.data } }, + }; + }, +}; + +const pdsResolveIdentityValidates: Check = { + id: "identity.pds-resolve-identity.validates", + category: "identity", + label: "PDS resolveIdentity response matches lexicon", + requires: ["pds"], + run: async (): Promise => { + if (!pdsResolveIdentityBody) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoIdentityResolveIdentity.mainSchema.output.schema, + pdsResolveIdentityBody, + ); + }, +}; + +export const identityChecks: Check[] = [ + parseInput, + resolveHandle, + fetchDidDocument, + extractPdsEndpoint, + pdsResolveHandle, + pdsResolveHandleValidates, + pdsResolveDid, + pdsResolveDidValidates, + pdsResolveIdentity, + pdsResolveIdentityValidates, +]; diff --git a/apps/check/src/checks/index.ts b/apps/check/src/checks/index.ts new file mode 100644 index 00000000..5e6eb6a6 --- /dev/null +++ b/apps/check/src/checks/index.ts @@ -0,0 +1,29 @@ +import type { Check } from "../types"; +import { accountChecks } from "./account"; +import { blobsChecks } from "./blobs"; +import { firehoseChecks } from "./firehose"; +import { identityChecks } from "./identity"; +import { oauthDiscoveryChecks } from "./oauth-discovery"; +import { repoReadChecks } from "./repo-read"; +import { repoWriteChecks } from "./repo-write"; +import { serverChecks } from "./server"; +import { syncChecks } from "./sync"; + +// Public/anonymous checks — the main VERIFY button. No auth, no writes. +export const anonymousChecks: readonly Check[] = [ + ...identityChecks, + ...serverChecks, + ...repoReadChecks, + ...syncChecks, + ...blobsChecks, + ...firehoseChecks, + ...oauthDiscoveryChecks, +]; + +// Write tests — gated by sign-in AND an explicit confirmation step. +// Includes identity (to populate ctx.pds/ctx.did) + account (verify session) + the actual writes. +export const writeChecks: readonly Check[] = [ + ...identityChecks, + ...accountChecks, + ...repoWriteChecks, +]; diff --git a/apps/check/src/checks/oauth-discovery.ts b/apps/check/src/checks/oauth-discovery.ts new file mode 100644 index 00000000..b4748764 --- /dev/null +++ b/apps/check/src/checks/oauth-discovery.ts @@ -0,0 +1,666 @@ +import type { Check, CheckOutcome } from "../types"; + +interface ProtectedResourceMetadata { + resource?: string; + authorization_servers?: string[]; + [k: string]: unknown; +} + +interface JwksDocument { + keys?: Array>; + [k: string]: unknown; +} + +let protectedResource: ProtectedResourceMetadata | undefined; +let authServerUrl: string | undefined; +let authServerMetadata: Record | undefined; +let jwksUri: string | undefined; +let jwksDocument: JwksDocument | undefined; + +function reset(): void { + protectedResource = undefined; + authServerUrl = undefined; + authServerMetadata = undefined; + jwksUri = undefined; + jwksDocument = undefined; +} + +function trimTrailingSlash(value: string): string { + return value.endsWith("/") ? value.slice(0, -1) : value; +} + +async function fetchJson( + url: string, +): Promise< + | { ok: true; status: number; body: unknown } + | { ok: false; status?: number; error: string; body?: unknown } +> { + let response: Response; + try { + response = await fetch(url, { headers: { accept: "application/json" } }); + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + }; + } + let body: unknown; + let text: string | undefined; + try { + text = await response.text(); + body = text.length === 0 ? undefined : JSON.parse(text); + } catch (error) { + return { + ok: false, + status: response.status, + error: `JSON parse failed: ${ + error instanceof Error ? error.message : String(error) + }`, + body: text, + }; + } + if (!response.ok) { + return { + ok: false, + status: response.status, + error: `HTTP ${response.status}`, + body, + }; + } + return { ok: true, status: response.status, body }; +} + +const protectedResourceResponds: Check = { + id: "oauth.protected-resource-responds", + category: "oauth", + label: ".well-known/oauth-protected-resource responds", + description: + "RFC 9728 OAuth Protected Resource Metadata document must be served at /.well-known/oauth-protected-resource.", + requires: ["pds"], + run: async (ctx): Promise => { + reset(); + if (!ctx.pds) { + return { status: "skip", message: "No PDS endpoint" }; + } + const url = `${trimTrailingSlash(ctx.pds)}/.well-known/oauth-protected-resource`; + const result = await fetchJson(url); + if (!result.ok) { + return { + status: "fail", + message: result.error, + evidence: { + request: { method: "GET", url }, + response: { status: result.status, body: result.body }, + error: result.error, + }, + }; + } + if (!result.body || typeof result.body !== "object") { + return { + status: "fail", + message: "Response body is not a JSON object", + evidence: { + request: { method: "GET", url }, + response: { status: result.status, body: result.body }, + }, + }; + } + protectedResource = result.body as ProtectedResourceMetadata; + return { + status: "pass", + message: `HTTP ${result.status}`, + evidence: { + request: { method: "GET", url }, + response: { status: result.status, body: result.body }, + }, + }; + }, +}; + +const protectedResourceValidates: Check = { + id: "oauth.protected-resource-validates", + category: "oauth", + label: ".well-known/oauth-protected-resource validates", + description: + "Protected resource metadata must declare a resource matching the PDS and at least one authorization_servers entry.", + requires: ["pds"], + run: async (ctx): Promise => { + if (!protectedResource) { + return { + status: "skip", + message: "Protected resource metadata unavailable", + }; + } + const issues: string[] = []; + const expectedResource = ctx.pds + ? trimTrailingSlash(ctx.pds) + : undefined; + const resource = protectedResource.resource; + if (typeof resource !== "string" || resource.length === 0) { + issues.push("missing field: resource"); + } else if ( + expectedResource && + trimTrailingSlash(resource) !== expectedResource + ) { + issues.push( + `resource: expected ${expectedResource}, got ${resource}`, + ); + } + const servers = protectedResource.authorization_servers; + if (!Array.isArray(servers) || servers.length === 0) { + issues.push( + "missing or empty field: authorization_servers (must be non-empty array)", + ); + } else if (!servers.every((s) => typeof s === "string" && s.length > 0)) { + issues.push("authorization_servers must contain non-empty strings"); + } else { + authServerUrl = trimTrailingSlash(servers[0]!); + } + if (issues.length > 0) { + return { + status: "fail", + message: issues[0]!, + evidence: { + actual: protectedResource, + error: issues.join("\n"), + }, + }; + } + return { + status: "pass", + message: `resource=${resource}, authorization_servers[${servers!.length}]`, + evidence: { actual: protectedResource }, + }; + }, +}; + +const authServerResponds: Check = { + id: "oauth.auth-server-responds", + category: "oauth", + label: ".well-known/oauth-authorization-server responds", + description: + "RFC 8414 OAuth Authorization Server Metadata document must be served by the authorization server.", + requires: ["pds"], + run: async (): Promise => { + if (!authServerUrl) { + return { + status: "skip", + message: "no authorization_server discovered", + }; + } + const url = `${authServerUrl}/.well-known/oauth-authorization-server`; + const result = await fetchJson(url); + if (!result.ok) { + return { + status: "fail", + message: result.error, + evidence: { + request: { method: "GET", url }, + response: { status: result.status, body: result.body }, + error: result.error, + }, + }; + } + if (!result.body || typeof result.body !== "object") { + return { + status: "fail", + message: "Response body is not a JSON object", + evidence: { + request: { method: "GET", url }, + response: { status: result.status, body: result.body }, + }, + }; + } + authServerMetadata = result.body as Record; + return { + status: "pass", + message: `HTTP ${result.status}`, + evidence: { + request: { method: "GET", url }, + response: { status: result.status, body: result.body }, + }, + }; + }, +}; + +const REQUIRED_RFC8414_FIELDS = [ + "issuer", + "authorization_endpoint", + "token_endpoint", + "response_types_supported", +] as const; + +function asStringArray(value: unknown): string[] | undefined { + if (!Array.isArray(value)) return undefined; + return value.every((v) => typeof v === "string") + ? (value as string[]) + : undefined; +} + +const authServerValidates: Check = { + id: "oauth.auth-server-validates", + category: "oauth", + label: ".well-known/oauth-authorization-server validates", + description: + "Metadata must satisfy RFC 8414 required fields and the AT Protocol OAuth profile (DPoP ES256, PAR, S256, atproto scope, client_id_metadata_document_supported).", + requires: ["pds"], + run: async (): Promise => { + if (!authServerMetadata) { + return { + status: "skip", + message: "Authorization server metadata unavailable", + }; + } + const md = authServerMetadata; + const failures: string[] = []; + + for (const field of REQUIRED_RFC8414_FIELDS) { + if (md[field] === undefined || md[field] === null) { + failures.push(`missing field: ${field}`); + } + } + if ( + md.response_types_supported !== undefined && + !asStringArray(md.response_types_supported) + ) { + failures.push("response_types_supported must be an array of strings"); + } + + const dpopAlgs = asStringArray(md.dpop_signing_alg_values_supported); + if (!dpopAlgs) { + failures.push( + "missing field: dpop_signing_alg_values_supported (must include ES256)", + ); + } else if (!dpopAlgs.includes("ES256")) { + failures.push( + `dpop_signing_alg_values_supported: must include ES256, got [${dpopAlgs.join(", ")}]`, + ); + } + + if (typeof md.pushed_authorization_request_endpoint !== "string") { + failures.push("missing field: pushed_authorization_request_endpoint"); + } + + if (md.require_pushed_authorization_requests !== true) { + failures.push( + `require_pushed_authorization_requests: expected true, got ${JSON.stringify( + md.require_pushed_authorization_requests, + )}`, + ); + } + + const codeChallenge = asStringArray(md.code_challenge_methods_supported); + if (!codeChallenge) { + failures.push( + "missing field: code_challenge_methods_supported (must include S256)", + ); + } else if (!codeChallenge.includes("S256")) { + failures.push( + `code_challenge_methods_supported: must include S256, got [${codeChallenge.join(", ")}]`, + ); + } + + const scopes = asStringArray(md.scopes_supported); + if (!scopes) { + failures.push("missing field: scopes_supported (must include atproto)"); + } else if (!scopes.includes("atproto")) { + failures.push( + `scopes_supported: must include atproto, got [${scopes.join(", ")}]`, + ); + } + + if (md.client_id_metadata_document_supported !== true) { + failures.push( + `client_id_metadata_document_supported: expected true, got ${JSON.stringify( + md.client_id_metadata_document_supported, + )}`, + ); + } + + if (typeof md.jwks_uri === "string" && md.jwks_uri.length > 0) { + jwksUri = md.jwks_uri; + } + + const warnings: string[] = []; + if (md.grant_types_supported === undefined) { + warnings.push( + "recommended field missing: grant_types_supported (should include authorization_code and refresh_token)", + ); + } else { + const grants = asStringArray(md.grant_types_supported); + if (grants) { + if (!grants.includes("authorization_code")) { + warnings.push( + "grant_types_supported: should include authorization_code", + ); + } + if (!grants.includes("refresh_token")) { + warnings.push( + "grant_types_supported: should include refresh_token", + ); + } + } + } + if (md.token_endpoint_auth_methods_supported === undefined) { + warnings.push( + "recommended field missing: token_endpoint_auth_methods_supported", + ); + } + if (md.jwks_uri === undefined) { + warnings.push("recommended field missing: jwks_uri"); + } + + if (failures.length > 0) { + return { + status: "fail", + message: failures[0]!, + evidence: { + actual: md, + error: [...failures, ...warnings].join("\n"), + }, + }; + } + if (warnings.length > 0) { + return { + status: "warn", + message: warnings[0]!, + evidence: { + actual: md, + error: warnings.join("\n"), + }, + }; + } + return { + status: "pass", + message: "RFC 8414 + AT Protocol OAuth profile satisfied", + evidence: { actual: md }, + }; + }, +}; + +const jwksResponds: Check = { + id: "oauth.jwks-responds", + category: "oauth", + label: "JWKS endpoint responds", + description: + "The authorization server's jwks_uri must return a JSON Web Key Set.", + requires: ["pds"], + run: async (): Promise => { + if (!jwksUri) { + return { status: "skip", message: "no jwks_uri" }; + } + const result = await fetchJson(jwksUri); + if (!result.ok) { + return { + status: "fail", + message: result.error, + evidence: { + request: { method: "GET", url: jwksUri }, + response: { status: result.status, body: result.body }, + error: result.error, + }, + }; + } + if (!result.body || typeof result.body !== "object") { + return { + status: "fail", + message: "Response body is not a JSON object", + evidence: { + request: { method: "GET", url: jwksUri }, + response: { status: result.status, body: result.body }, + }, + }; + } + jwksDocument = result.body as JwksDocument; + return { + status: "pass", + message: `HTTP ${result.status}`, + evidence: { + request: { method: "GET", url: jwksUri }, + response: { status: result.status, body: result.body }, + }, + }; + }, +}; + +const jwksValidates: Check = { + id: "oauth.jwks-validates", + category: "oauth", + label: "JWKS document validates", + description: + "JWKS must be a JSON object with a `keys` array per RFC 7517. Each key, if present, must carry kty/kid (alg is recommended). An empty keys array is technically valid — the AS may not need to publish any signing keys (DPoP uses client-side keys, not AS keys).", + requires: ["pds"], + run: async (): Promise => { + if (!jwksDocument) { + return { status: "skip", message: "JWKS document unavailable" }; + } + const hardFailures: string[] = []; + const warnings: string[] = []; + const keys = jwksDocument.keys; + if (!Array.isArray(keys)) { + return { + status: "fail", + message: "missing field: keys (must be an array)", + evidence: { + actual: jwksDocument, + error: "missing field: keys", + }, + }; + } + if (keys.length === 0) { + warnings.push( + "keys array is empty (valid per RFC 7517, but AS publishes no verifiable signing keys)", + ); + } + keys.forEach((key, i) => { + if (!key || typeof key !== "object") { + hardFailures.push(`keys[${i}]: not a JSON object`); + return; + } + // kty is required per RFC 7517 §4.1 + if (typeof key.kty !== "string" || (key.kty as string).length === 0) { + hardFailures.push(`keys[${i}]: missing field kty (required by RFC 7517 §4.1)`); + } + // kid is recommended for key rotation; warn if absent + if (typeof key.kid !== "string" || (key.kid as string).length === 0) { + warnings.push(`keys[${i}]: missing kid (recommended for key rotation)`); + } + // alg is recommended for clarity; warn if absent + if (typeof key.alg !== "string" || (key.alg as string).length === 0) { + warnings.push(`keys[${i}]: missing alg (recommended)`); + } + }); + if (hardFailures.length > 0) { + return { + status: "fail", + message: hardFailures[0]!, + evidence: { + actual: jwksDocument, + error: [...hardFailures, ...warnings].join("\n"), + }, + }; + } + if (warnings.length > 0) { + return { + status: "warn", + message: + warnings.length === 1 + ? warnings[0]! + : `${warnings.length} non-blocking issues`, + evidence: { + actual: jwksDocument, + error: warnings.join("\n"), + }, + }; + } + return { + status: "pass", + message: `${keys.length} key${keys.length === 1 ? "" : "s"}`, + evidence: { actual: jwksDocument }, + }; + }, +}; + +const scopeAdvertises = ( + id: string, + label: string, + predicate: (scopes: readonly string[]) => CheckOutcome, +): Check => ({ + id, + category: "oauth", + label, + requires: ["pds"], + run: async (): Promise => { + if (!authServerMetadata) { + return { + status: "skip", + message: "auth server metadata unavailable", + }; + } + const raw = authServerMetadata.scopes_supported; + if (!Array.isArray(raw)) { + return { + status: "fail", + message: "scopes_supported missing or not an array", + evidence: { actual: raw }, + }; + } + const scopes = raw.filter((s): s is string => typeof s === "string"); + return predicate(scopes); + }, +}); + +const scopeAdvertisesAtproto = scopeAdvertises( + "oauth-discovery.scope-atproto", + "Auth server advertises atproto scope", + (scopes) => + scopes.includes("atproto") + ? { + status: "pass", + message: "atproto scope advertised (required)", + } + : { + status: "fail", + message: "scopes_supported is missing atproto (required by spec)", + evidence: { actual: scopes }, + }, +); + +const scopeAdvertisesTransitionGeneric = scopeAdvertises( + "oauth-discovery.scope-transition-generic", + "Auth server advertises transition:generic", + (scopes) => + scopes.includes("transition:generic") + ? { + status: "pass", + message: "transition:generic advertised (legacy bundle)", + } + : { + status: "warn", + message: + "transition:generic missing — most clients still rely on this bundle", + evidence: { actual: scopes }, + }, +); + +const scopeAdvertisesPhase2Granular = scopeAdvertises( + "oauth-discovery.scope-phase2-granular", + "Auth server advertises Phase 2 granular scopes", + (scopes) => { + const granular = scopes.filter( + (s) => + s === "repo:read" || + s === "repo:write" || + s.startsWith("repo:write:") || + s.startsWith("repo:read:") || + s.startsWith("account.") || + s.startsWith("pds."), + ); + if (granular.length === 0) { + return { + status: "warn", + message: + "no Phase 2 granular scopes (repo:read, repo:write:, account.*, pds.*) advertised", + evidence: { actual: scopes }, + }; + } + return { + status: "pass", + message: `${granular.length} granular scope${granular.length === 1 ? "" : "s"}: ${granular.slice(0, 4).join(", ")}${granular.length > 4 ? "…" : ""}`, + evidence: { actual: granular }, + }; + }, +); + +const scopeAdvertisesResourceBuckets = scopeAdvertises( + "oauth-discovery.scope-resource-buckets", + "Auth server advertises Phase 2 resource-bucket scopes", + (scopes) => { + // Per discussion #4013, the Phase 2 design organizes permissions into + // resource buckets: repo, rpc, blob, identity, account. + const buckets: Record = { + repo: [], + rpc: [], + blob: [], + identity: [], + account: [], + }; + for (const s of scopes) { + for (const bucket of Object.keys(buckets)) { + if (s === bucket || s.startsWith(`${bucket}:`) || s.startsWith(`${bucket}.`)) { + buckets[bucket]!.push(s); + } + } + } + const present = Object.entries(buckets).filter( + ([, v]) => v.length > 0, + ); + if (present.length === 0) { + return { + status: "warn", + message: + "no resource-bucket scopes (repo/rpc/blob/identity/account) advertised", + evidence: { actual: scopes }, + }; + } + return { + status: "pass", + message: `buckets advertised: ${present.map(([k]) => k).join(", ")}`, + evidence: { actual: Object.fromEntries(present) }, + }; + }, +); + +const scopeAdvertisesPermissionSets = scopeAdvertises( + "oauth-discovery.scope-permission-sets", + "Auth server advertises permission set scopes", + (scopes) => { + const sets = scopes.filter((s) => s.startsWith("include:")); + if (sets.length === 0) { + return { + status: "warn", + message: "no permission set scopes (include:*) advertised", + evidence: { actual: scopes }, + }; + } + return { + status: "pass", + message: `${sets.length} permission set${sets.length === 1 ? "" : "s"}: ${sets.join(", ")}`, + evidence: { actual: sets }, + }; + }, +); + +export const oauthDiscoveryChecks: Check[] = [ + protectedResourceResponds, + protectedResourceValidates, + authServerResponds, + authServerValidates, + scopeAdvertisesAtproto, + scopeAdvertisesTransitionGeneric, + scopeAdvertisesPhase2Granular, + scopeAdvertisesResourceBuckets, + scopeAdvertisesPermissionSets, + jwksResponds, + jwksValidates, +]; diff --git a/apps/check/src/checks/repo-read.ts b/apps/check/src/checks/repo-read.ts new file mode 100644 index 00000000..79793a86 --- /dev/null +++ b/apps/check/src/checks/repo-read.ts @@ -0,0 +1,641 @@ +import { + ComAtprotoRepoDescribeRepo, + ComAtprotoRepoGetRecord, + ComAtprotoRepoListRecords, +} from "@atcute/atproto"; +import { Client, simpleFetchHandler } from "@atcute/client"; +import type { Did, Nsid } from "@atcute/lexicons/syntax"; +import { CarReader } from "@ipld/car"; +import { validateLexicon } from "../lib/xrpc"; +import type { Check, CheckOutcome } from "../types"; + +let cachedClient: { pds: string; client: Client } | undefined; + +function getClient(pds: string): Client { + if (cachedClient && cachedClient.pds === pds) return cachedClient.client; + const client = new Client({ handler: simpleFetchHandler({ service: pds }) }); + cachedClient = { pds, client }; + return client; +} + +interface SampledRecord { + collection: string; + uri: string; + cid: string; + rkey: string; +} + +let collections: string[] | undefined; +let sampleRecord: SampledRecord | undefined; +let listRecordsEmpty = false; +let describeRepoBody: ComAtprotoRepoDescribeRepo.$output | undefined; +let listRecordsBody: ComAtprotoRepoListRecords.$output | undefined; +let getRecordBody: ComAtprotoRepoGetRecord.$output | undefined; +let repoCarBytes: Uint8Array | undefined; + +function xrpcUrl(pds: string, nsid: string, params: Record): string { + const qs = new URLSearchParams(params).toString(); + return `${pds}/xrpc/${nsid}${qs ? `?${qs}` : ""}`; +} + +function rkeyFromUri(uri: string): string { + const parts = uri.split("/"); + return parts[parts.length - 1] ?? ""; +} + +const describeRepo: Check = { + id: "repo-read.describe-repo", + category: "repo-read", + label: "Describe repo", + requires: ["pds", "did"], + run: async (ctx): Promise => { + collections = undefined; + sampleRecord = undefined; + listRecordsEmpty = false; + describeRepoBody = undefined; + listRecordsBody = undefined; + getRecordBody = undefined; + repoCarBytes = undefined; + + const pds = ctx.pds!; + const did = ctx.did!; + const url = xrpcUrl(pds, "com.atproto.repo.describeRepo", { repo: did }); + try { + const res = await getClient(pds).get("com.atproto.repo.describeRepo", { + params: { repo: did as Did }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.status} ${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } + const body = res.data; + if (body.did !== did) { + return { + status: "fail", + message: `DID mismatch: expected ${did}, got ${body.did}`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body }, + expected: did, + actual: body.did, + }, + }; + } + if (!Array.isArray(body.collections) || typeof body.handle !== "string") { + return { + status: "fail", + message: "Response missing handle or collections", + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body }, + }, + }; + } + collections = body.collections; + describeRepoBody = body; + return { + status: "pass", + message: `handle ${body.handle}, ${body.collections.length} collections`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { + request: { method: "GET", url }, + error: String(error), + }, + }; + } + }, +}; + +const listCollections: Check = { + id: "repo-read.list-collections", + category: "repo-read", + label: "List collections", + requires: ["pds", "did"], + run: async (): Promise => { + if (collections === undefined) { + return { status: "skip", message: "describeRepo did not succeed" }; + } + if (collections.length === 0) { + return { + status: "warn", + message: "No collections (empty or new repo)", + evidence: { actual: collections }, + }; + } + return { + status: "pass", + message: `${collections.length}: ${collections.slice(0, 4).join(", ")}${collections.length > 4 ? "…" : ""}`, + evidence: { actual: collections }, + }; + }, +}; + +const listRecords: Check = { + id: "repo-read.list-records", + category: "repo-read", + label: "List records", + requires: ["pds", "did"], + run: async (ctx): Promise => { + const pds = ctx.pds!; + const did = ctx.did!; + const collection = + collections && collections.length > 0 ? collections[0]! : "app.bsky.feed.post"; + const url = xrpcUrl(pds, "com.atproto.repo.listRecords", { + repo: did, + collection, + limit: "5", + }); + try { + const res = await getClient(pds).get("com.atproto.repo.listRecords", { + params: { repo: did as Did, collection: collection as Nsid, limit: 5 }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.status} ${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } + const records = res.data.records; + if (!Array.isArray(records)) { + return { + status: "fail", + message: "Response missing records array", + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } + if (records.length === 0) { + listRecordsEmpty = true; + return { + status: "warn", + message: `No records in ${collection}`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } + const first = records[0]!; + sampleRecord = { + collection, + uri: first.uri, + cid: first.cid, + rkey: rkeyFromUri(first.uri), + }; + listRecordsBody = res.data; + return { + status: "pass", + message: `${records.length} from ${collection}`, + evidence: { + request: { method: "GET", url }, + response: { + status: res.status, + body: { cursor: res.data.cursor, sample: sampleRecord }, + }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { + request: { method: "GET", url }, + error: String(error), + }, + }; + } + }, +}; + +const getRecord: Check = { + id: "repo-read.get-record", + category: "repo-read", + label: "Get record", + requires: ["pds", "did"], + run: async (ctx): Promise => { + if (listRecordsEmpty) { + return { status: "skip", message: "No records to fetch" }; + } + if (!sampleRecord) { + return { status: "skip", message: "No sample record available" }; + } + const pds = ctx.pds!; + const did = ctx.did!; + const { collection, rkey, cid: expectedCid, uri } = sampleRecord; + const url = xrpcUrl(pds, "com.atproto.repo.getRecord", { + repo: did, + collection, + rkey, + }); + try { + const res = await getClient(pds).get("com.atproto.repo.getRecord", { + params: { repo: did as Did, collection: collection as Nsid, rkey }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.status} ${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } + if (res.data.cid !== expectedCid) { + return { + status: "fail", + message: `CID mismatch for ${uri}`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + expected: expectedCid, + actual: res.data.cid, + }, + }; + } + getRecordBody = res.data; + return { + status: "pass", + message: `CID matches (${expectedCid.slice(0, 16)}…)`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: { uri: res.data.uri, cid: res.data.cid } }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { + request: { method: "GET", url }, + error: String(error), + }, + }; + } + }, +}; + +const listRecordsCursor: Check = { + id: "repo-read.list-records-cursor", + category: "repo-read", + label: "Paginate listRecords with cursor", + requires: ["pds", "did"], + run: async (ctx): Promise => { + if (listRecordsEmpty) { + return { status: "skip", message: "No records to paginate" }; + } + const pds = ctx.pds!; + const did = ctx.did!; + const collection = sampleRecord?.collection ?? collections?.[0]; + if (!collection) { + return { status: "skip", message: "No collection to paginate" }; + } + const url1 = xrpcUrl(pds, "com.atproto.repo.listRecords", { + repo: did, + collection, + limit: "1", + }); + try { + const client = getClient(pds); + const first = await client.get("com.atproto.repo.listRecords", { + params: { repo: did as Did, collection: collection as Nsid, limit: 1 }, + }); + if (!first.ok) { + return { + status: "fail", + message: `First page: ${first.status} ${first.data.error}`, + evidence: { + request: { method: "GET", url: url1 }, + response: { status: first.status, body: first.data }, + }, + }; + } + const firstRecord = first.data.records[0]; + const cursor = first.data.cursor; + if (!firstRecord) { + return { + status: "warn", + message: "First page empty", + evidence: { + request: { method: "GET", url: url1 }, + response: { status: first.status, body: first.data }, + }, + }; + } + if (!cursor) { + return { + status: "pass", + message: "Only one record; no cursor returned (end of list)", + evidence: { + request: { method: "GET", url: url1 }, + response: { status: first.status, body: first.data }, + }, + }; + } + const url2 = xrpcUrl(pds, "com.atproto.repo.listRecords", { + repo: did, + collection, + limit: "1", + cursor, + }); + const second = await client.get("com.atproto.repo.listRecords", { + params: { repo: did as Did, collection: collection as Nsid, limit: 1, cursor }, + }); + if (!second.ok) { + return { + status: "fail", + message: `Second page: ${second.status} ${second.data.error}`, + evidence: { + request: { method: "GET", url: url2 }, + response: { status: second.status, body: second.data }, + }, + }; + } + const secondRecord = second.data.records[0]; + if (!secondRecord) { + if (second.data.cursor) { + return { + status: "fail", + message: "Cursor returned but second page had no records", + evidence: { + request: { method: "GET", url: url2 }, + response: { status: second.status, body: second.data }, + }, + }; + } + return { + status: "pass", + message: "Cursor exhausted cleanly (empty page, no cursor)", + evidence: { + request: { method: "GET", url: url2 }, + response: { status: second.status, body: second.data }, + }, + }; + } + if (secondRecord.uri === firstRecord.uri) { + return { + status: "fail", + message: "Cursor returned same record as first page", + evidence: { + request: { method: "GET", url: url2 }, + response: { status: second.status, body: second.data }, + expected: `uri !== ${firstRecord.uri}`, + actual: secondRecord.uri, + }, + }; + } + return { + status: "pass", + message: "Cursor advanced to a new record", + evidence: { + request: { method: "GET", url: url2 }, + response: { + status: second.status, + body: { first: firstRecord.uri, second: secondRecord.uri }, + }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { error: String(error) }, + }; + } + }, +}; + +const getRepoCar: Check = { + id: "repo-read.get-repo-car", + category: "repo-read", + label: "Download repo CAR", + description: + "Fetches the entire repository as a CAR file. Skipped by default because repos can be hundreds of MB; opt-in via the landing-page checkbox.", + requires: ["pds", "did"], + run: async (ctx): Promise => { + if (!ctx.downloadCar) { + return { + status: "skip", + message: + "opt-in required — repos can be hundreds of MB; enable on the landing page to run this", + }; + } + const pds = ctx.pds!; + const did = ctx.did!; + const url = xrpcUrl(pds, "com.atproto.sync.getRepo", { did }); + try { + const res = await fetch(url); + const contentType = res.headers.get("content-type") ?? ""; + if (!res.ok) { + let body: unknown; + try { + body = await res.json(); + } catch { + body = await res.text().catch(() => undefined); + } + return { + status: "fail", + message: `${res.status} ${res.statusText}`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body }, + }, + }; + } + const bytes = new Uint8Array(await res.arrayBuffer()); + if (!contentType.includes("application/vnd.ipld.car")) { + return { + status: "fail", + message: `Unexpected content-type: ${contentType || "(none)"}`, + evidence: { + request: { method: "GET", url }, + response: { + status: res.status, + body: { contentType, byteLength: bytes.byteLength }, + }, + expected: "application/vnd.ipld.car", + actual: contentType, + }, + }; + } + if (bytes.byteLength === 0) { + return { + status: "fail", + message: "CAR response was empty", + evidence: { + request: { method: "GET", url }, + response: { + status: res.status, + body: { contentType, byteLength: 0 }, + }, + }, + }; + } + repoCarBytes = bytes; + return { + status: "pass", + message: `${bytes.byteLength.toLocaleString()} bytes of CAR`, + evidence: { + request: { method: "GET", url }, + response: { + status: res.status, + body: { contentType, byteLength: bytes.byteLength }, + }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { + request: { method: "GET", url }, + error: String(error), + }, + }; + } + }, +}; + +const describeRepoValidates: Check = { + id: "repo-read.describe-repo.validates", + category: "repo-read", + label: "describeRepo response matches lexicon", + requires: ["pds", "did"], + run: async (): Promise => { + if (!describeRepoBody) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoRepoDescribeRepo.mainSchema.output.schema, + describeRepoBody, + ); + }, +}; + +const listRecordsValidates: Check = { + id: "repo-read.list-records.validates", + category: "repo-read", + label: "listRecords response matches lexicon", + requires: ["pds", "did"], + run: async (): Promise => { + if (!listRecordsBody) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoRepoListRecords.mainSchema.output.schema, + listRecordsBody, + ); + }, +}; + +const getRecordValidates: Check = { + id: "repo-read.get-record.validates", + category: "repo-read", + label: "getRecord response matches lexicon", + requires: ["pds", "did"], + run: async (): Promise => { + if (!getRecordBody) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoRepoGetRecord.mainSchema.output.schema, + getRecordBody, + ); + }, +}; + +const getRepoCarValidates: Check = { + id: "repo-read.get-repo-car.validates", + category: "repo-read", + label: "CAR file parses cleanly", + requires: ["pds", "did"], + run: async (): Promise => { + if (!repoCarBytes) { + return { status: "skip", message: "no CAR bytes to parse" }; + } + try { + const reader = await CarReader.fromBytes(repoCarBytes); + const roots = await reader.getRoots(); + if (roots.length === 0) { + return { + status: "fail", + message: "CAR header declares no roots", + evidence: { actual: roots }, + }; + } + let blockCount = 0; + const cidStrings = new Set(); + for await (const block of reader.blocks()) { + blockCount++; + cidStrings.add(block.cid.toString()); + } + if (blockCount === 0) { + return { + status: "fail", + message: "CAR contains no blocks", + }; + } + const missingRoots = roots + .filter((cid) => !cidStrings.has(cid.toString())) + .map((cid) => cid.toString()); + if (missingRoots.length > 0) { + return { + status: "fail", + message: `${missingRoots.length} root CID(s) not present in block set`, + evidence: { expected: missingRoots, actual: [...cidStrings].slice(0, 5) }, + }; + } + return { + status: "pass", + message: `${blockCount} block(s), root ${roots[0]!.toString().slice(0, 24)}…`, + evidence: { + response: { + body: { + blockCount, + roots: roots.map((c) => c.toString()), + }, + }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { error: String(error) }, + }; + } + }, +}; + +export const repoReadChecks: Check[] = [ + describeRepo, + describeRepoValidates, + listCollections, + listRecords, + listRecordsValidates, + getRecord, + getRecordValidates, + listRecordsCursor, + getRepoCar, + getRepoCarValidates, +]; diff --git a/apps/check/src/checks/repo-write.ts b/apps/check/src/checks/repo-write.ts new file mode 100644 index 00000000..485c7624 --- /dev/null +++ b/apps/check/src/checks/repo-write.ts @@ -0,0 +1,515 @@ +import { + ComAtprotoRepoApplyWrites, + ComAtprotoRepoCreateRecord, + ComAtprotoRepoDeleteRecord, + ComAtprotoRepoListRecords, + ComAtprotoRepoUploadBlob, +} from "@atcute/atproto"; +import { Client } from "@atcute/client"; +import type { ActorIdentifier, Did, Nsid } from "@atcute/lexicons/syntax"; +import { validateLexicon } from "../lib/xrpc"; +import type { Check, CheckOutcome } from "../types"; + +const TEST_COLLECTION = "earth.cirrus.check.testrecord" as Nsid; + +let createdUri: string | undefined; +let createdCid: string | undefined; +let blobRef: + | { $type: "blob"; ref: { $link: string }; mimeType: string; size: number } + | undefined; +let blobRecordUri: string | undefined; + +let createRecordBody: ComAtprotoRepoCreateRecord.$output | undefined; +let listRecordsIncludesBody: ComAtprotoRepoListRecords.$output | undefined; +let applyWritesBody: ComAtprotoRepoApplyWrites.$output | undefined; +let deleteRecordBody: ComAtprotoRepoDeleteRecord.$output | undefined; +let uploadBlobBody: ComAtprotoRepoUploadBlob.$output | undefined; + +function reset() { + createdUri = undefined; + createdCid = undefined; + blobRef = undefined; + blobRecordUri = undefined; + createRecordBody = undefined; + listRecordsIncludesBody = undefined; + applyWritesBody = undefined; + deleteRecordBody = undefined; + uploadBlobBody = undefined; + currentRunId = `run-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; +} + +function sessionMismatch(ctx: { + did?: string; + agent?: { sub: Did }; +}): CheckOutcome | null { + if (!ctx.agent) { + return { status: "skip", message: "No active session" }; + } + if (ctx.agent.sub !== ctx.did) { + return { + status: "skip", + message: `Session is for ${ctx.agent.sub}, target is ${ctx.did}`, + }; + } + return null; +} + +function buildClient(agent: { handle: (path: string, init?: RequestInit) => Promise }) { + return new Client({ handler: agent }); +} + +let currentRunId: string | undefined; + +function makeTestRecord(extra: Record = {}) { + return { + $type: TEST_COLLECTION, + message: "pdscheck verification — safe to delete", + createdAt: new Date().toISOString(), + verifier: location.origin, + runId: currentRunId, + ...extra, + }; +} + +const createRecord: Check = { + id: "repo-write.create-record", + category: "repo-write", + label: "Create record", + requires: ["pds", "session"], + run: async (ctx): Promise => { + reset(); + const guard = sessionMismatch(ctx); + if (guard) return guard; + const client = buildClient(ctx.agent!); + const res = await client.post("com.atproto.repo.createRecord", { + input: { + repo: ctx.did as ActorIdentifier, + collection: TEST_COLLECTION, + record: makeTestRecord(), + }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + createdUri = res.data.uri; + createdCid = res.data.cid; + createRecordBody = res.data; + return { + status: "pass", + message: res.data.uri, + evidence: { response: { status: res.status, body: res.data } }, + }; + }, +}; + +const getRecord: Check = { + id: "repo-write.get-created-record", + category: "repo-write", + label: "Get created record", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + if (!createdUri || !createdCid) { + return { status: "skip", message: "createRecord did not succeed" }; + } + const rkey = createdUri.split("/").pop()!; + const client = buildClient(ctx.agent!); + const res = await client.get("com.atproto.repo.getRecord", { + params: { + repo: ctx.did as ActorIdentifier, + collection: TEST_COLLECTION, + rkey, + }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + if (res.data.cid !== createdCid) { + return { + status: "fail", + message: "Returned CID does not match created record", + evidence: { expected: createdCid, actual: res.data.cid }, + }; + } + return { + status: "pass", + message: `CID matches (${createdCid.slice(0, 16)}…)`, + evidence: { response: { status: res.status, body: res.data } }, + }; + }, +}; + +const listRecordsIncludes: Check = { + id: "repo-write.list-includes-created", + category: "repo-write", + label: "listRecords surfaces created record", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + if (!createdUri) { + return { status: "skip", message: "No record was created" }; + } + const client = buildClient(ctx.agent!); + const res = await client.get("com.atproto.repo.listRecords", { + params: { + repo: ctx.did as ActorIdentifier, + collection: TEST_COLLECTION, + limit: 10, + }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + listRecordsIncludesBody = res.data; + const found = res.data.records.some((r) => r.uri === createdUri); + if (!found) { + return { + status: "fail", + message: "Created record not listed", + evidence: { + expected: createdUri, + actual: res.data.records.map((r) => r.uri), + }, + }; + } + return { + status: "pass", + message: `${res.data.records.length} record(s) in ${TEST_COLLECTION}`, + }; + }, +}; + +const applyWritesAtomic: Check = { + id: "repo-write.apply-writes", + category: "repo-write", + label: "applyWrites atomic create+delete", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + const client = buildClient(ctx.agent!); + const ephemeralRkey = `tmp-${Date.now().toString(36)}`; + const res = await client.post("com.atproto.repo.applyWrites", { + input: { + repo: ctx.did as ActorIdentifier, + writes: [ + { + $type: "com.atproto.repo.applyWrites#create", + collection: TEST_COLLECTION, + rkey: ephemeralRkey, + value: makeTestRecord(), + }, + { + $type: "com.atproto.repo.applyWrites#delete", + collection: TEST_COLLECTION, + rkey: ephemeralRkey, + }, + ], + }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + applyWritesBody = res.data; + return { + status: "pass", + message: "create + delete applied atomically", + evidence: { response: { status: res.status, body: res.data } }, + }; + }, +}; + +const deleteCreatedRecord: Check = { + id: "repo-write.delete-record", + category: "repo-write", + label: "Delete created record", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + if (!createdUri) { + return { status: "skip", message: "No record was created" }; + } + const rkey = createdUri.split("/").pop()!; + const client = buildClient(ctx.agent!); + const res = await client.post("com.atproto.repo.deleteRecord", { + input: { + repo: ctx.did as ActorIdentifier, + collection: TEST_COLLECTION, + rkey, + }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + deleteRecordBody = res.data; + createdUri = undefined; + createdCid = undefined; + return { status: "pass", message: "deleted" }; + }, +}; + +const getDeletedRecord: Check = { + id: "repo-write.deleted-record-404", + category: "repo-write", + label: "Deleted record returns 404", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + const client = buildClient(ctx.agent!); + const res = await client.get("com.atproto.repo.listRecords", { + params: { + repo: ctx.did as ActorIdentifier, + collection: TEST_COLLECTION, + limit: 50, + }, + }); + if (!res.ok) { + return { + status: "fail", + message: "listRecords failed after delete", + evidence: { response: { status: res.status, body: res.data } }, + }; + } + const stillThere = res.data.records.some((r) => r.uri === createdUri); + if (stillThere) { + return { + status: "fail", + message: "Deleted record still appears in listRecords", + }; + } + return { status: "pass", message: "record no longer listed" }; + }, +}; + +// 1×1 transparent PNG (smallest valid PNG) +const TINY_PNG = new Uint8Array([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, + 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, + 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, + 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0d, + 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, + 0x60, 0x82, +]); + +const uploadBlob: Check = { + id: "repo-write.upload-blob", + category: "repo-write", + label: "Upload blob", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + const client = buildClient(ctx.agent!); + const blob = new Blob([TINY_PNG], { type: "image/png" }); + const res = await client.post("com.atproto.repo.uploadBlob", { + input: blob, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + blobRef = res.data.blob as typeof blobRef; + uploadBlobBody = res.data; + return { + status: "pass", + message: `${TINY_PNG.byteLength} bytes uploaded`, + evidence: { response: { status: res.status, body: res.data } }, + }; + }, +}; + +const createRecordWithBlob: Check = { + id: "repo-write.create-record-with-blob", + category: "repo-write", + label: "Reference blob in record", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + if (!blobRef) { + return { status: "skip", message: "Blob upload did not succeed" }; + } + const client = buildClient(ctx.agent!); + const res = await client.post("com.atproto.repo.createRecord", { + input: { + repo: ctx.did as ActorIdentifier, + collection: TEST_COLLECTION, + record: makeTestRecord({ blob: blobRef }), + }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body: res.data } }, + }; + } + blobRecordUri = res.data.uri; + return { + status: "pass", + message: "blob referenced in new record", + }; + }, +}; + +const cleanup: Check = { + id: "repo-write.cleanup", + category: "repo-write", + label: "Clean up leftover test records", + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + const client = buildClient(ctx.agent!); + const leftovers: string[] = []; + if (createdUri) leftovers.push(createdUri); + if (blobRecordUri) leftovers.push(blobRecordUri); + + // Also sweep anything else that may still be in our test collection + try { + const list = await client.get("com.atproto.repo.listRecords", { + params: { + repo: ctx.did as ActorIdentifier, + collection: TEST_COLLECTION, + limit: 100, + }, + }); + if (list.ok) { + for (const r of list.data.records) { + if (!leftovers.includes(r.uri)) leftovers.push(r.uri); + } + } + } catch { + // best effort + } + + let deleted = 0; + const failures: string[] = []; + for (const uri of leftovers) { + const rkey = uri.split("/").pop()!; + const res = await client.post("com.atproto.repo.deleteRecord", { + input: { + repo: ctx.did as ActorIdentifier, + collection: TEST_COLLECTION, + rkey, + }, + }); + if (res.ok) deleted++; + else failures.push(uri); + } + + if (failures.length > 0) { + return { + status: "warn", + message: `${deleted} deleted, ${failures.length} stragglers`, + evidence: { actual: failures }, + }; + } + return { + status: "pass", + message: deleted === 0 ? "nothing to clean up" : `${deleted} deleted`, + }; + }, +}; + +function makeValidates( + id: string, + label: string, + schema: Parameters[0], + getter: () => unknown, +): Check { + return { + id, + category: "repo-write", + label, + requires: ["pds", "session"], + run: async (ctx): Promise => { + const guard = sessionMismatch(ctx); + if (guard) return guard; + const body = getter(); + if (!body) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon(schema, body); + }, + }; +} + +const createRecordValidates = makeValidates( + "repo-write.create-record.validates", + "createRecord response matches lexicon", + ComAtprotoRepoCreateRecord.mainSchema.output.schema, + () => createRecordBody, +); + +const listRecordsIncludesValidates = makeValidates( + "repo-write.list-includes-created.validates", + "listRecords response matches lexicon", + ComAtprotoRepoListRecords.mainSchema.output.schema, + () => listRecordsIncludesBody, +); + +const applyWritesValidates = makeValidates( + "repo-write.apply-writes.validates", + "applyWrites response matches lexicon", + ComAtprotoRepoApplyWrites.mainSchema.output.schema, + () => applyWritesBody, +); + +const deleteRecordValidates = makeValidates( + "repo-write.delete-record.validates", + "deleteRecord response matches lexicon", + ComAtprotoRepoDeleteRecord.mainSchema.output.schema, + () => deleteRecordBody, +); + +const uploadBlobValidates = makeValidates( + "repo-write.upload-blob.validates", + "uploadBlob response matches lexicon", + ComAtprotoRepoUploadBlob.mainSchema.output.schema, + () => uploadBlobBody, +); + +export const repoWriteChecks: Check[] = [ + createRecord, + createRecordValidates, + getRecord, + listRecordsIncludes, + listRecordsIncludesValidates, + applyWritesAtomic, + applyWritesValidates, + deleteCreatedRecord, + deleteRecordValidates, + getDeletedRecord, + uploadBlob, + uploadBlobValidates, + createRecordWithBlob, + cleanup, +]; diff --git a/apps/check/src/checks/server.ts b/apps/check/src/checks/server.ts new file mode 100644 index 00000000..f1164d3e --- /dev/null +++ b/apps/check/src/checks/server.ts @@ -0,0 +1,236 @@ +import { + ComAtprotoServerDescribeServer, + ComAtprotoSyncListRepos, +} from "@atcute/atproto"; +import { publicClient, validateLexicon } from "../lib/xrpc"; +import type { Check, CheckOutcome } from "../types"; + +let describeServerResponse: ComAtprotoServerDescribeServer.$output | undefined; +let listReposResponse: ComAtprotoSyncListRepos.$output | undefined; + +function reset() { + describeServerResponse = undefined; + listReposResponse = undefined; +} + +function xrpcUrl( + pds: string, + nsid: string, + params: Record, +): string { + const qs = new URLSearchParams(params).toString(); + return `${pds}/xrpc/${nsid}${qs ? `?${qs}` : ""}`; +} + +const describeServer: Check = { + id: "server.describe-server", + category: "server", + label: "describeServer", + requires: ["pds"], + run: async (ctx): Promise => { + reset(); + const pds = ctx.pds!; + const url = xrpcUrl(pds, "com.atproto.server.describeServer", {}); + try { + const res = await publicClient(pds).get( + "com.atproto.server.describeServer", + {}, + ); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } + describeServerResponse = res.data; + return { + status: "pass", + message: `did ${res.data.did}`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { + request: { method: "GET", url }, + error: String(error), + }, + }; + } + }, +}; + +const describeServerValidates: Check = { + id: "server.describe-server.validates", + category: "server", + label: "describeServer response matches lexicon", + requires: ["pds"], + run: async (): Promise => { + if (!describeServerResponse) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoServerDescribeServer.mainSchema.output.schema, + describeServerResponse, + ); + }, +}; + +const listRepos: Check = { + id: "server.list-repos", + category: "server", + label: "listRepos", + requires: ["pds"], + run: async (ctx): Promise => { + const pds = ctx.pds!; + const url = xrpcUrl(pds, "com.atproto.sync.listRepos", { limit: "10" }); + try { + const res = await publicClient(pds).get("com.atproto.sync.listRepos", { + params: { limit: 10 }, + }); + if (!res.ok) { + return { + status: "fail", + message: `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } + listReposResponse = res.data; + return { + status: "pass", + message: `${res.data.repos.length} repos`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { + request: { method: "GET", url }, + error: String(error), + }, + }; + } + }, +}; + +const listReposValidates: Check = { + id: "server.list-repos.validates", + category: "server", + label: "listRepos response matches lexicon", + requires: ["pds"], + run: async (): Promise => { + if (!listReposResponse) { + return { status: "skip", message: "no response to validate" }; + } + return validateLexicon( + ComAtprotoSyncListRepos.mainSchema.output.schema, + listReposResponse, + ); + }, +}; + +// PDS health endpoint — a de-facto convention used by @atproto/pds and most +// implementations (cirrus included). Returns { status, version }. Not in the +// formal spec but useful for surfacing the PDS's implementation name/version. +const healthCheck: Check = { + id: "server.health", + category: "server", + label: "PDS health", + requires: ["pds"], + run: async (ctx): Promise => { + const pds = ctx.pds!; + const url = `${pds}/xrpc/_health`; + try { + const res = await fetch(url, { + headers: { accept: "application/json" }, + }); + if (!res.ok) { + return { + status: "warn", + message: `HTTP ${res.status} — endpoint not implemented`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status }, + }, + }; + } + const body = (await res.json().catch(() => ({}))) as { + status?: string; + version?: string; + }; + const version = body.version; + const status = body.status; + // Explicit non-"ok" status is a real warn (PDS reporting unhealthy). + if (typeof status === "string" && status !== "ok") { + return { + status: "warn", + message: + version !== undefined + ? `version: ${version} — status: ${status}` + : `status: ${status} (no version reported)`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body }, + error: `PDS reports status="${status}" (expected "ok")`, + }, + }; + } + // Empty body — endpoint responded but with nothing useful. + if (version === undefined && status === undefined) { + return { + status: "warn", + message: "responded but body had neither version nor status", + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body }, + error: + "_health convention is { status, version } — both missing", + }, + }; + } + // Pass: either status === "ok", or status missing but we got a version. + return { + status: "pass", + message: + version !== undefined + ? status === "ok" + ? `version: ${version} — status: ok` + : `version: ${version}` + : "status: ok", + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body }, + }, + }; + } catch (error) { + return { + status: "warn", + message: error instanceof Error ? error.message : String(error), + evidence: { error: String(error) }, + }; + } + }, +}; + +export const serverChecks: Check[] = [ + healthCheck, + describeServer, + describeServerValidates, + listRepos, + listReposValidates, +]; diff --git a/apps/check/src/checks/sync.ts b/apps/check/src/checks/sync.ts new file mode 100644 index 00000000..4c93a6b1 --- /dev/null +++ b/apps/check/src/checks/sync.ts @@ -0,0 +1,322 @@ +import { + ComAtprotoSyncGetBlocks, + ComAtprotoSyncGetLatestCommit, + ComAtprotoSyncGetRepoStatus, + ComAtprotoSyncListReposByCollection, +} from "@atcute/atproto"; +import type { Did, Nsid } from "@atcute/lexicons/syntax"; +import { publicClient, validateLexicon } from "../lib/xrpc"; +import type { Check, CheckOutcome } from "../types"; + +let getLatestCommitResponse: ComAtprotoSyncGetLatestCommit.$output | undefined; +let getRepoStatusResponse: ComAtprotoSyncGetRepoStatus.$output | undefined; +let getBlocksResponseBytes: Uint8Array | undefined; +let listReposByCollectionResponse: + | ComAtprotoSyncListReposByCollection.$output + | undefined; + +function reset() { + getLatestCommitResponse = undefined; + getRepoStatusResponse = undefined; + getBlocksResponseBytes = undefined; + listReposByCollectionResponse = undefined; +} + +function xrpcUrl( + pds: string, + nsid: string, + params: Record, +): string { + const qs = new URLSearchParams(params).toString(); + return `${pds}/xrpc/${nsid}${qs ? `?${qs}` : ""}`; +} + +const getLatestCommit: Check = { + id: "sync.get-latest-commit", + category: "sync", + label: "getLatestCommit", + requires: ["pds", "did"], + run: async (ctx): Promise => { + reset(); + const pds = ctx.pds!; + const did = ctx.did!; + const url = xrpcUrl(pds, "com.atproto.sync.getLatestCommit", { did }); + try { + const res = await publicClient(pds).get( + "com.atproto.sync.getLatestCommit", + { params: { did: did as Did } }, + ); + if (!res.ok) { + return { + status: "fail", + message: + `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } + getLatestCommitResponse = res.data; + return { + status: "pass", + message: `rev ${res.data.rev}, cid ${res.data.cid.slice(0, 16)}…`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { + request: { method: "GET", url }, + error: String(error), + }, + }; + } + }, +}; + +const getLatestCommitValidates: Check = { + id: "sync.get-latest-commit.validates", + category: "sync", + label: "getLatestCommit response matches lexicon", + requires: ["pds", "did"], + run: async (): Promise => { + if (!getLatestCommitResponse) { + return { status: "skip", message: "getLatestCommit did not succeed" }; + } + return validateLexicon( + ComAtprotoSyncGetLatestCommit.mainSchema.output.schema, + getLatestCommitResponse, + ); + }, +}; + +const getRepoStatus: Check = { + id: "sync.get-repo-status", + category: "sync", + label: "getRepoStatus", + requires: ["pds", "did"], + run: async (ctx): Promise => { + const pds = ctx.pds!; + const did = ctx.did!; + const url = xrpcUrl(pds, "com.atproto.sync.getRepoStatus", { did }); + try { + const res = await publicClient(pds).get( + "com.atproto.sync.getRepoStatus", + { params: { did: did as Did } }, + ); + if (!res.ok) { + return { + status: "fail", + message: + `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } + getRepoStatusResponse = res.data; + const statusPart = res.data.status ? `, status ${res.data.status}` : ""; + const revPart = res.data.rev ? `, rev ${res.data.rev}` : ""; + return { + status: "pass", + message: `active=${res.data.active}${statusPart}${revPart}`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { + request: { method: "GET", url }, + error: String(error), + }, + }; + } + }, +}; + +const getRepoStatusValidates: Check = { + id: "sync.get-repo-status.validates", + category: "sync", + label: "getRepoStatus response matches lexicon", + requires: ["pds", "did"], + run: async (): Promise => { + if (!getRepoStatusResponse) { + return { status: "skip", message: "getRepoStatus did not succeed" }; + } + return validateLexicon( + ComAtprotoSyncGetRepoStatus.mainSchema.output.schema, + getRepoStatusResponse, + ); + }, +}; + +const getBlocks: Check = { + id: "sync.get-blocks", + category: "sync", + label: "getBlocks", + requires: ["pds", "did"], + run: async (ctx): Promise => { + if (!getLatestCommitResponse) { + return { status: "skip", message: "no commit CID available" }; + } + const pds = ctx.pds!; + const did = ctx.did!; + const cid = getLatestCommitResponse.cid; + const url = xrpcUrl(pds, "com.atproto.sync.getBlocks", { + did, + cids: cid, + }); + try { + const res = await publicClient(pds).get("com.atproto.sync.getBlocks", { + params: { did: did as Did, cids: [cid] }, + as: "bytes", + }); + if (!res.ok) { + return { + status: "fail", + message: + `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } + const bytes = res.data; + if (!bytes || bytes.byteLength === 0) { + return { + status: "fail", + message: "Empty CAR response", + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: { byteLength: 0 } }, + }, + }; + } + getBlocksResponseBytes = bytes; + return { + status: "pass", + message: `${bytes.byteLength.toLocaleString()} bytes of CAR`, + evidence: { + request: { method: "GET", url }, + response: { + status: res.status, + body: { byteLength: bytes.byteLength }, + }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { + request: { method: "GET", url }, + error: String(error), + }, + }; + } + }, +}; + +const getBlocksValidates: Check = { + id: "sync.get-blocks.validates", + category: "sync", + label: "getBlocks response matches lexicon", + requires: ["pds", "did"], + run: async (): Promise => { + if (!getBlocksResponseBytes) { + return { status: "skip", message: "getBlocks did not succeed" }; + } + return { status: "skip", message: "binary response — CAR file" }; + }, +}; + +const listReposByCollection: Check = { + id: "sync.list-repos-by-collection", + category: "sync", + label: "listReposByCollection", + requires: ["pds", "did"], + run: async (ctx): Promise => { + const pds = ctx.pds!; + const collection = "app.bsky.actor.profile"; + const url = xrpcUrl(pds, "com.atproto.sync.listReposByCollection", { + collection, + limit: "5", + }); + try { + const res = await publicClient(pds).get( + "com.atproto.sync.listReposByCollection", + { params: { collection: collection as Nsid, limit: 5 } }, + ); + if (!res.ok) { + return { + status: "fail", + message: + `${res.data.error}: ${res.data.message ?? ""}`.trim(), + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } + listReposByCollectionResponse = res.data; + return { + status: "pass", + message: `${res.data.repos.length} repos for ${collection}`, + evidence: { + request: { method: "GET", url }, + response: { status: res.status, body: res.data }, + }, + }; + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { + request: { method: "GET", url }, + error: String(error), + }, + }; + } + }, +}; + +const listReposByCollectionValidates: Check = { + id: "sync.list-repos-by-collection.validates", + category: "sync", + label: "listReposByCollection response matches lexicon", + requires: ["pds", "did"], + run: async (): Promise => { + if (!listReposByCollectionResponse) { + return { + status: "skip", + message: "listReposByCollection did not succeed", + }; + } + return validateLexicon( + ComAtprotoSyncListReposByCollection.mainSchema.output.schema, + listReposByCollectionResponse, + ); + }, +}; + +export const syncChecks: Check[] = [ + getLatestCommit, + getLatestCommitValidates, + getRepoStatus, + getRepoStatusValidates, + getBlocks, + getBlocksValidates, + listReposByCollection, + listReposByCollectionValidates, +]; diff --git a/apps/check/src/components/CheckRow.tsx b/apps/check/src/components/CheckRow.tsx new file mode 100644 index 00000000..ceb191b2 --- /dev/null +++ b/apps/check/src/components/CheckRow.tsx @@ -0,0 +1,149 @@ +import { Show, createSignal } from "solid-js"; +import { specUrlFor } from "../lib/spec-urls"; +import type { CheckResult } from "../types"; +import { StatusGlyph } from "./StatusGlyph"; + +function formatDuration(result: CheckResult): string { + if (!result.startedAt || !result.endedAt) return ""; + const ms = result.endedAt - result.startedAt; + if (ms < 1000) return `${ms}ms`; + return `${(ms / 1000).toFixed(1)}s`; +} + +export function CheckRow(props: { result: CheckResult }) { + const [open, setOpen] = createSignal(false); + + const toggle = (event: MouseEvent) => { + // Allow text-selection drags inside the row to NOT toggle expansion. + if ( + window.getSelection && + (window.getSelection()?.toString().length ?? 0) > 0 + ) + return; + setOpen(!open()); + event.preventDefault(); + }; + + const rowAccent = () => { + switch (props.result.status) { + case "fail": + case "error": + return "border-l-4 border-l-fail bg-fail/5"; + case "warn": + return "border-l-4 border-l-warn bg-warn/5"; + default: + return "border-l-4 border-l-transparent"; + } + }; + const messageTone = () => { + switch (props.result.status) { + case "fail": + case "error": + return "text-fail font-medium"; + case "warn": + return "text-warn font-medium"; + default: + return "text-muted"; + } + }; + const labelTone = () => { + switch (props.result.status) { + case "fail": + case "error": + case "warn": + return "font-bold"; + default: + return ""; + } + }; + + return ( +
  • +
    { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setOpen(!open()); + } + }} + aria-expanded={open()} + > + + + + + {props.result.check.label} + + {(message) => ( + — {message()} + )} + + + + {formatDuration(props.result)} + + + + {(url) => ( + e.stopPropagation()} + onMouseUp={(e) => e.stopPropagation()} + title="View spec / lexicon" + > + spec ↗ + + )} + + + + ▾ + +
    + +
    +
    + + check + + {props.result.check.id} +
    + + {(message) => ( +
    + + message + + + {message()} + +
    + )} +
    + + {(evidence) => ( +
    + + evidence + +
    +									{JSON.stringify(evidence(), null, 2)}
    +								
    +
    + )} +
    +
    +
    +
  • + ); +} diff --git a/apps/check/src/components/OAuthFlowView.tsx b/apps/check/src/components/OAuthFlowView.tsx new file mode 100644 index 00000000..598e5fef --- /dev/null +++ b/apps/check/src/components/OAuthFlowView.tsx @@ -0,0 +1,429 @@ +import { For, Show, createMemo, createSignal } from "solid-js"; +import type { FlowState, FlowStep } from "../lib/oauth-flow"; +import { specUrlFor } from "../lib/spec-urls"; +import { StatusGlyph } from "./StatusGlyph"; + +function durationOf(step: FlowStep): string { + if (!step.startedAt || !step.endedAt) return ""; + const ms = step.endedAt - step.startedAt; + if (ms < 1000) return `${ms}ms`; + return `${(ms / 1000).toFixed(1)}s`; +} + +function downloadString(filename: string, content: string, mime: string) { + const blob = new Blob([content], { type: mime }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.append(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); +} + +function renderFlowFinding(step: FlowStep, lines: string[]) { + lines.push(`### ${step.label}`); + lines.push(""); + lines.push(`- **Step ID:** \`${step.id}\``); + lines.push(`- **Status:** ${step.status}`); + const spec = specUrlFor(step.id); + if (spec) lines.push(`- **Spec:** ${spec}`); + if (step.message) lines.push(`- **Message:** ${step.message}`); + if (step.startedAt && step.endedAt) { + lines.push(`- **Duration:** ${step.endedAt - step.startedAt}ms`); + } + lines.push(""); + if (step.evidence) { + lines.push("**Evidence:**"); + lines.push(""); + lines.push("```json"); + lines.push(JSON.stringify(step.evidence, null, 2)); + lines.push("```"); + lines.push(""); + } +} + +function generateFlowFindingsMarkdown(state: FlowState): string { + const failures = state.steps.filter((s) => s.status === "fail"); + const warnings = state.steps.filter((s) => s.status === "warn"); + const passed = state.steps.filter((s) => s.status === "pass").length; + const skipped = state.steps.filter((s) => s.status === "skip").length; + + const lines: string[] = []; + lines.push("# OAuth conformance flow findings"); + lines.push(""); + lines.push(`**Target:** \`${state.target}\` `); + if (state.handle) lines.push(`**Handle:** \`${state.handle}\` `); + if (state.did) lines.push(`**DID:** \`${state.did}\` `); + if (state.pds) lines.push(`**PDS:** \`${state.pds}\` `); + if (state.authServerUrl) + lines.push(`**Auth server:** \`${state.authServerUrl}\` `); + lines.push(`**Run started:** ${new Date(state.startedAt).toISOString()} `); + if (state.endedAt) { + lines.push(`**Run ended:** ${new Date(state.endedAt).toISOString()} `); + } + lines.push(`**Verifier:** ${location.origin} `); + lines.push(""); + lines.push( + "> **For agents:** each finding below describes a behavioral conformance issue observed " + + "during a real OAuth dance against the target PDS. Use the evidence (HTTP requests/responses, " + + "oauth4webapi error codes, scope strings) to diagnose. atproto OAuth spec lives at " + + "https://atproto.com/specs/oauth and the implementation reference is `oauth4webapi` " + + "+ `@atproto/oauth-scopes`.", + ); + lines.push(""); + + lines.push("## Summary"); + lines.push(""); + lines.push( + `- **${failures.length}** failure${failures.length === 1 ? "" : "s"}`, + ); + if (warnings.length > 0) { + lines.push( + `- **${warnings.length}** warning${warnings.length === 1 ? "" : "s"}`, + ); + } + lines.push(`- ${passed} passed`); + if (skipped > 0) lines.push(`- ${skipped} skipped`); + lines.push(`- ${state.steps.length} steps total`); + lines.push(""); + + if (failures.length === 0 && warnings.length === 0) { + lines.push("No failures or warnings — every applicable step passed."); + return lines.join("\n"); + } + + if (failures.length > 0) { + lines.push("## Failures"); + lines.push(""); + for (const s of failures) renderFlowFinding(s, lines); + } + + if (warnings.length > 0) { + lines.push("## Warnings"); + lines.push(""); + for (const s of warnings) renderFlowFinding(s, lines); + } + + return lines.join("\n"); +} + +function downloadFlowFindings(state: FlowState) { + const slug = (state.target || "oauth-flow").replace(/[^a-z0-9.-]/gi, "_"); + downloadString( + `pdscheck-${slug}-${state.startedAt}-oauth-flow-findings.md`, + generateFlowFindingsMarkdown(state), + "text/markdown", + ); +} + +function downloadFlowJson(state: FlowState) { + const slug = (state.target || "oauth-flow").replace(/[^a-z0-9.-]/gi, "_"); + const payload = { + target: state.target, + handle: state.handle, + did: state.did, + pds: state.pds, + authServerUrl: state.authServerUrl, + startedAt: new Date(state.startedAt).toISOString(), + endedAt: state.endedAt ? new Date(state.endedAt).toISOString() : null, + phase: state.phase, + steps: state.steps.map((s) => ({ + id: s.id, + label: s.label, + status: s.status, + message: s.message, + evidence: s.evidence, + durationMs: + s.startedAt && s.endedAt ? s.endedAt - s.startedAt : null, + })), + }; + downloadString( + `pdscheck-${slug}-${state.startedAt}-oauth-flow.json`, + JSON.stringify(payload, null, 2), + "application/json", + ); +} + +function FlowStepRow(props: { step: FlowStep }) { + const [open, setOpen] = createSignal(false); + const toggle = (event: MouseEvent) => { + if ( + window.getSelection && + (window.getSelection()?.toString().length ?? 0) > 0 + ) + return; + setOpen(!open()); + event.preventDefault(); + }; + + const rowAccent = () => { + switch (props.step.status) { + case "fail": + return "border-l-4 border-l-fail bg-fail/5"; + case "warn": + return "border-l-4 border-l-warn bg-warn/5"; + default: + return "border-l-4 border-l-transparent"; + } + }; + const messageTone = () => { + switch (props.step.status) { + case "fail": + return "text-fail font-medium"; + case "warn": + return "text-warn font-medium"; + default: + return "text-muted"; + } + }; + const labelTone = () => + props.step.status === "fail" || props.step.status === "warn" + ? "font-bold" + : ""; + + return ( +
  • +
    { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setOpen(!open()); + } + }} + aria-expanded={open()} + > + + + + + {props.step.label} + + {(message) => ( + — {message()} + )} + + + + {durationOf(props.step)} + + + + {(url) => ( + e.stopPropagation()} + onMouseUp={(e) => e.stopPropagation()} + title="View spec / RFC" + > + spec ↗ + + )} + + + + ▾ + +
    + +
    +
    + step + {props.step.id} +
    + + {(message) => ( +
    + + message + + {message()} +
    + )} +
    + + {(evidence) => ( +
    + + evidence + +
    +									{JSON.stringify(evidence(), null, 2)}
    +								
    +
    + )} +
    +
    +
    +
  • + ); +} + +export function OAuthFlowView(props: { + state: FlowState; + onExit: () => void; + onRedirect?: () => void; +}) { + const summary = createMemo(() => { + let pass = 0; + let fail = 0; + let applicable = 0; + let done = 0; + for (const step of props.state.steps) { + if (step.status !== "pending" && step.status !== "running") done++; + if (step.status === "pass") { + pass++; + applicable++; + } + if ( + step.status === "fail" || + step.status === "warn" + ) { + applicable++; + } + if (step.status === "fail") fail++; + } + return { pass, fail, applicable, done, total: props.state.steps.length }; + }); + + const phaseLabel = () => + ({ + idle: "idle", + "pre-redirect": "preparing authorization request", + "ready-to-redirect": "ready to redirect — review and continue", + redirecting: "redirecting to authorization endpoint…", + "post-callback": "exchanging code and exercising token", + done: "complete", + })[props.state.phase]; + + return ( +
    +
    + + ☁️ + CHECK + + + ascorbic/cirrus + +
    + +
    +
    +
    + OAuth conformance flow +
    +
    +

    + {props.state.target} +

    + + + +
    +
    + + {summary().pass} / {summary().applicable} steps passing + 0}> + + {summary().fail} failing + + + + {phaseLabel()} +
    + +
    + + +
    +
    +
    + what next +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    + Pre-redirect checks passed +
    +

    + Continue to{" "} + + {new URL(props.state.authUrl ?? "about:blank").host} + {" "} + to authorize. The remaining checks run when you come back. +

    +
    + +
    +
    +
    + +
    +
    +
      + + {(step) => } + +
    +
    +
    +
    + ); +} diff --git a/apps/check/src/components/RecentRuns.tsx b/apps/check/src/components/RecentRuns.tsx new file mode 100644 index 00000000..7415cfca --- /dev/null +++ b/apps/check/src/components/RecentRuns.tsx @@ -0,0 +1,38 @@ +import { For, Show } from "solid-js"; +import { recentRuns } from "../lib/recent"; + +export function RecentRuns(props: { onSelect: (target: string) => void }) { + return ( + 0}> +
    +
    + Recent +
    +
      + + {(run) => { + const ok = run.fail === 0; + return ( +
    • + +
    • + ); + }} +
      +
    +
    +
    + ); +} diff --git a/apps/check/src/components/RunView.tsx b/apps/check/src/components/RunView.tsx new file mode 100644 index 00000000..a22b2543 --- /dev/null +++ b/apps/check/src/components/RunView.tsx @@ -0,0 +1,536 @@ +import { For, Show, createMemo, createSignal } from "solid-js"; +import { specUrlFor } from "../lib/spec-urls"; +import { + CATEGORY_LABELS, + CATEGORY_ORDER, + type CheckCategory, + type CheckResult, + type Run, +} from "../types"; +import { CheckRow } from "./CheckRow"; + +function gradeFor(pass: number, total: number): string { + if (total === 0) return "—"; + const pct = pass / total; + if (pct === 1) return "A+"; + if (pct >= 0.95) return "A"; + if (pct >= 0.9) return "A-"; + if (pct >= 0.85) return "B+"; + if (pct >= 0.8) return "B"; + if (pct >= 0.7) return "C"; + if (pct >= 0.5) return "D"; + return "F"; +} + +function downloadString(filename: string, content: string, mime: string): void { + const blob = new Blob([content], { type: mime }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.append(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); +} + +function generateFailuresMarkdown(run: Run): string { + const failures = run.results.filter( + (r) => r.status === "fail" || r.status === "error", + ); + const warnings = run.results.filter((r) => r.status === "warn"); + const passed = run.results.filter((r) => r.status === "pass").length; + const skipped = run.results.filter((r) => r.status === "skip").length; + + const lines: string[] = []; + lines.push("# PDS conformance findings"); + lines.push(""); + lines.push(`**Target:** \`${run.target}\` `); + lines.push(`**Run started:** ${new Date(run.startedAt).toISOString()} `); + if (run.endedAt) { + lines.push(`**Run ended:** ${new Date(run.endedAt).toISOString()} `); + } + lines.push(`**Verifier:** ${location.origin} `); + lines.push(""); + lines.push( + "> **For agents:** each finding below is a real failure observed against an AT Protocol PDS. " + + "Use the evidence (request URL, response body, validation issues with field paths) to diagnose the cause, " + + "consult the relevant spec at https://atproto.com/specs and the lexicon definitions in `@atcute/atproto`, " + + "and re-run pdscheck against the target after fixing to confirm.", + ); + lines.push(""); + + lines.push("## Summary"); + lines.push(""); + lines.push(`- **${failures.length}** failure${failures.length === 1 ? "" : "s"}`); + if (warnings.length > 0) { + lines.push( + `- **${warnings.length}** warning${warnings.length === 1 ? "" : "s"}`, + ); + } + lines.push(`- ${passed} passed`); + if (skipped > 0) lines.push(`- ${skipped} skipped`); + lines.push(`- ${run.results.length} checks total`); + lines.push(""); + + if (failures.length === 0 && warnings.length === 0) { + lines.push("No failures or warnings — every applicable check passed."); + return lines.join("\n"); + } + + const renderFinding = (r: CheckResult) => { + lines.push(`### ${r.check.label}`); + lines.push(""); + lines.push(`- **Check ID:** \`${r.check.id}\``); + lines.push(`- **Category:** ${r.check.category}`); + lines.push(`- **Status:** ${r.status}`); + const spec = specUrlFor(r.check.id); + if (spec) lines.push(`- **Spec:** ${spec}`); + if (r.outcome?.message) { + lines.push(`- **Message:** ${r.outcome.message}`); + } + if (r.startedAt && r.endedAt) { + lines.push(`- **Duration:** ${r.endedAt - r.startedAt}ms`); + } + lines.push(""); + + const evidence = r.outcome?.evidence; + if (evidence) { + if (evidence.request) { + lines.push( + `**Request:** \`${evidence.request.method} ${evidence.request.url}\``, + ); + lines.push(""); + } + if (evidence.response) { + lines.push("**Response:**"); + lines.push(""); + lines.push("```json"); + lines.push(JSON.stringify(evidence.response, null, 2)); + lines.push("```"); + lines.push(""); + } + if (evidence.expected !== undefined || evidence.actual !== undefined) { + lines.push("**Expected vs actual:**"); + lines.push(""); + lines.push("```json"); + lines.push( + JSON.stringify( + { expected: evidence.expected, actual: evidence.actual }, + null, + 2, + ), + ); + lines.push("```"); + lines.push(""); + } + if (evidence.error) { + lines.push("**Error / validation issues:**"); + lines.push(""); + lines.push("```"); + lines.push(evidence.error); + lines.push("```"); + lines.push(""); + } + } + }; + + if (failures.length > 0) { + lines.push("## Failures"); + lines.push(""); + for (const r of failures) renderFinding(r); + } + + if (warnings.length > 0) { + lines.push("## Warnings"); + lines.push(""); + for (const r of warnings) renderFinding(r); + } + + lines.push("---"); + lines.push(""); + lines.push( + `To reproduce: ${location.origin}/?target=${encodeURIComponent(run.target)}`, + ); + lines.push(""); + + return lines.join("\n"); +} + +function downloadJson(run: Run): void { + const payload = { + target: run.target, + startedAt: new Date(run.startedAt).toISOString(), + endedAt: run.endedAt ? new Date(run.endedAt).toISOString() : null, + results: run.results.map((r) => ({ + id: r.check.id, + category: r.check.category, + label: r.check.label, + status: r.status, + message: r.outcome?.message, + evidence: r.outcome?.evidence, + durationMs: + r.startedAt && r.endedAt ? r.endedAt - r.startedAt : null, + })), + }; + const slug = run.target.replace(/[^a-z0-9.-]/gi, "_"); + downloadString( + `pdscheck-${slug}-${run.startedAt}.json`, + JSON.stringify(payload, null, 2), + "application/json", + ); +} + +function downloadFailuresMarkdown(run: Run): void { + const slug = run.target.replace(/[^a-z0-9.-]/gi, "_"); + downloadString( + `pdscheck-${slug}-${run.startedAt}-findings.md`, + generateFailuresMarkdown(run), + "text/markdown", + ); +} + +function groupByCategory( + results: readonly CheckResult[], +): Array<[CheckCategory, CheckResult[]]> { + const map = new Map(); + for (const result of results) { + const list = map.get(result.check.category) ?? []; + list.push(result); + map.set(result.check.category, list); + } + return CATEGORY_ORDER.filter((c) => map.has(c)).map((c) => [c, map.get(c)!]); +} + +function summarize(results: readonly CheckResult[]) { + let done = 0; + let pass = 0; + let fail = 0; + let applicable = 0; + for (const r of results) { + if (r.status !== "pending" && r.status !== "running") done++; + if (r.status === "pass") { + pass++; + applicable++; + } + if ( + r.status === "fail" || + r.status === "error" || + r.status === "warn" + ) { + applicable++; + } + if (r.status === "fail" || r.status === "error") fail++; + } + return { + done, + pass, + fail, + applicable, + total: results.length, + }; +} + +export function RunView(props: { + run: Run; + mode: "verify" | "writes"; + onCancel: () => void; + onReadChecks?: () => void; + onWriteTests?: () => void; + onOAuthConformance?: () => void; +}) { + const groups = createMemo(() => groupByCategory(props.run.results)); + const summary = createMemo(() => summarize(props.run.results)); + const progress = createMemo(() => + summary().total === 0 ? 0 : (summary().done / summary().total) * 100, + ); + const finished = createMemo(() => props.run.endedAt !== undefined); + + return ( +
    +
    + + ☁️ + CHECK + + + ascorbic/cirrus + +
    + + +
    +
    +

    + {props.run.target} +

    + +
    +
    +
    +
    +
    + + {summary().done} / {summary().total} checks + 0}> + + {summary().fail} failing + + + + running… +
    +
    + + } + > + + + +
    +
    +
    + + {(result) => ( + + {result.check.label} failed + + )} + +
    + + + {([category, results]) => { + const catSummary = createMemo(() => summarize(results)); + return ( +
    +

    + {CATEGORY_LABELS[category]} + + 0} + fallback={skipped} + > + {catSummary().pass} / {catSummary().applicable} + + +

    +
      + + {(result) => } + +
    +
    + ); + }} +
    +
    +
    +
    + ); +} + +function ResultSummary(props: { + run: Run; + mode: "verify" | "writes"; + summary: { + done: number; + pass: number; + fail: number; + applicable: number; + total: number; + }; + total: number; + onCancel: () => void; + onReadChecks?: () => void; + onWriteTests?: () => void; + onOAuthConformance?: () => void; +}) { + const [copied, setCopied] = createSignal(false); + const duration = () => + props.run.endedAt && props.run.startedAt + ? `${((props.run.endedAt - props.run.startedAt) / 1000).toFixed(1)}s` + : ""; + const grade = () => gradeFor(props.summary.pass, props.summary.applicable); + const skipped = () => props.summary.total - props.summary.applicable; + + async function copyLink() { + try { + await navigator.clipboard.writeText(location.href); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch { + // clipboard blocked + } + } + + return ( +
    +
    +
    +
    +
    + Result +
    +

    + {props.run.target} +

    +
    +
    +
    +
    + Score +
    +
    + {props.summary.pass} / {props.summary.applicable} +
    + 0}> +
    + {skipped()} skipped +
    +
    +
    +
    +
    + Grade +
    +
    + {grade()} +
    +
    +
    +
    +
    + + + + finished in {duration()} +
    + +
    +
    + what next +
    +
    + + + + + + + + + + +
    +
    +
    +
    + ); +} diff --git a/apps/check/src/components/StatusGlyph.tsx b/apps/check/src/components/StatusGlyph.tsx new file mode 100644 index 00000000..7fdacaf7 --- /dev/null +++ b/apps/check/src/components/StatusGlyph.tsx @@ -0,0 +1,53 @@ +import { Show } from "solid-js"; +import type { CheckStatus } from "../types"; + +const GLYPHS: Record, string> = { + pending: "◯", + pass: "✓", + fail: "✗", + warn: "▲", + skip: "⊘", + error: "✗", +}; + +const LABELS: Record = { + pending: "Pending", + running: "Running", + pass: "Pass", + fail: "Fail", + warn: "Warning", + skip: "Skipped", + error: "Error", +}; + +const COLOR: Record, string> = { + pending: "text-faint", + pass: "text-pass", + fail: "text-fail", + warn: "text-warn", + skip: "text-faint", + error: "text-fail", +}; + +export function StatusGlyph(props: { status: CheckStatus }) { + return ( + ]} + aria-label={LABELS[props.status]} + role="status" + > + {GLYPHS[props.status as Exclude]} + + } + > + + + ); +} diff --git a/apps/check/src/lib/oauth-flow.ts b/apps/check/src/lib/oauth-flow.ts new file mode 100644 index 00000000..a16d3ef1 --- /dev/null +++ b/apps/check/src/lib/oauth-flow.ts @@ -0,0 +1,1984 @@ +import { Client } from "@atcute/client"; +import type { ActorIdentifier, Did, Handle } from "@atcute/lexicons/syntax"; +import { get as idbGet, set as idbSet, del as idbDel } from "idb-keyval"; +import * as oauth from "oauth4webapi"; +import { createStore, produce } from "solid-js/store"; +import { actorResolver } from "./resolvers"; + +export type StepStatus = + | "pending" + | "running" + | "pass" + | "fail" + | "warn" + | "skip"; + +export interface FlowStep { + id: string; + label: string; + status: StepStatus; + message?: string; + evidence?: unknown; + startedAt?: number; + endedAt?: number; +} + +export interface FlowState { + phase: + | "idle" + | "pre-redirect" + | "ready-to-redirect" + | "redirecting" + | "post-callback" + | "done"; + target: string; + handle?: string; + did?: string; + pds?: string; + authServerUrl?: string; + authServer?: oauth.AuthorizationServer; + protectedResource?: Record; + codeVerifier?: string; + codeChallenge?: string; + stateNonce?: string; + requestUri?: string; + authUrl?: string; + accessToken?: string; + refreshToken?: string; + tokenType?: string; + steps: FlowStep[]; + startedAt: number; + endedAt?: number; +} + +const STATE_KEY = "pdscheck.oauth-flow.state"; +const DPOP_KEY = "pdscheck.oauth-flow.dpop-keypair"; + +interface PersistedState { + target: string; + handle?: string; + did?: string; + pds?: string; + authServerUrl?: string; + codeVerifier: string; + stateNonce: string; + expectedIss: string; + scope: string; +} + +// Scope is chosen at runtime based on the auth server's advertised scopes: +// prefer the granular Phase 2 form when supported, fall back to the legacy +// bundle otherwise. PDSes like bsky that don't advertise `repo:*` will reject +// the granular scope at PAR ("invalid_scope: not declared in client metadata"), +// so we don't even attempt it on those servers. +const LEGACY_SCOPE = "atproto transition:generic"; +const GRANULAR_SCOPE = + "atproto repo:earth.cirrus.check.testrecord include:site.standard.authFull"; +const OUT_OF_SCOPE_COLLECTION = "earth.cirrus.check.othertestrecord"; +const CALLBACK_PATH = "/oauth/flow-callback"; + +// Mutable so the select-scope step can set it for the rest of the pre-redirect +// flow, and post-callback can restore it from persisted state. Single in-flight +// flow at a time so this is safe. +let activeScope: string = LEGACY_SCOPE; + +function clientId(): string { + const isLoopback = + location.hostname === "localhost" || location.hostname === "127.0.0.1"; + const redirectUri = `${location.origin}${CALLBACK_PATH}`; + if (isLoopback) { + const params = new URLSearchParams({ + redirect_uri: redirectUri, + scope: activeScope, + }); + return `http://localhost?${params.toString()}`; + } + return `${location.origin}/client-metadata.json`; +} + +// Whether the granted scope authorizes a createRecord to a given collection. +// atproto granular scopes for repo are `repo:[?action=...]` with +// default actions covering create+update+delete. `repo:*` grants any collection. +// `transition:generic` is the legacy catch-all. +function scopeGrantsWriteTo(grantedScope: string, collection: string): boolean { + const parts = grantedScope.split(/\s+/).filter(Boolean); + return parts.some((s) => { + if (s === "transition:generic") return true; + const match = s.match(/^repo:([^?]+)(?:\?(.*))?$/); + if (!match) return false; + const scopeCollection = decodeURIComponent(match[1]!); + if (scopeCollection !== "*" && scopeCollection !== collection) return false; + const actions = new URLSearchParams(match[2] ?? "").getAll("action"); + if (actions.length === 0) return true; // defaults include create + return actions.includes("create") || actions.includes("*"); + }); +} + +function redirectUri(): string { + return `${location.origin}${CALLBACK_PATH}`; +} + +// RFC 9449 §8: a DPoP-aware authorization server may require requests to carry +// a `nonce` claim in the proof, signaling this by returning use_dpop_nonce on +// the first attempt and a DPoP-Nonce response header. oauth4webapi's DPoP +// handle captures the nonce automatically from any response that carries one, +// so we just need to retry the call once after a use_dpop_nonce error. +async function withNonceRetry(call: () => Promise): Promise { + try { + return await call(); + } catch (error) { + if (oauth.isDPoPNonceError(error)) { + return await call(); + } + throw error; + } +} + +// protectedResourceRequest may THROW a WWWAuthenticateChallengeError when the +// response has an OAuth error in WWW-Authenticate (RFC 6750/9449). The +// Response is still on the error (body unused), so we unwrap to a Response and +// then check for use_dpop_nonce — if so, the DPoP handle has captured the +// nonce, retry once. Otherwise return the Response so the caller can inspect +// it normally (status/headers/body) and surface the challenge in evidence. +/** + * Reads an OAuth-style error response: prefer the JSON body's `error` / + * `error_description`, fall back to the WWW-Authenticate header, then to the + * HTTP status. Returns a concise message and the parsed body for evidence. + */ +export async function readOAuthError( + res: Response, +): Promise<{ message: string; body: unknown; wwwAuthenticate: string | null }> { + const wwwAuthenticate = res.headers.get("www-authenticate"); + let body: unknown; + try { + body = await res.clone().json(); + } catch { + body = await res.text().catch(() => ""); + } + const errBody = body as + | { + error?: string; + message?: string; + error_description?: string; + } + | undefined; + const errCode = errBody?.error; + const errDesc = errBody?.message ?? errBody?.error_description; + let message: string; + if (errCode) { + message = errDesc ? `${errCode}: ${errDesc}` : errCode; + } else if (wwwAuthenticate) { + message = `HTTP ${res.status} — ${wwwAuthenticate}`; + } else { + message = `HTTP ${res.status}`; + } + return { message, body, wwwAuthenticate }; +} + +async function protectedFetchWithNonceRetry( + ...args: Parameters +): Promise { + const call = async (): Promise => { + try { + return await oauth.protectedResourceRequest(...args); + } catch (err) { + if (err instanceof oauth.WWWAuthenticateChallengeError) { + return err.response; + } + throw err; + } + }; + + let res = await call(); + if (res.status === 400 || res.status === 401 || res.status === 403) { + const challenge = ( + res.headers.get("www-authenticate") ?? "" + ).toLowerCase(); + let isNonceChallenge = challenge.includes("use_dpop_nonce"); + if (!isNonceChallenge) { + try { + const peeked = (await res.clone().json()) as { error?: string }; + if (peeked?.error === "use_dpop_nonce") isNonceChallenge = true; + } catch { + // non-JSON body — keep isNonceChallenge as-is + } + } + if (isNonceChallenge) res = await call(); + } + return res; +} + +export function isFlowCallback(): boolean { + return location.pathname === CALLBACK_PATH; +} + +async function persistState(state: PersistedState) { + sessionStorage.setItem(STATE_KEY, JSON.stringify(state)); +} + +function loadPersistedState(): PersistedState | null { + try { + const raw = sessionStorage.getItem(STATE_KEY); + if (!raw) return null; + return JSON.parse(raw) as PersistedState; + } catch { + return null; + } +} + +function clearPersistedState() { + sessionStorage.removeItem(STATE_KEY); +} + +async function persistDpopKey(keyPair: CryptoKeyPair) { + await idbSet(DPOP_KEY, keyPair); +} + +async function loadDpopKey(): Promise { + try { + const kp = (await idbGet(DPOP_KEY)) as CryptoKeyPair | undefined; + return kp ?? null; + } catch { + return null; + } +} + +async function clearDpopKey() { + try { + await idbDel(DPOP_KEY); + } catch { + // best-effort + } +} + +function originOf(url: string): string { + try { + return new URL(url).origin; + } catch { + return url.replace(/\/+$/, ""); + } +} + +async function generateDpopKeyPair(): Promise { + return (await crypto.subtle.generateKey( + { name: "ECDSA", namedCurve: "P-256" }, + false, + ["sign", "verify"], + )) as CryptoKeyPair; +} + +const PRE_REDIRECT_STEPS = [ + "flow.resolve-target", + "flow.discover-protected-resource", + "flow.discover-auth-server", + "flow.validate-auth-server-metadata", + "flow.atproto-conformance", + "flow.select-scope", + "flow.generate-pkce", + "flow.generate-dpop-key", + "flow.send-par", + "flow.par-response-shape", + "flow.par-rejects-unregistered-redirect-uri", + "flow.par-rejects-invalid-include", + "flow.par-accepts-advertised-include", + "flow.par-accepts-known-permission-set", + "flow.build-authorization-url", +] as const; + +const POST_CALLBACK_STEPS = [ + "flow.callback-params-present", + "flow.iss-matches", + "flow.state-matches", + "flow.exchange-code", + "flow.token-response-shape", + "flow.scope-echoed", + "flow.use-access-token", + "flow.session-did-matches", + "flow.boundary-write-in-scope", + "flow.boundary-write-out-of-scope", + "flow.boundary-cleanup", + "flow.refresh-token", + "flow.use-refreshed-token", + "flow.revoke-token", + "flow.revoked-token-rejected", +] as const; + +function initialStepsFor(ids: readonly string[]): FlowStep[] { + const labels: Record = { + "flow.resolve-target": "Resolve handle to DID and PDS", + "flow.discover-protected-resource": + "Discover .well-known/oauth-protected-resource", + "flow.discover-auth-server": + "Discover .well-known/oauth-authorization-server", + "flow.validate-auth-server-metadata": + "Auth server metadata validates (oauth4webapi)", + "flow.atproto-conformance": "AT Proto OAuth conformance", + "flow.select-scope": + "Select scope (granular when AS advertises, legacy otherwise)", + "flow.generate-pkce": "Generate PKCE code verifier and challenge", + "flow.generate-dpop-key": "Generate DPoP ES256 keypair", + "flow.send-par": "Send pushed authorization request", + "flow.par-response-shape": "PAR response contains request_uri + expires_in", + "flow.par-rejects-unregistered-redirect-uri": + "PAR rejects unregistered redirect_uri (RFC 6749 §3.1.2.4)", + "flow.par-rejects-invalid-include": + "PAR rejects a nonexistent permission set include:", + "flow.par-accepts-advertised-include": + "PAR accepts an include: scope advertised in scopes_supported", + "flow.par-accepts-known-permission-set": + "PAR accepts include:site.standard.authFull (a published, lexicon-resolved permission set)", + "flow.build-authorization-url": "Build authorization URL", + "flow.callback-params-present": "Callback has code, state, iss", + "flow.iss-matches": "iss parameter matches auth server (RFC 9207)", + "flow.state-matches": "state matches the nonce we sent", + "flow.exchange-code": "Exchange code for tokens", + "flow.token-response-shape": + "Token response has access_token, refresh_token, token_type=DPoP", + "flow.scope-echoed": "Granted scope echoes the requested scope", + "flow.use-access-token": "Call getSession with DPoP-bound access token", + "flow.session-did-matches": "Session DID matches expected", + "flow.boundary-write-in-scope": + "In-scope createRecord (collection covered by scope) succeeds", + "flow.boundary-write-out-of-scope": + "Out-of-scope createRecord is rejected with insufficient_scope", + "flow.boundary-cleanup": "Clean up any records left from boundary tests", + "flow.refresh-token": "Refresh access token", + "flow.use-refreshed-token": "Call getSession with refreshed token", + "flow.revoke-token": "Revoke access token", + "flow.revoked-token-rejected": "Revoked token is rejected", + }; + return ids.map((id) => ({ + id, + label: labels[id] ?? id, + status: "pending" as StepStatus, + })); +} + +export function createFlowState(target: string): FlowState { + return { + phase: "pre-redirect", + target, + steps: initialStepsFor([...PRE_REDIRECT_STEPS, ...POST_CALLBACK_STEPS]), + startedAt: Date.now(), + }; +} + +// Reactive flow run + +export interface FlowRun { + state: FlowState; + cancel: () => void; + redirect: () => void; +} + +export function startPreRedirectFlow(target: string): FlowRun { + const [state, setState] = createStore(createFlowState(target)); + let aborted = false; + + const idxOf = (id: string) => state.steps.findIndex((s) => s.id === id); + const setStep = (id: string, patch: Partial) => { + const i = idxOf(id); + if (i < 0) return; + setState( + "steps", + i, + produce((s) => Object.assign(s, patch)), + ); + }; + + const runStep = async ( + id: string, + fn: () => Promise<{ + status: Exclude; + message?: string; + evidence?: unknown; + patch?: Partial; + result?: T; + }>, + ): Promise => { + if (aborted) return undefined; + setStep(id, { status: "running", startedAt: Date.now() }); + try { + const out = await fn(); + if (out.patch) + setState(produce((s) => Object.assign(s, out.patch))); + setStep(id, { + status: out.status, + message: out.message, + evidence: out.evidence, + endedAt: Date.now(), + }); + return out.result; + } catch (error) { + setStep(id, { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { error: String(error) }, + endedAt: Date.now(), + }); + throw error; + } + }; + + const haltAndSkipRest = () => { + for (let i = 0; i < state.steps.length; i++) { + if (state.steps[i]!.status === "pending") { + setState( + "steps", + i, + produce((s) => { + s.status = "skip"; + s.message = "previous step failed"; + }), + ); + } + } + setState("phase", "done"); + setState("endedAt", Date.now()); + }; + + void (async () => { + try { + // 1. Resolve target + const resolved = await runStep<{ + did: Did; + handle: Handle; + pds: string; + }>("flow.resolve-target", async () => { + const r = await actorResolver.resolve(target as ActorIdentifier); + return { + status: "pass", + message: `${r.handle} → ${r.did} @ ${r.pds}`, + evidence: r, + patch: { handle: r.handle, did: r.did, pds: originOf(r.pds) }, + result: { + did: r.did, + handle: r.handle, + pds: originOf(r.pds), + }, + }; + }); + if (!resolved) { + haltAndSkipRest(); + return; + } + + // 2. Discover protected resource + const protectedResource = await runStep>( + "flow.discover-protected-resource", + async () => { + const url = `${resolved.pds}/.well-known/oauth-protected-resource`; + const res = await fetch(url, { + headers: { accept: "application/json" }, + }); + if (!res.ok) + return { + status: "fail", + message: `HTTP ${res.status}`, + evidence: { request: { method: "GET", url }, response: { status: res.status } }, + }; + const body = (await res.json()) as Record; + const servers = body.authorization_servers; + if (!Array.isArray(servers) || servers.length === 0) { + return { + status: "fail", + message: "authorization_servers is empty or missing", + evidence: { actual: body }, + }; + } + return { + status: "pass", + message: `auth server: ${servers[0] as string}`, + evidence: { response: { status: res.status, body } }, + patch: { + protectedResource: body, + authServerUrl: servers[0] as string, + }, + result: body, + }; + }, + ); + if (!protectedResource) { + haltAndSkipRest(); + return; + } + + // 3. Discover auth server + const authServer = await runStep( + "flow.discover-auth-server", + async () => { + const issuerUrl = new URL(state.authServerUrl!); + const res = await oauth.discoveryRequest(issuerUrl, { + algorithm: "oauth2", + }); + if (!res.ok) + return { + status: "fail", + message: `HTTP ${res.status}`, + evidence: { response: { status: res.status } }, + }; + const body = (await res.clone().json()) as Record; + return { + status: "pass", + message: `issuer: ${body.issuer as string}`, + evidence: { response: { body } }, + patch: { authServer: body as oauth.AuthorizationServer }, + result: body as oauth.AuthorizationServer, + }; + }, + ); + if (!authServer) { + haltAndSkipRest(); + return; + } + + // 4. Validate auth server metadata via oauth4webapi + await runStep("flow.validate-auth-server-metadata", async () => { + const issuerUrl = new URL(state.authServerUrl!); + const res = await oauth.discoveryRequest(issuerUrl, { + algorithm: "oauth2", + }); + try { + const validated = await oauth.processDiscoveryResponse( + issuerUrl, + res, + ); + return { + status: "pass", + message: "oauth4webapi accepts the metadata", + evidence: { response: { body: validated } }, + patch: { authServer: validated }, + }; + } catch (error) { + return { + status: "fail", + message: + error instanceof Error ? error.message : String(error), + evidence: { error: String(error) }, + }; + } + }); + + // 5. AT Proto-specific conformance + await runStep("flow.atproto-conformance", async () => { + const issues: string[] = []; + const as = state.authServer!; + const get = (k: string) => (as as Record)[k]; + + if (get("require_pushed_authorization_requests") !== true) + issues.push( + "require_pushed_authorization_requests must be true", + ); + if (!get("pushed_authorization_request_endpoint")) + issues.push("pushed_authorization_request_endpoint missing"); + const dpopAlgs = get("dpop_signing_alg_values_supported"); + if ( + !Array.isArray(dpopAlgs) || + !(dpopAlgs as unknown[]).includes("ES256") + ) + issues.push("dpop_signing_alg_values_supported missing ES256"); + const pkceMethods = get("code_challenge_methods_supported"); + if ( + !Array.isArray(pkceMethods) || + !(pkceMethods as unknown[]).includes("S256") + ) + issues.push("code_challenge_methods_supported missing S256"); + const scopes = get("scopes_supported"); + if ( + !Array.isArray(scopes) || + !(scopes as unknown[]).includes("atproto") + ) + issues.push("scopes_supported missing atproto"); + const authMethods = get( + "token_endpoint_auth_methods_supported", + ); + const authMethodArr = Array.isArray(authMethods) + ? (authMethods as unknown[]) + : []; + if (!authMethodArr.includes("none")) + issues.push( + "token_endpoint_auth_methods_supported missing none (required for public clients)", + ); + if (!authMethodArr.includes("private_key_jwt")) + issues.push( + "token_endpoint_auth_methods_supported missing private_key_jwt (atproto spec requires both)", + ); + if (get("client_id_metadata_document_supported") !== true) + issues.push( + "client_id_metadata_document_supported must be true", + ); + if (get("authorization_response_iss_parameter_supported") !== true) + issues.push( + "authorization_response_iss_parameter_supported must be true (RFC 9207)", + ); + + if (issues.length === 0) { + return { + status: "pass", + message: "all atproto MUSTs satisfied", + }; + } + return { + status: "fail", + message: + issues.length === 1 + ? issues[0]! + : `${issues.length} conformance issues — ${issues[0]}`, + evidence: { error: issues.join("\n") }, + }; + }); + + // 5b. Select scope based on what the AS advertises. + await runStep("flow.select-scope", async () => { + const supported = ( + (state.authServer as Record | undefined) + ?.scopes_supported as string[] | undefined + )?.filter((s) => typeof s === "string") ?? []; + const hasGranular = supported.some( + (s) => + s === "repo" || + s.startsWith("repo:") || + s.startsWith("repo "), + ); + activeScope = hasGranular ? GRANULAR_SCOPE : LEGACY_SCOPE; + if (hasGranular) { + return { + status: "pass", + message: `granular scope: ${activeScope}`, + evidence: { + actual: { + scopesSupported: supported, + selected: activeScope, + }, + }, + }; + } + return { + status: "warn", + message: `AS doesn't advertise repo:* scopes — falling back to legacy ${LEGACY_SCOPE}; granular boundary tests will skip`, + evidence: { + expected: + "scopes_supported to include at least one repo:* (Phase 2 granular) scope", + actual: { + scopesSupported: supported, + selected: activeScope, + }, + error: + "PDS doesn't support Phase 2 granular scopes — the verifier can't differentiate scope enforcement using this AS.", + }, + }; + }); + + // 6. Generate PKCE + const codeVerifier = oauth.generateRandomCodeVerifier(); + const codeChallenge = + await oauth.calculatePKCECodeChallenge(codeVerifier); + const stateNonce = oauth.generateRandomState(); + await runStep("flow.generate-pkce", async () => ({ + status: "pass", + message: "verifier + S256 challenge generated", + evidence: { + response: { + body: { + verifierLength: codeVerifier.length, + challenge: codeChallenge, + }, + }, + }, + patch: { codeVerifier, codeChallenge, stateNonce }, + })); + + // 7. Generate DPoP keypair + const dpopKeyPair = await generateDpopKeyPair(); + await persistDpopKey(dpopKeyPair); + await runStep("flow.generate-dpop-key", async () => ({ + status: "pass", + message: "ECDSA P-256 keypair, non-extractable", + evidence: { + response: { + body: { algorithm: "ES256", curve: "P-256" }, + }, + }, + })); + + // 8. Send PAR (with DPoP-nonce retry built in — RFC 9449 §8 allows the + // AS to require a nonce; the first request fails with use_dpop_nonce + // and the captured nonce is used automatically on retry.) + const parParams = { + client_id: clientId(), + redirect_uri: redirectUri(), + response_type: "code", + scope: activeScope, + code_challenge: codeChallenge, + code_challenge_method: "S256", + state: stateNonce, + login_hint: resolved.handle, + }; + const dpop = oauth.DPoP({ [oauth.clockSkew]: 0 }, dpopKeyPair); + const sendPAR = () => + oauth.pushedAuthorizationRequest( + state.authServer!, + { client_id: clientId() }, + oauth.None(), + parParams, + { DPoP: dpop }, + ); + + const parResponse = await runStep( + "flow.send-par", + async () => { + try { + const res = await sendPAR(); + return { + status: "pass", + message: `HTTP ${res.status}`, + evidence: { response: { status: res.status } }, + result: res, + }; + } catch (error) { + return { + status: "fail", + message: + error instanceof Error ? error.message : String(error), + evidence: { error: String(error) }, + }; + } + }, + ); + if (!parResponse) { + haltAndSkipRest(); + return; + } + + // 9. Validate PAR response. + // If the AS responded with use_dpop_nonce, the DPoP handle captured the + // nonce automatically — we just re-send PAR with the new proof and process. + let nonceRetried = false; + const requestUri = await runStep( + "flow.par-response-shape", + async () => { + const tryProcess = async (res: Response) => + await oauth.processPushedAuthorizationResponse( + state.authServer!, + { client_id: clientId() }, + res, + ); + try { + let par: oauth.PushedAuthorizationResponse; + try { + par = await tryProcess(parResponse); + } catch (error) { + if (oauth.isDPoPNonceError(error)) { + nonceRetried = true; + const retryRes = await sendPAR(); + par = await tryProcess(retryRes); + } else { + throw error; + } + } + if (!par.request_uri) { + return { + status: "fail", + message: "Response missing request_uri", + evidence: { actual: par }, + }; + } + return { + status: "pass", + message: nonceRetried + ? `request_uri expires in ${par.expires_in}s (after DPoP-nonce retry)` + : `request_uri expires in ${par.expires_in}s`, + evidence: { + response: { body: par }, + ...(nonceRetried && { + error: "AS required DPoP-nonce — retried with captured nonce", + }), + }, + patch: { requestUri: par.request_uri }, + result: par.request_uri, + }; + } catch (error) { + // oauth4webapi throws ResponseBodyError when the server returns + // an OAuth error body (e.g. invalid_scope, invalid_request). + // Surface the actual error code + description so the user/agent + // sees what the PDS rejected. + if (error instanceof oauth.ResponseBodyError) { + const desc = (error.cause?.error_description as string) ?? ""; + return { + status: "fail", + message: `${error.error}: ${desc}`.trim().replace(/:$/, ""), + evidence: { + response: { + status: error.status, + body: error.cause, + }, + error: `OAuth error: ${error.error}${desc ? ` — ${desc}` : ""}`, + }, + }; + } + return { + status: "fail", + message: + error instanceof Error ? error.message : String(error), + evidence: { error: String(error) }, + }; + } + }, + ); + if (!requestUri) { + haltAndSkipRest(); + return; + } + + // 9b. Security probe: PAR must reject redirect_uri values that aren't + // registered in the client metadata. RFC 6749 §3.1.2.4 / §10.6 — failing + // this is an open-redirect / code-exfiltration vulnerability. + await runStep( + "flow.par-rejects-unregistered-redirect-uri", + async () => { + const evilRedirectUri = + "https://pdscheck-probe.invalid/unregistered-redirect"; + const probeParams = { + client_id: clientId(), + redirect_uri: evilRedirectUri, + response_type: "code", + scope: activeScope, + code_challenge: codeChallenge, + code_challenge_method: "S256", + state: oauth.generateRandomState(), + }; + const attempt = async () => { + const res = await oauth.pushedAuthorizationRequest( + state.authServer!, + { client_id: clientId() }, + oauth.None(), + probeParams, + { DPoP: dpop }, + ); + return await oauth.processPushedAuthorizationResponse( + state.authServer!, + { client_id: clientId() }, + res, + ); + }; + try { + const accepted = await withNonceRetry(attempt); + return { + status: "fail", + message: + "AS accepted PAR with an unregistered redirect_uri — open-redirect / code-exfiltration risk (RFC 6749 §3.1.2.4)", + evidence: { + expected: + "400 invalid_request (or similar) — unregistered redirect_uri rejected", + actual: { + request_uri_issued: accepted.request_uri, + redirect_uri_probed: evilRedirectUri, + }, + error: + "AS issued a request_uri for a redirect not declared in the client metadata. This lets an attacker craft an authorization URL that completes at a different domain.", + }, + }; + } catch (error) { + if (error instanceof oauth.ResponseBodyError) { + return { + status: "pass", + message: `correctly rejected: ${error.error}${error.cause?.error_description ? ` — ${error.cause.error_description}` : ""}`, + evidence: { + response: { + status: error.status, + body: error.cause, + }, + }, + }; + } + // Network error or unexpected throw — surface but don't fail + return { + status: "warn", + message: `probe inconclusive: ${error instanceof Error ? error.message : String(error)}`, + evidence: { error: String(error) }, + }; + } + }, + ); + + // 9c. Permission-set probes: only meaningful if the AS advertises any + // `include:*` scope in scopes_supported. Skip otherwise. + const advertisedScopes = ( + (state.authServer as Record | undefined) + ?.scopes_supported as string[] | undefined + )?.filter((s) => typeof s === "string") ?? []; + const advertisedIncludes = advertisedScopes.filter((s) => + s.startsWith("include:"), + ); + + // 9c.i — request a clearly-nonexistent permission set. The AS should + // reject with invalid_scope (or similar) once it tries to resolve. + await runStep("flow.par-rejects-invalid-include", async () => { + if (advertisedIncludes.length === 0) { + return { + status: "skip", + message: + "AS doesn't advertise any include:* scopes — permission set support not exercisable", + }; + } + const bogusInclude = + "include:earth.cirrus.check.invalidnonexistentpermissionset"; + const probeParams = { + client_id: clientId(), + redirect_uri: redirectUri(), + response_type: "code", + scope: `atproto ${bogusInclude}`, + code_challenge: codeChallenge, + code_challenge_method: "S256", + state: oauth.generateRandomState(), + }; + const attempt = async () => { + const res = await oauth.pushedAuthorizationRequest( + state.authServer!, + { client_id: clientId() }, + oauth.None(), + probeParams, + { DPoP: dpop }, + ); + return await oauth.processPushedAuthorizationResponse( + state.authServer!, + { client_id: clientId() }, + res, + ); + }; + try { + const accepted = await withNonceRetry(attempt); + return { + status: "fail", + message: `AS accepted an include: pointing at a nonexistent permission set — should have rejected with invalid_scope`, + evidence: { + expected: + "invalid_scope error (the include: NSID doesn't resolve to a real permission set)", + actual: { + probed: bogusInclude, + request_uri_issued: accepted.request_uri, + }, + }, + }; + } catch (error) { + if (error instanceof oauth.ResponseBodyError) { + const isScopeError = + error.error === "invalid_scope" || + error.error === "invalid_request"; + return { + status: isScopeError ? "pass" : "warn", + message: isScopeError + ? `correctly rejected: ${error.error}${error.cause?.error_description ? ` — ${error.cause.error_description}` : ""}` + : `rejected, but with ${error.error} (expected invalid_scope)`, + evidence: { + response: { status: error.status, body: error.cause }, + }, + }; + } + return { + status: "warn", + message: `probe inconclusive: ${error instanceof Error ? error.message : String(error)}`, + evidence: { error: String(error) }, + }; + } + }); + + // 9c.ii — request the AS's OWN advertised include: scope. If the AS + // advertises it in scopes_supported, it must be able to accept and + // resolve it. Rejecting your own advertised scope is a real bug. + await runStep("flow.par-accepts-advertised-include", async () => { + if (advertisedIncludes.length === 0) { + return { + status: "skip", + message: + "AS doesn't advertise any include:* scopes — nothing to probe", + }; + } + const advertised = advertisedIncludes[0]!; + const probeParams = { + client_id: clientId(), + redirect_uri: redirectUri(), + response_type: "code", + scope: `atproto ${advertised}`, + code_challenge: codeChallenge, + code_challenge_method: "S256", + state: oauth.generateRandomState(), + }; + const attempt = async () => { + const res = await oauth.pushedAuthorizationRequest( + state.authServer!, + { client_id: clientId() }, + oauth.None(), + probeParams, + { DPoP: dpop }, + ); + return await oauth.processPushedAuthorizationResponse( + state.authServer!, + { client_id: clientId() }, + res, + ); + }; + try { + const accepted = await withNonceRetry(attempt); + return { + status: "pass", + message: `AS accepted its own advertised ${advertised} (request_uri expires in ${accepted.expires_in}s)`, + evidence: { + response: { body: accepted }, + actual: { probed: advertised }, + }, + }; + } catch (error) { + if (error instanceof oauth.ResponseBodyError) { + return { + status: "fail", + message: `AS rejected ${advertised} (its own advertised scope): ${error.error}${error.cause?.error_description ? ` — ${error.cause.error_description}` : ""}`, + evidence: { + response: { status: error.status, body: error.cause }, + error: `Permission set ${advertised} is listed in scopes_supported but PAR rejects it — the AS is advertising a scope it can't actually honor.`, + }, + }; + } + return { + status: "warn", + message: `probe inconclusive: ${error instanceof Error ? error.message : String(error)}`, + evidence: { error: String(error) }, + }; + } + }); + + // 9c.iii — request a published permission set (`site.standard.authFull`) + // that does NOT need to appear in scopes_supported. This tests whether + // the AS can dynamically resolve `include:*` NSIDs via lexicon resolution. + // An AS that only supports its own pre-advertised includes will fail here. + await runStep("flow.par-accepts-known-permission-set", async () => { + const knownInclude = "include:site.standard.authFull"; + const probeParams = { + client_id: clientId(), + redirect_uri: redirectUri(), + response_type: "code", + scope: `atproto ${knownInclude}`, + code_challenge: codeChallenge, + code_challenge_method: "S256", + state: oauth.generateRandomState(), + }; + const attempt = async () => { + const res = await oauth.pushedAuthorizationRequest( + state.authServer!, + { client_id: clientId() }, + oauth.None(), + probeParams, + { DPoP: dpop }, + ); + return await oauth.processPushedAuthorizationResponse( + state.authServer!, + { client_id: clientId() }, + res, + ); + }; + try { + const accepted = await withNonceRetry(attempt); + return { + status: "pass", + message: `AS resolved site.standard.authFull (request_uri expires in ${accepted.expires_in}s)`, + evidence: { + response: { body: accepted }, + actual: { probed: knownInclude }, + }, + }; + } catch (error) { + if (error instanceof oauth.ResponseBodyError) { + return { + status: "fail", + message: `AS rejected ${knownInclude}: ${error.error}${error.cause?.error_description ? ` — ${error.cause.error_description}` : ""}`, + evidence: { + response: { status: error.status, body: error.cause }, + expected: + "AS resolves the published lexicon and accepts the include:", + actual: error.error, + error: + "site.standard.authFull is a published permission set lexicon. Rejecting it means this AS doesn't support dynamic lexicon-based permission-set resolution.", + }, + }; + } + return { + status: "warn", + message: `probe inconclusive: ${error instanceof Error ? error.message : String(error)}`, + evidence: { error: String(error) }, + }; + } + }); + + // 10. Build authorization URL and persist state. Pause for user confirmation + // rather than redirecting immediately, so they can review the pre-redirect steps. + const authUrl = new URL(state.authServer!.authorization_endpoint!); + authUrl.searchParams.set("client_id", clientId()); + authUrl.searchParams.set("request_uri", requestUri); + await runStep("flow.build-authorization-url", async () => ({ + status: "pass", + message: "ready to redirect — review and continue", + evidence: { request: { method: "GET", url: authUrl.toString() } }, + })); + + await persistState({ + target, + handle: resolved.handle, + did: resolved.did, + pds: resolved.pds, + authServerUrl: state.authServerUrl!, + codeVerifier, + stateNonce, + expectedIss: (state.authServer!.issuer as string) ?? "", + scope: activeScope, + }); + + setState( + produce((s) => { + s.authUrl = authUrl.toString(); + s.phase = "ready-to-redirect"; + }), + ); + } catch { + // Mark any still-pending steps as skip + for (let i = 0; i < state.steps.length; i++) { + if (state.steps[i]!.status === "pending") { + setState( + "steps", + i, + produce((s) => { + s.status = "skip"; + s.message = "previous step failed"; + }), + ); + } + } + setState("phase", "done"); + setState("endedAt", Date.now()); + } + })(); + + return { + state, + cancel: () => { + aborted = true; + }, + redirect: () => { + if (!state.authUrl) return; + setState("phase", "redirecting"); + location.assign(state.authUrl); + }, + }; +} + +export interface CallbackRun { + state: FlowState; +} + +export async function runPostCallback(): Promise { + const persisted = loadPersistedState(); + const dpopKeyPair = await loadDpopKey(); + + // Restore the scope chosen during pre-redirect — used by clientId() (which + // embeds it in the loopback URL) and by boundary checks below. + if (persisted?.scope) activeScope = persisted.scope; + + const initial = createFlowState(persisted?.target ?? ""); + // Mark pre-redirect steps complete + for (const id of PRE_REDIRECT_STEPS) { + const idx = initial.steps.findIndex((s) => s.id === id); + if (idx >= 0) initial.steps[idx]!.status = "pass"; + } + initial.phase = "post-callback"; + initial.handle = persisted?.handle; + initial.did = persisted?.did; + initial.pds = persisted?.pds; + initial.authServerUrl = persisted?.authServerUrl; + + const [state, setState] = createStore(initial); + + const idxOf = (id: string) => state.steps.findIndex((s) => s.id === id); + const setStep = (id: string, patch: Partial) => { + const i = idxOf(id); + if (i < 0) return; + setState( + "steps", + i, + produce((s) => Object.assign(s, patch)), + ); + }; + const runStep = async ( + id: string, + fn: () => Promise<{ + status: Exclude; + message?: string; + evidence?: unknown; + patch?: Partial; + result?: T; + }>, + ): Promise => { + setStep(id, { status: "running", startedAt: Date.now() }); + try { + const out = await fn(); + if (out.patch) + setState(produce((s) => Object.assign(s, out.patch))); + setStep(id, { + status: out.status, + message: out.message, + evidence: out.evidence, + endedAt: Date.now(), + }); + return out.result; + } catch (error) { + setStep(id, { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { error: String(error) }, + endedAt: Date.now(), + }); + } + return undefined; + }; + + void (async () => { + if (!persisted || !dpopKeyPair) { + for (const id of POST_CALLBACK_STEPS) { + setStep(id, { + status: "skip", + message: "no persisted flow state — open a fresh flow from the landing page", + }); + } + setState("phase", "done"); + setState("endedAt", Date.now()); + return; + } + + const params = new URLSearchParams(location.search); + + // 1. Callback params present + await runStep("flow.callback-params-present", async () => { + const code = params.get("code"); + const iss = params.get("iss"); + const stateParam = params.get("state"); + const error = params.get("error"); + if (error) { + return { + status: "fail", + message: `OAuth error: ${error}: ${params.get("error_description") ?? ""}`, + evidence: { error: String(params) }, + }; + } + const missing: string[] = []; + if (!code) missing.push("code"); + if (!iss) missing.push("iss"); + if (!stateParam) missing.push("state"); + if (missing.length > 0) { + return { + status: "fail", + message: `missing: ${missing.join(", ")}`, + evidence: { actual: Object.fromEntries(params) }, + }; + } + return { + status: "pass", + message: "code, state, iss all present", + evidence: { response: { body: Object.fromEntries(params) } }, + }; + }); + + // 2. iss matches expected (RFC 9207) + await runStep("flow.iss-matches", async () => { + const iss = params.get("iss"); + if (iss === persisted.expectedIss) { + return { + status: "pass", + message: iss, + }; + } + return { + status: "fail", + message: `expected ${persisted.expectedIss}, got ${iss}`, + evidence: { expected: persisted.expectedIss, actual: iss }, + }; + }); + + // 3. state matches + await runStep("flow.state-matches", async () => { + const stateParam = params.get("state"); + if (stateParam === persisted.stateNonce) { + return { + status: "pass", + message: "state nonce matches", + }; + } + return { + status: "fail", + message: "state nonce mismatch (possible CSRF)", + evidence: { + expected: persisted.stateNonce, + actual: stateParam, + }, + }; + }); + + // Re-fetch auth server (could re-use, but simpler to refetch) + const issuerUrl = new URL(persisted.authServerUrl!); + const discoveryRes = await oauth.discoveryRequest(issuerUrl, { + algorithm: "oauth2", + }); + const authServer = await oauth.processDiscoveryResponse( + issuerUrl, + discoveryRes, + ); + setState("authServer", authServer); + + const dpop = oauth.DPoP({ [oauth.clockSkew]: 0 }, dpopKeyPair); + + // 4. Exchange code for tokens. + // oauth4webapi requires the params be obtained from validateAuthResponse() — + // that helper performs the state/iss/error checks first and returns a + // "validated" URLSearchParams that the token-exchange call will accept. + const tokenResp = await runStep( + "flow.exchange-code", + async () => { + try { + const validated = oauth.validateAuthResponse( + authServer, + { client_id: clientId() }, + params, + persisted.stateNonce, + ); + const exchange = async () => { + const res = await oauth.authorizationCodeGrantRequest( + authServer, + { client_id: clientId() }, + oauth.None(), + validated, + redirectUri(), + persisted.codeVerifier, + { DPoP: dpop }, + ); + return await oauth.processAuthorizationCodeResponse( + authServer, + { client_id: clientId() }, + res, + ); + }; + const parsed = await withNonceRetry(exchange); + return { + status: "pass", + message: `token_type=${parsed.token_type}`, + evidence: { response: { body: parsed } }, + patch: { + accessToken: parsed.access_token, + refreshToken: parsed.refresh_token, + tokenType: parsed.token_type, + }, + result: parsed, + }; + } catch (error) { + if (error instanceof oauth.ResponseBodyError) { + const desc = + (error.cause?.error_description as string) ?? ""; + return { + status: "fail", + message: `${error.error}: ${desc}` + .trim() + .replace(/:$/, ""), + evidence: { + response: { status: error.status, body: error.cause }, + error: `OAuth error: ${error.error}${desc ? ` — ${desc}` : ""}`, + }, + }; + } + return { + status: "fail", + message: + error instanceof Error ? error.message : String(error), + evidence: { error: String(error) }, + }; + } + }, + ); + if (!tokenResp) { + setState("phase", "done"); + setState("endedAt", Date.now()); + return; + } + + // 5. Validate token response shape + await runStep("flow.token-response-shape", async () => { + const issues: string[] = []; + if (!tokenResp.access_token) issues.push("access_token missing"); + if (tokenResp.token_type?.toLowerCase() !== "dpop") + issues.push( + `token_type must be "DPoP" (got ${tokenResp.token_type})`, + ); + if (!tokenResp.refresh_token) + issues.push("refresh_token missing (atproto requires)"); + if (typeof tokenResp.expires_in !== "number") + issues.push("expires_in missing or not a number"); + if (tokenResp.scope === undefined) + issues.push("scope echo missing (recommended)"); + if (issues.length === 0) + return { + status: "pass", + message: `DPoP-bound, refresh=${tokenResp.refresh_token ? "yes" : "no"}, expires_in=${tokenResp.expires_in}s`, + }; + return { + status: issues.some( + (i) => + i.includes("access_token") || + i.includes("token_type") || + i.includes("refresh_token") || + i.includes("expires_in"), + ) + ? "fail" + : "warn", + message: + issues.length === 1 ? issues[0]! : `${issues.length} issues`, + evidence: { error: issues.join("\n") }, + }; + }); + + // 5b. Scope-echoed: verify the server returned the scope we asked for. + await runStep("flow.scope-echoed", async () => { + const requested = activeScope; + const granted = tokenResp.scope ?? ""; + if (!granted) { + return { + status: "warn", + message: "token response omitted scope (RFC 6749 §5.1 OPTIONAL)", + evidence: { expected: requested, actual: null }, + }; + } + const requestedSet = new Set(requested.split(/\s+/).filter(Boolean)); + const grantedSet = new Set(granted.split(/\s+/).filter(Boolean)); + const dropped: string[] = []; + const added: string[] = []; + for (const s of requestedSet) if (!grantedSet.has(s)) dropped.push(s); + for (const s of grantedSet) if (!requestedSet.has(s)) added.push(s); + if (dropped.length === 0 && added.length === 0) { + return { status: "pass", message: granted }; + } + if (dropped.length === 0 && added.length > 0) { + // Server granted MORE than we requested — that's a real conformance bug + return { + status: "fail", + message: `server granted scopes we didn't ask for: ${added.join(", ")}`, + evidence: { + expected: requested, + actual: granted, + error: `unexpected additions: ${added.join(" ")}`, + }, + }; + } + return { + status: "warn", + message: `narrower than requested — dropped: ${dropped.join(", ")}`, + evidence: { + expected: requested, + actual: granted, + error: `dropped: ${dropped.join(" ")}\nadded: ${added.join(" ")}`, + }, + }; + }); + + // 6. Use access token: call getSession with DPoP + const sessionData = await runStep<{ + did: string; + handle: string; + }>("flow.use-access-token", async () => { + try { + const res = await protectedFetchWithNonceRetry( + tokenResp.access_token, + "GET", + new URL(`${persisted.pds}/xrpc/com.atproto.server.getSession`), + new Headers(), + null, + { DPoP: dpop }, + ); + if (!res.ok) { + const detail = await readOAuthError(res); + return { + status: "fail", + message: detail.message, + evidence: { + response: { status: res.status, body: detail.body }, + ...(detail.wwwAuthenticate && { + error: `WWW-Authenticate: ${detail.wwwAuthenticate}`, + }), + }, + }; + } + const body = (await res.json()) as { + did: string; + handle: string; + }; + return { + status: "pass", + message: `${body.handle} (${body.did})`, + evidence: { response: { status: res.status, body } }, + result: body, + }; + } catch (error) { + return { + status: "fail", + message: + error instanceof Error ? error.message : String(error), + evidence: { error: String(error) }, + }; + } + }); + + // 7. Session DID matches + if (sessionData) { + await runStep("flow.session-did-matches", async () => { + if (sessionData.did === persisted.did) { + return { status: "pass", message: sessionData.did }; + } + return { + status: "fail", + message: `expected ${persisted.did}, got ${sessionData.did}`, + evidence: { + expected: persisted.did, + actual: sessionData.did, + }, + }; + }); + } else { + setStep("flow.session-did-matches", { + status: "skip", + message: "no session response", + }); + } + + // 7b. Boundary tests: try writes inside/outside the granted scope. + // Capture narrowed locals — closures below execute later and lose TS narrowing. + const accessToken = tokenResp.access_token; + const pdsUrl = persisted.pds!; + const repoDid = persisted.did!; + const createdUris: string[] = []; + const grantedScope = tokenResp.scope ?? activeScope; + const TEST_COLLECTION = "earth.cirrus.check.testrecord"; + + async function tryCreateRecord( + collection: string, + ): Promise { + try { + return await protectedFetchWithNonceRetry( + accessToken, + "POST", + new URL(`${pdsUrl}/xrpc/com.atproto.repo.createRecord`), + new Headers({ "content-type": "application/json" }), + JSON.stringify({ + repo: repoDid, + collection, + record: { + $type: collection, + message: + "pdscheck OAuth conformance boundary test — safe to delete", + verifier: location.origin, + createdAt: new Date().toISOString(), + }, + }), + { DPoP: dpop }, + ); + } catch (error) { + return { + error: error instanceof Error ? error.message : String(error), + }; + } + } + + // In-scope write: the granted scope should permit createRecord to the test collection. + await runStep("flow.boundary-write-in-scope", async () => { + const grants = scopeGrantsWriteTo(grantedScope, TEST_COLLECTION); + if (!grants) { + return { + status: "skip", + message: `granted scope (${grantedScope}) does not grant write to ${TEST_COLLECTION}`, + }; + } + const res = await tryCreateRecord(TEST_COLLECTION); + if ("error" in res) { + return { + status: "fail", + message: res.error, + evidence: { error: res.error }, + }; + } + const body = (await res.json().catch(() => ({}))) as { + uri?: string; + error?: string; + message?: string; + }; + if (!res.ok) { + return { + status: "fail", + message: `${res.status} ${body.error ?? ""}: ${body.message ?? ""}`.trim(), + evidence: { response: { status: res.status, body } }, + }; + } + if (body.uri) createdUris.push(body.uri); + return { + status: "pass", + message: body.uri ?? "created", + evidence: { response: { status: res.status, body } }, + }; + }); + + // Out-of-scope write: createRecord to a collection NOT covered by the granted scope. + // If the granted scope is broad (transition:generic, repo:write), this is uninformative + // because the write will succeed legitimately; skip with that note. Otherwise the PDS + // should reject with insufficient_scope — anything else is a scope-enforcement bug. + await runStep("flow.boundary-write-out-of-scope", async () => { + const grantsOutOfScope = scopeGrantsWriteTo( + grantedScope, + OUT_OF_SCOPE_COLLECTION, + ); + if (grantsOutOfScope) { + return { + status: "skip", + message: `granted scope (${grantedScope}) is broad enough to write to ${OUT_OF_SCOPE_COLLECTION} — can't differentiate enforcement`, + }; + } + const res = await tryCreateRecord(OUT_OF_SCOPE_COLLECTION); + if ("error" in res) { + return { + status: "fail", + message: res.error, + evidence: { error: res.error }, + }; + } + const body = (await res.json().catch(() => ({}))) as { + uri?: string; + error?: string; + message?: string; + }; + if (res.ok) { + if (body.uri) createdUris.push(body.uri); + return { + status: "fail", + message: `write succeeded — PDS is not enforcing granular scope ${grantedScope}`, + evidence: { + expected: `insufficient_scope rejection for ${OUT_OF_SCOPE_COLLECTION}`, + actual: body, + }, + }; + } + // Accept either OAuth canonical (RFC 6750, `insufficient_scope`) OR + // the atproto-XRPC convention (`InsufficientScope`). Normalize by + // lowercasing and stripping underscores so both forms collide. + const wwwAuth = res.headers.get("www-authenticate") ?? ""; + const errorCode = String(body.error ?? "") + .toLowerCase() + .replace(/_/g, ""); + const headerSaysInsufficient = wwwAuth + .toLowerCase() + .includes("insufficient_scope"); + const bodySaysInsufficient = errorCode === "insufficientscope"; + if (bodySaysInsufficient || headerSaysInsufficient) { + const usingAtprotoStyle = + bodySaysInsufficient && body.error !== "insufficient_scope"; + return { + status: "pass", + message: usingAtprotoStyle + ? `correctly rejected (${res.status}, atproto-style ${body.error})` + : `correctly rejected with insufficient_scope (${res.status})`, + evidence: { + response: { + status: res.status, + body, + }, + }, + }; + } + return { + status: "warn", + message: `rejected, but not as insufficient_scope: ${res.status} ${body.error ?? ""}`, + evidence: { + expected: + 'body.error to normalize to "insufficientscope" (OAuth insufficient_scope or atproto InsufficientScope)', + actual: body, + }, + }; + }); + + // Cleanup any records we created during the boundary tests. + await runStep("flow.boundary-cleanup", async () => { + if (createdUris.length === 0) { + return { status: "pass", message: "nothing to clean up" }; + } + let deleted = 0; + const stragglers: string[] = []; + for (const uri of createdUris) { + const m = uri.match(/^at:\/\/[^/]+\/([^/]+)\/([^/]+)$/); + if (!m) { + stragglers.push(uri); + continue; + } + const collection = m[1]!; + const rkey = m[2]!; + try { + const res = await protectedFetchWithNonceRetry( + accessToken, + "POST", + new URL( + `${pdsUrl}/xrpc/com.atproto.repo.deleteRecord`, + ), + new Headers({ "content-type": "application/json" }), + JSON.stringify({ + repo: repoDid, + collection, + rkey, + }), + { DPoP: dpop }, + ); + if (res.ok) deleted++; + else stragglers.push(uri); + } catch { + stragglers.push(uri); + } + } + if (stragglers.length > 0) { + return { + status: "warn", + message: `${deleted} deleted, ${stragglers.length} stragglers`, + evidence: { actual: stragglers }, + }; + } + return { status: "pass", message: `${deleted} deleted` }; + }); + + // 8. Refresh token + const refreshedResp = tokenResp.refresh_token + ? await runStep( + "flow.refresh-token", + async () => { + try { + const refresh = async () => { + const res = await oauth.refreshTokenGrantRequest( + authServer, + { client_id: clientId() }, + oauth.None(), + tokenResp.refresh_token!, + { DPoP: dpop }, + ); + return await oauth.processRefreshTokenResponse( + authServer, + { client_id: clientId() }, + res, + ); + }; + const parsed = await withNonceRetry(refresh); + return { + status: "pass", + message: `new access_token issued, expires_in=${parsed.expires_in}s`, + evidence: { response: { body: parsed } }, + result: parsed, + }; + } catch (error) { + if (error instanceof oauth.ResponseBodyError) { + const desc = + (error.cause?.error_description as string) ?? ""; + return { + status: "fail", + message: `${error.error}: ${desc}` + .trim() + .replace(/:$/, ""), + evidence: { + response: { + status: error.status, + body: error.cause, + }, + error: `OAuth error: ${error.error}${desc ? ` — ${desc}` : ""}`, + }, + }; + } + return { + status: "fail", + message: + error instanceof Error + ? error.message + : String(error), + evidence: { error: String(error) }, + }; + } + }, + ) + : undefined; + + if (!tokenResp.refresh_token) { + setStep("flow.refresh-token", { + status: "skip", + message: "no refresh_token in token response", + }); + } + + // 9. Use refreshed token + const tokenForCalls = refreshedResp?.access_token ?? tokenResp.access_token; + if (refreshedResp) { + await runStep("flow.use-refreshed-token", async () => { + try { + const res = await protectedFetchWithNonceRetry( + refreshedResp.access_token, + "GET", + new URL( + `${persisted.pds}/xrpc/com.atproto.server.getSession`, + ), + new Headers(), + null, + { DPoP: dpop }, + ); + if (!res.ok) { + return { + status: "fail", + message: `HTTP ${res.status}`, + evidence: { response: { status: res.status } }, + }; + } + return { + status: "pass", + message: "refreshed token works", + }; + } catch (error) { + return { + status: "fail", + message: + error instanceof Error + ? error.message + : String(error), + evidence: { error: String(error) }, + }; + } + }); + } else { + setStep("flow.use-refreshed-token", { + status: "skip", + message: "refresh did not succeed", + }); + } + + // 10. Revoke token + const revocationEndpoint = ( + authServer as Record + ).revocation_endpoint as string | undefined; + if (revocationEndpoint) { + await runStep("flow.revoke-token", async () => { + try { + // Use oauth4webapi's revocationRequest — handles client auth + // (None() for public clients) and request shape per RFC 7009. + const res = await oauth.revocationRequest( + authServer, + { client_id: clientId() }, + oauth.None(), + tokenForCalls, + ); + await oauth.processRevocationResponse(res); + return { + status: "pass", + message: `HTTP ${res.status}`, + evidence: { response: { status: res.status } }, + }; + } catch (error) { + if (error instanceof oauth.ResponseBodyError) { + const desc = + (error.cause?.error_description as string) ?? ""; + return { + status: "fail", + message: `${error.error}: ${desc}` + .trim() + .replace(/:$/, ""), + evidence: { + response: { status: error.status, body: error.cause }, + }, + }; + } + return { + status: "fail", + message: + error instanceof Error + ? error.message + : String(error), + evidence: { error: String(error) }, + }; + } + }); + + // 11. Revoked token rejected (RFC 7009 §3: revoked tokens MUST + // be invalidated; subsequent resource requests with the revoked + // token MUST fail). Pause briefly before testing in case the AS + // has any internal propagation delay. + await new Promise((resolve) => setTimeout(resolve, 250)); + await runStep("flow.revoked-token-rejected", async () => { + try { + const res = await protectedFetchWithNonceRetry( + tokenForCalls, + "GET", + new URL( + `${persisted.pds}/xrpc/com.atproto.server.getSession`, + ), + new Headers(), + null, + { DPoP: dpop }, + ); + if (res.status === 401 || res.status === 403) { + const detail = await readOAuthError(res); + return { + status: "pass", + message: `HTTP ${res.status} — token correctly rejected (${detail.message})`, + evidence: { + response: { status: res.status, body: detail.body }, + ...(detail.wwwAuthenticate && { + error: `WWW-Authenticate: ${detail.wwwAuthenticate}`, + }), + }, + }; + } + return { + status: "fail", + message: `expected 401/403, got ${res.status} — revoked token still grants access (RFC 7009 §3)`, + evidence: { + expected: + "401 or 403 with invalid_token after revocation", + actual: { status: res.status }, + error: + "Resource server accepted a revoked token. Either revocation isn't actually invalidating the token, or the resource server isn't checking revocation status.", + }, + }; + } catch (error) { + return { + status: "fail", + message: + error instanceof Error + ? error.message + : String(error), + evidence: { error: String(error) }, + }; + } + }); + } else { + setStep("flow.revoke-token", { + status: "skip", + message: "auth server doesn't advertise revocation_endpoint", + }); + setStep("flow.revoked-token-rejected", { + status: "skip", + message: "revocation step skipped", + }); + } + + // Clean up + clearPersistedState(); + await clearDpopKey(); + setState("phase", "done"); + setState("endedAt", Date.now()); + })(); + + return { state }; +} + +export function abandonFlow() { + clearPersistedState(); + void clearDpopKey(); +} diff --git a/apps/check/src/lib/oauth.ts b/apps/check/src/lib/oauth.ts new file mode 100644 index 00000000..f016e617 --- /dev/null +++ b/apps/check/src/lib/oauth.ts @@ -0,0 +1,88 @@ +import type { ActorIdentifier, Did } from "@atcute/lexicons"; +import { + configureOAuth, + createAuthorizationUrl, + deleteStoredSession, + finalizeAuthorization, + getSession, + listStoredSessions, + OAuthUserAgent, + type Session, +} from "@atcute/oauth-browser-client"; +import { createSignal } from "solid-js"; +import { actorResolver } from "./resolvers"; + +const SCOPE = "atproto transition:generic"; +const CALLBACK_PATH = "/oauth/callback"; + +const isLoopback = + location.hostname === "localhost" || location.hostname === "127.0.0.1"; + +const REDIRECT_URI = `${location.origin}${CALLBACK_PATH}`; + +const CLIENT_ID = isLoopback + ? `http://localhost?${new URLSearchParams({ + redirect_uri: REDIRECT_URI, + scope: SCOPE, + }).toString()}` + : `${location.origin}/client-metadata.json`; + +configureOAuth({ + metadata: { client_id: CLIENT_ID, redirect_uri: REDIRECT_URI }, + identityResolver: actorResolver, +}); + +const [currentDid, setCurrentDid] = createSignal( + listStoredSessions()[0] ?? null, +); + +export const signedInDid = currentDid; + +export async function startLogin(identifier: string): Promise { + const url = await createAuthorizationUrl({ + target: { type: "account", identifier: identifier as ActorIdentifier }, + scope: SCOPE, + }); + location.assign(url.toString()); + throw new Error("redirecting"); +} + +export function isCallbackPath(pathname = location.pathname): boolean { + return pathname === CALLBACK_PATH; +} + +export async function completeCallback(): Promise { + const params = new URLSearchParams( + location.hash.startsWith("#") ? location.hash.slice(1) : location.search, + ); + const { session } = await finalizeAuthorization(params); + setCurrentDid(session.info.sub); + return session; +} + +export async function getAgent(): Promise { + const did = currentDid(); + if (!did) return null; + try { + const session = await getSession(did); + return new OAuthUserAgent(session); + } catch { + deleteStoredSession(did); + setCurrentDid(null); + return null; + } +} + +export async function signOut(): Promise { + const agent = await getAgent(); + if (agent) { + try { + await agent.signOut(); + } catch { + // best-effort + } + } + const did = currentDid(); + if (did) deleteStoredSession(did); + setCurrentDid(null); +} diff --git a/apps/check/src/lib/recent.ts b/apps/check/src/lib/recent.ts new file mode 100644 index 00000000..8e675847 --- /dev/null +++ b/apps/check/src/lib/recent.ts @@ -0,0 +1,72 @@ +import { createSignal } from "solid-js"; +import type { Run } from "../types"; + +export interface RecentRun { + target: string; + completedAt: number; + pass: number; + fail: number; + total: number; +} + +const STORAGE_KEY = "pdscheck.recent-runs"; +const MAX_ENTRIES = 8; + +function read(): RecentRun[] { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return []; + const parsed = JSON.parse(raw) as unknown; + return Array.isArray(parsed) ? (parsed as RecentRun[]) : []; + } catch { + return []; + } +} + +function write(runs: RecentRun[]): void { + try { + localStorage.setItem( + STORAGE_KEY, + JSON.stringify(runs.slice(0, MAX_ENTRIES)), + ); + } catch { + // localStorage disabled or full + } +} + +const [runs, setRuns] = createSignal(read()); + +export const recentRuns = runs; + +export function recordRun(run: Run): void { + if (!run.endedAt) return; + let pass = 0; + let fail = 0; + let applicable = 0; + for (const result of run.results) { + if (result.status === "pass") { + pass++; + applicable++; + } else if ( + result.status === "fail" || + result.status === "error" || + result.status === "warn" + ) { + applicable++; + } + if (result.status === "fail" || result.status === "error") fail++; + } + const entry: RecentRun = { + target: run.target, + completedAt: run.endedAt, + pass, + fail, + total: applicable, + }; + const next = [entry, ...runs().filter((r) => r.target !== entry.target)].slice( + 0, + MAX_ENTRIES, + ); + setRuns(next); + write(next); +} diff --git a/apps/check/src/lib/resolvers.ts b/apps/check/src/lib/resolvers.ts new file mode 100644 index 00000000..c37364bb --- /dev/null +++ b/apps/check/src/lib/resolvers.ts @@ -0,0 +1,31 @@ +import { + CompositeDidDocumentResolver, + CompositeHandleResolver, + DohJsonHandleResolver, + LocalActorResolver, + PlcDidDocumentResolver, + WebDidDocumentResolver, + WellKnownHandleResolver, +} from "@atcute/identity-resolver"; + +export const handleResolver = new CompositeHandleResolver({ + strategy: "race", + methods: { + dns: new DohJsonHandleResolver({ + dohUrl: "https://mozilla.cloudflare-dns.com/dns-query", + }), + http: new WellKnownHandleResolver(), + }, +}); + +export const didDocResolver = new CompositeDidDocumentResolver({ + methods: { + plc: new PlcDidDocumentResolver({ apiUrl: "https://plc.directory" }), + web: new WebDidDocumentResolver(), + }, +}); + +export const actorResolver = new LocalActorResolver({ + handleResolver, + didDocumentResolver: didDocResolver, +}); diff --git a/apps/check/src/lib/spec-urls.ts b/apps/check/src/lib/spec-urls.ts new file mode 100644 index 00000000..534d036d --- /dev/null +++ b/apps/check/src/lib/spec-urls.ts @@ -0,0 +1,141 @@ +/** + * Spec URLs per check ID. Links to the most authoritative source — usually a + * lexicon JSON for XRPC endpoint checks, or a section of atproto.com/specs + * (or the relevant RFC) for behavioral checks. `.validates` siblings inherit + * the parent's URL. + */ + +const LEX = "https://github.com/bluesky-social/atproto/blob/main/lexicons"; +const SPECS = "https://atproto.com/specs"; +const RFC = "https://www.rfc-editor.org/rfc"; + +const MAP: Record = { + // identity + "identity.parse-input": `${SPECS}/handle`, + "identity.resolve-handle": `${SPECS}/handle#handle-resolution`, + "identity.fetch-did-document": `${SPECS}/did`, + "identity.extract-pds": `${SPECS}/account`, + "identity.pds-resolve-handle": `${LEX}/com/atproto/identity/resolveHandle.json`, + "identity.pds-resolve-did": `${LEX}/com/atproto/identity/resolveDid.json`, + "identity.pds-resolve-identity": `${LEX}/com/atproto/identity/resolveIdentity.json`, + + // server + "server.health": + "https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/index.ts", + "server.describe-server": `${LEX}/com/atproto/server/describeServer.json`, + "server.list-repos": `${LEX}/com/atproto/sync/listRepos.json`, + + // repo-read + "repo-read.describe-repo": `${LEX}/com/atproto/repo/describeRepo.json`, + "repo-read.list-collections": `${LEX}/com/atproto/repo/describeRepo.json`, + "repo-read.list-records": `${LEX}/com/atproto/repo/listRecords.json`, + "repo-read.get-record": `${LEX}/com/atproto/repo/getRecord.json`, + "repo-read.list-records-cursor": `${LEX}/com/atproto/repo/listRecords.json`, + "repo-read.get-repo-car": `${LEX}/com/atproto/sync/getRepo.json`, + "repo-read.get-repo-car.validates": "https://ipld.io/specs/transport/car/carv1/", + + // sync + "sync.get-latest-commit": `${LEX}/com/atproto/sync/getLatestCommit.json`, + "sync.get-repo-status": `${LEX}/com/atproto/sync/getRepoStatus.json`, + "sync.get-blocks": `${LEX}/com/atproto/sync/getBlocks.json`, + "sync.list-repos-by-collection": `${LEX}/com/atproto/sync/listReposByCollection.json`, + + // blobs + "blobs.list-blobs": `${LEX}/com/atproto/sync/listBlobs.json`, + "blobs.get-blob": `${LEX}/com/atproto/sync/getBlob.json`, + + // firehose + "firehose.connect": `${SPECS}/sync`, + "firehose.collect-frames": `${SPECS}/sync`, + "firehose.frame-decodes": `${SPECS}/sync`, + "firehose.commit-has-prevdata": `${LEX}/com/atproto/sync/subscribeRepos.json`, + "firehose.commit-blocks-is-car": `${LEX}/com/atproto/sync/subscribeRepos.json`, + "firehose.commit-ops-have-prev": `${LEX}/com/atproto/sync/subscribeRepos.json`, + "firehose.commit-deprecated-toobig": `${LEX}/com/atproto/sync/subscribeRepos.json`, + "firehose.commit-deprecated-blobs": `${LEX}/com/atproto/sync/subscribeRepos.json`, + "firehose.commit-deprecated-rebase": `${LEX}/com/atproto/sync/subscribeRepos.json`, + "firehose.emits-sync-events": `${LEX}/com/atproto/sync/subscribeRepos.json`, + "firehose.emits-account-events": `${LEX}/com/atproto/sync/subscribeRepos.json`, + "firehose.account-event-shape": `${LEX}/com/atproto/sync/subscribeRepos.json`, + "firehose.emits-identity-events": `${LEX}/com/atproto/sync/subscribeRepos.json`, + + // oauth discovery + "oauth.protected-resource-responds": `${RFC}/rfc9728`, + "oauth.protected-resource-validates": `${RFC}/rfc9728`, + "oauth.auth-server-responds": `${RFC}/rfc8414`, + "oauth.auth-server-validates": `${RFC}/rfc8414`, + "oauth.jwks-responds": `${RFC}/rfc7517`, + "oauth.jwks-validates": `${RFC}/rfc7517`, + "oauth-discovery.scope-atproto": `${SPECS}/oauth#scopes`, + "oauth-discovery.scope-transition-generic": `${SPECS}/oauth#scopes`, + "oauth-discovery.scope-phase2-granular": + "https://github.com/bluesky-social/atproto/discussions/4013", + "oauth-discovery.scope-resource-buckets": + "https://github.com/bluesky-social/atproto/discussions/4013", + "oauth-discovery.scope-permission-sets": + "https://github.com/bluesky-social/atproto/discussions/4013", + + // account + "account.get-session": `${LEX}/com/atproto/server/getSession.json`, + "account.check-account-status": `${LEX}/com/atproto/server/checkAccountStatus.json`, + "account.list-app-passwords": `${LEX}/com/atproto/server/listAppPasswords.json`, + "account.get-account-invite-codes": `${LEX}/com/atproto/server/getAccountInviteCodes.json`, + "account.get-service-auth": `${LEX}/com/atproto/server/getServiceAuth.json`, + "account.get-recommended-did-credentials": `${LEX}/com/atproto/identity/getRecommendedDidCredentials.json`, + + // repo-write + "repo-write.create-record": `${LEX}/com/atproto/repo/createRecord.json`, + "repo-write.get-created-record": `${LEX}/com/atproto/repo/getRecord.json`, + "repo-write.list-includes-created": `${LEX}/com/atproto/repo/listRecords.json`, + "repo-write.apply-writes": `${LEX}/com/atproto/repo/applyWrites.json`, + "repo-write.delete-record": `${LEX}/com/atproto/repo/deleteRecord.json`, + "repo-write.deleted-record-404": `${LEX}/com/atproto/repo/listRecords.json`, + "repo-write.upload-blob": `${LEX}/com/atproto/repo/uploadBlob.json`, + "repo-write.reference-blob-in-record": `${LEX}/com/atproto/repo/createRecord.json`, + "repo-write.cleanup": `${LEX}/com/atproto/repo/deleteRecord.json`, + + // OAuth flow steps + "flow.resolve-target": `${SPECS}/handle#handle-resolution`, + "flow.discover-protected-resource": `${RFC}/rfc9728`, + "flow.discover-auth-server": `${RFC}/rfc8414`, + "flow.validate-auth-server-metadata": `${RFC}/rfc8414`, + "flow.atproto-conformance": `${SPECS}/oauth`, + "flow.select-scope": `${SPECS}/oauth#scopes`, + "flow.generate-pkce": `${RFC}/rfc7636`, + "flow.generate-dpop-key": `${RFC}/rfc9449`, + "flow.send-par": `${RFC}/rfc9126`, + "flow.par-response-shape": `${RFC}/rfc9126`, + "flow.par-rejects-unregistered-redirect-uri": `${RFC}/rfc6749#section-3.1.2.4`, + "flow.par-rejects-invalid-include": + "https://github.com/bluesky-social/atproto/discussions/4013", + "flow.par-accepts-advertised-include": + "https://github.com/bluesky-social/atproto/discussions/4013", + "flow.par-accepts-known-permission-set": + "https://github.com/bluesky-social/atproto/discussions/4013", + "flow.build-authorization-url": `${RFC}/rfc6749#section-4.1`, + "flow.callback-params-present": `${RFC}/rfc6749#section-4.1.2`, + "flow.iss-matches": `${RFC}/rfc9207`, + "flow.state-matches": `${RFC}/rfc6749#section-10.12`, + "flow.exchange-code": `${RFC}/rfc6749#section-4.1.3`, + "flow.token-response-shape": `${RFC}/rfc6749#section-5.1`, + "flow.scope-echoed": `${RFC}/rfc6749#section-3.3`, + "flow.use-access-token": `${RFC}/rfc9449#section-7`, + "flow.session-did-matches": `${LEX}/com/atproto/server/getSession.json`, + "flow.boundary-write-in-scope": `${SPECS}/oauth#scopes`, + "flow.boundary-write-out-of-scope": `${RFC}/rfc6750#section-3.1`, + "flow.boundary-cleanup": `${LEX}/com/atproto/repo/deleteRecord.json`, + "flow.refresh-token": `${RFC}/rfc6749#section-6`, + "flow.use-refreshed-token": `${RFC}/rfc9449#section-7`, + "flow.revoke-token": `${RFC}/rfc7009`, + "flow.revoked-token-rejected": `${RFC}/rfc7009#section-2.2`, +}; + +export function specUrlFor(checkId: string): string | undefined { + if (MAP[checkId]) return MAP[checkId]; + // `.validates` siblings inherit from their parent + if (checkId.endsWith(".validates")) { + const parent = checkId.slice(0, -".validates".length); + if (MAP[parent]) return MAP[parent]; + } + return undefined; +} diff --git a/apps/check/src/lib/xrpc.ts b/apps/check/src/lib/xrpc.ts new file mode 100644 index 00000000..2fa9abc3 --- /dev/null +++ b/apps/check/src/lib/xrpc.ts @@ -0,0 +1,82 @@ +import "@atcute/atproto"; +import { Client, simpleFetchHandler } from "@atcute/client"; +import { + safeParse, + type BaseSchema, + type Issue, +} from "@atcute/lexicons/validations"; +import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; +import type { CheckOutcome } from "../types"; + +export function publicClient(pds: string): Client { + return new Client({ handler: simpleFetchHandler({ service: pds }) }); +} + +export function authedClient(agent: OAuthUserAgent): Client { + return new Client({ handler: agent }); +} + +function formatPath(path: readonly (string | number)[]): string { + if (path.length === 0) return "(root)"; + let out = ""; + for (const seg of path) { + if (typeof seg === "number") out += `[${seg}]`; + else out += out ? `.${seg}` : seg; + } + return out; +} + +function summarizeIssue(issue: Issue): string { + const path = formatPath(issue.path); + switch (issue.code) { + case "missing_value": + return `${path}: required field missing`; + case "invalid_type": + return `${path}: expected ${issue.expected}`; + case "invalid_literal": + return `${path}: expected one of ${issue.expected.join(", ")}`; + case "invalid_variant": + return `${path}: expected variant ${issue.expected.join(" | ")}`; + case "invalid_string_format": + return `${path}: not a valid ${issue.expected}`; + case "invalid_string_length": + return `${path}: string length must be ${issue.minLength}..${issue.maxLength}`; + case "invalid_string_graphemes": + return `${path}: grapheme count must be ${issue.minGraphemes}..${issue.maxGraphemes}`; + case "invalid_array_length": + return `${path}: array length must be ${issue.minLength}..${issue.maxLength}`; + case "invalid_integer_range": + return `${path}: must be in [${issue.min}, ${issue.max}]`; + case "invalid_bytes_size": + return `${path}: byte size must be ${issue.minSize}..${issue.maxSize}`; + case "invalid_blob_size": + return `${path}: blob size must be ≤ ${issue.maxSize}`; + case "invalid_blob_mime_type": + return `${path}: mime type must be one of ${issue.accept.join(", ")}`; + } +} + +export function validateLexicon( + schema: BaseSchema, + data: unknown, +): CheckOutcome { + const result = safeParse(schema, data); + if (result.ok) { + return { + status: "pass", + message: "response matches lexicon", + }; + } + const summaries = result.issues.map(summarizeIssue); + return { + status: "fail", + message: + summaries.length === 1 + ? summaries[0]! + : `${summaries.length} validation issues — ${summaries[0]}`, + evidence: { + actual: data, + error: summaries.join("\n"), + }, + }; +} diff --git a/apps/check/src/main.tsx b/apps/check/src/main.tsx new file mode 100644 index 00000000..c11df769 --- /dev/null +++ b/apps/check/src/main.tsx @@ -0,0 +1,7 @@ +import { render } from "solid-js/web"; +import { App } from "./App"; +import "./app.css"; + +const root = document.getElementById("root"); +if (!root) throw new Error("missing #root"); +render(() => , root); diff --git a/apps/check/src/runner.ts b/apps/check/src/runner.ts new file mode 100644 index 00000000..122ec7bd --- /dev/null +++ b/apps/check/src/runner.ts @@ -0,0 +1,105 @@ +import { createStore, produce } from "solid-js/store"; +import type { + Check, + CheckContext, + CheckResult, + CheckStatus, + Run, +} from "./types"; + +export interface RunStore extends Run { + results: readonly CheckResult[]; +} + +export function startRun( + target: string, + checks: readonly Check[], + initial?: Partial, +): RunStore { + const [store, setStore] = createStore<{ + target: string; + startedAt: number; + endedAt?: number; + results: CheckResult[]; + }>({ + target, + startedAt: Date.now(), + results: checks.map((check) => ({ check, status: "pending" })), + }); + + let aborted = false; + const ctx: CheckContext = { target, signedIn: false, ...initial }; + + const setResult = (i: number, patch: Partial) => + setStore( + "results", + i, + produce((result) => Object.assign(result, patch)), + ); + + void (async () => { + for (let i = 0; i < checks.length; i++) { + if (aborted) return; + const check = checks[i]!; + + if (!hasRequirements(check, ctx)) { + setResult(i, { status: "skip", startedAt: Date.now(), endedAt: Date.now() }); + continue; + } + + setResult(i, { status: "running", startedAt: Date.now() }); + try { + const outcome = await check.run(ctx); + if (outcome.context) Object.assign(ctx, outcome.context); + setResult(i, { + status: outcomeToStatus(outcome.status), + outcome, + endedAt: Date.now(), + }); + } catch (error) { + setResult(i, { + status: "error", + outcome: { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { error: String(error) }, + }, + endedAt: Date.now(), + }); + } + } + setStore("endedAt", Date.now()); + })(); + + return { + get target() { + return store.target; + }, + get startedAt() { + return store.startedAt; + }, + get endedAt() { + return store.endedAt; + }, + get results() { + return store.results; + }, + cancel() { + aborted = true; + }, + } satisfies Run; +} + +function outcomeToStatus(s: "pass" | "fail" | "warn" | "skip"): CheckStatus { + return s; +} + +function hasRequirements(check: Check, ctx: CheckContext): boolean { + if (!check.requires) return true; + for (const req of check.requires) { + if (req === "did" && !ctx.did) return false; + if (req === "pds" && !ctx.pds) return false; + if (req === "session" && !ctx.signedIn) return false; + } + return true; +} diff --git a/apps/check/src/types.ts b/apps/check/src/types.ts new file mode 100644 index 00000000..28bd5aed --- /dev/null +++ b/apps/check/src/types.ts @@ -0,0 +1,100 @@ +export type CheckStatus = + | "pending" + | "running" + | "pass" + | "fail" + | "warn" + | "skip" + | "error"; + +export type CheckCategory = + | "identity" + | "server" + | "repo-read" + | "sync" + | "blobs" + | "firehose" + | "oauth" + | "account" + | "repo-write"; + +export type CheckRequirement = "did" | "pds" | "session"; + +export interface CheckEvidence { + expected?: unknown; + actual?: unknown; + request?: { method: string; url: string }; + response?: { status?: number; body?: unknown }; + error?: string; +} + +export interface CheckOutcome { + status: "pass" | "fail" | "warn" | "skip"; + message?: string; + evidence?: CheckEvidence; + context?: Partial; +} + +export interface CheckContext { + target: string; + handle?: string; + did?: string; + didDoc?: import("@atcute/identity").DidDocument; + pds?: string; + signedIn: boolean; + agent?: import("@atcute/oauth-browser-client").OAuthUserAgent; + /** + * When true, opt-in for the full repository CAR download check. + * Off by default because repos can be hundreds of MB. + */ + downloadCar?: boolean; +} + +export interface Check { + id: string; + category: CheckCategory; + label: string; + description?: string; + requires?: CheckRequirement[]; + run: (ctx: CheckContext) => Promise; +} + +export interface CheckResult { + check: Check; + status: CheckStatus; + outcome?: CheckOutcome; + startedAt?: number; + endedAt?: number; +} + +export interface Run { + target: string; + startedAt: number; + endedAt?: number; + results: readonly CheckResult[]; + cancel: () => void; +} + +export const CATEGORY_LABELS: Record = { + identity: "Identity", + server: "Server", + "repo-read": "Repo Read", + sync: "Sync", + blobs: "Blobs", + firehose: "Firehose", + oauth: "OAuth", + account: "Account", + "repo-write": "Repo Write", +}; + +export const CATEGORY_ORDER: CheckCategory[] = [ + "identity", + "server", + "repo-read", + "sync", + "blobs", + "firehose", + "oauth", + "account", + "repo-write", +]; diff --git a/apps/check/src/vite-env.d.ts b/apps/check/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/apps/check/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/check/tsconfig.json b/apps/check/tsconfig.json new file mode 100644 index 00000000..6b25da93 --- /dev/null +++ b/apps/check/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "moduleResolution": "bundler", + "lib": ["es2022", "dom", "dom.iterable"], + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/apps/check/vite.config.ts b/apps/check/vite.config.ts new file mode 100644 index 00000000..a1831291 --- /dev/null +++ b/apps/check/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import solid from "vite-plugin-solid"; +import tailwind from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [solid(), tailwind()], + server: { + host: "127.0.0.1", + }, +}); diff --git a/apps/check/wrangler.jsonc b/apps/check/wrangler.jsonc new file mode 100644 index 00000000..c69db45f --- /dev/null +++ b/apps/check/wrangler.jsonc @@ -0,0 +1,18 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "pdscheck", + "compatibility_date": "2026-05-24", + "assets": { + "directory": "./dist", + "not_found_handling": "single-page-application" + }, + "routes": [ + { + "pattern": "check.cirrus.earth", + "custom_domain": true + } + ], + "observability": { + "enabled": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ce08093..e0bf5ec5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,7 +32,86 @@ importers: version: 5.9.3 vitest: specifier: 4.1.0-beta.1 - version: 4.1.0-beta.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0) + version: 4.1.0-beta.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) + + apps/check: + dependencies: + '@atcute/atproto': + specifier: ^4.0.0 + version: 4.0.0(@atcute/lexicons@2.0.0) + '@atcute/bluesky': + specifier: ^4.0.3 + version: 4.0.3(@atcute/lexicons@2.0.0) + '@atcute/cbor': + specifier: ^2.3.3 + version: 2.3.3(@atcute/cid@2.4.1) + '@atcute/cid': + specifier: ^2.4.1 + version: 2.4.1 + '@atcute/client': + specifier: ^5.0.0 + version: 5.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3) + '@atcute/identity': + specifier: ^2.0.0 + version: 2.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3) + '@atcute/identity-resolver': + specifier: ^2.0.0 + version: 2.0.0(@atcute/identity@2.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3))(@atcute/lexicons@2.0.0)(typescript@6.0.3) + '@atcute/lexicons': + specifier: ^2.0.0 + version: 2.0.0 + '@atcute/oauth-browser-client': + specifier: ^4.0.0 + version: 4.0.0(@atcute/identity-resolver@2.0.0(@atcute/identity@2.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3))(@atcute/lexicons@2.0.0)(typescript@6.0.3))(@atcute/lexicons@2.0.0)(typescript@6.0.3) + '@atcute/tid': + specifier: ^1.1.2 + version: 1.1.2 + '@ipld/car': + specifier: ^5.4.6 + version: 5.4.6 + '@kobalte/core': + specifier: ^0.13.11 + version: 0.13.11(solid-js@1.9.13) + '@solid-primitives/storage': + specifier: ^4.3.4 + version: 4.3.4(solid-js@1.9.13) + idb-keyval: + specifier: ^6.2.4 + version: 6.2.4 + jose: + specifier: ^6.2.3 + version: 6.2.3 + multiformats: + specifier: ^14.0.0 + version: 14.0.0 + oauth4webapi: + specifier: ^3.8.6 + version: 3.8.6 + solid-js: + specifier: ^1.9.13 + version: 1.9.13 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.3.0 + version: 4.3.0(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)) + tailwindcss: + specifier: ^4.3.0 + version: 4.3.0 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.0.14 + version: 8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0) + vite-plugin-solid: + specifier: ^2.11.12 + version: 2.11.12(solid-js@1.9.13)(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)) + vitest: + specifier: 4.1.7 + version: 4.1.7(@types/node@24.10.11)(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)) + wrangler: + specifier: ^4.63.0 + version: 4.94.0(@cloudflare/workers-types@4.20260207.0) demos/pds: dependencies: @@ -42,10 +121,10 @@ importers: devDependencies: '@cloudflare/vite-plugin': specifier: ^1.17.0 - version: 1.23.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0))(workerd@1.20260205.0)(wrangler@4.63.0(@cloudflare/workers-types@4.20260207.0)) + version: 1.23.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0))(workerd@1.20260521.1)(wrangler@4.63.0(@cloudflare/workers-types@4.20260207.0)) vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0) + version: 6.4.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) wrangler: specifier: ^4.63.0 version: 4.63.0(@cloudflare/workers-types@4.20260207.0) @@ -115,7 +194,7 @@ importers: version: 5.9.3 vitest: specifier: 4.1.0-beta.1 - version: 4.1.0-beta.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0) + version: 4.1.0-beta.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) packages/pds: dependencies: @@ -197,10 +276,10 @@ importers: version: 0.18.20 '@cloudflare/vite-plugin': specifier: ^1.17.0 - version: 1.23.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0))(workerd@1.20260205.0)(wrangler@4.63.0(@cloudflare/workers-types@4.20260207.0)) + version: 1.23.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0))(workerd@1.20260205.0)(wrangler@4.63.0(@cloudflare/workers-types@4.20260207.0)) '@cloudflare/vitest-pool-workers': specifier: https://pkg.pr.new/@cloudflare/vitest-pool-workers@79a1932669f44a24342c1ffd66c29decd8a8fee9 - version: https://pkg.pr.new/@cloudflare/vitest-pool-workers@79a1932669f44a24342c1ffd66c29decd8a8fee9(@cloudflare/workers-types@4.20260207.0)(@vitest/runner@4.1.0-beta.1)(@vitest/snapshot@4.1.0-beta.1)(vitest@4.1.0-beta.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0)) + version: https://pkg.pr.new/@cloudflare/vitest-pool-workers@79a1932669f44a24342c1ffd66c29decd8a8fee9(@cloudflare/workers-types@4.20260207.0)(@vitest/runner@4.1.7)(@vitest/snapshot@4.1.7)(vitest@4.1.0-beta.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)) '@cloudflare/workers-types': specifier: ^4.20251225.0 version: 4.20260207.0 @@ -227,10 +306,10 @@ importers: version: 5.9.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0) + version: 6.4.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) vitest: specifier: 4.1.0-beta.1 - version: 4.1.0-beta.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0) + version: 4.1.0-beta.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) wrangler: specifier: ^4.63.0 version: 4.63.0(@cloudflare/workers-types@4.20260207.0) @@ -255,17 +334,29 @@ packages: '@atcute/atproto@3.1.10': resolution: {integrity: sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==} + '@atcute/atproto@4.0.0': + resolution: {integrity: sha512-Rd21kW0tl/QEXhyPkJXRpUV8HYs7sMOmmXRu2z1sKMZbm1tqvHz1L3CQeTFcC/Xf/5cg7S6ITGIKRP0++zu0aw==} + peerDependencies: + '@atcute/lexicons': ^2.0.0 + '@atcute/bluesky@3.2.17': resolution: {integrity: sha512-Li+RsPkcRNC6AnNlqOGnlmAcjSwBdXIKFubJL1nwACDngKNXG4ooGL5cvzeekdDEfHmtFhS/tyZNaUx9QXYEUw==} + '@atcute/bluesky@4.0.3': + resolution: {integrity: sha512-82DKs9Htf9my/QCNmnxODYnK9xYn/6iHBJcy8CeSgKA3LmIQpeqdjnCjrwcchUUFRw3kbUal8RdycaTqjUs/kQ==} + peerDependencies: + '@atcute/lexicons': ^2.0.0 + '@atcute/car@5.1.1': resolution: {integrity: sha512-MeRUJNXYgAHrJZw7mMoZJb9xIqv3LZLQw90rRRAVAo8SGNdICwyqe6Bf2LGesX73QM04MBuYO6Kqhvold3TFfg==} '@atcute/cbor@2.3.0': resolution: {integrity: sha512-7G2AndkfYzIXMBOBqUPUWP6oIJJm77KY5nYzS4Mr5NNxnmnrBrXEQqp+seCE3X5TV8FUSWQK5YRTU87uPjafMQ==} - '@atcute/cbor@2.3.2': - resolution: {integrity: sha512-xP2SORSau/VVI00x2V4BjwIkHr6EQ7l/MXEOPaa4LGYtePFc4gnD4L1yN10dT5NEuUnvGEuCh6arLB7gz1smVQ==} + '@atcute/cbor@2.3.3': + resolution: {integrity: sha512-zZ4nHOK837zTMWJtta35YD7pcukrTzDc8jkpIGlSgoDYzu3l4BX3WVgpPJtRn3K6h2v97uyiWfiVjSpM7JSFzQ==} + peerDependencies: + '@atcute/cid': ^2.0.0 '@atcute/cid@2.4.0': resolution: {integrity: sha512-6+5u9MpUrgSRQ94z7vaIX4BYk8fYr2KXUBS+rrr2NhlPy8xam8nbTlmd3hvBbtpSwShbhRAE4tA5Ab7eYUp2Yw==} @@ -276,6 +367,11 @@ packages: '@atcute/client@4.2.1': resolution: {integrity: sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==} + '@atcute/client@5.0.0': + resolution: {integrity: sha512-Dtrc1no1oAtpUTmkBxH0xKSgy8qcnk8orPf/PkEATW+MB3k8FPHtPH2fshiKd55rioZkb+xaN+7A29WtqPQHRA==} + peerDependencies: + '@atcute/lexicons': ^2.0.0 + '@atcute/crypto@2.4.1': resolution: {integrity: sha512-tJ3Pi/XYcAsABKtqSlSOTKfO5YiQ4XdqlTuPS8HiRZSezOPcXBFFzAFWpSIJPURbVPFQL3LLrrK0Ea24wl5qeQ==} @@ -284,9 +380,20 @@ packages: peerDependencies: '@atcute/identity': ^1.0.0 + '@atcute/identity-resolver@2.0.0': + resolution: {integrity: sha512-IKg1BDQAF2bIdN10DL6KAXmTjK+3enTU2IRbuani9TsFahBwGZ7O5FiVmTiL6QlGfauGNW5S0xNCOxWXWMoR2Q==} + peerDependencies: + '@atcute/identity': ^2.0.0 + '@atcute/lexicons': ^2.0.0 + '@atcute/identity@1.1.4': resolution: {integrity: sha512-RCw1IqflfuSYCxK5m0lZCm0UnvIzcUnuhngiBhJEJb9a9Mc2SEf1xP3H8N5r8pvEH1LoAYd6/zrvCNU+uy9esw==} + '@atcute/identity@2.0.0': + resolution: {integrity: sha512-YXFsggO7eJYifqkN85+kUXJE2a1iI9AyuzPTDjtS/4WE1Zs1/XiPkWmwZlAgtp+pYhVtjm3mJqy/h/mZ0OnIVw==} + peerDependencies: + '@atcute/lexicons': ^2.0.0 + '@atcute/lexicon-doc@2.2.0': resolution: {integrity: sha512-6l4lDlL6KPLDGknRh6HlfGbv98haUgQ0DFaAr1yA4vA95b8YYZUZ4/370ENpiq+d6Lv0tdDAMvOon2mynrp3pQ==} @@ -302,6 +409,9 @@ packages: '@atcute/lexicons@1.3.0': resolution: {integrity: sha512-Eq5y+9onnCXNVUlNiMf31beSXHKqptB7lUo/68YbhlmxdaR7ooywHmahya9goP5AsmlYEA1z+dRPXIDAa9O7cg==} + '@atcute/lexicons@2.0.0': + resolution: {integrity: sha512-fIlwP+TPEAGoF5aU5s+f8N5sOjOu8Mww/sQL1B57Dp2hj3G/EWG9XwOHPokzycBCgXx+UxIIrzZCGy8whsVDZw==} + '@atcute/mst@1.0.0': resolution: {integrity: sha512-pMce2efib+dmKtnGnIvJZitVncJkpr3AmhyfgfYllni8KzsaDGsJmuGavSVpuojAhQe+6jYwHFtpm/beiiH4uw==} @@ -311,24 +421,51 @@ packages: '@atcute/multibase@1.2.0': resolution: {integrity: sha512-ZK2GRra+qIYq9nNuQB52m2ul0hOmCQEtPobGfTSUxm7pF0OGEkWGkWHugFhNEDVzHzTwPxHp6VGotdZFue4lYQ==} + '@atcute/oauth-browser-client@4.0.0': + resolution: {integrity: sha512-rvxmRcA6WOZz5TfcKp1kVtxUioogMzQH5OdCrW3Fm+ldEPPe2ZEyP7x1PzRWo6Es3F8lm4Q7BCJJgJFkQp2a5Q==} + peerDependencies: + '@atcute/identity-resolver': ^2.0.0 + '@atcute/lexicons': ^2.0.0 + + '@atcute/oauth-crypto@1.0.0': + resolution: {integrity: sha512-2UC1msk4PyUArk/5Pl8zgtz1T8O+LZdFfB8ENLHjQVYitpqzGj2ZpDJaWZvCF3Y8lly4KoeUHLpFPDzbP+3u+g==} + + '@atcute/oauth-keyset@0.1.1': + resolution: {integrity: sha512-BpaaXSuMawxILhWTOR0YIpKzFSA0MQC1W5Hn0HGE+giTqYFAKcdf0oA+2RZG9ZLVIzfO2txBsTeMpxB5qL6lEQ==} + + '@atcute/oauth-types@1.0.0': + resolution: {integrity: sha512-YOpjLU8H5PG6oKfgau+dx7rSmGsLxIA36MeGL7BDeopcyq80RqPSBAzOasEEsmbMRJ/nTsMRJhnmGkp3RCa/Zw==} + '@atcute/repo@0.1.4': resolution: {integrity: sha512-uzbGJkE+1A8UFviosJrtw7HW87u8nCCH1V3yOQ79FPrRhS67EvEHF6GTg4aMkP21ze/pRtttJ1k9pFfDmyTlTg==} '@atcute/tid@1.1.1': resolution: {integrity: sha512-djJ8UGhLkTU5V51yCnBEruMg35qETjWzWy5sJG/2gEOl2Gd7rQWHSaf+yrO6vMS5EFA38U2xOWE3EDUPzvc2ZQ==} + '@atcute/tid@1.1.2': + resolution: {integrity: sha512-bmPuOX/TOfcm/vsK9vM98spjkcx2wgd9S2PeK5oLgEr8IbNRPq7iMCAPzOL1nu5XAW3LlkOYQEbYRcw5vcQ37w==} + '@atcute/time-ms@1.2.0': resolution: {integrity: sha512-dtNKebVIbr1+yu3a6vgtL4sfkNgxkL3aA+ohHsjtW83WWMjjGvX8GVTVmYCJ2dYSxIoxK0q1yWs11PmlqzmQ/A==} + '@atcute/time-ms@1.3.2': + resolution: {integrity: sha512-F+qOyR9pO55g1d/QmN+Gr+fimoUQQLusdGSB6pjV0wW5KPILR4oQ4e2ZhWzqUbeHLAgWvgoTTMsMDdz62Xa2tg==} + '@atcute/uint8array@1.1.0': resolution: {integrity: sha512-JtHXIVW6LPU9FMWp7SgE4HbUs3uV2WdfkK/2RWdEGjr4EgMV50P3FdU6fPeGlTfDNBJVYMIsuD2wwaKRPV/Aqg==} '@atcute/uint8array@1.1.1': resolution: {integrity: sha512-3LsC8XB8TKe9q/5hOA5sFuzGaIFdJZJNewC5OKa3o/eU6+K7JR6see9Zy2JbQERNVnRl11EzbNov1efgLMAs4g==} + '@atcute/uint8array@1.1.2': + resolution: {integrity: sha512-n+lutnbN9mKzSjSVdfsYfzJ40u2971H+iLSL46D6d7zcrA4delxusf/ftGFvj5oGW03OioaFgQOy3Lqa3JmTeA==} + '@atcute/util-fetch@1.0.5': resolution: {integrity: sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==} + '@atcute/util-fetch@2.0.0': + resolution: {integrity: sha512-v+4aFQ/tuBqTV+URDJaFgm3mASWdglKXiPaGutJ1bs7QtQKmPZeesPY5MzW/a+MtI8GWCEJk8X9wOfalPOFSlg==} + '@atcute/util-text@1.1.0': resolution: {integrity: sha512-34G9KD5Z9f7oEdFpZOmqrMnU86p8ne6LlxJowfZzKNszRcl1GH+FtEPh3N1woelJT2SkPXMK2anwT8DESTluwA==} @@ -398,10 +535,48 @@ packages: '@atproto/xrpc@0.7.7': resolution: {integrity: sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.18.6': + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -410,15 +585,42 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.29.0': resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.28.6': resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -501,6 +703,10 @@ packages: resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} engines: {node: '>=18.0.0'} + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + '@cloudflare/unenv-preset@2.12.0': resolution: {integrity: sha512-NK4vN+2Z/GbfGS4BamtbbVk1rcu5RmqaYGiyHJQrA09AoxdZPHDF3W/EhgI0YSK8p3vRo/VNCtbSJFPON7FWMQ==} peerDependencies: @@ -510,6 +716,15 @@ packages: workerd: optional: true + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + '@cloudflare/vite-plugin@1.23.1': resolution: {integrity: sha512-TnE2+U0xM8QWQBC5SlthtIPyit9j6RD7YB0I61jRj28fU4beBH3zYoNXcmHjnhSVU6Y//gIg2xrGV4jXIvdwXw==} peerDependencies: @@ -530,30 +745,60 @@ packages: cpu: [x64] os: [darwin] + '@cloudflare/workerd-darwin-64@1.20260521.1': + resolution: {integrity: sha512-aiNdXmxlhwGjTSajL3I7uQPpN4lAOcXjvg5ZOlJKIywnevr798n9XCS6lvuqgniM3KjurBNWRRypMJntg/eSLg==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20260205.0': resolution: {integrity: sha512-402ZqLz+LrG0NDXp7Hn7IZbI0DyhjNfjAlVenb0K3yod9KCuux0u3NksNBvqJx0mIGHvVR4K05h+jfT5BTHqGA==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20260521.1': + resolution: {integrity: sha512-ikN8aKSi4Ak28ndOkuSO5rq6lmV6wwDQu9F9Vu6J7EkwAOth74J/Hjn4j4EuFceW/npw2Ws0Y/muzA6WKHl4TA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + '@cloudflare/workerd-linux-64@1.20260205.0': resolution: {integrity: sha512-rz9jBzazIA18RHY+osa19hvsPfr0LZI1AJzIjC6UqkKKphcTpHBEQ25Xt8cIA34ivMIqeENpYnnmpDFesLkfcQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] + '@cloudflare/workerd-linux-64@1.20260521.1': + resolution: {integrity: sha512-D/gUhvQcG0pJr5aJl6yUoi2JxbFpjVtDq9xUJHPjfkAjL28TUVgCR/e5r8YGirepv4I1DK7ihuii9LZ2GGMJbw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + '@cloudflare/workerd-linux-arm64@1.20260205.0': resolution: {integrity: sha512-jr6cKpMM/DBEbL+ATJ9rYue758CKp0SfA/nXt5vR32iINVJrb396ye9iat2y9Moa/PgPKnTrFgmT6urUmG3IUg==} engines: {node: '>=16'} cpu: [arm64] os: [linux] + '@cloudflare/workerd-linux-arm64@1.20260521.1': + resolution: {integrity: sha512-vhjWPIHenczegTakhRPwEmTeaavCpNqsuo3RlLCkUdU47HrwLvy/4QersGggs4+kF4Do+IE/EznCGyT40xYcLA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + '@cloudflare/workerd-windows-64@1.20260205.0': resolution: {integrity: sha512-SMPW5jCZYOG7XFIglSlsgN8ivcl0pCrSAYxCwxtWvZ88whhcDB/aISNtiQiDZujPH8tIo2hE5dEkxW7tGEwc3A==} engines: {node: '>=16'} cpu: [x64] os: [win32] + '@cloudflare/workerd-windows-64@1.20260521.1': + resolution: {integrity: sha512-wBolYC/+lnGIEbkkPdzFtjTOWip2uQH6maeAP1ZV0kyxi5SGpsa83+wD5rH5OOle+sHE5qJMdwCKjwRwj+FKJg==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + '@cloudflare/workers-types@4.20260207.0': resolution: {integrity: sha512-PSxgnAOK0EtTytlY7/+gJcsQJYg0Qo7KlOMSC/wiBE+pBqKjuKdd1ZgM+NvpPNqZAjWV5jqAMTTNYEmgk27gYw==} @@ -561,19 +806,33 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@corvu/utils@0.4.2': + resolution: {integrity: sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA==} + peerDependencies: + solid-js: ^1.8 + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -592,6 +851,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -610,6 +875,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -628,6 +899,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -646,6 +923,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -664,6 +947,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -682,6 +971,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -700,6 +995,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -718,6 +1019,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -736,6 +1043,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -754,6 +1067,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -772,6 +1091,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -790,6 +1115,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -808,6 +1139,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -826,6 +1163,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -844,6 +1187,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -862,6 +1211,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -880,6 +1235,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} @@ -898,6 +1259,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -916,6 +1283,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} @@ -934,6 +1307,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -952,6 +1331,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} @@ -970,6 +1355,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -988,6 +1379,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -1006,6 +1403,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -1024,6 +1427,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -1042,6 +1451,21 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@hexagon/base64@1.1.28': resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} @@ -1191,10 +1615,23 @@ packages: '@types/node': optional: true + '@internationalized/date@3.12.1': + resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==} + + '@internationalized/number@3.6.6': + resolution: {integrity: sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==} + '@ipld/car@5.4.2': resolution: {integrity: sha512-gfyrJvePyXnh2Fbj8mPg4JYvEZ3izhk8C9WgAle7xIYbrJNSXmNQ6BxAls8Gof97vvGbCROdxbTWRmHJtTCbcg==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} + '@ipld/car@5.4.6': + resolution: {integrity: sha512-f1Iyb9ci8B/TGCyjAuSQ6BOLiy2u8en7rB+SumbyweqXDu3gxYJwYbGzoposKjOzc4CeGlkEbwEw8REoekYAKQ==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} + + '@ipld/dag-cbor@10.0.1': + resolution: {integrity: sha512-nF07iiZPqduSXyMxc0jGANArRHFa9hjMQpQlgLOV2O/3xI1CNb5sXhvbmigbMiz5owSGi0Oq10VtauupMojuiw==} + '@ipld/dag-cbor@7.0.3': resolution: {integrity: sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA==} @@ -1205,6 +1642,9 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1218,6 +1658,16 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@kobalte/core@0.13.11': + resolution: {integrity: sha512-hK7TYpdib/XDb/r/4XDBFaO9O+3ZHz4ZWryV4/3BfES+tSQVgg2IJupDnztKXB0BqbSRy/aWlHKw1SPtNPYCFQ==} + peerDependencies: + solid-js: ^1.8.15 + + '@kobalte/utils@0.9.1': + resolution: {integrity: sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w==} + peerDependencies: + solid-js: ^1.8.8 + '@levischuck/tiny-cbor@0.2.11': resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} @@ -1233,6 +1683,12 @@ packages: '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@noble/curves@1.9.7': resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} engines: {node: ^14.21.3 || >=16} @@ -1262,6 +1718,9 @@ packages: '@oxc-project/types@0.112.0': resolution: {integrity: sha512-m6RebKHIRsax2iCwVpYW2ErQwa4ywHJrE4sCK3/8JK8ZZAWOKXaRJFl/uP51gaVyyXlaS4+chU1nSCdzYf6QqQ==} + '@oxc-project/types@0.132.0': + resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} + '@oxc-resolver/binding-android-arm-eabi@11.17.0': resolution: {integrity: sha512-kVnY21v0GyZ/+LG6EIO48wK3mE79BUuakHUYLIqobO/Qqq4mJsjuYXMSn3JtLcKZpN1HDVit4UHpGJHef1lrlw==} cpu: [arm] @@ -1427,6 +1886,12 @@ packages: cpu: [arm64] os: [android] + '@rolldown/binding-android-arm64@1.0.2': + resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-darwin-arm64@1.0.0-beta.57': resolution: {integrity: sha512-9c4FOhRGpl+PX7zBK5p17c5efpF9aSpTPgyigv57hXf5NjQUaJOOiejPLAtFiKNBIfm5Uu6yFkvLKzOafNvlTw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1439,6 +1904,12 @@ packages: cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-arm64@1.0.2': + resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-beta.57': resolution: {integrity: sha512-6RsB8Qy4LnGqNGJJC/8uWeLWGOvbRL/KG5aJ8XXpSEupg/KQtlBEiFaYU/Ma5Usj1s+bt3ItkqZYAI50kSplBA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1451,6 +1922,12 @@ packages: cpu: [x64] os: [darwin] + '@rolldown/binding-darwin-x64@1.0.2': + resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-freebsd-x64@1.0.0-beta.57': resolution: {integrity: sha512-uA9kG7+MYkHTbqwv67Tx+5GV5YcKd33HCJIi0311iYBd25yuwyIqvJfBdt1VVB8tdOlyTb9cPAgfCki8nhwTQg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1463,6 +1940,12 @@ packages: cpu: [x64] os: [freebsd] + '@rolldown/binding-freebsd-x64@1.0.2': + resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.57': resolution: {integrity: sha512-3KkS0cHsllT2T+Te+VZMKHNw6FPQihYsQh+8J4jkzwgvAQpbsbXmrqhkw3YU/QGRrD8qgcOvBr6z5y6Jid+rmw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1475,6 +1958,12 @@ packages: cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.57': resolution: {integrity: sha512-A3/wu1RgsHhqP3rVH2+sM81bpk+Qd2XaHTl8LtX5/1LNR7QVBFBCpAoiXwjTdGnI5cMdBVi7Z1pi52euW760Fw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1487,6 +1976,12 @@ packages: cpu: [arm64] os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.2': + resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.57': resolution: {integrity: sha512-d0kIVezTQtazpyWjiJIn5to8JlwfKITDqwsFv0Xc6s31N16CD2PC/Pl2OtKgS7n8WLOJbfqgIp5ixYzTAxCqMg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1499,16 +1994,40 @@ packages: cpu: [arm64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': - resolution: {integrity: sha512-E199LPijo98yrLjPCmETx8EF43sZf9t3guSrLee/ej1rCCc3zDVTR4xFfN9BRAapGVl7/8hYqbbiQPTkv73kUg==} + '@rolldown/binding-linux-arm64-musl@1.0.2': + resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] + cpu: [arm64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.3': - resolution: {integrity: sha512-iSXXZsQp08CSilff/DCTFZHSVEpEwdicV3W8idHyrByrcsRDVh9sGC3sev6d8BygSGj3vt8GvUKBPCoyMA4tgQ==} + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.2': + resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': + resolution: {integrity: sha512-E199LPijo98yrLjPCmETx8EF43sZf9t3guSrLee/ej1rCCc3zDVTR4xFfN9BRAapGVl7/8hYqbbiQPTkv73kUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.3': + resolution: {integrity: sha512-iSXXZsQp08CSilff/DCTFZHSVEpEwdicV3W8idHyrByrcsRDVh9sGC3sev6d8BygSGj3vt8GvUKBPCoyMA4tgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.2': + resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] os: [linux] '@rolldown/binding-linux-x64-musl@1.0.0-beta.57': @@ -1523,6 +2042,12 @@ packages: cpu: [x64] os: [linux] + '@rolldown/binding-linux-x64-musl@1.0.2': + resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@rolldown/binding-openharmony-arm64@1.0.0-beta.57': resolution: {integrity: sha512-voDEBcNqxbUv/GeXKFtxXVWA+H45P/8Dec4Ii/SbyJyGvCqV1j+nNHfnFUIiRQ2Q40DwPe/djvgYBs9PpETiMA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1535,6 +2060,12 @@ packages: cpu: [arm64] os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.0.2': + resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-wasm32-wasi@1.0.0-beta.57': resolution: {integrity: sha512-bRhcF7NLlCnpkzLVlVhrDEd0KH22VbTPkPTbMjlYvqhSmarxNIq5vtlQS8qmV7LkPKHrNLWyJW/V/sOyFba26Q==} engines: {node: '>=14.0.0'} @@ -1545,6 +2076,11 @@ packages: engines: {node: '>=14.0.0'} cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.0.2': + resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.57': resolution: {integrity: sha512-rnDVGRks2FQ2hgJ2g15pHtfxqkGFGjJQUDWzYznEkE8Ra2+Vag9OffxdbJMZqBWXHVM0iS4dv8qSiEn7bO+n1Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1557,6 +2093,12 @@ packages: cpu: [arm64] os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.0.2': + resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.57': resolution: {integrity: sha512-OqIUyNid1M4xTj6VRXp/Lht/qIP8fo25QyAZlCP+p6D2ATCEhyW4ZIFLnC9zAGN/HMbXoCzvwfa8Jjg/8J4YEg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1569,12 +2111,21 @@ packages: cpu: [x64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.2': + resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/pluginutils@1.0.0-beta.57': resolution: {integrity: sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ==} '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/rollup-android-arm-eabi@4.57.1': resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} cpu: [arm] @@ -1712,15 +2263,190 @@ packages: resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} + '@solid-primitives/event-listener@2.4.5': + resolution: {integrity: sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/keyed@1.5.3': + resolution: {integrity: sha512-zNadtyYBhJSOjXtogkGHmRxjGdz9KHc8sGGVAGlUABkE8BED2tbIZoxkwSqzOwde8OcUEH0bb5DLZUWIMvyBSA==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/map@0.4.13': + resolution: {integrity: sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/media@2.3.5': + resolution: {integrity: sha512-LX9fB5WDaK87FMDtUB1qokBOfT2et9Uobv/zZaKLH9caFSz4+P70MBKEIBHcZQy+9MV5M2XvGYLTbLskjkzMjA==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/props@3.2.3': + resolution: {integrity: sha512-XzG6en9gSFwmvbKcATm2BxL63HegZ+BAG5fmHi8jyBppQHcaths7ffz+6vYvwYy3nlgLa20ufJLj7tst+PcHFA==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/refs@1.1.3': + resolution: {integrity: sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/resize-observer@2.1.5': + resolution: {integrity: sha512-AiyTknKcNBaKHbcSMuxtSNM8FjIuiSuFyFghdD0TcCMU9hKi9EmsC5pjfjDwxE+5EueB1a+T/34PLRI5vbBbKw==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/rootless@1.5.3': + resolution: {integrity: sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/static-store@0.1.3': + resolution: {integrity: sha512-uxez7SXnr5GiRnzqO2IEDjOJRIXaG+0LZLBizmUA1FwSi+hrpuMzVBwyk70m4prcl8X6FDDXUl9O8hSq8wHbBQ==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/storage@4.3.4': + resolution: {integrity: sha512-GSxPAIuyxhJWOcv7n10iv3aid5oHN3KUgyA9IV0GYWlPpgyGs43aS9E85b0VXDLoH+D4ThNK8+2WEJ8B/S6Ccg==} + peerDependencies: + '@tauri-apps/plugin-store': '*' + solid-js: ^1.6.12 + solid-start: '*' + peerDependenciesMeta: + '@tauri-apps/plugin-store': + optional: true + solid-start: + optional: true + + '@solid-primitives/trigger@1.2.3': + resolution: {integrity: sha512-Za2JebEiDyfamjmDwRaESYqBBYOlgYGzB8kHYH0QrkXyLf2qNADlKdGN+z3vWSLCTDcKxChS43Kssjuc0OZhng==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/utils@6.4.0': + resolution: {integrity: sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A==} + peerDependencies: + solid-js: ^1.6.12 + '@speed-highlight/core@1.2.14': resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/helpers@0.5.21': + resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} + + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bun@1.3.8': resolution: {integrity: sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA==} @@ -1748,6 +2474,9 @@ packages: '@vitest/expect@4.1.0-beta.1': resolution: {integrity: sha512-LpwvdERiCpuXWE8IRLGgqMWPvcGxZVUNmVyRnvs4ZPCazbEgjkm5wlqFN7lJYfQkRamldHO+38348EefLtRY0A==} + '@vitest/expect@4.1.7': + resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + '@vitest/mocker@4.1.0-beta.1': resolution: {integrity: sha512-IiiJL8aqJrk5oNgiOsTvrzMAt71qWL1pvHKJJiaKQCMX3Lvj6w++HvNJIl3d0rm5D9sngAJYtF87EN0CTWhF8Q==} peerDependencies: @@ -1759,21 +2488,47 @@ packages: vite: optional: true + '@vitest/mocker@4.1.7': + resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@4.1.0-beta.1': resolution: {integrity: sha512-CeI3uthjV/XKA6KBCr/B5HlCQaFdCgprdl7gBg/sUExQPary8BBhYoVWJeAPTeg9u+ppT9S4v/sYjjNjn3Qsrw==} + '@vitest/pretty-format@4.1.7': + resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + '@vitest/runner@4.1.0-beta.1': resolution: {integrity: sha512-oE0nFu+0zT6IhhAu8Z9wWCWWy63a7btZLvq4zUkrGwJ9U4sabXHWzYakBE6ZDLXpI8aDv796+0AMej2AJ7m3tw==} + '@vitest/runner@4.1.7': + resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + '@vitest/snapshot@4.1.0-beta.1': resolution: {integrity: sha512-wSt0PAy1QCZjzPUgpIYXBtZFTFXPw65GIQxz9mjhl0yjkAE+wnRU08+w3R3X5hrCKYVhTS3HHV8zs6Yin2K0Dw==} + '@vitest/snapshot@4.1.7': + resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + '@vitest/spy@4.1.0-beta.1': resolution: {integrity: sha512-DzHg9PJuWYivWLt4O9SYF2u5/mGlfM3tgP8DdlSwMr7C+hBusejK+r0rqaCSYwBH47ePhU4jccBm4i6bE2dahg==} + '@vitest/spy@4.1.7': + resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + '@vitest/utils@4.1.0-beta.1': resolution: {integrity: sha512-IUCsqDFj8E8WJq3wGRQ7MiMb2571tjTnjyrJ1oy+0HODutA2TpZGRqBA8ziLCIWTOL/e4RArE2k6eZh/jXgk9A==} + '@vitest/utils@4.1.7': + resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -1834,9 +2589,28 @@ packages: await-lock@2.2.2: resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} + babel-plugin-jsx-dom-expressions@0.40.7: + resolution: {integrity: sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ==} + peerDependencies: + '@babel/core': ^7.20.12 + + babel-preset-solid@1.9.12: + resolution: {integrity: sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==} + peerDependencies: + '@babel/core': ^7.0.0 + solid-js: ^1.9.12 + peerDependenciesMeta: + solid-js: + optional: true + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.32: + resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} + engines: {node: '>=6.0.0'} + hasBin: true + bcryptjs@3.0.3: resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} hasBin: true @@ -1855,6 +2629,11 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -1869,6 +2648,9 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + cborg@1.10.2: resolution: {integrity: sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==} hasBin: true @@ -1877,6 +2659,10 @@ packages: resolution: {integrity: sha512-6/viltD51JklRhq4L7jC3zgy6gryuG5xfZ3kzpE+PravtyeQLeQmCYLREhQH7pWENg5pY4Yu/XCd6a7dKScVlw==} hasBin: true + cborg@5.1.1: + resolution: {integrity: sha512-BDbSRIp6XrQXkTc7g+DN0RB9RrDPTUfals2ecWUlt3juPLjbAvy/V72mJcXY0Ehu0Dq/3WpNCOCT68HUTbW+lw==} + hasBin: true + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -1936,6 +2722,9 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -1944,9 +2733,21 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -1982,6 +2783,9 @@ packages: oxc-resolver: optional: true + electron-to-chromium@1.5.361: + resolution: {integrity: sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1992,10 +2796,18 @@ packages: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} + enhanced-resolve@5.22.0: + resolution: {integrity: sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==} + engines: {node: '>=10.13.0'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -2006,6 +2818,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -2021,6 +2836,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2103,6 +2923,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2135,6 +2959,9 @@ packages: hookable@6.0.1: resolution: {integrity: sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==} + html-entities@2.3.3: + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} + human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -2143,6 +2970,9 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + idb-keyval@6.2.4: + resolution: {integrity: sha512-D/NzHWUmYJGXi++z67aMSrnisb9A3621CyRK5G89JyTlN13C8xf0g04DLxUKMufPem3e3L2JAXR6Z00OWy183Q==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2174,6 +3004,10 @@ packages: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -2188,9 +3022,19 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -2204,6 +3048,11 @@ packages: engines: {node: '>=6'} hasBin: true + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -2219,6 +3068,76 @@ packages: '@types/node': '>=18' typescript: '>=5.0.4 <7' + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -2230,6 +3149,9 @@ packages: resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} engines: {node: 20 || >=22} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2244,6 +3166,10 @@ packages: engines: {node: '>= 16'} hasBin: true + merge-anything@5.1.7: + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} + engines: {node: '>=12.13'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2257,6 +3183,11 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + miniflare@4.20260521.0: + resolution: {integrity: sha512-roRfxPq49OkuSeQsc43hRjSB1+HdHtDNKRwDEVk2hCjCBuBWxb5Wvwq88b0ULj6QVEJLN/+ZqF19M+h4VYJ/zg==} + engines: {node: '>=22.0.0'} + hasBin: true + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -2264,9 +3195,15 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + multiformats@13.4.2: resolution: {integrity: sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==} + multiformats@14.0.0: + resolution: {integrity: sha512-iWK1RrAS58p2NDfeZFuSUSv3ZPewTIhsGbh/5NgeGGJwJmRljLxGtjRR3nkn+loG3zl+IrfR/W1590QnrSK+Gg==} + multiformats@9.9.0: resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} @@ -2278,6 +3215,16 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.11: + resolution: {integrity: sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==} + engines: {node: ^18 || >=20} + hasBin: true + node-emoji@2.2.0: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} @@ -2295,6 +3242,13 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + + oauth4webapi@3.8.6: + resolution: {integrity: sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2347,6 +3301,9 @@ packages: parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2376,6 +3333,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -2394,6 +3355,10 @@ packages: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -2506,11 +3471,36 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rolldown@1.0.2: + resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup@4.57.1: resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rosie-skills-darwin-arm64@0.6.4: + resolution: {integrity: sha512-rn1s5hqFKcxeiDEWWoFa1hdGPshR8TkwHLzy/cBavb9XJNAaUxbe3oQ78W9sQkRHAgRyzJYyk9tw68Qrdnizgg==} + cpu: [arm64] + os: [darwin] + + rosie-skills-freebsd-x64@0.6.4: + resolution: {integrity: sha512-SxCRduPBMtfjkQ+q56Yw9OLA3PyaqoALzt7kER7IDKuUVfM2O/1w8sa5xhTDiCvWkZJixnH5d5Ya6KT+/Mwcng==} + cpu: [x64] + os: [freebsd] + + rosie-skills-linux-x64@0.6.4: + resolution: {integrity: sha512-D9Y9mfu7goB0s0X59uU3hcFeUTef3VbpCIDwFMzyvJrAq3XhRACWBDMHQsHlyWdHxTXPX/ILyW65RXyrJlgqng==} + cpu: [x64] + os: [linux] + + rosie-skills@0.6.4: + resolution: {integrity: sha512-ojfhSiQRdZ2QyWbmKAHOSAUbaLYrTc5zIH7mS1jKoP8KCFSQddwVhMyFqldckTeybTfW3zNcsZzyOTzGTN1SBA==} + engines: {node: '>=18'} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2528,11 +3518,25 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true + seroval-plugins@1.5.4: + resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.4: + resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} + engines: {node: '>=10'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -2570,6 +3574,24 @@ packages: resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} engines: {node: '>= 18'} + solid-js@1.9.13: + resolution: {integrity: sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ==} + + solid-presence@0.1.8: + resolution: {integrity: sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA==} + peerDependencies: + solid-js: ^1.8 + + solid-prevent-scroll@0.1.10: + resolution: {integrity: sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw==} + peerDependencies: + solid-js: ^1.8 + + solid-refresh@0.6.3: + resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} + peerDependencies: + solid-js: ^1.3 + sonic-boom@3.8.1: resolution: {integrity: sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==} @@ -2593,6 +3615,9 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2624,6 +3649,13 @@ packages: resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} engines: {node: '>=14.18'} + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -2645,14 +3677,26 @@ packages: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} + tinyexec@1.2.2: + resolution: {integrity: sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinyrainbow@3.0.3: + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.0.3: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + tlds@1.261.0: resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} hasBin: true @@ -2718,6 +3762,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + uint8arrays@3.0.0: resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} @@ -2731,6 +3780,10 @@ packages: resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} engines: {node: '>=20.18.1'} + undici@7.24.8: + resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} + engines: {node: '>=20.18.1'} + unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -2755,6 +3808,20 @@ packages: synckit: optional: true + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + valibot@1.4.0: + resolution: {integrity: sha512-iC/x7fVcSyOwlm/VSt7RlHnzNGLGvR9GnxdifUeWoCJo0q4ZZvrVkIHC6faTlkxG47I2Y4UrFquPuVHCrOnrLg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + validate-npm-package-name@5.0.1: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -2762,6 +3829,16 @@ packages: varint@6.0.0: resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + vite-plugin-solid@2.11.12: + resolution: {integrity: sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA==} + peerDependencies: + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* + solid-js: ^1.7.2 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@testing-library/jest-dom': + optional: true + vite@6.4.1: resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2802,6 +3879,97 @@ packages: yaml: optional: true + vite@7.3.3: + resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vite@8.0.14: + resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + vitest@4.1.0-beta.1: resolution: {integrity: sha512-iJKx5pLpS9eRgHJuVCKuuphoKZNCUtSoBiqpHdgrVyqFAZNU66m90H9lSHek1WiRCeqPfgV/ugK0Bl+ZOv8K9Q==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -2836,6 +4004,47 @@ packages: jsdom: optional: true + vitest@4.1.7: + resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.7 + '@vitest/browser-preview': 4.1.7 + '@vitest/browser-webdriverio': 4.1.7 + '@vitest/coverage-istanbul': 4.1.7 + '@vitest/coverage-v8': 4.1.7 + '@vitest/ui': 4.1.7 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + walk-up-path@4.0.0: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} @@ -2864,6 +4073,11 @@ packages: engines: {node: '>=16'} hasBin: true + workerd@1.20260521.1: + resolution: {integrity: sha512-HzIThcZ0ZVEuzVxpY2IYZ3yssSrTjtrWXAVfmOl5rVwyqcu7aeZXGMiwrEmi9MOcC3wjy+BNv+hFrMMY5OrjQQ==} + engines: {node: '>=16'} + hasBin: true + wrangler@4.63.0: resolution: {integrity: sha512-+R04jF7Eb8K3KRMSgoXpcIdLb8GC62eoSGusYh1pyrSMm/10E0hbKkd7phMJO4HxXc6R7mOHC5SSoX9eof30Uw==} engines: {node: '>=20.0.0'} @@ -2874,6 +4088,16 @@ packages: '@cloudflare/workers-types': optional: true + wrangler@4.94.0: + resolution: {integrity: sha512-GsNw0DomGFfeXFtKVTwn2X69UKcCxcTB0CXykjsMineJIxOeyrw7LovlHQ/3JU8KJHH7repLB+kOHvfTBA/Eew==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260521.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -2906,6 +4130,18 @@ packages: utf-8-validate: optional: true + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} @@ -2913,6 +4149,9 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -2970,29 +4209,38 @@ snapshots: dependencies: '@atcute/lexicons': 1.3.0 + '@atcute/atproto@4.0.0(@atcute/lexicons@2.0.0)': + dependencies: + '@atcute/lexicons': 2.0.0 + '@atcute/bluesky@3.2.17': dependencies: '@atcute/atproto': 3.1.10 '@atcute/lexicons': 1.3.0 + '@atcute/bluesky@4.0.3(@atcute/lexicons@2.0.0)': + dependencies: + '@atcute/atproto': 4.0.0(@atcute/lexicons@2.0.0) + '@atcute/lexicons': 2.0.0 + '@atcute/car@5.1.1': dependencies: - '@atcute/cbor': 2.3.2 + '@atcute/cbor': 2.3.3(@atcute/cid@2.4.1) '@atcute/cid': 2.4.1 - '@atcute/uint8array': 1.1.1 + '@atcute/uint8array': 1.1.2 '@atcute/varint': 2.0.0 '@atcute/cbor@2.3.0': dependencies: - '@atcute/cid': 2.4.0 + '@atcute/cid': 2.4.1 '@atcute/multibase': 1.1.7 '@atcute/uint8array': 1.1.0 - '@atcute/cbor@2.3.2': + '@atcute/cbor@2.3.3(@atcute/cid@2.4.1)': dependencies: '@atcute/cid': 2.4.1 '@atcute/multibase': 1.2.0 - '@atcute/uint8array': 1.1.1 + '@atcute/uint8array': 1.1.2 '@atcute/cid@2.4.0': dependencies: @@ -3002,17 +4250,24 @@ snapshots: '@atcute/cid@2.4.1': dependencies: '@atcute/multibase': 1.2.0 - '@atcute/uint8array': 1.1.1 + '@atcute/uint8array': 1.1.2 '@atcute/client@4.2.1': dependencies: '@atcute/identity': 1.1.4 '@atcute/lexicons': 1.3.0 + '@atcute/client@5.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3)': + dependencies: + '@atcute/identity': 2.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3) + '@atcute/lexicons': 2.0.0 + transitivePeerDependencies: + - typescript + '@atcute/crypto@2.4.1': dependencies: '@atcute/multibase': 1.2.0 - '@atcute/uint8array': 1.1.1 + '@atcute/uint8array': 1.1.2 '@noble/secp256k1': 3.1.0 '@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4)': @@ -3022,16 +4277,32 @@ snapshots: '@atcute/util-fetch': 1.0.5 '@badrap/valita': 0.4.6 + '@atcute/identity-resolver@2.0.0(@atcute/identity@2.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3))(@atcute/lexicons@2.0.0)(typescript@6.0.3)': + dependencies: + '@atcute/identity': 2.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3) + '@atcute/lexicons': 2.0.0 + '@atcute/util-fetch': 2.0.0(typescript@6.0.3) + valibot: 1.4.0(typescript@6.0.3) + transitivePeerDependencies: + - typescript + '@atcute/identity@1.1.4': dependencies: '@atcute/lexicons': 1.3.0 '@badrap/valita': 0.4.6 + '@atcute/identity@2.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3)': + dependencies: + '@atcute/lexicons': 2.0.0 + valibot: 1.4.0(typescript@6.0.3) + transitivePeerDependencies: + - typescript + '@atcute/lexicon-doc@2.2.0': dependencies: '@atcute/identity': 1.1.4 '@atcute/lexicons': 1.3.0 - '@atcute/uint8array': 1.1.1 + '@atcute/uint8array': 1.1.2 '@atcute/util-text': 1.3.1 '@badrap/valita': 0.4.6 @@ -3060,11 +4331,18 @@ snapshots: '@standard-schema/spec': 1.1.0 esm-env: 1.2.2 + '@atcute/lexicons@2.0.0': + dependencies: + '@atcute/uint8array': 1.1.2 + '@atcute/util-text': 1.3.1 + '@standard-schema/spec': 1.1.0 + esm-env: 1.2.2 + '@atcute/mst@1.0.0': dependencies: - '@atcute/cbor': 2.3.2 + '@atcute/cbor': 2.3.3(@atcute/cid@2.4.1) '@atcute/cid': 2.4.1 - '@atcute/uint8array': 1.1.1 + '@atcute/uint8array': 1.1.2 '@atcute/multibase@1.1.7': dependencies: @@ -3072,35 +4350,85 @@ snapshots: '@atcute/multibase@1.2.0': dependencies: - '@atcute/uint8array': 1.1.1 + '@atcute/uint8array': 1.1.2 + + '@atcute/oauth-browser-client@4.0.0(@atcute/identity-resolver@2.0.0(@atcute/identity@2.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3))(@atcute/lexicons@2.0.0)(typescript@6.0.3))(@atcute/lexicons@2.0.0)(typescript@6.0.3)': + dependencies: + '@atcute/client': 5.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3) + '@atcute/identity-resolver': 2.0.0(@atcute/identity@2.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3))(@atcute/lexicons@2.0.0)(typescript@6.0.3) + '@atcute/lexicons': 2.0.0 + '@atcute/multibase': 1.2.0 + '@atcute/oauth-crypto': 1.0.0(typescript@6.0.3) + '@atcute/oauth-types': 1.0.0(typescript@6.0.3) + nanoid: 5.1.11 + transitivePeerDependencies: + - typescript + + '@atcute/oauth-crypto@1.0.0(typescript@6.0.3)': + dependencies: + '@atcute/multibase': 1.2.0 + '@atcute/uint8array': 1.1.2 + nanoid: 5.1.11 + valibot: 1.4.0(typescript@6.0.3) + transitivePeerDependencies: + - typescript + + '@atcute/oauth-keyset@0.1.1(typescript@6.0.3)': + dependencies: + '@atcute/oauth-crypto': 1.0.0(typescript@6.0.3) + transitivePeerDependencies: + - typescript + + '@atcute/oauth-types@1.0.0(typescript@6.0.3)': + dependencies: + '@atcute/identity': 2.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3) + '@atcute/lexicons': 2.0.0 + '@atcute/oauth-keyset': 0.1.1(typescript@6.0.3) + valibot: 1.4.0(typescript@6.0.3) + transitivePeerDependencies: + - typescript '@atcute/repo@0.1.4': dependencies: '@atcute/car': 5.1.1 - '@atcute/cbor': 2.3.2 + '@atcute/cbor': 2.3.3(@atcute/cid@2.4.1) '@atcute/cid': 2.4.1 '@atcute/crypto': 2.4.1 '@atcute/lexicons': 1.3.0 '@atcute/mst': 1.0.0 - '@atcute/uint8array': 1.1.1 + '@atcute/uint8array': 1.1.2 '@atcute/tid@1.1.1': dependencies: '@atcute/time-ms': 1.2.0 + '@atcute/tid@1.1.2': + dependencies: + '@atcute/time-ms': 1.3.2 + '@atcute/time-ms@1.2.0': dependencies: '@types/bun': 1.3.8 node-gyp-build: 4.8.4 + '@atcute/time-ms@1.3.2': {} + '@atcute/uint8array@1.1.0': {} '@atcute/uint8array@1.1.1': {} + '@atcute/uint8array@1.1.2': {} + '@atcute/util-fetch@1.0.5': dependencies: '@badrap/valita': 0.4.6 + '@atcute/util-fetch@2.0.0(typescript@6.0.3)': + dependencies: + valibot: 1.4.0(typescript@6.0.3) + transitivePeerDependencies: + - typescript + '@atcute/util-text@1.1.0': dependencies: unicode-segmenter: 0.14.5 @@ -3230,6 +4558,34 @@ snapshots: '@atproto/lexicon': 0.6.1 zod: 3.25.76 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/generator@7.29.1': dependencies: '@babel/parser': 7.29.0 @@ -3238,16 +4594,82 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.18.6': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@babel/parser@7.29.0': dependencies: '@babel/types': 7.29.0 + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/runtime@7.28.6': {} + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -3429,18 +4851,32 @@ snapshots: '@cloudflare/kv-asset-handler@0.4.2': {} + '@cloudflare/kv-asset-handler@0.5.0': {} + '@cloudflare/unenv-preset@2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260205.0)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: workerd: 1.20260205.0 - '@cloudflare/vite-plugin@1.23.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0))(workerd@1.20260205.0)(wrangler@4.63.0(@cloudflare/workers-types@4.20260207.0))': + '@cloudflare/unenv-preset@2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260521.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260521.1 + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260521.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260521.1 + + '@cloudflare/vite-plugin@1.23.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0))(workerd@1.20260205.0)(wrangler@4.63.0(@cloudflare/workers-types@4.20260207.0))': dependencies: '@cloudflare/unenv-preset': 2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260205.0) miniflare: 4.20260205.0 unenv: 2.0.0-rc.24 - vite: 6.4.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0) + vite: 6.4.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) wrangler: 4.63.0(@cloudflare/workers-types@4.20260207.0) ws: 8.18.0 transitivePeerDependencies: @@ -3448,15 +4884,28 @@ snapshots: - utf-8-validate - workerd - '@cloudflare/vitest-pool-workers@https://pkg.pr.new/@cloudflare/vitest-pool-workers@79a1932669f44a24342c1ffd66c29decd8a8fee9(@cloudflare/workers-types@4.20260207.0)(@vitest/runner@4.1.0-beta.1)(@vitest/snapshot@4.1.0-beta.1)(vitest@4.1.0-beta.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0))': + '@cloudflare/vite-plugin@1.23.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0))(workerd@1.20260521.1)(wrangler@4.63.0(@cloudflare/workers-types@4.20260207.0))': dependencies: - '@vitest/runner': 4.1.0-beta.1 - '@vitest/snapshot': 4.1.0-beta.1 + '@cloudflare/unenv-preset': 2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260521.1) + miniflare: 4.20260205.0 + unenv: 2.0.0-rc.24 + vite: 6.4.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) + wrangler: 4.63.0(@cloudflare/workers-types@4.20260207.0) + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - workerd + + '@cloudflare/vitest-pool-workers@https://pkg.pr.new/@cloudflare/vitest-pool-workers@79a1932669f44a24342c1ffd66c29decd8a8fee9(@cloudflare/workers-types@4.20260207.0)(@vitest/runner@4.1.7)(@vitest/snapshot@4.1.7)(vitest@4.1.0-beta.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0))': + dependencies: + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 cjs-module-lexer: 1.4.3 esbuild: 0.27.0 miniflare: 4.20260205.0 - vitest: 4.1.0-beta.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0) - wrangler: 4.63.0(@cloudflare/workers-types@4.20260207.0) + vitest: 4.1.0-beta.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) + wrangler: 4.94.0(@cloudflare/workers-types@4.20260207.0) zod: 3.25.76 transitivePeerDependencies: - '@cloudflare/workers-types' @@ -3466,33 +4915,64 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20260205.0': optional: true + '@cloudflare/workerd-darwin-64@1.20260521.1': + optional: true + '@cloudflare/workerd-darwin-arm64@1.20260205.0': optional: true + '@cloudflare/workerd-darwin-arm64@1.20260521.1': + optional: true + '@cloudflare/workerd-linux-64@1.20260205.0': optional: true + '@cloudflare/workerd-linux-64@1.20260521.1': + optional: true + '@cloudflare/workerd-linux-arm64@1.20260205.0': optional: true + '@cloudflare/workerd-linux-arm64@1.20260521.1': + optional: true + '@cloudflare/workerd-windows-64@1.20260205.0': optional: true + '@cloudflare/workerd-windows-64@1.20260521.1': + optional: true + '@cloudflare/workers-types@4.20260207.0': {} '@colors/colors@1.5.0': optional: true + '@corvu/utils@0.4.2(solid-js@1.9.13)': + dependencies: + '@floating-ui/dom': 1.7.6 + solid-js: 1.9.13 + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 @@ -3503,6 +4983,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -3512,6 +4997,9 @@ snapshots: '@esbuild/aix-ppc64@0.27.3': optional: true + '@esbuild/aix-ppc64@0.27.7': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true @@ -3521,6 +5009,9 @@ snapshots: '@esbuild/android-arm64@0.27.3': optional: true + '@esbuild/android-arm64@0.27.7': + optional: true + '@esbuild/android-arm@0.25.12': optional: true @@ -3530,6 +5021,9 @@ snapshots: '@esbuild/android-arm@0.27.3': optional: true + '@esbuild/android-arm@0.27.7': + optional: true + '@esbuild/android-x64@0.25.12': optional: true @@ -3539,6 +5033,9 @@ snapshots: '@esbuild/android-x64@0.27.3': optional: true + '@esbuild/android-x64@0.27.7': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true @@ -3548,6 +5045,9 @@ snapshots: '@esbuild/darwin-arm64@0.27.3': optional: true + '@esbuild/darwin-arm64@0.27.7': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true @@ -3557,6 +5057,9 @@ snapshots: '@esbuild/darwin-x64@0.27.3': optional: true + '@esbuild/darwin-x64@0.27.7': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true @@ -3566,6 +5069,9 @@ snapshots: '@esbuild/freebsd-arm64@0.27.3': optional: true + '@esbuild/freebsd-arm64@0.27.7': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true @@ -3575,6 +5081,9 @@ snapshots: '@esbuild/freebsd-x64@0.27.3': optional: true + '@esbuild/freebsd-x64@0.27.7': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true @@ -3584,6 +5093,9 @@ snapshots: '@esbuild/linux-arm64@0.27.3': optional: true + '@esbuild/linux-arm64@0.27.7': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true @@ -3593,6 +5105,9 @@ snapshots: '@esbuild/linux-arm@0.27.3': optional: true + '@esbuild/linux-arm@0.27.7': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true @@ -3602,6 +5117,9 @@ snapshots: '@esbuild/linux-ia32@0.27.3': optional: true + '@esbuild/linux-ia32@0.27.7': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true @@ -3611,6 +5129,9 @@ snapshots: '@esbuild/linux-loong64@0.27.3': optional: true + '@esbuild/linux-loong64@0.27.7': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true @@ -3620,6 +5141,9 @@ snapshots: '@esbuild/linux-mips64el@0.27.3': optional: true + '@esbuild/linux-mips64el@0.27.7': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true @@ -3629,6 +5153,9 @@ snapshots: '@esbuild/linux-ppc64@0.27.3': optional: true + '@esbuild/linux-ppc64@0.27.7': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true @@ -3638,6 +5165,9 @@ snapshots: '@esbuild/linux-riscv64@0.27.3': optional: true + '@esbuild/linux-riscv64@0.27.7': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true @@ -3647,6 +5177,9 @@ snapshots: '@esbuild/linux-s390x@0.27.3': optional: true + '@esbuild/linux-s390x@0.27.7': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true @@ -3656,6 +5189,9 @@ snapshots: '@esbuild/linux-x64@0.27.3': optional: true + '@esbuild/linux-x64@0.27.7': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true @@ -3665,6 +5201,9 @@ snapshots: '@esbuild/netbsd-arm64@0.27.3': optional: true + '@esbuild/netbsd-arm64@0.27.7': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true @@ -3674,6 +5213,9 @@ snapshots: '@esbuild/netbsd-x64@0.27.3': optional: true + '@esbuild/netbsd-x64@0.27.7': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true @@ -3683,6 +5225,9 @@ snapshots: '@esbuild/openbsd-arm64@0.27.3': optional: true + '@esbuild/openbsd-arm64@0.27.7': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true @@ -3692,6 +5237,9 @@ snapshots: '@esbuild/openbsd-x64@0.27.3': optional: true + '@esbuild/openbsd-x64@0.27.7': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true @@ -3701,6 +5249,9 @@ snapshots: '@esbuild/openharmony-arm64@0.27.3': optional: true + '@esbuild/openharmony-arm64@0.27.7': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true @@ -3710,6 +5261,9 @@ snapshots: '@esbuild/sunos-x64@0.27.3': optional: true + '@esbuild/sunos-x64@0.27.7': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true @@ -3719,6 +5273,9 @@ snapshots: '@esbuild/win32-arm64@0.27.3': optional: true + '@esbuild/win32-arm64@0.27.7': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true @@ -3728,6 +5285,9 @@ snapshots: '@esbuild/win32-ia32@0.27.3': optional: true + '@esbuild/win32-ia32@0.27.7': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true @@ -3737,6 +5297,20 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true + '@esbuild/win32-x64@0.27.7': + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + '@hexagon/base64@1.1.28': {} '@img/colour@1.0.0': {} @@ -3842,6 +5416,14 @@ snapshots: optionalDependencies: '@types/node': 24.10.11 + '@internationalized/date@3.12.1': + dependencies: + '@swc/helpers': 0.5.21 + + '@internationalized/number@3.6.6': + dependencies: + '@swc/helpers': 0.5.21 + '@ipld/car@5.4.2': dependencies: '@ipld/dag-cbor': 9.2.5 @@ -3849,6 +5431,18 @@ snapshots: multiformats: 13.4.2 varint: 6.0.0 + '@ipld/car@5.4.6': + dependencies: + '@ipld/dag-cbor': 10.0.1 + cborg: 5.1.1 + multiformats: 14.0.0 + varint: 6.0.0 + + '@ipld/dag-cbor@10.0.1': + dependencies: + cborg: 5.1.1 + multiformats: 14.0.0 + '@ipld/dag-cbor@7.0.3': dependencies: cborg: 1.10.2 @@ -3864,6 +5458,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -3878,6 +5477,29 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@kobalte/core@0.13.11(solid-js@1.9.13)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@internationalized/date': 3.12.1 + '@internationalized/number': 3.6.6 + '@kobalte/utils': 0.9.1(solid-js@1.9.13) + '@solid-primitives/props': 3.2.3(solid-js@1.9.13) + '@solid-primitives/resize-observer': 2.1.5(solid-js@1.9.13) + solid-js: 1.9.13 + solid-presence: 0.1.8(solid-js@1.9.13) + solid-prevent-scroll: 0.1.10(solid-js@1.9.13) + + '@kobalte/utils@0.9.1(solid-js@1.9.13)': + dependencies: + '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.13) + '@solid-primitives/keyed': 1.5.3(solid-js@1.9.13) + '@solid-primitives/map': 0.4.13(solid-js@1.9.13) + '@solid-primitives/media': 2.3.5(solid-js@1.9.13) + '@solid-primitives/props': 3.2.3(solid-js@1.9.13) + '@solid-primitives/refs': 1.1.3(solid-js@1.9.13) + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + '@levischuck/tiny-cbor@0.2.11': {} '@loaderkit/resolve@1.0.4': @@ -3907,6 +5529,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + '@noble/curves@1.9.7': dependencies: '@noble/hashes': 1.8.0 @@ -3931,6 +5560,8 @@ snapshots: '@oxc-project/types@0.112.0': {} + '@oxc-project/types@0.132.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.17.0': optional: true @@ -4113,60 +5744,96 @@ snapshots: '@rolldown/binding-android-arm64@1.0.0-rc.3': optional: true + '@rolldown/binding-android-arm64@1.0.2': + optional: true + '@rolldown/binding-darwin-arm64@1.0.0-beta.57': optional: true '@rolldown/binding-darwin-arm64@1.0.0-rc.3': optional: true + '@rolldown/binding-darwin-arm64@1.0.2': + optional: true + '@rolldown/binding-darwin-x64@1.0.0-beta.57': optional: true '@rolldown/binding-darwin-x64@1.0.0-rc.3': optional: true + '@rolldown/binding-darwin-x64@1.0.2': + optional: true + '@rolldown/binding-freebsd-x64@1.0.0-beta.57': optional: true '@rolldown/binding-freebsd-x64@1.0.0-rc.3': optional: true + '@rolldown/binding-freebsd-x64@1.0.2': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.57': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.3': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.57': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.3': optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.2': + optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.57': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-rc.3': optional: true + '@rolldown/binding-linux-arm64-musl@1.0.2': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.2': + optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.57': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-rc.3': optional: true + '@rolldown/binding-linux-x64-gnu@1.0.2': + optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-beta.57': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-rc.3': optional: true + '@rolldown/binding-linux-x64-musl@1.0.2': + optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-beta.57': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-rc.3': optional: true + '@rolldown/binding-openharmony-arm64@1.0.2': + optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-beta.57': dependencies: '@napi-rs/wasm-runtime': 1.1.1 @@ -4177,22 +5844,37 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.1 optional: true + '@rolldown/binding-wasm32-wasi@1.0.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.57': optional: true '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.3': optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.2': + optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.57': optional: true '@rolldown/binding-win32-x64-msvc@1.0.0-rc.3': optional: true + '@rolldown/binding-win32-x64-msvc@1.0.2': + optional: true + '@rolldown/pluginutils@1.0.0-beta.57': {} '@rolldown/pluginutils@1.0.0-rc.3': {} + '@rolldown/pluginutils@1.0.1': {} + '@rollup/rollup-android-arm-eabi@4.57.1': optional: true @@ -4214,83 +5896,245 @@ snapshots: '@rollup/rollup-linux-arm-gnueabihf@4.57.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.57.1': - optional: true + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + + '@simplewebauthn/server@13.2.2': + dependencies: + '@hexagon/base64': 1.1.28 + '@levischuck/tiny-cbor': 0.2.11 + '@peculiar/asn1-android': 2.6.0 + '@peculiar/asn1-ecc': 2.6.0 + '@peculiar/asn1-rsa': 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + '@peculiar/x509': 1.14.3 + + '@sindresorhus/is@4.6.0': {} + + '@sindresorhus/is@7.2.0': {} + + '@solid-primitives/event-listener@2.4.5(solid-js@1.9.13)': + dependencies: + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/keyed@1.5.3(solid-js@1.9.13)': + dependencies: + solid-js: 1.9.13 + + '@solid-primitives/map@0.4.13(solid-js@1.9.13)': + dependencies: + '@solid-primitives/trigger': 1.2.3(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/media@2.3.5(solid-js@1.9.13)': + dependencies: + '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.13) + '@solid-primitives/rootless': 1.5.3(solid-js@1.9.13) + '@solid-primitives/static-store': 0.1.3(solid-js@1.9.13) + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/props@3.2.3(solid-js@1.9.13)': + dependencies: + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/refs@1.1.3(solid-js@1.9.13)': + dependencies: + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/resize-observer@2.1.5(solid-js@1.9.13)': + dependencies: + '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.13) + '@solid-primitives/rootless': 1.5.3(solid-js@1.9.13) + '@solid-primitives/static-store': 0.1.3(solid-js@1.9.13) + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/rootless@1.5.3(solid-js@1.9.13)': + dependencies: + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/static-store@0.1.3(solid-js@1.9.13)': + dependencies: + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/storage@4.3.4(solid-js@1.9.13)': + dependencies: + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/trigger@1.2.3(solid-js@1.9.13)': + dependencies: + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 + + '@solid-primitives/utils@6.4.0(solid-js@1.9.13)': + dependencies: + solid-js: 1.9.13 + + '@speed-highlight/core@1.2.14': {} + + '@standard-schema/spec@1.1.0': {} - '@rollup/rollup-linux-arm64-gnu@4.57.1': - optional: true + '@swc/helpers@0.5.21': + dependencies: + tslib: 2.8.1 - '@rollup/rollup-linux-arm64-musl@4.57.1': - optional: true + '@tailwindcss/node@4.3.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.22.0 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 - '@rollup/rollup-linux-loong64-gnu@4.57.1': + '@tailwindcss/oxide-android-arm64@4.3.0': optional: true - '@rollup/rollup-linux-loong64-musl@4.57.1': + '@tailwindcss/oxide-darwin-arm64@4.3.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.57.1': + '@tailwindcss/oxide-darwin-x64@4.3.0': optional: true - '@rollup/rollup-linux-ppc64-musl@4.57.1': + '@tailwindcss/oxide-freebsd-x64@4.3.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.57.1': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.57.1': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.57.1': + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.57.1': + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': optional: true - '@rollup/rollup-linux-x64-musl@4.57.1': + '@tailwindcss/oxide-linux-x64-musl@4.3.0': optional: true - '@rollup/rollup-openbsd-x64@4.57.1': + '@tailwindcss/oxide-wasm32-wasi@4.3.0': optional: true - '@rollup/rollup-openharmony-arm64@4.57.1': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.57.1': + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.57.1': - optional: true + '@tailwindcss/oxide@4.3.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/vite@4.3.0(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0) - '@rollup/rollup-win32-x64-gnu@4.57.1': + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 optional: true - '@rollup/rollup-win32-x64-msvc@4.57.1': + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 optional: true - '@simplewebauthn/server@13.2.2': + '@types/babel__core@7.20.5': dependencies: - '@hexagon/base64': 1.1.28 - '@levischuck/tiny-cbor': 0.2.11 - '@peculiar/asn1-android': 2.6.0 - '@peculiar/asn1-ecc': 2.6.0 - '@peculiar/asn1-rsa': 2.6.0 - '@peculiar/asn1-schema': 2.6.0 - '@peculiar/asn1-x509': 2.6.0 - '@peculiar/x509': 1.14.3 - - '@sindresorhus/is@4.6.0': {} - - '@sindresorhus/is@7.2.0': {} + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 - '@speed-highlight/core@1.2.14': {} + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 - '@standard-schema/spec@1.1.0': {} + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 - '@tybys/wasm-util@0.10.1': + '@types/babel__traverse@7.28.0': dependencies: - tslib: 2.8.1 - optional: true + '@babel/types': 7.29.0 '@types/bun@1.3.8': dependencies: @@ -4328,36 +6172,77 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.1.0-beta.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0))': + '@vitest/expect@4.1.7': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.0-beta.1(vite@7.3.3(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0))': dependencies: '@vitest/spy': 4.1.0-beta.1 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0) + vite: 7.3.3(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) + + '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 4.1.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0) '@vitest/pretty-format@4.1.0-beta.1': dependencies: tinyrainbow: 3.0.3 + '@vitest/pretty-format@4.1.7': + dependencies: + tinyrainbow: 3.1.0 + '@vitest/runner@4.1.0-beta.1': dependencies: '@vitest/utils': 4.1.0-beta.1 pathe: 2.0.3 + '@vitest/runner@4.1.7': + dependencies: + '@vitest/utils': 4.1.7 + pathe: 2.0.3 + '@vitest/snapshot@4.1.0-beta.1': dependencies: '@vitest/pretty-format': 4.1.0-beta.1 magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/snapshot@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + '@vitest/utils': 4.1.7 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/spy@4.1.0-beta.1': {} + '@vitest/spy@4.1.7': {} + '@vitest/utils@4.1.0-beta.1': dependencies: '@vitest/pretty-format': 4.1.0-beta.1 tinyrainbow: 3.0.3 + '@vitest/utils@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -4405,8 +6290,26 @@ snapshots: await-lock@2.2.2: {} + babel-plugin-jsx-dom-expressions@0.40.7(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + html-entities: 2.3.3 + parse5: 7.3.0 + + babel-preset-solid@1.9.12(@babel/core@7.29.0)(solid-js@1.9.13): + dependencies: + '@babel/core': 7.29.0 + babel-plugin-jsx-dom-expressions: 0.40.7(@babel/core@7.29.0) + optionalDependencies: + solid-js: 1.9.13 + base64-js@1.5.1: {} + baseline-browser-mapping@2.10.32: {} + bcryptjs@3.0.3: {} better-path-resolve@1.0.0: @@ -4421,6 +6324,14 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.32 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.361 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -4434,10 +6345,14 @@ snapshots: camelcase@5.3.1: {} + caniuse-lite@1.0.30001793: {} + cborg@1.10.2: {} cborg@4.5.8: {} + cborg@5.1.1: {} + chai@6.2.2: {} chalk@4.1.2: @@ -4496,6 +6411,8 @@ snapshots: consola@3.4.2: {} + convert-source-map@2.0.0: {} + cookie@1.1.1: {} cross-spawn@7.0.6: @@ -4504,8 +6421,14 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.2.3: {} + dataloader@1.4.0: {} + debug@4.4.3: + dependencies: + ms: 2.1.3 + decamelize@1.2.0: {} defu@6.1.4: {} @@ -4526,23 +6449,34 @@ snapshots: optionalDependencies: oxc-resolver: 11.17.0 + electron-to-chromium@1.5.361: {} + emoji-regex@8.0.0: {} emojilib@2.4.0: {} empathic@2.0.0: {} + enhanced-resolve@5.22.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + entities@6.0.1: {} + environment@1.1.0: {} error-stack-parser-es@1.0.5: {} es-module-lexer@1.7.0: {} + es-module-lexer@2.1.0: {} + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -4630,6 +6564,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.3 '@esbuild/win32-x64': 0.27.3 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} esm-env@1.2.2: {} @@ -4670,6 +6633,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + fflate@0.8.2: {} fill-range@7.1.1: @@ -4700,6 +6667,8 @@ snapshots: fsevents@2.3.3: optional: true + gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} get-tsconfig@4.13.6: @@ -4729,12 +6698,16 @@ snapshots: hookable@6.0.1: {} + html-entities@2.3.3: {} + human-id@4.1.3: {} iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 + idb-keyval@6.2.4: {} + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -4755,6 +6728,8 @@ snapshots: dependencies: better-path-resolve: 1.0.0 + is-what@4.1.16: {} + is-windows@1.0.2: {} isexe@2.0.0: {} @@ -4763,8 +6738,14 @@ snapshots: jiti@2.6.1: {} + jiti@2.7.0: {} + jose@6.1.3: {} + jose@6.2.3: {} + + js-tokens@4.0.0: {} + js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -4776,6 +6757,8 @@ snapshots: jsesc@3.1.0: {} + json5@2.2.3: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -4799,6 +6782,55 @@ snapshots: typescript: 5.9.3 zod: 4.3.6 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -4807,6 +6839,10 @@ snapshots: lru-cache@11.2.5: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4824,6 +6860,10 @@ snapshots: marked@9.1.6: {} + merge-anything@5.1.7: + dependencies: + is-what: 4.1.16 + merge2@1.4.1: {} micromatch@4.0.8: @@ -4843,12 +6883,28 @@ snapshots: - bufferutil - utf-8-validate + miniflare@4.20260521.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.24.8 + workerd: 1.20260521.1 + ws: 8.20.1 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimist@1.2.8: {} mri@1.2.0: {} + ms@2.1.3: {} + multiformats@13.4.2: {} + multiformats@14.0.0: {} + multiformats@9.9.0: {} mz@2.7.0: @@ -4859,6 +6915,10 @@ snapshots: nanoid@3.3.11: {} + nanoid@3.3.12: {} + + nanoid@5.1.11: {} + node-emoji@2.2.0: dependencies: '@sindresorhus/is': 4.6.0 @@ -4872,6 +6932,10 @@ snapshots: node-gyp-build@4.8.4: {} + node-releases@2.0.46: {} + + oauth4webapi@3.8.6: {} + object-assign@4.1.1: {} obug@2.1.1: {} @@ -4933,6 +6997,10 @@ snapshots: parse5@6.0.1: {} + parse5@7.3.0: + dependencies: + entities: 6.0.1 + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -4949,6 +7017,8 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: {} + pify@4.0.1: {} pino-abstract-transport@1.2.0: @@ -4974,6 +7044,12 @@ snapshots: pngjs@5.0.0: {} + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -5098,6 +7174,27 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.3 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.3 + rolldown@1.0.2: + dependencies: + '@oxc-project/types': 0.132.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.2 + '@rolldown/binding-darwin-arm64': 1.0.2 + '@rolldown/binding-darwin-x64': 1.0.2 + '@rolldown/binding-freebsd-x64': 1.0.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 + '@rolldown/binding-linux-arm64-gnu': 1.0.2 + '@rolldown/binding-linux-arm64-musl': 1.0.2 + '@rolldown/binding-linux-ppc64-gnu': 1.0.2 + '@rolldown/binding-linux-s390x-gnu': 1.0.2 + '@rolldown/binding-linux-x64-gnu': 1.0.2 + '@rolldown/binding-linux-x64-musl': 1.0.2 + '@rolldown/binding-openharmony-arm64': 1.0.2 + '@rolldown/binding-wasm32-wasi': 1.0.2 + '@rolldown/binding-win32-arm64-msvc': 1.0.2 + '@rolldown/binding-win32-x64-msvc': 1.0.2 + rollup@4.57.1: dependencies: '@types/estree': 1.0.8 @@ -5129,6 +7226,21 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 + rosie-skills-darwin-arm64@0.6.4: + optional: true + + rosie-skills-freebsd-x64@0.6.4: + optional: true + + rosie-skills-linux-x64@0.6.4: + optional: true + + rosie-skills@0.6.4: + optionalDependencies: + rosie-skills-darwin-arm64: 0.6.4 + rosie-skills-freebsd-x64: 0.6.4 + rosie-skills-linux-x64: 0.6.4 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -5143,8 +7255,16 @@ snapshots: safer-buffer@2.1.2: {} + semver@6.3.1: {} + semver@7.7.4: {} + seroval-plugins@1.5.4(seroval@1.5.4): + dependencies: + seroval: 1.5.4 + + seroval@1.5.4: {} + set-blocking@2.0.0: {} sharp@0.34.5: @@ -5198,6 +7318,31 @@ snapshots: smol-toml@1.6.0: {} + solid-js@1.9.13: + dependencies: + csstype: 3.2.3 + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) + + solid-presence@0.1.8(solid-js@1.9.13): + dependencies: + '@corvu/utils': 0.4.2(solid-js@1.9.13) + solid-js: 1.9.13 + + solid-prevent-scroll@0.1.10(solid-js@1.9.13): + dependencies: + '@corvu/utils': 0.4.2(solid-js@1.9.13) + solid-js: 1.9.13 + + solid-refresh@0.6.3(solid-js@1.9.13): + dependencies: + '@babel/generator': 7.29.1 + '@babel/helper-module-imports': 7.28.6 + '@babel/types': 7.29.0 + solid-js: 1.9.13 + transitivePeerDependencies: + - supports-color + sonic-boom@3.8.1: dependencies: atomic-sleep: 1.0.0 @@ -5217,6 +7362,8 @@ snapshots: std-env@3.10.0: {} + std-env@4.1.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5246,6 +7393,10 @@ snapshots: has-flag: 4.0.0 supports-color: 7.2.0 + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + term-size@2.2.1: {} thenify-all@1.6.0: @@ -5264,13 +7415,22 @@ snapshots: tinyexec@1.0.2: {} + tinyexec@1.2.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} + tlds@1.261.0: {} to-regex-range@5.0.1: @@ -5329,6 +7489,8 @@ snapshots: typescript@5.9.3: {} + typescript@6.0.3: {} + uint8arrays@3.0.0: dependencies: multiformats: 9.9.0 @@ -5342,6 +7504,8 @@ snapshots: undici@7.18.2: {} + undici@7.24.8: {} + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -5356,11 +7520,34 @@ snapshots: dependencies: rolldown: 1.0.0-rc.3 + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + valibot@1.4.0(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + validate-npm-package-name@5.0.1: {} varint@6.0.0: {} - vite@6.4.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0): + vite-plugin-solid@2.11.12(solid-js@1.9.13)(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)): + dependencies: + '@babel/core': 7.29.0 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.9.12(@babel/core@7.29.0)(solid-js@1.9.13) + merge-anything: 5.1.7 + solid-js: 1.9.13 + solid-refresh: 0.6.3(solid-js@1.9.13) + vite: 8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0) + vitefu: 1.1.3(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)) + transitivePeerDependencies: + - supports-color + + vite@6.4.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -5371,13 +7558,47 @@ snapshots: optionalDependencies: '@types/node': 24.10.11 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 2.7.0 + lightningcss: 1.32.0 + tsx: 4.21.0 + + vite@7.3.3(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.11 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + tsx: 4.21.0 + + vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.2 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.10.11 + esbuild: 0.27.7 + fsevents: 2.3.3 + jiti: 2.7.0 tsx: 4.21.0 - vitest@4.1.0-beta.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0): + vitefu@1.1.3(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)): + optionalDependencies: + vite: 8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0) + + vitest@4.1.0-beta.1(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0): dependencies: '@vitest/expect': 4.1.0-beta.1 - '@vitest/mocker': 4.1.0-beta.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0)) + '@vitest/mocker': 4.1.0-beta.1(vite@7.3.3(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)) '@vitest/pretty-format': 4.1.0-beta.1 '@vitest/runner': 4.1.0-beta.1 '@vitest/snapshot': 4.1.0-beta.1 @@ -5394,7 +7615,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@24.10.11)(jiti@2.6.1)(tsx@4.21.0) + vite: 7.3.3(@types/node@24.10.11)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.11 @@ -5411,6 +7632,33 @@ snapshots: - tsx - yaml + vitest@4.1.7(@types/node@24.10.11)(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)): + dependencies: + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.14(@types/node@24.10.11)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.10.11 + transitivePeerDependencies: + - msw + walk-up-path@4.0.0: {} webidl-conversions@3.0.1: {} @@ -5439,6 +7687,14 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260205.0 '@cloudflare/workerd-windows-64': 1.20260205.0 + workerd@1.20260521.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260521.1 + '@cloudflare/workerd-darwin-arm64': 1.20260521.1 + '@cloudflare/workerd-linux-64': 1.20260521.1 + '@cloudflare/workerd-linux-arm64': 1.20260521.1 + '@cloudflare/workerd-windows-64': 1.20260521.1 + wrangler@4.63.0(@cloudflare/workers-types@4.20260207.0): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 @@ -5456,6 +7712,24 @@ snapshots: - bufferutil - utf-8-validate + wrangler@4.94.0(@cloudflare/workers-types@4.20260207.0): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260521.1) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260521.0 + path-to-regexp: 6.3.0 + rosie-skills: 0.6.4 + unenv: 2.0.0-rc.24 + workerd: 1.20260521.1 + optionalDependencies: + '@cloudflare/workers-types': 4.20260207.0 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -5472,10 +7746,14 @@ snapshots: ws@8.19.0: {} + ws@8.20.1: {} + y18n@4.0.3: {} y18n@5.0.8: {} + yallist@3.1.1: {} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3c9ea064..c26e1fe5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - "packages/*" - "demos/*" + - "apps/*" From 47f95ceed652dbbce0e3c150cadee2e14681c468 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 24 May 2026 16:44:07 +0100 Subject: [PATCH 2/9] fix(check): handle permission-set expansion in scope-echoed check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permission sets get resolved by the AS into either an echoed include: literal (the grant preserves the reference; AS recomputes at refresh) or expanded resource scopes (a snapshot of the computed permissions). Both are spec-valid — atproto.com/specs/permission doesn't mandate either representation in the token's scope claim. Previously the check flagged the expanded form as a "narrowing" because the include: token was dropped and replaced with repo?collection=X&... tokens. Now it recognizes pure expansion (only include: dropped, scopes added) as a pass with the expansion surfaced as evidence. Also teach scopeGrantsWriteTo about the multi-collection token form (repo?collection=X&collection=Y) that expansion produces, alongside the existing single-collection form (repo:X). Without this, boundary write checks falsely reported no write coverage after a permission set expanded. --- apps/check/src/lib/oauth-flow.ts | 48 ++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/apps/check/src/lib/oauth-flow.ts b/apps/check/src/lib/oauth-flow.ts index a16d3ef1..2de516d5 100644 --- a/apps/check/src/lib/oauth-flow.ts +++ b/apps/check/src/lib/oauth-flow.ts @@ -97,19 +97,35 @@ function clientId(): string { } // Whether the granted scope authorizes a createRecord to a given collection. -// atproto granular scopes for repo are `repo:[?action=...]` with -// default actions covering create+update+delete. `repo:*` grants any collection. +// atproto granular scopes for repo come in two forms: +// `repo:[?action=...]` — single-collection token +// `repo?collection=&collection=` — multi-collection token (produced by +// permission-set expansion) +// Default actions cover create+update+delete. `repo:*` grants any collection. // `transition:generic` is the legacy catch-all. function scopeGrantsWriteTo(grantedScope: string, collection: string): boolean { const parts = grantedScope.split(/\s+/).filter(Boolean); return parts.some((s) => { if (s === "transition:generic") return true; + // Multi-collection form: repo?collection=X&collection=Y[&action=...] + if (s.startsWith("repo?")) { + const params = new URLSearchParams(s.slice("repo?".length)); + const collections = params.getAll("collection"); + const matchesCollection = collections.some( + (c) => c === "*" || c === collection, + ); + if (!matchesCollection) return false; + const actions = params.getAll("action"); + if (actions.length === 0) return true; + return actions.includes("create") || actions.includes("*"); + } + // Single-collection form: repo:[?action=...] const match = s.match(/^repo:([^?]+)(?:\?(.*))?$/); if (!match) return false; const scopeCollection = decodeURIComponent(match[1]!); if (scopeCollection !== "*" && scopeCollection !== collection) return false; const actions = new URLSearchParams(match[2] ?? "").getAll("action"); - if (actions.length === 0) return true; // defaults include create + if (actions.length === 0) return true; return actions.includes("create") || actions.includes("*"); }); } @@ -1441,6 +1457,9 @@ export async function runPostCallback(): Promise { }); // 5b. Scope-echoed: verify the server returned the scope we asked for. + // `include:*` scopes are permission-set references that the AS resolves + // into expanded resource scopes — so if an include: was dropped AND new + // scopes appeared, we treat that as legitimate expansion, not a bug. await runStep("flow.scope-echoed", async () => { const requested = activeScope; const granted = tokenResp.scope ?? ""; @@ -1457,11 +1476,30 @@ export async function runPostCallback(): Promise { const added: string[] = []; for (const s of requestedSet) if (!grantedSet.has(s)) dropped.push(s); for (const s of grantedSet) if (!requestedSet.has(s)) added.push(s); + const droppedIncludes = dropped.filter((s) => s.startsWith("include:")); + const droppedOther = dropped.filter((s) => !s.startsWith("include:")); if (dropped.length === 0 && added.length === 0) { return { status: "pass", message: granted }; } + const isPureExpansion = + droppedOther.length === 0 && + droppedIncludes.length > 0 && + added.length > 0; + if (isPureExpansion) { + return { + status: "pass", + message: `expanded ${droppedIncludes.join(", ")} → ${added.length} resource scope${added.length === 1 ? "" : "s"}`, + evidence: { + expected: requested, + actual: granted, + actualDetail: { + expanded: droppedIncludes, + expansion: added, + }, + }, + }; + } if (dropped.length === 0 && added.length > 0) { - // Server granted MORE than we requested — that's a real conformance bug return { status: "fail", message: `server granted scopes we didn't ask for: ${added.join(", ")}`, @@ -1474,7 +1512,7 @@ export async function runPostCallback(): Promise { } return { status: "warn", - message: `narrower than requested — dropped: ${dropped.join(", ")}`, + message: `narrower than requested — dropped: ${droppedOther.join(", ") || dropped.join(", ")}`, evidence: { expected: requested, actual: granted, From 4ac5e042d27c0e0ddbeea93851c45c7b6ee76e6d Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 24 May 2026 19:40:09 +0100 Subject: [PATCH 3/9] fix(check): align granular-scope discovery checks with actual spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two of the three granular-scope advertisement checks were checking for syntax that pre-dated the final atproto.com/specs/permission spec and flagged spec-conformant ASes as warnings: - scope-phase2-granular looked for repo:read, repo:write:, account.*, pds.* — none of these are in the actual spec. The real grammar uses bare resource-type tokens (repo, rpc, blob, account, identity) with parameters appended at request time by the client. Removed entirely; it was redundant with scope-resource-buckets. - scope-permission-sets looked for include: strings in scopes_supported, but specific NSIDs are dynamically resolved at PAR time via lexicon resolution — the AS only advertises bare `include` as a resource type. Updated to check for that. - scope-resource-buckets reworked to expect all five resource tokens (repo, rpc, blob, identity, account) and warn on partial coverage. Spec URLs now point at atproto.com/specs/permission instead of the obsolete proposal discussion thread. --- apps/check/src/checks/oauth-discovery.ts | 95 +++++++++--------------- apps/check/src/lib/spec-urls.ts | 8 +- 2 files changed, 37 insertions(+), 66 deletions(-) diff --git a/apps/check/src/checks/oauth-discovery.ts b/apps/check/src/checks/oauth-discovery.ts index b4748764..c808a1be 100644 --- a/apps/check/src/checks/oauth-discovery.ts +++ b/apps/check/src/checks/oauth-discovery.ts @@ -563,90 +563,66 @@ const scopeAdvertisesTransitionGeneric = scopeAdvertises( }, ); -const scopeAdvertisesPhase2Granular = scopeAdvertises( - "oauth-discovery.scope-phase2-granular", - "Auth server advertises Phase 2 granular scopes", +const scopeAdvertisesResourceBuckets = scopeAdvertises( + "oauth-discovery.scope-resource-buckets", + "Auth server advertises granular resource scopes", (scopes) => { - const granular = scopes.filter( - (s) => - s === "repo:read" || - s === "repo:write" || - s.startsWith("repo:write:") || - s.startsWith("repo:read:") || - s.startsWith("account.") || - s.startsWith("pds."), - ); - if (granular.length === 0) { + // Per atproto.com/specs/permission, granular scopes are bare resource- + // type tokens (repo, rpc, blob, account, identity) in scopes_supported. + // Clients construct fully-qualified scopes by appending parameters at + // request time (e.g. `repo:my.collection?action=create`). + const RESOURCES = ["repo", "rpc", "blob", "identity", "account"] as const; + const present = RESOURCES.filter((r) => scopes.includes(r)); + if (present.length === 0) { return { status: "warn", message: - "no Phase 2 granular scopes (repo:read, repo:write:, account.*, pds.*) advertised", - evidence: { actual: scopes }, + "no granular resource scopes (repo, rpc, blob, identity, account) advertised — AS supports only legacy transition:* bundles", + evidence: { + expected: + "scopes_supported to include resource-type tokens: repo, rpc, blob, account, identity", + actual: scopes, + }, }; } - return { - status: "pass", - message: `${granular.length} granular scope${granular.length === 1 ? "" : "s"}: ${granular.slice(0, 4).join(", ")}${granular.length > 4 ? "…" : ""}`, - evidence: { actual: granular }, - }; - }, -); - -const scopeAdvertisesResourceBuckets = scopeAdvertises( - "oauth-discovery.scope-resource-buckets", - "Auth server advertises Phase 2 resource-bucket scopes", - (scopes) => { - // Per discussion #4013, the Phase 2 design organizes permissions into - // resource buckets: repo, rpc, blob, identity, account. - const buckets: Record = { - repo: [], - rpc: [], - blob: [], - identity: [], - account: [], - }; - for (const s of scopes) { - for (const bucket of Object.keys(buckets)) { - if (s === bucket || s.startsWith(`${bucket}:`) || s.startsWith(`${bucket}.`)) { - buckets[bucket]!.push(s); - } - } - } - const present = Object.entries(buckets).filter( - ([, v]) => v.length > 0, - ); - if (present.length === 0) { + const missing = RESOURCES.filter((r) => !scopes.includes(r)); + if (missing.length > 0) { return { status: "warn", - message: - "no resource-bucket scopes (repo/rpc/blob/identity/account) advertised", - evidence: { actual: scopes }, + message: `partial: advertises ${present.join(", ")}; missing ${missing.join(", ")}`, + evidence: { actual: { present, missing } }, }; } return { status: "pass", - message: `buckets advertised: ${present.map(([k]) => k).join(", ")}`, - evidence: { actual: Object.fromEntries(present) }, + message: `all five granular resources advertised: ${present.join(", ")}`, + evidence: { actual: present }, }; }, ); const scopeAdvertisesPermissionSets = scopeAdvertises( "oauth-discovery.scope-permission-sets", - "Auth server advertises permission set scopes", + "Auth server advertises permission set support", (scopes) => { - const sets = scopes.filter((s) => s.startsWith("include:")); - if (sets.length === 0) { + // The AS advertises `include` as a resource type to signal it supports + // permission sets; specific include: scopes are dynamically + // resolved at PAR time via lexicon resolution, not enumerated here. + if (!scopes.includes("include")) { return { status: "warn", - message: "no permission set scopes (include:*) advertised", - evidence: { actual: scopes }, + message: + "`include` not in scopes_supported — AS does not advertise permission set resolution", + evidence: { + expected: "`include` token in scopes_supported", + actual: scopes, + }, }; } return { status: "pass", - message: `${sets.length} permission set${sets.length === 1 ? "" : "s"}: ${sets.join(", ")}`, - evidence: { actual: sets }, + message: "`include` advertised — AS resolves permission sets dynamically via lexicon resolution", + evidence: { actual: ["include"] }, }; }, ); @@ -658,7 +634,6 @@ export const oauthDiscoveryChecks: Check[] = [ authServerValidates, scopeAdvertisesAtproto, scopeAdvertisesTransitionGeneric, - scopeAdvertisesPhase2Granular, scopeAdvertisesResourceBuckets, scopeAdvertisesPermissionSets, jwksResponds, diff --git a/apps/check/src/lib/spec-urls.ts b/apps/check/src/lib/spec-urls.ts index 534d036d..9cb430fc 100644 --- a/apps/check/src/lib/spec-urls.ts +++ b/apps/check/src/lib/spec-urls.ts @@ -68,12 +68,8 @@ const MAP: Record = { "oauth.jwks-validates": `${RFC}/rfc7517`, "oauth-discovery.scope-atproto": `${SPECS}/oauth#scopes`, "oauth-discovery.scope-transition-generic": `${SPECS}/oauth#scopes`, - "oauth-discovery.scope-phase2-granular": - "https://github.com/bluesky-social/atproto/discussions/4013", - "oauth-discovery.scope-resource-buckets": - "https://github.com/bluesky-social/atproto/discussions/4013", - "oauth-discovery.scope-permission-sets": - "https://github.com/bluesky-social/atproto/discussions/4013", + "oauth-discovery.scope-resource-buckets": `${SPECS}/permission`, + "oauth-discovery.scope-permission-sets": `${SPECS}/permission#permission-sets`, // account "account.get-session": `${LEX}/com/atproto/server/getSession.json`, From 6e6f87c08808e94d70b94e6d309e3a86551b4335 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 24 May 2026 19:44:09 +0100 Subject: [PATCH 4/9] feat(check): pivot to other check modes from OAuth result page The OAuth conformance result only offered "verify a different account", forcing users back to the landing page when they wanted to also run read-only or write tests against the same target. Add the same pivot buttons RunView already shows. The handlers exit the flow first (clearing flow state and signing out), then start the requested run mode against the OAuth target. --- apps/check/src/App.tsx | 26 +++++++++++ apps/check/src/components/OAuthFlowView.tsx | 52 +++++++++++++++++---- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/apps/check/src/App.tsx b/apps/check/src/App.tsx index 89cd84b8..bf7d82d4 100644 --- a/apps/check/src/App.tsx +++ b/apps/check/src/App.tsx @@ -270,6 +270,19 @@ export function App() { state={state()} onExit={exitFlow} onRedirect={continueFlowRedirect} + onReadChecks={() => { + const v = state().target.trim(); + exitFlow(); + if (v) void beginRun(v, anonymousChecks, "verify"); + }} + onWriteTests={() => { + const v = state().target.trim(); + exitFlow(); + if (v) { + setTarget(v); + void startWriteTests(); + } + }} /> )}
    @@ -324,6 +337,19 @@ export function App() { state={flowBoot().state} onExit={exitFlow} onRedirect={continueFlowRedirect} + onReadChecks={() => { + const v = flowBoot().state.target.trim(); + exitFlow(); + if (v) void beginRun(v, anonymousChecks, "verify"); + }} + onWriteTests={() => { + const v = flowBoot().state.target.trim(); + exitFlow(); + if (v) { + setTarget(v); + void startWriteTests(); + } + }} /> )} diff --git a/apps/check/src/components/OAuthFlowView.tsx b/apps/check/src/components/OAuthFlowView.tsx index 598e5fef..b808286a 100644 --- a/apps/check/src/components/OAuthFlowView.tsx +++ b/apps/check/src/components/OAuthFlowView.tsx @@ -274,6 +274,8 @@ export function OAuthFlowView(props: { state: FlowState; onExit: () => void; onRedirect?: () => void; + onReadChecks?: () => void; + onWriteTests?: () => void; }) { const summary = createMemo(() => { let pass = 0; @@ -374,16 +376,46 @@ export function OAuthFlowView(props: {
    what next
    - +
    + + + + + + + +
    From 59cf5f6d4abcaa7a568f0fd4e7ebe7baf7c2c35b Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 24 May 2026 19:46:32 +0100 Subject: [PATCH 5/9] fix(check): drop grade letter, keep score --- apps/check/src/components/RunView.tsx | 32 +++++---------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/apps/check/src/components/RunView.tsx b/apps/check/src/components/RunView.tsx index a22b2543..c8cc8a8f 100644 --- a/apps/check/src/components/RunView.tsx +++ b/apps/check/src/components/RunView.tsx @@ -9,19 +9,6 @@ import { } from "../types"; import { CheckRow } from "./CheckRow"; -function gradeFor(pass: number, total: number): string { - if (total === 0) return "—"; - const pct = pass / total; - if (pct === 1) return "A+"; - if (pct >= 0.95) return "A"; - if (pct >= 0.9) return "A-"; - if (pct >= 0.85) return "B+"; - if (pct >= 0.8) return "B"; - if (pct >= 0.7) return "C"; - if (pct >= 0.5) return "D"; - return "F"; -} - function downloadString(filename: string, content: string, mime: string): void { const blob = new Blob([content], { type: mime }); const url = URL.createObjectURL(blob); @@ -392,7 +379,6 @@ function ResultSummary(props: { props.run.endedAt && props.run.startedAt ? `${((props.run.endedAt - props.run.startedAt) / 1000).toFixed(1)}s` : ""; - const grade = () => gradeFor(props.summary.pass, props.summary.applicable); const skipped = () => props.summary.total - props.summary.applicable; async function copyLink() { @@ -422,7 +408,11 @@ function ResultSummary(props: {
    Score
    -
    +
    {props.summary.pass} / {props.summary.applicable}
    0}> @@ -431,18 +421,6 @@ function ResultSummary(props: {
    -
    -
    - Grade -
    -
    - {grade()} -
    -
    From b482c3ea4a20b4dfc513680c53896d84a4021313 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 24 May 2026 20:13:49 +0100 Subject: [PATCH 6/9] feat(check): sample live firehose during write probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The anonymous firehose sample is historical (cursor=0), so on long-lived PDSes every #commit frame can predate the Sync 1.1 upgrade — producing false fails on `commit-has-prevdata` and `commit-ops-have-prev`. Two changes: - Anonymous: downgrade the strict pass/fail to a tri-state. Any sampled frame with prevData → pass. None at all → warn (ambiguous: PDS may not support Sync 1.1, or the sample may predate the upgrade). Same shape for ops[].prev on update/delete. - Authenticated write probe: subscribe to the firehose with no cursor before the create/applyWrites/uploadBlob/delete run, then close + validate after. Fresh frames go through the same validators in strict mode, giving a definitive Sync 1.1 verdict. --- apps/check/src/checks/firehose.ts | 273 ++++++++++++++++++++++++++++-- apps/check/src/checks/index.ts | 12 +- 2 files changed, 267 insertions(+), 18 deletions(-) diff --git a/apps/check/src/checks/firehose.ts b/apps/check/src/checks/firehose.ts index 9eebfd25..db3ff820 100644 --- a/apps/check/src/checks/firehose.ts +++ b/apps/check/src/checks/firehose.ts @@ -19,23 +19,30 @@ interface DecodeFailure { error: string; } +type SampleMode = "history" | "live" | "none"; + let collectedFrames: Frame[] = []; let decodeFailures: DecodeFailure[] = []; let collectionAttempted = false; let collectionTerminationReason = ""; let collectionElapsedMs = 0; +let sampleMode: SampleMode = "none"; +let liveWs: WebSocket | null = null; +let liveStartedAt = 0; +let liveFrameIndex = 0; const FRAME_TARGET = 200; const COLLECT_TIMEOUT_MS = 8000; const CONNECT_TIMEOUT_MS = 5000; const INACTIVITY_TIMEOUT_MS = 1500; const MIN_FRAMES_BEFORE_DIVERSITY_EXIT = 50; +const LIVE_TAIL_QUIESCE_MS = 750; -function wsUrlFor(pds: string): string { +function wsUrlFor(pds: string, opts: { cursor?: number } = {}): string { const url = new URL(pds); url.protocol = url.protocol === "http:" ? "ws:" : "wss:"; url.pathname = "/xrpc/com.atproto.sync.subscribeRepos"; - url.search = "?cursor=0"; + url.search = opts.cursor === undefined ? "" : `?cursor=${opts.cursor}`; return url.toString(); } @@ -65,12 +72,13 @@ const connect: Check = { collectionAttempted = false; collectionTerminationReason = ""; collectionElapsedMs = 0; + sampleMode = "none"; if (!ctx.pds) { return { status: "skip", message: "No PDS endpoint" }; } - const url = wsUrlFor(ctx.pds); + const url = wsUrlFor(ctx.pds, { cursor: 0 }); let ws: WebSocket; try { ws = new WebSocket(url); @@ -221,6 +229,7 @@ const connect: Check = { collectionAttempted = true; collectionTerminationReason = terminationReason; collectionElapsedMs = Date.now() - collectionStartedAt; + sampleMode = "history"; return { status: "pass", @@ -316,7 +325,7 @@ const commitHasPrevData: Check = { category: "firehose", label: "#commit frames include prevData", description: - "Every #commit event must carry the previous MST root CID as prevData (atproto Sync 1.1).", + "Every #commit event must carry the previous MST root CID as prevData (atproto Sync 1.1). Strict on live samples; informational on historical replay since pre-upgrade events are retained in the firehose.", requires: ["pds"], run: async (): Promise => { if (!collectionAttempted) { @@ -326,12 +335,23 @@ const commitHasPrevData: Check = { if (commits.length === 0) { return { status: "skip", message: "No #commit frames observed" }; } - const missing = commits.filter((f) => f.body.prevData === undefined); - if (missing.length > 0) { - const offending = missing[0]!; + const withPrev = commits.filter((f) => f.body.prevData !== undefined); + const missing = commits.length - withPrev.length; + + if (missing === 0) { + return { + status: "pass", + message: `All ${commits.length} #commit frame${ + commits.length === 1 ? "" : "s" + } include prevData`, + }; + } + + if (sampleMode === "live") { + const offending = commits.find((f) => f.body.prevData === undefined)!; return { status: "fail", - message: `${missing.length}/${commits.length} #commit frame${ + message: `${missing}/${commits.length} live #commit frame${ commits.length === 1 ? "" : "s" } missing prevData — required by atproto Sync 1.1`, evidence: { @@ -343,11 +363,25 @@ const commitHasPrevData: Check = { }, }; } + + // History sample: any prevData → pass; none → warn (ambiguous: could be + // pre-Sync 1.1 PDS, or pre-upgrade events retained in the firehose). + if (withPrev.length > 0) { + return { + status: "pass", + message: `${withPrev.length}/${commits.length} sampled #commit frames include prevData (rest may predate the Sync 1.1 upgrade)`, + }; + } return { - status: "pass", - message: `All ${commits.length} #commit frame${ - commits.length === 1 ? "" : "s" - } include prevData`, + status: "warn", + message: `No #commit frames in the historical sample carry prevData (${commits.length}/${commits.length} missing) — could not confirm Sync 1.1 support. Sign in to run the live write probe, or trigger a fresh write and re-run.`, + evidence: { + expected: "body.prevData present on at least one sampled #commit", + actual: { + header: commits[0]!.header, + bodyKeys: Object.keys(commits[0]!.body), + }, + }, }; }, }; @@ -661,10 +695,17 @@ const commitOpsHavePrev: Check = { message: "No update/delete ops in sampled commits — only creates", }; } - if (missingPrev > 0) { + const withPrev = updateDeleteOps - missingPrev; + if (missingPrev === 0) { + return { + status: "pass", + message: `All ${updateDeleteOps} update/delete op${updateDeleteOps === 1 ? "" : "s"} carry prev`, + }; + } + if (sampleMode === "live") { return { status: "fail", - message: `${missingPrev}/${updateDeleteOps} update/delete op${ + message: `${missingPrev}/${updateDeleteOps} live update/delete op${ updateDeleteOps === 1 ? "" : "s" } missing prev — required for inductive firehose (Sync 1.1)`, evidence: { @@ -674,9 +715,20 @@ const commitOpsHavePrev: Check = { }, }; } + if (withPrev > 0) { + return { + status: "pass", + message: `${withPrev}/${updateDeleteOps} sampled update/delete ops carry prev (rest may predate the Sync 1.1 upgrade)`, + }; + } return { - status: "pass", - message: `All ${updateDeleteOps} update/delete op${updateDeleteOps === 1 ? "" : "s"} carry prev`, + status: "warn", + message: `No sampled update/delete ops carry prev (${missingPrev}/${updateDeleteOps} missing) — could not confirm Sync 1.1 support. Sign in to run the live write probe, or trigger a fresh write and re-run.`, + evidence: { + expected: + "at least one #repoOp with action=update|delete carries prev", + actual: firstOffending, + }, }; }, }; @@ -741,6 +793,179 @@ const emitsIdentityEvents = eventPresenceCheck( "#identity", ); +const liveListenStart: Check = { + id: "firehose.live-listen-start", + category: "firehose", + label: "Subscribe to firehose (live tail)", + description: + "Open subscribeRepos with no cursor before the write probe so fresh #commit events can be sampled. Frames buffer in the background while subsequent write checks run.", + requires: ["pds", "session"], + run: async (ctx): Promise => { + collectedFrames = []; + decodeFailures = []; + collectionAttempted = false; + collectionTerminationReason = ""; + collectionElapsedMs = 0; + sampleMode = "none"; + liveFrameIndex = 0; + + if (liveWs) { + try { + liveWs.close(); + } catch { + // ignore + } + liveWs = null; + } + + if (!ctx.pds) { + return { status: "skip", message: "No PDS endpoint" }; + } + + const url = wsUrlFor(ctx.pds); + let ws: WebSocket; + try { + ws = new WebSocket(url); + } catch (error) { + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { request: { method: "WS", url }, error: String(error) }, + }; + } + ws.binaryType = "arraybuffer"; + + try { + await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error(`Connection timed out after ${CONNECT_TIMEOUT_MS}ms`)); + }, CONNECT_TIMEOUT_MS); + ws.addEventListener( + "open", + () => { + clearTimeout(timer); + resolve(); + }, + { once: true }, + ); + ws.addEventListener( + "error", + () => { + clearTimeout(timer); + reject(new Error("WebSocket error before open")); + }, + { once: true }, + ); + ws.addEventListener( + "close", + (ev) => { + clearTimeout(timer); + reject( + new Error( + `WebSocket closed before open: code=${ev.code} reason=${ev.reason || "(none)"}`, + ), + ); + }, + { once: true }, + ); + }); + } catch (error) { + try { + ws.close(); + } catch { + // ignore + } + return { + status: "fail", + message: error instanceof Error ? error.message : String(error), + evidence: { request: { method: "WS", url }, error: String(error) }, + }; + } + + ws.addEventListener("message", (event) => { + const data = event.data; + if (!(data instanceof ArrayBuffer)) return; + const bytes = new Uint8Array(data); + const i = liveFrameIndex++; + try { + const [header, rest] = decodeFirst(bytes); + const [body] = decodeFirst(rest); + collectedFrames.push({ + header: header as FrameHeader, + body: (body ?? {}) as Record, + raw: bytes, + }); + } catch (error) { + decodeFailures.push({ + index: i, + error: error instanceof Error ? error.message : String(error), + }); + } + }); + + liveWs = ws; + liveStartedAt = Date.now(); + sampleMode = "live"; + + return { + status: "pass", + message: `Subscribed to ${url} — buffering frames during the write probe`, + evidence: { request: { method: "WS", url } }, + }; + }, +}; + +const liveListenEnd: Check = { + id: "firehose.live-listen-end", + category: "firehose", + label: "Capture live firehose frames", + description: `After the write probe, give the firehose ${LIVE_TAIL_QUIESCE_MS}ms to deliver any final frames, then close the subscription and run Sync 1.1 validators against the captured sample.`, + requires: ["pds", "session"], + run: async (): Promise => { + if (!liveWs) { + return { status: "skip", message: "Live listen did not start" }; + } + + await new Promise((resolve) => setTimeout(resolve, LIVE_TAIL_QUIESCE_MS)); + + try { + liveWs.close(1000, "complete"); + } catch { + // ignore + } + liveWs = null; + + collectionElapsedMs = Date.now() - liveStartedAt; + collectionAttempted = true; + collectionTerminationReason = "write-probe-complete"; + + const total = collectedFrames.length + decodeFailures.length; + if (total === 0) { + return { + status: "warn", + message: `No frames received during the write probe (${collectionElapsedMs}ms) — Sync 1.1 validators will skip`, + evidence: { + actual: { frames: 0, elapsedMs: collectionElapsedMs }, + }, + }; + } + return { + status: "pass", + message: `Captured ${total} live frame${total === 1 ? "" : "s"} in ${collectionElapsedMs}ms`, + evidence: { + actual: { + frames: total, + decoded: collectedFrames.length, + types: countHeaderTypes(collectedFrames), + }, + }, + }; + }, +}; + +// Anonymous flow: history replay via cursor=0. Strict Sync 1.1 checks are +// downgraded to "warn" when no live frames are available (see commitHasPrevData +// / commitOpsHavePrev — they branch on sampleMode). export const firehoseChecks: Check[] = [ connect, collectFrames, @@ -756,3 +981,19 @@ export const firehoseChecks: Check[] = [ accountEventShape, emitsIdentityEvents, ]; + +// Live-tail probe: open WS before writes (firehoseLiveStartChecks), let the +// write probe produce events, then close + validate (firehoseLiveEndChecks). +export const firehoseLiveStartChecks: Check[] = [liveListenStart]; + +export const firehoseLiveEndChecks: Check[] = [ + liveListenEnd, + frameDecodes, + commitHasPrevData, + commitBlocksIsValidCar, + commitOpsHavePrev, + commitDeprecatedTooBig, + commitDeprecatedBlobs, + commitDeprecatedRebase, + accountEventShape, +]; diff --git a/apps/check/src/checks/index.ts b/apps/check/src/checks/index.ts index 5e6eb6a6..36e5ad8a 100644 --- a/apps/check/src/checks/index.ts +++ b/apps/check/src/checks/index.ts @@ -1,7 +1,11 @@ import type { Check } from "../types"; import { accountChecks } from "./account"; import { blobsChecks } from "./blobs"; -import { firehoseChecks } from "./firehose"; +import { + firehoseChecks, + firehoseLiveEndChecks, + firehoseLiveStartChecks, +} from "./firehose"; import { identityChecks } from "./identity"; import { oauthDiscoveryChecks } from "./oauth-discovery"; import { repoReadChecks } from "./repo-read"; @@ -21,9 +25,13 @@ export const anonymousChecks: readonly Check[] = [ ]; // Write tests — gated by sign-in AND an explicit confirmation step. -// Includes identity (to populate ctx.pds/ctx.did) + account (verify session) + the actual writes. +// Subscribes to the firehose live tail before the write probe so the +// create/applyWrites/delete operations produce a fresh sample that strictly +// validates Sync 1.1 (prevData, ops[].prev). export const writeChecks: readonly Check[] = [ ...identityChecks, ...accountChecks, + ...firehoseLiveStartChecks, ...repoWriteChecks, + ...firehoseLiveEndChecks, ]; From 1f90e167611612749cdbc5bfe63444aa0ccd6c96 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 24 May 2026 20:28:14 +0100 Subject: [PATCH 7/9] Fix docs builds --- demos/pds/package.json | 2 +- docs/.gitignore | 6 + docs/astro.config.mjs | 8 +- docs/package.json | 15 +- docs/tsconfig.json | 12 +- docs/wrangler.jsonc | 12 + package.json | 8 +- packages/pds/e2e/fixture/package.json | 2 +- packages/pds/package.json | 6 +- pnpm-lock.yaml | 510 ++++++++++---------------- 10 files changed, 247 insertions(+), 334 deletions(-) create mode 100644 docs/wrangler.jsonc diff --git a/demos/pds/package.json b/demos/pds/package.json index b316b96e..71fce226 100644 --- a/demos/pds/package.json +++ b/demos/pds/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@cloudflare/vite-plugin": "^1.17.0", "vite": "^6.4.1", - "wrangler": "^4.54.0" + "wrangler": "^4.93.0" }, "scripts": { "dev": "vite dev", diff --git a/docs/.gitignore b/docs/.gitignore index 6240da8b..8b1b8a6d 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -19,3 +19,9 @@ pnpm-debug.log* # macOS-specific files .DS_Store + +# wrangler files +.wrangler +.dev.vars* +!.dev.vars.example +!.env.example diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 3e9c220d..3bba444e 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -4,7 +4,8 @@ import starlight from "@astrojs/starlight"; // https://astro.build/config export default defineConfig({ - site: "https://getcirrus.dev", + site: "https://cirrus.cloud", + integrations: [ starlight({ title: "Cirrus", @@ -79,7 +80,10 @@ export default defineConfig({ slug: "guides/app-password", }, { label: "Update a deployed PDS", slug: "guides/update" }, - { label: "Troubleshoot common errors", slug: "guides/troubleshoot" }, + { + label: "Troubleshoot common errors", + slug: "guides/troubleshoot", + }, ], }, { diff --git a/docs/package.json b/docs/package.json index 637e25a1..1b835bf9 100644 --- a/docs/package.json +++ b/docs/package.json @@ -6,13 +6,22 @@ "dev": "astro dev", "start": "astro dev", "build": "astro build", - "preview": "astro preview", - "astro": "astro" + "preview": "pnpm build && wrangler dev", + "astro": "astro", + "generate-types": "wrangler types", + "deploy": "pnpm build && wrangler deploy", + "cf-typegen": "wrangler types" }, "dependencies": { "@astrojs/starlight": "^0.39.2", "astro": "^6.3.1", "hls.js": "^1.6.16", "plyr": "^3.8.4" + }, + "devDependencies": { + "wrangler": "^4.93.0" + }, + "overrides": { + "vite": "^7" } -} \ No newline at end of file +} diff --git a/docs/tsconfig.json b/docs/tsconfig.json index 8bf91d3b..41f5e7f2 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -1,5 +1,11 @@ { "extends": "astro/tsconfigs/strict", - "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist"] -} + "include": [ + ".astro/types.d.ts", + "**/*", + "./worker-configuration.d.ts" + ], + "exclude": [ + "dist" + ] +} \ No newline at end of file diff --git a/docs/wrangler.jsonc b/docs/wrangler.jsonc new file mode 100644 index 00000000..9e4d05b5 --- /dev/null +++ b/docs/wrangler.jsonc @@ -0,0 +1,12 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "cirrusdocs", + "compatibility_date": "2025-05-24", + "assets": { + "directory": "./dist", + "not_found_handling": "404-page" + }, + "observability": { + "enabled": true + } +} \ No newline at end of file diff --git a/package.json b/package.json index 99953f8d..22b26269 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,5 @@ "typescript": "^5.9.3", "vitest": "4.1.0-beta.1" }, - "packageManager": "pnpm@10.26.2", - "pnpm": { - "overrides": { - "wrangler": "^4.63.0", - "miniflare": "^4.20260205.0" - } - } + "packageManager": "pnpm@10.26.2" } diff --git a/packages/pds/e2e/fixture/package.json b/packages/pds/e2e/fixture/package.json index 3f6900ac..3da3b58f 100644 --- a/packages/pds/e2e/fixture/package.json +++ b/packages/pds/e2e/fixture/package.json @@ -12,6 +12,6 @@ "devDependencies": { "@cloudflare/vite-plugin": "^1.17.0", "vite": "^6.4.1", - "wrangler": "^4.54.0" + "wrangler": "^4.93.0" } } diff --git a/packages/pds/package.json b/packages/pds/package.json index 1cabb968..06aacab5 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -50,8 +50,8 @@ "@arethetypeswrong/cli": "^0.18.2", "@atproto/api": "^0.18.9", "@cloudflare/vite-plugin": "^1.17.0", - "@cloudflare/vitest-pool-workers": "https://pkg.pr.new/@cloudflare/vitest-pool-workers@79a1932669f44a24342c1ffd66c29decd8a8fee9", - "@cloudflare/workers-types": "^4.20251225.0", + "@cloudflare/vitest-pool-workers": "^0.16.9", + "@cloudflare/workers-types": "^4.20260524.1", "@ipld/car": "^5.4.2", "@types/qrcode": "^1.5.6", "@types/ws": "^8.18.1", @@ -61,7 +61,7 @@ "typescript": "^5.9.3", "vite": "^6.4.1", "vitest": "4.1.0-beta.1", - "wrangler": "^4.54.0", + "wrangler": "^4.93.0", "ws": "^8.18.3" }, "repository": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c43faa4..d7938d67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,10 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - wrangler: ^4.63.0 - miniflare: ^4.20260205.0 - importers: .: @@ -110,8 +106,8 @@ importers: specifier: 4.1.7 version: 4.1.7(@types/node@24.10.11)(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) wrangler: - specifier: ^4.63.0 - version: 4.63.0(@cloudflare/workers-types@4.20260207.0) + specifier: ^4.94.0 + version: 4.94.0(@cloudflare/workers-types@4.20260524.1) demos/pds: dependencies: @@ -121,13 +117,13 @@ importers: devDependencies: '@cloudflare/vite-plugin': specifier: ^1.17.0 - version: 1.23.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))(workerd@1.20260205.0)(wrangler@4.63.0(@cloudflare/workers-types@4.20260207.0)) + version: 1.23.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))(workerd@1.20260521.1)(wrangler@4.94.0(@cloudflare/workers-types@4.20260524.1)) vite: specifier: ^6.4.1 version: 6.4.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) wrangler: - specifier: ^4.63.0 - version: 4.63.0(@cloudflare/workers-types@4.20260207.0) + specifier: ^4.93.0 + version: 4.94.0(@cloudflare/workers-types@4.20260524.1) docs: dependencies: @@ -143,6 +139,10 @@ importers: plyr: specifier: ^3.8.4 version: 3.8.4 + devDependencies: + wrangler: + specifier: ^4.93.0 + version: 4.94.0(@cloudflare/workers-types@4.20260524.1) packages/create-pds: devDependencies: @@ -291,13 +291,13 @@ importers: version: 0.18.20 '@cloudflare/vite-plugin': specifier: ^1.17.0 - version: 1.23.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))(workerd@1.20260205.0)(wrangler@4.63.0(@cloudflare/workers-types@4.20260207.0)) + version: 1.23.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))(workerd@1.20260521.1)(wrangler@4.94.0(@cloudflare/workers-types@4.20260524.1)) '@cloudflare/vitest-pool-workers': - specifier: https://pkg.pr.new/@cloudflare/vitest-pool-workers@79a1932669f44a24342c1ffd66c29decd8a8fee9 - version: https://pkg.pr.new/@cloudflare/vitest-pool-workers@79a1932669f44a24342c1ffd66c29decd8a8fee9(@cloudflare/workers-types@4.20260207.0)(@vitest/runner@4.1.7)(@vitest/snapshot@4.1.7)(vitest@4.1.0-beta.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) + specifier: ^0.16.9 + version: 0.16.9(@cloudflare/workers-types@4.20260524.1)(@vitest/runner@4.1.7)(@vitest/snapshot@4.1.7)(vitest@4.1.0-beta.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)) '@cloudflare/workers-types': - specifier: ^4.20251225.0 - version: 4.20260207.0 + specifier: ^4.20260524.1 + version: 4.20260524.1 '@ipld/car': specifier: ^5.4.2 version: 5.4.2 @@ -326,8 +326,8 @@ importers: specifier: 4.1.0-beta.1 version: 4.1.0-beta.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) wrangler: - specifier: ^4.63.0 - version: 4.63.0(@cloudflare/workers-types@4.20260207.0) + specifier: ^4.93.0 + version: 4.94.0(@cloudflare/workers-types@4.20260524.1) ws: specifier: ^8.18.3 version: 8.19.0 @@ -754,9 +754,9 @@ packages: resolution: {integrity: sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==} engines: {node: '>= 20.12.0'} - '@cloudflare/kv-asset-handler@0.4.2': - resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} - engines: {node: '>=18.0.0'} + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} '@cloudflare/unenv-preset@2.12.0': resolution: {integrity: sha512-NK4vN+2Z/GbfGS4BamtbbVk1rcu5RmqaYGiyHJQrA09AoxdZPHDF3W/EhgI0YSK8p3vRo/VNCtbSJFPON7FWMQ==} @@ -767,19 +767,27 @@ packages: workerd: optional: true + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + '@cloudflare/vite-plugin@1.23.1': resolution: {integrity: sha512-TnE2+U0xM8QWQBC5SlthtIPyit9j6RD7YB0I61jRj28fU4beBH3zYoNXcmHjnhSVU6Y//gIg2xrGV4jXIvdwXw==} peerDependencies: vite: ^6.1.0 || ^7.0.0 wrangler: ^4.63.0 - '@cloudflare/vitest-pool-workers@https://pkg.pr.new/@cloudflare/vitest-pool-workers@79a1932669f44a24342c1ffd66c29decd8a8fee9': - resolution: {tarball: https://pkg.pr.new/@cloudflare/vitest-pool-workers@79a1932669f44a24342c1ffd66c29decd8a8fee9} - version: 0.12.6 + '@cloudflare/vitest-pool-workers@0.16.9': + resolution: {integrity: sha512-qdohrJbLhuB3mq3j/Vg4nHQU+Gw0ZhQErlZ7xr9A2VpP1F4QzCewwwJmWZnXlwU2rMbvGVwwOD91Eb39EvfQmQ==} peerDependencies: - '@vitest/runner': 4.1.0-beta.1 - '@vitest/snapshot': 4.1.0-beta.1 - vitest: 4.1.0-beta.1 + '@vitest/runner': ^4.1.0 + '@vitest/snapshot': ^4.1.0 + vitest: ^4.1.0 '@cloudflare/workerd-darwin-64@1.20260205.0': resolution: {integrity: sha512-ToOItqcirmWPwR+PtT+Q4bdjTn/63ZxhJKEfW4FNn7FxMTS1Tw5dml0T0mieOZbCpcvY8BdvPKFCSlJuI8IVHQ==} @@ -787,33 +795,66 @@ packages: cpu: [x64] os: [darwin] + '@cloudflare/workerd-darwin-64@1.20260521.1': + resolution: {integrity: sha512-aiNdXmxlhwGjTSajL3I7uQPpN4lAOcXjvg5ZOlJKIywnevr798n9XCS6lvuqgniM3KjurBNWRRypMJntg/eSLg==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20260205.0': resolution: {integrity: sha512-402ZqLz+LrG0NDXp7Hn7IZbI0DyhjNfjAlVenb0K3yod9KCuux0u3NksNBvqJx0mIGHvVR4K05h+jfT5BTHqGA==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20260521.1': + resolution: {integrity: sha512-ikN8aKSi4Ak28ndOkuSO5rq6lmV6wwDQu9F9Vu6J7EkwAOth74J/Hjn4j4EuFceW/npw2Ws0Y/muzA6WKHl4TA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + '@cloudflare/workerd-linux-64@1.20260205.0': resolution: {integrity: sha512-rz9jBzazIA18RHY+osa19hvsPfr0LZI1AJzIjC6UqkKKphcTpHBEQ25Xt8cIA34ivMIqeENpYnnmpDFesLkfcQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] + '@cloudflare/workerd-linux-64@1.20260521.1': + resolution: {integrity: sha512-D/gUhvQcG0pJr5aJl6yUoi2JxbFpjVtDq9xUJHPjfkAjL28TUVgCR/e5r8YGirepv4I1DK7ihuii9LZ2GGMJbw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + '@cloudflare/workerd-linux-arm64@1.20260205.0': resolution: {integrity: sha512-jr6cKpMM/DBEbL+ATJ9rYue758CKp0SfA/nXt5vR32iINVJrb396ye9iat2y9Moa/PgPKnTrFgmT6urUmG3IUg==} engines: {node: '>=16'} cpu: [arm64] os: [linux] + '@cloudflare/workerd-linux-arm64@1.20260521.1': + resolution: {integrity: sha512-vhjWPIHenczegTakhRPwEmTeaavCpNqsuo3RlLCkUdU47HrwLvy/4QersGggs4+kF4Do+IE/EznCGyT40xYcLA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + '@cloudflare/workerd-windows-64@1.20260205.0': resolution: {integrity: sha512-SMPW5jCZYOG7XFIglSlsgN8ivcl0pCrSAYxCwxtWvZ88whhcDB/aISNtiQiDZujPH8tIo2hE5dEkxW7tGEwc3A==} engines: {node: '>=16'} cpu: [x64] os: [win32] + '@cloudflare/workerd-windows-64@1.20260521.1': + resolution: {integrity: sha512-wBolYC/+lnGIEbkkPdzFtjTOWip2uQH6maeAP1ZV0kyxi5SGpsa83+wD5rH5OOle+sHE5qJMdwCKjwRwj+FKJg==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + '@cloudflare/workers-types@4.20260207.0': resolution: {integrity: sha512-PSxgnAOK0EtTytlY7/+gJcsQJYg0Qo7KlOMSC/wiBE+pBqKjuKdd1ZgM+NvpPNqZAjWV5jqAMTTNYEmgk27gYw==} + '@cloudflare/workers-types@4.20260524.1': + resolution: {integrity: sha512-9o939wce6hAlfNAIG4W58bySW7twgkqgPkxmSNrk/DV0eEexjTPyqChGdsQeKZEXJAjxSUFklaebMYJWg1GM0g==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -855,12 +896,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.0': - resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} @@ -873,12 +908,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.0': - resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.27.3': resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} @@ -891,12 +920,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.0': - resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.27.3': resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} @@ -909,12 +932,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.0': - resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.27.3': resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} @@ -927,12 +944,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.0': - resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.27.3': resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} @@ -945,12 +956,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.0': - resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.27.3': resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} @@ -963,12 +968,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.0': - resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.27.3': resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} @@ -981,12 +980,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.0': - resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.27.3': resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} @@ -999,12 +992,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.0': - resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.27.3': resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} @@ -1017,12 +1004,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.0': - resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.27.3': resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} @@ -1035,12 +1016,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.0': - resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.27.3': resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} @@ -1053,12 +1028,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.0': - resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.27.3': resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} @@ -1071,12 +1040,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.0': - resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.27.3': resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} @@ -1089,12 +1052,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.0': - resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.27.3': resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} @@ -1107,12 +1064,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.0': - resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.27.3': resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} @@ -1125,12 +1076,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.0': - resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.27.3': resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} @@ -1143,12 +1088,6 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.0': - resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.27.3': resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} @@ -1161,12 +1100,6 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.27.0': - resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.27.3': resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} engines: {node: '>=18'} @@ -1179,12 +1112,6 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.0': - resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.27.3': resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} @@ -1197,12 +1124,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.27.0': - resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.27.3': resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} engines: {node: '>=18'} @@ -1215,12 +1136,6 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.0': - resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.27.3': resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} @@ -1233,12 +1148,6 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.27.0': - resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/openharmony-arm64@0.27.3': resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} engines: {node: '>=18'} @@ -1251,12 +1160,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.0': - resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.27.3': resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} @@ -1269,12 +1172,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.0': - resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.27.3': resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} @@ -1287,12 +1184,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.0': - resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.27.3': resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} @@ -1305,12 +1196,6 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.0': - resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.27.3': resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} @@ -3003,11 +2888,6 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.27.0: - resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} - engines: {node: '>=18'} - hasBin: true - esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} @@ -3734,6 +3614,11 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + miniflare@4.20260521.0: + resolution: {integrity: sha512-roRfxPq49OkuSeQsc43hRjSB1+HdHtDNKRwDEVk2hCjCBuBWxb5Wvwq88b0ULj6QVEJLN/+ZqF19M+h4VYJ/zg==} + engines: {node: '>=22.0.0'} + hasBin: true + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -4196,6 +4081,26 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rosie-skills-darwin-arm64@0.6.4: + resolution: {integrity: sha512-rn1s5hqFKcxeiDEWWoFa1hdGPshR8TkwHLzy/cBavb9XJNAaUxbe3oQ78W9sQkRHAgRyzJYyk9tw68Qrdnizgg==} + cpu: [arm64] + os: [darwin] + + rosie-skills-freebsd-x64@0.6.4: + resolution: {integrity: sha512-SxCRduPBMtfjkQ+q56Yw9OLA3PyaqoALzt7kER7IDKuUVfM2O/1w8sa5xhTDiCvWkZJixnH5d5Ya6KT+/Mwcng==} + cpu: [x64] + os: [freebsd] + + rosie-skills-linux-x64@0.6.4: + resolution: {integrity: sha512-D9Y9mfu7goB0s0X59uU3hcFeUTef3VbpCIDwFMzyvJrAq3XhRACWBDMHQsHlyWdHxTXPX/ILyW65RXyrJlgqng==} + cpu: [x64] + os: [linux] + + rosie-skills@0.6.4: + resolution: {integrity: sha512-ojfhSiQRdZ2QyWbmKAHOSAUbaLYrTc5zIH7mS1jKoP8KCFSQddwVhMyFqldckTeybTfW3zNcsZzyOTzGTN1SBA==} + engines: {node: '>=18'} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -4534,6 +4439,10 @@ packages: resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} engines: {node: '>=20.18.1'} + undici@7.24.8: + resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} + engines: {node: '>=20.18.1'} + unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -4943,12 +4852,17 @@ packages: engines: {node: '>=16'} hasBin: true - wrangler@4.63.0: - resolution: {integrity: sha512-+R04jF7Eb8K3KRMSgoXpcIdLb8GC62eoSGusYh1pyrSMm/10E0hbKkd7phMJO4HxXc6R7mOHC5SSoX9eof30Uw==} - engines: {node: '>=20.0.0'} + workerd@1.20260521.1: + resolution: {integrity: sha512-HzIThcZ0ZVEuzVxpY2IYZ3yssSrTjtrWXAVfmOl5rVwyqcu7aeZXGMiwrEmi9MOcC3wjy+BNv+hFrMMY5OrjQQ==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.94.0: + resolution: {integrity: sha512-GsNw0DomGFfeXFtKVTwn2X69UKcCxcTB0CXykjsMineJIxOeyrw7LovlHQ/3JU8KJHH7repLB+kOHvfTBA/Eew==} + engines: {node: '>=22.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20260205.0 + '@cloudflare/workers-types': ^4.20260521.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -4985,6 +4899,18 @@ packages: utf-8-validate: optional: true + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} @@ -5824,36 +5750,42 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@cloudflare/kv-asset-handler@0.4.2': {} + '@cloudflare/kv-asset-handler@0.5.0': {} - '@cloudflare/unenv-preset@2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260205.0)': + '@cloudflare/unenv-preset@2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260521.1)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: - workerd: 1.20260205.0 + workerd: 1.20260521.1 + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260521.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260521.1 - '@cloudflare/vite-plugin@1.23.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))(workerd@1.20260205.0)(wrangler@4.63.0(@cloudflare/workers-types@4.20260207.0))': + '@cloudflare/vite-plugin@1.23.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))(workerd@1.20260521.1)(wrangler@4.94.0(@cloudflare/workers-types@4.20260524.1))': dependencies: - '@cloudflare/unenv-preset': 2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260205.0) + '@cloudflare/unenv-preset': 2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260521.1) miniflare: 4.20260205.0 unenv: 2.0.0-rc.24 vite: 6.4.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) - wrangler: 4.63.0(@cloudflare/workers-types@4.20260207.0) + wrangler: 4.94.0(@cloudflare/workers-types@4.20260524.1) ws: 8.18.0 transitivePeerDependencies: - bufferutil - utf-8-validate - workerd - '@cloudflare/vitest-pool-workers@https://pkg.pr.new/@cloudflare/vitest-pool-workers@79a1932669f44a24342c1ffd66c29decd8a8fee9(@cloudflare/workers-types@4.20260207.0)(@vitest/runner@4.1.7)(@vitest/snapshot@4.1.7)(vitest@4.1.0-beta.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': + '@cloudflare/vitest-pool-workers@0.16.9(@cloudflare/workers-types@4.20260524.1)(@vitest/runner@4.1.7)(@vitest/snapshot@4.1.7)(vitest@4.1.0-beta.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': dependencies: '@vitest/runner': 4.1.7 '@vitest/snapshot': 4.1.7 cjs-module-lexer: 1.4.3 - esbuild: 0.27.0 - miniflare: 4.20260205.0 + esbuild: 0.27.3 + miniflare: 4.20260521.0 vitest: 4.1.0-beta.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) - wrangler: 4.63.0(@cloudflare/workers-types@4.20260207.0) + wrangler: 4.94.0(@cloudflare/workers-types@4.20260524.1) zod: 3.25.76 transitivePeerDependencies: - '@cloudflare/workers-types' @@ -5863,20 +5795,37 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20260205.0': optional: true + '@cloudflare/workerd-darwin-64@1.20260521.1': + optional: true + '@cloudflare/workerd-darwin-arm64@1.20260205.0': optional: true + '@cloudflare/workerd-darwin-arm64@1.20260521.1': + optional: true + '@cloudflare/workerd-linux-64@1.20260205.0': optional: true + '@cloudflare/workerd-linux-64@1.20260521.1': + optional: true + '@cloudflare/workerd-linux-arm64@1.20260205.0': optional: true + '@cloudflare/workerd-linux-arm64@1.20260521.1': + optional: true + '@cloudflare/workerd-windows-64@1.20260205.0': optional: true + '@cloudflare/workerd-windows-64@1.20260521.1': + optional: true + '@cloudflare/workers-types@4.20260207.0': {} + '@cloudflare/workers-types@4.20260524.1': {} + '@colors/colors@1.5.0': optional: true @@ -5926,234 +5875,156 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/aix-ppc64@0.27.0': - optional: true - '@esbuild/aix-ppc64@0.27.3': optional: true '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm64@0.27.0': - optional: true - '@esbuild/android-arm64@0.27.3': optional: true '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-arm@0.27.0': - optional: true - '@esbuild/android-arm@0.27.3': optional: true '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/android-x64@0.27.0': - optional: true - '@esbuild/android-x64@0.27.3': optional: true '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.27.0': - optional: true - '@esbuild/darwin-arm64@0.27.3': optional: true '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/darwin-x64@0.27.0': - optional: true - '@esbuild/darwin-x64@0.27.3': optional: true '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.27.0': - optional: true - '@esbuild/freebsd-arm64@0.27.3': optional: true '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.27.0': - optional: true - '@esbuild/freebsd-x64@0.27.3': optional: true '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm64@0.27.0': - optional: true - '@esbuild/linux-arm64@0.27.3': optional: true '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-arm@0.27.0': - optional: true - '@esbuild/linux-arm@0.27.3': optional: true '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-ia32@0.27.0': - optional: true - '@esbuild/linux-ia32@0.27.3': optional: true '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-loong64@0.27.0': - optional: true - '@esbuild/linux-loong64@0.27.3': optional: true '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-mips64el@0.27.0': - optional: true - '@esbuild/linux-mips64el@0.27.3': optional: true '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-ppc64@0.27.0': - optional: true - '@esbuild/linux-ppc64@0.27.3': optional: true '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.27.0': - optional: true - '@esbuild/linux-riscv64@0.27.3': optional: true '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-s390x@0.27.0': - optional: true - '@esbuild/linux-s390x@0.27.3': optional: true '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/linux-x64@0.27.0': - optional: true - '@esbuild/linux-x64@0.27.3': optional: true '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.27.0': - optional: true - '@esbuild/netbsd-arm64@0.27.3': optional: true '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.27.0': - optional: true - '@esbuild/netbsd-x64@0.27.3': optional: true '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.27.0': - optional: true - '@esbuild/openbsd-arm64@0.27.3': optional: true '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.27.0': - optional: true - '@esbuild/openbsd-x64@0.27.3': optional: true '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.27.0': - optional: true - '@esbuild/openharmony-arm64@0.27.3': optional: true '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/sunos-x64@0.27.0': - optional: true - '@esbuild/sunos-x64@0.27.3': optional: true '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-arm64@0.27.0': - optional: true - '@esbuild/win32-arm64@0.27.3': optional: true '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-ia32@0.27.0': - optional: true - '@esbuild/win32-ia32@0.27.3': optional: true '@esbuild/win32-x64@0.25.12': optional: true - '@esbuild/win32-x64@0.27.0': - optional: true - '@esbuild/win32-x64@0.27.3': optional: true @@ -7773,35 +7644,6 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 - esbuild@0.27.0: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.0 - '@esbuild/android-arm': 0.27.0 - '@esbuild/android-arm64': 0.27.0 - '@esbuild/android-x64': 0.27.0 - '@esbuild/darwin-arm64': 0.27.0 - '@esbuild/darwin-x64': 0.27.0 - '@esbuild/freebsd-arm64': 0.27.0 - '@esbuild/freebsd-x64': 0.27.0 - '@esbuild/linux-arm': 0.27.0 - '@esbuild/linux-arm64': 0.27.0 - '@esbuild/linux-ia32': 0.27.0 - '@esbuild/linux-loong64': 0.27.0 - '@esbuild/linux-mips64el': 0.27.0 - '@esbuild/linux-ppc64': 0.27.0 - '@esbuild/linux-riscv64': 0.27.0 - '@esbuild/linux-s390x': 0.27.0 - '@esbuild/linux-x64': 0.27.0 - '@esbuild/netbsd-arm64': 0.27.0 - '@esbuild/netbsd-x64': 0.27.0 - '@esbuild/openbsd-arm64': 0.27.0 - '@esbuild/openbsd-x64': 0.27.0 - '@esbuild/openharmony-arm64': 0.27.0 - '@esbuild/sunos-x64': 0.27.0 - '@esbuild/win32-arm64': 0.27.0 - '@esbuild/win32-ia32': 0.27.0 - '@esbuild/win32-x64': 0.27.0 - esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -8914,6 +8756,18 @@ snapshots: - bufferutil - utf-8-validate + miniflare@4.20260521.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.24.8 + workerd: 1.20260521.1 + ws: 8.20.1 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimist@1.2.8: {} mri@1.2.0: {} @@ -9512,6 +9366,21 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 + rosie-skills-darwin-arm64@0.6.4: + optional: true + + rosie-skills-freebsd-x64@0.6.4: + optional: true + + rosie-skills-linux-x64@0.6.4: + optional: true + + rosie-skills@0.6.4: + optionalDependencies: + rosie-skills-darwin-arm64: 0.6.4 + rosie-skills-freebsd-x64: 0.6.4 + rosie-skills-linux-x64: 0.6.4 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -9838,6 +9707,8 @@ snapshots: undici@7.18.2: {} + undici@7.24.8: {} + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -10121,18 +9992,27 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260205.0 '@cloudflare/workerd-windows-64': 1.20260205.0 - wrangler@4.63.0(@cloudflare/workers-types@4.20260207.0): + workerd@1.20260521.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260521.1 + '@cloudflare/workerd-darwin-arm64': 1.20260521.1 + '@cloudflare/workerd-linux-64': 1.20260521.1 + '@cloudflare/workerd-linux-arm64': 1.20260521.1 + '@cloudflare/workerd-windows-64': 1.20260521.1 + + wrangler@4.94.0(@cloudflare/workers-types@4.20260524.1): dependencies: - '@cloudflare/kv-asset-handler': 0.4.2 - '@cloudflare/unenv-preset': 2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260205.0) + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260521.1) blake3-wasm: 2.1.5 - esbuild: 0.27.0 - miniflare: 4.20260205.0 + esbuild: 0.27.3 + miniflare: 4.20260521.0 path-to-regexp: 6.3.0 + rosie-skills: 0.6.4 unenv: 2.0.0-rc.24 - workerd: 1.20260205.0 + workerd: 1.20260521.1 optionalDependencies: - '@cloudflare/workers-types': 4.20260207.0 + '@cloudflare/workers-types': 4.20260524.1 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil @@ -10154,6 +10034,8 @@ snapshots: ws@8.19.0: {} + ws@8.20.1: {} + xxhash-wasm@1.1.0: {} y18n@4.0.3: {} From 7f24acf1a9b08ce8e0399da6e8ccfc874c3d35b6 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 24 May 2026 20:32:42 +0100 Subject: [PATCH 8/9] chore(check): drop empty test script The package has no test files; vitest run was failing CI with "No test files found" plus a jsdom env-detection complaint. Removing the script lets pnpm's filtered test runner skip the package cleanly. --- apps/check/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/check/package.json b/apps/check/package.json index 8e5b9d5c..86d1ba1e 100644 --- a/apps/check/package.json +++ b/apps/check/package.json @@ -10,8 +10,7 @@ "preview": "vite preview", "wrangler:dev": "wrangler dev", "deploy": "vite build && wrangler deploy", - "check": "tsc --noEmit", - "test": "vitest run" + "check": "tsc --noEmit" }, "dependencies": { "@atcute/atproto": "^4.0.0", From d3dea3bcd2e42a28f0c9ffd4a73e8c3b8c5d228a Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 24 May 2026 20:50:00 +0100 Subject: [PATCH 9/9] chore(check): drop unused deps + knip ignores worktrees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apps/check was pulling six runtime deps and vitest with no consumers: @atcute/bluesky, @atcute/tid, @kobalte/core, @solid-primitives/storage, jose, multiformats. Drop them from package.json. Also un-export two oauth-flow helpers that are only used within the same module. Ignore .claude/** in knip — local subagent worktrees pollute results without affecting CI. --- apps/check/package.json | 7 - apps/check/src/lib/oauth-flow.ts | 4 +- knip.json | 1 + pnpm-lock.yaml | 392 ------------------------------- 4 files changed, 3 insertions(+), 401 deletions(-) diff --git a/apps/check/package.json b/apps/check/package.json index 86d1ba1e..1fc3d2a9 100644 --- a/apps/check/package.json +++ b/apps/check/package.json @@ -14,7 +14,6 @@ }, "dependencies": { "@atcute/atproto": "^4.0.0", - "@atcute/bluesky": "^4.0.3", "@atcute/cbor": "^2.3.3", "@atcute/cid": "^2.4.1", "@atcute/client": "^5.0.0", @@ -22,13 +21,8 @@ "@atcute/identity-resolver": "^2.0.0", "@atcute/lexicons": "^2.0.0", "@atcute/oauth-browser-client": "^4.0.0", - "@atcute/tid": "^1.1.2", "@ipld/car": "^5.4.6", - "@kobalte/core": "^0.13.11", - "@solid-primitives/storage": "^4.3.4", "idb-keyval": "^6.2.4", - "jose": "^6.2.3", - "multiformats": "^14.0.0", "oauth4webapi": "^3.8.6", "solid-js": "^1.9.13" }, @@ -38,7 +32,6 @@ "typescript": "^6.0.3", "vite": "^8.0.14", "vite-plugin-solid": "^2.11.12", - "vitest": "4.1.7", "wrangler": "^4.94.0" } } diff --git a/apps/check/src/lib/oauth-flow.ts b/apps/check/src/lib/oauth-flow.ts index 2de516d5..57568259 100644 --- a/apps/check/src/lib/oauth-flow.ts +++ b/apps/check/src/lib/oauth-flow.ts @@ -161,7 +161,7 @@ async function withNonceRetry(call: () => Promise): Promise { * `error_description`, fall back to the WWW-Authenticate header, then to the * HTTP status. Returns a concise message and the parsed body for evidence. */ -export async function readOAuthError( +async function readOAuthError( res: Response, ): Promise<{ message: string; body: unknown; wwwAuthenticate: string | null }> { const wwwAuthenticate = res.headers.get("www-authenticate"); @@ -370,7 +370,7 @@ function initialStepsFor(ids: readonly string[]): FlowStep[] { })); } -export function createFlowState(target: string): FlowState { +function createFlowState(target: string): FlowState { return { phase: "pre-redirect", target, diff --git a/knip.json b/knip.json index ef0a261b..6a1e52b7 100644 --- a/knip.json +++ b/knip.json @@ -1,5 +1,6 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", + "ignore": [".claude/**"], "ignoreExportsUsedInFile": { "interface": true, "type": true diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7938d67..0339dc97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: '@atcute/atproto': specifier: ^4.0.0 version: 4.0.0(@atcute/lexicons@2.0.0) - '@atcute/bluesky': - specifier: ^4.0.3 - version: 4.0.3(@atcute/lexicons@2.0.0) '@atcute/cbor': specifier: ^2.3.3 version: 2.3.3(@atcute/cid@2.4.1) @@ -59,27 +56,12 @@ importers: '@atcute/oauth-browser-client': specifier: ^4.0.0 version: 4.0.0(@atcute/identity-resolver@2.0.0(@atcute/identity@2.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3))(@atcute/lexicons@2.0.0)(typescript@6.0.3))(@atcute/lexicons@2.0.0)(typescript@6.0.3) - '@atcute/tid': - specifier: ^1.1.2 - version: 1.1.2 '@ipld/car': specifier: ^5.4.6 version: 5.4.6 - '@kobalte/core': - specifier: ^0.13.11 - version: 0.13.11(solid-js@1.9.13) - '@solid-primitives/storage': - specifier: ^4.3.4 - version: 4.3.4(solid-js@1.9.13) idb-keyval: specifier: ^6.2.4 version: 6.2.4 - jose: - specifier: ^6.2.3 - version: 6.2.3 - multiformats: - specifier: ^14.0.0 - version: 14.0.0 oauth4webapi: specifier: ^3.8.6 version: 3.8.6 @@ -102,9 +84,6 @@ importers: vite-plugin-solid: specifier: ^2.11.12 version: 2.11.12(solid-js@1.9.13)(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) - vitest: - specifier: 4.1.7 - version: 4.1.7(@types/node@24.10.11)(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) wrangler: specifier: ^4.94.0 version: 4.94.0(@cloudflare/workers-types@4.20260524.1) @@ -388,11 +367,6 @@ packages: '@atcute/bluesky@3.2.17': resolution: {integrity: sha512-Li+RsPkcRNC6AnNlqOGnlmAcjSwBdXIKFubJL1nwACDngKNXG4ooGL5cvzeekdDEfHmtFhS/tyZNaUx9QXYEUw==} - '@atcute/bluesky@4.0.3': - resolution: {integrity: sha512-82DKs9Htf9my/QCNmnxODYnK9xYn/6iHBJcy8CeSgKA3LmIQpeqdjnCjrwcchUUFRw3kbUal8RdycaTqjUs/kQ==} - peerDependencies: - '@atcute/lexicons': ^2.0.0 - '@atcute/car@5.1.1': resolution: {integrity: sha512-MeRUJNXYgAHrJZw7mMoZJb9xIqv3LZLQw90rRRAVAo8SGNdICwyqe6Bf2LGesX73QM04MBuYO6Kqhvold3TFfg==} @@ -488,15 +462,9 @@ packages: '@atcute/tid@1.1.1': resolution: {integrity: sha512-djJ8UGhLkTU5V51yCnBEruMg35qETjWzWy5sJG/2gEOl2Gd7rQWHSaf+yrO6vMS5EFA38U2xOWE3EDUPzvc2ZQ==} - '@atcute/tid@1.1.2': - resolution: {integrity: sha512-bmPuOX/TOfcm/vsK9vM98spjkcx2wgd9S2PeK5oLgEr8IbNRPq7iMCAPzOL1nu5XAW3LlkOYQEbYRcw5vcQ37w==} - '@atcute/time-ms@1.2.0': resolution: {integrity: sha512-dtNKebVIbr1+yu3a6vgtL4sfkNgxkL3aA+ohHsjtW83WWMjjGvX8GVTVmYCJ2dYSxIoxK0q1yWs11PmlqzmQ/A==} - '@atcute/time-ms@1.3.2': - resolution: {integrity: sha512-F+qOyR9pO55g1d/QmN+Gr+fimoUQQLusdGSB6pjV0wW5KPILR4oQ4e2ZhWzqUbeHLAgWvgoTTMsMDdz62Xa2tg==} - '@atcute/uint8array@1.1.0': resolution: {integrity: sha512-JtHXIVW6LPU9FMWp7SgE4HbUs3uV2WdfkK/2RWdEGjr4EgMV50P3FdU6fPeGlTfDNBJVYMIsuD2wwaKRPV/Aqg==} @@ -859,11 +827,6 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@corvu/utils@0.4.2': - resolution: {integrity: sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA==} - peerDependencies: - solid-js: ^1.8 - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1214,15 +1177,6 @@ packages: '@expressive-code/plugin-text-markers@0.42.0': resolution: {integrity: sha512-l59lUx8fq1v5g6SpmbDjiU0+7IdfbiWnAyRmtTVSpfhyq+nZMN4UcmYyu2b9Mynhzt7Gr+O+cXyEPDNb2AVWVQ==} - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - '@hexagon/base64@1.1.28': resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} @@ -1372,12 +1326,6 @@ packages: '@types/node': optional: true - '@internationalized/date@3.12.1': - resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==} - - '@internationalized/number@3.6.6': - resolution: {integrity: sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==} - '@ipld/car@5.4.2': resolution: {integrity: sha512-gfyrJvePyXnh2Fbj8mPg4JYvEZ3izhk8C9WgAle7xIYbrJNSXmNQ6BxAls8Gof97vvGbCROdxbTWRmHJtTCbcg==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} @@ -1415,16 +1363,6 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@kobalte/core@0.13.11': - resolution: {integrity: sha512-hK7TYpdib/XDb/r/4XDBFaO9O+3ZHz4ZWryV4/3BfES+tSQVgg2IJupDnztKXB0BqbSRy/aWlHKw1SPtNPYCFQ==} - peerDependencies: - solid-js: ^1.8.15 - - '@kobalte/utils@0.9.1': - resolution: {integrity: sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w==} - peerDependencies: - solid-js: ^1.8.8 - '@levischuck/tiny-cbor@0.2.11': resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} @@ -2104,82 +2042,12 @@ packages: resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} - '@solid-primitives/event-listener@2.4.5': - resolution: {integrity: sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA==} - peerDependencies: - solid-js: ^1.6.12 - - '@solid-primitives/keyed@1.5.3': - resolution: {integrity: sha512-zNadtyYBhJSOjXtogkGHmRxjGdz9KHc8sGGVAGlUABkE8BED2tbIZoxkwSqzOwde8OcUEH0bb5DLZUWIMvyBSA==} - peerDependencies: - solid-js: ^1.6.12 - - '@solid-primitives/map@0.4.13': - resolution: {integrity: sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew==} - peerDependencies: - solid-js: ^1.6.12 - - '@solid-primitives/media@2.3.5': - resolution: {integrity: sha512-LX9fB5WDaK87FMDtUB1qokBOfT2et9Uobv/zZaKLH9caFSz4+P70MBKEIBHcZQy+9MV5M2XvGYLTbLskjkzMjA==} - peerDependencies: - solid-js: ^1.6.12 - - '@solid-primitives/props@3.2.3': - resolution: {integrity: sha512-XzG6en9gSFwmvbKcATm2BxL63HegZ+BAG5fmHi8jyBppQHcaths7ffz+6vYvwYy3nlgLa20ufJLj7tst+PcHFA==} - peerDependencies: - solid-js: ^1.6.12 - - '@solid-primitives/refs@1.1.3': - resolution: {integrity: sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA==} - peerDependencies: - solid-js: ^1.6.12 - - '@solid-primitives/resize-observer@2.1.5': - resolution: {integrity: sha512-AiyTknKcNBaKHbcSMuxtSNM8FjIuiSuFyFghdD0TcCMU9hKi9EmsC5pjfjDwxE+5EueB1a+T/34PLRI5vbBbKw==} - peerDependencies: - solid-js: ^1.6.12 - - '@solid-primitives/rootless@1.5.3': - resolution: {integrity: sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA==} - peerDependencies: - solid-js: ^1.6.12 - - '@solid-primitives/static-store@0.1.3': - resolution: {integrity: sha512-uxez7SXnr5GiRnzqO2IEDjOJRIXaG+0LZLBizmUA1FwSi+hrpuMzVBwyk70m4prcl8X6FDDXUl9O8hSq8wHbBQ==} - peerDependencies: - solid-js: ^1.6.12 - - '@solid-primitives/storage@4.3.4': - resolution: {integrity: sha512-GSxPAIuyxhJWOcv7n10iv3aid5oHN3KUgyA9IV0GYWlPpgyGs43aS9E85b0VXDLoH+D4ThNK8+2WEJ8B/S6Ccg==} - peerDependencies: - '@tauri-apps/plugin-store': '*' - solid-js: ^1.6.12 - solid-start: '*' - peerDependenciesMeta: - '@tauri-apps/plugin-store': - optional: true - solid-start: - optional: true - - '@solid-primitives/trigger@1.2.3': - resolution: {integrity: sha512-Za2JebEiDyfamjmDwRaESYqBBYOlgYGzB8kHYH0QrkXyLf2qNADlKdGN+z3vWSLCTDcKxChS43Kssjuc0OZhng==} - peerDependencies: - solid-js: ^1.6.12 - - '@solid-primitives/utils@6.4.0': - resolution: {integrity: sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A==} - peerDependencies: - solid-js: ^1.6.12 - '@speed-highlight/core@1.2.14': resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@swc/helpers@0.5.21': - resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} - '@tailwindcss/node@4.3.0': resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} @@ -2348,9 +2216,6 @@ packages: '@vitest/expect@4.1.0-beta.1': resolution: {integrity: sha512-LpwvdERiCpuXWE8IRLGgqMWPvcGxZVUNmVyRnvs4ZPCazbEgjkm5wlqFN7lJYfQkRamldHO+38348EefLtRY0A==} - '@vitest/expect@4.1.7': - resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} - '@vitest/mocker@4.1.0-beta.1': resolution: {integrity: sha512-IiiJL8aqJrk5oNgiOsTvrzMAt71qWL1pvHKJJiaKQCMX3Lvj6w++HvNJIl3d0rm5D9sngAJYtF87EN0CTWhF8Q==} peerDependencies: @@ -2362,17 +2227,6 @@ packages: vite: optional: true - '@vitest/mocker@4.1.7': - resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - '@vitest/pretty-format@4.1.0-beta.1': resolution: {integrity: sha512-CeI3uthjV/XKA6KBCr/B5HlCQaFdCgprdl7gBg/sUExQPary8BBhYoVWJeAPTeg9u+ppT9S4v/sYjjNjn3Qsrw==} @@ -2394,9 +2248,6 @@ packages: '@vitest/spy@4.1.0-beta.1': resolution: {integrity: sha512-DzHg9PJuWYivWLt4O9SYF2u5/mGlfM3tgP8DdlSwMr7C+hBusejK+r0rqaCSYwBH47ePhU4jccBm4i6bE2dahg==} - '@vitest/spy@4.1.7': - resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} - '@vitest/utils@4.1.0-beta.1': resolution: {integrity: sha512-IUCsqDFj8E8WJq3wGRQ7MiMb2571tjTnjyrJ1oy+0HODutA2TpZGRqBA8ziLCIWTOL/e4RArE2k6eZh/jXgk9A==} @@ -3265,9 +3116,6 @@ packages: jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} - jose@6.2.3: - resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4190,16 +4038,6 @@ packages: solid-js@1.9.13: resolution: {integrity: sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ==} - solid-presence@0.1.8: - resolution: {integrity: sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA==} - peerDependencies: - solid-js: ^1.8 - - solid-prevent-scroll@0.1.10: - resolution: {integrity: sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw==} - peerDependencies: - solid-js: ^1.8 - solid-refresh@0.6.3: resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} peerDependencies: @@ -4235,9 +4073,6 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - std-env@4.1.0: - resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} - stream-replace-string@2.0.0: resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==} @@ -4776,47 +4611,6 @@ packages: jsdom: optional: true - vitest@4.1.7: - resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.7 - '@vitest/browser-preview': 4.1.7 - '@vitest/browser-webdriverio': 4.1.7 - '@vitest/coverage-istanbul': 4.1.7 - '@vitest/coverage-v8': 4.1.7 - '@vitest/ui': 4.1.7 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - walk-up-path@4.0.0: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} @@ -5105,11 +4899,6 @@ snapshots: '@atcute/atproto': 3.1.10 '@atcute/lexicons': 1.3.0 - '@atcute/bluesky@4.0.3(@atcute/lexicons@2.0.0)': - dependencies: - '@atcute/atproto': 4.0.0(@atcute/lexicons@2.0.0) - '@atcute/lexicons': 2.0.0 - '@atcute/car@5.1.1': dependencies: '@atcute/cbor': 2.3.3(@atcute/cid@2.4.1) @@ -5289,17 +5078,11 @@ snapshots: dependencies: '@atcute/time-ms': 1.2.0 - '@atcute/tid@1.1.2': - dependencies: - '@atcute/time-ms': 1.3.2 - '@atcute/time-ms@1.2.0': dependencies: '@types/bun': 1.3.8 node-gyp-build: 4.8.4 - '@atcute/time-ms@1.3.2': {} - '@atcute/uint8array@1.1.0': {} '@atcute/uint8array@1.1.1': {} @@ -5829,11 +5612,6 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@corvu/utils@0.4.2(solid-js@1.9.13)': - dependencies: - '@floating-ui/dom': 1.7.6 - solid-js: 1.9.13 - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -6053,17 +5831,6 @@ snapshots: dependencies: '@expressive-code/core': 0.42.0 - '@floating-ui/core@1.7.5': - dependencies: - '@floating-ui/utils': 0.2.11 - - '@floating-ui/dom@1.7.6': - dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 - - '@floating-ui/utils@0.2.11': {} - '@hexagon/base64@1.1.28': {} '@img/colour@1.0.0': {} @@ -6169,14 +5936,6 @@ snapshots: optionalDependencies: '@types/node': 24.10.11 - '@internationalized/date@3.12.1': - dependencies: - '@swc/helpers': 0.5.21 - - '@internationalized/number@3.6.6': - dependencies: - '@swc/helpers': 0.5.21 - '@ipld/car@5.4.2': dependencies: '@ipld/dag-cbor': 9.2.5 @@ -6230,29 +5989,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@kobalte/core@0.13.11(solid-js@1.9.13)': - dependencies: - '@floating-ui/dom': 1.7.6 - '@internationalized/date': 3.12.1 - '@internationalized/number': 3.6.6 - '@kobalte/utils': 0.9.1(solid-js@1.9.13) - '@solid-primitives/props': 3.2.3(solid-js@1.9.13) - '@solid-primitives/resize-observer': 2.1.5(solid-js@1.9.13) - solid-js: 1.9.13 - solid-presence: 0.1.8(solid-js@1.9.13) - solid-prevent-scroll: 0.1.10(solid-js@1.9.13) - - '@kobalte/utils@0.9.1(solid-js@1.9.13)': - dependencies: - '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.13) - '@solid-primitives/keyed': 1.5.3(solid-js@1.9.13) - '@solid-primitives/map': 0.4.13(solid-js@1.9.13) - '@solid-primitives/media': 2.3.5(solid-js@1.9.13) - '@solid-primitives/props': 3.2.3(solid-js@1.9.13) - '@solid-primitives/refs': 1.1.3(solid-js@1.9.13) - '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) - solid-js: 1.9.13 - '@levischuck/tiny-cbor@0.2.11': {} '@loaderkit/resolve@1.0.4': @@ -6821,78 +6557,10 @@ snapshots: '@sindresorhus/is@7.2.0': {} - '@solid-primitives/event-listener@2.4.5(solid-js@1.9.13)': - dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) - solid-js: 1.9.13 - - '@solid-primitives/keyed@1.5.3(solid-js@1.9.13)': - dependencies: - solid-js: 1.9.13 - - '@solid-primitives/map@0.4.13(solid-js@1.9.13)': - dependencies: - '@solid-primitives/trigger': 1.2.3(solid-js@1.9.13) - solid-js: 1.9.13 - - '@solid-primitives/media@2.3.5(solid-js@1.9.13)': - dependencies: - '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.13) - '@solid-primitives/rootless': 1.5.3(solid-js@1.9.13) - '@solid-primitives/static-store': 0.1.3(solid-js@1.9.13) - '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) - solid-js: 1.9.13 - - '@solid-primitives/props@3.2.3(solid-js@1.9.13)': - dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) - solid-js: 1.9.13 - - '@solid-primitives/refs@1.1.3(solid-js@1.9.13)': - dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) - solid-js: 1.9.13 - - '@solid-primitives/resize-observer@2.1.5(solid-js@1.9.13)': - dependencies: - '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.13) - '@solid-primitives/rootless': 1.5.3(solid-js@1.9.13) - '@solid-primitives/static-store': 0.1.3(solid-js@1.9.13) - '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) - solid-js: 1.9.13 - - '@solid-primitives/rootless@1.5.3(solid-js@1.9.13)': - dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) - solid-js: 1.9.13 - - '@solid-primitives/static-store@0.1.3(solid-js@1.9.13)': - dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) - solid-js: 1.9.13 - - '@solid-primitives/storage@4.3.4(solid-js@1.9.13)': - dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) - solid-js: 1.9.13 - - '@solid-primitives/trigger@1.2.3(solid-js@1.9.13)': - dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) - solid-js: 1.9.13 - - '@solid-primitives/utils@6.4.0(solid-js@1.9.13)': - dependencies: - solid-js: 1.9.13 - '@speed-highlight/core@1.2.14': {} '@standard-schema/spec@1.1.0': {} - '@swc/helpers@0.5.21': - dependencies: - tslib: 2.8.1 - '@tailwindcss/node@4.3.0': dependencies: '@jridgewell/remapping': 2.3.5 @@ -7059,15 +6727,6 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/expect@4.1.7': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 - chai: 6.2.2 - tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.0-beta.1(vite@6.4.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))': dependencies: '@vitest/spy': 4.1.0-beta.1 @@ -7076,14 +6735,6 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0) - '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': - dependencies: - '@vitest/spy': 4.1.7 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.14(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) - '@vitest/pretty-format@4.1.0-beta.1': dependencies: tinyrainbow: 3.0.3 @@ -7117,8 +6768,6 @@ snapshots: '@vitest/spy@4.1.0-beta.1': {} - '@vitest/spy@4.1.7': {} - '@vitest/utils@4.1.0-beta.1': dependencies: '@vitest/pretty-format': 4.1.0-beta.1 @@ -8134,8 +7783,6 @@ snapshots: jose@6.1.3: {} - jose@6.2.3: {} - js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -9484,16 +9131,6 @@ snapshots: seroval: 1.5.4 seroval-plugins: 1.5.4(seroval@1.5.4) - solid-presence@0.1.8(solid-js@1.9.13): - dependencies: - '@corvu/utils': 0.4.2(solid-js@1.9.13) - solid-js: 1.9.13 - - solid-prevent-scroll@0.1.10(solid-js@1.9.13): - dependencies: - '@corvu/utils': 0.4.2(solid-js@1.9.13) - solid-js: 1.9.13 - solid-refresh@0.6.3(solid-js@1.9.13): dependencies: '@babel/generator': 7.29.1 @@ -9526,8 +9163,6 @@ snapshots: std-env@3.10.0: {} - std-env@4.1.0: {} - stream-replace-string@2.0.0: {} string-width@4.2.3: @@ -9933,33 +9568,6 @@ snapshots: - tsx - yaml - vitest@4.1.7(@types/node@24.10.11)(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)): - dependencies: - '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) - '@vitest/pretty-format': 4.1.7 - '@vitest/runner': 4.1.7 - '@vitest/snapshot': 4.1.7 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.1.0 - tinybench: 2.9.0 - tinyexec: 1.2.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.1.0 - vite: 8.0.14(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 24.10.11 - transitivePeerDependencies: - - msw - walk-up-path@4.0.0: {} web-namespaces@2.0.1: {}