Skip to content

A hosted stack's owner can copy it out over the wire #427

Open

Description

@cuibonobo

Problem

copyStack() (#426) is the one primitive for export, import and moving between storage engines. It reads its source only through StackAdapter methods, so an APIAdapter opened as the owner should be a source like any other. Two things stop that (portability.md § Copying a stack):

  • Blobs can't be enumerated. A source needs listBlobs(), and the wire has no endpoint that lists blobs, so APIAdapter doesn't implement it. copyStack() refuses it with InvalidAdapterError before anything is read.
  • Nothing holds the source still. The spec says the source is not written during a copy and that nothing enforces it, because a local source's only writer is the process copying it. A host keeps taking writes from members and apps, so a copy over the wire yields a target that matches no moment of the source.

Without both, a group on a host can't leave without the host's help.

Decision

No export endpoint and no second format. The wire carries what copyStack() needs from a source, and the copy is the same call it is locally:

const host = await APIAdapter.open({ url, credential, ownerEntityId });
const exported = await copyStack(host, portable('./my-stack.db'));
  • A blob listing, GET /blobs or similar, answering BlobInfo for every blob the store holds. It's served to the owner acting alone (isOwnerActingAlone()), the gate includeUnlisted, purge and POST /types already use, and refused with 403 otherwise, delegation included. A blob listing reveals files no Record the requester reaches names.
  • APIAdapter.listBlobs(), present when discovery advertises the endpoint.
  • A consistent copy, by whatever mechanism the questions below settle. It's stated in portability.md as a rule every source meets, not an exception for the wire.

The rest of a copy already travels: an owner reads every Record with includeDeleted and includeUnlisted, every version and journal entry, every Type, and every blob by fileId. #471 tests that path against a real server.

Questions

  • Consistency. Options: the server pauses writes for the owner's copy; the server serves reads as of one point in time (a snapshot token on each request); or the copy reads the change feed from where it started and refuses if anything landed. Which one keeps the rule the same for local and remote sources?
  • Paging. listBlobs() returns every blob in one array. Does the wire page it, and if so does the adapter method page too, so local and remote stay one contract?
  • Garbage collection. Once APIAdapter has listBlobs(), Stack's collection over the wire can find bare-bytes orphans. Is that wanted, or does the owner-alone gate already make it safe?

Actions

  • Spec: the endpoint in wire-format.md, its owner-alone gate, and its discovery key; the consistency rule in portability.md § Copying a stack; drop the sentence saying a stack behind an APIAdapter can't be copied out.
  • @haverstack/wire-types: the response type and a discovery reader, like supportsInstallRequests().
  • Conformance fixtures: an owner listing, and a refused non-owner and delegated request.
  • APIAdapter.listBlobs(), and a mocked-fetch test that copyStack() accepts it as a source.
  • Changesets for @haverstack/core (if portability.md's rule changes the copy), @haverstack/wire-types, @haverstack/conformance-fixtures and @haverstack/adapter-api.

Order

Unblocked by #426. Land after #472 (it may change the versions and journal endpoints a copy pages) and #473 (it changes discovery). Unblocks #471.

Activity

  1. added theissue type on Oct 10, 2026
  2. changed the title [-]An export endpoint only the owner can call[/-] [+]A hosted stack's owner can copy it out over the wire[/+] on Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions