You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.
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).
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.
Problem
The
_configsingleton is the stack's identity and configuration —ownerEntityIdis read from it at open and consulted by every permission check — and nothing protects it:update('_config', { entityId: 'someone-else' })works. It passes_config@1schema validation (entityIdis just a required string) and changes stack ownership out from under a running system. Every owner check,ScopedStackbypass, 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:entityIdbecomes the stack's DID.delete('_config', { hard: true })bricks the stack. The sqljs adapter'sreadConfig()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)."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;
Stackis the library's invariant layer;ScopedStackis per-requester policy. Schema validation already lives inStackabove the adapters — integrity guards for_configare the same class of thing, written once and inherited by every adapter and byScopedStack(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.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).Stack.update('_config')rejects changes toentityId. Other fields (timezonetoday) 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._configrecord; it is addressable only by ID throughgetConfig-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,MemoryAdapterimplements 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._configrow. The guard of record is core's.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
Stack.delete()/Stack.update()(+ restore path)_configaddressability + protections), §Queries (exclusion rule), error-table rowMemoryAdapter(Core code treats one query() page as the complete result set (grants, attachment metadata, uploader checks) #50)_config; timezone update succeedsScopedStack-as-owner paths (delegation means core guards hold there too)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).