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
49 changes: 49 additions & 0 deletions .changeset/showcase-checklist-seed-fixtures.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
---
"@objectstack/example-showcase": patch
---

Land the showcase seed fixtures the platform checklist could not run without (#9308)

Three capabilities the platform ships had no fixture anywhere in the reference app, so the
checklist items covering them were not failing — they were unrunnable. Each is closed here
with the smallest stock addition that makes it observable, and with the negative control
left intact.

**A second, actually loginable member.** The demo personas (Mei Phone the submitter, Ada
Auditor the sole `auditor`) have existed as `sys_user` rows since #3409/#3411, and neither
could sign in — so every item needing two acting identities was stuck: per-group 会签 needs
the two groups decided by two different people, submitter-side viewer gating needs the
submitter looking at their own request, and an out-of-office delegation is only falsifiable
when the delegate holds a separate token. The non-obvious half is why a password hash was
never enough: better-auth 1.7 keys accounts on `(issuer, providerAccountId)`, so a
credential row carrying any other issuer is invisible to sign-in, which then fails
`INVALID_EMAIL_OR_PASSWORD` behind a "User not found" warn pointing at the user row rather
than at the account. `seed-approval-demo.ts` now provisions the credential account through
better-auth's own `$context` — its hasher, its `internalAdapter.createAccount` — and READS
the issuer off the dev admin's own credential row instead of re-spelling a constant
`plugin-auth` owns, so the two cannot drift. Dev-only by construction: the bootstrap runs
only where the dev admin exists, and that admin is hard-gated on `NODE_ENV=development`.

**An object that opts into `publicSharing`.** No stock object declared it, so
`POST /share-links` answered 422 `SHARING_NOT_ENABLED` for every showcase object and the
whole downstream half of link sharing — resolve, redaction, the audience and password
gates, fail-closed revoke — was unreachable. `showcase_client_brief` opts in with
`redactFields`, an expiry cap and an `eligibility` predicate, and the seed carries both a
`published` brief (mint-eligible) and a `draft` one (refused `RECORD_NOT_ELIGIBLE`) so the
predicate is falsifiable and not merely satisfied. Every other object still declines the
opt-in, which is what keeps the per-object 422 a real control.

**A `readable: false` FLS grant.** The app governed the three `showcase_project` budget
figures with `readable: true, editable: false` — the WRITE half of field-level security —
and authored no read-withheld grant at all, leaving `plugin-security`'s field masker with
no stock fixture. `showcase_client_liaison` is that grant, on the same three fields, so the
two sets read side by side as the two halves of one mechanism. All three figures move
together because `budget_remaining` is a formula over `budget - spent` and masking one
leaks it back through arithmetic.

Downstream reconciliations, each deliberate: `access-matrix.json` gains two rows and moves
none; the persona × CRUD sweep's census follows the matrix (50/50 → 54/54, arithmetic
recorded at the assertion) and its fixture maps learn the new object; the position count
pin follows the new position. The five checklist items whose `knownGaps` this closes are
revised in the same change — gap text kept, marked closed-by-fixture, `revision` bumped,
`history` appended.
30 changes: 30 additions & 0 deletions docs/qa/platform-checklist/FOLLOW-UPS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,3 +136,33 @@ Ledger 182 → 190 items; `coverage.json` 28 mapped / 2 waived → **30 mapped /
`integration-system.datasource-credential-refusal-matrix`) assert guards that are
already shipped and already public in their ADRs/issues; nothing here discloses an
unfixed hole.

## 6. Resolution 2026-08-18 — §3's fixture list, three of seven paid (#9308)

