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 MIGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,39 @@ Breaking changes and upgrade notes for downstream projects.

---

## @casl/ability v6 → v7 (2026-05-22)

`@casl/ability` upgraded from `^6.8.1` to `^7.0.0`.

### What changed (this repo)

- **`lib/middlewares/policy.js`** — v7 renames `PureAbility` to `Ability` and **drops its default conditions matcher**, so the `Ability` export no longer does MongoDB-style condition matching out of the box (`createMongoAbility` is the replacement for the old behavior). `defineAbilityFor()` now builds via `createMongoAbility`:

```js
// before (v6)
const { AbilityBuilder, Ability } = await import('@casl/ability');
const { can, cannot, build } = new AbilityBuilder(Ability);
// after (v7)
const { AbilityBuilder, createMongoAbility } = await import('@casl/ability');
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Without this, conditions like `can('manage', 'Organization', { _id })` stop matching → authorization silently denies → endpoints return 403/422.
- **JSDoc type refs** `import('@casl/ability').Ability` → `MongoAbility` (`lib/middlewares/policy.js`, `lib/helpers/abilities.js`).
- **`package.json`** — `@casl/ability` `^6.8.1` → `^7.0.0`.

### Downstream action required

The `policy.js` fix is a devkit-owned file → it arrives via `/update-stack` (`--theirs`). The **dependency bump does not auto-propagate** (`package.json` is `--ours`):

1. Bump `@casl/ability` to `^7.0.0` in `package.json` and reinstall.
2. After `/update-stack`, verify `lib/middlewares/policy.js` (~line 95) reads `new AbilityBuilder(createMongoAbility)`.
3. **Module policy files need no change** — they use `can`/`cannot` closures, never the `Ability` class.
4. The serialized rules format is **unchanged** (`createMongoAbility` keeps the MongoQuery rule shape), so Node→client rule packing stays compatible.
5. Run unit + integration + e2e to confirm authorization paths still pass.

---

## Sentry removed — PostHog Error Tracking is now sole source (2026-05-10)

The `@sentry/node` integration shipped in 2026-03-26 (still documented below as **PostHog Analytics (2026-03-26)** + the now-removed Sentry monitoring section) is dropped. Error capture moves entirely to PostHog Error Tracking via `posthog.capture('$exception', ...)`.
Expand Down
2 changes: 1 addition & 1 deletion lib/helpers/abilities.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Serialize CASL abilities to a JSON-safe array compatible with createMongoAbility().
* @param {import('@casl/ability').Ability} ability - The CASL ability instance
* @param {import('@casl/ability').MongoAbility} ability - The CASL ability instance
* @returns {Array<{action: string, subject: string, conditions?: Object, inverted?: boolean}>} Array of serialized rules
*/
const serializeAbilities = (ability) =>
Expand Down
8 changes: 5 additions & 3 deletions lib/middlewares/policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,13 @@ const registerAbilities = (entry) => {
* Iterates over all registered ability builder functions from module policy files.
* @param {Object|null} user - The authenticated user, or null/undefined for guests
* @param {Object|null} [membership] - Optional organization membership (reserved for future use)
* @returns {Promise<import('@casl/ability').Ability>} CASL ability instance
* @returns {Promise<import('@casl/ability').MongoAbility>} CASL ability instance
*/
const defineAbilityFor = async (user, membership) => {
const { AbilityBuilder, Ability } = await loadCasl();
const { can, cannot, build } = new AbilityBuilder(Ability);
// v7: the `Ability` export is now PureAbility (no default conditions matcher).
// createMongoAbility restores MongoDB-style condition matching ({ _id }, { organizationId }).
const { AbilityBuilder, createMongoAbility } = await loadCasl();
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
Comment thread
PierreBrisorgueil marked this conversation as resolved.

// Normalize Mongoose membership to a plain object so all fields (role, status, etc.)
// are accessible, and flatten populated organizationId to a plain string ID.
Expand Down
9 changes: 9 additions & 0 deletions lib/middlewares/tests/policy.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jest.unstable_mockModule('@casl/ability', () => ({
build: jest.fn().mockReturnValue({ can: jest.fn().mockReturnValue(true) }),
})),
Ability: jest.fn(),
createMongoAbility: jest.fn(),
subject: jest.fn((type, doc) => doc),
}));

Expand Down Expand Up @@ -69,4 +70,12 @@ describe('policy discoverPolicies unit tests:', () => {
expect.stringContaining('exports abilities/guestAbilities but no SubjectRegistration'),
);
});

test('defineAbilityFor builds via createMongoAbility (v7 — guards the #3693 auth regression)', async () => {
// v7 drops the conditions matcher from the `Ability` export; the builder must be
// seeded with createMongoAbility or Mongo-style conditions ({ _id }, ...) stop matching.
const { AbilityBuilder, createMongoAbility } = await import('@casl/ability');
await policy.defineAbilityFor({ _id: 'u1', roles: ['user'] }, null);
expect(AbilityBuilder).toHaveBeenCalledWith(createMongoAbility);
});
});
44 changes: 22 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"release:auto": "npx semantic-release"
},
"dependencies": {
"@casl/ability": "^6.8.1",
"@casl/ability": "^7.0.0",
"@jest/globals": "^30.4.1",
"axios": "^1.16.1",
"bcrypt": "^6.0.0",
Expand Down
Loading