diff --git a/.changeset/reconcile-system-append-only-api-methods.md b/.changeset/reconcile-system-append-only-api-methods.md new file mode 100644 index 0000000000..3f664ef217 --- /dev/null +++ b/.changeset/reconcile-system-append-only-api-methods.md @@ -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. diff --git a/packages/metadata-core/src/objects/sys-metadata.object.ts b/packages/metadata-core/src/objects/sys-metadata.object.ts index f24e88cb60..144274ab8a 100644 --- a/packages/metadata-core/src/objects/sys-metadata.object.ts +++ b/packages/metadata-core/src/objects/sys-metadata.object.ts @@ -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, }, diff --git a/packages/services/service-realtime/src/objects/sys-presence.object.test.ts b/packages/services/service-realtime/src/objects/sys-presence.object.test.ts index 029327f745..9f3e84e1b0 100644 --- a/packages/services/service-realtime/src/objects/sys-presence.object.test.ts +++ b/packages/services/service-realtime/src/objects/sys-presence.object.test.ts @@ -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); + } + }); }); diff --git a/packages/services/service-realtime/src/objects/sys-presence.object.ts b/packages/services/service-realtime/src/objects/sys-presence.object.ts index 26b1c458f8..562961a00f 100644 --- a/packages/services/service-realtime/src/objects/sys-presence.object.ts +++ b/packages/services/service-realtime/src/objects/sys-presence.object.ts @@ -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, },