§3 above is left exactly as written; this section is its resolution row, per this file's
own append-never-rewrite rule. Three of the seven fixtures §3 asked for landed in
`examples/app-showcase/**`, and the checklist items they unblock were revised in the same
change (gap text kept and marked closed-by-fixture, `revision` bumped, `history`
appended — the #7670 pattern).

| §3 line | landed as | items unblocked |
|---|---|---|
| "a `publicSharing.enabled` object" | `showcase_client_brief` — `redactFields`, `maxExpiryDays`, and an `eligibility` predicate, with a `published` AND a `draft` brief seeded so the predicate is falsifiable | `access-security.share-link-capability-tokens` (was `blocked(fixture)`, now runnable) |
| "a second signed-up (non-admin) user in seeds" | both demo personas get a better-auth credential account at boot (`seed-approval-demo.ts` → `ensureCredentialAccount`), issuer derived from the dev admin's own row | `approvals.per-group-signoff`, `approvals.viewer-gating-submitter-side` (was `blocked(fixture)`), `approvals.ooo-delegation-reroute` |
| — (not in §3; found by #9308) | `showcase_client_liaison` — the app's first `readable: false` FLS grant | `access-security.fls-mask-and-strip` clause 5, whose UI half had no reachable fixture |

**Still open from §3, unchanged and still correctly listed there:** the OIDC/social IdP,
the gantt fixture variants, the not-auto-bound audience suggestion, the
`IMPORT_CONSOLE_LIVE` import harness, and the escalation clock-control harness.

**One fixture #9308 scoped but did NOT land: the writable-package summary field** for
`automation.rollup-summary-filter`'s editor half. It is not seed data and could not be
made into seed data. A writable package is a DB-backed `sys_packages` row plus authored
`sys_metadata` items (`isWritablePackage`: a booted CODE package is read-only by
definition, and the showcase is one), and a roll-up needs a parent AND a child object, so
the fixture is a boot-time metadata-authoring bootstrap that mints two tables on every
fresh boot of the reference app. Whether the showcase should ship a permanent writable
base is a showcase design call with consequences beyond this item — it is also the
contrast side `access-security.readonly-package-locks-studio` needs — so it is filed
separately rather than guessed at. The item keeps its `blocked(fixture)` and its
`knownGap` untouched.
34 changes: 23 additions & 11 deletions docs/qa/platform-checklist/areas/access-security.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -621,7 +621,7 @@
"title": "Field-level security: editable:false strips/denies writes API-side and renders read-only in the UI; masked-read half needs an authored readable:false grant",
"since": "v15",
"status": "active",
"revision": 2,
"revision": 3,
"priority": "P1",
"surface": "mixed",
"personas": [
Expand All@@ -632,10 +632,12 @@
"app": "showcase",
"requires": [
"showcase_contributor.fields FLS on showcase_project budget figures (permission-sets.ts — keys are <object>.<field> qualified; bare keys silently enforce nothing, the compile-time lint security-fls-unqualified-key guards that)",
"a seeded showcase_project row with a non-null budget"
"a seeded showcase_project row with a non-null budget",
"showcase_client_liaison — the STOCK readable:false grant since #9308 fixture 4 (permission-sets.ts): showcase_project { allowRead: true } plus fields { 'showcase_project.budget' | '.spent' | '.budget_remaining': { readable: false, editable: false } }, bound to the client_liaison position. Grant it to a FRESH member and to nobody who also holds showcase_contributor: field permissions merge most-permissive across the sets a caller holds, so contributor's readable:true would legitimately un-mask all three"
],
"knownGaps": [
"[API half CLOSED by #9481; the seed gap itself stays open] stock showcase authors NO readable:false FLS grant, so the read-MASKING half (field absent/nulled on GET, plugin-security/src/field-masker.ts) has no stock fixture; to run it, author a scratch permission set carrying readable:false on a showcase_project field and grant it to a fresh member — if the run cannot author one through a supported surface, record that half blocked(fixture) rather than ticking on the write half alone. What changed: showcase-fls-read-mask-strip.dogfood.test.ts does exactly that at runtime, so the SERVER half no longer waits on the seed. The UI half still does — a console render cannot be driven off a permission set that exists only inside a test's stack — and #9308 fixture 4 is the card that would land the stock grant.",
"[CLOSED — API half by #9481, the seed gap itself by #9308 fixture 4; the gap text stays because it carries the reason] stock showcase authored NO readable:false FLS grant, so the read-MASKING half (plugin-security/src/field-masker.ts) had no stock fixture and a run had to author a scratch permission set at runtime — which the SERVER half could do (showcase-fls-read-mask-strip.dogfood.test.ts does exactly that) but the UI half could not, because a console render cannot be driven off a permission set that exists only inside a test's stack. showcase_client_liaison is now that grant on stock seeds, so clause 5 is runnable end to end: grant the set to a fresh member, sign in as them, and compare their API body and their console render against the admin's. packages/qa/dogfood/test/showcase-client-liaison-fixtures.dogfood.test.ts pins the server half of the stock grant (by id, in lists, through an explicit $select) with the admin as the entitled contrast.",
"all three budget figures are withheld together, not just `budget` — `budget_remaining` is a formula over `budget - spent`, so masking one of the three leaks it back through arithmetic. A run that ticks clause 5 having observed only `budget` absent has not checked the mask that matters.",
"MASKED and STRIPPED are two different mechanisms with two different wire shapes, and this item's clause 4 (\"absent or nulled\") predates the measurement. Measured answer: a permission-set readable:false DELETES the key (strip); the #8993 `maskingRule` path REPLACES the value and leaves the key present (mask). A test that only checks \"I did not get the real value\" passes for both and pins neither, so the pin asserts key-absence. The masked half has no fixture at all — do not tick it off the strip pin."
]
},
Expand DownExpand Up@@ -709,6 +711,12 @@
"date": "2026-08-18",
"change": "READ half pinned (QA run #9401 scored this item partial on a write-only pin). automated.ref now names the read pin and states what it does NOT reach: the UI render, and the #8993 maskingRule path, which has no fixture in this repo. knownGaps records the measured mask-vs-strip distinction — readable:false DELETES the key, it does not null it — so the next runner does not tick the masked half off the strip pin",
"ref": "#9481"
},
{
"revision": 3,
"date": "2026-08-18",
"change": "the SEED gap is closed too: showcase_client_liaison is a stock permission set carrying readable:false on all three showcase_project budget figures, so clause 5's UI half is reachable for the first time — a real member, a real console render, no test-local stack. knownGaps rewritten as closed-by-fixture rather than deleted, with the merge caveat (do not also grant showcase_contributor) and the reason all three figures move together. The #8993 maskingRule gap is UNTOUCHED and still has no fixture anywhere in this repo",
"ref": "#9308"
}
]
},
Expand DownExpand Up@@ -1413,7 +1421,7 @@
"title": "Share-link capability tokens: anon resolve renders the record minus redactFields, password/audience gates hold, revoke/expire refuse without leaking",
"since": "v16",
"status": "active",
"revision": 1,
"revision": 2,
"priority": "P2",
"surface": "api",
"personas": [
Expand All@@ -1424,17 +1432,15 @@
"fixtures": {
"app": "showcase",
"requires": [
"an object declaring publicSharing { enabled:true, allowedAudiences, allowedPermissions, redactFields } with at least one visible record — REQUIRED to mint any link"
"an object declaring publicSharing { enabled:true, allowedAudiences, allowedPermissions, redactFields } with at least one visible record — REQUIRED to mint any link",
"showcase_client_brief is that object since #9308 fixture 2: publicSharing { enabled:true, allowedAudiences ['link_only','signed_in','email'], allowedPermissions ['view'], maxExpiryDays 30, redactFields ['internal_notes','deal_value'], eligibility \"record.status == 'published'\" }. The minter persona is a member holding showcase_client_liaison (bound to the client_liaison position); the seed carries a PUBLISHED brief ('Northwind — Website Relaunch brief', mint-eligible) and a DRAFT one ('Fabrikam — Compliance Audit brief', which the eligibility predicate refuses 422 RECORD_NOT_ELIGIBLE)"
],
"knownGaps": [
"NO stock showcase object sets publicSharing.enabled (verified across examples/app-showcase/src) — createLink answers 422 SHARING_NOT_ENABLED for every showcase object, so the resolve/redact/password/revoke clauses are BLOCKED until a publicSharing-enabled fixture object (e.g. qa_public_note with redactFields) is added; only the SHARING_NOT_ENABLED clause is runnable on stock today",
"the GET /share-links/:token/messages half is ai_conversations-only (Cloud/EE service-ai in the `cloud` repo) — split out as a separate knownGap; do not exercise messages against open-framework showcase"
"[CLOSED by #9308 fixture 2 — the gap text stays because it carries the reason] NO stock showcase object set publicSharing.enabled (verified across examples/app-showcase/src), so createLink answered 422 SHARING_NOT_ENABLED for every showcase object and only the SHARING_NOT_ENABLED clause was runnable; the resolve/redact/password/revoke clauses were BLOCKED. showcase_client_brief now opts in (see requires). NOTE the negative control did NOT move: every OTHER showcase object still declines the opt-in, so clause 1's 422 is still runnable — probe it against showcase_private_note, not against the new object",
"the GET /share-links/:token/messages half is ai_conversations-only (Cloud/EE service-ai in the `cloud` repo) — split out as a separate knownGap; do not exercise messages against open-framework showcase",
"audience `public` is deliberately NOT in the fixture object's allowedAudiences (nothing in the showcase wants an un-tokened, indexable surface), so a `public`-audience mint is refused PERMISSION/AUDIENCE-not-allowed rather than being a resolve variant. Record that as the declared policy, not as a defect"
]
},
"blocked": {
"by": "fixture",
"ref": "no showcase object sets publicSharing.enabled — needs a publicSharing-enabled fixture object (PENDING-GAPS.md §C share-link-capability-tokens)"
},
"steps": [
"STOCK-RUNNABLE probe: as a member, POST /api/v1/share-links {\"object\": \"showcase_private_note\", \"recordId\": \"<a visible note id>\"} — capture the 422 SHARING_NOT_ENABLED that proves the block (no showcase object opts in)",
"with a publicSharing-enabled fixture object F (redactFields declared): as a member who can see a record r of F, POST /api/v1/share-links {object:F, recordId:r, redactFields?, audience?, password?, expiresAt?} — capture the 201 {token}",
Expand DownExpand Up@@ -1513,6 +1519,12 @@
"date": "2026-08-08",
"change": "new — share-link capability tokens (ADR-0047): resolve-minus-redactFields, password/audience gates, fail-closed revoke/expire/record-gone, caller-scoped list; blocked(fixture) because no showcase object opts into publicSharing, messages half split as a Cloud/EE knownGap",
"ref": "claude/platform-test-checklist-ocwugl"
},
{
"revision": 2,
"date": "2026-08-18",
"change": "UNBLOCKED. showcase_client_brief is the stock publicSharing fixture and the seed carries both an eligible and an ineligible record, so mint / resolve / redaction are runnable on stock and the declared eligibility predicate is falsifiable too. `blocked` removed; the fixture is named in requires; the knownGap is rewritten as closed-by-fixture rather than deleted, with the warning that clause 1's negative control must stay pointed at an object that did NOT opt in. packages/qa/dogfood/test/showcase-client-liaison-fixtures.dogfood.test.ts pins the mint, the eligibility refusal and the redaction",
"ref": "#9308"
}
]
},
Expand Down
Loading
Loading