Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/reconcile-system-append-only-api-methods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
"@objectstack/service-realtime": patch
"@objectstack/metadata-core": patch
---

fix(identity): close the generic-write apiMethods hole on sys_presence and sys_metadata (#3220)

Follow-through on #1591/#3213 (better-auth apiMethods reconciliation) for two
non-better-auth managed objects that shipped the same contradiction: their
`enable.apiMethods` advertised generic `create`/`update`/`delete` while their
`managedBy` bucket forbids user-context writes, leaving the generic `/data`
route open to a write the bucket does not permit.

- `sys_presence` (`managedBy: 'append-only'`) advertised `create`/`update`/`delete`
(update/delete on an append-only object at that) but is written only over the
realtime websocket/in-memory path, never through ObjectQL. Narrowed to
`['get', 'list']`.
- `sys_metadata` (`managedBy: 'system'`) advertised full CRUD but customization
overlays are authored only through the metadata-protocol RPC (engine writes
carry a transaction context, not a user session); neither the framework nor
the Console (objectui) POSTs `/data/sys_metadata`. Narrowed to `['get', 'list']`.

Reads stay open. The metadata-protocol / realtime write paths are engine-level
and bypass the HTTP exposure gate, so they are unaffected — verified by the
metadata-authoring dogfood and the objectql overlay tests.

A blast-radius audit confirmed the broader `system`/`append-only` buckets are NOT
safe to guard wholesale: several `system` objects (`sys_user_position`,
`sys_user_permission_set`, `sys_position_permission_set`, `sys_user_preference`,
`sys_import_job`) are legitimately user-writable by design (delegated
administration, user preferences, imports). Generalizing the engine write guard
to those buckets is intentionally NOT done here — see #3220 for the bucket-taxonomy
root cause.
8 changes: 7 additions & 1 deletion packages/metadata-core/src/objects/sys-metadata.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,13 @@ export const SysMetadataObject = ObjectSchema.create({
trackHistory: true,
searchable: false,
apiEnabled: true,
apiMethods: ['get', 'list', 'create', 'update', 'delete'],
// #3220 — sys_metadata is `managedBy: 'system'`: customization overlays are
// authored ONLY through the metadata-protocol RPC (its engine writes carry a
// transaction context, not a user session), never the generic /data route.
// Neither the framework nor the Console (objectui) POSTs /data/sys_metadata,
// so the generic write verbs were a latent hole for a user-context raw write
// the bucket forbids. Reads stay open for the Setup "Data Model" grids.
apiMethods: ['get', 'list'],
trash: false,
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,12 @@ describe('SysPresence object definition', () => {
it('should have API enabled', () => {
expect(SysPresence.enable?.apiEnabled).toBe(true);
});

it('exposes reads only — append-only, written over the realtime path, never /data (#3220)', () => {
expect(SysPresence.enable?.apiMethods).toEqual(['get', 'list']);
// Guard against re-introducing a generic write verb on an append-only object.
for (const verb of ['create', 'update', 'delete', 'upsert']) {
expect(SysPresence.enable?.apiMethods).not.toContain(verb);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,13 @@ export const SysPresence = ObjectSchema.create({
trackHistory: false,
searchable: false,
apiEnabled: true,
apiMethods: ['get', 'list', 'create', 'update', 'delete'],
// #3220 — sys_presence is `managedBy: 'append-only'` and is written only
// over the realtime (websocket/in-memory) path, never through ObjectQL.
// Advertising create/update/delete here (and update/delete on an
// append-only object at that) was a latent hole: it left the generic
// /data route open for a user-context write the bucket forbids. Reads
// stay open for diagnostic list views.
apiMethods: ['get', 'list'],
trash: false,
mru: false,
},
Expand Down