fix: complete the v17 permission, approval and media migrations - #580
Merged
os-zhuang merged 1 commit intoAug 1, 2026
Conversation
The version bump leaves three 17.0 contracts half-migrated. The first is a silent outage. allowExport became an opt-in bit. Before 17.0 an unset value inherited read, so "can list => can export" and the bit only ever hid a button; 17.0 inverted the default, and `resolveUserExportAllowed` now demands an explicit `allowExport: true` with neither `viewAllRecords` nor `modifyAllRecords` standing in for it. No profile carried the bit, so every CSV/XLSX list export and every report export would have 403'd for every user, system_admin included — while `os validate`, `os build` and the whole suite stayed green, because the grant is well-formed, it just isn't there. Grant it on the five objects with a real export surface (a list view declaring `exportOptions`, or a report whose dataset is built on them), for each profile that already reads them. `guest_portal` gets none: ADR-0090 D9 classes `allowExport` as high-privilege, so a set carrying it stops binding to the `guest` anchor entirely. New guards in authorization-coverage fail in both directions — an export surface nobody can reach, and a grant with no surface behind it — so the next `exportOptions` cannot ship without its grant. Approval nodes drop the hand-rolled `org_membership_level: 'owner'` approver for the native `onEmptyApprovers: 'admin_rescue'`. That entry existed only to avoid the empty-position dead-end, and with `behavior: 'first_response'` it overshot: it made an org owner a routine approver on every deal over $100K rather than a rescue when the bench is empty. Media fields declare `accept` / `maxSize`, which 17.0 enforces server-side against the stored sys_file (ADR-0104 D3 wave 2). Previously the upload widget read them while FieldSchema dropped them at parse, so the constraint never reached a caller talking to the API directly. Also drop the `edit`/`delete`/`view` builtin whitelist from the rowActions guard. 17.0's `action-name-undefined` rule refuses a rowActions string naming no defined action, so the whitelist was letting metadata past CI that the platform then rejects. MAINTENANCE.md §3.2 documents the `os migrate` data gates and the `OS_ALLOW_LAX_*` escape hatches: `pnpm verify` and `demo:reset` start from an empty database, so neither can catch what an in-place upgrade needs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHzpTPCxNfskro88FRuBwH
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
os-zhuang
marked this pull request as ready for review
August 1, 2026 00:57
os-zhuang
added a commit
that referenced
this pull request
Aug 1, 2026
* feat: upgrade ObjectStack to 17.0.0-rc.1 * chore: sync StackBlitz npm lockfile * fix: complete the v17 permission, approval and media migrations (#580) The version bump leaves three 17.0 contracts half-migrated. The first is a silent outage. allowExport became an opt-in bit. Before 17.0 an unset value inherited read, so "can list => can export" and the bit only ever hid a button; 17.0 inverted the default, and `resolveUserExportAllowed` now demands an explicit `allowExport: true` with neither `viewAllRecords` nor `modifyAllRecords` standing in for it. No profile carried the bit, so every CSV/XLSX list export and every report export would have 403'd for every user, system_admin included — while `os validate`, `os build` and the whole suite stayed green, because the grant is well-formed, it just isn't there. Grant it on the five objects with a real export surface (a list view declaring `exportOptions`, or a report whose dataset is built on them), for each profile that already reads them. `guest_portal` gets none: ADR-0090 D9 classes `allowExport` as high-privilege, so a set carrying it stops binding to the `guest` anchor entirely. New guards in authorization-coverage fail in both directions — an export surface nobody can reach, and a grant with no surface behind it — so the next `exportOptions` cannot ship without its grant. Approval nodes drop the hand-rolled `org_membership_level: 'owner'` approver for the native `onEmptyApprovers: 'admin_rescue'`. That entry existed only to avoid the empty-position dead-end, and with `behavior: 'first_response'` it overshot: it made an org owner a routine approver on every deal over $100K rather than a rescue when the bench is empty. Media fields declare `accept` / `maxSize`, which 17.0 enforces server-side against the stored sys_file (ADR-0104 D3 wave 2). Previously the upload widget read them while FieldSchema dropped them at parse, so the constraint never reached a caller talking to the API directly. Also drop the `edit`/`delete`/`view` builtin whitelist from the rowActions guard. 17.0's `action-name-undefined` rule refuses a rowActions string naming no defined action, so the whitelist was letting metadata past CI that the platform then rejects. MAINTENANCE.md §3.2 documents the `os migrate` data gates and the `OS_ALLOW_LAX_*` escape hatches: `pnpm verify` and `demo:reset` start from an empty database, so neither can catch what an in-place upgrade needs. Claude-Session: https://claude.ai/code/session_01YHzpTPCxNfskro88FRuBwH Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Follow-up to #579, stacked on its branch. The version bump there is sound, but three 17.0 contracts are left half-migrated. The first is a silent outage: it passes
os validate,os buildand the entire test suite, and only fails at runtime, for every user.Type of Change
Related Issues
Related to #579
Changes Made
allowExport— the opt-in export axis (upstream #3544). This is the blocker.Before 17.0 an unset
allowExportinherited read, so "can list ⇒ can export" and the bit only ever hid a button. 17.0 inverted the default:Neither
viewAllRecordsnormodifyAllRecordssubstitutes. No profile on the branch carried the bit, so every CSV/XLSX list export and every report export would have 403'd for every user,system_adminincluded — at both bulk-egress doors, the list views' built-inexportOptionsandReportService.assertExportAllowed(EXPORT_NOT_PERMITTED). Nothing in CI catches it, because the grant is well-formed; it just isn't there.The grant is now authored on the five objects with a real export surface —
crm_account,crm_case,crm_contact,crm_lead,crm_opportunity— for each profile that already reads them.guest_portaldeliberately carries none: ADR-0090 D9 classesallowExportas high-privilege, so a set holding it stops binding to theguestanchor altogether. Canonical rationale lives insrc/profiles/index.ts.Export is read-derived, so the grant opens the door without widening the rows: a
sales_repexportingcrm_opportunitystill gets their own book.Approvals —
opportunity_approval's two nodes drop the hand-rolledorg_membership_level: 'owner'approver for the nativeonEmptyApprovers: 'admin_rescue'. That entry existed only to avoid the empty-position dead-end, and withbehavior: 'first_response'it overshot: it made an org owner a routine approver on every deal over $100K rather than a rescue when the bench is empty. This is a deliberate behaviour change — flagging it explicitly.Media fields —
crm_product.image,crm_product.datasheet,crm_account.logo,crm_contact.avatardeclareaccept/maxSize, which 17.0 enforces server-side against the storedsys_file(ADR-0104 D3 wave 2). Previously the upload widget read them whileFieldSchemadropped them at parse, so the constraint never reached a caller talking to the API directly.rowActions guard — dropped the
edit/delete/viewbuiltin whitelist fromtest/metadata-references.test.ts. 17.0'saction-name-undefinedrule refuses arowActionsstring naming no defined action, so the whitelist was letting metadata past CI that the platform then rejects. (This confirms #579's removal ofrowActionswas correct and required — an earlier read of mine, based on the Zod schema alone, had it backwards;os validatesettled it.)Docs —
docs/MAINTENANCE.md§3.2 covers theos migratedata gates (files-to-references,value-shapes) and theOS_ALLOW_LAX_*escape hatches.pnpm verifyanddemo:resetcannot catch these: they start from an empty database, so a green local run says nothing about an in-place upgrade.Testing
crm_campaign_member/crm_task)Full
pnpm verify(validate → typecheck → lint → hygiene → build → test) is green against@objectstack/*17.0.0-rc.1 installed from the branch lockfile.New guards in
test/authorization-coverage.test.tsderive the export surfaces from real metadata (views declaringexportOptions, plus every report's dataset object, walkingblocks[]) and fail in both directions — an export surface no profile can reach, and a grant with no surface behind it. Teeth verified by a negative run: addingallowExporttocrm_quotefails withundeclared bulk egress.Checklist
.changeset/upgrade-objectstack-v17-rc.mdAdditional Notes
Not verified in a browser. The export fix is the one change whose payoff is a runtime permission decision, and the guards prove the metadata is authored, not that the Export button works end-to-end. Worth one console pass on a list view and a report export before this leaves draft.
The
allowExportgrant set is a judgement call, not a mechanical migration — the rule chosen ("a profile grants it where it already reads AND the app ships an export surface") is documented insrc/profiles/index.tsand enforced by the guards, but the per-persona spread is worth a review, particularlyservice_agentoncrm_account/crm_contact.Generated by Claude Code