Skip to content

_config is unprotected: update can change stack ownership, hard delete bricks the stack, query exclusion is a per-adapter convention #67

Description

@cuibonobo

Problem

The _config singleton is the stack's identity and configuration — ownerEntityId is read from it at open and consulted by every permission check — and nothing protects it:

  1. update('_config', { entityId: 'someone-else' }) works. It passes _config@1 schema validation (entityId is just a required string) and changes stack ownership out from under a running system. Every owner check, ScopedStack bypass, and "owner records carry no entityId" inference silently re-anchors. RFC: identity model — entityId becomes a DID (did:key floor), _entity records become local profiles #49 raises the stakes: entityId becomes the stack's DID.
  2. delete('_config', { hard: true }) bricks the stack. The sqljs adapter's readConfig() throws on next open when the row is missing (packages/record-adapter-sqljs/src/index.ts:515). Soft delete is nearly as bad (config unreadable through normal paths).
  3. Query exclusion is a hardcoded SQL string in one adapter"r.id != '_config'" in the sqljs where-builder (packages/record-adapter-sqljs/src/index.ts:283). Every future adapter must independently remember it; MemoryAdapter (per Core code treats one query() page as the complete result set (grants, attachment metadata, uploader checks) #50's fidelity findings) is the existence proof that conventions get forgotten.

Decided direction

Layering principle (settled in discussion): adapters are storage engines; Stack is the library's invariant layer; ScopedStack is per-requester policy. Schema validation already lives in Stack above the adapters — integrity guards for _config are the same class of thing, written once and inherited by every adapter and by ScopedStack (which delegates). The exception is query exclusion, which must stay pushed down (it's a WHERE predicate; post-filtering in core breaks pagination) — so it gets promoted from convention to contract instead of moved.

  1. Stack.delete('_config') — soft or hard — is rejected (StackConflictError: the operation conflicts with stack integrity; 409 + APIAdapter collapses the error taxonomy: Stack* errors never survive the wire round trip #53 code).
  2. Stack.update('_config') rejects changes to entityId. Other fields (timezone today) update normally. Ownership/identity transfer, if it ever exists, is a deliberate RFC: identity model — entityId becomes a DID (did:key floor), _entity records become local profiles #49-followup API with key custody semantics — not a field write.
  3. Query exclusion becomes normative spec + shared implementation + fixture: the spec states the rule (generic queries never return the _config record; it is addressable only by ID through getConfig-shaped paths), Split SQLite support: native record-adapter-sqlite for Node, browser-only record-adapter-sqljs, and a StackTokenStore interface in core #46's shared SQL layer implements it once for both SQLite adapters, MemoryAdapter implements it for real (Core code treats one query() page as the complete result set (grants, attachment metadata, uploader checks) #50), and a PATCH contract mismatch: spec defines content-only merge patch, APIAdapter sends record-field envelopes #52 conformance fixture asserts it so new adapters can't silently forget.
  4. Non-normative defense-in-depth: adapters MAY additionally refuse to hard-delete the _config row. The guard of record is core's.
  5. restoreVersion('_config', …) inherits protection via the update path (restoreVersion() must validate the snapshot and restore its typeId #62's validation runs; entityId-change rejection applies to restored content too).

Work items

Refs #49 (entityId → DID; ownership transfer deferred there), #53 (error code), #46 (shared SQL layer), #50 (MemoryAdapter fidelity), #52 (fixtures), #62 (restore path), #55 (reserved-ID validation keeps clients from minting _-prefixed records; this issue guards the one that ships with the stack).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions