/*! * Copyright (c) 2026 Interop Alliance. All rights reserved. */ /** * An `@interop/edv-client` `Transport` that maps Encrypted Data Vault (EDV) * document operations onto ordinary WAS Resource CRUD -- the "EDV-over-WAS" * layout profile (Layer 1). It is paired with `EdvClientCore`, which does all * encryption, decryption, and index blinding client-side; this transport only * moves opaque JWE documents to and from a WAS server, reusing the client's * zcap-signed request layer (`WasClient.request()`). Keys never reach the * server. * * Profile decisions encoded here: * * - **Vault per Collection.** The WAS Collection is the EDV vault; each * encrypted document is one WAS Resource. * - **Restrict-mode ids.** The WAS resource id IS the EDV document id (the * 128-bit multibase value `EdvClientCore.generateId()` produces), which is * URL-safe and never a reserved segment. * - **Encrypted content type.** Documents are stored as `application/json` by * default, so the profile works against an unmodified WAS server. The * preferred type is `application/jose+json` (exported as * `JOSE_CONTENT_TYPE`), which distinguishes EDV envelopes from plaintext * application JSON in listings and metadata -- but the server must register * an `application/*+json` content-type parser to accept it (the reference * was-teaching-server does; a server that does not will reject it with 415). * Pass `contentType: JOSE_CONTENT_TYPE` to opt into it where the server * supports it. * * Scope: documents (`insert` / `update` / `get`, plus `deleteDocument`, which * is not part of the EDV `Transport` contract but lets a driver undo a * half-finished multi-request write) plus blinded-index content * query (`find`, the `blinded-index` profile of the reserved Collection * `POST .../query` endpoint -- the server's `blinded-index-query` backend * feature). `updateIndex` throws: in this profile the `indexed` array rides * inside the stored document envelope, so `update()` IS the re-index * operation and no separate index endpoint exists. Chunked streams * (`storeChunk` / `getChunk`) map each EDV chunk onto the reserved * `/{id}/chunks/{n}` sub-segment (the server's `chunked-streams` affordance), * storing the chunk object as an opaque JSON body -- so `EdvClientCore.insert({ * stream })` / `getStream` drive chunked encrypted blobs over a WAS server * unchanged. Both chunk methods are gated on the backend advertising * `chunked-streams` (throwing `NotSupportedError` when it is absent), like * `find` is on `blinded-index-query`. * `insert` uses an atomic `If-None-Match: *` create when the backend * advertises the optional `conditional-writes` feature; otherwise (and for * `update`) writes are advisory -- the EDV `sequence` is not enforced * (last-writer-wins on `update`). */ import { Transport } from '@interop/edv-client'; import type { IEDVChunk, IEDVQuery, IEncryptedDocument } from '@interop/data-integrity-core'; import type { WasClient } from '../WasClient.js'; import type { FeatureProbe } from '../internal/features.js'; /** * The subset of `WasClient` this transport depends on: the signed-request * escape hatch. Declared structurally so tests can supply a lightweight stub. */ type WasRequester = Pick; export declare class WasTransport extends Transport { #private; readonly spaceId: string; readonly collectionId: string; readonly contentType: string; /** * @param options {object} * @param options.was {WasClient} a WAS client holding the signer * @param options.spaceId {string} the vault's Space id * @param options.collectionId {string} the vault Collection id * @param [options.contentType] {string} content type for stored envelopes; * defaults to `application/json` (accepted by an unmodified server). Pass * `JOSE_CONTENT_TYPE` against a server that registers an * `application/*+json` parser. * @param [options.features] {FeatureProbe} an already-memoized backend * feature probe to reuse (a Collection handle's shared one). Without it the * transport builds its own, reading the backend descriptor itself. * @param [options.documentHeaders] {Record} extra headers * sent with every document write, for a caller that needs the WAS-level * stamps the codec seam applies (notably `Key-Epoch`). Chunk writes do not * carry them -- a chunk is an opaque body, not a resource with metadata. */ constructor({ was, spaceId, collectionId, contentType, features, documentHeaders }: { was: WasRequester; spaceId: string; collectionId: string; contentType?: string; features?: FeatureProbe; documentHeaders?: Record; }); /** * The last document write this transport completed: its id and, where the * backend returned one, the `ETag` validator. `undefined` until the first * document write succeeds, so a driver can also tell "nothing was written" * from "written, but the backend advertises no `conditional-writes`". * * @returns {{ id: string; etag?: string } | undefined} */ get lastDocumentWrite(): { id: string; etag?: string; } | undefined; /** * Deletes a document resource. Not part of the EDV `Transport` contract (the * EDV core never deletes) -- it is the compensating action a driver needs * when a multi-request write fails partway and the document it already wrote * would otherwise be orphaned. * * @param options {object} * @param options.id {string} the document id (= WAS resource id) * @returns {Promise} */ deleteDocument({ id }: { id: string; }): Promise; /** * @inheritdoc * * Inserts a new encrypted document. WAS `PUT` is an upsert, so EDV insert * semantics (`DuplicateError` if the id already exists) need a guard. When * the backend advertises `conditional-writes`, the insert is a single atomic * `PUT` with `If-None-Match: *`, and the server's 412 maps to * `DuplicateError`. Otherwise it degrades to a bodiless existence check * (`HEAD`) before the `PUT` -- advisory and non-atomic, but no longer * downloading the whole stored envelope just to discard it. In either path a * 409 (a `unique: true` blinded attribute already held by another document) * likewise maps to `DuplicateError`. * * @param options {object} * @param options.encrypted {IEncryptedDocument} the document to insert * @returns {Promise} */ insert({ encrypted }?: { encrypted?: IEncryptedDocument; }): Promise; /** * @inheritdoc * * Updates (upserts) an encrypted document. The EDV `sequence` is advisory * here -- without server-side conditional writes, a stale write is not * rejected (last-writer-wins). * * Two write-time conflicts are mapped to the names `EdvClientCore` dispatches * on. A server enforcing conditional writes rejects a stale/sequence * conflict with 412 (precondition-failed), which surfaces as * `InvalidStateError` -- the recoverable case, by re-fetching the current * document and retrying. A 409 is the EDV unique-attribute collision (a * `unique: true` blinded attribute already held by another document), which * is NOT recoverable by re-fetch-and-retry; it surfaces as `DuplicateError`. * * @param options {object} * @param options.encrypted {IEncryptedDocument} the document to update * @returns {Promise} */ update({ encrypted }?: { encrypted?: IEncryptedDocument; }): Promise; /** * @inheritdoc * * Reads an encrypted document by id. Throws a `NotFoundError` (the name * `EdvClientCore` expects) when the resource is missing or not visible. * * @param options {object} * @param options.id {string} the document id to read * @returns {Promise} */ get({ id }?: { id?: string; }): Promise; /** * @inheritdoc * * Runs a blinded-index content query: a signed `POST` of * `{ profile: 'blinded-index', ...query }` to the Collection's reserved * `/query` endpoint. Requires the backend's `blinded-index-query` affordance * (throws `NotSupportedError` when it is absent). The server evaluates the * blinded `equals` / `has` filters against the `indexed` entries of stored * documents (opaque string comparison -- it does no crypto) and returns * `{ documents, hasMore, cursor? }` (the encrypted envelopes verbatim, in * ascending resource-id order, with `cursor` present iff `hasMore`), or a * bare `{ count }` when `query.count` is `true`. The body is returned * untouched: `EdvClientCore.find` decrypts `documents` and passes * `hasMore` / `cursor` through. * * @param options {object} * @param options.query {IEDVQuery} the blinded query (`index` plus one of * `equals` / `has`, and optional `count` / `limit` / `cursor`), as built * by `EdvClientCore`'s `IndexHelper.buildQuery` * @returns {Promise} the server's response body verbatim */ find({ query }?: { query?: IEDVQuery; }): Promise; /** * @inheritdoc * * Not supported, deliberately: in the EDV-over-WAS profile, index entries * are not a separate server-side resource -- the `indexed` array rides * inside the stored document envelope, and every `insert` / `update` * already carries it. Re-indexing a document is therefore an ordinary * `update()` of the full envelope; there is no `/{id}/index` endpoint to * bind this to. */ updateIndex(): Promise; /** * @inheritdoc * * Stores one encrypted chunk of a document's data stream. The EDV chunk * object (`{ sequence, index, jwe, offset }`) is serialized to JSON and * `PUT` as an opaque binary body ({@link CHUNK_CONTENT_TYPE}) to the chunk's * own URL (`.../chunks/{index}`), signed like every other write. The server * stores the bytes verbatim -- it never parses the chunk -- so any * client-side crypto framing is transparent to it. Requires the backend's * `chunked-streams` affordance (throws `NotSupportedError` when it is * absent). The parent Resource must already exist * (`EdvClientCore.insert`/`update` writes the document envelope before * draining the stream), so a 404 here surfaces as a `NotFoundError`. * * @param options {object} * @param options.docId {string} the owning document id (= WAS resource id) * @param options.chunk {IEDVChunk} the encrypted chunk to store * @returns {Promise} */ storeChunk({ docId, chunk }?: { docId?: string; chunk?: IEDVChunk; }): Promise; /** * @inheritdoc * * Reads one encrypted chunk back by index, `GET`ting the chunk's own URL and * parsing the opaque body (stored as raw bytes, so parsed client-side) back * into the EDV chunk object the decrypt stream consumes. Requires the * backend's `chunked-streams` affordance (throws `NotSupportedError` when it * is absent). A missing chunk (404) surfaces as a `NotFoundError` (the name * `EdvClientCore` expects), so a reassembling reader can distinguish it. * * @param options {object} * @param options.docId {string} the owning document id * @param options.chunkIndex {number} the chunk's ordinal index * @returns {Promise} */ getChunk({ docId, chunkIndex }?: { docId?: string; chunkIndex?: number; }): Promise; } export {}; //# sourceMappingURL=WasTransport.d.ts.map