diff --git a/isolation/ISOLATION.md b/isolation/ISOLATION.md index 83f08b8c..9e8e9af7 100644 --- a/isolation/ISOLATION.md +++ b/isolation/ISOLATION.md @@ -114,8 +114,9 @@ Which parts of the UI end up inside the boundary is a choice of the frame config │ - unsigned in → signed out │ └─────────────────────────────────┘ -* unknown: prompt — documents not in the host repo are not auto-allowlisted; - the user is prompted via window.confirm — see the Allowlist section. +* unknown: prompt — a document not in the host repo is auto-allowlisted only if + every change is authored by the iframe's own author id (it created it); + otherwise the user is prompted via window.confirm — see the Allowlist section. ``` > **Not yet implemented (future / Keyhive):** the "Keyhive Isolation Identity" box, the "Signs 'signable' commits with isolation identity" line, and the sync-channel "'signed or signable' filter" / "unsigned in → signed out" notes describe the Keyhive-enabled design. Today the intermediary holds no isolation identity and signs nothing, and edits flow through unsigned. See [Security without Keyhive](#security-without-keyhive). @@ -216,7 +217,7 @@ The allowlist is seeded from the boot spec's `rootUrls`. It is expanded through 1. **Transitive discovery.** Each root document's content is scanned for embedded automerge URLs (recursively walking objects, arrays, and strings). All discovered URLs are added to the allowlist (unless denylisted). This reflects the assumption that if the user opened a document, its referenced children are authorized for the tool rendering it. -2. **User approval.** When the iframe requests a document that is not on the allowlist, the user is prompted via `window.confirm()` and can approve access explicitly. If the document is one the host repo already knows about, the allowlist is first refreshed (re-scanning all root documents for new URLs, e.g. a reference the user just typed) and the prompt is skipped if it now matches. Documents the host has never seen (newly created by the iframe, added by a collaborator, or embedded in the tool) skip that refresh — a root re-scan cannot surface a document the host has never seen — and prompt directly. Unknown documents are **not** auto-allowlisted; this prevents a tool from silently gaining access to any URL it constructs, at the cost of prompting for documents the iframe itself just created. Once the Author ID API is available, documents created by the iframe's own author ID will be auto-allowlisted while other unknown documents continue to prompt. _(See "Waiting on automerge/keyhive teams" below.)_ +2. **Iframe-authored auto-allowlist + user approval.** When the iframe requests a document that is not on the allowlist: if the document is one the host repo already knows about, the allowlist is first refreshed (re-scanning all root documents for new URLs, e.g. a reference the user just typed) and access is granted if it now matches. Otherwise the gate checks **authorship** — using the Author ID API, the iframe's `Repo` is configured with a per-isolation-context author id (generated host-side, passed into the iframe at boot), so changes the iframe's tools make are attributed to it. If **every** change in the requested document was authored by that id, the document is **auto-allowlisted with no prompt** — it is one the iframe just created. Any other unknown document (never seen by the host, or with any change by a different/absent author) **prompts the user** via `window.confirm()`. This preserves the safe default — a tool cannot silently gain access to a URL it constructs or to a foreign document — while removing the spurious prompt for a tool opening the document it just made. The all-changes check is conservative on purpose: a doc with even one foreign-authored (or unauthored) change still prompts, so a tool can't smuggle access by appending an own-authored change to someone else's document. (Reading the author materializes the doc in the trusted host repo only; the doc becomes iframe-syncable only after it is added to the allowlist, so materializing to check authorship does not itself grant access.) ### Keyhive integration (future) @@ -330,10 +331,7 @@ The modal renders a `` in the host DOM (outside the isolation bo These are API changes being developed by the automerge and keyhive teams. The isolation architecture depends on them but cannot implement them until they ship. -- **Author ID API.** Configure an author ID on the iframe's Repo so that edits made by tools are correctly attributed. The iframe repo will be configured with the isolation identity's author ID. (Not yet available on main.) Once available, improve the unknown-document handling in `onAccessRequest`: - - When the intermediary encounters an unknown document whose author matches the iframe's assigned author ID, auto-allowlist it (the iframe created it) instead of prompting. - - For unknown documents with a different author, keep prompting the user. - - Today, _all_ unknown documents prompt the user (the blanket auto-allowlist workaround has been removed); this change would restore silent access for the iframe's own creations only. +- **~~Author ID API~~ — IMPLEMENTED.** The iframe's `Repo` is now configured with a per-isolation-context author id (generated host-side, passed in at boot), and `onAccessRequest` auto-allowlists an unknown document iff every change is by that author id (otherwise prompts). See the "Iframe-authored auto-allowlist" item under [Allowlist](#allowlist). (Requires the automerge authors release — `automerge-repo ≥ 2.7.0-authors`, `automerge ≥ 3.4.0-rev-frag-hex`; `RepoConfig.authorId` + `ChangeMetadata.author`.) - **"Signed or signable" bridge config.** Configure the bridge connection (NetworkAdapter or similar) between the intermediary repo and the iframe repo to only accept signed or signable commits from the iframe direction. Signable commits are signed with the isolation identity; mis-attributed commits are dropped. (Not yet available.) ### Tracked separately diff --git a/isolation/README.md b/isolation/README.md index 396e275b..67dae603 100644 --- a/isolation/README.md +++ b/isolation/README.md @@ -95,6 +95,29 @@ props tweak (collapse toggle, tool reorder) updates in place. versions must match what the host serves (i.e. the other base modules' versions). Keep `package.json` in step with `threepane/package.json`. +**Requires the automerge authors release** (`@automerge/automerge-repo ≥ +2.7.0-authors`, `@automerge/automerge ≥ 3.4.0-rev-frag-hex`) for the author-id +feature below — `RepoConfig.authorId` + `ChangeMetadata.author`. The host import +map (patchwork-next catalog) must serve these too. + +## Iframe-authored documents auto-allowlist + +The iframe's `Repo` is configured with a per-isolation-context **author id** +(generated host-side in `bootIsolation`, passed into the iframe at boot, used as +both `peerId` and the repo's `authorId`). So every change the iframe's tools make +is attributed to that id. + +The access gate (`handleAccessRequest`) uses this to skip the permission prompt +for a document the tool **just created**: when the iframe requests an +unknown-but-not-denylisted document, the gate reads its change metadata and +**auto-allowlists it iff *every* change is authored by the iframe's own id**. +Any other unknown document (or one with a foreign/absent-author change) still +prompts via `window.confirm()`. The all-changes check is deliberately +conservative — one foreign change and it prompts — so a tool can't gain access to +someone else's document by appending an own-authored change. Reading the author +materializes the doc only in the trusted host repo; a doc becomes iframe-syncable +only once added to the allowlist, so the check itself grants nothing. + ## Registration Like every patchwork module, this is published/registered independently (not a diff --git a/isolation/package.json b/isolation/package.json index 0ab9c8d5..3d162276 100644 --- a/isolation/package.json +++ b/isolation/package.json @@ -21,8 +21,9 @@ "author": "Ink & Switch", "license": "MIT", "dependencies": { - "@automerge/automerge-repo": "^2.6.0-subduction.26", - "@automerge/automerge-repo-network-messagechannel": "^2.6.0-subduction.26", + "@automerge/automerge": "^3.4.0-rev-frag-hex.3", + "@automerge/automerge-repo": "^2.7.0-authors.1", + "@automerge/automerge-repo-network-messagechannel": "^2.7.0-authors.1", "@inkandswitch/patchwork-elements": "1.0.0", "@inkandswitch/patchwork-filesystem": "^0.0.8", "@inkandswitch/patchwork-plugins": "^0.0.11", diff --git a/isolation/src/boot/host/boot.ts b/isolation/src/boot/host/boot.ts index f89fb105..4f26d484 100644 --- a/isolation/src/boot/host/boot.ts +++ b/isolation/src/boot/host/boot.ts @@ -93,6 +93,16 @@ export function bootIsolation(host: HTMLElement): IsolationHandle { const rootComponentId = readRootComponentId(host); const rootUrls = readAllowlistUrls(host); + // The isolation context's identity, generated host-side so the access gate can + // compare against the same value the iframe repo authors with (no round-trip). + // - `authorId`: a hex id (UUID prefix — all hex chars) used as the iframe + // repo's Automerge author, so changes it makes are attributed to it. The + // gate auto-allowlists an unknown doc iff *every* change is by this author. + // - `peerId`: the same id, human-readable, for the iframe repo's peer id + + // logging. + const authorId = crypto.randomUUID().slice(0, 8); + const isolationPeerId = `isolation-${authorId}`; + // State wired up during the boot, torn down together. All start empty, so // teardown() before/during boot is a safe no-op over them. const cleanups: Array<() => void> = []; @@ -101,7 +111,10 @@ export function bootIsolation(host: HTMLElement): IsolationHandle { let iframe: HTMLIFrameElement | null = null; async function run() { - log(`init root "${rootComponentId}" with ${rootUrls.length} root URLs`); + log( + `init root "${rootComponentId}" with ${rootUrls.length} root URLs, ` + + `authorId=${authorId}` + ); const repo = getRepo(host); if (!repo) return; @@ -134,6 +147,9 @@ export function bootIsolation(host: HTMLElement): IsolationHandle { allowlist, hostRepo: repo, denylist, + // The iframe repo authors its changes with this id (see the boot message); + // the intermediary auto-allowlists docs solely authored by it. + iframeAuthorId: authorId, onAccessRequest: (documentId) => handleAccessRequest(repo, rootUrls, allowlist, denylist, documentId), }); @@ -226,6 +242,11 @@ export function bootIsolation(host: HTMLElement): IsolationHandle { // change between boot start and this async send is reflected in the // initial boot rather than lost, since a pre-port push no-ops. rootComponentData: readRootComponentData(host), + // The iframe repo's identity, generated host-side (see bootIsolation). + // `authorId` attributes the iframe's changes so the access gate can + // recognize docs it created; `peerId` is the readable form. + authorId, + peerId: isolationPeerId, registryEntries, esmsSource: assets.esmsSource, hostStyles: assets.hostStyles, diff --git a/isolation/src/boot/iframe/main.ts b/isolation/src/boot/iframe/main.ts index 7fb7155a..eecdd190 100644 --- a/isolation/src/boot/iframe/main.ts +++ b/isolation/src/boot/iframe/main.ts @@ -44,6 +44,10 @@ interface InitMessage { syncPort: MessagePort; data: { rootComponentId: string; + /** The iframe repo's Automerge author id (hex), generated host-side. */ + authorId: string; + /** The iframe repo's peer id (readable form of the author id), host-side. */ + peerId: string; /** * Opaque JSON data for the root component, relayed verbatim from the host * (the boundary never parses it). Materialized as-is into the root's inert @@ -107,6 +111,9 @@ export async function boot(deps: BootDeps) { // until modules load, but we need logging during bootstrap. // Respects the same localStorage("debug") namespace convention. const NAMESPACE = "patchwork:elements:isolation:iframe"; + // A local id used only to tag early console output (before the boot message + // arrives). The repo's real identity — peerId + authorId — comes from the boot + // message (host-generated), so the access gate can compare against it. const peerId = "isolation-" + crypto.randomUUID().slice(0, 8); let debugEnabled = false; const log = (...args: unknown[]) => { @@ -273,8 +280,12 @@ export async function boot(deps: BootDeps) { const syncAdapter = new messagechannel.MessageChannelNetworkAdapter( init.syncPort ); + // Identity is generated host-side (so the access gate compares against the + // same value): `authorId` attributes this repo's changes as the isolation + // context, letting the host auto-allowlist docs the iframe created. const repo = new automergeRepo.Repo({ - peerId: peerId, + peerId: d.peerId as any, + authorId: d.authorId, network: [syncAdapter], }); (window as any).repo = repo; diff --git a/isolation/src/bridges/access-control.ts b/isolation/src/bridges/access-control.ts index 702795c6..42cacc70 100644 --- a/isolation/src/bridges/access-control.ts +++ b/isolation/src/bridges/access-control.ts @@ -38,7 +38,7 @@ import { log } from "../log.js"; /** * Scan a single document's content for automerge URLs and add any new ones to * the allowlist (unless they are denylisted or turn out to be sensitive — see - * {@link checkAndDenylistIfSensitive}). + * {@link denylistIfSensitive}). * * This is a one-shot scan, not a live subscription: it reads the document's * current contents once. Callers re-invoke it (via the wrappers below) when @@ -114,64 +114,44 @@ export async function buildAllowlist( log(`allowlisted root ${url}`); } - await populateAllowlistFromRoots( - repo, - rootUrls, - allowlist, - denylist, - isStale - ); + await refreshAllowlistFromRoots(repo, rootUrls, allowlist, denylist, isStale); return allowlist; } /** - * Scan multiple root documents into the allowlist, adding everything they - * transitively reference. Stops early if `isStale` flips (a newer init epoch - * started). Used by `buildAllowlist` for the initial boot-time seed. + * Scan all root documents into the allowlist, adding everything they + * transitively reference. Two uses: + * - boot-time seed (via `buildAllowlist`), passing `isStale` so the scan stops + * early if a newer init epoch started; + * - lazy refresh (e.g. when an access request arrives), called with no + * `isStale` to pick up references the user just added. + * + * When `isStale` is omitted the early-out is simply skipped. */ -async function populateAllowlistFromRoots( +async function refreshAllowlistFromRoots( repo: Repo, rootUrls: AutomergeUrl[], allowlist: SyncAllowlist, denylist: SyncDenylist | undefined, - isStale: () => boolean + isStale?: () => boolean ): Promise { for (const url of rootUrls) { await scanDocIntoAllowlist(repo, url, allowlist, denylist, isStale); - if (isStale()) return; - } -} - -/** - * Re-scan all root documents and add any newly-referenced automerge URLs to - * the allowlist. Called lazily (e.g. when an access request arrives) rather - * than on every change, to catch references the user just added. - */ -async function refreshAllowlistFromRoots( - repo: Repo, - rootUrls: AutomergeUrl[], - allowlist: SyncAllowlist, - denylist: SyncDenylist | undefined -): Promise { - for (const url of rootUrls) { - await scanDocIntoAllowlist(repo, url, allowlist, denylist); + if (isStale?.()) return; } } /** - * Decide whether the iframe may access a document that isn't yet on the - * allowlist — the intermediary repo's `onAccessRequest` gate. + * Prompt the user to grant the iframe access to a document that isn't on the + * allowlist and wasn't auto-allowlisted (i.e. not solely authored by the iframe; + * the author check lives in the intermediary's `resolveAccess` / + * `isAuthoredSolelyByIframe` — see repo-bridge). Reached as the fallback for docs + * the iframe references but did not itself create. * - * Unknown documents are NOT auto-allowlisted; the user is prompted. This is a - * safe default: it stops a tool from silently gaining access to any URL it - * constructs. The cost is that documents the iframe itself just created also - * prompt. - * - * TODO: once the Author ID API is available, auto-allowlist unknown documents - * whose author matches the iframe's assigned author ID (the iframe created - * them) and continue to prompt for all others. - * - * Returns true (and allowlists the doc) if access is granted. + * If the document is one the host repo already knows about, the allowlist is + * first refreshed (re-scanning roots for newly-added references) and the prompt + * is skipped if it now matches. Returns true (and allowlists) if access is + * granted. */ export async function handleAccessRequest( repo: Repo, @@ -180,28 +160,32 @@ export async function handleAccessRequest( denylist: SyncDenylist, documentId: DocumentId ): Promise { + log(`handling access request for ${documentId}`); + let approved = false; if (repo.handles[documentId]) { // Known to the host but not yet allowlisted — the URL may have been added // since the initial scan (e.g. the user typed a new reference), so re-scan - // roots before asking. (Skipped for unknown docs: a root re-scan can't - // surface a doc the host has never seen, so it would be wasted work.) + // roots before asking. await refreshAllowlistFromRoots(repo, rootUrls, allowlist, denylist); if (allowlist.has(documentId)) return true; + + approved = window.confirm( + `A tool wants to access a document on your system:\n\n` + + `Document ID: ${documentId}\n\n` + + `Allow access?` + ); + } else { + approved = window.confirm( + `A tool wants to access a remote document:\n\n` + + `Document ID: ${documentId}\n\n` + + `This document was not created by the tool and is not on your local system. Allow access?` + ); } - // TODO: remove temp approval - // const approved = window.confirm( - // `A tool wants to access a document:\n\n` + - // `Document ID: ${documentId}\n\n` + - // `This may be a document the tool just created, or one it is ` + - // `trying to open. Allow access?` - // ); - // if (approved) { - // allowlist.addDocumentId(documentId); - // } - // return approved; - allowlist.addDocumentId(documentId); - return true; + if (approved) { + allowlist.addDocumentId(documentId); + } + return approved; } /** @@ -365,7 +349,7 @@ async function denylistModuleSettings( * discovered later (e.g. referenced deep in user content) are caught lazily by * `denylistIfSensitive`, which shares the same recognition logic. */ -export async function populateDenylist( +async function populateDenylist( repo: Repo, denylist: SyncDenylist ): Promise { @@ -427,7 +411,7 @@ function sameDoc(a: AutomergeUrl, b: AutomergeUrl): boolean { * deliberately do NOT fingerprint folders by shape, which would wrongly block * the user's own content folders. */ -export async function denylistIfSensitive( +async function denylistIfSensitive( repo: Repo, url: AutomergeUrl, denylist: SyncDenylist diff --git a/isolation/src/bridges/repo-bridge.ts b/isolation/src/bridges/repo-bridge.ts index 3da09687..75e4b58d 100644 --- a/isolation/src/bridges/repo-bridge.ts +++ b/isolation/src/bridges/repo-bridge.ts @@ -16,8 +16,10 @@ import { type AutomergeUrl, type PeerId, type DocumentId, + type DocHandle, parseAutomergeUrl, } from "@automerge/automerge-repo"; +import { getChangesMetaSince } from "@automerge/automerge"; import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel"; import { log } from "../log.js"; @@ -94,15 +96,25 @@ export class SyncDenylist extends SyncDocumentSet { } export interface IntermediaryRepoOptions { - /** The allowlist controlling which documents can sync to the iframe. */ + /** The allowlist controlling which documents can sync to the host. */ allowlist: SyncAllowlist; /** The host Repo to sync allowed documents from. */ hostRepo: Repo; /** Optional denylist — denylisted documents are blocked regardless of allowlist. */ denylist?: SyncDenylist; /** - * Called when the iframe requests a document that's not on the allowlist - * or denylist. If it returns true, the document is added to the allowlist. + * The iframe repo's Automerge author id (generated host-side). An unallowlisted + * document the iframe references that is resident in the intermediary (it + * arrived over the open iframe channel) and whose changes are *all* authored by + * this id was created by the iframe itself, so it is auto-allowlisted (no + * prompt). See `resolveAccess`. + */ + iframeAuthorId: string; + /** + * Called (out of band, never from the sync gate) for a document the iframe + * requests that isn't allowlisted and wasn't created by the iframe — i.e. a + * foreign document that did not arrive over the open iframe channel. If it + * returns true, the document is added to the allowlist. */ onAccessRequest?: (documentId: DocumentId) => Promise; } @@ -114,6 +126,56 @@ export interface IntermediaryRepo { shutdown(): void; } +/** + * How long the access classifier waits for the iframe to supply a document's + * content before concluding it is foreign and prompting the user. Only a ceiling + * for the *negative* (foreign) case — an iframe-created doc reaches `ready` as + * soon as its already-queued sync message applies, resolving well before this. + * Erring generous only costs a slightly later prompt for a genuinely-foreign doc + * (which then waits on `window.confirm` anyway); the trade is a rare spurious + * prompt for an iframe-created doc whose content lands after the grace. + */ +const IFRAME_SUPPLY_GRACE_MS = 500; + +/** + * Resolve with the ready `DocHandle` if the query reaches `ready` within + * `graceMs`, else `null`. + * + * Used to classify an undecided host-channel document while the host peer is + * held `loading` (see `hostChannelAllows`). With the host gated, the only source + * that can bring the intermediary's query to `ready` is the IFRAME supplying the + * content over the open channel — i.e. a document the iframe created. A foreign + * document the iframe merely *requested* has no supplier, so its query stays + * `pending` and this resolves `null` at the grace ceiling. The `ready` signal + * itself is deterministic and usually near-instant; the timer only bounds the + * no-signal (foreign) case. Returning the handle (rather than a boolean) hands + * the author check the exact doc it observed ready. The `settled` guard makes it + * act exactly once. + */ +function waitForIframeSupplied( + progress: ReturnType, + graceMs: number +): Promise | null> { + return new Promise((resolve) => { + let settled = false; + let timer: ReturnType | undefined; + let unsubscribe: (() => void) | undefined; + const finish = (handle: DocHandle | null) => { + if (settled) return; + settled = true; + if (timer !== undefined) clearTimeout(timer); + unsubscribe?.(); + resolve(handle); + }; + const initial = progress.peek(); + if (initial.state === "ready") return finish(initial.handle); + timer = setTimeout(() => finish(null), graceMs); + unsubscribe = progress.subscribe((s) => { + if (s.state === "ready") finish(s.handle); + }); + }); +} + /** * Create an intermediary repo gated by the caller-supplied allowlist and * denylist (both already seeded before this runs). @@ -122,14 +184,32 @@ export interface IntermediaryRepo { * 1. hostChannel — connects intermediary ↔ host repo * 2. iframeChannel — connects intermediary ↔ iframe repo * - * The intermediary's `shareConfig` gates document sync: - * - Only allowlisted documents are accepted from any peer (including the host) - * - Only allowlisted documents are announced to the iframe peer + * The intermediary's `shareConfig` gates sync ASYMMETRICALLY by channel: + * - Host channel: denylist THEN allowlist — a doc crosses only if it is not + * denylisted AND is allowlisted. This is the security boundary; nothing + * denylisted or un-allowlisted enters/leaves the host graph. + * - Iframe channel: fully open — all docs sync both ways. Safe because the + * intermediary is ephemeral and its only other peer (the host) is gated, so + * it can only ever hold allowlisted docs + docs the iframe itself pushed. A + * denylisted doc can never enter the intermediary to reach the iframe. + * + * A document the iframe references that isn't yet allowlisted is left UNDECIDED + * by the gate, which returns a pending promise (see `hostChannelAllows`) rather + * than an immediate deny — this holds the host peer at sharePolicyState + * "loading" so the intermediary's query stays `pending` and the iframe's + * one-shot `find()` never rejects. Meanwhile it is classified out of band (see + * `resolveAccess` below): if the iframe created it (resident and authored solely + * by the iframe) it is auto-allowlisted; otherwise the user is prompted. Once + * decided, the pending promise is resolved and the gate re-run via + * `repo.shareConfigChanged()`, which now answers synchronously from the + * allowlist/denied sets. The gate itself never blocks on a fetch or a prompt — + * doing so would stall the sync (see `hostChannelAllows`). */ export function createIntermediaryRepo( options: IntermediaryRepoOptions ): IntermediaryRepo { - const { allowlist, hostRepo, denylist, onAccessRequest } = options; + const { allowlist, hostRepo, denylist, iframeAuthorId, onAccessRequest } = + options; const hostRepoPeerId = hostRepo.peerId; @@ -145,24 +225,55 @@ export function createIntermediaryRepo( useWeakRef: true, }); - // The one denylist gate, shared by `announce` and `access` so the two can - // never drift apart (both must agree for the security invariant to hold). - // Returns why a document is blocked from crossing to the iframe, or null if - // the denylist permits it (the caller still applies the allowlist). - // - "not-ready": the denylist hasn't finished populating; fail closed to the - // iframe so a protected doc can't sync during the population window. - // Defense in depth — the boot path already awaits readiness before this - // repo exists, so on the normal path this never fires. - // - "denylisted": the document is in the protected set. - // The host peer is never gated here; only iframe-bound sync is. - const denylistBlock = ( - peerId: PeerId, + // Whether a document may cross the HOST channel: it must NOT be denylisted and + // MUST be allowlisted. Terminal answers (denylisted / allowlisted / user- + // denied) return synchronously. An *undecided* document returns a PENDING + // promise that resolves once `resolveAccess` decides it. + // + // Why pending rather than an immediate deny: automerge-repo holds a peer at + // sharePolicyState "loading" while its `access` promise is unresolved, and a + // query with any "loading" peer stays `pending` (never `unavailable`). If we + // denied the host peer up front instead, the intermediary's query would settle + // `unavailable` and emit `doc-unavailable` to the iframe — which makes the + // iframe's one-shot `find()` reject, so the tool gives up and never recovers + // even after the user approves (it would need a full re-render). Holding the + // host peer "loading" keeps the iframe's `find()` pending until we decide, so + // an approval resolves it to `ready` with no re-render. See `resolveAccess`. + // + // While the denylist is still populating we fail closed (allow nothing to/from + // the host) — the boot path awaits denylist readiness before this repo exists, + // so this is defense in depth. This is the single security boundary; the + // iframe channel is open. + const hostChannelAllows = ( documentId: DocumentId - ): "not-ready" | "denylisted" | null => { - if (peerId === hostRepoPeerId) return null; - if (denylist && !denylist.isReady) return "not-ready"; - if (denylist?.has(documentId)) return "denylisted"; - return null; + ): boolean | Promise => { + if (denylist && !denylist.isReady) return false; + if (denylist?.has(documentId)) return false; + if (allowlist.has(documentId)) return true; // approved / auto-allowed + if (denied.has(documentId)) return false; // user said no + + // Undecided: return the SAME pending decision promise on every re-eval + // (dedup), and kick off out-of-band resolution exactly once. + const decision = decisionFor(documentId); + if (!pending.has(documentId)) { + pending.add(documentId); + void resolveAccess(documentId); + } + return decision.promise; + }; + + // Per-channel gate. The iframe channel is fully open (all docs sync both + // ways); the host channel enforces denylist-then-allowlist. Both `access` + // (bidirectional gate) and `announce` (proactive push) use the same rule — so + // once a pending decision resolves true, the host peer becomes "announce" and + // the intermediary actively serves the doc in both directions. + const shareGate = async ( + peerId: PeerId, + documentId?: DocumentId + ): Promise => { + if (!documentId) return peerId !== hostRepoPeerId; + if (peerId !== hostRepoPeerId) return true; // iframe channel: open + return hostChannelAllows(documentId); // host channel: denylist then allowlist }; const repo = new Repo({ @@ -170,45 +281,8 @@ export function createIntermediaryRepo( network: [hostAdapter, iframeAdapter], isEphemeral: true, shareConfig: { - announce: async (peerId: PeerId, documentId?: DocumentId) => { - if (!documentId) return true; - if (denylistBlock(peerId, documentId)) return false; - return allowlist.has(documentId); - }, - access: async (peerId: PeerId, documentId?: DocumentId) => { - if (!documentId) return false; - const blocked = denylistBlock(peerId, documentId); - if (blocked === "not-ready") { - log(`access ${documentId} BLOCKED (denylist not ready)`); - return false; - } - if (blocked === "denylisted") { - log(`access ${documentId} DENIED`); - return false; - } - if (allowlist.has(documentId)) return true; - - // Not allowlisted, not denylisted — prompt the user if this - // is an iframe request and we have a callback - if (peerId !== hostRepoPeerId && onAccessRequest) { - log(`access ${documentId} prompting user`); - const approved = await onAccessRequest(documentId); - if (approved) { - log(`access ${documentId} APPROVED by user`); - // Re-evaluate share config so previously-denied peers - // pick up the new allowlist entry - repo.shareConfigChanged(); - return true; - } - log(`access ${documentId} REJECTED by user`); - return false; - } - - if (peerId !== hostRepoPeerId) { - log(`access ${documentId} BLOCKED`); - } - return false; - }, + announce: shareGate, + access: shareGate, }, }); @@ -221,6 +295,124 @@ export function createIntermediaryRepo( ); hostRepo.networkSubsystem.addNetworkAdapter(isolationHostAdapter); + // ── Out-of-band access resolution ──────────────────────────── + // The gate (hostChannelAllows) returns a pending decision promise for any + // undecided document and enqueues it here. + // - `pending` dedupes: it guards that we spawn exactly one `resolveAccess` + // (and thus at most one prompt) per document, even though the gate is + // re-evaluated repeatedly. + // - `denied` remembers a user's "no" so the gate answers a synchronous false + // without re-prompting. + // - `decisions` holds the single shared pending promise per document, so + // every re-evaluation during the window returns the *same* promise (which + // is what pins the host peer at "loading" — see hostChannelAllows). + const pending = new Set(); + const denied = new Set(); + + type Decision = { + promise: Promise; + resolve: (allow: boolean) => void; + settled: boolean; + }; + const decisions = new Map(); + + const decisionFor = (documentId: DocumentId): Decision => { + let d = decisions.get(documentId); + if (!d) { + let resolve!: (allow: boolean) => void; + const promise = new Promise((res) => { + resolve = res; + }); + d = { promise, resolve, settled: false }; + decisions.set(documentId, d); + } + return d; + }; + + const settleDecision = (documentId: DocumentId, allow: boolean): void => { + // Normally a get — the gate created this decision before spawning + // resolveAccess. The create-if-missing keeps settle safe if a future path + // ever settles a doc before any gate evaluation. + const d = decisionFor(documentId); + if (d.settled) return; + d.settled = true; + d.resolve(allow); + }; + + // Whether every change in a *resident* document was authored by the iframe. + // Reads the already-ready handle directly — it must never trigger a fetch (a + // fetch would re-enter the host-channel gate and deadlock), so the caller is + // responsible for only passing a handle that is already `ready`. The + // length > 0 guard is conservative on purpose: an empty doc doesn't + // auto-allow, it falls through to the prompt. + const isAuthoredSolelyByIframe = (handle: DocHandle): boolean => { + try { + const doc = handle.doc(); + if (!doc) return false; + const metas = getChangesMetaSince(doc, []); + return ( + metas.length > 0 && metas.every((m) => m.author === iframeAuthorId) + ); + } catch (err) { + log(`author check for ${handle.documentId} threw`, err); + return false; + } + }; + + // Decide an undecided document out of band (never blocking the gate), resolve + // its pending decision promise, then re-run the gate via shareConfigChanged(). + const resolveAccess = async (documentId: DocumentId): Promise => { + const finish = (allow: boolean, reason: string) => { + if (allow) { + allowlist.addDocumentId(documentId); + log(`access ${documentId} allowed (${reason})`); + } else { + denied.add(documentId); + log(`access ${documentId} denied (${reason})`); + } + pending.delete(documentId); + // Resolve the pending host-gate promise FIRST (see `hostChannelAllows`), + // then re-run the gate for every doc. The fresh evaluation triggered by + // shareConfigChanged now sees the doc in allowlist/denied and answers a + // synchronous boolean — flipping the host peer to "announce" (so the + // intermediary fetches the doc from the host and pushes it to the + // still-waiting iframe) or to "denied" (query settles unavailable → the + // iframe is told, as it should be on an explicit denial). + settleDecision(documentId, allow); + repo.shareConfigChanged(); + }; + + // Prompt for a foreign document, then finish. + const prompt = async () => { + if (!onAccessRequest) { + finish(false, "no onAccessRequest handler"); + return; + } + log(`access ${documentId} not allowlisted; prompting user`); + let approved = false; + try { + approved = await onAccessRequest(documentId); + } catch (err) { + log(`onAccessRequest for ${documentId} threw`, err); + } + finish(approved, approved ? "user approved" : "user denied"); + }; + + // Classify iframe-created vs foreign. The host peer is held "loading" by the + // pending gate, so the intermediary's query for this doc can only reach + // `ready` if the IFRAME supplied the content over the open channel — i.e. the + // iframe created it. If the iframe doesn't supply it within the grace window, + // it is a FOREIGN document the iframe is merely requesting → prompt. + const progress = repo.findWithProgress(documentId); + const handle = await waitForIframeSupplied(progress, IFRAME_SUPPLY_GRACE_MS); + if (handle && isAuthoredSolelyByIframe(handle)) { + finish(true, "authored by iframe"); + } else { + // Not iframe-supplied, or resident but not solely iframe-authored → prompt. + await prompt(); + } + }; + return { iframePort: iframeChannel.port2, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 075b6c5d..3bb689bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,7 +67,7 @@ importers: version: 1.9.12(@babel/core@7.28.4(supports-color@5.5.0))(solid-js@1.9.13) solid-automerge: specifier: ^2.0.0 - version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13) + version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34(supports-color@5.5.0))(solid-js@1.9.13)(supports-color@5.5.0) solid-js: specifier: ^1.9.9 version: 1.9.13 @@ -115,7 +115,7 @@ importers: version: 2.1.1 solid-automerge: specifier: ^2.0.0 - version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13) + version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34(supports-color@5.5.0))(solid-js@1.9.13)(supports-color@5.5.0) solid-js: specifier: ^1.9.9 version: 1.9.13 @@ -183,7 +183,7 @@ importers: devDependencies: '@chee/patchwork-bundles': specifier: ^0.2.0 - version: 0.2.0(@automerge/automerge@3.2.6)(solid-js@1.9.13) + version: 0.2.0(@automerge/automerge@3.2.6)(solid-js@1.9.13)(supports-color@5.5.0) '@inkandswitch/patchwork-bootloader': specifier: ^0.0.3 version: 0.0.3(@automerge/automerge-repo@2.5.6(supports-color@5.5.0))(@automerge/automerge@3.2.6)(@keyhive/keyhive@0.0.0-alpha.40)(supports-color@5.5.0) @@ -228,7 +228,7 @@ importers: version: 0.2.3(@automerge/automerge-repo-solid-primitives@2.6.0-subduction.39(@automerge/automerge@3.3.0-fragments.1)(solid-js@1.9.13))(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13) solid-automerge: specifier: ^2.0.0 - version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13) + version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34(supports-color@5.5.0))(solid-js@1.9.13)(supports-color@5.5.0) solid-js: specifier: ^1.9.9 version: 1.9.13 @@ -396,7 +396,7 @@ importers: version: 0.0.7(@automerge/automerge@3.3.0-fragments.1)(@codemirror/state@6.7.0)(@codemirror/view@6.43.2)(solid-js@1.9.13) solid-automerge: specifier: ^2.0.0 - version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13) + version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34(supports-color@5.5.0))(solid-js@1.9.13)(supports-color@5.5.0) solid-js: specifier: ^1.9.9 version: 1.9.13 @@ -516,7 +516,7 @@ importers: version: 0.2.3(@automerge/automerge-repo-solid-primitives@2.6.0-subduction.39(@automerge/automerge@3.3.0-fragments.1)(solid-js@1.9.13))(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13) solid-automerge: specifier: ^2.0.0 - version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13) + version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34(supports-color@5.5.0))(solid-js@1.9.13)(supports-color@5.5.0) solid-js: specifier: ^1.9.9 version: 1.9.13 @@ -702,7 +702,7 @@ importers: version: 0.0.6(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.2.6)(@inkandswitch/patchwork-filesystem@0.0.4(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.2.6)) solid-automerge: specifier: ^2.0.0 - version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13) + version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34(supports-color@5.5.0))(solid-js@1.9.13)(supports-color@5.5.0) solid-js: specifier: ^1.9.9 version: 1.9.13 @@ -766,7 +766,7 @@ importers: version: 0.2.3(@automerge/automerge-repo-solid-primitives@2.6.0-subduction.39(@automerge/automerge@3.3.0-fragments.1)(solid-js@1.9.13))(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13) solid-automerge: specifier: ^2.0.0 - version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13) + version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34(supports-color@5.5.0))(solid-js@1.9.13)(supports-color@5.5.0) solid-js: specifier: ^1.9.10 version: 1.9.13 @@ -801,24 +801,27 @@ importers: isolation: dependencies: + '@automerge/automerge': + specifier: ^3.4.0-rev-frag-hex.3 + version: 3.4.0-rev-frag-hex.3 '@automerge/automerge-repo': - specifier: ^2.6.0-subduction.26 - version: 2.6.0-subduction.34(supports-color@5.5.0) + specifier: ^2.7.0-authors.1 + version: 2.7.0-authors.1 '@automerge/automerge-repo-network-messagechannel': - specifier: ^2.6.0-subduction.26 - version: 2.6.0-subduction.34(supports-color@5.5.0) + specifier: ^2.7.0-authors.1 + version: 2.7.0-authors.1 '@inkandswitch/patchwork-elements': specifier: 1.0.0 - version: 1.0.0(@automerge/automerge-repo@2.6.0-subduction.34)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.3.0-fragments.1))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.3.0-fragments.1)))(@inkandswitch/patchwork-providers@0.2.2(@automerge/automerge-repo@2.6.0-subduction.34))(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13)(supports-color@5.5.0) + version: 1.0.0(@automerge/automerge-repo@2.7.0-authors.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)))(@inkandswitch/patchwork-providers@0.2.2(@automerge/automerge-repo@2.7.0-authors.1))(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13) '@inkandswitch/patchwork-filesystem': specifier: ^0.0.8 - version: 0.0.8(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.3.0-fragments.1)(supports-color@5.5.0) + version: 0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3) '@inkandswitch/patchwork-plugins': specifier: ^0.0.11 - version: 0.0.11(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.3.0-fragments.1)) + version: 0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)) '@inkandswitch/patchwork-providers': specifier: 0.2.2 - version: 0.2.2(@automerge/automerge-repo@2.6.0-subduction.34) + version: 0.2.2(@automerge/automerge-repo@2.7.0-authors.1) debug: specifier: ^4.4.3 version: 4.4.3(supports-color@5.5.0) @@ -828,7 +831,7 @@ importers: devDependencies: '@inkandswitch/patchwork-bootloader': specifier: ^0.2.8 - version: 0.2.8(@automerge/automerge-repo-keyhive@0.3.0-alpha.sub.1c(ws@8.21.0))(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.3.0-fragments.1)(@automerge/vanillajs@2.6.0-subduction.29)(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13) + version: 0.2.8(@automerge/automerge-repo-keyhive@0.3.0-alpha.sub.1c(ws@8.21.0))(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)(@automerge/vanillajs@2.6.0-subduction.29)(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13) typescript: specifier: ^5.6.0 version: 5.9.3 @@ -849,7 +852,7 @@ importers: version: 0.0.6(@automerge/automerge-repo@2.6.0-subduction.34)(@inkandswitch/patchwork-filesystem@0.0.4(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.2.6))(@inkandswitch/patchwork-plugins@0.0.6(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.2.6)(@inkandswitch/patchwork-filesystem@0.0.4(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.2.6))) solid-automerge: specifier: ^2.0.0 - version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13) + version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34(supports-color@5.5.0))(solid-js@1.9.13)(supports-color@5.5.0) devDependencies: '@automerge/automerge': specifier: ^3.2.5 @@ -949,7 +952,7 @@ importers: version: 4.4.3(supports-color@5.5.0) solid-automerge: specifier: ^2.0.0 - version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13) + version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34(supports-color@5.5.0))(solid-js@1.9.13)(supports-color@5.5.0) solid-js: specifier: ^1.9.9 version: 1.9.13 @@ -1126,10 +1129,10 @@ importers: version: 6.43.2 '@inkandswitch/patchwork-bootloader': specifier: ^0.2.4 - version: 0.2.8(@automerge/automerge-repo-keyhive@0.3.0-alpha.sub.1c(ws@8.21.0))(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1)(@automerge/vanillajs@2.6.0-subduction.29)(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13) + version: 0.2.8(@automerge/automerge-repo-keyhive@0.3.0-alpha.sub.1c(ws@8.21.0))(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1)(@automerge/vanillajs@2.6.0-subduction.29)(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13) '@inkandswitch/patchwork-plugins': specifier: ^0.0.11 - version: 0.0.11(@automerge/automerge-repo@2.6.0-subduction.39)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.1.2(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.2)) + version: 0.0.11(@automerge/automerge-repo@2.6.0-subduction.39)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.1.2(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)) '@lezer/highlight': specifier: ^1.2.1 version: 1.2.3 @@ -1187,7 +1190,7 @@ importers: version: 4.3.5(solid-js@1.9.13) solid-automerge: specifier: ^2.0.0 - version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13) + version: 2.0.0(@automerge/automerge-repo@2.6.0-subduction.34(supports-color@5.5.0))(solid-js@1.9.13)(supports-color@5.5.0) solid-js: specifier: ^1.9.10 version: 1.9.13 @@ -1371,6 +1374,10 @@ packages: resolution: {integrity: sha512-fW1/75QNhYtuAGey/KhlZksxcFNksYNzAi/S4aU5tgLqHqQkWXPp2ndvbUC7t3cYPKxlf6M/TsnPbmmoO60dRw==} engines: {node: '>=22.13'} + '@automerge/automerge-repo-network-messagechannel@2.7.0-authors.1': + resolution: {integrity: sha512-x8r9F1AHVO87RRwBrzR+xbJyYxTv3+zkYOpZzi49eMrCuKRX0Ufp6m1Qmo0a3f5dRPbDnv8WNfNSpNUlW9YNNw==} + engines: {node: '>=22.13'} + '@automerge/automerge-repo-network-websocket@2.5.0': resolution: {integrity: sha512-zrwbkW8hk3fWkXdeaH+YDfGG63WaB9WLfWYyinZx2AQv4mr6D6UhIhviIjweMmRxMLen6NlIcdu3toHV33ZuKg==} @@ -1490,6 +1497,10 @@ packages: resolution: {integrity: sha512-9r3GS8FtZKoIJOtJqkQR0ukF7HSu+IyV5Iqf7MP3ELDz/TEaUbZV8ysyl5Ju4fVZjmagI/n3oH2h4ySk4FdEFQ==} engines: {node: '>=22.13'} + '@automerge/automerge-repo@2.7.0-authors.1': + resolution: {integrity: sha512-6yN94JYJoLp3IhkfMHnsttLxpx4/99GrXBZzfQ4F2+tHBxpj+AvgXJs1rBynW/apUVyhrYrS6WfmdNNf5zT7dA==} + engines: {node: '>=22.13'} + '@automerge/automerge-subduction@0.12.1': resolution: {integrity: sha512-s1s40RTozkqExIk1LYdogKPzpCMFfe/7KBS0ccAlwsy4JwdcUGG9NQO2Pm3kZq8vD3OPw7DcIOSPG+bnLESQFg==} @@ -1523,6 +1534,9 @@ packages: '@automerge/automerge@3.3.0-fragments.2': resolution: {integrity: sha512-F5KL+YXD0GMFH/as0/kJ3+lotpuJsH1SSrhhKdYSGtR+S9ytBNTw+s3pAjF2Vr0+ZdTMHxsGjqj5SRkJiSNokQ==} + '@automerge/automerge@3.4.0-rev-frag-hex.3': + resolution: {integrity: sha512-EmFMwRWvzWrumg3pwVHqA0ZNhskFcTz3Fdfjwz89ktqXTvbvxwo7xQcA3pI0fTtHUuQE2JFfaZXW+Z7rxDKPyw==} + '@automerge/react@2.6.0-subduction.34': resolution: {integrity: sha512-CKB65u1gSHXTwkyro1/8UmDcNlR4UMHdqrXgbtLgvYff3U0Guptvh1jW8n0+ZIIwJDDTtktSiaBN6TzCpALViw==} engines: {node: '>=22.13'} @@ -5754,6 +5768,15 @@ snapshots: - supports-color - utf-8-validate + '@automerge/automerge-repo-network-messagechannel@2.7.0-authors.1': + dependencies: + '@automerge/automerge-repo': 2.7.0-authors.1 + eventemitter3: 5.0.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@automerge/automerge-repo-network-websocket@2.5.0': dependencies: '@automerge/automerge-repo': 2.5.0 @@ -6170,7 +6193,7 @@ snapshots: - supports-color - utf-8-validate - '@automerge/automerge-repo@2.6.0-subduction.40': + '@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0)': dependencies: '@automerge/automerge': 3.3.0-fragments.2 '@automerge/automerge-subduction': 0.16.0 @@ -6188,6 +6211,24 @@ snapshots: - supports-color - utf-8-validate + '@automerge/automerge-repo@2.7.0-authors.1': + dependencies: + '@automerge/automerge': 3.4.0-rev-frag-hex.3 + '@automerge/automerge-subduction': 0.16.0 + bs58check: 4.0.0 + cbor-x: 1.6.4 + debug: 4.4.3(supports-color@5.5.0) + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.0) + uuid: 14.0.1 + ws: 8.21.0 + xstate: 5.32.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@automerge/automerge-subduction@0.12.1': {} '@automerge/automerge-subduction@0.13.0': {} @@ -6212,6 +6253,8 @@ snapshots: '@automerge/automerge@3.3.0-fragments.2': {} + '@automerge/automerge@3.4.0-rev-frag-hex.3': {} + '@automerge/react@2.6.0-subduction.34(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@automerge/automerge-repo': 2.6.0-subduction.34(supports-color@5.5.0) @@ -6490,11 +6533,11 @@ snapshots: '@cbor-extract/cbor-extract-win32-x64@2.2.2': optional: true - '@chee/patchwork-bundles@0.2.0(@automerge/automerge@3.2.6)(solid-js@1.9.13)': + '@chee/patchwork-bundles@0.2.0(@automerge/automerge@3.2.6)(solid-js@1.9.13)(supports-color@5.5.0)': dependencies: - '@automerge/automerge-repo': 2.6.0-subduction.40 - '@inkandswitch/patchwork-bootloader': 0.3.2(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6)(solid-js@1.9.13) - '@inkandswitch/patchwork-filesystem': 0.0.8(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6) + '@automerge/automerge-repo': 2.6.0-subduction.40(supports-color@5.5.0) + '@inkandswitch/patchwork-bootloader': 0.3.2(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6)(solid-js@1.9.13) + '@inkandswitch/patchwork-filesystem': 0.0.8(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6) es-module-lexer: 2.3.0 resolve.exports: 2.0.3 unplugin: 2.3.11 @@ -7364,20 +7407,48 @@ snapshots: - supports-color - utf-8-validate - '@inkandswitch/patchwork-bootloader@0.2.8(@automerge/automerge-repo-keyhive@0.3.0-alpha.sub.1c(ws@8.21.0))(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1)(@automerge/vanillajs@2.6.0-subduction.29)(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13)': + '@inkandswitch/patchwork-bootloader@0.2.8(@automerge/automerge-repo-keyhive@0.3.0-alpha.sub.1c(ws@8.21.0))(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1)(@automerge/vanillajs@2.6.0-subduction.29)(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13)': dependencies: '@automerge/automerge': 3.3.0-fragments.1 - '@automerge/automerge-repo': 2.6.0-subduction.40 + '@automerge/automerge-repo': 2.7.0-authors.1 + '@automerge/automerge-repo-keyhive': 0.3.0-alpha.sub.1c(ws@8.21.0) + '@automerge/automerge-repo-network-messagechannel': 2.6.0-subduction.29 + '@automerge/automerge-repo-network-websocket': 2.6.0-subduction.29 + '@automerge/automerge-repo-storage-indexeddb': 2.6.0-subduction.29 + '@automerge/automerge-subduction': 0.15.0 + '@automerge/vanillajs': 2.6.0-subduction.29 + '@inkandswitch/patchwork-elements': 2.0.0(@automerge/automerge-repo@2.7.0-authors.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1)))(@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.7.0-authors.1))(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13) + '@inkandswitch/patchwork-filesystem': 0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1) + '@inkandswitch/patchwork-plugins': 0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1)) + '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.7.0-authors.1) + '@keyhive/keyhive': 0.0.0-alpha.56 + '@types/debug': 4.1.13 + debug: 4.4.3(supports-color@5.5.0) + resolve.exports: 2.0.3 + service-worker-types: '@types/serviceworker@0.0.153' + tinyargs: 0.1.4 + transitivePeerDependencies: + - '@types/react' + - bufferutil + - react + - solid-js + - supports-color + - utf-8-validate + + '@inkandswitch/patchwork-bootloader@0.2.8(@automerge/automerge-repo-keyhive@0.3.0-alpha.sub.1c(ws@8.21.0))(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)(@automerge/vanillajs@2.6.0-subduction.29)(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13)': + dependencies: + '@automerge/automerge': 3.4.0-rev-frag-hex.3 + '@automerge/automerge-repo': 2.7.0-authors.1 '@automerge/automerge-repo-keyhive': 0.3.0-alpha.sub.1c(ws@8.21.0) '@automerge/automerge-repo-network-messagechannel': 2.6.0-subduction.29 '@automerge/automerge-repo-network-websocket': 2.6.0-subduction.29 '@automerge/automerge-repo-storage-indexeddb': 2.6.0-subduction.29 '@automerge/automerge-subduction': 0.15.0 '@automerge/vanillajs': 2.6.0-subduction.29 - '@inkandswitch/patchwork-elements': 2.0.0(@automerge/automerge-repo@2.6.0-subduction.40)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1)))(@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.6.0-subduction.40))(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13) - '@inkandswitch/patchwork-filesystem': 0.0.8(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1) - '@inkandswitch/patchwork-plugins': 0.0.11(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1)) - '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.6.0-subduction.40) + '@inkandswitch/patchwork-elements': 2.0.0(@automerge/automerge-repo@2.7.0-authors.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)))(@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.7.0-authors.1))(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13) + '@inkandswitch/patchwork-filesystem': 0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3) + '@inkandswitch/patchwork-plugins': 0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)) + '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.7.0-authors.1) '@keyhive/keyhive': 0.0.0-alpha.56 '@types/debug': 4.1.13 debug: 4.4.3(supports-color@5.5.0) @@ -7448,18 +7519,18 @@ snapshots: - supports-color - utf-8-validate - '@inkandswitch/patchwork-bootloader@0.3.2(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6)(solid-js@1.9.13)': + '@inkandswitch/patchwork-bootloader@0.3.2(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6)(solid-js@1.9.13)': dependencies: '@automerge/automerge': 3.2.6 - '@automerge/automerge-repo': 2.6.0-subduction.40 + '@automerge/automerge-repo': 2.6.0-subduction.40(supports-color@5.5.0) '@automerge/automerge-repo-network-messagechannel': 2.6.0-subduction.39 '@automerge/automerge-repo-network-websocket': 2.6.0-subduction.39 '@automerge/automerge-repo-storage-indexeddb': 2.6.0-subduction.39 '@automerge/automerge-subduction': 0.16.0 - '@inkandswitch/patchwork-elements': 2.0.0(@automerge/automerge-repo@2.6.0-subduction.40)(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6)(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6)))(@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.6.0-subduction.40))(solid-js@1.9.13) - '@inkandswitch/patchwork-filesystem': 0.1.1(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6) - '@inkandswitch/patchwork-plugins': 0.0.11(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6)(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6)) - '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.6.0-subduction.40) + '@inkandswitch/patchwork-elements': 2.0.0(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6)(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6)))(@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0)))(solid-js@1.9.13) + '@inkandswitch/patchwork-filesystem': 0.1.1(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6) + '@inkandswitch/patchwork-plugins': 0.0.11(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6)(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6)) + '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0)) '@keyhive/keyhive': 0.1.0-alpha.5 '@types/debug': 4.1.13 debug: 4.4.3(supports-color@5.5.0) @@ -7558,6 +7629,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@inkandswitch/patchwork-elements@1.0.0(@automerge/automerge-repo@2.7.0-authors.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)))(@inkandswitch/patchwork-providers@0.2.2(@automerge/automerge-repo@2.7.0-authors.1))(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13)': + dependencies: + '@automerge/automerge-repo': 2.7.0-authors.1 + '@inkandswitch/patchwork-filesystem': 0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3) + '@inkandswitch/patchwork-plugins': 0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)) + '@inkandswitch/patchwork-providers': 0.2.2(@automerge/automerge-repo@2.7.0-authors.1) + debug: 4.4.3(supports-color@5.5.0) + optionalDependencies: + '@types/react': 18.3.31 + react: 18.3.1 + solid-js: 1.9.13 + transitivePeerDependencies: + - supports-color + '@inkandswitch/patchwork-elements@2.0.0(@automerge/automerge-repo@2.6.0-subduction.34)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.2.6))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.2.6)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.34)(@automerge/automerge@3.2.6)))(@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.6.0-subduction.34))(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13)': dependencies: '@automerge/automerge-repo': 2.6.0-subduction.34(supports-color@5.5.0) @@ -7628,12 +7713,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-elements@2.0.0(@automerge/automerge-repo@2.6.0-subduction.40)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1)))(@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.6.0-subduction.40))(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13)': + '@inkandswitch/patchwork-elements@2.0.0(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6)(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6)))(@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0)))(solid-js@1.9.13)': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.40(supports-color@5.5.0) + '@inkandswitch/patchwork-filesystem': 0.1.1(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6) + '@inkandswitch/patchwork-plugins': 0.0.11(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6)(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6)) + '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0)) + debug: 4.4.3(supports-color@5.5.0) + optionalDependencies: + solid-js: 1.9.13 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-elements@2.0.0(@automerge/automerge-repo@2.7.0-authors.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1)))(@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.7.0-authors.1))(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13)': dependencies: - '@automerge/automerge-repo': 2.6.0-subduction.40 - '@inkandswitch/patchwork-filesystem': 0.0.8(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1) - '@inkandswitch/patchwork-plugins': 0.0.11(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1)) - '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.6.0-subduction.40) + '@automerge/automerge-repo': 2.7.0-authors.1 + '@inkandswitch/patchwork-filesystem': 0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1) + '@inkandswitch/patchwork-plugins': 0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1)) + '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.7.0-authors.1) debug: 4.4.3(supports-color@5.5.0) optionalDependencies: '@types/react': 18.3.31 @@ -7642,14 +7739,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-elements@2.0.0(@automerge/automerge-repo@2.6.0-subduction.40)(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6)(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6)))(@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.6.0-subduction.40))(solid-js@1.9.13)': + '@inkandswitch/patchwork-elements@2.0.0(@automerge/automerge-repo@2.7.0-authors.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3))(@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)))(@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.7.0-authors.1))(@types/react@18.3.31)(react@18.3.1)(solid-js@1.9.13)': dependencies: - '@automerge/automerge-repo': 2.6.0-subduction.40 - '@inkandswitch/patchwork-filesystem': 0.1.1(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6) - '@inkandswitch/patchwork-plugins': 0.0.11(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6)(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6)) - '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.6.0-subduction.40) + '@automerge/automerge-repo': 2.7.0-authors.1 + '@inkandswitch/patchwork-filesystem': 0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3) + '@inkandswitch/patchwork-plugins': 0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)) + '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.7.0-authors.1) debug: 4.4.3(supports-color@5.5.0) optionalDependencies: + '@types/react': 18.3.31 + react: 18.3.1 solid-js: 1.9.13 transitivePeerDependencies: - supports-color @@ -7742,10 +7841,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6)': + '@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6)': dependencies: '@automerge/automerge': 3.2.6 - '@automerge/automerge-repo': 2.6.0-subduction.40 + '@automerge/automerge-repo': 2.6.0-subduction.40(supports-color@5.5.0) '@types/debug': 4.1.13 '@types/node': 20.19.43 debug: 4.4.3(supports-color@5.5.0) @@ -7753,10 +7852,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1)': + '@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1)': dependencies: '@automerge/automerge': 3.3.0-fragments.1 - '@automerge/automerge-repo': 2.6.0-subduction.40 + '@automerge/automerge-repo': 2.7.0-authors.1 + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + debug: 4.4.3(supports-color@5.5.0) + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)': + dependencies: + '@automerge/automerge': 3.4.0-rev-frag-hex.3 + '@automerge/automerge-repo': 2.7.0-authors.1 '@types/debug': 4.1.13 '@types/node': 20.19.43 debug: 4.4.3(supports-color@5.5.0) @@ -7786,10 +7896,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6)': + '@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6)': dependencies: '@automerge/automerge': 3.2.6 - '@automerge/automerge-repo': 2.6.0-subduction.40 + '@automerge/automerge-repo': 2.6.0-subduction.40(supports-color@5.5.0) '@types/debug': 4.1.13 '@types/node': 20.19.43 debug: 4.4.3(supports-color@5.5.0) @@ -7830,10 +7940,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-filesystem@0.1.2(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.2)': + '@inkandswitch/patchwork-filesystem@0.1.2(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)': dependencies: - '@automerge/automerge': 3.3.0-fragments.2 - '@automerge/automerge-repo': 2.6.0-subduction.40 + '@automerge/automerge': 3.4.0-rev-frag-hex.3 + '@automerge/automerge-repo': 2.7.0-authors.1 '@types/debug': 4.1.13 '@types/node': 20.19.43 debug: 4.4.3(supports-color@5.5.0) @@ -7906,11 +8016,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-subduction.39)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.1.2(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.2))': + '@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-subduction.39)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.1.2(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3))': dependencies: '@automerge/automerge': 3.3.0-fragments.1 '@automerge/automerge-repo': 2.6.0-subduction.39 - '@inkandswitch/patchwork-filesystem': 0.1.2(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.2) + '@inkandswitch/patchwork-filesystem': 0.1.2(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3) '@types/debug': 4.1.13 '@types/node': 20.19.43 debug: 4.4.3(supports-color@5.5.0) @@ -7919,11 +8029,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6)(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6))': + '@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6)(@inkandswitch/patchwork-filesystem@0.1.1(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6))': dependencies: '@automerge/automerge': 3.2.6 - '@automerge/automerge-repo': 2.6.0-subduction.40 - '@inkandswitch/patchwork-filesystem': 0.1.1(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.6) + '@automerge/automerge-repo': 2.6.0-subduction.40(supports-color@5.5.0) + '@inkandswitch/patchwork-filesystem': 0.1.1(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))(@automerge/automerge@3.2.6) '@types/debug': 4.1.13 '@types/node': 20.19.43 debug: 4.4.3(supports-color@5.5.0) @@ -7932,11 +8042,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1))': + '@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1))': dependencies: '@automerge/automerge': 3.3.0-fragments.1 - '@automerge/automerge-repo': 2.6.0-subduction.40 - '@inkandswitch/patchwork-filesystem': 0.0.8(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.3.0-fragments.1) + '@automerge/automerge-repo': 2.7.0-authors.1 + '@inkandswitch/patchwork-filesystem': 0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.3.0-fragments.1) + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + debug: 4.4.3(supports-color@5.5.0) + eventemitter3: 5.0.4 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-plugins@0.0.11(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3)(@inkandswitch/patchwork-filesystem@0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3))': + dependencies: + '@automerge/automerge': 3.4.0-rev-frag-hex.3 + '@automerge/automerge-repo': 2.7.0-authors.1 + '@inkandswitch/patchwork-filesystem': 0.0.8(@automerge/automerge-repo@2.7.0-authors.1)(@automerge/automerge@3.4.0-rev-frag-hex.3) '@types/debug': 4.1.13 '@types/node': 20.19.43 debug: 4.4.3(supports-color@5.5.0) @@ -8113,6 +8236,10 @@ snapshots: dependencies: '@automerge/automerge-repo': 2.6.0-subduction.39 + '@inkandswitch/patchwork-providers@0.2.2(@automerge/automerge-repo@2.7.0-authors.1)': + dependencies: + '@automerge/automerge-repo': 2.7.0-authors.1 + '@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.6.0-subduction.34)': dependencies: '@automerge/automerge-repo': 2.6.0-subduction.34(supports-color@5.5.0) @@ -8121,9 +8248,13 @@ snapshots: dependencies: '@automerge/automerge-repo': 2.6.0-subduction.39 - '@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.6.0-subduction.40)': + '@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.6.0-subduction.40(supports-color@5.5.0))': dependencies: - '@automerge/automerge-repo': 2.6.0-subduction.40 + '@automerge/automerge-repo': 2.6.0-subduction.40(supports-color@5.5.0) + + '@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.7.0-authors.1)': + dependencies: + '@automerge/automerge-repo': 2.7.0-authors.1 '@internationalized/date@3.12.2': dependencies: @@ -10151,7 +10282,7 @@ snapshots: '@noble/hashes': 1.8.0 bs58: 6.0.0 - cabbages@0.2.8: + cabbages@0.2.8(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: @@ -11082,16 +11213,16 @@ snapshots: dependencies: '@automerge/automerge-repo': 2.5.6(supports-color@5.5.0) '@solid-primitives/utils': 6.4.1(solid-js@1.9.13) - cabbages: 0.2.8 + cabbages: 0.2.8(supports-color@5.5.0) solid-js: 1.9.13 transitivePeerDependencies: - supports-color - solid-automerge@2.0.0(@automerge/automerge-repo@2.6.0-subduction.34)(solid-js@1.9.13): + solid-automerge@2.0.0(@automerge/automerge-repo@2.6.0-subduction.34(supports-color@5.5.0))(solid-js@1.9.13)(supports-color@5.5.0): dependencies: '@automerge/automerge-repo': 2.6.0-subduction.34(supports-color@5.5.0) '@solid-primitives/utils': 6.4.1(solid-js@1.9.13) - cabbages: 0.2.8 + cabbages: 0.2.8(supports-color@5.5.0) solid-js: 1.9.13 transitivePeerDependencies: - supports-color @@ -11100,7 +11231,7 @@ snapshots: dependencies: '@automerge/automerge-repo': 2.6.0-subduction.39 '@solid-primitives/utils': 6.4.1(solid-js@1.9.13) - cabbages: 0.2.8 + cabbages: 0.2.8(supports-color@5.5.0) solid-js: 1.9.13 transitivePeerDependencies: - supports-color