/*! * Copyright (c) 2026 Interop Alliance. All rights reserved. */ /** * WasRemoteStore (delegated-only): the relying-party view of the user's WAS * Space, stripped to the RP model -- this app NEVER provisions the Space, never * derives the spaceId, and never touches the `id` collection or DID publishing. * It receives a set of wallet-delegated zcaps, reads the server URL + space id * straight out of their `invocationTarget`s ({@link parseGrants}), and holds: * * - a `WasClient` wrapping the app's own `ZcapClient` (its invocation signer is * the seed-derived controller the grants were delegated to); * - per-collection capability routing, so each sync request invokes the exact * collection grant; * - a best-effort encryption-descriptor PUT (whether a delegated collection-scoped * RW zcap authorizes writing the collection description). It is non-fatal * either way -- envelopes replicate into an unmarked (plaintext) collection * just the same. A PUBLIC collection is never marked: public implies * plaintext, so the descriptor PUT is skipped outright; * - the sibling best-effort index declarations -- the `indexes` description PUT * for public collections and the blinded-index schema write for private ones * ({@link WasRemoteStore.declareBlindedIndexes}) -- plus the equality query * verb itself ({@link WasRemoteStore.queryCollectionByEquality}), which * routes on the collection's visibility: the canonical sorted * `filter[attr]=value` GET for a public collection, the client-blinded * `blinded-index` query for a private one, both parsed into the same * `{ documents, hasMore, cursor? }` page shape. */ import type { ZcapClient } from '@interop/ezcap'; import type { IKeyAgreementKey, IKeyResolver, IZcap } from '@interop/data-integrity-core'; import { WasClient } from '@interop/was-client'; import type { CollectionEncryption } from '@interop/was-client'; import type { EncryptionDescriptorSource } from '@interop/wallet-core/descriptors'; import type { WasCollectionConfig } from '../config.js'; import type { ParsedGrants } from '../grants.js'; /** * The outcome of a best-effort declaration write (an encryption-descriptor PUT, * an `indexes` PUT, or a blinded-index schema write), for diagnostics. */ export interface DeclarationResult { collectionId: string; ok: boolean; status?: number; error?: string; /** * True when no write was attempted -- the declaration does not apply to this * collection at all (e.g. the encryption descriptor on a public collection), * or it already carries an `encryption` block (e.g. an epoch roster the wallet * provisioned at consent time, which a bare-descriptor PUT must never clobber). * Reported as `ok` since the goal state holds either way. */ skipped?: boolean; } /** * One page of equality-query results: the shared shape of the GET * `filter[attr]=value` filter and the POST `equality` query profile. `data` is * the stored JSON content (absent for a blob resource); `custom` is the * resource's custom metadata object, present when it has one. The opaque * `cursor` continues the page walk when `hasMore` is true. */ export interface EqualityQueryPage { documents: Array<{ id: string; data?: unknown; custom?: unknown; }>; hasMore: boolean; cursor?: string; } export declare class WasRemoteStore { #private; readonly was: WasClient; readonly serverUrl: string; readonly spaceId: string; private constructor(); /** * Builds a delegated remote store from a parsed grant set and the app's * ZcapClient (whose invocation signer is the controller the grants target). * * Replication itself needs no keystore at all: it moves opaque envelopes * verbatim through `was.request()`, which bypasses the codec, and * encrypt/decrypt is a local read/write concern. The keystore matters only * for the codec-driven verbs -- the blinded-index query and schema * declaration -- so it answers with the app's identity keys when they are * supplied and with `null` (fail-closed: "this client holds no keys for that * collection") when they are not. The blinding key itself is never passed * here: the codec unwraps it from the collection's own encryption descriptor, * so a collection provisioned without one simply has no blinded index. * * @param options {object} * @param options.parsed {ParsedGrants} * @param options.zcapClient {ZcapClient} * @param [options.collections] {WasCollectionConfig[]} the collection * registry; entries with `visibility: 'public'` are never marked encrypted * @param [options.keys] {object} this app's identity key-agreement key and * its resolver (the same pair `IdentityAgents` carries) * @param options.keys.keyAgreementKey {IKeyAgreementKey} * @param options.keys.keyResolver {IKeyResolver} * @returns {WasRemoteStore} */ static fromGrants({ parsed, zcapClient, collections, keys }: { parsed: ParsedGrants; zcapClient: ZcapClient; collections?: WasCollectionConfig[]; keys?: { keyAgreementKey: IKeyAgreementKey; keyResolver: IKeyResolver; }; }): WasRemoteStore; /** * The delegated capability for one WAS collection, or `undefined` when no * grant covers it (the sync port then invokes without a capability and the * server denies it -- the intended fail-closed mode). * * @param collectionId {string} the WAS collection id * @returns {IZcap | undefined} */ collectionCapability(collectionId: string): IZcap | undefined; /** * Reads one collection's `encryption` descriptor from its Collection Description, * invoked with that collection's delegated zcap. Returns the * {@link CollectionEncryption} block (a multi-recipient descriptor carries key * epochs) or `undefined` when the collection carries no descriptor. Non-fatal like * the descriptor PUTs: returns `undefined` rather than throwing when no capability * covers the collection or the read fails (offline, unauthorized), so a caller * can fall back to its cached copy. * * @param collectionId {string} the WAS collection id * @returns {Promise} */ readCollectionEncryption(collectionId: string): Promise; /** * Reads one collection's stored `/meta` value RAW, invoked with that * collection's delegated zcap. On an encrypted collection the returned * `custom` is the opaque metadata envelope exactly as stored -- which is what * the local store's cipher wants, since it decodes the envelope itself * (`applyMeta`) to recover the persisted blinded-index schema. Deliberately * NOT `Collection.meta()`: that one DECODES `custom` to plaintext, which * `applyMeta` cannot consume. * * Non-fatal like the descriptor read: answers `undefined` rather than * throwing when no capability covers the collection, the read is unauthorized, * the app is offline, or the backend has no metadata support. * * @param collectionId {string} the WAS collection id * @returns {Promise<{ custom?: unknown } | undefined>} */ readCollectionMeta(collectionId: string): Promise<{ custom?: unknown; } | undefined>; /** * Best-effort declaration of the `{ encryption: { scheme: 'edv' } }` descriptor on * one collection, invoked with that collection's delegated RW zcap. Non-fatal: * returns the outcome rather than throwing, so a server that does not authorize * a delegated description write leaves replication untouched (the collection * simply stays unmarked / plaintext, which still stores envelopes). A PUBLIC * collection is never marked (public implies plaintext): the PUT is skipped * and reported as `ok` + `skipped`. * * A collection that ALREADY carries an `encryption` block is also skipped: the * wallet provisions a multi-recipient epoch roster on the descriptor at consent * time, and overwriting it with the bare `{ scheme: 'edv' }` descriptor would * destroy that roster. The description is read first, so this method is a * no-op fallback for servers/wallets that did not provision the roster. * * @param collectionId {string} the WAS collection id * @param options {object} * @param options.encryption {CollectionEncryption | undefined} the * already-read descriptor the caller fetched from the collection * description (the bootstrap reads it once and feeds both the cipher * rebuild and this guard). `undefined` means "read, and the collection * carries no descriptor" -- not "unknown" * @returns {Promise} */ markCollectionEncrypted(collectionId: string, { encryption }: { encryption: CollectionEncryption | undefined; }): Promise; /** * Best-effort declaration of a public collection's equality-indexed * attributes (`{ indexes: [...] }`) on its collection description, invoked * with that collection's delegated RW zcap. The server rejects * `filter[attr]=value` queries on undeclared attributes fail-closed, so a * public collection that wants `store.query()` must announce its `indexes` * here. Non-fatal like the encryption descriptor: returns the outcome rather * than throwing. Skipped (reported `ok` + `skipped`) for a private * collection or one that declares no indexes. * * @param collectionId {string} the WAS collection id * @returns {Promise} */ declareCollectionIndexes(collectionId: string): Promise; /** * Best-effort declaration of a PRIVATE collection's blinded-index attributes. * Unlike the public `indexes` PUT, the schema is collection state stored in * the collection's own ENCRYPTED metadata (a compare-and-swap write through * `Collection.declareIndex`), so every recipient discovers what is queryable * without out-of-band coordination and the server never sees the attribute * names. Only the attributes not already in the persisted schema are * declared, so a returning session issues no writes at all. Non-fatal like * the descriptor PUTs: returns the outcome rather than throwing. Skipped * (reported `ok` + `skipped`) for a public collection or one that declares no * indexes. * * A collection whose descriptor carries no `hmac` member is reported NOT ok * (rather than skipped): the blinding key is installed with the collection's * first key epoch or never, so this is a provisioning gap the caller should * warn about -- the declarations cannot be made and queries on the collection * will keep failing. * * Declarations are prospective: a document written before its attribute was * declared carries no blinded entry for it and is not findable until it is * rewritten. * * @param collectionId {string} the WAS collection id * @param options {object} * @param options.encryption {CollectionEncryption | undefined} the * already-read descriptor the caller fetched from the collection * description; `undefined` means the collection carries none * @returns {Promise} */ declareBlindedIndexes(collectionId: string, { encryption }: { encryption: CollectionEncryption | undefined; }): Promise; /** * Runs one equality query against a registered collection, routing on its * visibility and answering the same `{ documents, hasMore, cursor? }` page * either way. Values are string equality only, and multiple `equals` * attributes AND together. * * - A PUBLIC (plaintext) collection uses the canonical GET * `filter[attr]=value` form of the server's `equality` profile, invoked * with the collection's delegated zcap (an anonymous reader would issue the * same URL unsigned against a `PublicCanRead` collection). Filter * attributes are emitted in sorted order so identical queries produce * identical URLs (cache-friendly). * - A PRIVATE (encrypted) collection uses the `blinded-index` query profile: * each attribute name and value is blinded client-side with the * collection's blinding key before it leaves the browser, the server * matches opaque tokens, and the returned envelopes are decrypted here. * Attribute names are rooted at the EDV document's `content`, which for a * JSON payload IS the stored payload verbatim, so the configured `author` * is queried as `content.author`. * * Fails closed before any network round trip on a collection the registry * does not know, an empty term set, an attribute missing from the * collection's declared `indexes`, an uncovered collection, and -- on the * private path -- a store built without this app's identity keys. * * Standing limitation of the private path: the sync path emits blinded * entries only once the bootstrap has installed the collection's persisted * schema on its cipher, so a document written before that install carries no * entries and a blinded query does not find it. Such a document becomes * findable once it is rewritten. * * @param options {object} * @param options.collectionId {string} the WAS collection id * @param options.equals {Record} equality terms; multiple * attributes AND together * @param [options.limit] {number} page size (server default when omitted) * @param [options.cursor] {string} opaque continuation cursor from the * prior page * @returns {Promise} */ queryCollectionByEquality({ collectionId, equals, limit, cursor }: { collectionId: string; equals: Record; limit?: number; cursor?: string; }): Promise; /** * The world-readable share URL for one document in a public (plaintext) * collection: the exact URL an unauthenticated reader fetches (e.g. via * `WasClient.publicRead`) to consume a share link. Because a public * collection stores the payload under its own logical `id`, this URL is * stable across edits of the document. Fails closed before composing * anything on a non-public collection (the encrypted path stores under a * random envelope id, so no stable public URL exists), an empty id, or a * collection no delegated capability covers (catching typo'd or * unprovisioned collection ids). The URL resolves publicly only once the * document has replicated to the server -- a locally-inserted doc shares * after the next sync push. * * @param options {object} * @param options.collectionId {string} the WAS collection id * @param options.id {string} the document's logical uuid * @returns {string} */ publicUrlFor({ collectionId, id }: { collectionId: string; id: string; }): string; } /** * The `EncryptionDescriptorSource` (`@interop/wallet-core/descriptors`) over a * delegated {@link WasRemoteStore}: one Collection Description read per * collection, invoked with THAT collection's delegated zcap. It is the seam the * descriptor-refresh machinery -- the local store's unknown-epoch policy and a * shared collection's self-refreshing cipher -- re-reads a rotated key-epoch * roster through. * * Deliberately NOT `wasDescriptorSource` from the same subpath: that one * describes through the client's root capability, which a relying-party app * never holds. Every read here must invoke the per-collection grant, which is * exactly what {@link WasRemoteStore.readCollectionEncryption} does (and, like * the rest of the descriptor plumbing, it answers `undefined` rather than * throwing when the read is unauthorized or the app is offline). * * @param options {object} * @param options.remoteStore {WasRemoteStore} * @returns {EncryptionDescriptorSource} */ export declare function remoteDescriptorSource({ remoteStore }: { remoteStore: WasRemoteStore; }): EncryptionDescriptorSource; //# sourceMappingURL=wasRemoteStore.d.ts.map