Compare commits

...
Author SHA1 Message Date
Stanisław Chmiela 9c692afbcb fix: classify PostgreSQL lock timeouts as transient 2026-10-05 17:46:12 +02:00
renovate[bot] 17bd06e67e chore(deps): update dependency @types/node to v24.19.1 (#713)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@types/node](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | [`24.13.5` → `24.19.1`](https://renovatebot.com/diffs/npm/@types%2fnode/24.13.5/24.19.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fnode/24.19.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fnode/24.13.5/24.19.1?slim=true) |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMjUuMSIsInVwZGF0ZWRJblZlciI6IjQ0LjEyNS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-10-05 08:41:40 -07:00
renovate[bot] 69ef7aa0c9 chore(deps): update dependency pg to v8.23.1 (#711)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [pg](https://redirect.github.com/brianc/node-postgres) ([source](https://redirect.github.com/brianc/node-postgres/tree/HEAD/packages/pg)) | [`8.23.0` → `8.23.1`](https://renovatebot.com/diffs/npm/pg/8.23.0/8.23.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/pg/8.23.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pg/8.23.0/8.23.1?slim=true) |

---

### Release Notes

<details>
<summary>brianc/node-postgres (pg)</summary>

### [`v8.23.1`](https://redirect.github.com/brianc/node-postgres/compare/[email protected]@8.23.1)

[Compare Source](https://redirect.github.com/brianc/node-postgres/compare/[email protected]@8.23.1)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMjUuMSIsInVwZGF0ZWRJblZlciI6IjQ0LjEyNS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-10-05 08:41:31 -07:00
renovate[bot] 8db4fbbd8a chore(deps): update redis docker digest to 6f81e89 (#710)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| redis | digest | `718f745` → `6f81e89` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMjUuMSIsInVwZGF0ZWRJblZlciI6IjQ0LjEyNS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-10-05 08:41:27 -07:00
Will Schurman c22ed50e80 v0.69.0 2026-09-29 08:34:30 -07:00
renovate[bot] 9dcbc3da23 chore(deps): update dependency ip-address to v10.5.1 [security] (#709)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [ip-address](https://redirect.github.com/beaugunderson/ip-address) | [`10.4.0` → `10.5.1`](https://renovatebot.com/diffs/npm/ip-address/10.4.0/10.5.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/ip-address/10.5.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/ip-address/10.4.0/10.5.1?slim=true) |

---

### ip-address: no classifier recognizes the NAT64 local-use range 64:ff9b:1::/48, allowing SSRF and trust-boundary bypass
[CVE-2026-101910](https://nvd.nist.gov/vuln/detail/CVE-2026-101910) / [GHSA-2vr4-cq9g-pvrc](https://redirect.github.com/advisories/GHSA-2vr4-cq9g-pvrc)

<details>
<summary>More information</summary>

#### Details
##### Summary

No classifier on `Address6` recognizes the NAT64 local-use range `64:ff9b:1::/48` (RFC 8215). `isPrivate()`, `isLoopback()`, `isLinkLocal()` and their siblings all return `false` for every address in it, so an internal IPv4 destination written through a local-use NAT64 prefix (`64:ff9b:1:7f00:0:100::` for `127.0.0.1`, `64:ff9b:1:a9fe:a9:fe00::` for `169.254.169.254`) reads as an ordinary global address. `getType()` names the range `'NAT64 (local-use)'`, so the library knows what the address is and classifies it as nothing.

An application that builds a network trust-boundary decision on these checks (for example a filter intended to block Server-Side Request Forgery, or SSRF) will classify an internal target as external and allow the request. SSRF is an attack in which a user-supplied address coaxes the server into making a request to an internal destination the user could not otherwise reach, such as a loopback service or a cloud metadata endpoint.

##### Details

The fix for GHSA-22jq-vg5j-6vgg (10.2.1) classifies IPv4-mapped (`::ffff:0:0/96`) and NAT64 well-known (`64:ff9b::/96`) addresses by their embedded IPv4 address: `embeddedIPv4()` in `src/ipv6.ts` decodes the trailing 32 bits and every special-use classifier delegates to the resulting `Address4`. Those are the only two ranges `embeddedIPv4()` handles.

The local-use range differs in kind from the well-known prefix, which is why extending the decoder does not fix it. RFC 8215 reserves `64:ff9b:1::/48` so an operator can carve their own NAT64 prefix out of it, of any length RFC 6052 allows (`/48`, `/56`, `/64`, or `/96`). Where the IPv4 address sits depends on that choice: `127.0.0.1` is `64:ff9b:1:7f00:0:100::` under a `/48` prefix and `64:ff9b:1::7f00:1` under a `/96` prefix, and each of those decodes to a different address under the other prefix length. There is no single embedded IPv4 address for the library to classify by.

What is well-defined is the range as a whole. The IANA IPv6 Special-Purpose Address Registry lists `64:ff9b:1::/48` as not globally reachable, and Python's `ipaddress` module reports `is_private` as `True` and `is_global` as `False` for every address in it. `isPrivate()` covered ULA (`fc00::/7`) plus the decoded mapped and well-known cases, and nothing in the local-use range.

##### Affected versions

`>= 10.2.0, <= 10.5.0`. The `is*` classification API was extended to `Address6` in 10.2.0; releases before it do not expose the method and are not affected through this vector.

##### Impact

Every address in `64:ff9b:1::/48` parses successfully, `isValid()` is `true`, and no classifier catches it. Which internal target is reached depends on the NAT64 prefix deployed on the server's network; the well-known prefix column is the patched control from GHSA-22jq-vg5j-6vgg.

| Internal target | Well-known form | Classified | Local-use form (`/48` prefix) | Classified |
|---|---|---|---|---|
| `127.0.0.1` | `64:ff9b::7f00:1` | loopback | `64:ff9b:1:7f00:0:100::` | nothing |
| `10.0.0.1` | `64:ff9b::a00:1` | private | `64:ff9b:1:a00:0:100::` | nothing |
| `169.254.169.254` | `64:ff9b::a9fe:a9fe` | link-local | `64:ff9b:1:a9fe:a9:fe00::` | nothing |
| `192.168.1.1` | `64:ff9b::c0a8:101` | private | `64:ff9b:1:c0a8:1:100::` | nothing |

##### Reachability

Reaching an internal host through one of these addresses requires that the server's network run a NAT64 translator on a prefix inside `64:ff9b:1::/48` and that the attacker guess or know the prefix length. That is the same precondition the well-known prefix carries (a translator on `64:ff9b::/96`), with the additional constraint that the prefix is operator-chosen rather than fixed. The severity reflects that precondition.

##### Proof of concept

`npm i [email protected]`, then:

```js
const { Address6 } = require('ip-address');

// A guard of the shape the library documents.
function isBlocked(host) {
  const a = new Address6(host);
  return a.isLoopback() || a.isLinkLocal() || a.isPrivate() || a.isMulticast() || a.isUnspecified();
}

for (const h of ['64:ff9b::7f00:1', '64:ff9b:1:7f00:0:100::', '64:ff9b:1::7f00:1']) {
  console.log(isBlocked(h) ? 'BLOCK' : 'ALLOW', h, '-> getType()', new Address6(h).getType());
}
```

On affected versions:

```
BLOCK 64:ff9b::7f00:1 -> getType() NAT64 (well-known)
ALLOW 64:ff9b:1:7f00:0:100:: -> getType() NAT64 (local-use)
ALLOW 64:ff9b:1::7f00:1 -> getType() NAT64 (local-use)
```

##### Remediation

Upgrade to the patched release. In the fix, `isPrivate()` returns `true` for every address in `64:ff9b:1::/48`, alongside ULA and the decoded mapped and well-known cases. The range is reported private as a whole rather than by a decoded IPv4 address, for the reason given above; `toAddress4Nat64(prefix)` remains the way to decode an address under a known deployment prefix. `isLoopback()` and `isLinkLocal()` are unchanged for this range, since without the prefix length the library cannot know which IPv4 address is embedded.

If you cannot upgrade immediately, test the range directly:

```js
const NAT64_LOCAL_USE = new Address6('64:ff9b:1::/48');
const localUse = new Address6(host).isHostInSubnet(NAT64_LOCAL_USE);
```

##### A note on SSRF defense

These methods are address classifiers, not a complete SSRF defense. Regardless of this fix, a robust SSRF guard must resolve the hostname and validate the *resolved* IP against the socket it connects to, and account for DNS rebinding and redirects. Treat these checks as one layer, not the only one.

#### Severity
- CVSS Score: 6.9 / 10 (Medium)
- Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N`

#### References
- [https://github.com/beaugunderson/ip-address/security/advisories/GHSA-2vr4-cq9g-pvrc](https://redirect.github.com/beaugunderson/ip-address/security/advisories/GHSA-2vr4-cq9g-pvrc)
- [https://nvd.nist.gov/vuln/detail/CVE-2026-101910](https://nvd.nist.gov/vuln/detail/CVE-2026-101910)
- [https://github.com/beaugunderson/ip-address/commit/ab3dc88bcf5374344168a2ba075ca7ac4ff257f8](https://redirect.github.com/beaugunderson/ip-address/commit/ab3dc88bcf5374344168a2ba075ca7ac4ff257f8)
- [https://github.com/beaugunderson/ip-address/releases/tag/v10.5.1](https://redirect.github.com/beaugunderson/ip-address/releases/tag/v10.5.1)
- [https://github.com/advisories/GHSA-2vr4-cq9g-pvrc](https://redirect.github.com/advisories/GHSA-2vr4-cq9g-pvrc)

This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-2vr4-cq9g-pvrc) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>

---

### Release Notes

<details>
<summary>beaugunderson/ip-address (ip-address)</summary>

### [`v10.5.1`](https://redirect.github.com/beaugunderson/ip-address/compare/v10.5.0...38b02d73cfc0724f9ad61c65b6328e116eac03ec)

[Compare Source](https://redirect.github.com/beaugunderson/ip-address/compare/v10.5.0...v10.5.1)

### [`v10.5.0`](https://redirect.github.com/beaugunderson/ip-address/releases/tag/v10.5.0)

[Compare Source](https://redirect.github.com/beaugunderson/ip-address/compare/v10.4.0...v10.5.0)

##### What's Changed

- Honor the fromURL graceful-failure contract for non-IPv6 hosts by [@&#8203;beaugunderson](https://redirect.github.com/beaugunderson) in [#&#8203;218](https://redirect.github.com/beaugunderson/ip-address/pull/218)
- Correct the documentation where it disagreed with the library by [@&#8203;beaugunderson](https://redirect.github.com/beaugunderson) in [#&#8203;219](https://redirect.github.com/beaugunderson/ip-address/pull/219)

**Full Changelog**: <https://github.com/beaugunderson/ip-address/compare/v10.4.0...v10.5.0>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMTIuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjExMi4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-29 08:23:06 -07:00
Will Schurman 🦦 e87e2a3a28 feat: add ID-based and field-based knex database loaders for use with locking modifiers (#706) 2026-09-28 16:29:15 -07:00
renovate[bot] 68e222f512 chore(deps): update postgres:18 docker digest to 5a5a84b (#707)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [postgres](https://hub.docker.com/_/postgres) ([source](https://redirect.github.com/docker-library/postgres)) | digest | `86c951e` → `5a5a84b` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMTIuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjExMi4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-28 08:37:30 -07:00
renovate[bot] e404297b30 chore(deps): update redis docker digest to 718f745 (#708)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| redis | digest | `8a1efc5` → `718f745` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMTIuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjExMi4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-28 08:37:26 -07:00
Will Schurman cbf51c0488 v0.68.0 2026-09-25 08:45:56 -07:00
Will Schurman 🦦 243a0386fb feat: add forShare and skipLocked selection modifiers to knex loader (#705) 2026-09-24 15:53:40 -07:00
Will Schurman 🦦 727f16662d feat: add forUpdate to knex loader (#704) 2026-09-23 15:29:41 -07:00
renovate[bot] 0e1b9f9452 chore(deps): update dependency @types/node to v24.13.5 (#703)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@types/node](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | [`24.13.4` → `24.13.5`](https://renovatebot.com/diffs/npm/@types%2fnode/24.13.4/24.13.5) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fnode/24.13.5?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fnode/24.13.4/24.13.5?slim=true) |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMDMuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjEwMy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-21 09:19:23 -07:00
renovate[bot] 52327bd56c chore(deps): update dependency @tsconfig/bases to v1.0.28 (#702)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@tsconfig/bases](https://redirect.github.com/tsconfig/bases) ([source](https://redirect.github.com/tsconfig/bases/tree/HEAD/bases)) | [`1.0.27` → `1.0.28`](https://renovatebot.com/diffs/npm/@tsconfig%2fbases/1.0.27/1.0.28) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@tsconfig%2fbases/1.0.28?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tsconfig%2fbases/1.0.27/1.0.28?slim=true) |

---

### Release Notes

<details>
<summary>tsconfig/bases (@&#8203;tsconfig/bases)</summary>

### [`v1.0.28`](https://redirect.github.com/tsconfig/bases/compare/270039fbe015427858d03d84a8549d113aac0b76...270039fbe015427858d03d84a8549d113aac0b76)

[Compare Source](https://redirect.github.com/tsconfig/bases/compare/270039fbe015427858d03d84a8549d113aac0b76...270039fbe015427858d03d84a8549d113aac0b76)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMDMuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjEwMy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-21 09:17:59 -07:00
renovate[bot] af121ed765 chore(deps): update redis docker digest to 8a1efc5 (#701)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| redis | digest | `298e5b3` → `8a1efc5` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMDMuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjEwMy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-21 09:17:53 -07:00
renovate[bot] ae9aab60bb chore(deps): update postgres:18 docker digest to 86c951e (#700)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [postgres](https://hub.docker.com/_/postgres) ([source](https://redirect.github.com/docker-library/postgres)) | digest | `4ef4dbc` → `86c951e` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMDMuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjEwMy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-09-21 09:17:41 -07:00
Will Schurman 9629e36e0d v0.67.0 2026-09-18 12:23:16 -07:00
Will Schurman 🦦 4af5ede87d feat!: add user-facing denial reason to privacy policies (#699) 2026-09-18 10:30:50 -07:00
renovate[bot] fa5af4783f chore(deps): update node.js to v24.21.0 (#698)
> ℹ️ **Note**
> 
> This PR body was truncated due to platform limits.

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [node](https://nodejs.org) ([source](https://redirect.github.com/nodejs/node)) | tools | minor | `24.18.1` → `24.21.0` |

---

### Release Notes

<details>
<summary>nodejs/node (node)</summary>

### [`v24.21.0`](https://redirect.github.com/nodejs/node/releases/tag/v24.21.0): 2026-09-08, Version 24.21.0 'Krypton' (LTS), @&#8203;aduh95

[Compare Source](https://redirect.github.com/nodejs/node/compare/v24.20.0...v24.21.0)

##### Notable Changes

- \[[`71106e1f17`](https://redirect.github.com/nodejs/node/commit/71106e1f17)] - **crypto**: update root certificates to NSS 3.126 (Node.js GitHub Bot) [#&#8203;65495](https://redirect.github.com/nodejs/node/pull/65495)
- \[[`afca0a912d`](https://redirect.github.com/nodejs/node/commit/afca0a912d)] - **(SEMVER-MINOR)** **crypto**: support loading private keys through STORE loaders (Filip Skokan) [#&#8203;63949](https://redirect.github.com/nodejs/node/pull/63949)
- \[[`6274fccbd9`](https://redirect.github.com/nodejs/node/commit/6274fccbd9)] - **deps**: update OpenSSL to 3.5.8 (Node.js GitHub Bot) [#&#8203;65542](https://redirect.github.com/nodejs/node/pull/65542)
- \[[`53cba013c7`](https://redirect.github.com/nodejs/node/commit/53cba013c7)] - **deps**: update Undici to 7.29.1 (Node.js GitHub Bot) [#&#8203;65789](https://redirect.github.com/nodejs/node/pull/65789)
- \[[`0529772798`](https://redirect.github.com/nodejs/node/commit/0529772798)] - **(SEMVER-MINOR)** **lib,src**: improve histogram implementation (James M Snell) [#&#8203;65024](https://redirect.github.com/nodejs/node/pull/65024)
- \[[`41c7062b81`](https://redirect.github.com/nodejs/node/commit/41c7062b81)] - **(SEMVER-MINOR)** **net**: improve performance of `net.BlockList` (James M Snell) [#&#8203;64974](https://redirect.github.com/nodejs/node/pull/64974)
- \[[`5197b5a3c5`](https://redirect.github.com/nodejs/node/commit/5197b5a3c5)] - **(SEMVER-MINOR)** **perf\_hooks**: add statistical hypothesis testing to histogram (James M Snell) [#&#8203;65416](https://redirect.github.com/nodejs/node/pull/65416)
- \[[`35c635b032`](https://redirect.github.com/nodejs/node/commit/35c635b032)] - **(SEMVER-MINOR)** **util**: add non-throwing `MIMEType.parse` (James M Snell) [#&#8203;64965](https://redirect.github.com/nodejs/node/pull/64965)

##### Commits

- \[[`84d706cb9b`](https://redirect.github.com/nodejs/node/commit/84d706cb9b)] - **assert**: improve documentation wording (Kamal Rawal) [#&#8203;64953](https://redirect.github.com/nodejs/node/pull/64953)
- \[[`90d127db33`](https://redirect.github.com/nodejs/node/commit/90d127db33)] - **(SEMVER-MINOR)** **benchmark**: add --analyze mode to compare.js (James M Snell) [#&#8203;65416](https://redirect.github.com/nodejs/node/pull/65416)
- \[[`ad1d7884c3`](https://redirect.github.com/nodejs/node/commit/ad1d7884c3)] - **benchmark**: add test-only and mock timers cases (Luan Muniz) [#&#8203;64097](https://redirect.github.com/nodejs/node/pull/64097)
- \[[`1d8f045914`](https://redirect.github.com/nodejs/node/commit/1d8f045914)] - **benchmark**: apply `highWaterMark` in webstreams `pipe-to` (Matteo Collina) [#&#8203;65138](https://redirect.github.com/nodejs/node/pull/65138)
- \[[`ed7ba3c993`](https://redirect.github.com/nodejs/node/commit/ed7ba3c993)] - **benchmark**: complete the sqlite is-transaction fix (Edy Silva) [#&#8203;65218](https://redirect.github.com/nodejs/node/pull/65218)
- \[[`c8837e1aa3`](https://redirect.github.com/nodejs/node/commit/c8837e1aa3)] - **benchmark**: add test runner hooks and options (Luan Muniz) [#&#8203;63754](https://redirect.github.com/nodejs/node/pull/63754)
- \[[`2ba8661e23`](https://redirect.github.com/nodejs/node/commit/2ba8661e23)] - **buffer**: prevent string write offset overflow (Matteo Collina) [#&#8203;65043](https://redirect.github.com/nodejs/node/pull/65043)
- \[[`cc9ee6c2ae`](https://redirect.github.com/nodejs/node/commit/cc9ee6c2ae)] - **buffer**: treat detached ArrayBuffers as empty (Archkon) [#&#8203;64504](https://redirect.github.com/nodejs/node/pull/64504)
- \[[`5a5d73e4c5`](https://redirect.github.com/nodejs/node/commit/5a5d73e4c5)] - **build**: pass target architecture to small-icu genccode (ulofiai) [#&#8203;65095](https://redirect.github.com/nodejs/node/pull/65095)
- \[[`273e72d1a5`](https://redirect.github.com/nodejs/node/commit/273e72d1a5)] - **build**: deprecate always enabled `--enable-static` (Chengzhong Wu) [#&#8203;65103](https://redirect.github.com/nodejs/node/pull/65103)
- \[[`89a67246e3`](https://redirect.github.com/nodejs/node/commit/89a67246e3)] - **build**: check FIPS option value in node.gyp (Filip Skokan) [#&#8203;64982](https://redirect.github.com/nodejs/node/pull/64982)
- \[[`2d21f41cd5`](https://redirect.github.com/nodejs/node/commit/2d21f41cd5)] - **build**: handle malformed OpenSSL macros (Filip Skokan) [#&#8203;64982](https://redirect.github.com/nodejs/node/pull/64982)
- \[[`f62bc0f862`](https://redirect.github.com/nodejs/node/commit/f62bc0f862)] - **build,win**: add PGO workload scripts (Stefan Stojanovic) [#&#8203;63696](https://redirect.github.com/nodejs/node/pull/63696)
- \[[`33d0c7dc12`](https://redirect.github.com/nodejs/node/commit/33d0c7dc12)] - **child\_process**: keep SIGWINCH from killing on Win (Kirill Saied) [#&#8203;64510](https://redirect.github.com/nodejs/node/pull/64510)
- \[[`71106e1f17`](https://redirect.github.com/nodejs/node/commit/71106e1f17)] - **crypto**: update root certificates to NSS 3.126 (Node.js GitHub Bot) [#&#8203;65495](https://redirect.github.com/nodejs/node/pull/65495)
- \[[`419af8b86d`](https://redirect.github.com/nodejs/node/commit/419af8b86d)] - **crypto**: fix missing error checks on ASN1\_STRING\_to\_UTF8() (Nora Dossche) [#&#8203;65200](https://redirect.github.com/nodejs/node/pull/65200)
- \[[`8029383f3f`](https://redirect.github.com/nodejs/node/commit/8029383f3f)] - **crypto**: use available BoringSSL APIs (Filip Skokan) [#&#8203;65423](https://redirect.github.com/nodejs/node/pull/65423)
- \[[`a9bd780e19`](https://redirect.github.com/nodejs/node/commit/a9bd780e19)] - **crypto**: remove obsolete BoringSSL shims (Filip Skokan) [#&#8203;65423](https://redirect.github.com/nodejs/node/pull/65423)
- \[[`7defefad3f`](https://redirect.github.com/nodejs/node/commit/7defefad3f)] - **crypto**: read WebCrypto inputs through primordials (Filip Skokan) [#&#8203;65115](https://redirect.github.com/nodejs/node/pull/65115)
- \[[`6ed1e38627`](https://redirect.github.com/nodejs/node/commit/6ed1e38627)] - **crypto**: fix disabling FIPS mode (Filip Skokan) [#&#8203;64982](https://redirect.github.com/nodejs/node/pull/64982)
- \[[`afca0a912d`](https://redirect.github.com/nodejs/node/commit/afca0a912d)] - **(SEMVER-MINOR)** **crypto**: support loading private keys through STORE loaders (Filip Skokan) [#&#8203;63949](https://redirect.github.com/nodejs/node/pull/63949)
- \[[`9b9dd6e9cf`](https://redirect.github.com/nodejs/node/commit/9b9dd6e9cf)] - **debugger**: wait for target startup (Filip Skokan) [#&#8203;65194](https://redirect.github.com/nodejs/node/pull/65194)
- \[[`07faaeeffd`](https://redirect.github.com/nodejs/node/commit/07faaeeffd)] - **deps**: update corepack to 0.36.0 (Node.js GitHub Bot) [#&#8203;65653](https://redirect.github.com/nodejs/node/pull/65653)
- \[[`53cba013c7`](https://redirect.github.com/nodejs/node/commit/53cba013c7)] - **deps**: update undici to 7.29.1 (Node.js GitHub Bot) [#&#8203;65789](https://redirect.github.com/nodejs/node/pull/65789)
- \[[`0268ca547c`](https://redirect.github.com/nodejs/node/commit/0268ca547c)] - **deps**: update archs files for openssl-3.5.8 (Node.js GitHub Bot) [#&#8203;65542](https://redirect.github.com/nodejs/node/pull/65542)
- \[[`6274fccbd9`](https://redirect.github.com/nodejs/node/commit/6274fccbd9)] - **deps**: upgrade openssl sources to openssl-3.5.8 (Node.js GitHub Bot) [#&#8203;65542](https://redirect.github.com/nodejs/node/pull/65542)
- \[[`6bdcd121fa`](https://redirect.github.com/nodejs/node/commit/6bdcd121fa)] - **deps**: update zlib to 1.3.2.1-motley-8002e91 (Node.js GitHub Bot) [#&#8203;65316](https://redirect.github.com/nodejs/node/pull/65316)
- \[[`c2aa446b6d`](https://redirect.github.com/nodejs/node/commit/c2aa446b6d)] - **deps**: update simdjson to 4.6.7 (Node.js GitHub Bot) [#&#8203;65318](https://redirect.github.com/nodejs/node/pull/65318)
- \[[`3e58e48ea8`](https://redirect.github.com/nodejs/node/commit/3e58e48ea8)] - **deps**: update googletest to [`49495ea`](https://redirect.github.com/nodejs/node/commit/49495eacfdbda3f4b6ba219923fedbb2e3f99376) (Node.js GitHub Bot) [#&#8203;65317](https://redirect.github.com/nodejs/node/pull/65317)
- \[[`670b3665c0`](https://redirect.github.com/nodejs/node/commit/670b3665c0)] - **deps**: cherry-pick [libuv/libuv@`e640dc9`](https://redirect.github.com/libuv/libuv/commit/e640dc9) (ulofiai) [#&#8203;65118](https://redirect.github.com/nodejs/node/pull/65118)
- \[[`ca1c67b021`](https://redirect.github.com/nodejs/node/commit/ca1c67b021)] - **deps**: float ICU-23262 patch for icu78 (René) [#&#8203;64678](https://redirect.github.com/nodejs/node/pull/64678)
- \[[`4ad043b0aa`](https://redirect.github.com/nodejs/node/commit/4ad043b0aa)] - **deps**: enable AVX-512 OpenSSL asm with clang (Daniel Lemire) [#&#8203;65136](https://redirect.github.com/nodejs/node/pull/65136)
- \[[`96b4af109b`](https://redirect.github.com/nodejs/node/commit/96b4af109b)] - **deps**: update googletest to [`d89aac5`](https://redirect.github.com/nodejs/node/commit/d89aac5f0dd4021198d903d39de16f896726de21) (Node.js GitHub Bot) [#&#8203;65153](https://redirect.github.com/nodejs/node/pull/65153)
- \[[`774f663c56`](https://redirect.github.com/nodejs/node/commit/774f663c56)] - **dgram**: don't swallow bind errors when callback is provided (armanmikoyan) [#&#8203;62602](https://redirect.github.com/nodejs/node/pull/62602)
- \[[`94b118d62e`](https://redirect.github.com/nodejs/node/commit/94b118d62e)] - **diagnostics\_channel**: validate before channel activation (Trivikram Kamat) [#&#8203;65313](https://redirect.github.com/nodejs/node/pull/65313)
- \[[`09788665bd`](https://redirect.github.com/nodejs/node/commit/09788665bd)] - **dns**: validate address type in lookupService (Lazizbek Ergashev) [#&#8203;64878](https://redirect.github.com/nodejs/node/pull/64878)
- \[[`15f95fc0e2`](https://redirect.github.com/nodejs/node/commit/15f95fc0e2)] - **dns**: validate port range in `setServers()` (René) [#&#8203;65021](https://redirect.github.com/nodejs/node/pull/65021)
- \[[`37b9e9a154`](https://redirect.github.com/nodejs/node/commit/37b9e9a154)] - **dns**: fix crash on setServers with port 0 (Lazizbek Ergashev) [#&#8203;65009](https://redirect.github.com/nodejs/node/pull/65009)
- \[[`cd6205fa0d`](https://redirect.github.com/nodejs/node/commit/cd6205fa0d)] - **doc**: update AHAFS reference link (Taeuk Ha) [#&#8203;65481](https://redirect.github.com/nodejs/node/pull/65481)
- \[[`0e6f9ae42e`](https://redirect.github.com/nodejs/node/commit/0e6f9ae42e)] - **doc**: fix property names in os.networkInterfaces() example (Jihwan) [#&#8203;65469](https://redirect.github.com/nodejs/node/pull/65469)
- \[[`034a827b41`](https://redirect.github.com/nodejs/node/commit/034a827b41)] - **doc**: fix broken links in cli.md (Donghoon Kang) [#&#8203;65412](https://redirect.github.com/nodejs/node/pull/65412)
- \[[`eb364621d4`](https://redirect.github.com/nodejs/node/commit/eb364621d4)] - **doc**: remove outdated WASI version fallback (이혜미) [#&#8203;65303](https://redirect.github.com/nodejs/node/pull/65303)
- \[[`b13f425bf8`](https://redirect.github.com/nodejs/node/commit/b13f425bf8)] - **doc**: fix broken GYP link in n-api.md (Donghoon Kang) [#&#8203;65413](https://redirect.github.com/nodejs/node/pull/65413)
- \[[`45c4011067`](https://redirect.github.com/nodejs/node/commit/45c4011067)] - **doc**: document that an empty OPENSSL\_CONF skips config loading (Orgad Shaneh) [#&#8203;64949](https://redirect.github.com/nodejs/node/pull/64949)
- \[[`fde6776c5f`](https://redirect.github.com/nodejs/node/commit/fde6776c5f)] - **doc**: fix broken TLS security level example (soreavis) [#&#8203;65391](https://redirect.github.com/nodejs/node/pull/65391)
- \[[`290c1fec04`](https://redirect.github.com/nodejs/node/commit/290c1fec04)] - **doc**: clarify socket destroyed behavior (Dayun) [#&#8203;65395](https://redirect.github.com/nodejs/node/pull/65395)
- \[[`a7e8269947`](https://redirect.github.com/nodejs/node/commit/a7e8269947)] - **doc**: update outdated nodejs.org guide links (Donghoon Kang) [#&#8203;65394](https://redirect.github.com/nodejs/node/pull/65394)
- \[[`7809f11249`](https://redirect.github.com/nodejs/node/commit/7809f11249)] - **doc**: clarify that ipv4 mapped to ipv6 are classified as ipv6 (Vedant Kulkarni) [#&#8203;62117](https://redirect.github.com/nodejs/node/pull/62117)
- \[[`64cd3a6e95`](https://redirect.github.com/nodejs/node/commit/64cd3a6e95)] - **doc**: clarify how fs.Dirent file types are determined (soreavis) [#&#8203;64532](https://redirect.github.com/nodejs/node/pull/64532)
- \[[`9b92fdce14`](https://redirect.github.com/nodejs/node/commit/9b92fdce14)] - **doc**: update security release prepare command (Rafael Gonzaga) [#&#8203;64699](https://redirect.github.com/nodejs/node/pull/64699)
- \[[`6f9b9df3c1`](https://redirect.github.com/nodejs/node/commit/6f9b9df3c1)] - **doc**: clarify copyFile symlink behavior (T) [#&#8203;62941](https://redirect.github.com/nodejs/node/pull/62941)
- \[[`2480acb550`](https://redirect.github.com/nodejs/node/commit/2480acb550)] - **doc**: document setRawMode write access on Windows (Erik Demaine) [#&#8203;63856](https://redirect.github.com/nodejs/node/pull/63856)
- \[[`a1e9c3a5db`](https://redirect.github.com/nodejs/node/commit/a1e9c3a5db)] - **doc**: add missing return types in fs.md (Chaseton Collins) [#&#8203;65307](https://redirect.github.com/nodejs/node/pull/65307)
- \[[`4533572040`](https://redirect.github.com/nodejs/node/commit/4533572040)] - **doc**: add missing return types in buffer.md (Yuya Inoue) [#&#8203;65308](https://redirect.github.com/nodejs/node/pull/65308)
- \[[`39ecedbbd2`](https://redirect.github.com/nodejs/node/commit/39ecedbbd2)] - **doc**: fix lint clean command (greenhead) [#&#8203;65274](https://redirect.github.com/nodejs/node/pull/65274)
- \[[`0b1fb8fcd8`](https://redirect.github.com/nodejs/node/commit/0b1fb8fcd8)] - **doc**: fix typo in onboarding.md (서울민트초코) [#&#8203;65295](https://redirect.github.com/nodejs/node/pull/65295)
- \[[`3165b5d38a`](https://redirect.github.com/nodejs/node/commit/3165b5d38a)] - **doc**: add missing `added:` tags to `fs.lchmod` (Lazizbek Ergashev) [#&#8203;65283](https://redirect.github.com/nodejs/node/pull/65283)
- \[[`113b808e59`](https://redirect.github.com/nodejs/node/commit/113b808e59)] - **doc**: fix SQLite changeset constant descriptions (greenhead) [#&#8203;65265](https://redirect.github.com/nodejs/node/pull/65265)
- \[[`c3eb51d5a1`](https://redirect.github.com/nodejs/node/commit/c3eb51d5a1)] - **doc**: document open pull request limit (Matteo Collina) [#&#8203;65250](https://redirect.github.com/nodejs/node/pull/65250)
- \[[`d7accdcd52`](https://redirect.github.com/nodejs/node/commit/d7accdcd52)] - **doc**: document http2 header constants (Harjoth Khara) [#&#8203;64548](https://redirect.github.com/nodejs/node/pull/64548)
- \[[`f47111416f`](https://redirect.github.com/nodejs/node/commit/f47111416f)] - **doc**: create ai-guidelines and include to CONTRIBUTING (Rafael Gonzaga) [#&#8203;62105](https://redirect.github.com/nodejs/node/pull/62105)
- \[[`c3b120e737`](https://redirect.github.com/nodejs/node/commit/c3b120e737)] - **doc**: update synopsis (Augustin Mauroy) [#&#8203;65171](https://redirect.github.com/nodejs/node/pull/65171)
- \[[`39f4c831fd`](https://redirect.github.com/nodejs/node/commit/39f4c831fd)] - **doc**: fix broken internal links (greenhead) [#&#8203;64901](https://redirect.github.com/nodejs/node/pull/64901)
- \[[`be25cdd69e`](https://redirect.github.com/nodejs/node/commit/be25cdd69e)] - **doc**: report proper return type on urlPattern.test (Brian Muenzenmeyer) [#&#8203;64831](https://redirect.github.com/nodejs/node/pull/64831)
- \[[`1bf7737810`](https://redirect.github.com/nodejs/node/commit/1bf7737810)] - **doc**: fix permission documentation examples (greenhead) [#&#8203;64897](https://redirect.github.com/nodejs/node/pull/64897)
- \[[`b7932e68a1`](https://redirect.github.com/nodejs/node/commit/b7932e68a1)] - **doc**: document sqlite parameter binding (Guilherme Araújo) [#&#8203;65089](https://redirect.github.com/nodejs/node/pull/65089)
- \[[`5234a5169c`](https://redirect.github.com/nodejs/node/commit/5234a5169c)] - **doc**: finalize statements in sqlite examples (Guilherme Araújo) [#&#8203;65088](https://redirect.github.com/nodejs/node/pull/65088)
- \[[`39ea929da7`](https://redirect.github.com/nodejs/node/commit/39ea929da7)] - **doc**: document quic stopSending() and resetStream() (Issac) [#&#8203;64888](https://redirect.github.com/nodejs/node/pull/64888)
- \[[`2ba198db73`](https://redirect.github.com/nodejs/node/commit/2ba198db73)] - **doc**: clarify sqlite bare parameter default (Sumit Kumar Das) [#&#8203;62009](https://redirect.github.com/nodejs/node/pull/62009)
- \[[`314f9b200f`](https://redirect.github.com/nodejs/node/commit/314f9b200f)] - **doc**: remove usage of `util.inherits` (Augustin Mauroy) [#&#8203;60817](https://redirect.github.com/nodejs/node/pull/60817)
- \[[`d82a61662c`](https://redirect.github.com/nodejs/node/commit/d82a61662c)] - **doc**: fix grammar in worker\_threads.md (이혜미) [#&#8203;64913](https://redirect.github.com/nodejs/node/pull/64913)
- \[[`44c0c8ff5b`](https://redirect.github.com/nodejs/node/commit/44c0c8ff5b)] - **doc**: clarify OpenSSL FIPS configuration (Filip Skokan) [#&#8203;64982](https://redirect.github.com/nodejs/node/pull/64982)
- \[[`9b2ca70e0d`](https://redirect.github.com/nodejs/node/commit/9b2ca70e0d)] - **doc**: remove `--expose-gc` flag from CLI documentation (Dario Piotrowicz) [#&#8203;58909](https://redirect.github.com/nodejs/node/pull/58909)
- \[[`a0a12397b9`](https://redirect.github.com/nodejs/node/commit/a0a12397b9)] - **doc**: document ArrayBuffer support in pbkd2Sync (kyungrae2002) [#&#8203;64976](https://redirect.github.com/nodejs/node/pull/64976)
- \[[`b48699e077`](https://redirect.github.com/nodejs/node/commit/b48699e077)] - **doc**: correct default highWaterMark values (Yilong Li) [#&#8203;64617](https://redirect.github.com/nodejs/node/pull/64617)
- \[[`0312ee133c`](https://redirect.github.com/nodejs/node/commit/0312ee133c)] - **esm**: avoid super-linear data URL MIME regex (Sumit Kumar Das) [#&#8203;61951](https://redirect.github.com/nodejs/node/pull/61951)
- \[[`cd84d55c81`](https://redirect.github.com/nodejs/node/commit/cd84d55c81)] - **esm**: only register text format when enabled (Efe Karasakal) [#&#8203;64992](https://redirect.github.com/nodejs/node/pull/64992)
- \[[`c0a8ef611e`](https://redirect.github.com/nodejs/node/commit/c0a8ef611e)] - **esm**: fix wasm import name in error message (이혜미) [#&#8203;64950](https://redirect.github.com/nodejs/node/pull/64950)
- \[[`e6c34f90c2`](https://redirect.github.com/nodejs/node/commit/e6c34f90c2)] - **events**: inline iterationCondition hybrid dispatch closure (Szymon Łągiewka) [#&#8203;64473](https://redirect.github.com/nodejs/node/pull/64473)
- \[[`6ee4b40c91`](https://redirect.github.com/nodejs/node/commit/6ee4b40c91)] - **events**: inline createEvent hybrid dispatch closure (Szymon Łągiewka) [#&#8203;64473](https://redirect.github.com/nodejs/node/pull/64473)
- \[[`367549eed5`](https://redirect.github.com/nodejs/node/commit/367549eed5)] - **fs**: use sized reads for large files in readFileUtf8 (Shelley Vohr) [#&#8203;65328](https://redirect.github.com/nodejs/node/pull/65328)
- \[[`8a5b1ae4c2`](https://redirect.github.com/nodejs/node/commit/8a5b1ae4c2)] - **fs**: fix realpath of namespaced drive paths (Jason Zhang) [#&#8203;65378](https://redirect.github.com/nodejs/node/pull/65378)
- \[[`c9233b950d`](https://redirect.github.com/nodejs/node/commit/c9233b950d)] - **fs**: fix glob early return skipping sibling entries (Srinu desetti) [#&#8203;64895](https://redirect.github.com/nodejs/node/pull/64895)
- \[[`bc54dd8905`](https://redirect.github.com/nodejs/node/commit/bc54dd8905)] - **fs**: pass symlink type in cp when filter is provided (Jerry Zhao) [#&#8203;62654](https://redirect.github.com/nodejs/node/pull/62654)
- \[[`fcb4333aca`](https://redirect.github.com/nodejs/node/commit/fcb4333aca)] - **fs**: allocate FSReqPromise stat arrays lazily (Samuel Attard) [#&#8203;63886](https://redirect.github.com/nodejs/node/pull/63886)
- \[[`c35876154e`](https://redirect.github.com/nodejs/node/commit/c35876154e)] - **fs**: fix out-of-bounds write in mkdtemp for long prefixes (Hierax\_Umbra) [#&#8203;64770](https://redirect.github.com/nodejs/node/pull/64770)
- \[[`b269616936`](https://redirect.github.com/nodejs/node/commit/b269616936)] - **fs**: treat `std::errc::permission_denied` as `EPERM` error (Kirill Saied) [#&#8203;64698](https://redirect.github.com/nodejs/node/pull/64698)
- \[[`212fe77e76`](https://redirect.github.com/nodejs/node/commit/212fe77e76)] - **fs**: add windowsHandle option to file streams (Kirill Saied) [#&#8203;63851](https://redirect.github.com/nodejs/node/pull/63851)
- \[[`75df6cb435`](https://redirect.github.com/nodejs/node/commit/75df6cb435)] - **http**: improve performance with known-length calls to end() (Tim Perry) [#&#8203;65466](https://redirect.github.com/nodejs/node/pull/65466)
- \[[`48d9cd4a28`](https://redirect.github.com/nodejs/node/commit/48d9cd4a28)] - **http**: cache maxHeaderPairs per header section (GetThatCookie) [#&#8203;64988](https://redirect.github.com/nodejs/node/pull/64988)
- \[[`04785c8f43`](https://redirect.github.com/nodejs/node/commit/04785c8f43)] - **http**: fix keylog listener setup on existing agent sockets (Shani Singh) [#&#8203;65066](https://redirect.github.com/nodejs/node/pull/65066)
- \[[`4b90031534`](https://redirect.github.com/nodejs/node/commit/4b90031534)] - **http**: emit drain on socket takeover and avoid stale HWM reuse (Naman Trivedi) [#&#8203;64991](https://redirect.github.com/nodejs/node/pull/64991)
- \[[`83a27559cd`](https://redirect.github.com/nodejs/node/commit/83a27559cd)] - **http2**: adapt receive deferral for Node.js 24 (Matteo Collina) [#&#8203;65093](https://redirect.github.com/nodejs/node/pull/65093)
- \[[`f43bed0ecc`](https://redirect.github.com/nodejs/node/commit/f43bed0ecc)] - **http2**: avoid uaf while receiving and sending rst\_stream (esgor) [#&#8203;64166](https://redirect.github.com/nodejs/node/pull/64166)
- \[[`b42d664321`](https://redirect.github.com/nodejs/node/commit/b42d664321)] - **inspector**: avoid calling into JS from V8 interrupts (Joyee Cheung) [#&#8203;65028](https://redirect.github.com/nodejs/node/pull/65028)
- \[[`6bf852197d`](https://redirect.github.com/nodejs/node/commit/6bf852197d)] - **lib**: use bracket notation instead of startsWith/endsWith for single char (Taejin Kim) [#&#8203;61500](https://redirect.github.com/nodejs/node/pull/61500)
- \[[`151ca7e104`](https://redirect.github.com/nodejs/node/commit/151ca7e104)] - **lib**: harden webidl dictionary member reads (Filip Skokan) [#&#8203;65115](https://redirect.github.com/nodejs/node/pull/65115)
- \[[`7e8c2c9f44`](https://redirect.github.com/nodejs/node/commit/7e8c2c9f44)] - **lib**: use validateArray for array arguments (greenhead) [#&#8203;64959](https://redirect.github.com/nodejs/node/pull/64959)
- \[[`424fe2bc5a`](https://redirect.github.com/nodejs/node/commit/424fe2bc5a)] - **lib**: add and test \[EnforceRange] in webcrypto dictionaries (Filip Skokan) [#&#8203;65091](https://redirect.github.com/nodejs/node/pull/65091)
- \[[`0529772798`](https://redirect.github.com/nodejs/node/commit/0529772798)] - **(SEMVER-MINOR)** **lib,src**: improve histogram implementation (James M Snell) [#&#8203;65024](https://redirect.github.com/nodejs/node/pull/65024)
- \[[`186e1b76e8`](https://redirect.github.com/nodejs/node/commit/186e1b76e8)] - **meta**: move targos to emeritus (Michaël Zasso) [#&#8203;65393](https://redirect.github.com/nodejs/node/pull/65393)
- \[[`b41b07c72a`](https://redirect.github.com/nodejs/node/commit/b41b07c72a)] - **meta**: add unified http api initiative (James M Snell) [#&#8203;65139](https://redirect.github.com/nodejs/node/pull/65139)
- \[[`f85b6ecd67`](https://redirect.github.com/nodejs/node/commit/f85b6ecd67)] - **meta**: move one or more collaborators to emeritus (Node.js GitHub Bot) [#&#8203;65182](https://redirect.github.com/nodejs/node/pull/65182)
- \[[`8f3d01bdce`](https://redirect.github.com/nodejs/node/commit/8f3d01bdce)] - **meta**: add Aviv Keller to `.mailmap` (Aviv Keller) [#&#8203;65048](https://redirect.github.com/nodejs/node/pull/65048)
- \[[`cfad1d5b28`](https://redirect.github.com/nodejs/node/commit/cfad1d5b28)] - **meta**: update sccache to 0.17.0 (René) [#&#8203;64985](https://redirect.github.com/nodejs/node/pull/64985)
- \[[`349c53c441`](https://redirect.github.com/nodejs/node/commit/349c53c441)] - **module**: report unreadable package.json (Paul Bouchon) [#&#8203;65223](https://redirect.github.com/nodejs/node/pull/65223)
- \[[`bd21e6706e`](https://redirect.github.com/nodejs/node/commit/bd21e6706e)] - **module**: cache nearest parent package.json per directory (Shelley Vohr) [#&#8203;65326](https://redirect.github.com/nodejs/node/pull/65326)
- \[[`961bd04370`](https://redirect.github.com/nodejs/node/commit/961bd04370)] - **module**: fix --check on ambiguous ESM files (Paul Bouchon) [#&#8203;65203](https://redirect.github.com/nodejs/node/pull/65203)
- \[[`f45ef73420`](https://redirect.github.com/nodejs/node/commit/f45ef73420)] - **net**: handle undefined parent in \_unrefTimer and \_destroy (Shivay-98) [#&#8203;64644](https://redirect.github.com/nodejs/node/pull/64644)
- \[[`41c7062b81`](https://redirect.github.com/nodejs/node/commit/41c7062b81)] - **(SEMVER-MINOR)** **net**: improve performance of net.BlockList (James M Snell) [#&#8203;64974](https://redirect.github.com/nodejs/node/pull/64974)
- \[[`5197b5a3c5`](https://redirect.github.com/nodejs/node/commit/5197b5a3c5)] - **(SEMVER-MINOR)** **perf\_hooks**: add statistical hypothesis testing to histogram (James M Snell) [#&#8203;65416](https://redirect.github.com/nodejs/node/pull/65416)
- \[[`1722ddac28`](https://redirect.github.com/nodejs/node/commit/1722ddac28)] - **permission**: enforce addon permission in GetLinkedBinding (Rafael Gonzaga) [#&#8203;65432](https://redirect.github.com/nodejs/node/pull/65432)
- \[[`dff2b675db`](https://redirect.github.com/nodejs/node/commit/dff2b675db)] - **process**: validate resource stats array offsets (Archkon) [#&#8203;65098](https://redirect.github.com/nodejs/node/pull/65098)
- \[[`f277983e7b`](https://redirect.github.com/nodejs/node/commit/f277983e7b)] - **quic**: changes for nghttp3\_conn\_close\_stream2 (Marten Richter) [#&#8203;64574](https://redirect.github.com/nodejs/node/pull/64574)
- \[[`a4c770c78e`](https://redirect.github.com/nodejs/node/commit/a4c770c78e)] - **quic**: mark drain promise handled (James M Snell) [#&#8203;65319](https://redirect.github.com/nodejs/node/pull/65319)
- \[[`2460b171c5`](https://redirect.github.com/nodejs/node/commit/2460b171c5)] - **quic**: reset rejected HTTP/3 request streams with H3\_REQUEST\_REJECTED (trivenay) [#&#8203;65442](https://redirect.github.com/nodejs/node/pull/65442)
- \[[`18a7ccf302`](https://redirect.github.com/nodejs/node/commit/18a7ccf302)] - **quic**: write desired size needs update on maxstream (Marten Richter) [#&#8203;64768](https://redirect.github.com/nodejs/node/pull/64768)
- \[[`9017f4a780`](https://redirect.github.com/nodejs/node/commit/9017f4a780)] - **quic**: do not destroy incoming streams that have a consumer (trivenay) [#&#8203;65335](https://redirect.github.com/nodejs/node/pull/65335)
- \[[`ddc41c1ef4`](https://redirect.github.com/nodejs/node/commit/ddc41c1ef4)] - **quic**: fix wake up blob (Marten Richter) [#&#8203;64044](https://redirect.github.com/nodejs/node/pull/64044)
- \[[`88bee43d7c`](https://redirect.github.com/nodejs/node/commit/88bee43d7c)] - **quic**: convert incoming :status header to number (Hallison Pereira Melo) [#&#8203;63589](https://redirect.github.com/nodejs/node/pull/63589)
- \[[`cd776fe97c`](https://redirect.github.com/nodejs/node/commit/cd776fe97c)] - **quic**: fix infinite loop if STOP\_SENDING received on a buffering stream (Tim Perry) [#&#8203;64715](https://redirect.github.com/nodejs/node/pull/64715)
- \[[`4f6eda3c23`](https://redirect.github.com/nodejs/node/commit/4f6eda3c23)] - **repl**: keep entries added while history file is loading (Mhayk Whandson) [#&#8203;64513](https://redirect.github.com/nodejs/node/pull/64513)
- \[[`cd1e6ce29b`](https://redirect.github.com/nodejs/node/commit/cd1e6ce29b)] - **repl**: add benchmarks (Aviv Keller) [#&#8203;64590](https://redirect.github.com/nodejs/node/pull/64590)
- \[[`2ba740669d`](https://redirect.github.com/nodejs/node/commit/2ba740669d)] - **sea**: avoid dangling CLI option pointers (Archkon) [#&#8203;64755](https://redirect.github.com/nodejs/node/pull/64755)
- \[[`ec5e2d6856`](https://redirect.github.com/nodejs/node/commit/ec5e2d6856)] - **sea**: handle NUL bytes in asset keys (Archkon) [#&#8203;64773](https://redirect.github.com/nodejs/node/pull/64773)
- \[[`703b854293`](https://redirect.github.com/nodejs/node/commit/703b854293)] - **sea**: reject trailing content in config JSON (Archkon) [#&#8203;64774](https://redirect.github.com/nodejs/node/pull/64774)
- \[[`a61a5fdd1c`](https://redirect.github.com/nodejs/node/commit/a61a5fdd1c)] - **sqlite**: prevent reentrant session.close() (Trivikram Kamat) [#&#8203;65349](https://redirect.github.com/nodejs/node/pull/65349)
- \[[`6ae81be0a3`](https://redirect.github.com/nodejs/node/commit/6ae81be0a3)] - **sqlite**: reject statement-less SQL in prepare() (Trevor Burnham) [#&#8203;65157](https://redirect.github.com/nodejs/node/pull/65157)
- \[[`043dfe4996`](https://redirect.github.com/nodejs/node/commit/043dfe4996)] - **sqlite**: reject statement-less SQL in SQLTagStore (Trevor Burnham) [#&#8203;65157](https://redirect.github.com/nodejs/node/pull/65157)
- \[[`b8faee02e1`](https://redirect.github.com/nodejs/node/commit/b8faee02e1)] - **sqlite**: check null returns from sqlite value functions (Nora Dossche) [#&#8203;63288](https://redirect.github.com/nodejs/node/pull/63288)
- \[[`d6d2a71bee`](https://redirect.github.com/nodejs/node/commit/d6d2a71bee)] - **sqlite**: validate maxSize argument in createTagStore() (Anshika Jain) [#&#8203;63792](https://redirect.github.com/nodejs/node/pull/63792)
- \[[`61a046309f`](https://redirect.github.com/nodejs/node/commit/61a046309f)] - **sqlite**: reject non-positive backup rates (Trivikram Kamat) [#&#8203;64893](https://redirect.github.com/nodejs/node/pull/64893)
- \[[`514e3f30fb`](https://redirect.github.com/nodejs/node/commit/514e3f30fb)] - **sqlite**: clear SQLTagStore bindings (Matteo Collina) [#&#8203;65041](https://redirect.github.com/nodejs/node/pull/65041)
- \[[`c1542255b8`](https://redirect.github.com/nodejs/node/commit/c1542255b8)] - **sqlite**: bind Boolean (mike-git374) [#&#8203;62001](https://redirect.github.com/nodejs/node/pull/62001)
- \[[`cdb732beb5`](https://redirect.github.com/nodejs/node/commit/cdb732beb5)] - **sqlite**: fix undefined behaviour in `Session::Changeset()` (Nora Dossche) [#&#8203;63637](https://redirect.github.com/nodejs/node/pull/63637)
- \[[`fbe8861111`](https://redirect.github.com/nodejs/node/commit/fbe8861111)] - **sqlite**: bind ArrayBuffer (mike-git374) [#&#8203;62061](https://redirect.github.com/nodejs/node/pull/62061)
- \[[`e3c6bd6bc8`](https://redirect.github.com/nodejs/node/commit/e3c6bd6bc8)] - **src**: add missing vector include (Filip Skokan) [#&#8203;65622](https://redirect.github.com/nodejs/node/pull/65622)
- \[[`793cf69df8`](https://redirect.github.com/nodejs/node/commit/793cf69df8)] - **src**: fix heap value deduplication in embedder graph (Ilyas Shabi) [#&#8203;64801](https://redirect.github.com/nodejs/node/pull/64801)
- \[[`ea5935b04c`](https://redirect.github.com/nodejs/node/commit/ea5935b04c)] - **src**: fix out-of-bounds write when transcoding odd-length ucs2 (nashit hayat) [#&#8203;64512](https://redirect.github.com/nodejs/node/pull/64512)
- \[[`22e5023f4d`](https://redirect.github.com/nodejs/node/commit/22e5023f4d)] - **src**: escape Windows environment variables in task runner (Antoine du Hamel) [#&#8203;65217](https://redirect.github.com/nodejs/node/pull/65217)
- \[[`a0f3c62bd9`](https://redirect.github.com/nodejs/node/commit/a0f3c62bd9)] - **src**: simplify c++ diagnostics channel API (James M Snell) [#&#8203;65158](https://redirect.github.com/nodejs/node/pull/65158)
- \[[`8e831a3d2e`](https://redirect.github.com/nodejs/node/commit/8e831a3d2e)] - **src**: make minor cleanup to permission checks (James M Snell) [#&#8203;65158](https://redirect.github.com/nodejs/node/pull/65158)
- \[[`968b2ca3a3`](https://redirect.github.com/nodejs/node/commit/968b2ca3a3)] - **src**: use DictionaryTemplate for permission diag channel message (James M Snell) [#&#8203;65158](https://redirect.github.com/nodejs/node/pull/65158)
- \[[`18e16e7f8d`](https://redirect.github.com/nodejs/node/commit/18e16e7f8d)] - **src**: cache permission strings (James M Snell) [#&#8203;65158](https://redirect.github.com/nodejs/node/pull/65158)
- \[[`c8625a4b6f`](https://redirect.github.com/nodejs/node/commit/c8625a4b6f)] - **src**: add SetAbortHandler (Max H Fisher) [#&#8203;64684](https://redirect.github.com/nodejs/node/pull/64684)
- \[[`c990140d60`](https://redirect.github.com/nodejs/node/commit/c990140d60)] - **src**: match cmd.exe case-insensitively in task runner (Archkon) [#&#8203;64907](https://redirect.github.com/nodejs/node/pull/64907)
- \[[`d7463b9dbc`](https://redirect.github.com/nodejs/node/commit/d7463b9dbc)] - **src**: reuse cached env strings in remaining files (Seongeun Lee) [#&#8203;65039](https://redirect.github.com/nodejs/node/pull/65039)
- \[[`b055a43b93`](https://redirect.github.com/nodejs/node/commit/b055a43b93)] - **src**: expose Windows-only fs open flags (Kirill Saied) [#&#8203;64775](https://redirect.github.com/nodejs/node/pull/64775)
- \[[`7f21e37496`](https://redirect.github.com/nodejs/node/commit/7f21e37496)] - **src**: report why --enable-fips failed (Filip Skokan) [#&#8203;64979](https://redirect.github.com/nodejs/node/pull/64979)
- \[[`8c11beae27`](https://redirect.github.com/nodejs/node/commit/8c11beae27)] - **src**: update repeated use strings to env (James M Snell) [#&#8203;64760](https://redirect.github.com/nodejs/node/pull/64760)
- \[[`9e2c477e26`](https://redirect.github.com/nodejs/node/commit/9e2c477e26)] - **stream**: normalize fused stateless transform results (Trivikram Kamat) [#&#8203;65367](https://redirect.github.com/nodejs/node/pull/65367)
- \[[`107e96dd41`](https://redirect.github.com/nodejs/node/commit/107e96dd41)] - **stream**: encode whole chunks in TextEncoderStream (Matteo Collina) [#&#8203;65414](https://redirect.github.com/nodejs/node/pull/65414)
- \[[`164068279b`](https://redirect.github.com/nodejs/node/commit/164068279b)] - **stream**: prevent share from eagerly draining source (Trivikram Kamat) [#&#8203;65338](https://redirect.github.com/nodejs/node/pull/65338)
- \[[`93d822bdc1`](https://redirect.github.com/nodejs/node/commit/93d822bdc1)] - **stream**: drain pending writes before broadcast end (Trivikram Kamat) [#&#8203;65334](https://redirect.github.com/nodejs/node/pull/65334)
- \[[`6b8b9c362f`](https://redirect.github.com/nodejs/node/commit/6b8b9c362f)] - **stream**: reuse unexposed managed read buffers (GetThatCookie) [#&#8203;64990](https://redirect.github.com/nodejs/node/pull/64990)
- \[[`4ec4fab367`](https://redirect.github.com/nodejs/node/commit/4ec4fab367)] - **stream**: avoid duplicated endReadableNT scheduling (Matteo Collina) [#&#8203;65310](https://redirect.github.com/nodejs/node/pull/65310)
- \[[`86d1196ebf`](https://redirect.github.com/nodejs/node/commit/86d1196ebf)] - **stream**: decouple transform backpressure changes (Matteo Collina) [#&#8203;65143](https://redirect.github.com/nodejs/node/pull/65143)
- \[[`b8a7a75b19`](https://redirect.github.com/nodejs/node/commit/b8a7a75b19)] - **stream**: reject pull on signal abort during flush (Trivikram Kamat) [#&#8203;65346](https://redirect.github.com/nodejs/node/pull/65346)
- \[[`386ed6a06d`](https://redirect.github.com/nodejs/node/commit/386ed6a06d)] - **stream**: avoid leaking consumers on signal failure (Trivikram Kamat) [#&#8203;65299](https://redirect.github.com/nodejs/node/pull/65299)
- \[[`74d53bd73b`](https://redirect.github.com/nodejs/node/commit/74d53bd73b)] - **stream**: use validateObject for zlib/iter params (greenhead) [#&#8203;65015](https://redirect.github.com/nodejs/node/pull/65015)
- \[[`3a174ce16b`](https://redirect.github.com/nodejs/node/commit/3a174ce16b)] - **stream**: use validateNumber for BYOB reader options.min (greenhead) [#&#8203;65014](https://redirect.github.com/nodejs/node/pull/65014)
- \[[`859ea01cb2`](https://redirect.github.com/nodejs/node/commit/859ea01cb2)] - **stream**: consolidate non-op algorithm callbacks (Matteo Collina) [#&#8203;65138](https://redirect.github.com/nodejs/node/pull/65138)
- \[[`c577669825`](https://redirect.github.com/nodejs/node/commit/c577669825)] - **stream**: cut promise churn in webstreams hot paths (Matteo Collina) [#&#8203;65138](https://redirect.github.com/nodejs/node/pull/65138)
- \[[`d631e910db`](https://redirect.github.com/nodejs/node/commit/d631e910db)] - **stream**: preserve falsy cancellation reasons (Trivikram Kamat) [#&#8203;64705](https://redirect.github.com/nodejs/node/pull/64705)
- \[[`f9c21eabbd`](https://redirect.github.com/nodejs/node/commit/f9c21eabbd)] - **stream**: use validateBuffer for BYOB reader view (greenhead) [#&#8203;65046](https://redirect.github.com/nodejs/node/pull/65046)
- \[[`b89c8f5d1e`](https://redirect.github.com/nodejs/node/commit/b89c8f5d1e)] - **stream**: fix recursive WritableStream abort (Jeong SeokChan) [#&#8203;64825](https://redirect.github.com/nodejs/node/pull/64825)
- \[[`39e0457e86`](https://redirect.github.com/nodejs/node/commit/39e0457e86)] - **test**: fix link-local dgram scope assertion (Filip Skokan) [#&#8203;65629](https://redirect.github.com/nodejs/node/pull/65629)
- \[[`1433cf9d5b`](https://redirect.github.com/nodejs/node/commit/1433cf9d5b)] - **test**: account for varied OpenSSL CCM final behaviours (Filip Skokan) [#&#8203;65542](https://redirect.github.com/nodejs/node/pull/65542)
- \[[`7d135d24b7`](https://redirect.github.com/nodejs/node/commit/7d135d24b7)] - **test**: convert forEach to for of test-messageevent-brandcheck file (Nachiketa Pathak) [#&#8203;65279](https://redirect.github.com/nodejs/node/pull/65279)
- \[[`421ee11715`](https://redirect.github.com/nodejs/node/commit/421ee11715)] - **test**: use spawnSyncAndAssert in windowsHide test (Junsoo Ha) [#&#8203;65351](https://redirect.github.com/nodejs/node/pull/65351)
- \[[`929d5705bc`](https://redirect.github.com/nodejs/node/commit/929d5705bc)] - **test**: remove test-debugger-run-after-quit-restart as flaky on macOS (Yuya Inoue) [#&#8203;65424](https://redirect.github.com/nodejs/node/pull/65424)
- \[[`f38f154c14`](https://redirect.github.com/nodejs/node/commit/f38f154c14)] - **test**: simplify test-timers-interval-promisified.js (Donghoon Kang) [#&#8203;65322](https://redirect.github.com/nodejs/node/pull/65322)
- \[[`a98e27f2a9`](https://redirect.github.com/nodejs/node/commit/a98e27f2a9)] - **test**: add Headers coverage and benchmark (Yagiz Nizipli) [#&#8203;65365](https://redirect.github.com/nodejs/node/pull/65365)
- \[[`38fdbb62aa`](https://redirect.github.com/nodejs/node/commit/38fdbb62aa)] - **test**: deflake test-net-listen-ipv6only (sangwook) [#&#8203;64173](https://redirect.github.com/nodejs/node/pull/64173)
- \[[`06c7684b01`](https://redirect.github.com/nodejs/node/commit/06c7684b01)] - **test**: use common/child\_process spawnSync helpers (Junsoo Ha) [#&#8203;65377](https://redirect.github.com/nodejs/node/pull/65377)
- \[[`6438c70004`](https://redirect.github.com/nodejs/node/commit/6438c70004)] - **test**: fix Linux debug skip in SEA test guard (구현우) [#&#8203;63751](https://redirect.github.com/nodejs/node/pull/63751)
- \[[`fe0e4f1b65`](https://redirect.github.com/nodejs/node/commit/fe0e4f1b65)] - **test**: avoid timer race in event loop delay test (Trivikram Kamat) [#&#8203;64728](https://redirect.github.com/nodejs/node/pull/64728)
- \[[`6ff69baea8`](https://redirect.github.com/nodejs/node/commit/6ff69baea8)] - **test**: enforce exit code in `test-http-server-stale-close` (Antoine du Hamel) [#&#8203;65198](https://redirect.github.com/nodejs/node/pull/65198)
- \[[`ba87603016`](https://redirect.github.com/nodejs/node/commit/ba87603016)] - **test**: convert test-async-local-storage-bind to async loop (freida-code) [#&#8203;65270](https://redirect.github.com/nodejs/node/pull/65270)
- \[[`8d6b89f454`](https://redirect.github.com/nodejs/node/commit/8d6b89f454)] - **test**: replace `forEach()` with `for...of` in parallel tests (Phillip Markert) [#&#8203;65272](https://redirect.github.com/nodejs/node/pull/65272)
- \[[`a60572a7bb`](https://redirect.github.com/nodejs/node/commit/a60572a7bb)] - **test**: convert forEach to for in test-constant.js file (NIxxy25) [#&#8203;65271](https://redirect.github.com/nodejs/node/pull/65271)
- \[[`f82060c6ce`](https://redirect.github.com/nodejs/node/commit/f82060c6ce)] - **test**: use for-of instead of forEach (Felix P.) [#&#8203;65268](https://redirect.github.com/nodejs/node/pull/65268)
- \[[`5ded71f9cc`](https://redirect.github.com/nodejs/node/commit/5ded71f9cc)] - **test**: cover `realpathSync` resolving symlinks after a FIFO stat (Hendrik Liebau) [#&#8203;65113](https://redirect.github.com/nodejs/node/pull/65113)
- \[[`ac9835225e`](https://redirect.github.com/nodejs/node/commit/ac9835225e)] - **test**: account for \[EnforceRange] in test-webcrypto-prototype-pollution (Filip Skokan) [#&#8203;65173](https://redirect.github.com/nodejs/node/pull/65173)
- \[[`550d24277e`](https://redirect.github.com/nodejs/node/commit/550d24277e)] - **test**: update WPT for WebCryptoAPI to [`4c2fd05`](https://redirect.github.com/nodejs/node/commit/4c2fd05ed5) (Node.js GitHub Bot) [#&#8203;65150](https://redirect.github.com/nodejs/node/pull/65150)
- \[[`2aa26559f0`](https://redirect.github.com/nodejs/node/commit/2aa26559f0)] - **test**: update WPT for urlpattern to [`4832db4`](https://redirect.github.com/nodejs/node/commit/4832db4761) (Node.js GitHub Bot) [#&#8203;65151](https://redirect.github.com/nodejs/node/pull/65151)
- \[[`d45c010108`](https://redirect.github.com/nodejs/node/commit/d45c010108)] - **test**: fix hidden error in test-http-server-stale-close.js (Meghan Denny) [#&#8203;59357](https://redirect.github.com/nodejs/node/pull/59357)
- \[[`881f8d092d`](https://redirect.github.com/nodejs/node/commit/881f8d092d)] - **test**: avoid deadlock issue in pipeline http2 tests to fix flakiness (Tim Perry) [#&#8203;65079](https://redirect.github.com/nodejs/node/pull/65079)
- \[[`e4b1e3ee75`](https://redirect.github.com/nodejs/node/commit/e4b1e3ee75)] - **test**: allow half-open CONNECT tunnel sockets (Trivikram Kamat) [#&#8203;64973](https://redirect.github.com/nodejs/node/pull/64973)
- \[[`00f4240d48`](https://redirect.github.com/nodejs/node/commit/00f4240d48)] - **test**: update passphrases to comply with the next OpenSSL FIPS mode (Filip Skokan) [#&#8203;65077](https://redirect.github.com/nodejs/node/pull/65077)
- \[[`cf7680efb7`](https://redirect.github.com/nodejs/node/commit/cf7680efb7)] - **test**: use libuv clock for immediate queue test (Trivikram Kamat) [#&#8203;64889](https://redirect.github.com/nodejs/node/pull/64889)
- \[[`b0c12772ff`](https://redirect.github.com/nodejs/node/commit/b0c12772ff)] - **test**: increase timeout in probe-failure-hang-during-evaluate (Joyee Cheung) [#&#8203;64719](https://redirect.github.com/nodejs/node/pull/64719)
- \[[`7e279104b4`](https://redirect.github.com/nodejs/node/commit/7e279104b4)] - **test**: update WPT for WebCryptoAPI to [`82c3d90`](https://redirect.github.com/nodejs/node/commit/82c3d9069c) (Node.js GitHub Bot) [#&#8203;64977](https://redirect.github.com/nodejs/node/pull/64977)
- \[[`ac11f88d16`](https://redirect.github.com/nodejs/node/commit/ac11f88d16)] - **test,doc**: cover and document multi-byte offset/size in randomFill (kyungrae2002) [#&#8203;64834](https://redirect.github.com/nodejs/node/pull/64834)
- \[[`67da38cada`](https://redirect.github.com/nodejs/node/commit/67da38cada)] - **test\_runner**: match dotfiles in default coverage exclude (semimikoh) [#&#8203;63401](https://redirect.github.com/nodejs/node/pull/63401)
- \[[`b06c61a08f`](https://redirect.github.com/nodejs/node/commit/b06c61a08f)] - **test\_runner**: print coverage and diagnostic info with dot reporter (mag123c) [#&#8203;61423](https://redirect.github.com/nodejs/node/pull/61423)
- \[[`7cb9c9c126`](https://redirect.github.com/nodejs/node/commit/7cb9c9c126)] - **test\_runner**: use run options with isolation="none" (Sylvester Keil) [#&#8203;62269](https://redirect.github.com/nodejs/node/pull/62269)
- \[[`339acf4201`](https://redirect.github.com/nodejs/node/commit/339acf4201)] - **test\_runner**: mock dual-package with conditional exports (Maruthan G) [#&#8203;62943](https://redirect.github.com/nodejs/node/pull/62943)
- \[[`e7a68bca08`](https://redirect.github.com/nodejs/node/commit/e7a68bca08)] - **test\_runner**: add classname hierarchy for JUnit reporter (mag123c) [#&#8203;60220](https://redirect.github.com/nodejs/node/pull/60220)
- \[[`6a248acefe`](https://redirect.github.com/nodejs/node/commit/6a248acefe)] - **test\_runner**: fix junit report on empty diagnostic (Lazizbek Ergashev) [#&#8203;65357](https://redirect.github.com/nodejs/node/pull/65357)
- \[[`d01dda79b6`](https://redirect.github.com/nodejs/node/commit/d01dda79b6)] - **test\_runner**: do not tag-filter test file wrappers (Chemi Atlow) [#&#8203;65170](https://redirect.github.com/nodejs/node/pull/65170)
- \[[`32ead10ddd`](https://redirect.github.com/nodejs/node/commit/32ead10ddd)] - **test\_runner**: fix env option validation (Jihwan) [#&#8203;64865](https://redirect.github.com/nodejs/node/pull/64865)
- \[[`b0ef155440`](https://redirect.github.com/nodejs/node/commit/b0ef155440)] - **tls**: throw on invalid ALPNProtocols instead of aborting (Sankalp Thakur) [#&#8203;65076](https://redirect.github.com/nodejs/node/pull/65076)
- \[[`dcf65c50ce`](https://redirect.github.com/nodejs/node/commit/dcf65c50ce)] - **tls**: fix authorized state on no-cert TLS1.3 client cert resumption (Tim Perry) [#&#8203;64677](https://redirect.github.com/nodejs/node/pull/64677)
- \[[`b0f54bda78`](https://redirect.github.com/nodejs/node/commit/b0f54bda78)] - **tools**: improve commit queue failure comment (Filip Skokan) [#&#8203;65433](https://redirect.github.com/nodejs/node/pull/65433)
- \[[`bc1f2718d5`](https://redirect.github.com/nodejs/node/commit/bc1f2718d5)] - **tools**: fix max body length handler in `create-release-proposal.sh` (Antoine du Hamel) [#&#8203;65455](https://redirect.github.com/nodejs/node/pull/65455)
- \[[`bbe76516a9`](https://redirect.github.com/nodejs/node/commit/bbe76516a9)] - **tools**: bump brace-expansion in `/tools/clang-format` (dependabot\[bot]) [#&#8203;64984](https://redirect.github.com/nodejs/node/pull/64984)
- \[[`67697debdf`](https://redirect.github.com/nodejs/node/commit/67697debdf)] - **tools**: make env variables consistent in cron jobs (Antoine du Hamel) [#&#8203;65168](https://redirect.github.com/nodejs/node/pull/65168)
- \[[`ac8a68ec92`](https://redirect.github.com/nodejs/node/commit/ac8a68ec92)] - **tools**: only include fast-tracked and old enough PRs in CQ (Antoine du Hamel) [#&#8203;65197](https://redirect.github.com/nodejs/node/pull/65197)
- \[[`6d9999fa2e`](https://redirect.github.com/nodejs/node/commit/6d9999fa2e)] - **tools**: remove skip logic in `commit-queue.sh` (Antoine du Hamel) [#&#8203;65162](https://redirect.github.com/nodejs/node/pull/65162)
- \[[`60bfa1694e`](https://redirect.github.com/nodejs/node/commit/60bfa1694e)] - **tools**: bump js-yaml from 4.2.0 to 4.3.1 in /tools/lint-md (dependabot\[bot]) [#&#8203;65129](https://redirect.github.com/nodejs/node/pull/65129)
- \[[`782748527e`](https://redirect.github.com/nodejs/node/commit/782748527e)] - **tools**: bump js-yaml from 4.2.0 to 4.3.1 in /tools/eslint (dependabot\[bot]) [#&#8203;65130](https://redirect.github.com/nodejs/node/pull/65130)
- \[[`dac257340f`](https://redirect.github.com/nodejs/node/commit/dac257340f)] - **tools**: fix GITHUB\_TOKEN permissions for CQ workflow (Antoine du Hamel) [#&#8203;65192](https://redirect.github.com/nodejs/node/pull/65192)
- \[[`e624785846`](https://redirect.github.com/nodejs/node/commit/e624785846)] - **tools**: use the read-only token when filtering PRs in CQ (Antoine du Hamel) [#&#8203;65169](https://redirect.github.com/nodejs/node/pull/65169)
- \[[`7d9dcfaaa7`](https://redirect.github.com/nodejs/node/commit/7d9dcfaaa7)] - **tools**: delay removal of `commit-queue` label (Antoine du Hamel) [#&#8203;65101](https://redirect.github.com/nodejs/node/pull/65101)
- \[[`0d7c7936e7`](https://redirect.github.com/nodejs/node/commit/0d7c7936e7)] - **tools**: move ncu config to global for commit queue (Filip Skokan) [#&#8203;65132](https://redirect.github.com/nodejs/node/pull/65132)
- \[[`5e1db5a38a`](https://redirect.github.com/nodejs/node/commit/5e1db5a38a)] - **tools**: prefilter commit queue metadata (Filip Skokan) [#&#8203;64343](https://redirect.github.com/nodejs/node/pull/64343)
- \[[`f41509b91d`](https://redirect.github.com/nodejs/node/commit/f41509b91d)] - **tools**: lazy-abort failed PR merges in CQ (Antoine du Hamel) [#&#8203;65004](https://redirect.github.com/nodejs/node/pull/65004)
- \[[`ceb0e99acd`](https://redirect.github.com/nodejs/node/commit/ceb0e99acd)] - **tools**: sync mk-ca-bundle.pl with curl (Archkon) [#&#8203;64753](https://redirect.github.com/nodejs/node/pull/64753)
- \[[`600663b23f`](https://redirect.github.com/nodejs/node/commit/600663b23f)] - **tty**: add raw-vt and io raw modes (Samuel Williams) [#&#8203;64140](https://redirect.github.com/nodejs/node/pull/64140)
- \[[`a40bfc742e`](https://redirect.github.com/nodejs/node/commit/a40bfc742e)] - **typings**: add signal\_wrap internal binding types (Seongeun Lee) [#&#8203;65229](https://redirect.github.com/nodejs/node/pull/65229)
- \[[`b103a4a3b9`](https://redirect.github.com/nodejs/node/commit/b103a4a3b9)] - **typings**: add diagnostics\_channel typings (Seongeun Lee) [#&#8203;65227](https://redirect.github.com/nodejs/node/pull/65227)
- \[[`e64de34b89`](https://redirect.github.com/nodejs/node/commit/e64de34b89)] - **typings**: add watchdog internal binding types (Seongeun Lee) [#&#8203;65228](https://redirect.github.com/nodejs/node/pull/65228)
- \[[`c57c83b4d1`](https://redirect.github.com/nodejs/node/commit/c57c83b4d1)] - **typings**: add internal\_only\_v8 binding typeis (Donghoon Kang) [#&#8203;65071](https://redirect.github.com/nodejs/node/pull/65071)
- \[[`f15e8c9dcd`](https://redirect.github.com/nodejs/node/commit/f15e8c9dcd)] - **typings**: add credentials internal binding types (Donghoon Kang) [#&#8203;65036](https://redirect.github.com/nodejs/node/pull/65036)
- \[[`a500256b0b`](https://redirect.github.com/nodejs/node/commit/a500256b0b)] - **url**: skip unused href reuse comparison (Yagiz Nizipli) [#&#8203;65361](https://redirect.github.com/nodejs/node/pull/65361)
- \[[`58390f8bec`](https://redirect.github.com/nodejs/node/commit/58390f8bec)] - **url**: speed up WHATWG URL parsing (Yagiz Nizipli) [#&#8203;65361](https://redirect.github.com/nodejs/node/pull/65361)
- \[[`7d5428c812`](https://redirect.github.com/nodejs/node/commit/7d5428c812)] - **url**: speed up URLSearchParams (Yagiz Nizipli) [#&#8203;65363](https://redirect.github.com/nodejs/node/pull/65363)
- \[[`8e2461d819`](https://redirect.github.com/nodejs/node/commit/8e2461d819)] - **url**: bounds-check short Windows file URL paths (Archkon) [#&#8203;64788](https://redirect.github.com/nodejs/node/pull/64788)
- \[[`9ffc0da90c`](https://redirect.github.com/nodejs/node/commit/9ffc0da90c)] - **url**: handle unparsable serialized URLs in setters (Matteo Collina) [#&#8203;64651](https://redirect.github.com/nodejs/node/pull/64651)
- \[[`fa9d4e075d`](https://redirect.github.com/nodejs/node/commit/fa9d4e075d)] - **util**: allow single-line format when break length is infinite (Hamid Reza Ghavami) [#&#8203;64238](https://redirect.github.com/nodejs/node/pull/64238)
- \[[`b68a7c3862`](https://redirect.github.com/nodejs/node/commit/b68a7c3862)] - **util**: fix OSC 8 hyperlink stripping in stripVTControlCharacters (Dushyant Singh Hada) [#&#8203;64319](https://redirect.github.com/nodejs/node/pull/64319)
- \[[`10cbb6dc00`](https://redirect.github.com/nodejs/node/commit/10cbb6dc00)] - **util**: fix formatting of functions returned from getters (Richard Gibson) [#&#8203;64839](https://redirect.github.com/nodejs/node/pull/64839)
- \[[`64c20b449e`](https://redirect.github.com/nodejs/node/commit/64c20b449e)] - **util**: use more primordials in `comparisons.js` (Ayoub Mabrouk) [#&#8203;61198](https://redirect.github.com/nodejs/node/pull/61198)
- \[[`cea9786de8`](https://redirect.github.com/nodejs/node/commit/cea9786de8)] - **util**: preserve function names without source map names (Hiroki Osame) [#&#8203;65108](https://redirect.github.com/nodejs/node/pull/65108)
- \[[`35c635b032`](https://redirect.github.com/nodejs/node/commit/35c635b032)] - **(SEMVER-MINOR)** **util**: add non-throwing MIMEType.parse (James M Snell) [#&#8203;64965](https://redirect.github.com/nodejs/node/pull/64965)
- \[[`5858c2ba9a`](https://redirect.github.com/nodejs/node/commit/5858c2ba9a)] - **zlib**: validate pledgedSrcSize for sync zstd (Archkon) [#&#8203;64601](https://redirect.github.com/nodejs/node/pull/64601)

### [`v24.20.0`](https://redirect.github.com/nodejs/node/releases/tag/v24.20.0): 2026-08-26, Version 24.20.0 'Krypton' (LTS), @&#8203;aduh95

[Compare Source](https://redirect.github.com/nodejs/node/compare/v24.19.0...v24.20.0)

##### Notable Changes

- \[[`b12bcc9ae1`](https://redirect.github.com/nodejs/node/commit/b12bcc9ae1)] - **(SEMVER-MINOR)** **async\_hooks**: add `using` scopes to `AsyncLocalStorage` (Stephen Belanger) [#&#8203;61674](https://redirect.github.com/nodejs/node/pull/61674)
- \[[`e2eb88b36b`](https://redirect.github.com/nodejs/node/commit/e2eb88b36b)] - **(SEMVER-MINOR)** **buffer**: add `end` parameter (Robert Nagy) [#&#8203;62390](https://redirect.github.com/nodejs/node/pull/62390)
- \[[`1fefdda18e`](https://redirect.github.com/nodejs/node/commit/1fefdda18e)] - **crypto**: update root certificates to NSS 3.125 (Node.js GitHub Bot) [#&#8203;64746](https://redirect.github.com/nodejs/node/pull/64746)
- \[[`4a158cf1ab`](https://redirect.github.com/nodejs/node/commit/4a158cf1ab)] - **doc**: add MikeMcC399 as collaborator (Mike McCready) [#&#8203;64656](https://redirect.github.com/nodejs/node/pull/64656)
- \[[`d4cafce076`](https://redirect.github.com/nodejs/node/commit/d4cafce076)] - **(SEMVER-MINOR)** **lib,permission**: add `permission.drop` (Rafael Gonzaga) [#&#8203;62672](https://redirect.github.com/nodejs/node/pull/62672)
- \[[`b3cfb55267`](https://redirect.github.com/nodejs/node/commit/b3cfb55267)] - **(SEMVER-MINOR)** **loader**: implement package maps (Maël Nison) [#&#8203;62239](https://redirect.github.com/nodejs/node/pull/62239)
- \[[`cd1eb3e60b`](https://redirect.github.com/nodejs/node/commit/cd1eb3e60b)] - **(SEMVER-MINOR)** **src,permission**: add `--permission-audit` (RafaelGSS) [#&#8203;61869](https://redirect.github.com/nodejs/node/pull/61869)
- \[[`28dc85d8d2`](https://redirect.github.com/nodejs/node/commit/28dc85d8d2)] - **(SEMVER-MINOR)** **stream**: add `node:stream/iter` implementation (James M Snell) [#&#8203;62066](https://redirect.github.com/nodejs/node/pull/62066)
- \[[`d31c168740`](https://redirect.github.com/nodejs/node/commit/d31c168740)] - **(SEMVER-MINOR)** **test\_runner**: add `context.log()` and test:log event (Moshe Atlow) [#&#8203;64389](https://redirect.github.com/nodejs/node/pull/64389)
- \[[`add1edbc42`](https://redirect.github.com/nodejs/node/commit/add1edbc42)] - **(SEMVER-MINOR)** **test\_runner**: report `entryFile` in `TestStream` events (Moshe Atlow) [#&#8203;64309](https://redirect.github.com/nodejs/node/pull/64309)
- \[[`d937c8c6cd`](https://redirect.github.com/nodejs/node/commit/d937c8c6cd)] - **(SEMVER-MINOR)** **wasm**: enable JSPI (Guy Bedford) [#&#8203;59941](https://redirect.github.com/nodejs/node/pull/59941)

##### Commits

- \[[`1822c0f335`](https://redirect.github.com/nodejs/node/commit/1822c0f335)] - **assert,util**: fix TypeError on Maps with null keys (Paul Bouchon) [#&#8203;64441](https://redirect.github.com/nodejs/node/pull/64441)
- \[[`b12bcc9ae1`](https://redirect.github.com/nodejs/node/commit/b12bcc9ae1)] - **(SEMVER-MINOR)** **async\_hooks**: add us

> ❗ **Important**
> 
> ✂ PR body was truncated to here.


</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC43OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNzkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-14 08:03:32 -07:00
renovate[bot] 953b6a2d32 chore(deps): update lint (#697)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [oxfmt](https://oxc.rs/docs/guide/usage/formatter) ([source](https://redirect.github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt)) | [`0.66.0` → `0.67.0`](https://renovatebot.com/diffs/npm/oxfmt/0.66.0/0.67.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/oxfmt/0.67.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxfmt/0.66.0/0.67.0?slim=true) |
| [oxlint](https://oxc.rs/docs/guide/usage/linter) ([source](https://redirect.github.com/oxc-project/oxc/tree/HEAD/npm/oxlint)) | [`1.81.0` → `1.82.0`](https://renovatebot.com/diffs/npm/oxlint/1.81.0/1.82.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint/1.82.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint/1.81.0/1.82.0?slim=true) |

---

### Release Notes

<details>
<summary>oxc-project/oxc (oxfmt)</summary>

### [`v0.67.0`](https://redirect.github.com/oxc-project/oxc/blob/HEAD/npm/oxfmt/CHANGELOG.md#0670---2026-09-07)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxfmt_v0.66.0...oxfmt_v0.67.0)

##### 🛡️ Security

- [`9a612d5`](https://redirect.github.com/oxc-project/oxc/commit/9a612d5) deps: Update npm packages ([#&#8203;26178](https://redirect.github.com/oxc-project/oxc/issues/26178)) (renovate\[bot])

</details>

<details>
<summary>oxc-project/oxc (oxlint)</summary>

### [`v1.82.0`](https://redirect.github.com/oxc-project/oxc/blob/HEAD/npm/oxlint/CHANGELOG.md#1820---2026-09-07)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v1.81.0...oxlint_v1.82.0)

##### 🚀 Features

- [`6a0e19c`](https://redirect.github.com/oxc-project/oxc/commit/6a0e19c) linter/eslint/no-unmodified-loop-condition: Support `checkConditionalExpressions` option ([#&#8203;26249](https://redirect.github.com/oxc-project/oxc/issues/26249)) (camc314)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC43OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNzkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-14 08:03:13 -07:00
renovate[bot] 07b8abbb73 chore(deps): update dependency jdx/mise to v2026.9.7 (#696)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [jdx/mise](https://redirect.github.com/jdx/mise) | patch | `v2026.9.2` → `v2026.9.7` |

---

### Release Notes

<details>
<summary>jdx/mise (jdx/mise)</summary>

### [`v2026.9.7`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.7): : Lockfile revision 2 with npm and Python dependency graphs, dotfile conflict inspection

[Compare Source](https://redirect.github.com/jdx/mise/compare/v2026.9.6...v2026.9.7)

This release introduces `mise.lock` revision 2, which records complete transitive dependency graphs for npm tools (via embedded aube) and Python tools (via uv) in native sidecar files, and adds `mise bootstrap dotfiles conflicts` for inspecting dotfile sync conflicts before resolving them. It also lets dotfile templates consume bootstrap secrets, stops `minimum_release_age` from rejecting versions already committed to a lockfile, and closes a security gap in `history.describe_command`.

##### Added

- **lock:** Lockfile revision 2 records the full dependency graph of npm tools installed by embedded aube and of `pypi:` tools installed by uv, then replays it with a strict frozen install so two projects on the same top-level version can still receive their own reviewed transitive graph. Graphs live in native sidecar files (`uv.lock` / `aube-lock.yaml` plus a manifest) under `.mise/locks/<backend-tool>/<version>/`, referenced from `mise.lock` by relative path and SHA-256 digest, so the lockfile itself stays small. Commit the sidecar directory with `mise.lock`. New lockfiles use revision 2; existing revision 0 and 1 files keep their format until you run `mise lock --upgrade`. `mise lock --bump <tool>` refreshes a tool's transitive graph even when its top-level version is unchanged, ordinary `mise install` validates and accepts hand-edited sidecars, and `mise install --locked` rejects digest mismatches until you run `mise lock`. Python graph locking requires uv 0.12.10 or newer and published wheels for the target platform; Git sources, standalone pipx installs, and free-form `uvx_args`/`pipx_args` stay version-only. ([#&#8203;13131](https://redirect.github.com/jdx/mise/pull/13131), [#&#8203;13146](https://redirect.github.com/jdx/mise/pull/13146) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  mise lock --upgrade        # move an existing lockfile to revision 2 and resolve graphs
  mise install --locked      # replay the recorded graphs
  mise lock --bump pypi:black  # refresh Black's dependencies without changing its version
  ```
- **pypi:** `pypi:` is now the preferred name for the Python CLI backend; `pipx:` remains fully supported as an alias with no warnings, and settings accept both `pypi.*` and `pipx.*` names. The two spellings are distinct tool identities (`pypi-black` vs `pipx-black` install directories and lock entries), so switching spelling creates a new installation. ([#&#8203;13146](https://redirect.github.com/jdx/mise/pull/13146) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** `mise bootstrap dotfiles conflicts [PATH...]` shows a read-only comparison of the saved local and fetched remote versions of a conflicted dotfile so you can decide between `--take-remote` and `--keep-local` with full context. The default output is a unified diff including file-mode changes; `--difftool` opens the configured Git `diff.tool` (falling back to `merge.tool`) and `--tool <name>` picks one explicitly. Encrypted contents are decrypted only into private temporary files, and inspection never modifies either side or marks the conflict resolved. Bootstrap secrets are also now resolved from the same composed config maps as dotfile discovery, so root-scoped dotfile templates can use secrets declared by their bootstrap root. ([#&#8203;13144](https://redirect.github.com/jdx/mise/pull/13144) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  mise bootstrap dotfiles conflicts ~/.config/mise/config.toml
  mise bootstrap dotfiles conflicts --difftool ~/.config/mise/config.toml
  ```
- **dotfiles:** Dotfile templates (`mode = "template"`) can reference `[bootstrap.secrets]` values with `{{ secret(name="...") }}`, matching managed bootstrap file templates. Dotfiles commands that render templates (`add`, `apply`, `diff`, `edit`, `status`, `unapply`) accept `--prompt-secrets`; without an available value, rendering fails closed. A full `mise bootstrap` run preflights dotfile templates before making changes, `mise bootstrap status` reports secrets used only by dotfiles, and textual diffs redact resolved secret values. ([#&#8203;13140](https://redirect.github.com/jdx/mise/pull/13140) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [bootstrap.secrets]
  api_token = "EXAMPLE_API_TOKEN"

  [dotfiles."~/.config/example/credentials"]
  source = "dotfiles/credentials.tmpl"
  mode = "template"
  ```

##### Fixed

- **lock:** Installing from a committed `mise.lock` no longer fails when the locked release is younger than `minimum_release_age`. The cutoff still applies when resolving unlocked fuzzy requests and when generating or bumping a lockfile, and `npm:`/`pypi:` still forward it to unpinned transitive dependencies, but a reviewed lock entry now reproduces immediately in CI instead of waiting for the release to cool. ([#&#8203;13128](https://redirect.github.com/jdx/mise/pull/13128) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **config:** A `.python-version` (or other idiomatic version file) containing `system` selects the system interpreter without printing the mise-specific `@system` deprecation warning, matching the existing `.tool-versions` exception. Explicit `python@system` requests from mise configuration or command arguments still warn. ([#&#8203;13132](https://redirect.github.com/jdx/mise/pull/13132) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **npm:** Embedded aube is updated to 2.2.16, fixing the Bun checksum install regression and ensuring local npm tarballs keep their manifest package name. ([#&#8203;13145](https://redirect.github.com/jdx/mise/pull/13145) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **registry:** The `mc` shorthand uses `aqua:minio/mc` again now that the upstream Aqua registry entry is restored, with `asdf:mise-plugins/mise-mc` kept as the fallback. ([#&#8203;13124](https://redirect.github.com/jdx/mise/pull/13124) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Security

- **history:** `history.describe_command` is now global-only. Previously an implicitly trusted project could set it and have a later dotfiles history checkpoint execute the project-controlled command with unencrypted tracked-file diffs. The setting is honored only from system/global configuration or `MISE_HISTORY_DESCRIBE_COMMAND`; project values are ignored with a warning. ([#&#8203;13134](https://redirect.github.com/jdx/mise/pull/13134) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **oci:** `mise oci build` now renders dotfile templates with a restricted engine: `secret()` is rejected and the `env` context, `get_env()`, `exec()`, and `read_file()` are unavailable, so ambient credentials cannot be baked into a publishable image layer. ([#&#8203;13140](https://redirect.github.com/jdx/mise/pull/13140) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Breaking Changes

- **Lockfile revision 2 is not readable by older mise versions.** Newly created lockfiles use revision 2, and existing files switch only when you run `mise lock --upgrade`. Upgrade collaborators and CI to this release before committing a revision 2 `mise.lock`, and commit the `.mise/locks/` (or `.config/mise/locks/`) sidecar directory alongside it. Revision 2 `--locked` installs fail if a recorded graph is missing or its digest does not match. If you gitignore `mise.local.lock`, also ignore its matching sidecar subdirectory (for example `.mise/locks/mise.local/`).
- **`history.describe_command` in project configuration is ignored.** Move it to `~/.config/mise/config.toml` or set `MISE_HISTORY_DESCRIBE_COMMAND`.
- **`mise oci build` dotfile templates** can no longer call `secret()`, `get_env()`, `exec()`, or `read_file()` or read the `env` context.

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.6...v2026.9.7>

##### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

### [`v2026.9.6`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.6): : Project daemons, mise doctor project, and vfox backend tool discovery

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.9.5...v2026.9.6)

This release adds experimental project daemons backed by pitchfork, a `mise doctor project` command for project-declared diagnostic checks, and tool discovery from vfox backend plugins in `mise search`. It also changes the HTTP backend's default install layout so uninstall and prune reclaim disk space, speeds up warm `lockfile_mode = "generate"` installs and repeated OCI builds, and fixes a batch of nushell, monorepo, lockfile, brew, and Windows bootstrap issues.

#### Highlights

- **Services next to tools:** `[daemons]` declares background processes and PostgreSQL/Redis presets in `mise.toml`, managed through `mise daemons` and optionally started when you enter the project. `[doctor.checks.<name>]` lets projects declare their own environment probes for `mise doctor project`.
- **Discovery and output:** `mise search`, shell completion, and interactive `mise use` now include tools published by installed vfox backend plugins, and `settings.truncate` / `--no-truncate` disable terminal-width truncation (automatically when a coding agent is detected).
- **Storage and speed:** HTTP tools now extract into their own install directory by default (opt back into deduplication with `shared_extraction = true`), warm generate-mode installs skip needless lockfile rewrites, and OCI builds share a local tool-layer cache.

#### Added

- **daemons:** New experimental `[daemons]` section and `mise daemons` command family (`start`, `stop`, `restart`, `ls`, `status`, `logs`, `tui`) manage project background processes with pitchfork. PostgreSQL and Redis presets install the database as a tool (participating in lockfiles), supply connection environment variables and readiness checks, and keep project data across stop/start. Daemons with `auto = ["start", "stop"]` start when entering the project from an activated Bash, Zsh, or Fish shell and are released when the last shell session leaves. Requires `experimental = true` and pitchfork 2.25.0 or later; database presets are Unix-only and PostgreSQL uses loopback trust authentication intended for local development. ([#&#8203;13085](https://redirect.github.com/jdx/mise/pull/13085) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [settings]
  experimental = true

  [daemons]
  postgres = "18"
  redis = "8"

  [daemons.web]
  run = "npm run dev"
  port = 3000
  auto = ["start", "stop"]
  ```
- **doctor:** `mise doctor project` runs checks declared in `[doctor.checks.<name>]` with the project's environment and installed tools, reporting PASS/FAIL/error/skipped per check in text or `--json`. Checks support `description`, `hint`, `timeout` (default `10s`), `dir`, `shell`, and `os` selectors, run concurrently under the `jobs` limit, and exit nonzero when any check fails. Ordinary `mise doctor` does not run them, and hints are never executed. A follow-up aligned `dir` resolution with task conventions (config root for project configs including `~/mise.toml`, `~/` expansion), fixed head-of-line blocking when one probe hangs, and kept `nohup mise doctor project` alive on SIGHUP. ([#&#8203;13062](https://redirect.github.com/jdx/mise/pull/13062), [#&#8203;13089](https://redirect.github.com/jdx/mise/pull/13089) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [doctor.checks.openssl]
  description = "OpenSSL development files are discoverable"
  run = "pkg-config --exists openssl"
  hint = "Run `mise bootstrap packages apply` to install the declared build dependencies."
  timeout = "5s"
  os = ["linux", "macos"]
  ```
- **vfox:** Tools provided by installed vfox backend plugins now appear in `mise search`, shell completion, and interactive `mise use`, namespaced as `<plugin>:<tool>`. Plugins can implement `BackendListTools` for a finite catalog and/or `BackendSearchTools` for query-driven discovery in large ecosystems; a prefixed query like `npm:eslint` is routed only to that plugin. Results are cached, slow plugins fall back to stale cache, and existing plugins need no changes. `mise registry` remains registry-only. ([#&#8203;13111](https://redirect.github.com/jdx/mise/pull/13111) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **cli:** New `settings.truncate` (and `MISE_TRUNCATE`, default `true`) controls terminal-width shortening of table cells and task metadata. `mise ls`, `mise config ls`, and `mise bootstrap dotfiles status` gain `--truncate` / `--no-truncate`, and output is kept complete automatically when a known coding agent is detected. ([#&#8203;13112](https://redirect.github.com/jdx/mise/pull/13112) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  mise bootstrap dotfiles status --no-truncate
  ```
- **bootstrap:** `[bootstrap.macos.dock]` gains `apps`, an ordered list of pinned application paths. Status compares identity and order (ignoring Dock-added metadata), apply adds, removes, and reorders application tiles while preserving other tiles and `persistent-others`, and an empty list removes all application tiles. Paths must be absolute or home-relative `.app` bundles. ([#&#8203;13075](https://redirect.github.com/jdx/mise/pull/13075) by [@&#8203;azohra](https://redirect.github.com/azohra))

  ```toml
  [bootstrap.macos.dock]
  apps = [
    "/System/Applications/Utilities/Terminal.app",
    "/Applications/Firefox.app",
  ]
  ```
- **bootstrap:** `mise bootstrap packages where brew:<formula>` prints an installed formula's stable `opt` root (for example `/opt/homebrew/opt/unzip`), so scripts can put keg-only executables on PATH without hardcoding the Homebrew prefix or Cellar version. Missing installs exit nonzero with empty stdout. ([#&#8203;13083](https://redirect.github.com/jdx/mise/pull/13083) by [@&#8203;himkt](https://redirect.github.com/himkt))
- **dotfiles:** Destination `variants` can omit `source` when every variant sets a `target`; the entry key is then resolved as a relative path under `settings.dotfiles.root` instead of next to `mise.toml`. Parent traversal is rejected. ([#&#8203;13087](https://redirect.github.com/jdx/mise/pull/13087) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [dotfiles."vscode/settings.json"]
  mode = "copy"
  variants = [
    { os = "macos", target = "~/Library/Application Support/Code/User/settings.json" },
    { os = "linux", target = "~/.config/Code/User/settings.json" },
  ]
  ```
- **fmt:** `mise fmt` now sorts lists whose order has no meaning: `redactions` lexically, and task `sources`/`outputs`, `task_templates` sources/outputs, `task_config.global_inputs`, and `input_groups` by reach (`@group:` references, then globs, then literal paths). Lists containing `!` exclusions, entries starting with template syntax, or comments are left untouched, and precedence-sensitive lists such as `env_file`, `tools.*`, `includes`, and `depends` are never sorted. ([#&#8203;13058](https://redirect.github.com/jdx/mise/pull/13058) by [@&#8203;jrandolf](https://redirect.github.com/jrandolf))
- **oci:** `mise oci build` gains `--no-cache` to bypass the new local tool-layer cache; entries live under each tool's cache directory and are removed by `mise cache clear TOOL`. ([#&#8203;13056](https://redirect.github.com/jdx/mise/pull/13056) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Changed

- **http:** New `http:` installations extract directly into their own install directory, so `mise uninstall` and `mise prune` now remove their files instead of leaving payloads in `$MISE_DATA_DIR/http-tarballs/`. Set `shared_extraction = true` on a tool to keep the previous deduplicated symlink layout. Existing symlinked installs keep working; `mise install --force <tool>` migrates one to independent files without disturbing other installs that share the content. Legacy `http-tarballs` entries are not reclaimed automatically. Shared raw and compressed binary caches now also include the executable filename in their key, so differently named tools no longer reuse the wrong filename. ([#&#8203;13059](https://redirect.github.com/jdx/mise/pull/13059) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **oci:** `mise oci push --no-cache` now bypasses both the remote registry cache and the local tool-layer cache. ([#&#8203;13056](https://redirect.github.com/jdx/mise/pull/13056) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **registry:** `postgres`, `redis`, and `mongodb` now prefer `conda:` backends, installing prebuilt conda-forge binaries in seconds instead of compiling through vfox; vfox and asdf remain as fallbacks. `conda:redis-server` covers Linux and macOS only. ([#&#8203;13061](https://redirect.github.com/jdx/mise/pull/13061) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Performance

- **lockfile:** Warm `mise install` runs in `lockfile_mode = "generate"` skip scheduling work for tools whose artifact metadata is already reusable, and skip rebuilding, serializing, and staging the lockfile entirely when nothing was installed and the on-disk lock already matches (preserving comments in the file). Explicit `mise lock`, forced provenance verification, upgrades, and new platforms still regenerate. ([#&#8203;13101](https://redirect.github.com/jdx/mise/pull/13101), [#&#8203;13103](https://redirect.github.com/jdx/mise/pull/13103) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **oci:** `mise oci push --from BASE` no longer downloads base layers when the base and target live in the same repository, and `oci build`, `oci run`, and `oci push` share a local cache of packaged tool layers keyed on file contents, so repeated builds with overlapping tools skip tar and gzip work. ([#&#8203;13055](https://redirect.github.com/jdx/mise/pull/13055), [#&#8203;13056](https://redirect.github.com/jdx/mise/pull/13056) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Fixed

- **nushell:** `mise activate nu` no longer throws `env_variable_not_found` on every prompt or `cd` when a variable to hide is absent from the current scope; `hide-env` is now wrapped in `try`, matching the no-op behavior of other shells. ([#&#8203;13071](https://redirect.github.com/jdx/mise/pull/13071) by [@&#8203;i-api](https://redirect.github.com/i-api))
- **task:** Task-level tools are now auto-installed for tasks referenced from `run` entries, including names rendered at runtime, right before they execute; `--skip-tools` is honored and install failures are reported as task failures without blocking siblings. ([#&#8203;13086](https://redirect.github.com/jdx/mise/pull/13086) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **config:** `[monorepo]` settings are now merged across same-directory config layers (base plus `mise.<env>.toml` overlays): omitted fields are inherited, an overlay's `config_roots` replaces the base list, and `monorepo_root = false` in an overlay disables the root and its descendant trust. ([#&#8203;13084](https://redirect.github.com/jdx/mise/pull/13084) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **lockfile:** Runtime tool requests such as `mise which hk --tool hk@latest` now use the lockfile belonging to the config that effectively defines the tool, instead of merging project and global pins and reporting a false "multiple resolutions" ambiguity or selecting an overridden pin. `mise which --tool` warns when a lower-precedence config has a matching pin the effective config lacks. ([#&#8203;13042](https://redirect.github.com/jdx/mise/pull/13042) by [@&#8203;nettlesh](https://redirect.github.com/nettlesh))
- **lockfile:** Complete lockfile generation for Packslip tools skips platforms with no published artifact while still writing supported targets, and a verified Packslip signer may now replace legacy `github-attestations` metadata on upgrade instead of being rejected as a provenance downgrade. ([#&#8203;13102](https://redirect.github.com/jdx/mise/pull/13102), [#&#8203;13105](https://redirect.github.com/jdx/mise/pull/13105) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **upgrade:** `mise upgrade` now detects updates between letter-suffixed versions such as tmux `3.7b` to `3.7c`; `sub-N` aliases keep resolving numeric components as before. ([#&#8203;13119](https://redirect.github.com/jdx/mise/pull/13119) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew:** Formulae that are keg-only solely because macOS ships them (such as `brew:zip` and `brew:unzip`) are now linked into `<prefix>/bin` on Linux, matching Homebrew. Kegs installed by earlier mise versions stay unlinked until the next `mise bootstrap packages upgrade` or a reinstall. ([#&#8203;13108](https://redirect.github.com/jdx/mise/pull/13108) by [@&#8203;lil-lon](https://redirect.github.com/lil-lon))
- **brew-cask:** Tap casks with `preflight_steps` or `postflight_steps` no longer fail during metadata extraction; declarative `run` steps are captured as structured steps and executed by mise, with support for `must_succeed = false`. ([#&#8203;13060](https://redirect.github.com/jdx/mise/pull/13060) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** `mise bootstrap remote` on Windows now finds `ssh.exe` and `tar.exe` on PATH instead of failing with `required command 'ssh' not found`. ([#&#8203;13117](https://redirect.github.com/jdx/mise/pull/13117) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **bootstrap:** macOS defaults status explains type mismatches, showing for example `2 (real; expected integer)` instead of two identical-looking values marked `differs`. ([#&#8203;13096](https://redirect.github.com/jdx/mise/pull/13096) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **dotfiles:** `mise bootstrap dotfiles track` honors the global `yes` setting for confirmations, and warns when tracking a symlink whose resolved source is not itself tracked, suggesting the command to enroll it. ([#&#8203;13072](https://redirect.github.com/jdx/mise/pull/13072) by [@&#8203;nettlesh](https://redirect.github.com/nettlesh), [#&#8203;13095](https://redirect.github.com/jdx/mise/pull/13095) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **schema:** The JSON schema now models Git directory `manifest` dotfile entries, restricting explicit modes to `copy` or `symlink-each` and rejecting combinations with inline content or file-edit fields. ([#&#8203;12741](https://redirect.github.com/jdx/mise/pull/12741) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **asdf:** `mise asdf install` and `mise asdf reshim` no longer re-enter the full CLI dispatch, avoiding stack overflows on small-stack Linux environments; `asdf install` now follows the same implicit config trust as `mise install`. Bash completions are regenerated for the updated usage-rs word-break handling. ([#&#8203;13114](https://redirect.github.com/jdx/mise/pull/13114) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Registry

- Added `mpv` (`conda:mpv`, Linux and macOS) ([#&#8203;13049](https://redirect.github.com/jdx/mise/pull/13049) by [@&#8203;i-api](https://redirect.github.com/i-api)), `agent-browser` (`aqua:vercel-labs/agent-browser`) ([#&#8203;13088](https://redirect.github.com/jdx/mise/pull/13088) by [@&#8203;3w36zj6](https://redirect.github.com/3w36zj6)), and `himalaya` (`github:pimalaya/himalaya`) ([#&#8203;13091](https://redirect.github.com/jdx/mise/pull/13091) by [@&#8203;i-api](https://redirect.github.com/i-api)).
- `editorconfig-checker` installs again after 4.0.1 renamed its assets and executable; the shorthand now uses the GitHub backend. ([#&#8203;13098](https://redirect.github.com/jdx/mise/pull/13098) by [@&#8203;jdx](https://redirect.github.com/jdx))
- `mc` installs from MinIO's GitHub releases instead of the retired Aqua download URL that returned HTTP 410. ([#&#8203;13113](https://redirect.github.com/jdx/mise/pull/13113) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Documentation

- The docs landing page gains interactive diagrams for project environment switching, bootstrap machine resources, tool configuration precedence, artifact cache execution, and tracked dotfile synchronization. ([#&#8203;13065](https://redirect.github.com/jdx/mise/pull/13065), [#&#8203;13066](https://redirect.github.com/jdx/mise/pull/13066), [#&#8203;13067](https://redirect.github.com/jdx/mise/pull/13067), [#&#8203;13068](https://redirect.github.com/jdx/mise/pull/13068), [#&#8203;13069](https://redirect.github.com/jdx/mise/pull/13069) by [@&#8203;jdx](https://redirect.github.com/jdx))
- The task-running guide is reorganized around finding and running tasks, passing arguments, then controlling execution. ([#&#8203;13077](https://redirect.github.com/jdx/mise/pull/13077) by [@&#8203;azohra](https://redirect.github.com/azohra))

#### New Contributors

- [@&#8203;lil-lon](https://redirect.github.com/lil-lon) made their first contribution in [#&#8203;13108](https://redirect.github.com/jdx/mise/pull/13108)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.5...v2026.9.6>

#### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

### [`v2026.9.5`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.5): : macOS defaults, per-platform dotfiles, and complete lockfiles

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.9.4...vfox-v2026.9.5)

This release deepens macOS bootstrap support with current-host and nested defaults, lets dotfiles and tasks adapt to the platform and to parsed arguments, and adds an opt-in trial of complete lockfile generation. It also carries a batch of install progress, self-update, brew-cask, and sandbox fixes.

#### Added

- **bootstrap:** New `[[bootstrap.macos.defaults_entries]]` blocks let you set macOS preferences explicitly with `domain`, `key`, `value`, and an optional `host` (`any` by default, or `current`), covering preferences normally written via `defaults -currentHost` while keeping the existing `[bootstrap.macos.defaults]` shorthand. Entries also accept an optional `path` to patch a nested dictionary value without replacing its siblings, preserving property-list types and creating missing parents. ([#&#8203;12983](https://redirect.github.com/jdx/mise/pull/12983) by [@&#8203;azohra](https://redirect.github.com/azohra), [#&#8203;12984](https://redirect.github.com/jdx/mise/pull/12984) by [@&#8203;azohra](https://redirect.github.com/azohra))

  ```toml
  [[bootstrap.macos.defaults_entries]]
  domain = "com.apple.dock"
  key = "autohide"
  value = true
  host = "current"
  ```
- **bootstrap:** More friendly macOS preferences: Finder folder sorting and default cloud save location, Dock autohide delay and timing (integers or floats), and keyboard automatic capitalization and spelling correction, all using snake\_case names consistent with the existing sections. ([#&#8203;13032](https://redirect.github.com/jdx/mise/pull/13032) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** `[bootstrap.files]` and `[bootstrap.directories]` entries gain `phase = "pre-packages"` so repository definitions, apt sources, and signing keys can be applied before package installation instead of only afterward. Existing declarations default to `"post-packages"`. ([#&#8203;13052](https://redirect.github.com/jdx/mise/pull/13052) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [bootstrap.files."/etc/apt/sources.list.d/vendor.sources"]
  source = "./files/vendor.sources"
  phase = "pre-packages"
  ```
- **bootstrap:** Ordinary `[bootstrap.files]` templates can now reference resolved `[vars]` values, alongside the existing `config_root`, `target`, and `secret()` helpers. ([#&#8203;13033](https://redirect.github.com/jdx/mise/pull/13033) by [@&#8203;nettlesh](https://redirect.github.com/nettlesh))
- **dotfiles:** A single dotfiles source can deploy to different destinations per operating system, architecture, or mise profile using `variants` with an optional `target`. This works for `copy`, `symlink`, `symlink-each`, and `template` modes. ([#&#8203;13050](https://redirect.github.com/jdx/mise/pull/13050) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [dotfiles.settings]
  source = "dotfiles/vscode/settings.json"
  mode = "copy"
  variants = [
    { os = "macos", target = "~/Library/Application Support/Code/User/settings.json" },
    { os = "linux", target = "~/.config/Code/User/settings.json" },
  ]
  ```
- **task:** Task `sources` and `outputs` can now use `{{usage.*}}` templates, resolved per invocation from parsed arguments and flags before freshness and artifact-cache checks run, so different argument values track freshness independently. ([#&#8203;13051](https://redirect.github.com/jdx/mise/pull/13051) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew-cask:** Casks with structured `set_permissions` preflight/postflight steps now install correctly (for example `brew-cask:blender`), running an unprivileged `chmod` over resolved staged or appdir paths instead of failing with an unsupported step-type error. ([#&#8203;13043](https://redirect.github.com/jdx/mise/pull/13043) by [@&#8203;azohra](https://redirect.github.com/azohra))
- **lock:** Opt-in trial of complete lockfile generation via `lockfile_mode = "generate"` (or `MISE_LOCKFILE_MODE=generate`). The default remains incremental `merge`. Generate mode rebuilds lockfiles from current requests while treating the previous file as an immutable baseline, reusing unchanged artifacts and publishing through staged atomic writes so failures or concurrent edits do not clobber a good lockfile. This mode records only cryptographically verified provenance per target platform; `provenance_verified` is no longer treated as a trust signal. ([#&#8203;13031](https://redirect.github.com/jdx/mise/pull/13031) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **self-update:** New `disable_update_warning` setting (`MISE_DISABLE_UPDATE_WARNING`) suppresses "newer mise available" notices in `mise version`, `mise --version`, and `mise doctor`. Explicit self-update and automatic updates are unaffected. ([#&#8203;13028](https://redirect.github.com/jdx/mise/pull/13028) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Fixed

- **install:** Interactive installs no longer leave a permanent line for every resolved, skipped, or already-installed tool; live progress shows what is happening while the final summary lists what changed (for example `installed 1 tool in 1.1s: [email protected]`). `mise upgrade` no longer duplicates its old to new version list. ([#&#8203;13030](https://redirect.github.com/jdx/mise/pull/13030) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **install:** Long non-TTY installs no longer flood CI logs with a snapshot every three seconds. The heartbeat now scales to roughly 10% of elapsed time, clamped between 3 seconds and 1 minute. ([#&#8203;13036](https://redirect.github.com/jdx/mise/pull/13036) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **self-update:** On 32-bit ARM, `mise self-update` now selects the correct `linux-armv7` archive instead of requesting a missing `linux-arm` one and falling back to an ARM64 binary that failed signature verification. A missing archive now fails asset selection rather than picking the wrong architecture. ([#&#8203;13023](https://redirect.github.com/jdx/mise/pull/13023) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **self-update:** npm installs now ship the instructions file that redirects update guidance to the package manager, so they no longer advertise `mise self-update`. ([#&#8203;13028](https://redirect.github.com/jdx/mise/pull/13028) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew-cask:** `mise bootstrap packages upgrade` no longer replaces the bundle of a running self-updating app (for example Chrome), which could strand helper processes and blank out tabs. Such apps are skipped while running and left to update themselves. ([#&#8203;13041](https://redirect.github.com/jdx/mise/pull/13041) by [@&#8203;azohra](https://redirect.github.com/azohra))
- **brew-cask:** The cask metadata fetch error no longer includes stale advice about installing with `brew` and a broken documentation anchor; it now gives a short, accurate message. ([#&#8203;12800](https://redirect.github.com/jdx/mise/pull/12800) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **bootstrap:** `mise bootstrap dotfiles origin set <url>` now uses the repository's own default branch when `--branch` is omitted, so repositories on `master` connect correctly instead of publishing a second root branch. A missing requested branch is now reported clearly rather than mistaken for an empty repository. ([#&#8203;13037](https://redirect.github.com/jdx/mise/pull/13037) by [@&#8203;Dhaulagiri](https://redirect.github.com/Dhaulagiri))
- **dotfiles:** History watcher locks now live alongside the history store in the state directory instead of being hashed into the cache directory, so a launchd watcher and an interactive shell using different cache directories coordinate correctly and `dotfiles status` no longer misreports `declared-not-running`. Existing watchers must be stopped and restarted with the updated binary. ([#&#8203;13038](https://redirect.github.com/jdx/mise/pull/13038) by [@&#8203;ascarter](https://redirect.github.com/ascarter))
- **sandbox:** The macOS Seatbelt profile now allows `file-read-metadata` on the ancestors of readable paths, fixing `Operation not permitted` failures when a portable Ruby resolves its own executable during third-party tap evaluation. Symlinked data directories and allow-listed paths are also handled. ([#&#8203;13039](https://redirect.github.com/jdx/mise/pull/13039) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))

#### Registry

- Added `clipboard` ([github:Slackadays/Clipboard](https://redirect.github.com/Slackadays/Clipboard)) by [@&#8203;i-api](https://redirect.github.com/i-api) in [#&#8203;13044](https://redirect.github.com/jdx/mise/pull/13044)

#### New Contributors

- [@&#8203;Dhaulagiri](https://redirect.github.com/Dhaulagiri) made their first contribution in [#&#8203;13037](https://redirect.github.com/jdx/mise/pull/13037)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.4...v2026.9.5>

#### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

### [`v2026.9.4`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.4): : Nix Bootstrap, Environment Selectors, and Man Pages

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.9.3...vfox-v2026.9.4)

This release expands the bootstrap package model with Nix support and environment selectors, teaches packslip tools to ship man pages, and adds a task-scoped quiet setting. It also carries a wide batch of packslip, bootstrap, and cross-platform fixes plus a major speedup to dotfiles history rebuilds.

#### Added

- **bootstrap:** Nix is now a built-in `[bootstrap.packages]` manager on Linux and macOS. Declare packages with the `nix:` prefix and apply them through your normal Nix profile; the manager supports use, apply, status JSON, and targeted upgrades while leaving Nix sources, caches, trust, and profile rollback under Nix control. This also adds `mise bootstrap packages export --format nix` to emit a deterministic NixOS module from your `nix:` declarations and `packages use --no-install` to record declarations without touching package managers. ([#&#8203;13013](https://redirect.github.com/jdx/mise/pull/13013) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [bootstrap.packages]
  "nix:ripgrep" = "latest"
  ```
- **bootstrap:** `[bootstrap.packages]` entries gain an `env` selector (a single environment or a list), so a package is only enabled when one of its listed mise environments is active via `-E` or `MISE_ENV`. When both `os` and `env` are set, both must match. Packages for inactive environments stay declared and are protected from pruning. ([#&#8203;12956](https://redirect.github.com/jdx/mise/pull/12956) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [bootstrap.packages]
  "brew:postgresql" = { version = "latest", env = ["dev", "test"] }
  ```
- **packslip:** Packslip-installed tools can now ship man pages declared as static `man` resources, alongside completions and agent skills. While a tool version is active, mise prepends its man root to `MANPATH` and keeps system and caller-defined paths visible, so `man <tool>` works. Existing installs need to be reinstalled to pick up man pages. ([#&#8203;13012](https://redirect.github.com/jdx/mise/pull/13012) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **task:** New `task.quiet` setting (and `MISE_TASK_QUIET`) suppresses mise's own task messages, prefixes, and command-echo headers without hiding task output or affecting other commands. The bundled `output = "quiet"` mode is deprecated in favor of explicit output style plus this setting; removal is scheduled for 2027.9.3. ([#&#8203;12980](https://redirect.github.com/jdx/mise/pull/12980) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Fixed

- **install:** A lazy tool whose `depends` target is also lazy now installs correctly on first use through a shim, `mise x`, or a task; mise installs the provider together with its still-missing configured dependencies instead of failing the preflight. ([#&#8203;12997](https://redirect.github.com/jdx/mise/pull/12997) by [@&#8203;balintant](https://redirect.github.com/balintant))
- **backend:** 32-bit ARM resolution now uses Go's canonical `arm` architecture name for Aqua (so registry replacements apply), and automatic GitHub release asset selection no longer picks a generic `source.tar.gz` when no published binary targets the host. ([#&#8203;13004](https://redirect.github.com/jdx/mise/pull/13004) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Homebrew bottles with hard-linked Mach-O executables are now signed correctly on macOS. Every hard-link alias is tracked and included in the signing list after relocation, fixing cases like fish where an aliased binary was killed with SIGKILL after a successful install. ([#&#8203;12988](https://redirect.github.com/jdx/mise/pull/12988) by [@&#8203;nettlesh](https://redirect.github.com/nettlesh))
- **packslip:** `mise lock --platform` now verifies the signed release manifest and records the correct URL, checksum, size, and signer for each requested target platform (including Windows x64), so locked installs work across platforms. ([#&#8203;13002](https://redirect.github.com/jdx/mise/pull/13002) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **packslip:** On glibc Linux, an artifact's `glibc_min` is now honored during selection: when the GNU build requires a newer glibc than the host, mise falls back to a matching static musl build if one exists. ([#&#8203;13009](https://redirect.github.com/jdx/mise/pull/13009) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **packslip:** Windows installs now link shims with the correct `.exe` filename, while Unix keeps extensionless names. ([#&#8203;13006](https://redirect.github.com/jdx/mise/pull/13006) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **packslip:** Activated man roots are now scoped to Packslip-backed tool versions, and the caller's original `MANPATH` is included in the environment-cache identity so one cached process cannot serve another caller's `MANPATH`. ([#&#8203;13016](https://redirect.github.com/jdx/mise/pull/13016) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **prune:** `mise prune` no longer exits successfully without pruning when it cannot show a confirmation prompt; it now fails with guidance to pass `--yes` or set `MISE_YES=1`. Follow-up prunes from commands like `mise unuse` still leave installs in place when nobody can answer. ([#&#8203;13003](https://redirect.github.com/jdx/mise/pull/13003) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **generate:** `mise generate tool-stub` now respects a stub's top-level `os` selector when deciding whether to write a Windows `.cmd` launcher, so a linux/macos-only stub no longer produces a launcher even with `--lock`. ([#&#8203;13008](https://redirect.github.com/jdx/mise/pull/13008) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **dotfiles:** History path filters resolve correctly when `HOME` is a symlink, so aliased, canonical, and tilde paths address the same history entry and `show latest`/`diff` no longer report a missing checkpoint. ([#&#8203;12982](https://redirect.github.com/jdx/mise/pull/12982) by [@&#8203;azohra](https://redirect.github.com/azohra))
- **brew:** The published crate now includes the tap formula and cask metadata shims, fixing `cargo install --locked mise` builds from crates.io. ([#&#8203;13001](https://redirect.github.com/jdx/mise/pull/13001) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Changed

- **bootstrap:** `[bootstrap].config_roots` and the `mise bootstrap config-roots` command are now deprecated (hidden from help and removed from public docs), with removal scheduled for mise 2027.3.3. Existing configurations keep working during the compatibility window but emit a warning directing users to global or system bootstrap configuration. ([#&#8203;13010](https://redirect.github.com/jdx/mise/pull/13010) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **registry:** Added basecamp to the registry. ([#&#8203;13024](https://redirect.github.com/jdx/mise/pull/13024))

#### Performance

- **history:** Dotfiles history index rebuilds are dramatically faster, reconstructing checkpoint metadata in-process with gix instead of spawning Git per checkpoint. On an 80-checkpoint, 44-file reproducer this cut a rebuild from roughly 26-28 seconds and thousands of Git processes down to under a second. Removed annotations are now correctly dropped after a rebuild, and SHA-256 history repositories are supported. ([#&#8203;13011](https://redirect.github.com/jdx/mise/pull/13011) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Deprecated

- The task `output = "quiet"` mode (use `task.quiet` instead; removal in 2027.9.3) ([#&#8203;12980](https://redirect.github.com/jdx/mise/pull/12980)).
- `[bootstrap].config_roots` and `mise bootstrap config-roots` (removal in mise 2027.3.3) ([#&#8203;13010](https://redirect.github.com/jdx/mise/pull/13010)).

#### New Contributors

- [@&#8203;balintant](https://redirect.github.com/balintant) made their first contribution in [#&#8203;12997](https://redirect.github.com/jdx/mise/pull/12997)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.3...v2026.9.4>

#### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

### [`v2026.9.3`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.3): : Winget bootstrap, signed vfox plugins, and faster inline commands

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.9.2...vfox-v2026.9.3)

This release brings Windows bootstrap into the package-manager era with WinGet support, extends the packslip signing model to vfox plugins, and speeds up simple tasks by skipping the shell on Unix. It also carries a batch of bootstrap, sandbox, and Windows ARM64 fixes.

##### Added

- **bootstrap:** WinGet is now a built-in `[bootstrap.packages]` manager on Windows, so `mise bootstrap packages` can check status, install, and upgrade apps by exact package ID. It supports version pins, source refresh, and automatic source-agreement acceptance. Scoop, Chocolatey, and package removal remain future work. ([#&#8203;12928](https://redirect.github.com/jdx/mise/pull/12928) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [bootstrap.packages]
  "winget:Microsoft.PowerShell" = "latest"
  ```
- **vfox:** External vfox plugins can now be installed from signed packslip archives instead of a Git clone, reusing the packslip backend's release selection, signature verification, digest checks, and signer pins. This is aimed at non-registry plugins; registry defaults and embedded plugins are unchanged. ([#&#8203;12948](https://redirect.github.com/jdx/mise/pull/12948) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  mise plugins install vfox:bfs 'packslip:mise-plugins/vfox-bfs#0.1.0'
  ```
- **bootstrap:** Dotfile line edits gain `position = "prepend"` to insert a managed line at the top of a file instead of appending (the default). An existing exact match is left in place, and unrelated bytes, CRLF endings, and a UTF-8 BOM are preserved. ([#&#8203;12941](https://redirect.github.com/jdx/mise/pull/12941) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [dotfiles]
  "/etc/zshrc/zdotdir" = { line = 'ZDOTDIR=$HOME/.config/zsh/', position = "prepend" }
  ```
- **bootstrap:** macOS bootstrap `defaults` now support nested TOML arrays and tables (for example Dock `persistent-apps`), reading and writing preferences through Core Foundation so booleans, numbers, strings, arrays, and dictionaries keep their plist types. Collections apply as whole-value replacements; datetimes and binary plist data are still skipped with a warning. ([#&#8203;12947](https://redirect.github.com/jdx/mise/pull/12947) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Changed

- **bootstrap:** Adopting global configuration or a shared dotfile-history setup repository is now spelled `mise bootstrap --adopt` (and `mise bootstrap remote --adopt`), separate from `mise bootstrap --from` for running a bootstrap project. The dotfiles, history, setup, and services guides were reorganized to lead with usable examples. The previous `--from-git` spelling still works as a hidden alias but warns and is scheduled for removal in mise 2026.10.0. ([#&#8203;12953](https://redirect.github.com/jdx/mise/pull/12953) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Fixed

- **github:** Git operations (clones, fetches, submodules) that GitHub now challenges with HTTP 401 on public repositories can authenticate using mise's existing GitHub token sources via a command-scoped `github.com` HTTPS credential helper. Tokens are kept out of command arguments and URLs, and Enterprise hosts are out of scope. ([#&#8203;12945](https://redirect.github.com/jdx/mise/pull/12945) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **config:** The default inline task shell is now `sh -o errexit -c` so shells that consume the argument right after `-c` (such as FreeBSD sh) run your command instead of trying to execute `-o`. ([#&#8203;12949](https://redirect.github.com/jdx/mise/pull/12949) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **windows:** Fixed two Windows ARM64 issues: stale command-wrapper shims (like a leftover `cargo.exe`) that caused recursive `.exe`/`.cmd` dispatch after shim-mode changes are now pruned, and Aqua ARM64 emulation candidates apply the registry's `amd64` replacement so tools like LuaLS resolve their published `win32-x64` assets. ([#&#8203;12931](https://redirect.github.com/jdx/mise/pull/12931) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Bootstrap no longer aborts on Arch when `pacman -Q` prints a file advisory for a missing package that shares its name with a directory in the current working directory (for example a `fish/` folder in a dotfiles checkout). ([#&#8203;12932](https://redirect.github.com/jdx/mise/pull/12932) by [@&#8203;nettlesh](https://redirect.github.com/nettlesh))
- **sandbox:** Read-restricted macOS Seatbelt profiles now allow metadata-only access to `/private`, fixing startup failures for load-relative binaries like Ruby 4.0.6 when run under `/private/tmp`. Directory listings and descendant reads stay denied. ([#&#8203;12940](https://redirect.github.com/jdx/mise/pull/12940) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** The macOS notification helper is now built and Developer ID-signed at release time and spawned with a start handshake, so short `dotfiles sync` runs no longer race the helper and macOS accepts it. Unsigned source builds (such as Homebrew) disable notifications and warn during origin setup. ([#&#8203;12946](https://redirect.github.com/jdx/mise/pull/12946) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **vfox:** vfox install phases now flow through the active install progress reporter, mapping noisy download, checksum, attestation, and extraction lines into concise phases instead of scrolling log output. ([#&#8203;12944](https://redirect.github.com/jdx/mise/pull/12944) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **java:** The `java -version` banner shown during install verification now stays inside the interactive install progress row instead of printing to the terminal. ([#&#8203;12943](https://redirect.github.com/jdx/mise/pull/12943) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **version:** The auto-update hint from `mise version` now uses the `mise settings auto_update=true` syntax to match the rest of `mise settings`. ([#&#8203;12957](https://redirect.github.com/jdx/mise/pull/12957) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Performance

- On Unix, plain inline commands (like `node build.js`) now run directly when a conservative planner deems them safe, skipping the shell wrapper. Anything involving shell syntax, quoting, expansion, builtins, ambiguous PATH lookup, explicit shell settings, or sandboxed/audited tasks still uses the shell, and Windows is unchanged. In a microbenchmark of short commands this cut about 11% off elapsed time. ([#&#8203;12950](https://redirect.github.com/jdx/mise/pull/12950) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Registry

- Added cargo-deny, kingfisher, nushell, shellharden, sherif, and tauri-cli. ([#&#8203;12938](https://redirect.github.com/jdx/mise/pull/12938) by [@&#8203;jrandolf](https://redirect.github.com/jrandolf))

##### Documentation

- Restored the prominent project name and pronunciation on the homepage. ([#&#8203;12936](https://redirect.github.com/jdx/mise/pull/12936) by [@&#8203;jdx](https://redirect.github.com/jdx))
- Dropped the "you need Git installed" prerequisite from the install docs. ([#&#8203;12958](https://redirect.github.com/jdx/mise/pull/12958) by [@&#8203;jdx](https://redirect.github.com/jdx))

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.2...v2026.9.3>

##### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC43OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNzkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-14 08:03:05 -07:00
renovate[bot] 9c43369ad3 chore(deps): update dependency @types/node to v24.13.4 (#695)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@types/node](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | [`24.13.3` → `24.13.4`](https://renovatebot.com/diffs/npm/@types%2fnode/24.13.3/24.13.4) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fnode/24.13.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fnode/24.13.3/24.13.4?slim=true) |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC43OS4xIiwidXBkYXRlZEluVmVyIjoiNDQuNzkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-14 08:02:50 -07:00
renovate[bot] ca45510e08 chore(deps): update dependency @expo/batcher to v1.1.2 (#691)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| @&#8203;expo/batcher | [`1.0.0` → `1.1.2`](https://renovatebot.com/diffs/npm/@expo%2fbatcher/1.0.0/1.1.2) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@expo%2fbatcher/1.1.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@expo%2fbatcher/1.0.0/1.1.2?slim=true) |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42MS4zIiwidXBkYXRlZEluVmVyIjoiNDQuNjEuMyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-08 08:18:03 -07:00
renovate[bot] 4f5d13d5c7 chore(deps): update jest to v30.5.1 (#693)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@jest/globals](https://redirect.github.com/jestjs/jest) ([source](https://redirect.github.com/jestjs/jest/tree/HEAD/packages/jest-globals)) | [`30.4.1` → `30.5.1`](https://renovatebot.com/diffs/npm/@jest%2fglobals/30.4.1/30.5.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@jest%2fglobals/30.5.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@jest%2fglobals/30.4.1/30.5.1?slim=true) |
| [jest](https://jestjs.io/) ([source](https://redirect.github.com/jestjs/jest/tree/HEAD/packages/jest)) | [`30.4.2` → `30.5.1`](https://renovatebot.com/diffs/npm/jest/30.4.2/30.5.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/jest/30.5.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jest/30.4.2/30.5.1?slim=true) |

---

### Release Notes

<details>
<summary>jestjs/jest (@&#8203;jest/globals)</summary>

### [`v30.5.1`](https://redirect.github.com/jestjs/jest/blob/HEAD/CHANGELOG.md#3051)

[Compare Source](https://redirect.github.com/jestjs/jest/compare/v30.5.0...v30.5.1)

##### Fixes

- `[jest-config]` Don't warn about global-only options in the config that supplies the global config - the root config a project resolves to, or the first entry of `--projects` when no root config is passed ([#&#8203;16411](https://redirect.github.com/jestjs/jest/pull/16411))
- `[jest-config, jest-types]` Stop accepting `reporters`, `coverageReporters`, `workerIdleMemoryLimit`, `cwd` and `runnerOptions` in a project config - they were silently ignored, and now warn like the other global-only options ([#&#8203;16411](https://redirect.github.com/jestjs/jest/pull/16411))
- `[jest-config, jest-validate]` Warn about `maxWorkers` and `coverageThreshold` in a project config instead of dropping them without a word ([#&#8203;16411](https://redirect.github.com/jestjs/jest/pull/16411))
- `[jest-resolve]` Match `moduleNameMapper` patterns against the specifier as written again (reverting [#&#8203;16390](https://redirect.github.com/jestjs/jest/pull/16390)) ([#&#8203;16417](https://redirect.github.com/jestjs/jest/pull/16417))
- `[jest-runtime]` Resolve package `imports` specifiers like `#dep` under ESM again ([#&#8203;16413](https://redirect.github.com/jestjs/jest/pull/16413))

##### Chore & Maintenance

- `[jest-util]` Name the `testEnvironmentOptions.globalsCleanup` option and link the docs from the `JEST-01` deprecation warning, and document the option's modes ([#&#8203;16404](https://redirect.github.com/jestjs/jest/pull/16404))

### [`v30.5.0`](https://redirect.github.com/jestjs/jest/blob/HEAD/CHANGELOG.md#3050)

[Compare Source](https://redirect.github.com/jestjs/jest/compare/v30.4.1...v30.5.0)

##### Features

- `[@jest/expect-utils, jest-mock]` Add `mockFn.whenCalledWith(...args)` for configuring return values per argument list, with first-class asymmetric-matcher support ([#&#8203;16053](https://redirect.github.com/jestjs/jest/pull/16053))
- `[@jest/expect-utils]` Export `AsymmetricMatcher` and `FunctionParameters` types (previously private to `expect`) ([#&#8203;16053](https://redirect.github.com/jestjs/jest/pull/16053))
- `[jest-circus, jest-core, jest-jasmine2, jest-test-result, jest-types]` `--collectTests` now expands `test.each`/`describe.each` cases and reports per-status counts (skipped/todo via the new `wouldRun` flag for selected tests) plus a summary line that match a real run, including under `--testNamePattern` and `.only`/`fdescribe` focus on both the circus and jasmine2 runners ([#&#8203;16259](https://redirect.github.com/jestjs/jest/pull/16259))
- `[jest-circus, jest-environment, jest-runtime, jest-types]` Add describe-level retries via `jest.retryTimes(..., {entireDescribe: true})` ([#&#8203;16322](https://redirect.github.com/jestjs/jest/pull/16322))
- `[jest-circus, jest-message-util, jest-reporters, jest-types]` Add `retryMessages` to `AssertionResult` and export `formatErrorStack`, so the retry log renders nested `cause` and `AggregateError` sections with code frames instead of serialized `[cause]:`/`[errors]:` markers ([#&#8203;16316](https://redirect.github.com/jestjs/jest/pull/16316))
- `[jest-circus, jest-types]` Add `unhandledErrorsDetailed` to `Circus.RunResult`, so an unhandled rejection reports its `cause` chain and `AggregateError` entries with code frames instead of a pre-serialized stack ([#&#8203;16316](https://redirect.github.com/jestjs/jest/pull/16316))
- `[jest-haste-map]` Replace `NodeWatcher` and `FSEventsWatcher` with `@parcel/watcher` for the non-watchman watch path ([#&#8203;16188](https://redirect.github.com/jestjs/jest/pull/16188))
- `[jest-resolve]` Bump `unrs-resolver` to 1.12.1, remove `jest-pnp-resolver` and unnecessary checks ([#&#8203;15721](https://redirect.github.com/jestjs/jest/pull/15721))
- `[jest-resolve]` Honor Node's `--preserve-symlinks` / `NODE_PRESERVE_SYMLINKS` in the default resolver by passing `symlinks: false` to `unrs-resolver` ([#&#8203;16260](https://redirect.github.com/jestjs/jest/pull/16260))
- `[jest-runtime]` Apply automocking and manual `__mocks__` files to synchronously evaluable ESM graphs on Node 24.9+ - static imports, dynamic `import()` and `require()` of an ESM file now generate an automock from the real module's namespace instead of failing with "Attempting to import a mock without a factory". Graphs that need async evaluation (top-level await) or an async-only resolver or transformer still throw ([#&#8203;16391](https://redirect.github.com/jestjs/jest/pull/16391))
- `[jest-runtime]` Route `process.getBuiltinModule` through the sandbox, so it returns the sandbox `process` and the hooked `node:module` instead of the host's ([#&#8203;16391](https://redirect.github.com/jestjs/jest/pull/16391))
- `[jest-runtime]` Throw an actionable error from `module.register()` and `module.registerHooks()` inside a test - the hooks attached to the loader running Jest itself, never saw the sandboxed requires they were meant for, and stayed registered for every later test file in the worker ([#&#8203;16391](https://redirect.github.com/jestjs/jest/pull/16391))
- `[jest-runtime]` Surface resolution and import-attribute errors in an ESM graph before executing any of its CJS dependencies on Node 24.9+, matching Node's run-nothing-on-a-broken-graph behavior; the legacy loader on older versions keeps its linking-time execution order ([#&#8203;16391](https://redirect.github.com/jestjs/jest/pull/16391))
- `[jest-runtime]` Throw `ERR_SOURCE_PHASE_NOT_DEFINED` with an actionable message for `import source` and `import.source()`, instead of failing at instantiation with V8's bare "Source phase import object is not defined" ([#&#8203;16391](https://redirect.github.com/jestjs/jest/pull/16391))
- `[jest-runtime]` Emit the JSON-without-import-attribute deprecation warning once per test file instead of once per worker, so it is no longer silently swallowed for every file after the first ([#&#8203;16391](https://redirect.github.com/jestjs/jest/pull/16391))
- `[jest-runtime]` Set `import.meta.main` to `true` in the test file and `false` in every module it loads, matching Node 24+ ([#&#8203;16367](https://redirect.github.com/jestjs/jest/pull/16367))
- `[jest-runtime]` Resolve the `module-sync` export condition, so a package that exposes its ESM entry point for `require()` loads the same file Node would ([#&#8203;16336](https://redirect.github.com/jestjs/jest/pull/16336))
- `[jest-snapshot]` Add external snapshot paths to custom reporter failure details ([#&#8203;16374](https://redirect.github.com/jestjs/jest/pull/16374))

##### Fixes

- `[jest-console, jest-reporters]` `CustomConsole` now buffers console output so `TestResult.console` is populated for reporters when `verbose` is enabled, while `GitHubActionsReporter` avoids replaying buffered output in verbose mode ([#&#8203;16155](https://redirect.github.com/jestjs/jest/pull/16155))
- `[expect, jest-message-util, jest-pattern, jest-regex-util, jest-util]` Revert `node:` protocol imports to restore webpack/browser-bundle compatibility ([#&#8203;16167](https://redirect.github.com/jestjs/jest/pull/16167))
- `[expect]` Widen `toMatchObject` and `objectContaining` parameter type from `Record<string, unknown>` to `object` so class instances are accepted ([#&#8203;16196](https://redirect.github.com/jestjs/jest/pull/16196))
- `[jest-circus]` Call a generator test body with the shared test context, so `this` matches what a regular test function receives ([#&#8203;16347](https://redirect.github.com/jestjs/jest/pull/16347))
- `[jest-circus]` Capture the error listeners of the parent process instead of the in-sandbox `process`, so listeners registered before the test file survive teardown and sandbox listeners no longer leak onto the parent ([#&#8203;16347](https://redirect.github.com/jestjs/jest/pull/16347))
- `[jest-circus]` Clear `currentlyRunningTest` after skipped and todo tests ([#&#8203;16342](https://redirect.github.com/jestjs/jest/pull/16342))
- `[jest-circus]` Prevent late `done()` callbacks from affecting later test or hook invocations ([#&#8203;16343](https://redirect.github.com/jestjs/jest/pull/16343))
- `[jest-circus, jest-jasmine2]` Honor `--expand` when formatting `node:assert` failures, instead of always collapsing the diff ([#&#8203;16347](https://redirect.github.com/jestjs/jest/pull/16347))
- `[jest-circus, jest-jasmine2, jest-message-util]` Serialize the inner errors of an `AggregateError` into `failureMessages`, `retryReasons` and `unhandledErrors`, so `--json` output and reporter annotations include them ([#&#8203;16316](https://redirect.github.com/jestjs/jest/pull/16316))
- `[jest-circus, jest-snapshot]` Keep snapshot state and counts correct when a test retries ([#&#8203;16344](https://redirect.github.com/jestjs/jest/pull/16344))
- `[@jest/create-cache-key-function]` Include the caller support flags in the generated key, so a transformer that emits ESM or CJS based on them no longer shares one cache entry between the two ([#&#8203;16331](https://redirect.github.com/jestjs/jest/pull/16331))
- `[@jest/create-cache-key-function]` Include the stringified project config in the generated key, so editing a transformer's own settings invalidates what it cached ([#&#8203;16331](https://redirect.github.com/jestjs/jest/pull/16331))
- `[@jest/transform]` Include the caller support flags in a transform's cache key, so a file transformed both as ESM and as CJS no longer serves one shape's output for the other ([#&#8203;16331](https://redirect.github.com/jestjs/jest/pull/16331))
- `[jest-config]` Add missing `findRelatedTests`, `outputFile`, and `replname` entries to `ValidConfig` so they no longer trigger spurious "Unknown option" warnings ([#&#8203;16224](https://redirect.github.com/jestjs/jest/pull/16224))
- `[jest-config]` Use `--config` for the global config when multiple `--projects` are specified ([#&#8203;16273](https://redirect.github.com/jestjs/jest/pull/16273))
- `[jest-core]` Serialize `bigint` values in `--json` and `--outputFile` output as their literal form (`4n`), instead of failing the run with `TypeError: Do not know how to serialize a BigInt` ([#&#8203;16338](https://redirect.github.com/jestjs/jest/pull/16338))
- `[jest-core]` Do not report a `CustomGC` async resource (used by N-API addons such as napi-rs for per-isolate GC bookkeeping) as an open handle, since it is `napi_unref`'d by the addon and can never keep the event loop alive ([#&#8203;16379](https://redirect.github.com/jestjs/jest/pull/16379))
- `[jest-each]` Keep a `$&`, `` $` ``, `$'` or `$$` inside a `%p` param value out of the replacement, so the title shows the value instead of the text around it ([#&#8203;16338](https://redirect.github.com/jestjs/jest/pull/16338))
- `[jest-each]` Interpolate a `bigint` into a `%j` title as its literal form (`"4n"`) at any depth, instead of throwing `TypeError: Do not know how to serialize a BigInt` while collecting the tests ([#&#8203;16338](https://redirect.github.com/jestjs/jest/pull/16338))
- `[jest-environment, jest-runtime]` Bind `sandboxInjectedGlobals` to the right values when `injectGlobals` is `false`, instead of shifting every one of them by a position ([#&#8203;16377](https://redirect.github.com/jestjs/jest/pull/16377))
- `[jest-environment-node, jest-util]` Only warn about a conflicting `globalsCleanup` mode when one was explicitly configured, and follow the mode that is actually in effect ([#&#8203;16323](https://redirect.github.com/jestjs/jest/pull/16323))
- `[jest-environment-node, jest-util]` Stop resolving lazy globals when setting up an environment, so Node 26's builtin module globals are no longer loaded (and no longer emit their deprecation warnings) for every test file ([#&#8203;16324](https://redirect.github.com/jestjs/jest/pull/16324))
- `[jest-haste-map]` Keep watch mode alive when an outside process briefly makes a file unreadable on Windows, instead of tearing the watcher down on `EPERM` ([#&#8203;16295](https://redirect.github.com/jestjs/jest/pull/16295))
- `[jest-haste-map]` Keep indexing when an outside process holds a file open on Windows, instead of failing the whole crawl on `EPERM` ([#&#8203;16358](https://redirect.github.com/jestjs/jest/pull/16358))
- `[jest-haste-map]` Keep a duplicated manual mock resolving when the file it pointed at is deleted in watch mode ([#&#8203;16360](https://redirect.github.com/jestjs/jest/pull/16360))
- `[jest-haste-map]` Shut the worker farm down when a duplicate manual mock aborts the build under `throwOnModuleCollision` ([#&#8203;16354](https://redirect.github.com/jestjs/jest/pull/16354))
- `[jest-haste-map]` Attach the watchman client's `error` listener before the first command, so a watchman failure falls back to the node crawler instead of crashing on an unhandled `error` event, and always end the client ([#&#8203;16355](https://redirect.github.com/jestjs/jest/pull/16355))
- `[jest-haste-map]` Stop delivering watch events after `WatchmanWatcher` is closed, and route its warnings through the configured console ([#&#8203;16355](https://redirect.github.com/jestjs/jest/pull/16355))
- `[jest-haste-map]` Restore the nested `duplicates` index correctly in `ModuleMap.fromJSON`, so a haste collision reported inside a test worker raises `DuplicateHasteCandidatesError` instead of a `TypeError` ([#&#8203;16353](https://redirect.github.com/jestjs/jest/pull/16353))
- `[jest-haste-map]` Match watched files on a full extension, so `moduleFileExtensions: ['js']` no longer accepts `foo.mjs` ([#&#8203;16352](https://redirect.github.com/jestjs/jest/pull/16352))
- `[jest-haste-map]` Delimit the fields that make up the haste map cache key, so two different option sets cannot hash to the same cache file ([#&#8203;16352](https://redirect.github.com/jestjs/jest/pull/16352))
- `[jest-message-util]` Print the inner errors of an `AggregateError` thrown inside a test ([#&#8203;16316](https://redirect.github.com/jestjs/jest/pull/16316))
- `[jest-message-util]` Indent nested `cause` and `AggregateError` sections of a test failure by one level per depth, so the nesting is legible instead of rendering flat ([#&#8203;16316](https://redirect.github.com/jestjs/jest/pull/16316))
- `[jest-message-util]` Color stack traces line by line so blank lines stay blank ([#&#8203;16316](https://redirect.github.com/jestjs/jest/pull/16316))
- `[jest-message-util]` Detect Jest's own frames without assuming the checkout directory's name, and cover `@jest/*` packages, so stack traces and code frames point at user code ([#&#8203;16326](https://redirect.github.com/jestjs/jest/pull/16326))
- `[jest-mock]` `mockResolvedValue` / `mockRejectedValue` now see all overload return types, so a Promise-returning overload survives even when a later overload returns a non-Promise (e.g. `pg.Client['end']`) ([#&#8203;16237](https://redirect.github.com/jestjs/jest/pull/16237))
- `[@jest-environment/jsdom-abstract]` Make `@types/jsdom` a peer dependency ([#&#8203;16166](https://redirect.github.com/jestjs/jest/pull/16166))
- `[jest-mock]` Remove the leftover own accessor descriptor when restoring a `spyOn` of an inherited getter or setter, so the instance keeps reflecting the prototype ([#&#8203;16226](https://redirect.github.com/jestjs/jest/pull/16226))
- `[jest-resolve]` Include `extensionsToTreatAsEsm` in the `shouldLoadAsEsm` cache key, so projects with different extension lists don't read each other's answers ([#&#8203;16369](https://redirect.github.com/jestjs/jest/pull/16369))
- `[jest-resolve]` Make `getModuleIDAsync` build and cache `data:` URI module IDs the same way as `getModuleID` ([#&#8203;16370](https://redirect.github.com/jestjs/jest/pull/16370))
- `[jest-resolve]` Keep the `node:` prefix when resolving a core module asynchronously, so a builtin that only exists prefixed (`node:sea`, `node:sqlite`, `node:test`, `node:test/reporters`) resolves instead of failing as a missing bare package ([#&#8203;16388](https://redirect.github.com/jestjs/jest/pull/16388))
- `[jest-resolve]` Look up manual mocks for `node:` protocol specifiers under the unprefixed name they are stored as ([#&#8203;16388](https://redirect.github.com/jestjs/jest/pull/16388))
- `[jest-resolve]` Apply `moduleNameMapper` consistently to both spellings of core module specifiers (`fs` vs `node:fs`) ([#&#8203;16390](https://redirect.github.com/jestjs/jest/pull/16390))
- `[jest-resolve]` Keep virtual and ordinary mock module IDs isolated across test files ([#&#8203;16296](https://redirect.github.com/jestjs/jest/pull/16296))
- `[jest-resolve]` Guard missing `require.resolve.paths` ([#&#8203;16052](https://redirect.github.com/jestjs/jest/pull/16052))
- `[jest-resolve, jest-config, jest-runner]` Support a user resolver written as an ES module ([#&#8203;16332](https://redirect.github.com/jestjs/jest/pull/16332))
- `[jest-resolve, jest-runtime]` Throw the CJS parse error for ESM syntax in a `"type": "commonjs"` package or a `.cjs` file instead of loading it as ESM, matching Node ([#&#8203;16368](https://redirect.github.com/jestjs/jest/pull/16368))
- `[@jest/source-map]` Keep source map sources that name a scheme, such as `webpack:///`, instead of resolving them into a path that does not exist ([#&#8203;16327](https://redirect.github.com/jestjs/jest/pull/16327))
- `[@jest/source-map]` Look up `--testLocationInResults` positions at the right column, and keep a mapping to the first column instead of discarding it ([#&#8203;16327](https://redirect.github.com/jestjs/jest/pull/16327))
- `[@jest/source-map]` Warn when a source map cannot be parsed, instead of silently leaving its frames untranslated ([#&#8203;16327](https://redirect.github.com/jestjs/jest/pull/16327))
- `[jest-runner, @jest/source-map]` Keep a source-mapped stack for an error thrown after the test environment was torn down ([#&#8203;16327](https://redirect.github.com/jestjs/jest/pull/16327))
- `[jest-runtime, @jest/source-map]` Keep source maps past teardown and past the next test file's `install`, so a stack from a file no earlier stack mentioned still points at the original source ([#&#8203;16330](https://redirect.github.com/jestjs/jest/pull/16330))
- `[jest-runtime]` Report that no coverage was collected when `getAllV8CoverageInfoCopy` is called after `teardown`, instead of returning an empty result ([#&#8203;16385](https://redirect.github.com/jestjs/jest/pull/16385))
- `[jest-runtime]` Cache a CJS module's parsed exports before walking its re-exports, so two modules that re-export each other no longer overflow the stack when imported from ESM ([#&#8203;16363](https://redirect.github.com/jestjs/jest/pull/16363))
- `[jest-runtime]` Keep a re-exported ES module's parse failure from marking the re-exporting CommonJS file as ESM, so `module.exports = require('./dep.mjs')` loads instead of failing with `module is not defined` ([#&#8203;16363](https://redirect.github.com/jestjs/jest/pull/16363))
- `[jest-runtime]` Scope module mocks instantiated inside `jest.isolateModules`/`isolateModulesAsync` to that block, so a mock first imported there no longer outlives it - matching how CommonJS mocks already behave ([#&#8203;16365](https://redirect.github.com/jestjs/jest/pull/16365))
- `[jest-runtime]` Suspend module isolation while generating an automock, so loading the real module to read its shape no longer populates the isolated registry ([#&#8203;16365](https://redirect.github.com/jestjs/jest/pull/16365))
- `[jest-runtime]` Check a cached ES module's status before `require()` returns it, so a module whose evaluation threw rethrows that error and one left linked by a failed sibling is evaluated instead of returning uninitialized bindings ([#&#8203;16364](https://redirect.github.com/jestjs/jest/pull/16364))
- `[jest-runtime]` Report the original `ERR_REQUIRE_ASYNC_MODULE` when a `require()` of a top-level-await graph is retried, instead of a spurious "concurrent `import()`" error ([#&#8203;16364](https://redirect.github.com/jestjs/jest/pull/16364))
- `[jest-runtime]` Throw the evaluation error when another caller's `import()` of the same module failed while we awaited it, instead of resolving with the errored module ([#&#8203;16364](https://redirect.github.com/jestjs/jest/pull/16364))
- `[jest-runtime]` Mark the result of `require()`ing an ES module that has a default export with `__esModule: true` through a live-binding facade, and serve the same object from `require.cache`, matching Node ([#&#8203;16367](https://redirect.github.com/jestjs/jest/pull/16367))
- `[jest-runtime]` Provide a CommonJS module's exports under the `'module.exports'` named export when imported from ESM, matching Node 23+ ([#&#8203;16367](https://redirect.github.com/jestjs/jest/pull/16367))
- `[jest-runtime]` Give the test file itself a non-null `require.main` ([#&#8203;16367](https://redirect.github.com/jestjs/jest/pull/16367))
- `[jest-runtime]` Populate `module.children` with the modules a file loads, matching Node ([#&#8203;16368](https://redirect.github.com/jestjs/jest/pull/16368))
- `[jest-runtime]` Provide `import.meta.resolve` and `import.meta.jest` in `data:` URI modules, accept any-case mediatype parameters, and use Node's error codes for invalid `data:` URIs ([#&#8203;16368](https://redirect.github.com/jestjs/jest/pull/16368))
- `[jest-runtime]` Key ES modules by full URL, so query and fragment suffixes create the same module instances as Node and show up in `import.meta.url` ([#&#8203;16375](https://redirect.github.com/jestjs/jest/pull/16375))
- `[jest-runtime]` Share modules between overlapping graphs when a CommonJS module `require()`s an ES module mid-load, instead of evaluating shared dependencies twice ([#&#8203;16375](https://redirect.github.com/jestjs/jest/pull/16375))
- `[jest-runtime]` Throw `ERR_REQUIRE_CYCLE_MODULE` like Node when a CommonJS module `require()`s an ES module that is still being loaded, instead of evaluating the module a second time ([#&#8203;16366](https://redirect.github.com/jestjs/jest/pull/16366))
- `[jest-runtime]` Key builtin modules in the ESM registry by one canonical specifier ([#&#8203;16341](https://redirect.github.com/jestjs/jest/pull/16341))
- `[jest-runtime]` `import.meta.resolve()` for a builtin uses its `node:` specifier ([#&#8203;16341](https://redirect.github.com/jestjs/jest/pull/16341))
- `[jest-runtime]` Fall back to native ESM when a `.js` file contains ESM syntax but has no `"type":"module"` marker ([#&#8203;16152](https://redirect.github.com/jestjs/jest/pull/16152))
- `[jest-runtime]` Allow `require()` of ESM-marked files on Node < 24.9 via transform fallback ([#&#8203;16244](https://redirect.github.com/jestjs/jest/pull/16244))
- `[jest-runtime, @jest/transform]` Surface actionable `ERR_REQUIRE_ESM` error for files with untransformed ESM syntax instead of the generic "unexpected token" message ([#&#8203;16244](https://redirect.github.com/jestjs/jest/pull/16244))
- `[jest-runtime]` Support older test environments whose `moduleMocker` does not implement `clearMocksOnScope` ([#&#8203;16169](https://redirect.github.com/jestjs/jest/pull/16169))
- `[jest-runtime]` Apply `jest.unstable_mockModule` when the mocked file itself is `require()`d, not only when it is imported as a dependency ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Apply `jest.unstable_mockModule` to statically imported `data:` URIs on Node 24.9+, matching dynamic `import()` ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Run an async `jest.unstable_mockModule` factory once per module instead of twice, and fail the import instead of crashing the worker when the factory rejects ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Hide a `require(esm)` module that failed to evaluate from `require.cache`, as Node does, instead of exposing a namespace with uninitialized bindings ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Strip the byte-order mark when importing a JSON module, matching `require()` and Node ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Throw `ERR_REQUIRE_ASYNC_MODULE` when `require(esm)` runs under an async-only custom resolver, instead of silently resolving with the default resolver ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Parse imported JSON modules with the test realm's `JSON`, so their objects pass `instanceof Object` inside the test like `require()`d JSON does ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Accept every `file:` URL string in the sandboxed `module.createRequire`, including one with a `localhost` authority, as Node does ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-runtime]` Point at `{virtual: true}` when `jest.mock` or `jest.unstable_mockModule` is given a module that cannot be resolved ([#&#8203;16389](https://redirect.github.com/jestjs/jest/pull/16389))
- `[jest-reporters]` Fix coverage report table formatting in CI/GitHub Actions environments where `process.stdout.columns` is undefined by falling back to the `COLUMNS` env var or `80` columns in CI, preserving existing behaviour in other non-TTY environments ([#&#8203;16227](https://redirect.github.com/jestjs/jest/pull/16227))
- `[jest-runtime]` Support CJS-in-ESM exports via `"module.exports"` named exports ([#&#8203;16277](https://redirect.github.com/jestjs/jest/pull/16277))
- `[jest-snapshot]` Keep a skipped or failed test's hinted snapshots, instead of reporting them obsolete ([#&#8203;16348](https://redirect.github.com/jestjs/jest/pull/16348))
- `[jest-util]` Stop `globsToMatcher` reusing a cached matcher compiled with different picomatch options, and keep its `dot: true` default when `dot` is passed as `undefined` ([#&#8203;16381](https://redirect.github.com/jestjs/jest/pull/16381))
- `[pretty-format]` Move the `react-is` aliases into the `@jest` scope, so they cannot be shadowed by unrelated packages published under the alias names ([#&#8203;16333](https://redirect.github.com/jestjs/jest/pull/16333))

##### Chore & Maintenance

- `[docs]` Document the intentional divergences from Node's module system in the ECMAScript Modules page ([#&#8203;16368](https://redirect.github.com/jestjs/jest/pull/16368))
- `[docs]` Note deprecation of `react-test-renderer` in React Native tutorial and `pretty-format` README ([#&#8203;16294](https://redirect.github.com/jestjs/jest/pull/16294))
- `[docs]` Use `@testing-library/react-native` in the React Native tutorial instead of the deprecated `react-test-renderer` ([#&#8203;16318](https://redirect.github.com/jestjs/jest/pull/16318))
- `[babel-jest, @jest/transform]` Update `babel-plugin-istanbul` to v8 ([#&#8203;16049](https://redirect.github.com/jestjs/jest/pull/16049))
- `[jest-config, @jest/reporters, jest-runtime]` Update `glob` to v13 ([#&#8203;16397](https://redirect.github.com/jestjs/jest/pull/16397))
- `[jest-haste-map]` Refactor massive class into multiple files ([#&#8203;16180](https://redirect.github.com/jestjs/jest/pull/16180))
- `[jest-haste-map]` Drop `walker` dependency; replace hand-rolled directory recursion in the JS crawler and watcher startup with `fdir` ([#&#8203;16187](https://redirect.github.com/jestjs/jest/pull/16187))
- `[jest-haste-map]` Reuse cached metadata for files whose haste name is a known duplicate, instead of re-reading and re-parsing them on every startup ([#&#8203;16351](https://redirect.github.com/jestjs/jest/pull/16351))
- `[jest-haste-map]` Cache the watchman socket path and replace the `watchman --version` probe with `get-sockname`, so warm runs spawn no watchman processes ([#&#8203;16386](https://redirect.github.com/jestjs/jest/pull/16386))
- `[jest-resolve]` Store the per-directory package-type lookup in the cache it reads, so it actually memoizes ([#&#8203;16369](https://redirect.github.com/jestjs/jest/pull/16369))
- `[jest-resolve, jest-runtime]` Cut repeated work on the resolution hot path: hoist the platform-extension list to construction, memoize `isCoreModule` and the options cache-key serialization, skip mapper preparation when no `moduleNameMapper` is configured, run each mapper regex once, and stop re-parsing `NODE_OPTIONS` on every default-resolver call ([#&#8203;16371](https://redirect.github.com/jestjs/jest/pull/16371))
- `[jest-resolve]` Cut warm resolution cost to about a third: reuse one `unrs-resolver` factory per options shape instead of cloning per resolution, compose the factory cache key from per-array cached strings instead of serializing options, and stop constructing an `Error` for misses that `findNodeModule` swallows; add a `__benchmarks__` suite for the default resolver ([#&#8203;16373](https://redirect.github.com/jestjs/jest/pull/16373))
- `[jest-runner, @jest/source-map]` Replace `source-map-support` with an implementation in `@jest/source-map` ([#&#8203;16327](https://redirect.github.com/jestjs/jest/pull/16327))
- `[jest-snapshot]` Load babel, semver and synckit lazily, so requiring the package (which every test process does through `@jest/expect`) no longer loads \~200 modules that only writing inline snapshots needs ([#&#8203;16387](https://redirect.github.com/jestjs/jest/pull/16387))
- `[jest-runtime]` Reduce per-require overhead: skip module ID resolution when no mock can apply, answer core modules before probing for a manual mock, share one `require.cache` proxy across modules, and cache empty files ([#&#8203;16376](https://redirect.github.com/jestjs/jest/pull/16376))
- `[@jest/source-map]` Deprecate `getCallsite` in favour of `SourceMapSupport#getCallsite` ([#&#8203;16327](https://redirect.github.com/jestjs/jest/pull/16327))
- `[jest-runtime]` Avoid magical `null` value in ESM loader ([#&#8203;16160](https://redirect.github.com/jestjs/jest/pull/16160))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42MS4zIiwidXBkYXRlZEluVmVyIjoiNDQuNjEuMyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-08 08:17:22 -07:00
renovate[bot] 8557eff8db chore(deps): update lint (#694)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [oxfmt](https://oxc.rs/docs/guide/usage/formatter) ([source](https://redirect.github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt)) | [`0.63.0` → `0.66.0`](https://renovatebot.com/diffs/npm/oxfmt/0.63.0/0.66.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/oxfmt/0.66.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxfmt/0.63.0/0.66.0?slim=true) |
| [oxlint](https://oxc.rs/docs/guide/usage/linter) ([source](https://redirect.github.com/oxc-project/oxc/tree/HEAD/npm/oxlint)) | [`1.78.0` → `1.81.0`](https://renovatebot.com/diffs/npm/oxlint/1.78.0/1.81.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint/1.81.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint/1.78.0/1.81.0?slim=true) |
| [oxlint-config-universe](https://redirect.github.com/expo/oxlint-config-universe) | [`0.0.3` → `0.2.0`](https://renovatebot.com/diffs/npm/oxlint-config-universe/0.0.3/0.2.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint-config-universe/0.2.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint-config-universe/0.0.3/0.2.0?slim=true) |

---

### Release Notes

<details>
<summary>oxc-project/oxc (oxfmt)</summary>

### [`v0.66.0`](https://redirect.github.com/oxc-project/oxc/compare/oxfmt_v0.65.0...oxfmt_v0.66.0)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxfmt_v0.65.0...oxfmt_v0.66.0)

### [`v0.65.0`](https://redirect.github.com/oxc-project/oxc/compare/oxfmt_v0.64.0...oxfmt_v0.65.0)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxfmt_v0.64.0...oxfmt_v0.65.0)

### [`v0.64.0`](https://redirect.github.com/oxc-project/oxc/blob/HEAD/npm/oxfmt/CHANGELOG.md#0640---2026-08-18)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxfmt_v0.63.0...oxfmt_v0.64.0)

##### 🚀 Features

- [`c07fe7c`](https://redirect.github.com/oxc-project/oxc/commit/c07fe7c) oxfmt: Support `experimentalOperatorPosition` ([#&#8203;25643](https://redirect.github.com/oxc-project/oxc/issues/25643)) (leaysgur)

##### 📚 Documentation

- [`fed6681`](https://redirect.github.com/oxc-project/oxc/commit/fed6681) oxfmt: Skip expanding overrides options ([#&#8203;25572](https://redirect.github.com/oxc-project/oxc/issues/25572)) (leaysgur)

</details>

<details>
<summary>oxc-project/oxc (oxlint)</summary>

### [`v1.81.0`](https://redirect.github.com/oxc-project/oxc/blob/HEAD/npm/oxlint/CHANGELOG.md#1810---2026-08-31)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v1.80.0...oxlint_v1.81.0)

##### 📚 Documentation

- [`d5be037`](https://redirect.github.com/oxc-project/oxc/commit/d5be037) linter/typescript/switch-exhaustiveness-check: Clarify default case comment pattern ([#&#8203;26100](https://redirect.github.com/oxc-project/oxc/issues/26100)) (camc314)

### [`v1.80.0`](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v1.79.0...oxlint_v1.80.0)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v1.79.0...oxlint_v1.80.0)

### [`v1.79.0`](https://redirect.github.com/oxc-project/oxc/blob/HEAD/npm/oxlint/CHANGELOG.md#1790---2026-08-18)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v1.78.0...oxlint_v1.79.0)

##### 💥 BREAKING CHANGES

- [`8c4552d`](https://redirect.github.com/oxc-project/oxc/commit/8c4552d) linter: \[**BREAKING**] Split react/react-compiler into per-category rules ([#&#8203;25500](https://redirect.github.com/oxc-project/oxc/issues/25500)) (Boshen)

##### 🐛 Bug Fixes

- [`228e8e0`](https://redirect.github.com/oxc-project/oxc/commit/228e8e0) linter: Resolve inactive React compiler rules ([#&#8203;25830](https://redirect.github.com/oxc-project/oxc/issues/25830)) (Boshen)
- [`aa49d86`](https://redirect.github.com/oxc-project/oxc/commit/aa49d86) linter: Allow spread rule options in config types ([#&#8203;25675](https://redirect.github.com/oxc-project/oxc/issues/25675)) (ch3rry)
- [`36f8451`](https://redirect.github.com/oxc-project/oxc/commit/36f8451) linter/eslint/no-eval: Align indirect default with ESLint ([#&#8203;25656](https://redirect.github.com/oxc-project/oxc/issues/25656)) (camc314)
- [`beb724d`](https://redirect.github.com/oxc-project/oxc/commit/beb724d) linter/eslint/no-unused-vars: Report bare underscore parameters ([#&#8203;25663](https://redirect.github.com/oxc-project/oxc/issues/25663)) (camc314)
- [`4004c10`](https://redirect.github.com/oxc-project/oxc/commit/4004c10) linter/eslint/no-irregular-whitespace: Check comments by default ([#&#8203;25660](https://redirect.github.com/oxc-project/oxc/issues/25660)) (camc314)
- [`285820e`](https://redirect.github.com/oxc-project/oxc/commit/285820e) linter/no-large-snapshots: Precompile and document allowed snapshot matchers ([#&#8203;25611](https://redirect.github.com/oxc-project/oxc/issues/25611)) (Mikhail Baev)
- [`4df5835`](https://redirect.github.com/oxc-project/oxc/commit/4df5835) linter: Allow capitalized built-in calls ([#&#8203;25516](https://redirect.github.com/oxc-project/oxc/issues/25516)) (Boshen)

</details>

<details>
<summary>expo/oxlint-config-universe (oxlint-config-universe)</summary>

### [`v0.2.0`](https://redirect.github.com/expo/oxlint-config-universe/releases/tag/v0.2.0)

[Compare Source](https://redirect.github.com/expo/oxlint-config-universe/compare/a082d4f808eaea495b5f62b5bad4342d69de7442...v0.2.0)

#### What's Changed

- fix: fill in some documented gaps with rules that exist by [@&#8203;wschurman](https://redirect.github.com/wschurman) in [#&#8203;1](https://redirect.github.com/expo/oxlint-config-universe/pull/1)
- chore: explicitly enable some rules that are on by default by [@&#8203;wschurman](https://redirect.github.com/wschurman) in [#&#8203;2](https://redirect.github.com/expo/oxlint-config-universe/pull/2)
- fix: re-enable no-control-regex by [@&#8203;wschurman](https://redirect.github.com/wschurman) in [#&#8203;3](https://redirect.github.com/expo/oxlint-config-universe/pull/3)
- chore: explicitly enable more rules that are on by default by [@&#8203;wschurman](https://redirect.github.com/wschurman) in [#&#8203;4](https://redirect.github.com/expo/oxlint-config-universe/pull/4)
- chore: bump version, remove manual changelog for now by [@&#8203;wschurman](https://redirect.github.com/wschurman) in [#&#8203;5](https://redirect.github.com/expo/oxlint-config-universe/pull/5)
- fix: support oxlint 1.79 by [@&#8203;hirbod](https://redirect.github.com/hirbod) in [#&#8203;6](https://redirect.github.com/expo/oxlint-config-universe/pull/6)
- chore: modernize tooling — Yarn 4, generated types, npm trusted publishing by [@&#8203;hirbod](https://redirect.github.com/hirbod) in [#&#8203;7](https://redirect.github.com/expo/oxlint-config-universe/pull/7)
- fix: push version commit with bot PAT in publish workflow by [@&#8203;hirbod](https://redirect.github.com/hirbod) in [#&#8203;8](https://redirect.github.com/expo/oxlint-config-universe/pull/8)
- chore: drop production environment from publish workflow by [@&#8203;hirbod](https://redirect.github.com/hirbod) in [#&#8203;9](https://redirect.github.com/expo/oxlint-config-universe/pull/9)

#### New Contributors

- [@&#8203;wschurman](https://redirect.github.com/wschurman) made their first contribution in [#&#8203;1](https://redirect.github.com/expo/oxlint-config-universe/pull/1)
- [@&#8203;hirbod](https://redirect.github.com/hirbod) made their first contribution in [#&#8203;6](https://redirect.github.com/expo/oxlint-config-universe/pull/6)

**Full Changelog**: <https://github.com/expo/oxlint-config-universe/commits/v0.2.0>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42MS4zIiwidXBkYXRlZEluVmVyIjoiNDQuNjEuMyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-08 08:17:12 -07:00
renovate[bot] ba3ba9ea89 chore(deps): update dependency jdx/mise to v2026.9.2 (#692)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [jdx/mise](https://redirect.github.com/jdx/mise) | minor | `v2026.8.15` → `v2026.9.2` |

---

### Release Notes

<details>
<summary>jdx/mise (jdx/mise)</summary>

### [`v2026.9.2`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.2): : Packslip Backend, SSH Relay, and Reimagined Install Progress

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.9.1...vfox-v2026.9.2)

This is a large release headlined by the new stable `packslip:` backend for installing tools from a vendor's own signed release manifest, a read-only GitHub SSH relay for remote onboarding, redesigned install progress for both terminals and CI logs, and a rebuilt dotfiles-tracking model for bootstrap. It also carries dozens of fixes across shims, tasks, brew, Go, npm, and the schema, plus two security fixes.

##### Highlights

- The `packslip:` backend is now generally available: install tools directly from a project's cryptographically signed release manifest, with signer pinning, trusted stampers, host-requirement checks, shell completions, and agent skills all driven from the same signed source.
- Install output was reimagined end to end. Interactive terminals get a live region with a fractional install-wide bar and permanent completion lines, while CI logs, pipes, and AI-agent terminals get a compact append-only reporter instead of thousands of scrolling rows.
- Bootstrap grows a read-only GitHub SSH relay and `--from-git` onboarding, an AUR package manager, and a rebuilt dotfiles-history model that tracks files through ordinary Git commits synchronized with an origin.

##### Added

- **packslip:** New `packslip:` backend installs tools from a vendor-published, signed release manifest (a sigstore bundle) that names every artifact with its digest, platform, format, and executables. The tool name is a pin, like a `known_hosts` entry: `packslip:github.com/owner/repo` accepts only a packslip signed by that repository's release workflow, and signature, log entry, statement, digest, and size are all verified before anything is unpacked. Custom hosts and monorepo tools are supported. The backend is no longer experimental. ([#&#8203;12778](https://redirect.github.com/jdx/mise/pull/12778), [#&#8203;12811](https://redirect.github.com/jdx/mise/pull/12811) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [tools]
  "packslip:github.com/oxc-project/oxc/oxlint" = "latest"
  ```
- **packslip:** Signer pinning remembers, per project, the signer that signed the first accepted release in a `pins.toml` file (like SSH's known\_hosts) and records the signer in `mise.lock`, so a later release signed by anyone else, or one that weakens provenance, is refused. `mise packslip pins` lists what is pinned and `mise packslip forget <project>` accepts an announced key rotation. ([#&#8203;12783](https://redirect.github.com/jdx/mise/pull/12783) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **packslip:** New `packslip.stampers` setting lets you require that a trusted host has stamped a version before mise offers or installs it (a scanning service, a mirror, or eventually the registry), with a per-tool `trust = "vendor"` override. ([#&#8203;12782](https://redirect.github.com/jdx/mise/pull/12782) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **packslip:** Declared host requirements (OS/glibc minimums, shared libraries, required commands) are checked before download; confirmed failures refuse the install, gaps mise cannot verify warn only, and `ignore_requirements = true` overrides hard failures. `latest` also resolves from verified vendor recommendations. ([#&#8203;12804](https://redirect.github.com/jdx/mise/pull/12804), [#&#8203;12805](https://redirect.github.com/jdx/mise/pull/12805) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **packslip:** A tool's packslip can ship shell completions and agent skills. `mise completion <shell> --tool <name>` prints (or `--install` writes a version-aware stub for) completions for whichever version is active in the current directory, and `mise skills ls` / `mise skills sync` link a tool's Agent Skills into `.claude/skills` at the pinned version. Completions are also loaded automatically in activated shells. ([#&#8203;12779](https://redirect.github.com/jdx/mise/pull/12779), [#&#8203;12780](https://redirect.github.com/jdx/mise/pull/12780), [#&#8203;12848](https://redirect.github.com/jdx/mise/pull/12848) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **install:** Redesigned install progress. Interactive terminals now show a live region with an install-wide fractional bar, per-tool phase/elapsed/artifact rows, dependency-wait rows ("waiting for node\@&#8203;24.20.0"), and permanent completion lines written into scrollback. `prune`, `uninstall`, and upgrade removals reuse the same session, so pruning hundreds of versions no longer floods the screen. ([#&#8203;12906](https://redirect.github.com/jdx/mise/pull/12906) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **install:** CI logs, pipes, and AI-agent terminals (`CLAUDECODE`/`AI_AGENT`) get a compact append-only reporter: one permanent line per finished tool plus a periodic snapshot of the bar, active phases, transfer rates, and queue count, instead of hundreds of scrolling status rows. Resolver hosts and retry progress are surfaced too. ([#&#8203;12902](https://redirect.github.com/jdx/mise/pull/12902), [#&#8203;12907](https://redirect.github.com/jdx/mise/pull/12907) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **ssh:** New `mise ssh` runs ordinary OpenSSH sessions with optional session-scoped, read-only GitHub access, and `mise bootstrap remote --from-git` onboards a remote host from a Git repo. The initiating machine keeps the credential and brokers authorized clone/fetch and REST reads over an SSH Unix-socket forward; no token is installed on the target. ([#&#8203;12830](https://redirect.github.com/jdx/mise/pull/12830) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  mise ssh devbox --github-relay-read-only --github-relay-repo jdx/dotfiles
  ```
- **bootstrap:** `mise bootstrap --from-git <GIT_URL>` clones a repository-backed global config into `$MISE_CONFIG_DIR` so its `config.toml`, `conf.d/`, and tasks apply on the first bootstrap and stay active afterward. ([#&#8203;12715](https://redirect.github.com/jdx/mise/pull/12715) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** New `aur:` package manager installs from the Arch User Repository via `yay` (preferred) or `paru`, with foreign-package status detection so repo packages with colliding names cannot satisfy an AUR request. Bootstrap can also declare packages as absent to remove them, and now reports which root declarations were selected. ([#&#8203;12718](https://redirect.github.com/jdx/mise/pull/12718), [#&#8203;12716](https://redirect.github.com/jdx/mise/pull/12716), [#&#8203;12770](https://redirect.github.com/jdx/mise/pull/12770) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Rebuilt dotfiles tracking. Files enrolled with `mode = "track"` stay in place while mise commits changes to a separate bare Git store with checkpoints, rollback/undo, and optional origin synchronization, including encryption of shared files before storage. ([#&#8203;12918](https://redirect.github.com/jdx/mise/pull/12918) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **activate:** New `activate_shims = false` (`MISE_ACTIVATE_SHIMS=false`) keeps tool shim directories off PATH during activation and hooks without changing your auto-install or lazy-tool settings. Command wrappers such as mr-boxington's `cargo` keep working. ([#&#8203;12926](https://redirect.github.com/jdx/mise/pull/12926) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **core:** Rust tools accept `mr_boxington = true` and `mise use` gains a repeatable `--tool-option KEY=VALUE` flag, so setting up the Cargo wrapper is a single command. ([#&#8203;12908](https://redirect.github.com/jdx/mise/pull/12908) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  mise use --tool-option mr_boxington=true rust mr-boxington
  ```
- **ruby:** mise now reads Bundler's `ruby file: ".ruby-version"` form from a `Gemfile` (resolving the path next to the Gemfile), so projects that pin Ruby through a sibling file resolve correctly. ([#&#8203;12914](https://redirect.github.com/jdx/mise/pull/12914) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **self-update:** New global-only `self_update.repository` and `self_update.api_url` settings let organizations point manual updates, out-of-date hints, and automatic updates at a curated GitHub or GitHub Enterprise release mirror. Project config cannot redirect updates, and artifacts still pass the embedded-signature check. ([#&#8203;12735](https://redirect.github.com/jdx/mise/pull/12735) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** `install_env` is now applied when resolving and downloading tools, not just at build time. ([#&#8203;12777](https://redirect.github.com/jdx/mise/pull/12777) by [@&#8203;rabadin](https://redirect.github.com/rabadin))
- **brew:** mise can now evaluate ordinary third-party taps. ([#&#8203;12774](https://redirect.github.com/jdx/mise/pull/12774) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Fixed

- **shim:** Lazy tool names now match on Windows, and `mise-shim.exe` resolves correctly through a symlinked `mise.exe`. ([#&#8203;12699](https://redirect.github.com/jdx/mise/pull/12699) by [@&#8203;jdx](https://redirect.github.com/jdx), [#&#8203;12915](https://redirect.github.com/jdx/mise/pull/12915) by [@&#8203;acooler15](https://redirect.github.com/acooler15))
- **activate:** Paths are translated for the calling Windows POSIX shell, and PowerShell pipeline input is now forwarded. ([#&#8203;12736](https://redirect.github.com/jdx/mise/pull/12736), [#&#8203;12925](https://redirect.github.com/jdx/mise/pull/12925) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **nushell:** The activation prelude preserves the PATH list type. ([#&#8203;12754](https://redirect.github.com/jdx/mise/pull/12754) by [@&#8203;Junaid-PK](https://redirect.github.com/Junaid-PK))
- **task:** Task globs no longer loop on symlinks, `sources` accepts a single string, task status is preserved when the cache audit tracer fails, and POSIX shells no longer have PATH pre-converted (the shell already does it). ([#&#8203;12711](https://redirect.github.com/jdx/mise/pull/12711), [#&#8203;12769](https://redirect.github.com/jdx/mise/pull/12769) by [@&#8203;jdx](https://redirect.github.com/jdx), [#&#8203;12530](https://redirect.github.com/jdx/mise/pull/12530) by [@&#8203;risu729](https://redirect.github.com/risu729), [#&#8203;12696](https://redirect.github.com/jdx/mise/pull/12696) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **npm:** Global installs now work with pnpm 12. ([#&#8203;12717](https://redirect.github.com/jdx/mise/pull/12717) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **go:** Private modules resolve via Go, and loong64 uses the correct artifact architecture. ([#&#8203;12731](https://redirect.github.com/jdx/mise/pull/12731) by [@&#8203;kejne](https://redirect.github.com/kejne), [#&#8203;12767](https://redirect.github.com/jdx/mise/pull/12767) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew:** A tapped install now explains why it found no API metadata, and all tap formula layouts are supported. brew-cask packages with `auto_updates` enabled are upgraded correctly. ([#&#8203;12645](https://redirect.github.com/jdx/mise/pull/12645) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743), [#&#8203;12837](https://redirect.github.com/jdx/mise/pull/12837) by [@&#8203;soodoh](https://redirect.github.com/soodoh), [#&#8203;12857](https://redirect.github.com/jdx/mise/pull/12857) by [@&#8203;himkt](https://redirect.github.com/himkt))
- **github:** Releases with no assets are omitted from the version list, and OAuth-refresh warnings are silenced during env export and shown only once when a refresh is rejected. ([#&#8203;12572](https://redirect.github.com/jdx/mise/pull/12572) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743), [#&#8203;12855](https://redirect.github.com/jdx/mise/pull/12855), [#&#8203;12911](https://redirect.github.com/jdx/mise/pull/12911) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **config:** Every newly added `.tool-versions` line is now terminated with a newline. ([#&#8203;12740](https://redirect.github.com/jdx/mise/pull/12740) by [@&#8203;dylanpulver](https://redirect.github.com/dylanpulver))
- **env:** Bootstrap shims for lazy tools are created when printing the env, and duplicate unset directives are removed from hook output. ([#&#8203;12726](https://redirect.github.com/jdx/mise/pull/12726) by [@&#8203;hktitof](https://redirect.github.com/hktitof), [#&#8203;12707](https://redirect.github.com/jdx/mise/pull/12707) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **http:** A failed download's partial file is flushed before giving up, and cache compression finishes before files are published. ([#&#8203;12823](https://redirect.github.com/jdx/mise/pull/12823) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743), [#&#8203;12894](https://redirect.github.com/jdx/mise/pull/12894) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Tera templates render in hooks, tildes expand in managed file sources, and the relative config dir is pinned during reexec. ([#&#8203;12727](https://redirect.github.com/jdx/mise/pull/12727), [#&#8203;12768](https://redirect.github.com/jdx/mise/pull/12768), [#&#8203;12721](https://redirect.github.com/jdx/mise/pull/12721) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** Disabled plugins are excluded from shorthand resolution, and installed plugins are included in plugin completion. ([#&#8203;12927](https://redirect.github.com/jdx/mise/pull/12927), [#&#8203;12720](https://redirect.github.com/jdx/mise/pull/12720) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **rust:** Incomplete rustup toolchains are reconciled. ([#&#8203;12771](https://redirect.github.com/jdx/mise/pull/12771) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **oci/vfox/aqua:** OCI reused tool layers preserve their paths, vfox preserves the tool name from legacy lockfiles, and aqua handles repository transfers in attestations. ([#&#8203;12749](https://redirect.github.com/jdx/mise/pull/12749), [#&#8203;12745](https://redirect.github.com/jdx/mise/pull/12745), [#&#8203;12766](https://redirect.github.com/jdx/mise/pull/12766) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **sandbox:** `SYS_fork`/`SYS_vfork` are omitted on aarch64. ([#&#8203;12807](https://redirect.github.com/jdx/mise/pull/12807) by [@&#8203;jamescassell](https://redirect.github.com/jamescassell))
- **dotfiles:** A stale directory the deeper walk already removed is tolerated. ([#&#8203;12697](https://redirect.github.com/jdx/mise/pull/12697) by [@&#8203;vladbisceanu](https://redirect.github.com/vladbisceanu))
- **schema:** Absent bootstrap packages, inline dotfile content, and missing built-in deps providers are now modeled. ([#&#8203;12785](https://redirect.github.com/jdx/mise/pull/12785) by [@&#8203;nettlesh](https://redirect.github.com/nettlesh), [#&#8203;12738](https://redirect.github.com/jdx/mise/pull/12738), [#&#8203;12739](https://redirect.github.com/jdx/mise/pull/12739) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **lockfile:** The deferred provenance baseline is dropped once auto-lock verifies the upgrade. ([#&#8203;12825](https://redirect.github.com/jdx/mise/pull/12825) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Security

- **http:** URL replacements no longer forward authorization headers or URL credentials when an HTTPS origin is rewritten to HTTP; such credential-carrying downgrades are refused, and errors omit URLs so embedded credentials cannot leak through diagnostics. ([#&#8203;12879](https://redirect.github.com/jdx/mise/pull/12879) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **self-update:** Self-update now requires secure release sources. ([#&#8203;12737](https://redirect.github.com/jdx/mise/pull/12737) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### New Contributors

- [@&#8203;acooler15](https://redirect.github.com/acooler15) made their first contribution in [#&#8203;12915](https://redirect.github.com/jdx/mise/pull/12915)
- [@&#8203;soodoh](https://redirect.github.com/soodoh) made their first contribution in [#&#8203;12837](https://redirect.github.com/jdx/mise/pull/12837)
- [@&#8203;jamescassell](https://redirect.github.com/jamescassell) made their first contribution in [#&#8203;12807](https://redirect.github.com/jdx/mise/pull/12807)
- [@&#8203;nettlesh](https://redirect.github.com/nettlesh) made their first contribution in [#&#8203;12785](https://redirect.github.com/jdx/mise/pull/12785)
- [@&#8203;Junaid-PK](https://redirect.github.com/Junaid-PK) made their first contribution in [#&#8203;12754](https://redirect.github.com/jdx/mise/pull/12754)
- [@&#8203;kejne](https://redirect.github.com/kejne) made their first contribution in [#&#8203;12731](https://redirect.github.com/jdx/mise/pull/12731)
- [@&#8203;dylanpulver](https://redirect.github.com/dylanpulver) made their first contribution in [#&#8203;12740](https://redirect.github.com/jdx/mise/pull/12740)
- [@&#8203;Svector-anu](https://redirect.github.com/Svector-anu) made their first contribution in [#&#8203;12714](https://redirect.github.com/jdx/mise/pull/12714)
- [@&#8203;vladbisceanu](https://redirect.github.com/vladbisceanu) made their first contribution in [#&#8203;12697](https://redirect.github.com/jdx/mise/pull/12697)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.1...v2026.9.2>

##### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

### [`v2026.9.1`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.1): : Bootstrap firewall limiting, scoped locked mode, and lazy tools in tasks

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.9.0...vfox-v2026.9.1)

This release expands bootstrap system setup with firewall rate limiting and more systemd directives, adds a scoped locked mode for mixing distribution and user tool policies, and fixes lazy tools so they install when invoked from tasks and `mise x`.

#### Added

- **bootstrap:** New `action = "limit"` for incoming TCP firewall rules rate-limits connections per source. It maps to UFW's native limiting and bounded per-source IPv4/IPv6 nftables meters; firewalld fails closed since it cannot express safe per-source limiting. Limit rules are treated as preserving SSH access by the lockout guard. ([#&#8203;12669](https://redirect.github.com/jdx/mise/pull/12669) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Bootstrap systemd units now support `requires` (`Requires=`), repeatable `environment_file` (`EnvironmentFile=`), `nice`, and `umask`, with validation for nice ranges (-20 to 19) and octal umasks. Note that `requires` does not imply ordering; use `after` for that. ([#&#8203;12683](https://redirect.github.com/jdx/mise/pull/12683) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **config:** New global-only `locked_scopes` setting (also `MISE_LOCKED_SCOPES`) lets you scope locked mode to specific config sources, so a distribution can ship rolling or lazy tools through `/etc/mise/config.toml` without forcing users to maintain a system lockfile. Defaults to all three scopes to preserve existing behavior. ([#&#8203;12667](https://redirect.github.com/jdx/mise/pull/12667) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [settings]
  locked_scopes = ["project", "global"]
  ```

#### Fixed

- **task:** Lazy tools (`lazy = true`) now install when their command is invoked from a `mise run` task or `mise x`, matching the behavior of an activated shell. mise inserts the shim farms after real tool paths for lazy toolsets and reconciles missing bootstrap shims for hand-edited declarations, so tasks no longer fail with `command: not found`. ([#&#8203;12687](https://redirect.github.com/jdx/mise/pull/12687) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **env:** Environment variables configured for removal (`false` values and unset directives) are now consistently unset across `mise exec`, tasks, `mise env`, hook-env, deps, and tool stubs, so inherited or secret variables are no longer leaked back into child processes. ([#&#8203;12664](https://redirect.github.com/jdx/mise/pull/12664) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **shim:** `mise reshim` can now safely target shared executable directories like `~/.local/bin` or `/usr/local/bin`. It identifies mise-owned shims and only replaces or prunes those, leaving unmanaged files and symlinks in place, and `--force` rebuilds mise-owned shims rather than wiping the whole directory. ([#&#8203;12675](https://redirect.github.com/jdx/mise/pull/12675) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew-cask:** Cask binaries that reference their surrounding extracted payload now keep that payload beside the staged binary, so tools like `codex` that exec helpers or read manifests from their own tree continue working after temporary install files are cleaned up. ([#&#8203;12686](https://redirect.github.com/jdx/mise/pull/12686) by [@&#8203;azohra](https://redirect.github.com/azohra))
- **upgrade:** Fixed recursive deferred tool pruning that could hang new terminals when prompt hooks triggered a full tracked-config scan. Deferred cleanup now takes a non-blocking lock and is skipped for shell-integration and `--no-config` commands. ([#&#8203;12676](https://redirect.github.com/jdx/mise/pull/12676) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **install:** Install failures caused by a missing marker file now name the affected file and operation instead of reporting a bare OS error. ([#&#8203;12624](https://redirect.github.com/jdx/mise/pull/12624) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **install:** `mise install --dry-run` for an s3 tool that only declares a URL for another platform now reports the missing platform and available options instead of falsely claiming it would install. ([#&#8203;12641](https://redirect.github.com/jdx/mise/pull/12641) by [@&#8203;hktitof](https://redirect.github.com/hktitof))

#### Changed

- **registry:** Registry command (shim) names are now inferred from the preferred Aqua package at build time, removing redundant `bins` declarations from hundreds of registry entries. Explicit `bins` are kept only where mise intentionally exposes a different command set, and `mise registry --json` exposes the inferred names. ([#&#8203;12668](https://redirect.github.com/jdx/mise/pull/12668) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### New Contributors

- [@&#8203;azohra](https://redirect.github.com/azohra) made their first contribution in [#&#8203;12686](https://redirect.github.com/jdx/mise/pull/12686)
- [@&#8203;hktitof](https://redirect.github.com/hktitof) made their first contribution in [#&#8203;12641](https://redirect.github.com/jdx/mise/pull/12641)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.9.0...v2026.9.1>

#### 💚 Sponsor mise

mise is built and maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer at [**entire.io**](https://entire.io/), the title sponsor of his open source work.

If mise saves you or your team time, please consider becoming an [individual or company sponsor](https://jdx.dev/sponsors.html). Your support funds ongoing development and helps keep mise fast, free, and independent.

### [`v2026.9.0`](https://redirect.github.com/jdx/mise/releases/tag/v2026.9.0): : Lazy Tool Shims

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.8.16...vfox-v2026.9.0)

This release introduces lazy tool shims, which install a tool automatically the first time one of its commands is invoked, plus an opt-in Erlang precompiled OS override and a fix for nested `mise` calls during postinstall hooks.

#### Added

- **tools:** Declare a tool as lazy in `[tools]` with `lazy = true` and mise generates bootstrap shims into its normal user/system shim farms. The provider is installed only when one of its commands is first called, then executes immediately; subsequent calls run the real binary without another mise dispatch. Registry tools derive their command names from registry `bins`, while explicit or non-registry backends declare them with `lazy_bins`. A bare `mise install` skips lazy declarations; use `mise install --include-lazy` to provision them all. New global path settings (`shims_dir`/`MISE_SHIMS_DIR`, `system_installs_dir`/`MISE_SYSTEM_INSTALLS_DIR`, `system_shims_dir`/`MISE_SYSTEM_SHIMS_DIR`) and `mise reshim --system` support system-scoped and collocated layouts. ([#&#8203;12594](https://redirect.github.com/jdx/mise/pull/12594) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [tools]
  node = { version = "24", lazy = true }
  "github:example/acme" = { version = "1.2.3", lazy = true, lazy_bins = ["acme", "acmectl"] }
  ```
- **erlang:** New `erlang.precompiled_os` setting (and `MISE_ERLANG_PRECOMPILED_OS`) opts into a specific Bob Ubuntu build target—including Ubuntu 26.04—on compatible glibc distributions. Automatic target detection is unchanged, and lockfiles now preserve the selected OS for reproducible builds. Note the portability constraints around shared-library SONAMEs documented with the setting. ([#&#8203;12637](https://redirect.github.com/jdx/mise/pull/12637) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Fixed

- **install:** Postinstall hooks now activate the exact tool version being installed, so nested `mise` invocations (such as `mise which` or `mise ls`) resolve that version instead of an older discoverable one—or losing the tool entirely when it was declared through a custom config path with `mise use --path`. ([#&#8203;12635](https://redirect.github.com/jdx/mise/pull/12635) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Registry

- Added `engram` ([#&#8203;12480](https://redirect.github.com/jdx/mise/pull/12480) by [@&#8203;AndryOre](https://redirect.github.com/AndryOre))
- Added `gentle-ai` ([aqua:Gentleman-Programming/gentle-ai](https://redirect.github.com/Gentleman-Programming/gentle-ai)) ([#&#8203;12444](https://redirect.github.com/jdx/mise/pull/12444) by [@&#8203;AndryOre](https://redirect.github.com/AndryOre))

#### New Contributors

- [@&#8203;AndryOre](https://redirect.github.com/AndryOre) made their first contribution in [#&#8203;12444](https://redirect.github.com/jdx/mise/pull/12444)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.8.16...v2026.9.0>

#### 💚 Sponsor mise

mise is maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer for [**entire.io**](https://entire.io), the title sponsor of the [jdx.dev](https://jdx.dev) open source tools. Development is funded by sponsors.

If mise saves you or your team time, please consider sponsoring at [jdx.dev](https://jdx.dev/sponsors.html). Individual and company sponsorships keep mise fast, free, and independent.

### [`v2026.8.16`](https://redirect.github.com/jdx/mise/releases/tag/v2026.8.16): : Command wrappers and Windows shell fixes

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.8.15...vfox-v2026.8.16)

This release adds configurable command wrappers for intercepting ordinary commands, alongside a batch of shell and PATH fixes that make Windows activation more reliable and clearer upgrade and error messaging.

#### Added

- **shims:** New `[wrappers]` config lets you intercept an ordinary command name with a different command, arguments, and environment. Wrappers take precedence over mise-managed tools, and mise strips its dispatch directories before delegating so the underlying tool still resolves from mise or the system. Works with normal activation, `activate --shims`, and `mise exec`; managed wrapper shims are refreshed by `mise reshim`. ([#&#8203;12617](https://redirect.github.com/jdx/mise/pull/12617) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [tools]
  mr-boxington = "1.1.0"

  [wrappers.cargo]
  command = "mbx"
  env = { MBX_CARGO_SHIM_MODE = "1" }
  ```

#### Fixed

- **elvish:** `mise env` and `mise activate --shims` now emit valid Elvish quoting instead of bash's. This fixes several corruptions that mostly surfaced on Windows: stray quotes and the wrong PATH separator in `activate --shims`, apostrophes and `!` arriving with backslashes, and `\n` sequences in paths like `C:\nodejs` being turned into newlines. ([#&#8203;12584](https://redirect.github.com/jdx/mise/pull/12584) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **fish:** `mise env -s fish` now splits PATH on the host's separator and recognizes the `Path` spelling on Windows, so semicolon-separated Windows paths are no longer severed at each drive letter. Empty PATH segments are dropped rather than adding the current directory. ([#&#8203;12582](https://redirect.github.com/jdx/mise/pull/12582) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **http:** The "No URL for platform" error now lists every platform key a tool actually declares in `mise.toml`, including typos and unrecognized names, instead of falling back to a misleading "requires 'url' option" message with an empty available list. ([#&#8203;12580](https://redirect.github.com/jdx/mise/pull/12580) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **upgrade:** When tools are current for their configured version ranges but newer releases exist beyond those ranges, `mise upgrade` no longer prints "All tools are up to date". It now lists the affected tools with their current and available versions plus the config source, and points to `mise upgrade --bump`. ([#&#8203;12613](https://redirect.github.com/jdx/mise/pull/12613) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```text
  Newer versions are available but do not match the configured version ranges:
    node 26.7.0 → 26.8.1 (~/.config/mise/config.toml)
  Run `mise upgrade --bump` to update the configuration and upgrade.
  ```
- **task:** Double-dash task arguments are now preserved with `double_dash="preserve"`. The first `--` separator was previously consumed by the outer CLI parser and lost before reaching the task. ([#&#8203;12628](https://redirect.github.com/jdx/mise/pull/12628) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **npm:** Embedded Aube npm installs are now isolated as a standalone one-package workspace, so a `pnpm-workspace.yaml` above `MISE_DATA_DIR` can no longer redirect the install into the outer workspace or mutate it. This mainly affects GitLab CI layouts where the data dir lives under `CI_PROJECT_DIR`. ([#&#8203;12630](https://redirect.github.com/jdx/mise/pull/12630) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Repository status checks now honor the global `--jobs` limit, so `mise bootstrap` status, apply, and update run these read-only git checks with bounded concurrency. ([#&#8203;12629](https://redirect.github.com/jdx/mise/pull/12629) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Changed

- **registry:** `acli`, `mimirtool`, and `specstory` are no longer restricted to Linux and macOS and can now be installed and run on Windows. ([#&#8203;12627](https://redirect.github.com/jdx/mise/pull/12627) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **registry:** `llama.cpp` resolution now includes prereleases, since upstream began flagging its rolling `bNNNN` builds as GitHub prereleases. The existing minimum-release-age and `b` version prefix behavior is unchanged. ([#&#8203;12614](https://redirect.github.com/jdx/mise/pull/12614) by [@&#8203;jdx](https://redirect.github.com/jdx))

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.8.15...v2026.8.16>

#### 💚 Sponsor mise

mise is maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer for [**entire.io**](https://entire.io), the title sponsor of the [jdx.dev](https://jdx.dev) open source tools. Development is funded by sponsors.

If mise saves you or your team time, please consider sponsoring at [jdx.dev](https://jdx.dev/sponsors.html). Individual and company sponsorships keep mise fast, free, and independent.

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42MS4zIiwidXBkYXRlZEluVmVyIjoiNDQuNjkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2026-09-08 08:16:51 -07:00
renovate[bot] e57cd9e6ca chore(deps): update redis docker digest to 298e5b3 (#688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-31 11:57:17 -07:00
renovate[bot] cab81a25e0 chore(deps): update postgres:18 docker digest to 4ef4dbc (#687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-31 11:56:56 -07:00
renovate[bot] ada9f90a1c chore(deps): update ci workflows (#689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-31 11:52:40 -07:00
renovate[bot] ee194b7110 chore(deps): update redis docker digest to 691577a (#683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-24 11:59:46 -07:00
renovate[bot] da5f3b08e7 chore(deps): update dependency @tsconfig/bases to v1.0.27 (#684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-24 11:59:26 -07:00
renovate[bot] ddb3e848dc chore(deps): update dependency jdx/mise to v2026.8.11 (#685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-24 11:58:39 -07:00
renovate[bot] b79c67f88c chore(deps): update dependency uuid to v14.0.2 (#686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-24 11:58:20 -07:00
renovate[bot] fbdd599845 chore(deps): update postgres:18 docker digest to 06cad38 (#679)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 09:45:28 -07:00
renovate[bot] fbb161ecd2 chore(deps): update dependency pg to v8.23.0 (#681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 09:45:05 -07:00
renovate[bot] f1b13a9840 chore(deps): update ci workflows (#680)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 09:44:57 -07:00
renovate[bot] 3be085cd8f chore(deps): update lint (#682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 09:44:05 -07:00
renovate[bot]andWill Schurman b5f6cb6e4e chore(deps): update lint (#678)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Will Schurman <[email protected]>
2026-08-10 09:47:36 -07:00
renovate[bot] 2e5f9ce7f5 chore(deps): update postgres:18 docker digest to a02db8c (#675)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-10 09:43:16 -07:00
renovate[bot] 38122c8bd2 chore(deps): update redis docker digest to 344e394 (#676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-10 09:42:18 -07:00
renovate[bot] 96ead4f360 chore(deps): update dependency jdx/mise to v2026.8.3 (#677)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-10 09:33:24 -07:00
renovate[bot] 7cc580550b chore(deps): update dependency ip-address to v10.2.2 [security] (#674)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-04 08:19:24 -07:00
Will Schurman 🦦 a4740446e0 chore: treat typedoc warnings as errors to keep evergreen (#672) 2026-08-03 16:04:07 -07:00
Will Schurman 🦦 d12496dc1b chore: fix typedoc warnings (#671) 2026-08-03 16:04:07 -07:00
Will Schurman 613d5d7424 v0.66.0 2026-08-03 09:28:42 -07:00
renovate[bot] dd93c0144e chore(deps): update redis docker digest to a7efd4e (#667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-03 09:11:41 -07:00
renovate[bot] 0137c84baf chore(deps): update lint (#669)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-03 09:10:51 -07:00
renovate[bot] f196b0dcfb chore(deps): update ci workflows (#668)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-03 09:02:49 -07:00
renovate[bot] 6ec73542af chore(deps): update node.js to v24.18.1 (#670)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-03 09:02:01 -07:00
Gabe Debes 59e05de4dc feat!: emit a dedicated metric event for count queries (#662)
# Why

Count queries reported their result through the load event, so a `COUNT(*)` returning 8k looked like we loaded 8k entities.

# How

Give counts their own event (`logDataManagerCountEvent`) so the count number stops being read as entities loaded. Breaking: `IEntityMetricsAdapter` implementers need to add the method; I'm happy to go for another path, non-breaking change if you prefer, but this felt more fundamentally correct 

<img width="480" height="240" alt="giphy" src="https://github.com/user-attachments/assets/4630fefc-cca4-42ca-afc4-22b9919df4a8" />

# Test Plan

CI. Updated the metrics unit test to check the counts route to the new event.
2026-07-27 09:01:57 -07:00
renovate[bot] 017642a1b2 chore(deps): update redis docker digest to c88d347 (#663)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| redis | digest | `234c902` → `c88d347` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjI4MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-07-27 07:47:44 -07:00
renovate[bot] 44e18b2011 chore(deps): update dependency ctix to v2.8.2 (#664)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [ctix](https://imjuni.github.io/ctix) ([source](https://redirect.github.com/imjuni/ctix)) | [`2.8.1` → `2.8.2`](https://renovatebot.com/diffs/npm/ctix/2.8.1/2.8.2) | ![age](https://developer.mend.io/api/mc/badges/age/npm/ctix/2.8.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/ctix/2.8.1/2.8.2?slim=true) |

---

### Release Notes

<details>
<summary>imjuni/ctix (ctix)</summary>

### [`v2.8.2`](https://redirect.github.com/imjuni/ctix/releases/tag/v2.8.2)

[Compare Source](https://redirect.github.com/imjuni/ctix/compare/v2.8.1...v2.8.2)

#### What's Changed

- chore(release): prepare v2.8.2 by [@&#8203;imjuni](https://redirect.github.com/imjuni) in [#&#8203;202](https://redirect.github.com/imjuni/ctix/pull/202)

**Full Changelog**: <https://github.com/imjuni/ctix/compare/v2.8.1...v2.8.2>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjI4MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-07-27 07:47:33 -07:00
renovate[bot] a2fd6d3f3f chore(deps): update ci workflows (#665)
> ℹ️ **Note**
> 
> This PR body was truncated due to platform limits.

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/checkout](https://redirect.github.com/actions/checkout) | action | minor | `v6.0.3` → `v6.1.0` |
| [actions/setup-node](https://redirect.github.com/actions/setup-node) | action | minor | `v6.4.0` → `v6.5.0` |
| [jdx/mise](https://redirect.github.com/jdx/mise) |  | patch | `v2026.7.5` → `v2026.7.14` |
| [jdx/mise-action](https://redirect.github.com/jdx/mise-action) | action | patch | `v4.2.0` → `v4.2.3` |

---

### Release Notes

<details>
<summary>actions/checkout (actions/checkout)</summary>

### [`v6.1.0`](https://redirect.github.com/actions/checkout/compare/v6.0.3...v6.1.0)

[Compare Source](https://redirect.github.com/actions/checkout/compare/v6.0.3...v6.1.0)

</details>

<details>
<summary>actions/setup-node (actions/setup-node)</summary>

### [`v6.5.0`](https://redirect.github.com/actions/setup-node/releases/tag/v6.5.0)

[Compare Source](https://redirect.github.com/actions/setup-node/compare/v6.4.0...v6.5.0)

##### What's Changed

- Update [@&#8203;actions/cache](https://redirect.github.com/actions/cache) to 5.1.0 and add security overrides for undici and fast-xml-parser by [@&#8203;HarithaVattikuti](https://redirect.github.com/HarithaVattikuti) in [#&#8203;1579](https://redirect.github.com/actions/setup-node/pull/1579)

**Full Changelog**: <https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0>

</details>

<details>
<summary>jdx/mise (jdx/mise)</summary>

### [`v2026.7.14`](https://redirect.github.com/jdx/mise/releases/tag/v2026.7.14): : Multi-asset GitHub installs, safer defaults, and Windows pip

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.7.13...v2026.7.14)

This release adds overlay installs for GitHub-based tools, closes a config-trust security gap around default shell arguments, and lands a wide batch of correctness fixes across tasks, lockfiles, npm, Swift, brew casks, and Windows Python.

##### Added

- **github:** the GitHub/GitLab/Forgejo release backend now accepts `additional_asset_patterns`, so a single install can overlay multiple release archives from the same tag into one installation directory. Each supplemental artifact is locked and verified (checksums and provenance) on its own, and `--locked` fails if the recorded set no longer matches. This models release layouts like Ollama's optional ROCm archive without making a large payload unconditional. ([#&#8203;11272](https://redirect.github.com/jdx/mise/pull/11272) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [tools."github:ollama/ollama"]
  version = "latest"
  additional_asset_patterns = ["ollama-linux-amd64-rocm.tgz"]
  ```
- **npm:** new `allow_low_downloads` tool option lets an embedded aube install bypass the weekly-download popularity gate for the requested package only, so curated tools like `bibtex-tidy` and `purty` install again without `npm.shell_out`. Transitive dependencies still hit the gate. ([#&#8203;11305](https://redirect.github.com/jdx/mise/pull/11305) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **env:** structured env files (`env._.file` loading JSON/YAML/TOML) support an explicit `expand = true` option for shell-style variable expansion, including references to earlier values. See Fixed below for the default-behavior change. ([#&#8203;11269](https://redirect.github.com/jdx/mise/pull/11269) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Fixed

- **env:** values loaded from JSON/YAML/TOML env files are literal by default again, fixing a regression where every value was shell-expanded when `env_shell_expand` was on. That corrupted literals such as bcrypt-style `$6$salt$hash` and could pull in matching process-environment values. Opt back into expansion with `expand = true`. ([#&#8203;11269](https://redirect.github.com/jdx/mise/pull/11269) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **python:** `pip` is now usable on fresh Windows installs. mise synthesizes `pip.cmd`/`pip3.cmd` wrappers, adds the install's `Scripts` directory to PATH so pip-installed console scripts resolve, and fixes default-package installation on Windows. ([#&#8203;11278](https://redirect.github.com/jdx/mise/pull/11278) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **github:** `.exe` release assets are now preferred on Windows, so tools that ship both an extensionless and `.exe` binary no longer install the non-runnable one and fail with "cannot find binary path". ([#&#8203;11257](https://redirect.github.com/jdx/mise/pull/11257) by [@&#8203;gologames](https://redirect.github.com/gologames))
- **github:** a rejected GitHub token (401) now produces an actionable error that names the token source (gh CLI, `github_tokens.toml`, OAuth, env var, etc.) and includes GitHub's response and a remediation hint, instead of a bare `401 Unauthorized`. ([#&#8203;11236](https://redirect.github.com/jdx/mise/pull/11236) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **backend:** an archive containing a single binary with an OS/arch suffix (e.g. `gdscript-formatter-macos-aarch64`) is now renamed to its clean name on PATH, matching the existing behavior for single-file downloads. ([#&#8203;11232](https://redirect.github.com/jdx/mise/pull/11232) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **brew:** brew-cask installs handle more real-world casks — completion stanzas and `system_command` in lifecycle blocks are supported, and artifact links into root-owned directories like `/usr/local/bin` now elevate through mise's sudo policy instead of failing with permission errors. Fixes installing `docker-desktop`. ([#&#8203;11273](https://redirect.github.com/jdx/mise/pull/11273) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **lockfile:** `prefix:` tool requests now honor the locked version on `mise install` instead of silently re-resolving to the newest match. Constrained upgrades via `mise upgrade`/`mise lock --bump` still work. ([#&#8203;11255](https://redirect.github.com/jdx/mise/pull/11255) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **aqua:** cross-platform lockfiles for aqua HTTP packages preserve a reachable `v`-prefixed URL, fixing 404s where an entry locked to another platform's unprefixed URL. ([#&#8203;11267](https://redirect.github.com/jdx/mise/pull/11267) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **swift:** Swift lockfile entries now record the target Linux distro (`ubuntu24.04`, `fedora39`, `ubi9`, ...) so checksums are no longer verified across mismatched distro tarballs, and `mise lock` writes meaningful entries and re-locks correctly after changing `swift.platform`. ([#&#8203;11299](https://redirect.github.com/jdx/mise/pull/11299) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **task:** with a git worktree checked out inside the main checkout, mise no longer loads the enclosing monorepo root's tasks into the nested root's namespace. Env, tools, and vars still inherit as before. ([#&#8203;11283](https://redirect.github.com/jdx/mise/pull/11283) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **task:** changing a task's `run` command, `sources`, or `outputs` now invalidates its cached state, so tasks are no longer incorrectly skipped after such edits. ([#&#8203;11288](https://redirect.github.com/jdx/mise/pull/11288) by [@&#8203;rabadin](https://redirect.github.com/rabadin))
- **task:** a task's source hash is now persisted only after a successful run, so a failed run no longer marks stale sources as up to date. ([#&#8203;11296](https://redirect.github.com/jdx/mise/pull/11296) by [@&#8203;rabadin](https://redirect.github.com/rabadin))
- **completions:** shell completion no longer offers mise's own global flags after a task name (where they aren't valid), and a task flag that shares a name with a mise flag now completes its own values correctly. ([#&#8203;11284](https://redirect.github.com/jdx/mise/pull/11284) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **npm:** npm package versions with very large numeric components (up to `Number.MAX_SAFE_INTEGER`) now sort correctly, fixing cases where a `0.0.*` build could be picked over a newer stable release. ([#&#8203;11280](https://redirect.github.com/jdx/mise/pull/11280) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **npm:** aube trust-downgrade failures now surface a clearer explanation, and aube's own progress display no longer competes with mise's output (bumped to 1.33.1). ([#&#8203;11292](https://redirect.github.com/jdx/mise/pull/11292), [#&#8203;11308](https://redirect.github.com/jdx/mise/pull/11308) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **npm:** aube `allow_builds` is now serialized as a map. ([#&#8203;11262](https://redirect.github.com/jdx/mise/pull/11262) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **version:** the update-check cache now negative-caches, so machines that can't reach the network (or run a build newer than the latest release) stop re-running the full check — including building an HTTP client and parsing the CA trust store — on every invocation. This is a significant speedup for commands like `mise --version` in those cases. ([#&#8203;11285](https://redirect.github.com/jdx/mise/pull/11285) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **env:** remaining `std::env::vars()` call sites now use `vars_safe()`. ([#&#8203;11309](https://redirect.github.com/jdx/mise/pull/11309) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))

##### Changed

- **usage:** every mise command now declares its effect on the system — read-only, modifies state, or destructive — surfaced in the command reference. This requires usage 4.0 (`min_usage_version` bumped), so shell completions and doc rendering now depend on it. ([#&#8203;11306](https://redirect.github.com/jdx/mise/pull/11306) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Security

- **config:** the Unix/Windows default file and inline shell-argument settings are now global-only. Because local config is loaded before trust evaluation, an untrusted repository could previously influence how commands from trusted sources were executed. Global config and `MISE_*` environment variables still apply. Reported by [@&#8203;arpitjain099](https://redirect.github.com/arpitjain099). ([#&#8203;11293](https://redirect.github.com/jdx/mise/pull/11293) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Performance

- **cli:** the clap command tree is no longer rebuilt twice on every invocation. ([#&#8203;11297](https://redirect.github.com/jdx/mise/pull/11297) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Documentation

- **npm:** documented Socket integrations. ([#&#8203;11268](https://redirect.github.com/jdx/mise/pull/11268) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** corrected the `bin_path`/`asset_pattern` template variable docs. ([#&#8203;11298](https://redirect.github.com/jdx/mise/pull/11298) by [@&#8203;jdx](https://redirect.github.com/jdx))
- Retargeted a dead clap.rs link to docs.rs/clap. ([#&#8203;11194](https://redirect.github.com/jdx/mise/pull/11194) by [@&#8203;Bartok9](https://redirect.github.com/Bartok9))
- Added a generated `llms.txt` index for AI agents. ([#&#8203;11300](https://redirect.github.com/jdx/mise/pull/11300) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Registry

- Added `checkstyle` ([#&#8203;11287](https://redirect.github.com/jdx/mise/pull/11287) by [@&#8203;zeitlinger](https://redirect.github.com/zeitlinger)), `grok` ([#&#8203;11291](https://redirect.github.com/jdx/mise/pull/11291)), and `tak` ([#&#8203;11307](https://redirect.github.com/jdx/mise/pull/11307)) by [@&#8203;jdx](https://redirect.github.com/jdx).

##### New Contributors

- [@&#8203;gologames](https://redirect.github.com/gologames) made their first contribution in [#&#8203;11257](https://redirect.github.com/jdx/mise/pull/11257)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.7.13...v2026.7.14>

##### 💚 Sponsor mise

mise is maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer for [**entire.io**](https://entire.io), the title sponsor of the [jdx.dev](https://jdx.dev) open source tools. Development is funded by sponsors.

If mise saves you or your team time, please consider sponsoring at [jdx.dev](https://jdx.dev/sponsors.html). Individual and company sponsorships keep mise fast, free, and independent.

### [`v2026.7.13`](https://redirect.github.com/jdx/mise/releases/tag/v2026.7.13): : Multi-binary renaming, cask completions, and go.mod support

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.7.12...vfox-v2026.7.13)

This release expands archive backends to rename multiple binaries from a single release, adds Homebrew cask shell completions and `go.mod` version-file support, and fixes a broad batch of task, install, lockfile, and language-plugin correctness issues.

#### Added

- **backend:** `rename_exe` now accepts a table mapping source patterns to target names, so an archive that ships several executables can expose all of them cleanly on PATH instead of only one. The existing string form is unchanged. ([#&#8203;11231](https://redirect.github.com/jdx/mise/pull/11231) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  [tools."github:DanielGavin/ols"]
  version = "latest"
  rename_exe = { "ols-*" = "ols", "odinfmt-*" = "odinfmt" }
  ```
- **brew:** brew-cask now installs shell completions (declared `bash_completion`/`fish_completion`/`zsh_completion` files and `generate_completions_from_executable` for bash, zsh, fish, and pwsh) instead of skipping them, tracking them in cask receipts and installed-state checks. ([#&#8203;11198](https://redirect.github.com/jdx/mise/pull/11198) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **go:** `go.mod` can now be used as an idiomatic version file. A `toolchain goX.Y.Z` directive resolves as an exact pin, while a `go X.Y` minimum resolves to the latest matching patch. It is opt-in and unchanged unless enabled per-tool. ([#&#8203;11213](https://redirect.github.com/jdx/mise/pull/11213) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))

  ```sh
  mise settings add idiomatic_version_file_enable_tools go
  ```
- **task:** PowerShell task shells (`pwsh`/`powershell`) now run with `-NoProfile` by default, matching how mise spawns POSIX shells, so a profile that mutates `PATH` (such as a mise activation snippet) can no longer shadow a task's own tools and cause confusing "cannot find binary path" errors. Opt out with the new `windows_powershell_no_profile` setting (`MISE_WINDOWS_POWERSHELL_NO_PROFILE=false`). ([#&#8203;11199](https://redirect.github.com/jdx/mise/pull/11199) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Fixed

- **env:** `_.path`/`_.file`/`_.source` directives within a single `[env]._` block are now resolved in written order, so a `_.path` can reference a variable exported by a `_.source` written before it. ([#&#8203;11163](https://redirect.github.com/jdx/mise/pull/11163) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **install:** failed installs no longer print a contradictory "installed but not activated" hint before the real error. ([#&#8203;11227](https://redirect.github.com/jdx/mise/pull/11227) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **install:** logical state mutations during install are now serialized to avoid races. ([#&#8203;11207](https://redirect.github.com/jdx/mise/pull/11207) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **lockfile:** upgrading a locked `github:` tool to a newer version that is already installed on disk no longer triggers a false supply-chain "provenance regression" error. ([#&#8203;11230](https://redirect.github.com/jdx/mise/pull/11230) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **npm:** aube install failures now surface the full cause chain instead of an opaque "failed to resolve dependencies" message, with mise-native remediation guidance (`trust_policy_excludes` or `npm.shell_out`) for trust-downgrade errors. ([#&#8203;11226](https://redirect.github.com/jdx/mise/pull/11226) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **task:** inline task definitions now follow a consistent first-wins precedence model across local, `conf.d`, and monorepo configs, so lower-precedence metadata no longer leaks into script tasks and the winning config's context is preserved. ([#&#8203;11103](https://redirect.github.com/jdx/mise/pull/11103) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **task:** source freshness now tracks files correctly for workspace-rooted `sources` patterns used in nested subproject tasks, so edits inside a subproject trigger a rerun. ([#&#8203;11202](https://redirect.github.com/jdx/mise/pull/11202) by [@&#8203;rabadin](https://redirect.github.com/rabadin))
- **brew:** cask upgrades handle structured `preflight_steps`/`postflight_steps` for move/remove operations, and cleanup-only `zap` `pkgutil` IDs are no longer treated as install receipts (which could leave pkg cask status permanently missing). ([#&#8203;11197](https://redirect.github.com/jdx/mise/pull/11197) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew:** cask upgrades no longer fail when removing a protected app backup (e.g. docker-desktop) hits "Permission denied", recovering permissions the way Homebrew does before retrying. ([#&#8203;11219](https://redirect.github.com/jdx/mise/pull/11219) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **python:** a `python3` executable is now provided on Windows. ([#&#8203;11212](https://redirect.github.com/jdx/mise/pull/11212) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **ruby:** custom `ruby.precompiled_url = "owner/repo"` sources now fetch release metadata directly from GitHub instead of the restricted versions host, avoiding 403 warnings. ([#&#8203;11154](https://redirect.github.com/jdx/mise/pull/11154) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **ruby:** Gemfile version pins with multi-digit segments (e.g. `ruby "3.4.10"`) are now parsed correctly. ([#&#8203;11229](https://redirect.github.com/jdx/mise/pull/11229) by [@&#8203;capnregex](https://redirect.github.com/capnregex))
- **bootstrap:** `./`-prefixed relative `[bootstrap.repos]` paths no longer display with a leading `./` component. ([#&#8203;11214](https://redirect.github.com/jdx/mise/pull/11214) by [@&#8203;lilienblum](https://redirect.github.com/lilienblum))
- **http:** invalid URLs now return an error instead of panicking. ([#&#8203;11160](https://redirect.github.com/jdx/mise/pull/11160) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **settings:** avoid a panic when a parent settings key is an inline table. ([#&#8203;11184](https://redirect.github.com/jdx/mise/pull/11184) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **self-update:** the updater's TLS backend now matches the feature it was built with, fixing failures when compiled with `native-tls`. ([#&#8203;10834](https://redirect.github.com/jdx/mise/pull/10834) by [@&#8203;bltavares](https://redirect.github.com/bltavares))

#### Deprecated

- **python:** the legacy `virtualenv` tool option is deprecated in favor of the `_.python.venv` env directive. It now emits a warning and is scheduled for removal around `2027.7.0`. ([#&#8203;11234](https://redirect.github.com/jdx/mise/pull/11234) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))

#### Documentation

- **python:** clarify that `uv_venv_auto` requires a `uv.lock` file. ([#&#8203;11223](https://redirect.github.com/jdx/mise/pull/11223) by [@&#8203;jdx](https://redirect.github.com/jdx))
- Fix dead links in the contributing guide and the aqua-registry/pipx installation docs. ([#&#8203;11123](https://redirect.github.com/jdx/mise/pull/11123), [#&#8203;11167](https://redirect.github.com/jdx/mise/pull/11167) by [@&#8203;Bartok9](https://redirect.github.com/Bartok9))

#### New Contributors

- [@&#8203;capnregex](https://redirect.github.com/capnregex) made their first contribution in [#&#8203;11229](https://redirect.github.com/jdx/mise/pull/11229)
- [@&#8203;rabadin](https://redirect.github.com/rabadin) made their first contribution in [#&#8203;11202](https://redirect.github.com/jdx/mise/pull/11202)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.7.12...v2026.7.13>

#### 💚 Sponsor mise

mise is maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer for [**entire.io**](https://entire.io), the title sponsor of the [jdx.dev](https://jdx.dev) open source tools. Development is funded by sponsors.

If mise saves you or your team time, please consider sponsoring at [jdx.dev](https://jdx.dev/sponsors.html). Individual and company sponsorships keep mise fast, free, and independent.

### [`v2026.7.12`](https://redirect.github.com/jdx/mise/releases/tag/v2026.7.12): : Safe mode, node-free npm, and a built-in OCI push client

[Compare Source](https://redirect.github.com/jdx/mise/compare/vfox-v2026.7.11...vfox-v2026.7.12)

This release centers on running mise safely against untrusted config, shipping node-free `npm:` version resolution and installs, and replacing external tools (skopeo/crane, gpg) with built-in, dependency-free implementations. Along the way it fixes a `prefer_offline` timeout regression, several config/env correctness issues, and platform-specific problems on Windows and macOS.

#### Highlights

- New `MISE_SAFE=1` mode turns mise into an inert config reader so automation (CI, Renovate) can run `mise lock --bump` against untrusted branches without trust prompts or arbitrary code execution.
- The `npm:` backend no longer needs node or npm installed to resolve or install packages — version metadata is fetched over HTTP and installs run in-process via the embedded aube package manager.
- `mise oci push` gained a full built-in registry client: no more skopeo/crane, with blob dedup, chunked uploads, retries, cross-repo mounts, layer reuse from prior images, and multi-arch image indexes.

#### Added

- **Safe mode:** a new global-only `safe` setting (`MISE_SAFE=1`) makes mise refuse or ignore anything that would let project config execute code or inject environment. Blocked (with errors): template `exec()`/`read_file()`, `_.source`, hooks, tasks, asdf plugin scripts, and plugin installs. Ignored: project `[env]`, `_.path`, `_.file`, `[shell_alias]`, and `[settings]`. HTTP-based version resolution, lockfile refresh/bump, and already-installed/embedded plugins keep working. Because a config loaded this way is inert, safe mode also skips the trust requirement. ([#&#8203;11146](https://redirect.github.com/jdx/mise/pull/11146), [#&#8203;11151](https://redirect.github.com/jdx/mise/pull/11151) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```sh
  MISE_SAFE=1 mise lock --bump --dry-run --json   # detect updates with zero code execution, no trust needed
  ```
- **`mise lock --bump`:** advance fuzzy lockfile selectors (`latest`, `lts`, prefixes like `"20"`) to the newest matching remote versions without installing anything and without touching `mise.toml`. Exact pins stay put. Pairs with `--json` and `--dry-run` for a CI job that opens lockfile-only PRs. ([#&#8203;11145](https://redirect.github.com/jdx/mise/pull/11145) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```console
  $ mise lock --bump --dry-run --json
  [{ "name": "node", "backend": "core:node", "old_versions": ["22.14.0"], "new_versions": ["22.15.0"] }]
  ```
- **Node-free `npm:` backend:** `mise ls-remote npm:*` and `latest` resolution now query the npm registry directly over HTTP, and installs run in-process through the embedded aube package manager. node is only needed to *run* installed tools (and their lifecycle scripts), not to install them. User `~/.npmrc`, scoped registries, auth, and `minimum_release_age` are honored. A new `npm.shell_out` setting (`MISE_NPM_SHELL_OUT`) routes both metadata and installs through the npm CLI for setups relying on npm-only config like `cafile` or client certs. ([#&#8203;11147](https://redirect.github.com/jdx/mise/pull/11147), [#&#8203;11149](https://redirect.github.com/jdx/mise/pull/11149) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **Built-in OCI registry client:** `mise oci push` no longer requires skopeo or crane. It uses an in-tree OCI Distribution client that uploads only blobs the registry is missing, resolves credentials the way docker/podman do (`docker login`/`podman login` is all the setup needed), and answers registry auth challenges so private base-image pulls via `--from` also work. `mise oci run --engine docker` now streams the image into `docker load` instead of needing skopeo. The `--tool` flag was removed. ([#&#8203;11132](https://redirect.github.com/jdx/mise/pull/11132) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **OCI push robustness:** large layers upload in 64 MiB chunks, transient failures retry with mise's standard backoff, base layers are mounted cross-repo (`?mount=`) when they already live on the destination registry (zero bytes transferred), and pushes/pulls now show byte progress. A new `oci.insecure_registries` setting opts non-loopback registries into plain HTTP. ([#&#8203;11141](https://redirect.github.com/jdx/mise/pull/11141) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **OCI layer reuse:** `mise oci push` reuses unchanged tool layers from the previously pushed image (or `--cache-from REF`), skipping the tar/gzip build entirely — reused tools don't even need to be installed locally. `--no-cache` forces a full rebuild. ([#&#8203;11142](https://redirect.github.com/jdx/mise/pull/11142) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **Multi-arch OCI images:** `mise oci push --update-index` points a tag at an OCI image index and upserts the current platform while preserving entries pushed by other architectures, so one runner per arch can assemble a multi-arch tag. ([#&#8203;11144](https://redirect.github.com/jdx/mise/pull/11144) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **Built-in signature verification:** Node.js and Swift download signatures are now verified in-process with the pure-Rust rPGP crate against the bundled keyrings, dropping the external `gpg` binary dependency. Note: previously, if `gpg` was missing and the setting was unset, verification was silently skipped; now verification always runs when enabled — opt out with `gpg_verify = false`. ([#&#8203;11148](https://redirect.github.com/jdx/mise/pull/11148) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **Relative bootstrap repo paths:** `[bootstrap.repos]` destination keys can now be relative paths, resolved against the declaring project's root (with guards preventing escape outside the project tree), making a project's `mise.toml` portable across machines. ([#&#8203;11155](https://redirect.github.com/jdx/mise/pull/11155) by [@&#8203;lilienblum](https://redirect.github.com/lilienblum))

#### Fixed

- **http:** `mise lock`, `ls-remote`, `outdated`, and `upgrade` no longer get capped to a single 3s no-retry attempt when `prefer_offline` is set — they honor the configured `fetch_remote_versions_timeout` and retry budget again, fixing spurious timeouts. Interactive fast paths keep their bounded behavior. ([#&#8203;11190](https://redirect.github.com/jdx/mise/pull/11190) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **lock:** unfiltered `mise lock` no longer prunes OS-restricted tool entries (e.g. `os = ["macos"]`) when run on a platform that doesn't target them, keeping shared lockfiles stable across platforms. ([#&#8203;11175](https://redirect.github.com/jdx/mise/pull/11175) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **env:** values loaded from `_.file` env files now resolve references to variables defined in earlier files or `[env]` blocks instead of collapsing to empty, fixing flaky dotenv behavior. ([#&#8203;11158](https://redirect.github.com/jdx/mise/pull/11158) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **env:** `[env] _.source` now works on Windows by resolving a real POSIX bash (Git Bash / MSYS2, honoring `MISE_BASH_PATH`) instead of the WSL launcher or a missing `bash.exe`, and correctly filters MSYS runtime noise and converts prepended PATH entries back to Windows form. ([#&#8203;11097](https://redirect.github.com/jdx/mise/pull/11097) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **config:** `trusted_config_paths` (`MISE_TRUSTED_CONFIG_PATHS`) now overrides configs previously added to the ignore list, so a settings-trusted config is actually discovered and loaded. ([#&#8203;11152](https://redirect.github.com/jdx/mise/pull/11152) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))
- **config:** `env`/`vars` array parsing is now consistent — accidental array forms for `vars` and task `env`/`vars` are rejected with clear errors, while the intentional `[[env]]` and directive-level arrays (`_.source`, `_.file`, `_.path`) remain supported. ([#&#8203;11060](https://redirect.github.com/jdx/mise/pull/11060) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **config:** `mise config set` no longer panics when descending into a non-table value. ([#&#8203;11153](https://redirect.github.com/jdx/mise/pull/11153) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **config:** clearer warnings for unsupported semver ranges. ([#&#8203;11176](https://redirect.github.com/jdx/mise/pull/11176) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **hook-env:** entering a directory with an untrusted config now prints a single concise warning (`... is not trusted, run 'mise trust' to enable it`) instead of the full error chain. Explicit commands still show the full trust error and prompt. ([#&#8203;11159](https://redirect.github.com/jdx/mise/pull/11159) by [@&#8203;lilienblum](https://redirect.github.com/lilienblum))
- **activate:** `mise activate zsh` no longer hangs non-interactive login shells under Rosetta — a v2026.7.11 regression where env-state snapshotting autoloaded the `zsh/parameter` module via `dlopen`. ([#&#8203;11188](https://redirect.github.com/jdx/mise/pull/11188) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **brew-cask:** case-insensitive app bundle lookup (fixing casks like `yaak`) and correct resolution of preflight-generated wrapper scripts on the extract stage (fixing `vlc`). ([#&#8203;11164](https://redirect.github.com/jdx/mise/pull/11164) by [@&#8203;donbeave](https://redirect.github.com/donbeave))
- **brew:** cask binaries can symlink into `/usr/local` on arm64. ([#&#8203;11174](https://redirect.github.com/jdx/mise/pull/11174) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** qualified prerelease suffixes are now detected correctly. ([#&#8203;11179](https://redirect.github.com/jdx/mise/pull/11179) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **completions:** avoid a network lookup during usage completion. ([#&#8203;11169](https://redirect.github.com/jdx/mise/pull/11169) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **link:** clear a stale incomplete marker when linking a tool. ([#&#8203;11150](https://redirect.github.com/jdx/mise/pull/11150) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **shim:** unresolved Windows exe-shim dispatch (e.g. a project-scoped tool run outside the project) now shows the actionable "no version is set" guidance instead of an opaque "cannot find binary path". ([#&#8203;11189](https://redirect.github.com/jdx/mise/pull/11189) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **vfox:** preserve inner quotes when a vfox Lua command dispatches through `cmd.exe` on Windows, fixing commands such as the Yarn 2+ install flow. ([#&#8203;11166](https://redirect.github.com/jdx/mise/pull/11166) by [@&#8203;finalchild](https://redirect.github.com/finalchild))
- **github/gitlab/forgejo:** statically-linked `linux-musl` assets are now considered a fallback for Android/Termux when a package publishes no Android-specific asset, so tools like starship, bat, and hk can install there. ([#&#8203;10730](https://redirect.github.com/jdx/mise/pull/10730) by [@&#8203;bltavares](https://redirect.github.com/bltavares))
- **deps:** avoid installing tools during dry runs, and enforce the experimental gate for task providers. ([#&#8203;11016](https://redirect.github.com/jdx/mise/pull/11016), [#&#8203;11177](https://redirect.github.com/jdx/mise/pull/11177) by [@&#8203;risu729](https://redirect.github.com/risu729))

#### Documentation

- Document environment-variable backing for file-task arguments and flags, including CLI/env/default precedence. ([#&#8203;11165](https://redirect.github.com/jdx/mise/pull/11165) by [@&#8203;zeitlinger](https://redirect.github.com/zeitlinger))

#### Breaking Changes

- `mise oci push --tool` has been removed. Use `mise oci build -o ./img` followed by `skopeo copy` if you need the old escape hatch. ([#&#8203;11132](https://redirect.github.com/jdx/mise/pull/11132))
- Node/Swift signature verification now always runs when enabled (external `gpg` is no longer required). If you relied on a missing `gpg` binary to skip verification, set `gpg_verify = false` explicitly. ([#&#8203;11148](https://redirect.github.com/jdx/mise/pull/11148))
- The unreleased `npm.use_npm_view` setting is replaced by `npm.shell_out` (`MISE_NPM_SHELL_OUT`). ([#&#8203;11149](https://redirect.github.com/jdx/mise/pull/11149))

#### New Contributors

- [@&#8203;donbeave](https://redirect.github.com/donbeave) made their first contribution in [#&#8203;11164](https://redirect.github.com/jdx/mise/pull/11164)
- [@&#8203;lilienblum](https://redirect.github.com/lilienblum) made their first contribution in [#&#8203;11159](https://redirect.github.com/jdx/mise/pull/11159)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.7.11...v2026.7.12>

#### 💚 Sponsor mise

mise is maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer for [**entire.io**](https://entire.io), the title sponsor of the [jdx.dev](https://jdx.dev) open source tools. Development is funded by sponsors.

If mise saves you or your team time, please consider sponsoring at [jdx.dev](https://jdx.dev/sponsors.html). Individual and company sponsorships keep mise fast, free, and independent.

### [`v2026.7.11`](https://redirect.github.com/jdx/mise/releases/tag/v2026.7.11): : Task Source Tracking and Activation Speedups

[Compare Source](https://redirect.github.com/jdx/mise/compare/v2026.7.7...vfox-v2026.7.11)

This release sharpens task handling around remote files, global config, and tool selectors, trims redundant work from shell activation, and fixes several bootstrap and platform-specific edge cases.

> \[!NOTE]
> This is the first published release since v2026.7.7 — versions 2026.7.8 through 2026.7.10 were tagged but never published due to a release pipeline issue. Their changes are included here; see [**Also in this release**](#also-in-this-release-v202678v2026710) below.

#### Added

- **config:** structured tool definitions now accept `version`, `path`, `prefix`, and `ref` selectors consistently in root `[tools]`, inline task definitions, task templates, and file-task headers. Exactly one selector is required, and conflicting, missing, or non-string selectors now fail with a clear configuration error instead of silently defaulting or panicking. ([#&#8203;11069](https://redirect.github.com/jdx/mise/pull/11069) by [@&#8203;risu729](https://redirect.github.com/risu729))

  ```toml
  node = { version = "20" }
  go = { prefix = "1.22" }
  python = { ref = "main" }
  shellcheck = { path = "/opt/shellcheck" }
  ```

#### Fixed

- **task:** remote HTTP and Git-backed task files now have their `#MISE` headers parsed, so metadata like `tools`, `description`, and `hide` and inline TOML overrides are honored just like local file tasks. Git-backed task files are also made executable after cloning and on cache hits. ([#&#8203;11111](https://redirect.github.com/jdx/mise/pull/11111) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **task:** file tasks loaded from user-global or system configs are now correctly treated as global regardless of their include path, so custom global scripts appear under `mise tasks --global`, stay out of `--local`, and use the invoking project root. ([#&#8203;11106](https://redirect.github.com/jdx/mise/pull/11106) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **task:** `mise tasks info`, extended listings, task help, and the MCP tasks resource now report every configuration source contributing to a task, with a new `config_sources` array in JSON output. Changing metadata in a TOML overlay now correctly invalidates the merged file task's cache. ([#&#8203;11098](https://redirect.github.com/jdx/mise/pull/11098) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **watch:** `mise --env <profile> watch` now propagates the selected environment to the watched task, so profile-specific tasks resolve correctly on each rerun. ([#&#8203;11114](https://redirect.github.com/jdx/mise/pull/11114) by [@&#8203;Marukome0743](https://redirect.github.com/Marukome0743))
- **http:** raw executables (such as PHP PHARs like Composer) installed via the HTTP backend are now created readable as well as executable (mode `0755` rather than `0711`), fixing "Could not open input file" errors for interpreters run by non-owner users. ([#&#8203;11135](https://redirect.github.com/jdx/mise/pull/11135) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **java:** `mise latest java@<vendor>` now prefers the base vendor variant over dashed specializations and sorts multi-feature vendor prefixes correctly. ([#&#8203;11109](https://redirect.github.com/jdx/mise/pull/11109) by [@&#8203;roele](https://redirect.github.com/roele))
- **config:** reject conflicting tool selectors at parse time (see Added). ([#&#8203;11069](https://redirect.github.com/jdx/mise/pull/11069) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **bootstrap:** systemd timers can now reference a managed service by its bare TOML key (e.g. `unit = "dotfiles-maintain"`), which resolves to `dev.mise.<name>.service`. Fully qualified names like `nginx.service` are still written verbatim. ([#&#8203;11128](https://redirect.github.com/jdx/mise/pull/11128) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** Homebrew casks that declare `auto_updates` are now accepted instead of failing with an unsupported-lifecycle error. ([#&#8203;11107](https://redirect.github.com/jdx/mise/pull/11107) by [@&#8203;casparbreloh](https://redirect.github.com/casparbreloh))
- **bootstrap:** macOS `defaults` handling now treats a missing key or domain as unset rather than erroring during initial setup. ([#&#8203;11118](https://redirect.github.com/jdx/mise/pull/11118) by [@&#8203;roele](https://redirect.github.com/roele))
- **windows:** `mise uninstall` now removes dangling runtime version pointer files (like `16` or `latest`) and cleans up the now-empty tool directory. ([#&#8203;11095](https://redirect.github.com/jdx/mise/pull/11095) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562))

#### Performance

- **activate:** the redundant initial prompt hook that ran immediately after activation is now skipped for bash, zsh, and fish when the session is unchanged, avoiding an extra `mise` process on shell startup. ([#&#8203;11130](https://redirect.github.com/jdx/mise/pull/11130), [#&#8203;11131](https://redirect.github.com/jdx/mise/pull/11131), [#&#8203;11134](https://redirect.github.com/jdx/mise/pull/11134) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** archive extraction and macOS `hdiutil`/`pkgutil` waits now run via `block_in_place`, so parallel installs no longer starve download progress and other tasks of runtime threads. ([#&#8203;11136](https://redirect.github.com/jdx/mise/pull/11136) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Registry

- Allow the reviewed `@nubjs/nub` postinstall lifecycle script to run via the npm backend so upgrades keep the `~/.nub/shims` hardlinks refreshed; default-deny behavior is preserved for all other packages. ([#&#8203;11126](https://redirect.github.com/jdx/mise/pull/11126) by [@&#8203;risu729](https://redirect.github.com/risu729))

#### Documentation

- **environments:** clarify mise directory variables ([#&#8203;11127](https://redirect.github.com/jdx/mise/pull/11127) by [@&#8203;jdx](https://redirect.github.com/jdx))
- Fix dead VersionFox and MCP documentation links ([#&#8203;11117](https://redirect.github.com/jdx/mise/pull/11117) by [@&#8203;Bartok9](https://redirect.github.com/Bartok9))
- Add an example showing how to alias multiple tools from one GitHub release ([#&#8203;11110](https://redirect.github.com/jdx/mise/pull/11110) by [@&#8203;LukasKnuth](https://redirect.github.com/LukasKnuth))

***

#### Also in this release: v2026.7.8–v2026.7.10

Due to a release pipeline issue, versions 2026.7.8 through 2026.7.10 were tagged but never published. This is the first release since [v2026.7.7](https://redirect.github.com/jdx/mise/releases/tag/v2026.7.7), so it also ships everything below.

##### Added

- **bootstrap:** package manager plugins — vfox Lua plugins can now act as system package managers, with a declarative `[bootstrap.plugins]` config and `mise bootstrap plugins apply|status` commands. Declared plugins install before built-in packages, then plugin-managed packages apply once host tools are available. ([#&#8203;11023](https://redirect.github.com/jdx/mise/pull/11023), [#&#8203;11024](https://redirect.github.com/jdx/mise/pull/11024) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **bootstrap:** new `mise bootstrap repos update` and `mise bootstrap repos exec` commands. Unpinned `[bootstrap.repos]` entries are intentionally never pulled by declarative apply; `repos update` gives an explicit fetch + fast-forward path (with dry-run and path filtering), and `repos exec` runs a command across configured checkouts. `mise bootstrap --update` now also updates repos. ([#&#8203;11022](https://redirect.github.com/jdx/mise/pull/11022) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **registry:** opt-in floating registries — with `registry_floating`, mise fetches the latest released registry metadata (and the current aqua registry) instead of the release-pinned copies. Useful on distributions whose mise package lags behind releases while registry entries keep changing. ([#&#8203;10971](https://redirect.github.com/jdx/mise/pull/10971) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** exact-version fast path — when a request is an exact semver version, the cargo, npm, go, gem, pipx, and dotnet backends now skip fetching the remote version list entirely and let the underlying installer validate it. This speeds up exact pins without a lockfile, first-time `mise install [email protected]`, and `mise x [email protected]`. ([#&#8203;11013](https://redirect.github.com/jdx/mise/pull/11013) by [@&#8203;Turbo87](https://redirect.github.com/Turbo87), [#&#8203;11070](https://redirect.github.com/jdx/mise/pull/11070) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **task:** dependencies whose templated names render to an empty string are now skipped, so templates can conditionally disable a dependency. Empty templated dependency *args* are omitted as well. ([#&#8203;11057](https://redirect.github.com/jdx/mise/pull/11057) by [@&#8203;risu729](https://redirect.github.com/risu729), [#&#8203;11055](https://redirect.github.com/jdx/mise/pull/11055) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Fixed

- **http:** mise now fails fast when the network is unavailable: DNS resolver failures are treated as non-retryable, a process-local circuit opens after hard DNS/connection failures, and remote-version lookups are capped at 3 seconds under `prefer_offline`. Shims, shell activation, and `mise x` make one bounded attempt instead of grinding through full retry schedules per endpoint. ([#&#8203;11066](https://redirect.github.com/jdx/mise/pull/11066) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **config:** stricter, clearer configuration validation: `env` directive value aliases ([#&#8203;11062](https://redirect.github.com/jdx/mise/pull/11062)), `env.mise` directives ([#&#8203;11053](https://redirect.github.com/jdx/mise/pull/11053)), and the experimental monorepo root key ([#&#8203;11052](https://redirect.github.com/jdx/mise/pull/11052)) are deprecated, and non-string `postinstall` hooks are rejected at parse time ([#&#8203;11061](https://redirect.github.com/jdx/mise/pull/11061)) (all by [@&#8203;risu729](https://redirect.github.com/risu729))
- **hooks:** hook definitions are validated more strictly — unknown table fields ([#&#8203;11047](https://redirect.github.com/jdx/mise/pull/11047)) and nested hook arrays ([#&#8203;11051](https://redirect.github.com/jdx/mise/pull/11051)) are rejected, and the spawned-scripts form is deprecated ([#&#8203;11043](https://redirect.github.com/jdx/mise/pull/11043)) (all by [@&#8203;risu729](https://redirect.github.com/risu729))
- **schema:** a large batch of JSON schema corrections so editors validate real configs correctly: OCI copy entries ([#&#8203;11009](https://redirect.github.com/jdx/mise/pull/11009)), bootstrap shell activation ([#&#8203;11004](https://redirect.github.com/jdx/mise/pull/11004)) and bootstrap hooks ([#&#8203;11039](https://redirect.github.com/jdx/mise/pull/11039)), structured task tool options ([#&#8203;11021](https://redirect.github.com/jdx/mise/pull/11021)), required watch file patterns ([#&#8203;11040](https://redirect.github.com/jdx/mise/pull/11040)), plugin manifest fields ([#&#8203;11035](https://redirect.github.com/jdx/mise/pull/11035)), root hook definitions ([#&#8203;11041](https://redirect.github.com/jdx/mise/pull/11041)), integer types for integer settings ([#&#8203;11037](https://redirect.github.com/jdx/mise/pull/11037)), array-typed deps provider paths ([#&#8203;11064](https://redirect.github.com/jdx/mise/pull/11064)) (all by [@&#8203;risu729](https://redirect.github.com/risu729)), and systemd timer/lifecycle options ([#&#8203;11050](https://redirect.github.com/jdx/mise/pull/11050) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **dry runs** no longer cause side effects: task setup is skipped ([#&#8203;11015](https://redirect.github.com/jdx/mise/pull/11015)), install hooks are previewed instead of executed ([#&#8203;11010](https://redirect.github.com/jdx/mise/pull/11010)), and `mise prune --dry-run` preserves runtime symlinks ([#&#8203;11017](https://redirect.github.com/jdx/mise/pull/11017)) (all by [@&#8203;risu729](https://redirect.github.com/risu729))
- **bootstrap:** Homebrew cask metadata is preserved ([#&#8203;11012](https://redirect.github.com/jdx/mise/pull/11012)) and macOS metadata files in cask artifacts are ignored ([#&#8203;11063](https://redirect.github.com/jdx/mise/pull/11063)); repo origins are compared transport-agnostically so `git@` vs `https://` remotes don't trigger spurious mismatches ([#&#8203;11034](https://redirect.github.com/jdx/mise/pull/11034) by [@&#8203;jeremy](https://redirect.github.com/jeremy)); static systemd units no longer break `bootstrap status` ([#&#8203;11056](https://redirect.github.com/jdx/mise/pull/11056) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **backend:** prerelease versions without a separator (like `1.2.3rc1`) are detected correctly ([#&#8203;11032](https://redirect.github.com/jdx/mise/pull/11032) by [@&#8203;jdx](https://redirect.github.com/jdx)); cargo sparse-index version discovery is restored ([#&#8203;11011](https://redirect.github.com/jdx/mise/pull/11011) by [@&#8203;Turbo87](https://redirect.github.com/Turbo87)); go resolves direct package module prefixes ([#&#8203;11054](https://redirect.github.com/jdx/mise/pull/11054) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **install:** `false` install env values are treated as removals ([#&#8203;11033](https://redirect.github.com/jdx/mise/pull/11033) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **lockfile:** platform-specific tool option variants are preserved instead of being collapsed ([#&#8203;10999](https://redirect.github.com/jdx/mise/pull/10999) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **shell:** hyphenated tool names work in shell integrations ([#&#8203;10961](https://redirect.github.com/jdx/mise/pull/10961) by [@&#8203;risu729](https://redirect.github.com/risu729)), and bash `deactivate` no longer errors on empty array expansions under `set -u` ([#&#8203;11026](https://redirect.github.com/jdx/mise/pull/11026) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **shim:** recursion is prevented when the directory env is filtered ([#&#8203;10982](https://redirect.github.com/jdx/mise/pull/10982) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **task:** file tasks resolve source files relative to the task directory ([#&#8203;11073](https://redirect.github.com/jdx/mise/pull/11073) by [@&#8203;JamBalaya56562](https://redirect.github.com/JamBalaya56562)), and tasks generated from templates inherit the template's `output` setting ([#&#8203;11059](https://redirect.github.com/jdx/mise/pull/11059) by [@&#8203;risu729](https://redirect.github.com/risu729))
- **launchd:** `bootout` EIO for not-loaded agents is tolerated on macOS ([#&#8203;10965](https://redirect.github.com/jdx/mise/pull/10965) by [@&#8203;hsbt](https://redirect.github.com/hsbt))
- **nix:** `TZDIR` is set so timezone tests pass in the sandbox build ([#&#8203;11019](https://redirect.github.com/jdx/mise/pull/11019) by [@&#8203;coryzibell](https://redirect.github.com/coryzibell))

##### Performance

- **startup:** \~2ms shaved off every invocation — config lookup no longer compiles \~200 glob patterns per startup (literal filenames are stat'd directly), and the clap command tree is built once instead of twice. `mise version` drops from 8.1ms to 6.6ms, `hook-env` from 10.9ms to 9.3ms. ([#&#8203;11025](https://redirect.github.com/jdx/mise/pull/11025) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **release:** release binaries for linux-x64 and macos-arm64 are now built with profile-guided optimization, trained on a hermetic offline workload covering config load, toolset resolution, `hook-env`, env rendering, tasks, and exec — \~10% faster on hot paths and \~4% smaller binaries ([#&#8203;11031](https://redirect.github.com/jdx/mise/pull/11031) by [@&#8203;jdx](https://redirect.github.com/jdx)). macOS tarballs now target macOS 12 ([#&#8203;11027](https://redirect.github.com/jdx/mise/pull/11027) by [@&#8203;jdx](https://redirect.github.com/jdx)).
- **runtime:** default tokio worker threads are capped at 16, avoiding oversized thread pools on many-core machines ([#&#8203;11029](https://redirect.github.com/jdx/mise/pull/11029) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Refactor

- tar extraction, inspection, and verification are consolidated onto `jdx-tar`, removing the system `tar` fallback and a duplicate tar implementation — consistent archive behavior across platforms ([#&#8203;11028](https://redirect.github.com/jdx/mise/pull/11028) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Registry

- add zmx ([github:neurosnap/zmx](https://redirect.github.com/neurosnap/zmx)) ([#&#8203;11006](https://redirect.github.com/jdx/mise/pull/11006) by [@&#8203;offbyone](https://redirect.github.com/offbyone))
- docker-compose switched from the aqua to the github backend ([#&#8203;10823](https://redirect.github.com/jdx/mise/pull/10823) by [@&#8203;konono](https://redirect.github.com/konono))
- eza switched from the asdf to the vfox backend ([#&#8203;11068](https://redirect.github.com/jdx/mise/pull/11068) by [@&#8203;jdx](https://redirect.github.com/jdx))

##### Documentation

- **templates:** clarify `exec` behavior in dry runs ([#&#8203;11018](https://redirect.github.com/jdx/mise/pull/11018) by [@&#8203;risu729](https://redirect.github.com/risu729))

#### New Contributors

- [@&#8203;Bartok9](https://redirect.github.com/Bartok9) made their first contribution in [#&#8203;11117](https://redirect.github.com/jdx/mise/pull/11117)
- [@&#8203;LukasKnuth](https://redirect.github.com/LukasKnuth) made their first contribution in [#&#8203;11110](https://redirect.github.com/jdx/mise/pull/11110)
- [@&#8203;jeremy](https://redirect.github.com/jeremy) made their first contribution in [#&#8203;11034](https://redirect.github.com/jdx/mise/pull/11034)
- [@&#8203;Turbo87](https://redirect.github.com/Turbo87) made their first contribution in [#&#8203;11011](https://redirect.github.com/jdx/mise/pull/11011)
- [@&#8203;coryzibell](https://redirect.github.com/coryzibell) made their first contribution in [#&#8203;11019](https://redirect.github.com/jdx/mise/pull/11019)

**Full Changelog**: <https://github.com/jdx/mise/compare/v2026.7.7...v2026.7.11>

#### 💚 Sponsor mise

mise is maintained by [@&#8203;jdx](https://redirect.github.com/jdx), an open source developer for [**entire.io**](https://entire.io), the title sponsor of the [jdx.dev](https://jdx.dev) open source tools. Development is funded by sponsors.

If mise saves you or your team time, please consider sponsoring at [jdx.dev](https://jdx.dev/sponsors.html). Individual and company sponsorships keep mise fast, free, and independent.

### [`v2026.7.7`](https://redirect.github.com/jdx/mise/releases/tag/v2026.7.7): : Monorepo deps, systemd timers, and OAuth reliability

[Compare Source](https://redirect.github.com/jdx/mise/compare/v2026.7.6...v2026.7.7)

This release extends dependency providers and systemd bootstrap to monorepo and timer-based workflows, and hardens GitHub OAuth token refreshes and cache clearing against concurrent mise processes.

#### Added

- **deps:** experimental `mise deps --monorepo` runs dependency providers across every explicitly configured `[monorepo].config_roots` entry, aligned with `mise install --monorepo`. Provider IDs are qualified by config root (e.g. `//apps/api:uv`) so repeated provider names across subprojects no longer collide, and mise installs the union of tools declared across participating roots before running providers. Plain `mise deps` remains scoped to the current config root. ([#&#8203;10975](https://redirect.github.com/jdx/mise/pull/10975) by [@&#8203;jdx](https://redirect.github.com/jdx))

  ```toml
  monorepo_root = true

  [monorepo]
  config_roots = ["apps/*", "packages/*"]
  ```

  ```bash
  mise deps --monorepo
  mise deps --monorepo --only //apps/api:uv
  ```
- **bootstrap:** manage systemd user timers alongside services via `[bootstrap.linux.systemd.units]`. Timer entries render to `.timer` units with scheduling (`on_boot_sec`, `on_unit_active_sec`, `on_unit_inactive_sec`, `on_calendar`), persistence, and `timers.target` linkage. Services gain optional directives including `type`, `remain_after_exit`, `exec_stop`, start/stop timeouts, `no_new_privileges`, and `private_tmp`. When a unit name switches between service and timer, the stale sibling unit is stopped, disabled, and removed on apply. ([#&#8203;10984](https://redirect.github.com/jdx/mise/pull/10984) by [@&#8203;jdx](https://redirect.github.com/jdx))

#### Fixed

- **github:** serialize OAuth token refreshes across mise processes. Single-use refresh tokens were only guarded by an in-process mutex, so parallel mise runs could burn the same token or overwrite the cache with stale data. The full read-refresh-write cycle now holds a filesystem lock, re-reads the cache after acquiring it, and writes atomically. Rejected refreshes now surface a sanitized error with reauthorization guidance instead of falling back silently to anonymous requests. ([#&#8203;10995](https://redirect.github.com/jdx/mise/pull/10995) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **cache:** `mise cache clear` no longer fails with `DirectoryNotEmpty` when another mise process (e.g. `hook-env`) recreates cache files mid-removal. Removal now retries with bounded backoff and tolerates concurrently recreated entries, while permission and other errors still propagate. ([#&#8203;10993](https://redirect.github.com/jdx/mise/pull/10993) by [@&#8203;jdx](https://redirect.github.com/jdx))
- **github:** skip OS package and installer assets (`.apk`, `.deb`, `.rpm`, DMG/PKG, MSI/MSIX/AppX and sidecars) during automatic asset selection for the `github:`, `gitlab:`, and `forgejo:` backends, so releases containing only unsupported packages now report that no platform asset matches instead of installing an unusable file. Expli

> ✂ **Note**
> 
> PR body was truncated to here.


</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - "before 9am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/expo/entity).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjI4MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-07-27 07:46:49 -07:00
renovate[bot] 211232d254 chore(deps): update dependency jscodeshift to v17.4.0 (#666) 2026-07-27 07:46:25 -07:00
renovate[bot] 7d1e567cc4 chore(deps): update postgres:18 docker digest to 3a82e1f (#657) 2026-07-22 10:34:32 -07:00
renovate[bot] 0fb7ba1a36 chore(deps): update redis docker digest to 234c902 (#658) 2026-07-22 10:34:20 -07:00
renovate[bot] 64ef6556d0 chore(deps): update dependency @tsconfig/bases to v1.0.26 (#659) 2026-07-22 10:34:06 -07:00
renovate[bot] f7e1739440 chore(deps): update dependency @types/supertest to v7.2.1 (#660) 2026-07-22 10:33:46 -07:00
Will Schurman 🦦 0dd046d043 chore: update command for release process (#655) 2026-07-13 12:56:04 -07:00
renovate[bot] e6fbffcfee chore(deps): update postgres:18 docker digest to 22c89fe (#651) 2026-07-13 10:14:40 -07:00
renovate[bot] 0619bd1738 chore(deps): update dependency @types/node to v24.13.3 (#652) 2026-07-13 10:14:36 -07:00
renovate[bot] 2454e5b91a chore(deps): update dependency jdx/mise to v2026.7.5 (#653) 2026-07-13 10:14:31 -07:00
renovate[bot] a40f0f4574 chore(deps): update dependency typedoc to v0.28.20 (#654) 2026-07-13 10:14:27 -07:00
Will Schurman 2e8707dd32 v0.65.0 2026-07-10 13:03:50 -07:00
Gabe Debes df75910cbd feat: accept array of parent entity classes in cascade deletion rule (#650) 2026-07-09 16:46:02 -07:00
renovate[bot] 06688ff789 chore(deps): update jest (#649) 2026-07-06 09:40:51 -07:00
renovate[bot] 957387b0d3 chore(deps): update dependency jdx/mise to v2026.7.0 (#648) 2026-07-06 09:39:34 -07:00
renovate[bot] cd353dd058 chore(deps): update dependency @types/node to v24.13.2 (#647) 2026-07-06 09:39:14 -07:00
renovate[bot] b604ff7bf2 chore(deps): update dependency uuid to v14.0.1 (#646) 2026-07-06 09:38:47 -07:00
renovate[bot] 795c231dae chore(deps): update postgres:18 docker digest to 4aabea7 (#644) 2026-06-29 08:34:29 -07:00
renovate[bot] f8eaf094b6 chore(deps): update redis docker digest to 2838d55 (#645) 2026-06-29 08:34:18 -07:00
Will Schurman 🦦 f4201afa86 chore: update renovate groups for new linter (#643) 2026-06-22 10:10:49 -07:00
renovate[bot] 4e07e761c0 chore(deps): update dependency oxfmt to v0.55.0 (#641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-22 09:22:40 -07:00
renovate[bot] ab0eb5525f chore(deps): update dependency pg to v8.22.0 (#642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-22 09:20:35 -07:00
renovate[bot] a5e1738f5f chore(deps): update ci workflows (#639) 2026-06-22 09:19:49 -07:00
renovate[bot] 9b8427a211 chore(deps): update dependency ioredis to v5.11.1 (#640) 2026-06-22 09:18:35 -07:00
renovate[bot] 4badcf8a77 chore(deps): update redis docker digest to 0a97239 (#636) 2026-06-15 08:39:47 -07:00
renovate[bot] f0c01e4414 chore(deps): update ci workflows to v2026.6.10 (#637) 2026-06-15 08:39:28 -07:00
renovate[bot] d8a6c1d8bd chore(deps): update dependency prettier to v3.8.4 (#638) 2026-06-15 08:39:07 -07:00
renovate[bot] abe20b9168 chore(deps): update postgres:18 docker digest to 29ee7bb (#635) 2026-06-15 08:38:52 -07:00
renovate[bot] 1c17dc0f37 fix(deps): update dependency koa to v3.2.1 (#634) 2026-06-08 08:44:45 -07:00
renovate[bot] 5bd968f698 chore(deps): update dependency @tsconfig/bases to v1.0.25 (#632) 2026-06-08 08:34:20 -07:00
renovate[bot] 55be94ba64 chore(deps): update dependency ctix to v2.8.1 (#633) 2026-06-08 08:33:44 -07:00
renovate[bot] 2063fc715c chore(deps): update ci workflows (#631) 2026-06-08 08:21:13 -07:00
renovate[bot] 4454602f22 chore(deps): update dependency jdx/mise to v2026.5.18 (#630) 2026-06-01 14:54:22 -07:00
renovate[bot] 42a7912dc5 chore(deps): update redis docker digest to aa049e6 (#629) 2026-06-01 14:54:13 -07:00
renovate[bot] dd8c710e82 chore(deps): update dependency @isaacs/ttlcache to v2.1.5 (#625) 2026-05-25 09:54:24 -07:00
renovate[bot] 2912764a6a chore(deps): update redis docker digest to 4d25e2f (#624) 2026-05-25 09:39:50 -07:00
renovate[bot] 6879bbde9e chore(deps): update dependency @types/koa to v3.0.3 (#626) 2026-05-25 09:39:30 -07:00
renovate[bot] e89e8f7b6d chore(deps): update dependency jdx/mise to v2026.5.15 (#627) 2026-05-25 09:38:55 -07:00
Will Schurman 🦦 d5a36d1748 chore: upgrade oxlint and add rules for previous eslint rules (#623) 2026-05-22 11:09:08 -07:00
80 changed files with 4521 additions and 1246 deletions

No files matched your search

+9 -9
View File
@@ -7,28 +7,28 @@ on:
env:
# renovate: datasource=github-releases depName=jdx/mise
MISE_VERSION: v2026.5.13
MISE_VERSION: v2026.9.7
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
version: ${{ env.MISE_VERSION }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
cache: yarn
- run: mise run ci
- uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6.0.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: ./coverage
flags: unittest
fail_ci_if_error: true
- uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6.0.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: ./coverage-integration
@@ -37,12 +37,12 @@ jobs:
docs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
version: ${{ env.MISE_VERSION }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
cache: yarn
- run: mise run typedoc
+31
View File
@@ -3,6 +3,37 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
### Features
- add ID-based and field-based knex database loaders for use with locking modifiers ([#706](https://github.com/expo/entity/issues/706)) ([e87e2a3](https://github.com/expo/entity/commit/e87e2a3a28f4279f5911bfd0189964b94a2ced7f))
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
### Features
- add forShare and skipLocked selection modifiers to knex loader ([#705](https://github.com/expo/entity/issues/705)) ([243a038](https://github.com/expo/entity/commit/243a0386fb9755f3926ade8dcc1b92adf38cd923))
- add forUpdate to knex loader ([#704](https://github.com/expo/entity/issues/704)) ([727f166](https://github.com/expo/entity/commit/727f16662d8baa3757bb8e94f51d837f17fd333d))
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package root
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package root
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
### Bug Fixes
- **deps:** update dependency koa to v3.2.1 ([#634](https://github.com/expo/entity/issues/634)) ([1c17dc0](https://github.com/expo/entity/commit/1c17dc0f37ec68fb73aae1f92b3ce2c79062b93e))
### Features
- accept array of parent entity classes in cascade deletion rule ([#650](https://github.com/expo/entity/issues/650)) ([df75910](https://github.com/expo/entity/commit/df75910cbdc89df60ed2f23dbedf8862ed5c9077))
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
### Bug Fixes
+1 -1
View File
@@ -91,7 +91,7 @@ To release a new version:
1. `git checkout main`
1. `npm login` (to refresh token)
1. `yarn lerna publish [patch|minor|major] -- --conventional-commits --force-publish`
1. `yarn lerna publish [patch|minor|major] --conventional-commits --force-publish`
1. In GitHub release interface, create a new release from the tag, copy changelog changes to release description.
## License
+1 -1
View File
@@ -1,6 +1,6 @@
{
"packages": ["packages/*"],
"npmClient": "yarn",
"version": "0.64.0",
"version": "0.69.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+1 -1
View File
@@ -1,5 +1,5 @@
[tools]
node = "24.14.1"
node = "24.21.0"
[tasks.yarn]
run = "yarn install --immutable"
+40
View File
@@ -11,6 +11,39 @@ export default defineConfig({
rules: {
// Overrides from oxlint-config-universe defaults
eqeqeq: ['warn', 'always'],
'no-restricted-properties': [
'warn',
{
object: 'it',
property: 'only',
message: 'it.only should not be committed to main.',
},
{
object: 'test',
property: 'only',
message: 'test.only should not be committed to main.',
},
{
object: 'describe',
property: 'only',
message: 'describe.only should not be committed to main.',
},
{
object: 'it',
property: 'skip',
message: 'it.skip should not be committed to main.',
},
{
object: 'test',
property: 'skip',
message: 'test.skip should not be committed to main.',
},
{
object: 'describe',
property: 'skip',
message: 'describe.skip should not be committed to main.',
},
],
'no-console': 'warn',
'no-void': ['warn', { allowAsStatement: true }],
'no-restricted-imports': ['error', { paths: ['.', '..', '../..'] }],
@@ -44,6 +77,13 @@ export default defineConfig({
'typescript/unbound-method': 'off',
},
},
{
files: ['packages/**/*.ts'],
excludeFiles: ['**/*tests__/**', 'packages/entity-codemod/src/transforms/**'],
rules: {
'no-restricted-exports': ['error', { restrictDefaultExports: { direct: true } }],
},
},
],
ignorePatterns: [
'packages/entity-codemod/**/__testfixtures__/**',
+10 -10
View File
@@ -22,17 +22,17 @@
"typedoc": "yarn build && typedoc"
},
"devDependencies": {
"@tsconfig/bases": "1.0.24",
"@types/node": "24.12.4",
"ctix": "2.8.0",
"jest": "30.3.0",
"@tsconfig/bases": "1.0.28",
"@types/node": "24.19.1",
"ctix": "2.8.2",
"jest": "30.5.1",
"lerna": "9.0.7",
"oxfmt": "0.50.0",
"oxlint": "1.60.0",
"oxlint-config-universe": "0.0.3",
"oxlint-tsgolint": "0.20.0",
"prettier": "3.8.3",
"typedoc": "0.28.19",
"oxfmt": "0.67.0",
"oxlint": "1.82.0",
"oxlint-config-universe": "0.2.0",
"oxlint-tsgolint": "0.25.0",
"prettier": "3.9.6",
"typedoc": "0.28.20",
"typescript": "6.0.3"
},
"resolutions": {
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-cache-adapter-local-memory",
"version": "0.64.0",
"version": "0.69.0",
"description": "Cross-request local memory cache adapter for @expo/entity",
"keywords": [
"entity"
@@ -30,8 +30,8 @@
},
"devDependencies": {
"@expo/entity-testing-utils": "workspace:^",
"@isaacs/ttlcache": "2.1.4",
"@jest/globals": "30.3.0",
"@isaacs/ttlcache": "2.1.5",
"@jest/globals": "30.5.1",
"typescript": "6.0.3"
},
"engines": {
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-cache-adapter-redis",
"version": "0.64.0",
"version": "0.69.0",
"description": "Redis cache adapter for @expo/entity",
"keywords": [
"entity"
@@ -29,10 +29,10 @@
"@expo/entity": "workspace:^"
},
"devDependencies": {
"@expo/batcher": "1.0.0",
"@expo/batcher": "1.1.2",
"@expo/entity-testing-utils": "workspace:^",
"@jest/globals": "30.3.0",
"ioredis": "5.10.1",
"@jest/globals": "30.5.1",
"ioredis": "5.11.1",
"ts-mockito": "2.6.1",
"typescript": "6.0.3"
},
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-codemod
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
**Note:** Version bump only for package @expo/entity-codemod
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-codemod
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-codemod
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-codemod
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-codemod
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-codemod",
"version": "0.64.0",
"version": "0.69.0",
"description": "jscodeshift codemods for @expo/entity upgrades",
"keywords": [
"entity"
@@ -25,10 +25,10 @@
"test": "yarn test:all --rootDir $(pwd)"
},
"devDependencies": {
"@jest/globals": "30.3.0",
"@jest/globals": "30.5.1",
"@types/jscodeshift": "17.3.0",
"@types/node": "24.12.4",
"jscodeshift": "17.3.0",
"@types/node": "24.19.1",
"jscodeshift": "17.4.0",
"typescript": "6.0.3"
},
"engines": {
+2
View File
@@ -0,0 +1,2 @@
// oxlint-disable unicorn/no-empty-file
// placeholder file to fix tsc/typedoc
@@ -3,6 +3,29 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
### Features
- add forShare and skipLocked selection modifiers to knex loader ([#705](https://github.com/expo/entity/issues/705)) ([243a038](https://github.com/expo/entity/commit/243a0386fb9755f3926ade8dcc1b92adf38cd923))
- add forUpdate to knex loader ([#704](https://github.com/expo/entity/issues/704)) ([727f166](https://github.com/expo/entity/commit/727f16662d8baa3757bb8e94f51d837f17fd333d))
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-database-adapter-knex-testing-utils
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-database-adapter-knex-testing-utils",
"version": "0.64.0",
"version": "0.69.0",
"description": "Testing utilities for applications using Entity with Knex database adapter",
"keywords": [
"entity"
@@ -31,9 +31,9 @@
"uuid": "^14.0.0"
},
"devDependencies": {
"@jest/globals": "30.3.0",
"@jest/globals": "30.5.1",
"@types/invariant": "2.2.37",
"@types/node": "24.12.4",
"@types/node": "24.19.1",
"typescript": "6.0.3"
},
"peerDependencies": {
@@ -212,7 +212,14 @@ export class StubPostgresDatabaseAdapter<
tableName,
tableFieldSingleValueEqualityOperands,
tableFieldMultiValueEqualityOperands,
{ orderBy: undefined, offset: undefined, limit: undefined },
{
orderBy: undefined,
offset: undefined,
limit: undefined,
forUpdate: undefined,
forShare: undefined,
skipLocked: undefined,
},
);
return results.length;
}
@@ -3,6 +3,31 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
### Features
- add ID-based and field-based knex database loaders for use with locking modifiers ([#706](https://github.com/expo/entity/issues/706)) ([e87e2a3](https://github.com/expo/entity/commit/e87e2a3a28f4279f5911bfd0189964b94a2ced7f))
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
### Features
- add forShare and skipLocked selection modifiers to knex loader ([#705](https://github.com/expo/entity/issues/705)) ([243a038](https://github.com/expo/entity/commit/243a0386fb9755f3926ade8dcc1b92adf38cd923))
- add forUpdate to knex loader ([#704](https://github.com/expo/entity/issues/704)) ([727f166](https://github.com/expo/entity/commit/727f16662d8baa3757bb8e94f51d837f17fd333d))
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-database-adapter-knex
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
### Bug Fixes
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-database-adapter-knex",
"version": "0.64.0",
"version": "0.69.0",
"description": "Knex database adapter for @expo/entity",
"keywords": [
"entity"
@@ -31,8 +31,8 @@
},
"devDependencies": {
"@expo/entity-testing-utils": "workspace:^",
"@jest/globals": "30.3.0",
"pg": "8.20.0",
"@jest/globals": "30.5.1",
"pg": "8.23.1",
"ts-mockito": "2.6.1",
"typescript": "6.0.3"
},
@@ -1,12 +1,17 @@
import type {
EntityConfiguration,
EntityConstructionUtils,
EntityPrivacyPolicy,
EntityQueryContext,
IEntityClass,
IEntityMetricsAdapter,
ReadonlyEntity,
ViewerContext,
} from '@expo/entity';
import { EntityNotFoundError, mapMap } from '@expo/entity';
import type { Result } from '@expo/results';
import { result } from '@expo/results';
import assert from 'assert';
import type {
FieldEqualityCondition,
@@ -64,6 +69,48 @@ export type EntityLoaderOrderByClause<
| EntityLoaderFieldNameOrderByClause<TFields, TSelectedFields>
| EntityLoaderFieldFragmentOrderByClause<TFields, TSelectedFields>;
/**
* Row locking modifier that locks the selected rows for update using `SELECT ... FOR UPDATE`.
* The lock is held until the end of the transaction, so the query context must be transactional.
*/
export interface EntityLoaderForUpdateRowLockingModifier {
forUpdate: true;
forShare?: never;
}
/**
* Row locking modifier that locks the selected rows in share mode using `SELECT ... FOR SHARE`.
* The lock is held until the end of the transaction, so the query context must be transactional.
*/
export interface EntityLoaderForShareRowLockingModifier {
forShare: true;
forUpdate?: never;
}
/**
* Row locking modifiers for load methods that never report a missing row as an error. Exactly one of
* `forUpdate` or `forShare` is required. `skipLocked` may be set to skip rows already locked by another
* transaction (using `SKIP LOCKED`) instead of waiting for them; skipped rows are reported as missing.
*/
export type EntityLoaderRowLockingModifiers = (
| EntityLoaderForUpdateRowLockingModifier
| EntityLoaderForShareRowLockingModifier
) & {
skipLocked?: boolean;
};
/**
* Row locking modifiers for load methods that throw when a row is missing. Exactly one of `forUpdate`
* or `forShare` is required. `skipLocked` is not permitted since a row skipped because another transaction
* holds a lock on it would be indistinguishable from a row that does not exist.
*/
export type EntityLoaderRowLockingModifiersWithoutSkipLocked = (
| EntityLoaderForUpdateRowLockingModifier
| EntityLoaderForShareRowLockingModifier
) & {
skipLocked?: never;
};
/**
* SQL modifiers that only affect the selection but not the projection.
*/
@@ -85,6 +132,24 @@ export interface EntityLoaderQuerySelectionModifiers<
* Limit the number of entities returned.
*/
limit?: number;
/**
* Lock the selected rows for update using `SELECT ... FOR UPDATE`. The lock is held until the
* end of the transaction, so the query context must be transactional. Mutually exclusive with `forShare`.
*/
forUpdate?: boolean;
/**
* Lock the selected rows in share mode using `SELECT ... FOR SHARE`. The lock is held until the
* end of the transaction, so the query context must be transactional. Mutually exclusive with `forUpdate`.
*/
forShare?: boolean;
/**
* Skip rows that are already locked by another transaction using `SKIP LOCKED` instead of waiting for them.
* Requires `forUpdate` or `forShare`.
*/
skipLocked?: boolean;
}
/**
@@ -151,7 +216,10 @@ export type EntityLoaderSearchFieldSpecification<
| NonNullableSelectedFields<TFields, TSelectedFields>
| EntityLoaderSearchFieldSQLFragmentFnSpecification<TFields, TSelectedFields>;
interface SearchSpecificationBase<
/**
* Base specification shared by all search-based pagination strategies.
*/
export interface SearchSpecificationBase<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> {
@@ -166,7 +234,10 @@ interface SearchSpecificationBase<
fields: readonly EntityLoaderSearchFieldSpecification<TFields, TSelectedFields>[];
}
interface ILikeSearchSpecification<
/**
* Search specification for case-insensitive pattern matching using the SQL ILIKE operator.
*/
export interface ILikeSearchSpecification<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> extends SearchSpecificationBase<TFields, TSelectedFields> {
@@ -177,7 +248,10 @@ interface ILikeSearchSpecification<
strategy: PaginationStrategy.ILIKE_SEARCH;
}
interface TrigramSearchSpecification<
/**
* Search specification for similarity search using PostgreSQL trigram similarity.
*/
export interface TrigramSearchSpecification<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> extends SearchSpecificationBase<TFields, TSelectedFields> {
@@ -209,7 +283,10 @@ interface TrigramSearchSpecification<
extraOrderByFields?: readonly EntityLoaderSearchFieldSpecification<TFields, TSelectedFields>[];
}
interface StandardPaginationSpecification<
/**
* Pagination specification for standard (non-search) pagination.
*/
export interface StandardPaginationSpecification<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields = keyof TFields,
> {
@@ -345,6 +422,15 @@ export class AuthorizationResultBasedKnexEntityLoader<
> {
constructor(
private readonly queryContext: EntityQueryContext,
private readonly entityConfiguration: EntityConfiguration<TFields, TIDField>,
private readonly entityClass: IEntityClass<
TFields,
TIDField,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
private readonly knexDataManager: EntityKnexDataManager<TFields, TIDField>,
protected readonly metricsAdapter: IEntityMetricsAdapter,
private readonly constructionUtils: EntityConstructionUtils<
@@ -357,6 +443,127 @@ export class AuthorizationResultBasedKnexEntityLoader<
>,
) {}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns entity result for matching ID, where result error can be UnauthorizedError or EntityNotFoundError.
*/
async loadByIDFromDatabaseAsync(
id: TFields[TIDField],
modifiers: EntityLoaderRowLockingModifiersWithoutSkipLocked,
): Promise<Result<TEntity>> {
const entityResults = await this.loadManyByIDsFromDatabaseAsync([id], modifiers);
const entityResult = entityResults.get(id);
assert(entityResult !== undefined, `${id} should be guaranteed to be present in returned map`);
return entityResult;
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns entity result for matching ID, or null if no entity exists for ID, where result error can be UnauthorizedError.
*/
async loadByIDNullableFromDatabaseAsync(
id: TFields[TIDField],
modifiers: EntityLoaderRowLockingModifiers,
): Promise<Result<TEntity> | null> {
const entityResults = await this.loadManyByIDsNullableFromDatabaseAsync([id], modifiers);
return entityResults.get(id) ?? null;
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns map from ID to corresponding entity result, where result error can be UnauthorizedError or EntityNotFoundError.
*/
async loadManyByIDsFromDatabaseAsync(
ids: readonly TFields[TIDField][],
modifiers: EntityLoaderRowLockingModifiersWithoutSkipLocked,
): Promise<ReadonlyMap<TFields[TIDField], Result<TEntity>>> {
const entityResults = await this.loadManyByIDsNullableFromDatabaseAsync(ids, modifiers);
return mapMap(
entityResults,
(entityResult, id) =>
entityResult ??
result(
new EntityNotFoundError({
entityClass: this.entityClass,
fieldName: this.entityConfiguration.idField,
fieldValue: id,
}),
),
);
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns map from ID to nullable corresponding entity result, where result error can be UnauthorizedError.
*/
async loadManyByIDsNullableFromDatabaseAsync(
ids: readonly TFields[TIDField][],
modifiers: EntityLoaderRowLockingModifiers,
): Promise<ReadonlyMap<TFields[TIDField], Result<TEntity> | null>> {
const idField = this.entityConfiguration.idField;
this.constructionUtils.validateFieldAndValues(idField, ids);
const fieldObjects =
ids.length > 0
? await this.knexDataManager.loadManyByFieldEqualityConjunctionAsync(
this.queryContext,
[{ fieldName: idField, fieldValues: ids }],
modifiers,
)
: [];
const idsToFieldObjects = new Map<TFields[TIDField], readonly Readonly<TFields>[]>(
ids.map((id) => [id, []]),
);
for (const fieldObject of fieldObjects) {
const id = fieldObject[idField];
idsToFieldObjects.set(id, [...(idsToFieldObjects.get(id) ?? []), fieldObject]);
}
const idsToEntityResults =
await this.constructionUtils.constructAndAuthorizeEntitiesAsync(idsToFieldObjects);
return mapMap(idsToEntityResults, (entityResults) => entityResults[0] ?? null);
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns entity result where uniqueFieldName equals fieldValue, or null if no entity matches the condition, where result error can be UnauthorizedError.
* @throws when multiple entities match the condition
*/
async loadByFieldEqualingFromDatabaseAsync<N extends keyof Pick<TFields, TSelectedFields>>(
uniqueFieldName: N,
fieldValue: NonNullable<TFields[N]>,
modifiers: EntityLoaderRowLockingModifiers,
): Promise<Result<TEntity> | null> {
const entityResults = await this.loadManyByFieldEqualingFromDatabaseAsync(
uniqueFieldName,
fieldValue,
modifiers,
);
assert(
entityResults.length <= 1,
`loadByFieldEqualingFromDatabase: Multiple entities of type ${this.entityClass.name} found for ${String(
uniqueFieldName,
)}=${fieldValue}`,
);
return entityResults[0] ?? null;
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns array of entity results where fieldName equals fieldValue, where result error can be UnauthorizedError
*/
async loadManyByFieldEqualingFromDatabaseAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValue: NonNullable<TFields[N]>,
modifiers: EntityLoaderRowLockingModifiers,
): Promise<readonly Result<TEntity>[]> {
return await this.loadManyByFieldEqualityConjunctionAsync(
[{ fieldName, fieldValue }],
modifiers,
);
}
/**
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
* @returns the first entity results that matches the query, where result error can be
@@ -134,6 +134,21 @@ export interface PostgresQuerySelectionModifiers<TFields extends Record<string,
* Limit the number of entities returned.
*/
limit?: number;
/**
* Lock the selected rows for update using `SELECT ... FOR UPDATE`. Mutually exclusive with `forShare`.
*/
forUpdate?: boolean;
/**
* Lock the selected rows in share mode using `SELECT ... FOR SHARE`. Mutually exclusive with `forUpdate`.
*/
forShare?: boolean;
/**
* Skip rows that are already locked by another transaction using `SKIP LOCKED`. Requires `forUpdate` or `forShare`.
*/
skipLocked?: boolean;
}
export type TableOrderByClause<TFields extends Record<string, any>> =
@@ -152,6 +167,9 @@ export interface TableQuerySelectionModifiers<TFields extends Record<string, any
orderBy: TableOrderByClause<TFields>[] | undefined;
offset: number | undefined;
limit: number | undefined;
forUpdate: boolean | undefined;
forShare: boolean | undefined;
skipLocked: boolean | undefined;
}
export abstract class BasePostgresEntityDatabaseAdapter<
@@ -343,6 +361,9 @@ export abstract class BasePostgresEntityDatabaseAdapter<
: undefined,
offset: querySelectionModifiers.offset,
limit: querySelectionModifiers.limit,
forUpdate: querySelectionModifiers.forUpdate,
forShare: querySelectionModifiers.forShare,
skipLocked: querySelectionModifiers.skipLocked,
};
}
}
@@ -22,6 +22,9 @@ export abstract class BaseSQLQueryBuilder<
limit?: number;
offset?: number;
orderBy?: readonly EntityLoaderOrderByClause<TFields, TSelectedFields>[];
forUpdate?: boolean;
forShare?: boolean;
skipLocked?: boolean;
},
) {}
@@ -41,6 +44,35 @@ export abstract class BaseSQLQueryBuilder<
return this;
}
/**
* Lock the selected rows for update using `SELECT ... FOR UPDATE`. The lock is held until the
* end of the transaction, so the query must be executed in a transactional query context.
* Mutually exclusive with `forShare`.
*/
forUpdate(): this {
this.modifiers.forUpdate = true;
return this;
}
/**
* Lock the selected rows in share mode using `SELECT ... FOR SHARE`. The lock is held until the
* end of the transaction, so the query must be executed in a transactional query context.
* Mutually exclusive with `forUpdate`.
*/
forShare(): this {
this.modifiers.forShare = true;
return this;
}
/**
* Skip rows that are already locked by another transaction using `SKIP LOCKED` instead of waiting for them.
* Requires `forUpdate()` or `forShare()`.
*/
skipLocked(): this {
this.modifiers.skipLocked = true;
return this;
}
/**
* Order by a field. Can be called multiple times to add multiple order bys.
*/
@@ -6,11 +6,14 @@ import type {
ReadonlyEntity,
ViewerContext,
} from '@expo/entity';
import { mapMap } from '@expo/entity';
import type {
AuthorizationResultBasedKnexEntityLoader,
EntityLoaderLoadPageArgs,
EntityLoaderQuerySelectionModifiers,
EntityLoaderRowLockingModifiers,
EntityLoaderRowLockingModifiersWithoutSkipLocked,
} from './AuthorizationResultBasedKnexEntityLoader.ts';
import type { FieldEqualityCondition } from './BasePostgresEntityDatabaseAdapter.ts';
import { BaseSQLQueryBuilder } from './BaseSQLQueryBuilder.ts';
@@ -57,6 +60,140 @@ export class EnforcingKnexEntityLoader<
>,
) {}
/**
* Load an entity by ID directly from the database, bypassing the dataloader and cache.
*
* Unlike {@link "@expo/entity"!EnforcingEntityLoader.loadByIDAsync | EnforcingEntityLoader.loadByIDAsync}, this
* issues one database query per call and does not read from or write to the entity cache. It exists
* for cases that need row locking modifiers, which are required to make this intent explicit.
* `skipLocked` is not permitted here since a skipped row would be reported as not found.
* Use {@link loadByIDNullableFromDatabaseAsync} with `skipLocked`.
*
* @param id - ID of the entity
* @param modifiers - row locking modifiers for the query
* @returns entity matching ID
* @throws EntityNotAuthorizedError when viewer is not authorized to view the returned entity
* @throws EntityNotFoundError when no entity exists for ID
*/
async loadByIDFromDatabaseAsync(
id: TFields[TIDField],
modifiers: EntityLoaderRowLockingModifiersWithoutSkipLocked,
): Promise<TEntity> {
const entityResult = await this.knexEntityLoader.loadByIDFromDatabaseAsync(id, modifiers);
return entityResult.enforceValue();
}
/**
* Load an entity by ID directly from the database, or return null if non-existent.
* See {@link loadByIDFromDatabaseAsync} for how this differs from the standard loader.
*
* @param id - ID of the entity
* @param modifiers - row locking modifiers for the query
* @returns entity for matching ID, or null if no entity exists for ID
* @throws EntityNotAuthorizedError when viewer is not authorized to view the returned entity
*/
async loadByIDNullableFromDatabaseAsync(
id: TFields[TIDField],
modifiers: EntityLoaderRowLockingModifiers,
): Promise<TEntity | null> {
const entityResult = await this.knexEntityLoader.loadByIDNullableFromDatabaseAsync(
id,
modifiers,
);
return entityResult ? entityResult.enforceValue() : null;
}
/**
* Load many entities for a list of IDs directly from the database.
* See {@link loadByIDFromDatabaseAsync} for how this differs from the standard loader.
*
* `skipLocked` is not permitted here since a skipped row would be reported as not found.
* Use {@link loadManyByIDsNullableFromDatabaseAsync} with `skipLocked`.
*
* @param ids - IDs of the entities to load
* @param modifiers - row locking modifiers for the query
* @returns map from ID to corresponding entity
* @throws EntityNotAuthorizedError when viewer is not authorized to view one or more of the returned entities
* @throws EntityNotFoundError when no entity exists for one or more of the IDs
*/
async loadManyByIDsFromDatabaseAsync(
ids: readonly TFields[TIDField][],
modifiers: EntityLoaderRowLockingModifiersWithoutSkipLocked,
): Promise<ReadonlyMap<TFields[TIDField], TEntity>> {
const entityResults = await this.knexEntityLoader.loadManyByIDsFromDatabaseAsync(
ids,
modifiers,
);
return mapMap(entityResults, (entityResult) => entityResult.enforceValue());
}
/**
* Load many entities for a list of IDs directly from the database, returning null for any IDs that are non-existent.
* See {@link loadByIDFromDatabaseAsync} for how this differs from the standard loader.
*
* @param ids - IDs of the entities to load
* @param modifiers - row locking modifiers for the query
* @returns map from ID to nullable corresponding entity
* @throws EntityNotAuthorizedError when viewer is not authorized to view one or more of the returned entities
*/
async loadManyByIDsNullableFromDatabaseAsync(
ids: readonly TFields[TIDField][],
modifiers: EntityLoaderRowLockingModifiers,
): Promise<ReadonlyMap<TFields[TIDField], TEntity | null>> {
const entityResults = await this.knexEntityLoader.loadManyByIDsNullableFromDatabaseAsync(
ids,
modifiers,
);
return mapMap(entityResults, (entityResult) => entityResult?.enforceValue() ?? null);
}
/**
* Load an entity where uniqueFieldName equals fieldValue directly from the database, or null if no entity matches.
* See {@link loadByIDFromDatabaseAsync} for how this differs from the standard loader.
*
* @param uniqueFieldName - entity field being queried
* @param fieldValue - uniqueFieldName field value being queried
* @param modifiers - row locking modifiers for the query
* @returns entity where uniqueFieldName equals fieldValue, or null if no entity matches the condition
* @throws when multiple entities match the condition
* @throws EntityNotAuthorizedError when viewer is not authorized to view the returned entity
*/
async loadByFieldEqualingFromDatabaseAsync<N extends keyof Pick<TFields, TSelectedFields>>(
uniqueFieldName: N,
fieldValue: NonNullable<TFields[N]>,
modifiers: EntityLoaderRowLockingModifiers,
): Promise<TEntity | null> {
const entityResult = await this.knexEntityLoader.loadByFieldEqualingFromDatabaseAsync(
uniqueFieldName,
fieldValue,
modifiers,
);
return entityResult ? entityResult.enforceValue() : null;
}
/**
* Load many entities where fieldName equals fieldValue directly from the database.
* See {@link loadByIDFromDatabaseAsync} for how this differs from the standard loader.
*
* @param fieldName - entity field being queried
* @param fieldValue - fieldName field value being queried
* @param modifiers - row locking modifiers for the query
* @returns array of entities where fieldName equals fieldValue
* @throws EntityNotAuthorizedError when viewer is not authorized to view one or more of the returned entities
*/
async loadManyByFieldEqualingFromDatabaseAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValue: NonNullable<TFields[N]>,
modifiers: EntityLoaderRowLockingModifiers,
): Promise<readonly TEntity[]> {
const entityResults = await this.knexEntityLoader.loadManyByFieldEqualingFromDatabaseAsync(
fieldName,
fieldValue,
modifiers,
);
return entityResults.map((entityResult) => entityResult.enforceValue());
}
/**
* Load the first entity matching the conjunction of field equality operands and
* query modifiers.
@@ -88,7 +225,7 @@ export class EnforcingKnexEntityLoader<
* query modifiers.
*
* Typically this is used for complex queries that cannot be expressed through simpler
* convenience methods such as {@link loadManyByFieldEqualingAsync}.
* convenience methods such as {@link "@expo/entity"!EnforcingEntityLoader.loadManyByFieldEqualingAsync | EnforcingEntityLoader.loadManyByFieldEqualingAsync}.
*
* @throws EntityNotAuthorizedError if viewer is not authorized to view the entity
* @returns entities matching the filters
@@ -79,6 +79,8 @@ export class KnexEntityLoaderFactory<
return new AuthorizationResultBasedKnexEntityLoader(
queryContext,
this.entityCompanion.entityCompanionDefinition.entityConfiguration,
this.entityCompanion.entityCompanionDefinition.entityClass,
this.knexDataManager,
this.metricsAdapter,
constructionUtils,
@@ -116,7 +116,14 @@ export class PostgresEntityDatabaseAdapter<
tableValue: tableTuple[index],
})),
[],
{ limit: 1, orderBy: undefined, offset: undefined },
{
limit: 1,
orderBy: undefined,
offset: undefined,
forUpdate: undefined,
forShare: undefined,
skipLocked: undefined,
},
);
return results[0] ?? null;
}
@@ -125,7 +132,7 @@ export class PostgresEntityDatabaseAdapter<
query: Knex.QueryBuilder,
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
): Knex.QueryBuilder {
const { orderBy, offset, limit } = querySelectionModifiers;
const { orderBy, offset, limit, forUpdate, forShare, skipLocked } = querySelectionModifiers;
let ret = query;
@@ -161,6 +168,18 @@ export class PostgresEntityDatabaseAdapter<
ret = ret.limit(limit);
}
if (forUpdate) {
ret = ret.forUpdate();
}
if (forShare) {
ret = ret.forShare();
}
if (skipLocked) {
ret = ret.skipLocked();
}
return ret;
}
@@ -48,10 +48,8 @@ export class PostgresEntityQueryContextProvider extends EntityQueryContextProvid
}
};
return {
...(transactionConfig.isolationLevel
? { isolationLevel: convertIsolationLevel(transactionConfig.isolationLevel) }
: {}),
};
return transactionConfig.isolationLevel
? { isolationLevel: convertIsolationLevel(transactionConfig.isolationLevel) }
: {};
}
}
@@ -350,7 +350,10 @@ type PickStringValueKeys<T> = {
[K in keyof T]: T[K] extends string | null ? K : never;
}[keyof T];
type JsonSerializable =
/**
* A value that can be serialized to JSON for use with JSON(B) operators.
*/
export type JsonSerializable =
| string
| number
| boolean
@@ -567,7 +570,7 @@ export class SQLChainableFragment<
* Allowed PostgreSQL type names for the cast() helper.
* Only these types can be used to prevent SQL injection through type name interpolation.
*/
const ALLOWED_CAST_TYPES = [
export const ALLOWED_CAST_TYPES = [
'int',
'integer',
'int2',
@@ -632,18 +635,30 @@ function resolveInnerExpr<TFields extends Record<string, any>>(
// TValue from SQLChainableFragment (for type-safe value checking) while falling back to
// SupportedSQLValue for plain SQLFragment inputs.
// Extract TFields from a SQLFragment type
type ExtractFragmentFields<T> = T extends SQLFragment<infer F> ? F : never;
/**
* Extract TFields from a SQLFragment type.
*/
export type ExtractFragmentFields<T> = T extends SQLFragment<infer F> ? F : never;
// Conditional value types for expression overloads.
// Uses SQLChainableFragment<any, ...> so that TExpr alone drives inference (single type param).
type FragmentValueNullable<TFragment> =
/**
* The nullable value type accepted by an expression overload for the given fragment type.
*/
export type FragmentValueNullable<TFragment> =
TFragment extends SQLChainableFragment<any, infer TValue> ? TValue | null : SupportedSQLValue;
type FragmentValue<TFragment> =
/**
* The value type accepted by an expression overload for the given fragment type.
*/
export type FragmentValue<TFragment> =
TFragment extends SQLChainableFragment<any, infer TValue> ? TValue : SupportedSQLValue;
type FragmentValueArray<TFragment> =
/**
* The array-of-values type accepted by an expression overload for the given fragment type.
*/
export type FragmentValueArray<TFragment> =
TFragment extends SQLChainableFragment<any, infer TValue>
? readonly TValue[]
: readonly SupportedSQLValue[];
@@ -1,5 +1,6 @@
import {
EntityDatabaseAdapterEmptyUpdateResultError,
EntityNotFoundError,
TransactionIsolationLevel,
ViewerContext,
} from '@expo/entity';
@@ -211,6 +212,491 @@ describe('postgres entity integration', () => {
);
});
describe('row locking modifiers (forUpdate, forShare, skipLocked)', () => {
// Attempts to lock the row from a separate connection without waiting. Postgres raises
// lock_not_available (55P03) when another transaction already holds a conflicting lock on the row.
const tryLockRowFromOtherConnectionAsync = async (
id: string,
lockMode: 'FOR UPDATE' | 'FOR SHARE' = 'FOR UPDATE',
): Promise<string | null> => {
try {
await knexInstance.raw(
`SELECT * FROM postgres_test_entities WHERE id = ? ${lockMode} NOWAIT`,
[id],
);
return null;
} catch (e) {
return (e as any).code ?? null;
}
};
it('locks rows loaded with loadManyByFieldEqualityConjunctionAsync until the transaction ends', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity = await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
.setField('name', 'locked')
.createAsync(),
);
// not locked before the transaction
expect(await tryLockRowFromOtherConnectionAsync(entity.getID())).toBeNull();
await vc1.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
const results = await PostgresTestEntity.knexLoader(
vc1,
queryContext,
).loadManyByFieldEqualityConjunctionAsync([{ fieldName: 'name', fieldValue: 'locked' }], {
forUpdate: true,
});
expect(results).toHaveLength(1);
expect(results[0]!.getID()).toBe(entity.getID());
// locked while the transaction is open
expect(await tryLockRowFromOtherConnectionAsync(entity.getID())).toBe('55P03');
});
// unlocked after the transaction commits
expect(await tryLockRowFromOtherConnectionAsync(entity.getID())).toBeNull();
});
it('locks rows loaded with loadFirstByFieldEqualityConjunctionAsync', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity = await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
.setField('name', 'locked-first')
.createAsync(),
);
await vc1.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
const result = await PostgresTestEntity.knexLoader(
vc1,
queryContext,
).loadFirstByFieldEqualityConjunctionAsync(
[{ fieldName: 'name', fieldValue: 'locked-first' }],
{
orderBy: [{ fieldName: 'name', order: OrderByOrdering.ASCENDING }],
forUpdate: true,
},
);
expect(result?.getID()).toBe(entity.getID());
expect(await tryLockRowFromOtherConnectionAsync(entity.getID())).toBe('55P03');
});
expect(await tryLockRowFromOtherConnectionAsync(entity.getID())).toBeNull();
});
it('locks rows loaded with the loadManyBySQL query builder', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity = await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
.setField('name', 'locked-sql')
.createAsync(),
);
await vc1.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
// via the fluent builder method
const results = await PostgresTestEntity.knexLoader(vc1, queryContext)
.loadManyBySQL(sql`name = ${'locked-sql'}`)
.forUpdate()
.executeAsync();
expect(results).toHaveLength(1);
expect(results[0]!.getID()).toBe(entity.getID());
expect(await tryLockRowFromOtherConnectionAsync(entity.getID())).toBe('55P03');
// via the modifiers argument on the authorization-result-based loader
const authorizationResults = await PostgresTestEntity.knexLoaderWithAuthorizationResults(
vc1,
queryContext,
)
.loadManyBySQL(sql`name = ${'locked-sql'}`, { forUpdate: true })
.executeAsync();
expect(authorizationResults).toHaveLength(1);
expect(authorizationResults[0]!.enforceValue().getID()).toBe(entity.getID());
});
expect(await tryLockRowFromOtherConnectionAsync(entity.getID())).toBeNull();
});
it('blocks a concurrent forUpdate load until the first transaction completes', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity = await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
.setField('name', 'counter')
.setField('hasADog', false)
.createAsync(),
);
const events: string[] = [];
const lockAndUpdateAsync = async (label: string, holdDuration: number): Promise<void> => {
await vc1.runInTransactionForDatabaseAdapterFlavorAsync(
'postgres',
async (queryContext) => {
const locked = await PostgresTestEntity.knexLoader(vc1, queryContext)
.loadManyBySQL(sql`id = ${entity.getID()}`)
.forUpdate()
.executeAsync();
events.push(`${label}:locked`);
await setTimeout(holdDuration);
await PostgresTestEntity.updater(locked[0]!, queryContext)
.setField('name', locked[0]!.getField('name') + ',' + label)
.updateAsync();
events.push(`${label}:updated`);
},
);
};
await Promise.all([
lockAndUpdateAsync('a', 200),
setTimeout(50).then(() => lockAndUpdateAsync('b', 0)),
]);
// b cannot acquire the lock until a commits, so a's update always lands before b locks
expect(events).toEqual(['a:locked', 'a:updated', 'b:locked', 'b:updated']);
const reloaded = await PostgresTestEntity.loader(vc1).loadByIDAsync(entity.getID());
expect(reloaded.getField('name')).toBe('counter,a,b');
});
it('forShare allows concurrent share locks but blocks update locks', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity = await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
.setField('name', 'shared')
.createAsync(),
);
await vc1.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
const results = await PostgresTestEntity.knexLoader(
vc1,
queryContext,
).loadManyByFieldEqualityConjunctionAsync([{ fieldName: 'name', fieldValue: 'shared' }], {
forShare: true,
});
expect(results).toHaveLength(1);
// another share lock is compatible, an update lock is not
expect(await tryLockRowFromOtherConnectionAsync(entity.getID(), 'FOR SHARE')).toBeNull();
expect(await tryLockRowFromOtherConnectionAsync(entity.getID(), 'FOR UPDATE')).toBe(
'55P03',
);
// via the fluent builder method
const builderResults = await PostgresTestEntity.knexLoader(vc1, queryContext)
.loadManyBySQL(sql`name = ${'shared'}`)
.forShare()
.executeAsync();
expect(builderResults).toHaveLength(1);
});
expect(await tryLockRowFromOtherConnectionAsync(entity.getID(), 'FOR UPDATE')).toBeNull();
});
it('skipLocked skips rows locked by another transaction', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entityA = await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
.setField('name', 'queue')
.createAsync(),
);
const entityB = await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
.setField('name', 'queue')
.createAsync(),
);
await vc1.runInTransactionForDatabaseAdapterFlavorAsync(
'postgres',
async (outerQueryContext) => {
// lock entityA in the outer transaction
const locked = await PostgresTestEntity.knexLoader(vc1, outerQueryContext)
.loadManyBySQL(sql`id = ${entityA.getID()}`)
.forUpdate()
.executeAsync();
expect(locked.map((e) => e.getID())).toEqual([entityA.getID()]);
// a second transaction on another connection sees only the unlocked row
const vc2 = new ViewerContext(
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
);
await vc2.runInTransactionForDatabaseAdapterFlavorAsync(
'postgres',
async (innerQueryContext) => {
const skipLockedResults = await PostgresTestEntity.knexLoader(
vc2,
innerQueryContext,
).loadManyByFieldEqualityConjunctionAsync(
[{ fieldName: 'name', fieldValue: 'queue' }],
{ forUpdate: true, skipLocked: true },
);
expect(skipLockedResults.map((e) => e.getID())).toEqual([entityB.getID()]);
const builderResults = await PostgresTestEntity.knexLoader(vc2, innerQueryContext)
.loadManyBySQL(sql`name = ${'queue'}`)
.forShare()
.skipLocked()
.executeAsync();
expect(builderResults.map((e) => e.getID())).toEqual([entityB.getID()]);
},
);
},
);
// both rows are visible once the outer transaction commits
await vc1.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
const results = await PostgresTestEntity.knexLoader(
vc1,
queryContext,
).loadManyByFieldEqualityConjunctionAsync([{ fieldName: 'name', fieldValue: 'queue' }], {
forUpdate: true,
skipLocked: true,
});
expect(results).toHaveLength(2);
});
});
it('throws when forUpdate or forShare is used outside of a transaction', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await expect(
PostgresTestEntity.knexLoader(vc1).loadManyByFieldEqualityConjunctionAsync([], {
forUpdate: true,
}),
).rejects.toThrow('require a transactional query context');
await expect(
PostgresTestEntity.knexLoader(vc1).loadManyByFieldEqualityConjunctionAsync([], {
forShare: true,
}),
).rejects.toThrow('require a transactional query context');
await expect(
PostgresTestEntity.knexLoader(vc1)
.loadManyBySQL(sql`TRUE`)
.forUpdate()
.executeAsync(),
).rejects.toThrow('require a transactional query context');
});
it('throws for invalid row locking modifier combinations', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await vc1.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
await expect(
PostgresTestEntity.knexLoader(vc1, queryContext)
.loadManyBySQL(sql`TRUE`)
.forUpdate()
.forShare()
.executeAsync(),
).rejects.toThrow('forUpdate and forShare are mutually exclusive');
await expect(
PostgresTestEntity.knexLoader(vc1, queryContext)
.loadManyBySQL(sql`TRUE`)
.skipLocked()
.executeAsync(),
).rejects.toThrow('skipLocked requires forUpdate or forShare');
});
});
});
describe('FromDatabase load methods', () => {
const tryLockRowFromOtherConnectionAsync = async (id: string): Promise<string | null> => {
try {
await knexInstance.raw(
'SELECT * FROM postgres_test_entities WHERE id = ? FOR UPDATE NOWAIT',
[id],
);
return null;
} catch (e) {
return (e as any).code ?? null;
}
};
it('loadByIDFromDatabaseAsync locks the row and throws EntityNotFoundError when missing', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity = await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
.setField('name', 'by-id')
.createAsync(),
);
await vc1.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
const loaded = await PostgresTestEntity.knexLoader(
vc1,
queryContext,
).loadByIDFromDatabaseAsync(entity.getID(), { forUpdate: true });
expect(loaded.getID()).toBe(entity.getID());
expect(loaded.getField('name')).toBe('by-id');
expect(await tryLockRowFromOtherConnectionAsync(entity.getID())).toBe('55P03');
await expect(
PostgresTestEntity.knexLoader(vc1, queryContext).loadByIDFromDatabaseAsync(
'00000000-0000-0000-0000-000000000000',
{ forUpdate: true },
),
).rejects.toThrow(EntityNotFoundError);
const nullable = await PostgresTestEntity.knexLoader(
vc1,
queryContext,
).loadByIDNullableFromDatabaseAsync('00000000-0000-0000-0000-000000000000', {
forUpdate: true,
});
expect(nullable).toBeNull();
});
expect(await tryLockRowFromOtherConnectionAsync(entity.getID())).toBeNull();
});
it('loadManyByIDsFromDatabaseAsync returns a map and throws for missing IDs', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entityA = await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1).setField('name', 'a').createAsync(),
);
const entityB = await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1).setField('name', 'b').createAsync(),
);
const missingId = '00000000-0000-0000-0000-000000000000';
await vc1.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
const loaded = await PostgresTestEntity.knexLoader(
vc1,
queryContext,
).loadManyByIDsFromDatabaseAsync([entityA.getID(), entityB.getID()], { forShare: true });
expect(loaded.size).toBe(2);
expect(loaded.get(entityA.getID())!.getField('name')).toBe('a');
expect(loaded.get(entityB.getID())!.getField('name')).toBe('b');
await expect(
PostgresTestEntity.knexLoader(vc1, queryContext).loadManyByIDsFromDatabaseAsync(
[entityA.getID(), missingId],
{ forShare: true },
),
).rejects.toThrow(EntityNotFoundError);
const nullable = await PostgresTestEntity.knexLoader(
vc1,
queryContext,
).loadManyByIDsNullableFromDatabaseAsync([entityA.getID(), missingId], {
forShare: true,
});
expect(nullable.size).toBe(2);
expect(nullable.get(entityA.getID())!.getField('name')).toBe('a');
expect(nullable.get(missingId)).toBeNull();
});
});
it('loadManyByIDsNullableFromDatabaseAsync with skipLocked returns null for locked rows', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entityA = await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1).setField('name', 'a').createAsync(),
);
const entityB = await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1).setField('name', 'b').createAsync(),
);
await vc1.runInTransactionForDatabaseAdapterFlavorAsync(
'postgres',
async (outerQueryContext) => {
await PostgresTestEntity.knexLoader(vc1, outerQueryContext).loadByIDFromDatabaseAsync(
entityA.getID(),
{ forUpdate: true },
);
const vc2 = new ViewerContext(
createKnexIntegrationTestEntityCompanionProvider(knexInstance),
);
await vc2.runInTransactionForDatabaseAdapterFlavorAsync(
'postgres',
async (innerQueryContext) => {
const results = await PostgresTestEntity.knexLoader(
vc2,
innerQueryContext,
).loadManyByIDsNullableFromDatabaseAsync([entityA.getID(), entityB.getID()], {
forUpdate: true,
skipLocked: true,
});
expect(results.get(entityA.getID())).toBeNull();
expect(results.get(entityB.getID())!.getField('name')).toBe('b');
// the throwing variants do not permit skipLocked since a skipped row would be reported as not found
await expect(
PostgresTestEntity.knexLoader(vc2, innerQueryContext).loadByIDFromDatabaseAsync(
entityB.getID(),
// @ts-expect-error skipLocked is not permitted for throwing FromDatabase methods
{ forUpdate: true, skipLocked: true },
),
).resolves.toBeDefined();
},
);
},
);
});
it('loadByFieldEqualingFromDatabaseAsync and loadManyByFieldEqualingFromDatabaseAsync lock matching rows', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const unique = await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
.setField('name', 'unique')
.createAsync(),
);
await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
.setField('name', 'dup')
.createAsync(),
);
await enforceAsyncResult(
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
.setField('name', 'dup')
.createAsync(),
);
await vc1.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
const loaded = await PostgresTestEntity.knexLoader(
vc1,
queryContext,
).loadByFieldEqualingFromDatabaseAsync('name', 'unique', { forUpdate: true });
expect(loaded!.getID()).toBe(unique.getID());
expect(await tryLockRowFromOtherConnectionAsync(unique.getID())).toBe('55P03');
const missing = await PostgresTestEntity.knexLoader(
vc1,
queryContext,
).loadByFieldEqualingFromDatabaseAsync('name', 'nope', { forUpdate: true });
expect(missing).toBeNull();
await expect(
PostgresTestEntity.knexLoader(vc1, queryContext).loadByFieldEqualingFromDatabaseAsync(
'name',
'dup',
{ forUpdate: true },
),
).rejects.toThrow('Multiple entities of type PostgresTestEntity found for name=dup');
const many = await PostgresTestEntity.knexLoader(
vc1,
queryContext,
).loadManyByFieldEqualingFromDatabaseAsync('name', 'dup', { forUpdate: true });
expect(many).toHaveLength(2);
for (const e of many) {
expect(await tryLockRowFromOtherConnectionAsync(e.getID())).toBe('55P03');
}
});
expect(await tryLockRowFromOtherConnectionAsync(unique.getID())).toBeNull();
});
it('FromDatabase methods require a transaction when a lock is requested', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await expect(
PostgresTestEntity.knexLoader(vc1).loadByIDFromDatabaseAsync(
'00000000-0000-0000-0000-000000000000',
{ forUpdate: true },
),
).rejects.toThrow('require a transactional query context');
});
});
describe('JSON fields', () => {
it('supports both types of array fields', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
@@ -3,9 +3,14 @@ import type {
EntityQueryContext,
IEntityMetricsAdapter,
} from '@expo/entity';
import { enforceResultsAsync, EntityConstructionUtils, ViewerContext } from '@expo/entity';
import {
enforceResultsAsync,
EntityConstructionUtils,
EntityNotFoundError,
ViewerContext,
} from '@expo/entity';
import { describe, expect, it } from '@jest/globals';
import { anyOfClass, anything, instance, mock, spy, verify, when } from 'ts-mockito';
import { anyOfClass, anything, deepEqual, instance, mock, spy, verify, when } from 'ts-mockito';
import { v4 as uuidv4 } from 'uuid';
import { AuthorizationResultBasedKnexEntityLoader } from '../AuthorizationResultBasedKnexEntityLoader.ts';
@@ -87,6 +92,8 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => {
);
const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
queryContext,
testEntityConfiguration,
TestEntity,
instance(knexDataManagerMock),
metricsAdapter,
constructionUtils,
@@ -170,6 +177,8 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => {
);
const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
queryContext,
testEntityConfiguration,
TestEntity,
instance(knexDataManagerMock),
metricsAdapter,
constructionUtils,
@@ -263,6 +272,8 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => {
const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
queryContext,
testEntityConfiguration,
TestEntity,
instance(knexDataManagerMock),
metricsAdapter,
constructionUtils,
@@ -344,6 +355,8 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => {
const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
queryContext,
testEntityConfiguration,
TestEntity,
instance(knexDataManagerMock),
metricsAdapter,
constructionUtils,
@@ -430,6 +443,8 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => {
const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
queryContext,
testEntityConfiguration,
TestEntity,
instance(knexDataManagerMock),
metricsAdapter,
constructionUtils,
@@ -545,6 +560,8 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => {
const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
queryContext,
testPaginationEntityConfiguration,
TestPaginationEntity,
instance(knexDataManagerMock),
metricsAdapter,
constructionUtils,
@@ -631,6 +648,8 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => {
const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
queryContext,
testEntityConfiguration,
TestEntity,
instance(knexDataManagerMock),
metricsAdapter,
constructionUtils,
@@ -731,6 +750,8 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => {
const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
queryContext,
testPaginationEntityConfiguration,
TestPaginationEntity,
instance(knexDataManagerMock),
metricsAdapter,
constructionUtils,
@@ -825,6 +846,8 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => {
const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
queryContext,
testPaginationEntityConfiguration,
TestPaginationEntity,
instance(knexDataManagerMock),
metricsAdapter,
constructionUtils,
@@ -916,6 +939,8 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => {
const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
queryContext,
testPaginationEntityConfiguration,
TestPaginationEntity,
instance(knexDataManagerMock),
metricsAdapter,
constructionUtils,
@@ -980,6 +1005,8 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => {
const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
queryContext,
testPaginationEntityConfiguration,
TestPaginationEntity,
instance(knexDataManagerMock),
metricsAdapter,
constructionUtils,
@@ -1067,6 +1094,8 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => {
const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
queryContext,
testPaginationEntityConfiguration,
TestPaginationEntity,
instance(knexDataManagerMock),
metricsAdapter,
constructionUtils,
@@ -1087,4 +1116,326 @@ describe(AuthorizationResultBasedKnexEntityLoader, () => {
expect(connection.pageInfo.endCursor).toBeNull();
});
});
describe('FromDatabase load methods', () => {
const makeFieldObject = (id: string, testIndexedField: string): TestFields => ({
customIdField: id,
stringField: 'huh',
intField: 4,
testIndexedField,
dateField: new Date(),
nullableField: null,
});
const setup = (): {
knexEntityLoader: AuthorizationResultBasedKnexEntityLoader<
TestFields,
'customIdField',
ViewerContext,
TestEntity,
TestEntityPrivacyPolicy,
keyof TestFields
>;
knexDataManagerMock: EntityKnexDataManager<TestFields, 'customIdField'>;
queryContext: EntityQueryContext;
} => {
const privacyPolicy = new TestEntityPrivacyPolicy();
const viewerContext = instance(mock(ViewerContext));
const privacyPolicyEvaluationContext =
instance(
mock<
EntityPrivacyPolicyEvaluationContext<
TestFields,
'customIdField',
ViewerContext,
TestEntity
>
>(),
);
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
const queryContext = instance(mock<EntityQueryContext>());
const knexDataManagerMock =
mock<EntityKnexDataManager<TestFields, 'customIdField'>>(EntityKnexDataManager);
const constructionUtils = new EntityConstructionUtils(
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
testEntityConfiguration,
TestEntity,
/* entitySelectedFields */ undefined,
privacyPolicy,
metricsAdapter,
);
const knexEntityLoader = new AuthorizationResultBasedKnexEntityLoader(
queryContext,
testEntityConfiguration,
TestEntity,
instance(knexDataManagerMock),
metricsAdapter,
constructionUtils,
);
return { knexEntityLoader, knexDataManagerMock, queryContext };
};
it('loads entities with loadManyByIDsNullableFromDatabaseAsync', async () => {
const { knexEntityLoader, knexDataManagerMock, queryContext } = setup();
const id1 = uuidv4();
const id2 = uuidv4();
const missingId = uuidv4();
when(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
queryContext,
anything(),
anything(),
),
).thenResolve([makeFieldObject(id1, '1'), makeFieldObject(id2, '2')]);
const results = await knexEntityLoader.loadManyByIDsNullableFromDatabaseAsync(
[id1, id2, missingId],
{ forUpdate: true },
);
expect(results.size).toBe(3);
expect(results.get(id1)!.enforceValue().getID()).toBe(id1);
expect(results.get(id2)!.enforceValue().getID()).toBe(id2);
expect(results.get(missingId)).toBeNull();
verify(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
queryContext,
deepEqual([{ fieldName: 'customIdField', fieldValues: [id1, id2, missingId] }]),
deepEqual({ forUpdate: true }),
),
).once();
});
it('returns an empty map and does not query for an empty list of IDs', async () => {
const { knexEntityLoader, knexDataManagerMock } = setup();
const results = await knexEntityLoader.loadManyByIDsNullableFromDatabaseAsync([], {
forUpdate: true,
});
expect(results.size).toBe(0);
verify(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
anything(),
anything(),
anything(),
),
).never();
});
it('returns EntityNotFoundError results for missing IDs with loadManyByIDsFromDatabaseAsync', async () => {
const { knexEntityLoader, knexDataManagerMock, queryContext } = setup();
const id1 = uuidv4();
const missingId = uuidv4();
when(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
queryContext,
anything(),
anything(),
),
).thenResolve([makeFieldObject(id1, '1')]);
const results = await knexEntityLoader.loadManyByIDsFromDatabaseAsync([id1, missingId], {
forShare: true,
});
expect(results.size).toBe(2);
expect(results.get(id1)!.ok).toBe(true);
const missingResult = results.get(missingId)!;
expect(missingResult.ok).toBe(false);
expect(missingResult.enforceError()).toBeInstanceOf(EntityNotFoundError);
});
it('loads an entity with loadByIDFromDatabaseAsync', async () => {
const { knexEntityLoader, knexDataManagerMock, queryContext } = setup();
const id1 = uuidv4();
const missingId = uuidv4();
when(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
queryContext,
deepEqual([{ fieldName: 'customIdField', fieldValues: [id1] }]),
anything(),
),
).thenResolve([makeFieldObject(id1, '1')]);
when(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
queryContext,
deepEqual([{ fieldName: 'customIdField', fieldValues: [missingId] }]),
anything(),
),
).thenResolve([]);
const found = await knexEntityLoader.loadByIDFromDatabaseAsync(id1, { forUpdate: true });
expect(found.enforceValue().getID()).toBe(id1);
const missing = await knexEntityLoader.loadByIDFromDatabaseAsync(missingId, {
forUpdate: true,
});
expect(missing.ok).toBe(false);
expect(missing.enforceError()).toBeInstanceOf(EntityNotFoundError);
});
it('loads an entity or null with loadByIDNullableFromDatabaseAsync', async () => {
const { knexEntityLoader, knexDataManagerMock, queryContext } = setup();
const id1 = uuidv4();
const missingId = uuidv4();
when(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
queryContext,
deepEqual([{ fieldName: 'customIdField', fieldValues: [id1] }]),
anything(),
),
).thenResolve([makeFieldObject(id1, '1')]);
when(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
queryContext,
deepEqual([{ fieldName: 'customIdField', fieldValues: [missingId] }]),
anything(),
),
).thenResolve([]);
const found = await knexEntityLoader.loadByIDNullableFromDatabaseAsync(id1, {
forUpdate: true,
skipLocked: true,
});
expect(found!.enforceValue().getID()).toBe(id1);
const missing = await knexEntityLoader.loadByIDNullableFromDatabaseAsync(missingId, {
forUpdate: true,
skipLocked: true,
});
expect(missing).toBeNull();
});
it('requires exactly one lock mode at the type level', async () => {
const { knexEntityLoader, knexDataManagerMock, queryContext } = setup();
const id1 = uuidv4();
when(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
queryContext,
anything(),
anything(),
),
).thenResolve([makeFieldObject(id1, '1')]);
// valid: exactly one lock mode, optional skipLocked on nullable variants
await knexEntityLoader.loadByIDNullableFromDatabaseAsync(id1, { forUpdate: true });
await knexEntityLoader.loadByIDNullableFromDatabaseAsync(id1, { forShare: true });
await knexEntityLoader.loadByIDNullableFromDatabaseAsync(id1, {
forShare: true,
skipLocked: true,
});
await knexEntityLoader.loadByIDFromDatabaseAsync(id1, { forUpdate: true });
// @ts-expect-error a lock mode is required
await knexEntityLoader.loadByIDNullableFromDatabaseAsync(id1, {});
// @ts-expect-error skipLocked alone is not a lock mode
await knexEntityLoader.loadByIDNullableFromDatabaseAsync(id1, { skipLocked: true });
// @ts-expect-error forUpdate must be true
await knexEntityLoader.loadByIDNullableFromDatabaseAsync(id1, { forUpdate: false });
await knexEntityLoader.loadByIDNullableFromDatabaseAsync(id1, {
forUpdate: true,
// @ts-expect-error forUpdate and forShare are mutually exclusive
forShare: true,
});
// @ts-expect-error skipLocked is not permitted on throwing variants
await knexEntityLoader.loadByIDFromDatabaseAsync(id1, { forUpdate: true, skipLocked: true });
await knexEntityLoader.loadManyByIDsFromDatabaseAsync([id1], {
forShare: true,
// @ts-expect-error skipLocked is not permitted on throwing variants
skipLocked: true,
});
});
it('validates ID values for FromDatabase ID loads', async () => {
const { knexEntityLoader, knexDataManagerMock } = setup();
await expect(
knexEntityLoader.loadByIDFromDatabaseAsync('not-a-uuid', { forUpdate: true }),
).rejects.toThrow('Entity field not valid: TestEntity (customIdField = not-a-uuid)');
verify(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
anything(),
anything(),
anything(),
),
).never();
});
it('loads entities with loadManyByFieldEqualingFromDatabaseAsync', async () => {
const { knexEntityLoader, knexDataManagerMock, queryContext } = setup();
const id1 = uuidv4();
const id2 = uuidv4();
when(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
queryContext,
anything(),
anything(),
),
).thenResolve([makeFieldObject(id1, '1'), makeFieldObject(id2, '2')]);
const results = await knexEntityLoader.loadManyByFieldEqualingFromDatabaseAsync(
'stringField',
'huh',
{ forUpdate: true },
);
expect(results).toHaveLength(2);
expect(results.map((r) => r.enforceValue().getID())).toEqual([id1, id2]);
verify(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
queryContext,
deepEqual([{ fieldName: 'stringField', fieldValue: 'huh' }]),
deepEqual({ forUpdate: true }),
),
).once();
});
it('loads an entity or null with loadByFieldEqualingFromDatabaseAsync and throws on multiple matches', async () => {
const { knexEntityLoader, knexDataManagerMock, queryContext } = setup();
const id1 = uuidv4();
const id2 = uuidv4();
when(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
queryContext,
deepEqual([{ fieldName: 'testIndexedField', fieldValue: 'one' }]),
anything(),
),
).thenResolve([makeFieldObject(id1, 'one')]);
when(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
queryContext,
deepEqual([{ fieldName: 'testIndexedField', fieldValue: 'none' }]),
anything(),
),
).thenResolve([]);
when(
knexDataManagerMock.loadManyByFieldEqualityConjunctionAsync(
queryContext,
deepEqual([{ fieldName: 'testIndexedField', fieldValue: 'many' }]),
anything(),
),
).thenResolve([makeFieldObject(id1, 'many'), makeFieldObject(id2, 'many')]);
const found = await knexEntityLoader.loadByFieldEqualingFromDatabaseAsync(
'testIndexedField',
'one',
{ forShare: true },
);
expect(found!.enforceValue().getID()).toBe(id1);
const missing = await knexEntityLoader.loadByFieldEqualingFromDatabaseAsync(
'testIndexedField',
'none',
{ forShare: true },
);
expect(missing).toBeNull();
await expect(
knexEntityLoader.loadByFieldEqualingFromDatabaseAsync('testIndexedField', 'many', {
forShare: true,
}),
).rejects.toThrow(
'loadByFieldEqualingFromDatabase: Multiple entities of type TestEntity found for testIndexedField=many',
);
});
});
});
@@ -6,8 +6,10 @@ import { instance, mock } from 'ts-mockito';
import type {
TableFieldMultiValueEqualityCondition,
TableFieldSingleValueEqualityCondition,
TableQuerySelectionModifiers,
} from '../BasePostgresEntityDatabaseAdapter.ts';
import { BasePostgresEntityDatabaseAdapter } from '../BasePostgresEntityDatabaseAdapter.ts';
import { sql } from '../SQLOperator.ts';
import type { TestFields } from './fixtures/TestEntity.ts';
import { testEntityConfiguration } from './fixtures/TestEntity.ts';
@@ -22,6 +24,12 @@ class TestEntityDatabaseAdapter extends BasePostgresEntityDatabaseAdapter<
private readonly fetchEqualityConditionResults: object[];
private readonly fetchSQLFragmentResults: object[];
private readonly deleteCount: number;
public lastEqualityConditionQuerySelectionModifiers:
| TableQuerySelectionModifiers<TestFields>
| undefined;
public lastSQLFragmentQuerySelectionModifiers:
| TableQuerySelectionModifiers<TestFields>
| undefined;
constructor({
fetchResults = [],
@@ -76,7 +84,9 @@ class TestEntityDatabaseAdapter extends BasePostgresEntityDatabaseAdapter<
_queryInterface: any,
_tableName: string,
_sqlFragment: any,
querySelectionModifiers: TableQuerySelectionModifiers<TestFields>,
): Promise<object[]> {
this.lastSQLFragmentQuerySelectionModifiers = querySelectionModifiers;
return this.fetchSQLFragmentResults;
}
@@ -85,7 +95,9 @@ class TestEntityDatabaseAdapter extends BasePostgresEntityDatabaseAdapter<
_tableName: string,
_tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
_tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
querySelectionModifiers: TableQuerySelectionModifiers<TestFields>,
): Promise<object[]> {
this.lastEqualityConditionQuerySelectionModifiers = querySelectionModifiers;
return this.fetchEqualityConditionResults;
}
@@ -151,5 +163,63 @@ describe(BasePostgresEntityDatabaseAdapter, () => {
const results = await adapter.fetchManyByFieldEqualityConjunctionAsync(queryContext, [], {});
expect(results).toEqual([{ stringField: 'hello' }]);
});
it('converts query selection modifiers including row locking modifiers', async () => {
const queryContext = instance(mock(EntityQueryContext));
const adapter = new TestEntityDatabaseAdapter({});
await adapter.fetchManyByFieldEqualityConjunctionAsync(queryContext, [], {
limit: 2,
offset: 1,
forUpdate: true,
skipLocked: true,
});
expect(adapter.lastEqualityConditionQuerySelectionModifiers).toEqual({
orderBy: undefined,
limit: 2,
offset: 1,
forUpdate: true,
forShare: undefined,
skipLocked: true,
});
await adapter.fetchManyByFieldEqualityConjunctionAsync(queryContext, [], { forShare: true });
expect(adapter.lastEqualityConditionQuerySelectionModifiers).toEqual({
orderBy: undefined,
limit: undefined,
offset: undefined,
forUpdate: undefined,
forShare: true,
skipLocked: undefined,
});
await adapter.fetchManyByFieldEqualityConjunctionAsync(queryContext, [], {});
expect(adapter.lastEqualityConditionQuerySelectionModifiers).toEqual({
orderBy: undefined,
limit: undefined,
offset: undefined,
forUpdate: undefined,
forShare: undefined,
skipLocked: undefined,
});
});
});
describe('fetchManyBySQLFragmentAsync', () => {
it('converts query selection modifiers including row locking modifiers', async () => {
const queryContext = instance(mock(EntityQueryContext));
const adapter = new TestEntityDatabaseAdapter({});
await adapter.fetchManyBySQLFragmentAsync(queryContext, sql`TRUE`, {
forShare: true,
skipLocked: true,
});
expect(adapter.lastSQLFragmentQuerySelectionModifiers).toEqual({
orderBy: undefined,
limit: undefined,
offset: undefined,
forUpdate: undefined,
forShare: true,
skipLocked: true,
});
});
});
});
@@ -2,7 +2,7 @@ import type { IEntityMetricsAdapter } from '@expo/entity';
import { EntityConstructionUtils, EntityQueryContext } from '@expo/entity';
import { result } from '@expo/results';
import { describe, expect, it } from '@jest/globals';
import { anything, instance, mock, when } from 'ts-mockito';
import { anything, deepEqual, instance, mock, when } from 'ts-mockito';
import {
AuthorizationResultBasedKnexEntityLoader,
@@ -198,6 +198,294 @@ describe(EnforcingKnexEntityLoader, () => {
});
});
describe('FromDatabase load methods', () => {
const makeLoader = (
nonEnforcingKnexEntityLoaderMock: AuthorizationResultBasedKnexEntityLoader<
any,
any,
any,
any,
any,
any
>,
): EnforcingKnexEntityLoader<any, any, any, any, any, any> =>
new EnforcingKnexEntityLoader(
instance(nonEnforcingKnexEntityLoaderMock),
instance(mock(EntityQueryContext)),
instance(mock(EntityKnexDataManager)),
instance(mock<IEntityMetricsAdapter>()),
instance(mock(EntityConstructionUtils)),
);
describe('loadByIDFromDatabaseAsync', () => {
it('throws when result is unsuccessful', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
const rejection = new Error();
when(
nonEnforcingKnexEntityLoaderMock.loadByIDFromDatabaseAsync(anything(), anything()),
).thenResolve(result(rejection));
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadByIDFromDatabaseAsync('id', { forUpdate: true }),
).rejects.toThrow(rejection);
});
it('returns value when result is successful', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
const resolved = {};
when(
nonEnforcingKnexEntityLoaderMock.loadByIDFromDatabaseAsync(
'id',
deepEqual({ forUpdate: true }),
),
).thenResolve(result(resolved));
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadByIDFromDatabaseAsync('id', { forUpdate: true }),
).resolves.toEqual(resolved);
});
});
describe('loadByIDNullableFromDatabaseAsync', () => {
it('throws when result is unsuccessful', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
const rejection = new Error();
when(
nonEnforcingKnexEntityLoaderMock.loadByIDNullableFromDatabaseAsync(
anything(),
anything(),
),
).thenResolve(result(rejection));
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadByIDNullableFromDatabaseAsync('id', { forUpdate: true }),
).rejects.toThrow(rejection);
});
it('returns value when result is successful', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
const resolved = {};
when(
nonEnforcingKnexEntityLoaderMock.loadByIDNullableFromDatabaseAsync(
anything(),
anything(),
),
).thenResolve(result(resolved));
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadByIDNullableFromDatabaseAsync('id', { forUpdate: true }),
).resolves.toEqual(resolved);
});
it('returns null when result is null', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
when(
nonEnforcingKnexEntityLoaderMock.loadByIDNullableFromDatabaseAsync(
anything(),
anything(),
),
).thenResolve(null);
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadByIDNullableFromDatabaseAsync('id', { forUpdate: true }),
).resolves.toBeNull();
});
});
describe('loadManyByIDsFromDatabaseAsync', () => {
it('throws when result is unsuccessful', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
const rejection = new Error();
when(
nonEnforcingKnexEntityLoaderMock.loadManyByIDsFromDatabaseAsync(anything(), anything()),
).thenResolve(new Map([['id', result(rejection)]]));
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadManyByIDsFromDatabaseAsync(['id'], { forUpdate: true }),
).rejects.toThrow(rejection);
});
it('returns value when result is successful', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
const resolved = {};
when(
nonEnforcingKnexEntityLoaderMock.loadManyByIDsFromDatabaseAsync(anything(), anything()),
).thenResolve(new Map([['id', result(resolved)]]));
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadManyByIDsFromDatabaseAsync(['id'], { forUpdate: true }),
).resolves.toEqual(new Map([['id', resolved]]));
});
});
describe('loadManyByIDsNullableFromDatabaseAsync', () => {
it('throws when result is unsuccessful', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
const rejection = new Error();
when(
nonEnforcingKnexEntityLoaderMock.loadManyByIDsNullableFromDatabaseAsync(
anything(),
anything(),
),
).thenResolve(new Map([['id', result(rejection)]]));
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadManyByIDsNullableFromDatabaseAsync(['id'], {
forUpdate: true,
}),
).rejects.toThrow(rejection);
});
it('returns values and nulls when results are successful', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
const resolved = {};
when(
nonEnforcingKnexEntityLoaderMock.loadManyByIDsNullableFromDatabaseAsync(
anything(),
anything(),
),
).thenResolve(
new Map([
['id', result(resolved)],
['missing', null],
]),
);
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadManyByIDsNullableFromDatabaseAsync(['id', 'missing'], {
forUpdate: true,
}),
).resolves.toEqual(
new Map([
['id', resolved],
['missing', null],
]),
);
});
});
describe('loadByFieldEqualingFromDatabaseAsync', () => {
it('throws when result is unsuccessful', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
const rejection = new Error();
when(
nonEnforcingKnexEntityLoaderMock.loadByFieldEqualingFromDatabaseAsync(
anything(),
anything(),
anything(),
),
).thenResolve(result(rejection));
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadByFieldEqualingFromDatabaseAsync('field', 'value', {
forUpdate: true,
}),
).rejects.toThrow(rejection);
});
it('returns value when result is successful', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
const resolved = {};
when(
nonEnforcingKnexEntityLoaderMock.loadByFieldEqualingFromDatabaseAsync(
anything(),
anything(),
anything(),
),
).thenResolve(result(resolved));
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadByFieldEqualingFromDatabaseAsync('field', 'value', {
forUpdate: true,
}),
).resolves.toEqual(resolved);
});
it('returns null when result is null', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
when(
nonEnforcingKnexEntityLoaderMock.loadByFieldEqualingFromDatabaseAsync(
anything(),
anything(),
anything(),
),
).thenResolve(null);
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadByFieldEqualingFromDatabaseAsync('field', 'value', {
forUpdate: true,
}),
).resolves.toBeNull();
});
});
describe('loadManyByFieldEqualingFromDatabaseAsync', () => {
it('throws when result is unsuccessful', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
const rejection = new Error();
when(
nonEnforcingKnexEntityLoaderMock.loadManyByFieldEqualingFromDatabaseAsync(
anything(),
anything(),
anything(),
),
).thenResolve([result(rejection)]);
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadManyByFieldEqualingFromDatabaseAsync('field', 'value', {
forUpdate: true,
}),
).rejects.toThrow(rejection);
});
it('returns values when results are successful', async () => {
const nonEnforcingKnexEntityLoaderMock = mock(
AuthorizationResultBasedKnexEntityLoader<any, any, any, any, any, any>,
);
const resolved = {};
when(
nonEnforcingKnexEntityLoaderMock.loadManyByFieldEqualingFromDatabaseAsync(
anything(),
anything(),
anything(),
),
).thenResolve([result(resolved)]);
const enforcingKnexEntityLoader = makeLoader(nonEnforcingKnexEntityLoaderMock);
await expect(
enforcingKnexEntityLoader.loadManyByFieldEqualingFromDatabaseAsync('field', 'value', {
forUpdate: true,
}),
).resolves.toEqual([resolved]);
});
});
});
describe('loadPageAsync', () => {
it('throws when result is unsuccessful', async () => {
const queryContext = instance(mock(EntityQueryContext));
@@ -213,7 +213,14 @@ export class StubPostgresDatabaseAdapter<
tableName,
tableFieldSingleValueEqualityOperands,
tableFieldMultiValueEqualityOperands,
{ orderBy: undefined, offset: undefined, limit: undefined },
{
orderBy: undefined,
offset: undefined,
limit: undefined,
forUpdate: undefined,
forShare: undefined,
skipLocked: undefined,
},
);
return results.length;
}
@@ -1,8 +1,26 @@
import { EntityDatabaseAdapterTransientError, EntityErrorState } from '@expo/entity';
import { describe, expect, it } from '@jest/globals';
import { wrapNativePostgresCallAsync } from '../wrapNativePostgresCallAsync.ts';
describe(wrapNativePostgresCallAsync, () => {
it('classifies a PostgreSQL lock timeout as transient and preserves the cause', async () => {
const error = Object.assign(new Error('canceling statement due to lock timeout'), {
code: '55P03',
});
const result = wrapNativePostgresCallAsync(async () => {
throw error;
});
await expect(result).rejects.toBeInstanceOf(EntityDatabaseAdapterTransientError);
await expect(result).rejects.toMatchObject({
state: EntityErrorState.TRANSIENT,
message: error.message,
stack: error.stack,
cause: error,
});
});
it('rethrows literals', async () => {
const throwingFn = async (): Promise<void> => {
// eslint-disable-next-line no-throw-literal,@typescript-eslint/only-throw-error
@@ -38,6 +38,8 @@ function translatePostgresError(
return new EntityDatabaseAdapterCheckConstraintError(error.message, error);
case '23P01':
return new EntityDatabaseAdapterExclusionConstraintError(error.message, error);
case '55P03':
return new EntityDatabaseAdapterTransientError(error.message, error);
default:
return new EntityDatabaseAdapterUnknownError(error.message, error);
}
@@ -1,6 +1,7 @@
import type { EntityConfiguration, EntityQueryContext, IEntityMetricsAdapter } from '@expo/entity';
import {
EntityDatabaseAdapterPaginationCursorInvalidError,
EntityMetricsCountType,
EntityMetricsLoadType,
getDatabaseFieldForEntityField,
timeAndLogCountEventAsync,
@@ -176,6 +177,7 @@ export class EntityKnexDataManager<
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
): Promise<readonly Readonly<TFields>[]> {
EntityKnexDataManager.validateOrderByClauses(querySelectionModifiers.orderBy);
EntityKnexDataManager.validateRowLockingModifiers(queryContext, querySelectionModifiers);
return await timeAndLogLoadEventAsync(
this.metricsAdapter,
@@ -197,7 +199,7 @@ export class EntityKnexDataManager<
): Promise<number> {
return await timeAndLogCountEventAsync(
this.metricsAdapter,
EntityMetricsLoadType.COUNT_EQUALITY_CONJUNCTION,
EntityMetricsCountType.COUNT_EQUALITY_CONJUNCTION,
this.entityClassName,
queryContext,
)(
@@ -214,6 +216,7 @@ export class EntityKnexDataManager<
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
): Promise<readonly Readonly<TFields>[]> {
EntityKnexDataManager.validateOrderByClauses(querySelectionModifiers.orderBy);
EntityKnexDataManager.validateRowLockingModifiers(queryContext, querySelectionModifiers);
return await timeAndLogLoadEventAsync(
this.metricsAdapter,
@@ -235,7 +238,7 @@ export class EntityKnexDataManager<
): Promise<number> {
return await timeAndLogCountEventAsync(
this.metricsAdapter,
EntityMetricsLoadType.COUNT_SQL,
EntityMetricsCountType.COUNT_SQL,
this.entityClassName,
queryContext,
)(this.databaseAdapter.countBySQLFragmentAsync(queryContext, sqlFragment));
@@ -290,18 +293,16 @@ export class EntityKnexDataManager<
// 3. Reverse the final array to present items in name ASC order
return direction === PaginationDirection.FORWARD
? augmentedOrderByClauses
: augmentedOrderByClauses.map(
(clause): PostgresOrderByClause<TFields> => ({
...clause,
order:
clause.order === OrderByOrdering.ASCENDING
? OrderByOrdering.DESCENDING
: OrderByOrdering.ASCENDING,
nulls: clause.nulls
? EntityKnexDataManager.flipNullsOrderingSpread(clause.nulls)
: undefined,
}),
);
: augmentedOrderByClauses.map((clause): PostgresOrderByClause<TFields> => ({
...clause,
order:
clause.order === OrderByOrdering.ASCENDING
? OrderByOrdering.DESCENDING
: OrderByOrdering.ASCENDING,
nulls: clause.nulls
? EntityKnexDataManager.flipNullsOrderingSpread(clause.nulls)
: undefined,
}));
},
buildCursorCondition: (decodedCursorId, _direction, orderByClauses) => {
// all clauses are guaranteed to have the same order due to validation, so we can just look at the first one for effective ordering
@@ -532,6 +533,30 @@ export class EntityKnexDataManager<
}
}
/**
* Validates row locking modifiers (forUpdate, forShare, skipLocked).
*
* `SELECT ... FOR UPDATE` and `SELECT ... FOR SHARE` row locks are released at the end of the
* transaction. Outside of a transaction the lock is released as soon as the statement completes,
* which makes it useless, so require a transactional query context. FOR UPDATE and FOR SHARE
* cannot be combined in a single statement, and SKIP LOCKED is only valid with a row lock.
*/
private static validateRowLockingModifiers<TFields extends Record<string, any>>(
queryContext: EntityQueryContext,
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
): void {
const { forUpdate, forShare, skipLocked } = querySelectionModifiers;
if (!forUpdate && !forShare) {
assert(!skipLocked, 'skipLocked requires forUpdate or forShare.');
return;
}
assert(!(forUpdate && forShare), 'forUpdate and forShare are mutually exclusive.');
assert(
queryContext.isInTransaction(),
'forUpdate and forShare require a transactional query context since row locks are released at the end of the transaction.',
);
}
/**
* Cursor-based pagination uses Postgres tuple comparison (e.g., (a, b) \> (x, y)) which
* applies a single comparison direction to all columns. Mixed ordering directions would
@@ -757,12 +782,10 @@ export class EntityKnexDataManager<
// Order by search fields + ID to match cursor fields
const searchOrderByClauses: PostgresOrderByClause<TFields>[] = [
...search.fields.map(
(field): PostgresOrderByClause<TFields> => ({
fieldFragment: this.resolveSearchFieldToSQLFragment(field),
order: OrderByOrdering.ASCENDING,
}),
),
...search.fields.map((field): PostgresOrderByClause<TFields> => ({
fieldFragment: this.resolveSearchFieldToSQLFragment(field),
order: OrderByOrdering.ASCENDING,
})),
{
fieldName: this.entityConfiguration.idField,
order: OrderByOrdering.ASCENDING,
@@ -7,6 +7,7 @@ import { anyNumber, anything, deepEqual, instance, mock, verify, when } from 'ts
import { OrderByOrdering } from '../../BasePostgresEntityDatabaseAdapter.ts';
import { PaginationStrategy } from '../../PaginationStrategy.ts';
import { PostgresEntityDatabaseAdapter } from '../../PostgresEntityDatabaseAdapter.ts';
import { sql } from '../../SQLOperator.ts';
import type { TestFields } from '../../__tests__/fixtures/TestEntity.ts';
import { TestEntity, testEntityConfiguration } from '../../__tests__/fixtures/TestEntity.ts';
import { EntityKnexDataManager } from '../EntityKnexDataManager.ts';
@@ -193,6 +194,279 @@ describe(EntityKnexDataManager, () => {
});
});
describe('row locking modifiers', () => {
const fieldObject = {
customIdField: '1',
testIndexedField: 'unique1',
stringField: 'hello',
intField: 1,
dateField: new Date(),
nullableField: null,
};
it('throws when loading by field equality conjunction outside of a transaction', async () => {
const queryContext = new StubQueryContextProvider().getQueryContext();
const databaseAdapterMock = mock<PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>>(
PostgresEntityDatabaseAdapter,
);
const entityDataManager = new EntityKnexDataManager(
testEntityConfiguration,
instance(databaseAdapterMock),
new NoOpEntityMetricsAdapter(),
TestEntity.name,
);
await expect(
entityDataManager.loadManyByFieldEqualityConjunctionAsync(queryContext, [], {
forUpdate: true,
}),
).rejects.toThrow('require a transactional query context');
verify(
databaseAdapterMock.fetchManyByFieldEqualityConjunctionAsync(
anything(),
anything(),
anything(),
),
).never();
});
it('throws when loading by SQL fragment outside of a transaction', async () => {
const queryContext = new StubQueryContextProvider().getQueryContext();
const databaseAdapterMock = mock<PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>>(
PostgresEntityDatabaseAdapter,
);
const entityDataManager = new EntityKnexDataManager(
testEntityConfiguration,
instance(databaseAdapterMock),
new NoOpEntityMetricsAdapter(),
TestEntity.name,
);
await expect(
entityDataManager.loadManyBySQLFragmentAsync(queryContext, sql`TRUE`, {
forUpdate: true,
}),
).rejects.toThrow('require a transactional query context');
verify(
databaseAdapterMock.fetchManyBySQLFragmentAsync(anything(), anything(), anything()),
).never();
});
it('passes forUpdate through to the database adapter inside of a transaction', async () => {
const databaseAdapterMock = mock<PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>>(
PostgresEntityDatabaseAdapter,
);
when(
databaseAdapterMock.fetchManyByFieldEqualityConjunctionAsync(
anything(),
anything(),
anything(),
),
).thenResolve([fieldObject]);
when(
databaseAdapterMock.fetchManyBySQLFragmentAsync(anything(), anything(), anything()),
).thenResolve([fieldObject]);
const entityDataManager = new EntityKnexDataManager(
testEntityConfiguration,
instance(databaseAdapterMock),
new NoOpEntityMetricsAdapter(),
TestEntity.name,
);
await new StubQueryContextProvider().runInTransactionAsync(async (queryContext) => {
const equalityResults = await entityDataManager.loadManyByFieldEqualityConjunctionAsync(
queryContext,
[{ fieldName: 'stringField', fieldValue: 'hello' }],
{ forUpdate: true, limit: 1 },
);
expect(equalityResults).toHaveLength(1);
const sqlResults = await entityDataManager.loadManyBySQLFragmentAsync(
queryContext,
sql`TRUE`,
{ forUpdate: true },
);
expect(sqlResults).toHaveLength(1);
verify(
databaseAdapterMock.fetchManyByFieldEqualityConjunctionAsync(
queryContext,
anything(),
deepEqual({ forUpdate: true, limit: 1 }),
),
).once();
verify(
databaseAdapterMock.fetchManyBySQLFragmentAsync(
queryContext,
anything(),
deepEqual({ forUpdate: true }),
),
).once();
});
});
it('throws when forShare is used outside of a transaction', async () => {
const queryContext = new StubQueryContextProvider().getQueryContext();
const databaseAdapterMock = mock<PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>>(
PostgresEntityDatabaseAdapter,
);
const entityDataManager = new EntityKnexDataManager(
testEntityConfiguration,
instance(databaseAdapterMock),
new NoOpEntityMetricsAdapter(),
TestEntity.name,
);
await expect(
entityDataManager.loadManyByFieldEqualityConjunctionAsync(queryContext, [], {
forShare: true,
}),
).rejects.toThrow('require a transactional query context');
await expect(
entityDataManager.loadManyBySQLFragmentAsync(queryContext, sql`TRUE`, { forShare: true }),
).rejects.toThrow('require a transactional query context');
});
it('throws when forUpdate and forShare are both set', async () => {
const databaseAdapterMock = mock<PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>>(
PostgresEntityDatabaseAdapter,
);
const entityDataManager = new EntityKnexDataManager(
testEntityConfiguration,
instance(databaseAdapterMock),
new NoOpEntityMetricsAdapter(),
TestEntity.name,
);
await new StubQueryContextProvider().runInTransactionAsync(async (queryContext) => {
await expect(
entityDataManager.loadManyByFieldEqualityConjunctionAsync(queryContext, [], {
forUpdate: true,
forShare: true,
}),
).rejects.toThrow('forUpdate and forShare are mutually exclusive');
await expect(
entityDataManager.loadManyBySQLFragmentAsync(queryContext, sql`TRUE`, {
forUpdate: true,
forShare: true,
}),
).rejects.toThrow('forUpdate and forShare are mutually exclusive');
});
verify(
databaseAdapterMock.fetchManyByFieldEqualityConjunctionAsync(
anything(),
anything(),
anything(),
),
).never();
verify(
databaseAdapterMock.fetchManyBySQLFragmentAsync(anything(), anything(), anything()),
).never();
});
it('throws when skipLocked is set without forUpdate or forShare', async () => {
const databaseAdapterMock = mock<PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>>(
PostgresEntityDatabaseAdapter,
);
const entityDataManager = new EntityKnexDataManager(
testEntityConfiguration,
instance(databaseAdapterMock),
new NoOpEntityMetricsAdapter(),
TestEntity.name,
);
await new StubQueryContextProvider().runInTransactionAsync(async (queryContext) => {
await expect(
entityDataManager.loadManyByFieldEqualityConjunctionAsync(queryContext, [], {
skipLocked: true,
}),
).rejects.toThrow('skipLocked requires forUpdate or forShare');
await expect(
entityDataManager.loadManyBySQLFragmentAsync(queryContext, sql`TRUE`, {
skipLocked: true,
}),
).rejects.toThrow('skipLocked requires forUpdate or forShare');
});
});
it('passes forShare and skipLocked through to the database adapter inside of a transaction', async () => {
const databaseAdapterMock = mock<PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>>(
PostgresEntityDatabaseAdapter,
);
when(
databaseAdapterMock.fetchManyByFieldEqualityConjunctionAsync(
anything(),
anything(),
anything(),
),
).thenResolve([fieldObject]);
when(
databaseAdapterMock.fetchManyBySQLFragmentAsync(anything(), anything(), anything()),
).thenResolve([fieldObject]);
const entityDataManager = new EntityKnexDataManager(
testEntityConfiguration,
instance(databaseAdapterMock),
new NoOpEntityMetricsAdapter(),
TestEntity.name,
);
await new StubQueryContextProvider().runInTransactionAsync(async (queryContext) => {
await entityDataManager.loadManyByFieldEqualityConjunctionAsync(queryContext, [], {
forShare: true,
skipLocked: true,
});
await entityDataManager.loadManyBySQLFragmentAsync(queryContext, sql`TRUE`, {
forUpdate: true,
skipLocked: true,
});
verify(
databaseAdapterMock.fetchManyByFieldEqualityConjunctionAsync(
queryContext,
anything(),
deepEqual({ forShare: true, skipLocked: true }),
),
).once();
verify(
databaseAdapterMock.fetchManyBySQLFragmentAsync(
queryContext,
anything(),
deepEqual({ forUpdate: true, skipLocked: true }),
),
).once();
});
});
it('does not require a transaction when no row locking modifier is set', async () => {
const queryContext = new StubQueryContextProvider().getQueryContext();
const databaseAdapterMock = mock<PostgresEntityDatabaseAdapter<TestFields, 'customIdField'>>(
PostgresEntityDatabaseAdapter,
);
when(
databaseAdapterMock.fetchManyByFieldEqualityConjunctionAsync(
anything(),
anything(),
anything(),
),
).thenResolve([fieldObject]);
const entityDataManager = new EntityKnexDataManager(
testEntityConfiguration,
instance(databaseAdapterMock),
new NoOpEntityMetricsAdapter(),
TestEntity.name,
);
const results = await entityDataManager.loadManyByFieldEqualityConjunctionAsync(
queryContext,
[],
{ forUpdate: false, forShare: false, skipLocked: false },
);
expect(results).toHaveLength(1);
});
});
describe('pagination', () => {
describe('max page size validation', () => {
it('should throw when first exceeds maxPageSize', async () => {
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-example
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
**Note:** Version bump only for package @expo/entity-example
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-example
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-example
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
### Bug Fixes
- **deps:** update dependency koa to v3.2.1 ([#634](https://github.com/expo/entity/issues/634)) ([1c17dc0](https://github.com/expo/entity/commit/1c17dc0f37ec68fb73aae1f92b3ce2c79062b93e))
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-example
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-example",
"version": "0.64.0",
"version": "0.69.0",
"private": true,
"description": "An example integration of the @expo/entity framework",
"keywords": [
@@ -27,14 +27,14 @@
"@koa/cors": "5.0.0",
"@koa/router": "15.4.0",
"graphql": "16.13.2",
"koa": "3.2.0"
"koa": "3.2.1"
},
"devDependencies": {
"@jest/globals": "30.3.0",
"@types/koa": "3.0.2",
"@jest/globals": "30.5.1",
"@types/koa": "3.0.3",
"@types/koa__cors": "5.0.1",
"@types/koa__router": "12.0.5",
"@types/supertest": "7.2.0",
"@types/supertest": "7.2.1",
"supertest": "7.2.2",
"typescript": "6.0.3"
},
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-full-integration-tests
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
**Note:** Version bump only for package @expo/entity-full-integration-tests
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-full-integration-tests
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-full-integration-tests
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-full-integration-tests
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-full-integration-tests
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-full-integration-tests",
"version": "0.64.0",
"version": "0.69.0",
"private": true,
"description": "Full redis and knex integration tests for the entity framework",
"keywords": [
@@ -22,7 +22,7 @@
"@expo/entity-database-adapter-knex": "workspace:^"
},
"devDependencies": {
"@jest/globals": "30.3.0",
"@jest/globals": "30.5.1",
"typescript": "6.0.3"
},
"engines": {
@@ -1 +1,2 @@
// placeholder file to fix tsc
// oxlint-disable unicorn/no-empty-file
// placeholder file to fix tsc/typedoc
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-ip-address-field
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
**Note:** Version bump only for package @expo/entity-ip-address-field
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-ip-address-field
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-ip-address-field
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-ip-address-field
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-ip-address-field
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-ip-address-field",
"version": "0.64.0",
"version": "0.69.0",
"description": "IP address EntityField definitions for @expo/entity",
"keywords": [
"entity"
@@ -30,7 +30,7 @@
},
"devDependencies": {
"@expo/entity-testing-utils": "workspace:^",
"@jest/globals": "30.3.0",
"@jest/globals": "30.5.1",
"typescript": "6.0.3"
},
"engines": {
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-secondary-cache-local-memory
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
**Note:** Version bump only for package @expo/entity-secondary-cache-local-memory
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-secondary-cache-local-memory
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-secondary-cache-local-memory
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-secondary-cache-local-memory
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-secondary-cache-local-memory
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-secondary-cache-local-memory",
"version": "0.64.0",
"version": "0.69.0",
"description": "Local memory secondary cache for @expo/entity",
"keywords": [
"entity"
@@ -31,8 +31,8 @@
},
"devDependencies": {
"@expo/nullthrows": "2.1.0",
"@isaacs/ttlcache": "2.1.4",
"@jest/globals": "30.3.0",
"@isaacs/ttlcache": "2.1.5",
"@jest/globals": "30.5.1",
"typescript": "6.0.3"
},
"engines": {
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-secondary-cache-redis",
"version": "0.64.0",
"version": "0.69.0",
"description": "Redis secondary cache for @expo/entity",
"keywords": [
"entity"
@@ -30,8 +30,8 @@
},
"devDependencies": {
"@expo/nullthrows": "2.1.0",
"@jest/globals": "30.3.0",
"ioredis": "5.10.1",
"@jest/globals": "30.5.1",
"ioredis": "5.11.1",
"typescript": "6.0.3"
},
"engines": {
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity-testing-utils
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
**Note:** Version bump only for package @expo/entity-testing-utils
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity-testing-utils
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity-testing-utils
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
**Note:** Version bump only for package @expo/entity-testing-utils
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
**Note:** Version bump only for package @expo/entity-testing-utils
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-testing-utils",
"version": "0.64.0",
"version": "0.69.0",
"description": "A package containing utilities for testing applications that use Entity",
"keywords": [
"entity"
@@ -30,9 +30,9 @@
"ts-mockito": "^2.6.1"
},
"devDependencies": {
"@jest/globals": "30.3.0",
"@jest/globals": "30.5.1",
"@types/lodash-es": "4.17.12",
"@types/node": "24.12.4",
"@types/node": "24.19.1",
"typescript": "6.0.3"
},
"peerDependencies": {
@@ -5,7 +5,7 @@ import type {
PrivacyPolicyRule,
EntityPrivacyPolicyRuleEvaluationContext,
} from '@expo/entity';
import { RuleEvaluationResult } from '@expo/entity';
import { RuleEvaluationResult, normalizeRuleEvaluationOutcome } from '@expo/entity';
import { describe, expect, test } from '@jest/globals';
export interface Case<
@@ -25,6 +25,12 @@ export interface Case<
TSelectedFields
>;
entity: TEntity;
/**
* For skip and deny cases: the exact reason codes the rule must attach to its result, in order.
* Omit to assert only the result. An empty array asserts that no reasons were attached.
* Ignored for allow cases.
*/
expectedReasons?: readonly string[];
}
export type CaseMap<
@@ -62,9 +68,15 @@ export const describePrivacyPolicyRuleWithAsyncTestCase = <
test.each(Array.from(allowCases.keys()))('%p', async (caseKey) => {
const { viewerContext, queryContext, evaluationContext, entity } =
await allowCases.get(caseKey)!();
await expect(
privacyPolicyRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
).resolves.toEqual(RuleEvaluationResult.ALLOW);
const outcome = await privacyPolicyRule.evaluateAsync(
viewerContext,
queryContext,
evaluationContext,
entity,
);
expect(normalizeRuleEvaluationOutcome(outcome).result).toEqual(
RuleEvaluationResult.ALLOW,
);
});
});
}
@@ -72,11 +84,20 @@ export const describePrivacyPolicyRuleWithAsyncTestCase = <
if (skipCases && skipCases.size > 0) {
describe('skip cases', () => {
test.each(Array.from(skipCases.keys()))('%p', async (caseKey) => {
const { viewerContext, queryContext, evaluationContext, entity } =
const { viewerContext, queryContext, evaluationContext, entity, expectedReasons } =
await skipCases.get(caseKey)!();
await expect(
privacyPolicyRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
).resolves.toEqual(RuleEvaluationResult.SKIP);
const outcome = normalizeRuleEvaluationOutcome(
await privacyPolicyRule.evaluateAsync(
viewerContext,
queryContext,
evaluationContext,
entity,
),
);
expect(outcome.result).toEqual(RuleEvaluationResult.SKIP);
if (expectedReasons !== undefined) {
expect(outcome.reasons).toEqual(expectedReasons);
}
});
});
}
@@ -84,11 +105,20 @@ export const describePrivacyPolicyRuleWithAsyncTestCase = <
if (denyCases && denyCases.size > 0) {
describe('deny cases', () => {
test.each(Array.from(denyCases.keys()))('%p', async (caseKey) => {
const { viewerContext, queryContext, evaluationContext, entity } =
const { viewerContext, queryContext, evaluationContext, entity, expectedReasons } =
await denyCases.get(caseKey)!();
await expect(
privacyPolicyRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
).resolves.toEqual(RuleEvaluationResult.DENY);
const outcome = normalizeRuleEvaluationOutcome(
await privacyPolicyRule.evaluateAsync(
viewerContext,
queryContext,
evaluationContext,
entity,
),
);
expect(outcome.result).toEqual(RuleEvaluationResult.DENY);
if (expectedReasons !== undefined) {
expect(outcome.reasons).toEqual(expectedReasons);
}
});
});
}
@@ -1,14 +1,62 @@
import type { EntityPrivacyPolicyRuleEvaluationContext } from '@expo/entity';
import type { EntityPrivacyPolicyRuleEvaluationContext, RuleEvaluationOutcome } from '@expo/entity';
import {
AlwaysAllowPrivacyPolicyRule,
AlwaysDenyPrivacyPolicyRule,
AlwaysSkipPrivacyPolicyRule,
EntityQueryContext,
PrivacyPolicyRule,
ViewerContext,
denyWithReasons,
skipWithReasons,
} from '@expo/entity';
import { describe } from '@jest/globals';
import { anything, instance, mock } from 'ts-mockito';
import { describePrivacyPolicyRuleWithAsyncTestCase } from '../PrivacyPolicyRuleTestUtils.ts';
import {
describePrivacyPolicyRule,
describePrivacyPolicyRuleWithAsyncTestCase,
} from '../PrivacyPolicyRuleTestUtils.ts';
class SkipWithReasonsRule extends PrivacyPolicyRule<any, any, any, any, any> {
async evaluateAsync(): Promise<RuleEvaluationOutcome> {
return skipWithReasons('NOT_OWNER', 'NOT_MEMBER');
}
}
class DenyWithReasonsRule extends PrivacyPolicyRule<any, any, any, any, any> {
async evaluateAsync(): Promise<RuleEvaluationOutcome> {
return denyWithReasons('BLOCKED');
}
}
const makeCase = (expectedReasons?: readonly string[]): any => ({
viewerContext: instance(mock(ViewerContext)),
queryContext: instance(mock(EntityQueryContext)),
evaluationContext:
instance(mock<EntityPrivacyPolicyRuleEvaluationContext<any, any, any, any, any>>()),
entity: anything(),
expectedReasons,
});
describe(describePrivacyPolicyRule, () => {
describe('expectedReasons', () => {
describePrivacyPolicyRule(new SkipWithReasonsRule(), {
skipCases: [makeCase(), makeCase(['NOT_OWNER', 'NOT_MEMBER'])],
});
describePrivacyPolicyRule(new DenyWithReasonsRule(), {
denyCases: [makeCase(), makeCase(['BLOCKED'])],
});
describePrivacyPolicyRule(new AlwaysSkipPrivacyPolicyRule(), {
skipCases: [makeCase([])],
});
describePrivacyPolicyRule(new AlwaysDenyPrivacyPolicyRule(), {
denyCases: [makeCase([])],
});
});
});
describe(describePrivacyPolicyRuleWithAsyncTestCase, () => {
describe('default args do not execute', () => {
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.69.0](https://github.com/expo/entity/compare/v0.68.0...v0.69.0) (2026-09-29)
**Note:** Version bump only for package @expo/entity
# [0.68.0](https://github.com/expo/entity/compare/v0.67.0...v0.68.0) (2026-09-25)
**Note:** Version bump only for package @expo/entity
# [0.67.0](https://github.com/expo/entity/compare/v0.66.0...v0.67.0) (2026-09-18)
**Note:** Version bump only for package @expo/entity
# [0.66.0](https://github.com/expo/entity/compare/v0.65.0...v0.66.0) (2026-08-03)
**Note:** Version bump only for package @expo/entity
# [0.65.0](https://github.com/expo/entity/compare/v0.64.0...v0.65.0) (2026-07-10)
### Features
- accept array of parent entity classes in cascade deletion rule ([#650](https://github.com/expo/entity/issues/650)) ([df75910](https://github.com/expo/entity/commit/df75910cbdc89df60ed2f23dbedf8862ed5c9077))
# [0.62.0](https://github.com/expo/entity/compare/v0.61.0...v0.62.0) (2026-03-16)
### Bug Fixes
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/entity",
"version": "0.64.0",
"version": "0.69.0",
"description": "A privacy-first data model",
"keywords": [
"entity"
@@ -30,14 +30,14 @@
"invariant": "^2.2.4"
},
"devDependencies": {
"@jest/globals": "30.3.0",
"@jest/globals": "30.5.1",
"@types/invariant": "2.2.37",
"@types/lodash-es": "4.17.12",
"@types/node": "24.12.4",
"@types/node": "24.19.1",
"lodash-es": "4.18.1",
"ts-mockito": "2.6.1",
"typescript": "6.0.3",
"uuid": "14.0.0"
"uuid": "14.0.2"
},
"engines": {
"node": ">=18"
@@ -75,9 +75,7 @@ export class AuthorizationResultBasedEntityAssociationLoader<
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getLoaderFactory()
.forLoad(this.queryContext, { previousValue: null, cascadingDeleteCause: null });
return (await loader.loadByIDAsync(associatedEntityID)) as Result<
null extends TFields[TIdentifyingField] ? TAssociatedEntity | null : TAssociatedEntity
>;
return await loader.loadByIDAsync(associatedEntityID);
}
/**
@@ -247,10 +247,7 @@ export class AuthorizationResultBasedEntityLoader<
* @returns entity result for matching ID, or null if no entity exists for ID.
*/
async loadByIDNullableAsync(id: TFields[TIDField]): Promise<Result<TEntity> | null> {
return await this.loadByFieldEqualingAsync(
this.entityConfiguration.idField as TSelectedFields,
id,
);
return await this.loadByFieldEqualingAsync(this.entityConfiguration.idField, id);
}
/**
@@ -261,7 +258,7 @@ export class AuthorizationResultBasedEntityLoader<
ids: readonly TFields[TIDField][],
): Promise<ReadonlyMap<TFields[TIDField], Result<TEntity>>> {
const entityResults = (await this.loadManyByFieldEqualingManyAsync(
this.entityConfiguration.idField as TSelectedFields,
this.entityConfiguration.idField,
ids,
)) as ReadonlyMap<TFields[TIDField], readonly Result<TEntity>[]>;
return mapMap(entityResults, (entityResultsForId, id) => {
@@ -287,7 +284,7 @@ export class AuthorizationResultBasedEntityLoader<
ids: readonly TFields[TIDField][],
): Promise<ReadonlyMap<TFields[TIDField], Result<TEntity> | null>> {
const entityResults = (await this.loadManyByFieldEqualingManyAsync(
this.entityConfiguration.idField as TSelectedFields,
this.entityConfiguration.idField,
ids,
)) as ReadonlyMap<TFields[TIDField], readonly Result<TEntity>[]>;
return mapMap(entityResults, (entityResultsForId) => {
@@ -73,7 +73,7 @@ export class EntityConstructionUtils<
const selectedFields = pick(fieldsObject, entitySelectedFields);
return new this.entityClass({
viewerContext: this.viewerContext,
id: id as TFields[TIDField],
id,
databaseFields: fieldsObject,
selectedFields,
});
+1 -1
View File
@@ -109,7 +109,7 @@ export abstract class EntityDatabaseAdapter<
*
* @param queryContext - query context with which to perform the fetch
* @param key - load key being queried
* @param values - load value being queried
* @param value - load value being queried
* @returns object that matches the query for the value
*/
async fetchOneWhereAsync<
+4 -1
View File
@@ -34,7 +34,10 @@ export type EntityCascadingDeletionInfo = {
cascadingDeleteCause: EntityCascadingDeletionInfo | null;
};
type EntityTriggerOrValidatorMutationInfo<
/**
* Information about the mutation that invoked a trigger or validator.
*/
export type EntityTriggerOrValidatorMutationInfo<
TFields extends Record<string, any>,
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
TViewerContext extends ViewerContext,
+74 -21
View File
@@ -2,11 +2,19 @@ import type { EntityCascadingDeletionInfo } from './EntityMutationInfo.ts';
import type { EntityQueryContext } from './EntityQueryContext.ts';
import type { ReadonlyEntity } from './ReadonlyEntity.ts';
import type { ViewerContext } from './ViewerContext.ts';
import { EntityNotAuthorizedError } from './errors/EntityNotAuthorizedError.ts';
import type {
EntityNotAuthorizedDenialReason,
EntityNotAuthorizedRuleInfo,
EntityNotAuthorizedUserFacingReason,
} from './errors/EntityNotAuthorizedError.ts';
import {
EntityNotAuthorizedDenialType,
EntityNotAuthorizedError,
} from './errors/EntityNotAuthorizedError.ts';
import type { IEntityMetricsAdapter } from './metrics/IEntityMetricsAdapter.ts';
import { EntityMetricsAuthorizationResult } from './metrics/IEntityMetricsAdapter.ts';
import type { PrivacyPolicyRule } from './rules/PrivacyPolicyRule.ts';
import { RuleEvaluationResult } from './rules/PrivacyPolicyRule.ts';
import { RuleEvaluationResult, normalizeRuleEvaluationOutcome } from './rules/PrivacyPolicyRule.ts';
/**
* Information about the reason this privacy policy is being evaluated.
@@ -204,6 +212,35 @@ export abstract class EntityPrivacyPolicy<
};
}
/**
* Produce an end-user-safe explanation for a denial of this policy. The result is attached to the
* thrown EntityNotAuthorizedError as `userFacingReason`.
*
* @remarks
*
* The default returns null, meaning no user-facing explanation is available and callers should
* display a generic message. Override to map denials to messages appropriate for end users.
* When all rules skip, `denialReason.skippedRules` holds each skipped rule with the reason codes it
* contributed, so the policy can select a message for a specific reason, for example "You must be a
* member of this account to view this project." when a rule skipped with a NOT_ACCOUNT_MEMBER reason.
*
* The denied entity and the evaluation context are intentionally not passed to this method. The viewer
* is not authorized to see the entity, and the evaluation context may contain it (as the previous value
* or the cascading deletion cause), so neither may influence or appear in the user-facing message.
* Rules should instead communicate what is needed through reason codes.
*
* @param _viewerContext - viewer context that was denied
* @param _action - the action that was denied
* @param _denialReason - structured, developer-facing reason for the denial
*/
protected getUserFacingDenialReason(
_viewerContext: TViewerContext,
_action: EntityAuthorizationAction,
_denialReason: EntityNotAuthorizedDenialReason,
): EntityNotAuthorizedUserFacingReason | null {
return null;
}
/**
* Authorize an entity against creation policy.
* @param viewerContext - viewer context of user creating the entity
@@ -473,24 +510,26 @@ export abstract class EntityPrivacyPolicy<
entity: TEntity,
action: EntityAuthorizationAction,
): Promise<TEntity> {
const skippedRules: EntityNotAuthorizedRuleInfo[] = [];
for (let i = 0; i < ruleset.length; i++) {
const rule = ruleset[i]!;
const ruleEvaluationResult = await rule.evaluateAsync(
viewerContext,
queryContext,
{ ...evaluationContext, action },
entity,
const { result, reasons } = normalizeRuleEvaluationOutcome(
await rule.evaluateAsync(
viewerContext,
queryContext,
{ ...evaluationContext, action },
entity,
),
);
switch (ruleEvaluationResult) {
switch (result) {
case RuleEvaluationResult.DENY:
throw new EntityNotAuthorizedError<
TFields,
TIDField,
TViewerContext,
TEntity,
TSelectedFields
>(entity, viewerContext, action, i);
throw this.createNotAuthorizedError(viewerContext, entity, action, {
type: EntityNotAuthorizedDenialType.RULE_DENIED,
rule: { ruleIndex: i, ruleName: rule.constructor.name, reasons },
skippedRules,
});
case RuleEvaluationResult.SKIP:
skippedRules.push({ ruleIndex: i, ruleName: rule.constructor.name, reasons });
continue;
case RuleEvaluationResult.ALLOW:
return entity;
@@ -501,11 +540,25 @@ export abstract class EntityPrivacyPolicy<
}
}
throw new EntityNotAuthorizedError<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>(
entity,
viewerContext,
action,
-1,
);
throw this.createNotAuthorizedError(viewerContext, entity, action, {
type: EntityNotAuthorizedDenialType.ALL_RULES_SKIPPED,
skippedRules,
});
}
private createNotAuthorizedError(
viewerContext: TViewerContext,
entity: TEntity,
action: EntityAuthorizationAction,
denialReason: EntityNotAuthorizedDenialReason,
): EntityNotAuthorizedError<TFields, TIDField, TViewerContext, TEntity, TSelectedFields> {
const userFacingReason = this.getUserFacingDenialReason(viewerContext, action, denialReason);
return new EntityNotAuthorizedError<
TFields,
TIDField,
TViewerContext,
TEntity,
TSelectedFields
>(entity, viewerContext, action, denialReason, userFacingReason);
}
}
@@ -17,13 +17,29 @@ import {
} from '../EntityPrivacyPolicy.ts';
import { EntityQueryContext } from '../EntityQueryContext.ts';
import { ViewerContext } from '../ViewerContext.ts';
import { EntityNotAuthorizedError } from '../errors/EntityNotAuthorizedError.ts';
import type {
EntityNotAuthorizedDenialReason,
EntityNotAuthorizedUserFacingReason,
} from '../errors/EntityNotAuthorizedError.ts';
import {
EntityNotAuthorizedDenialType,
EntityNotAuthorizedError,
} from '../errors/EntityNotAuthorizedError.ts';
import type { IEntityMetricsAdapter } from '../metrics/IEntityMetricsAdapter.ts';
import { EntityMetricsAuthorizationResult } from '../metrics/IEntityMetricsAdapter.ts';
import { AllowIfAllSubRulesAllowPrivacyPolicyRule } from '../rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.ts';
import { AllowIfAnySubRuleAllowsPrivacyPolicyRule } from '../rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.ts';
import { AlwaysAllowPrivacyPolicyRule } from '../rules/AlwaysAllowPrivacyPolicyRule.ts';
import { AlwaysDenyPrivacyPolicyRule } from '../rules/AlwaysDenyPrivacyPolicyRule.ts';
import { AlwaysSkipPrivacyPolicyRule } from '../rules/AlwaysSkipPrivacyPolicyRule.ts';
import { PrivacyPolicyRule, RuleEvaluationResult } from '../rules/PrivacyPolicyRule.ts';
import { EvaluateIfEntityFieldPredicatePrivacyPolicyRule } from '../rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.ts';
import type { RuleEvaluationOutcome } from '../rules/PrivacyPolicyRule.ts';
import {
PrivacyPolicyRule,
RuleEvaluationResult,
denyWithReasons,
skipWithReasons,
} from '../rules/PrivacyPolicyRule.ts';
type BlahFields = {
id: string;
@@ -315,6 +331,88 @@ class ContextCapturingPolicy extends EntityPrivacyPolicy<
protected override readonly deleteRules = [this.deleteRule];
}
enum TestSkipReason {
NOT_OWNER = 'NOT_OWNER',
NOT_MEMBER = 'NOT_MEMBER',
BLOCKED = 'BLOCKED',
}
class SkipWithReasonRule extends PrivacyPolicyRule<BlahFields, 'id', ViewerContext, BlahEntity> {
constructor(private readonly reason: TestSkipReason) {
super();
}
async evaluateAsync(): Promise<RuleEvaluationOutcome> {
return skipWithReasons(this.reason);
}
}
class DenyWithReasonRule extends PrivacyPolicyRule<BlahFields, 'id', ViewerContext, BlahEntity> {
async evaluateAsync(): Promise<RuleEvaluationOutcome> {
return denyWithReasons(TestSkipReason.BLOCKED);
}
}
class ReasonedDenialPolicy extends EntityPrivacyPolicy<
BlahFields,
'id',
ViewerContext,
BlahEntity
> {
protected override readonly createRules = [
new SkipWithReasonRule(TestSkipReason.NOT_OWNER),
new AlwaysSkipPrivacyPolicyRule<BlahFields, 'id', ViewerContext, BlahEntity>(),
new SkipWithReasonRule(TestSkipReason.NOT_MEMBER),
new SkipWithReasonRule(TestSkipReason.NOT_OWNER),
];
protected override readonly readRules = [
new SkipWithReasonRule(TestSkipReason.NOT_OWNER),
new AlwaysSkipPrivacyPolicyRule<BlahFields, 'id', ViewerContext, BlahEntity>(),
new DenyWithReasonRule(),
new AlwaysAllowPrivacyPolicyRule<BlahFields, 'id', ViewerContext, BlahEntity>(),
];
protected override readonly updateRules = [
new AllowIfAnySubRuleAllowsPrivacyPolicyRule<BlahFields, 'id', ViewerContext, BlahEntity>([
new SkipWithReasonRule(TestSkipReason.NOT_OWNER),
new AllowIfAllSubRulesAllowPrivacyPolicyRule<BlahFields, 'id', ViewerContext, BlahEntity>([
new AlwaysAllowPrivacyPolicyRule<BlahFields, 'id', ViewerContext, BlahEntity>(),
new SkipWithReasonRule(TestSkipReason.NOT_MEMBER),
]),
]),
new EvaluateIfEntityFieldPredicatePrivacyPolicyRule<
BlahFields,
'id',
ViewerContext,
BlahEntity,
'id'
>('id', (id) => id === '1', new SkipWithReasonRule(TestSkipReason.BLOCKED)),
new EvaluateIfEntityFieldPredicatePrivacyPolicyRule<
BlahFields,
'id',
ViewerContext,
BlahEntity,
'id'
>('id', (id) => id === 'never', new SkipWithReasonRule(TestSkipReason.NOT_OWNER)),
];
protected override readonly deleteRules = [];
protected override getUserFacingDenialReason(
_viewerContext: ViewerContext,
_action: EntityAuthorizationAction,
denialReason: EntityNotAuthorizedDenialReason,
): EntityNotAuthorizedUserFacingReason | null {
if (
denialReason.type === EntityNotAuthorizedDenialType.ALL_RULES_SKIPPED &&
denialReason.skippedRules.some((skippedRule) =>
skippedRule.reasons.includes(TestSkipReason.NOT_MEMBER),
)
) {
return { code: 'MUST_BE_MEMBER', message: 'You must be a member to do this.' };
}
return null;
}
}
describe(EntityPrivacyPolicy, () => {
describe(EntityPrivacyPolicyEvaluationMode.ENFORCE.toString(), () => {
it('throws EntityNotAuthorizedError when deny', async () => {
@@ -761,6 +859,190 @@ describe(EntityPrivacyPolicy, () => {
});
});
describe('denial reasons', () => {
const setup = (): {
viewerContext: ViewerContext;
queryContext: EntityQueryContext;
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext<
BlahFields,
'id',
ViewerContext,
BlahEntity
>;
metricsAdapter: IEntityMetricsAdapter;
entity: BlahEntity;
} => {
const viewerContext = instance(mock(ViewerContext));
const queryContext = instance(mock(EntityQueryContext));
const privacyPolicyEvaluationContext =
instance(
mock<EntityPrivacyPolicyEvaluationContext<BlahFields, 'id', ViewerContext, BlahEntity>>(),
);
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
const entity = new BlahEntity({
viewerContext,
id: '1',
databaseFields: { id: '1' },
selectedFields: { id: '1' },
});
return {
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
metricsAdapter,
entity,
};
};
const rejectionOf = async <T>(
promise: Promise<T>,
): Promise<EntityNotAuthorizedError<BlahFields, 'id', ViewerContext, BlahEntity>> => {
try {
await promise;
} catch (e) {
expect(e).toBeInstanceOf(EntityNotAuthorizedError);
return e as EntityNotAuthorizedError<BlahFields, 'id', ViewerContext, BlahEntity>;
}
throw new Error('expected rejection');
};
it('collects reasons from all skipped rules and passes them to the policy handler', async () => {
const {
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
metricsAdapter,
entity,
} = setup();
const policy = new ReasonedDenialPolicy();
const error = await rejectionOf(
policy.authorizeCreateAsync(
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
entity,
metricsAdapter,
),
);
expect(error.action).toBe(EntityAuthorizationAction.CREATE);
expect(error.entityClassName).toBe('BlahEntity');
expect(error.entityID).toBe('1');
expect(error.denialReason).toEqual({
type: EntityNotAuthorizedDenialType.ALL_RULES_SKIPPED,
skippedRules: [
{ ruleIndex: 0, ruleName: 'SkipWithReasonRule', reasons: [TestSkipReason.NOT_OWNER] },
{ ruleIndex: 1, ruleName: 'AlwaysSkipPrivacyPolicyRule', reasons: [] },
{ ruleIndex: 2, ruleName: 'SkipWithReasonRule', reasons: [TestSkipReason.NOT_MEMBER] },
{ ruleIndex: 3, ruleName: 'SkipWithReasonRule', reasons: [TestSkipReason.NOT_OWNER] },
],
});
expect(error.message).toContain('action = CREATE, ruleIndex = -1)');
expect(error.message).not.toContain('SkipWithReasonRule');
expect(error.message).not.toContain('NOT_OWNER');
expect(error.userFacingDenialReason).toEqual({
code: 'MUST_BE_MEMBER',
message: 'You must be a member to do this.',
});
});
it('identifies the denying rule and the rules skipped before it when a rule denies', async () => {
const {
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
metricsAdapter,
entity,
} = setup();
const policy = new ReasonedDenialPolicy();
const error = await rejectionOf(
policy.authorizeReadAsync(
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
entity,
metricsAdapter,
),
);
expect(error.denialReason).toEqual({
type: EntityNotAuthorizedDenialType.RULE_DENIED,
rule: { ruleIndex: 2, ruleName: 'DenyWithReasonRule', reasons: [TestSkipReason.BLOCKED] },
skippedRules: [
{ ruleIndex: 0, ruleName: 'SkipWithReasonRule', reasons: [TestSkipReason.NOT_OWNER] },
{ ruleIndex: 1, ruleName: 'AlwaysSkipPrivacyPolicyRule', reasons: [] },
],
});
expect(error.message).toContain('action = READ, ruleIndex = 2)');
expect(error.message).not.toContain('BLOCKED');
expect(error.userFacingDenialReason).toBeNull();
});
it('surfaces reasons from sub-rules of composite rules', async () => {
const {
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
metricsAdapter,
entity,
} = setup();
const policy = new ReasonedDenialPolicy();
const error = await rejectionOf(
policy.authorizeUpdateAsync(
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
entity,
metricsAdapter,
),
);
expect(error.denialReason).toEqual({
type: EntityNotAuthorizedDenialType.ALL_RULES_SKIPPED,
skippedRules: [
{
ruleIndex: 0,
ruleName: 'AllowIfAnySubRuleAllowsPrivacyPolicyRule',
reasons: [TestSkipReason.NOT_OWNER, TestSkipReason.NOT_MEMBER],
},
{
ruleIndex: 1,
ruleName: 'EvaluateIfEntityFieldPredicatePrivacyPolicyRule',
reasons: [TestSkipReason.BLOCKED],
},
{
ruleIndex: 2,
ruleName: 'EvaluateIfEntityFieldPredicatePrivacyPolicyRule',
reasons: [],
},
],
});
});
it('reports an empty ruleset', async () => {
const {
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
metricsAdapter,
entity,
} = setup();
const policy = new ReasonedDenialPolicy();
const error = await rejectionOf(
policy.authorizeDeleteAsync(
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
entity,
metricsAdapter,
),
);
expect(error.denialReason).toEqual({
type: EntityNotAuthorizedDenialType.ALL_RULES_SKIPPED,
skippedRules: [],
});
expect(error.message).toContain('ruleIndex = -1)');
expect(error.userFacingDenialReason).toBeNull();
});
});
describe('EntityPrivacyPolicyRuleEvaluationContext', () => {
it('passes correct authorization action to rules for each CRUD operation', async () => {
const viewerContext = instance(mock(ViewerContext));
@@ -3,6 +3,77 @@ import type { ReadonlyEntity } from '../ReadonlyEntity.ts';
import type { ViewerContext } from '../ViewerContext.ts';
import { EntityError, EntityErrorCode, EntityErrorState } from './EntityError.ts';
/**
* Description of a single privacy policy rule as it participated in a denied evaluation.
*/
export type EntityNotAuthorizedRuleInfo = {
/**
* Index of the rule within the ruleset for the action.
*/
ruleIndex: number;
/**
* Class name of the rule.
*/
ruleName: string;
/**
* Reason codes the rule attached to its SKIP or DENY result. Empty when the rule returned a bare result.
*/
reasons: readonly string[];
};
/**
* How a privacy policy evaluation arrived at a denial.
*/
export enum EntityNotAuthorizedDenialType {
/**
* A rule explicitly returned DENY. Rules after it were not evaluated.
*/
RULE_DENIED = 'RULE_DENIED',
/**
* Every rule in the ruleset returned SKIP. None of the rules allowed the action.
*/
ALL_RULES_SKIPPED = 'ALL_RULES_SKIPPED',
}
/**
* Why a privacy policy evaluation was denied.
*/
export type EntityNotAuthorizedDenialReason =
| {
type: EntityNotAuthorizedDenialType.RULE_DENIED;
/**
* The rule that returned DENY. Rules after it were not evaluated.
*/
rule: EntityNotAuthorizedRuleInfo;
/**
* Rules evaluated before the denying rule, all of which returned SKIP.
*/
skippedRules: readonly EntityNotAuthorizedRuleInfo[];
}
| {
type: EntityNotAuthorizedDenialType.ALL_RULES_SKIPPED;
skippedRules: readonly EntityNotAuthorizedRuleInfo[];
};
/**
* A denial explanation that is safe to display to an end user.
*/
export type EntityNotAuthorizedUserFacingReason = {
/**
* Machine-readable code, stable across message wording changes. Suitable for localization keys
* and for mapping to transport-level errors.
*/
code: string;
/**
* Human-readable message suitable for display to the end user.
*/
message: string;
};
/**
* Error thrown when viewer context is not authorized to perform an action on an entity.
*/
@@ -26,16 +97,43 @@ export class EntityNotAuthorizedError<
}
public readonly entityClassName: string;
public readonly entityID: string;
public readonly action: EntityAuthorizationAction;
/**
* Structured, developer-facing explanation of the denial. May reveal privacy policy structure
* and facts about the entity and viewer; it is intentionally excluded from `message` and must
* not be surfaced directly to end users.
*/
public readonly denialReason: EntityNotAuthorizedDenialReason;
/**
* End-user-safe explanation supplied by the privacy policy, if any.
* See EntityPrivacyPolicy.getUserFacingDenialReason.
*/
public readonly userFacingDenialReason: EntityNotAuthorizedUserFacingReason | null;
constructor(
entity: TEntity,
viewerContext: TViewerContext,
action: EntityAuthorizationAction,
ruleIndex: number,
denialReason: EntityNotAuthorizedDenialReason,
userFacingDenialReason: EntityNotAuthorizedUserFacingReason | null = null,
) {
const ruleIndex =
denialReason.type === EntityNotAuthorizedDenialType.RULE_DENIED
? denialReason.rule.ruleIndex
: -1;
const entityString = `${entity.constructor.name}[${entity.getID()}]`;
super(
`Entity not authorized: ${entity} (viewer = ${viewerContext}, action = ${EntityAuthorizationAction[action]}, ruleIndex = ${ruleIndex})`,
`Entity not authorized: ${entityString} (viewer = ${viewerContext}, action = ${EntityAuthorizationAction[action]}, ruleIndex = ${ruleIndex})`,
);
this.entityClassName = entity.constructor.name;
this.entityID = String(entity.getID());
this.action = action;
this.denialReason = denialReason;
this.userFacingDenialReason = userFacingDenialReason;
}
}
@@ -4,7 +4,10 @@ import type { ReadonlyEntity } from '../ReadonlyEntity.ts';
import type { ViewerContext } from '../ViewerContext.ts';
import { EntityError, EntityErrorCode, EntityErrorState } from './EntityError.ts';
type EntityNotFoundOptions<
/**
* Options for constructing an {@link EntityNotFoundError} from a failed load.
*/
export type EntityNotFoundOptions<
TFields extends Record<string, any>,
TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
TViewerContext extends ViewerContext,
@@ -6,7 +6,10 @@ import { SimpleTestEntity } from '../../utils/__testfixtures__/SimpleTestEntity.
import { EntityCacheAdapterTransientError } from '../EntityCacheAdapterError.ts';
import { EntityErrorCode, EntityErrorState } from '../EntityError.ts';
import { EntityInvalidFieldValueError } from '../EntityInvalidFieldValueError.ts';
import { EntityNotAuthorizedError } from '../EntityNotAuthorizedError.ts';
import {
EntityNotAuthorizedDenialType,
EntityNotAuthorizedError,
} from '../EntityNotAuthorizedError.ts';
import { EntityNotFoundError } from '../EntityNotFoundError.ts';
describe('EntityError subclasses', () => {
@@ -25,7 +28,10 @@ describe('EntityError subclasses', () => {
databaseFields: data,
selectedFields: data,
});
const error = new EntityNotAuthorizedError(testEntity, viewerContext, 0, 0);
const error = new EntityNotAuthorizedError(testEntity, viewerContext, 0, {
type: EntityNotAuthorizedDenialType.ALL_RULES_SKIPPED,
skippedRules: [],
});
expect(error.state).toBe(EntityErrorState.PERMANENT);
expect(error.code).toBe(EntityErrorCode.ERR_ENTITY_NOT_AUTHORIZED);
});
@@ -2,6 +2,7 @@ import type { EntityQueryContext } from '../EntityQueryContext.ts';
import type { IEntityLoadValue } from '../internal/EntityLoadInterfaces.ts';
import { reduceMap } from '../utils/collections/maps.ts';
import type {
EntityMetricsCountType,
EntityMetricsLoadType,
EntityMetricsMutationType,
IEntityMetricsAdapter,
@@ -87,7 +88,7 @@ export const timeAndLogLoadMapEventAsync =
export const timeAndLogCountEventAsync =
(
metricsAdapter: IEntityMetricsAdapter,
loadType: EntityMetricsLoadType,
countType: EntityMetricsCountType,
entityClassName: string,
queryContext: EntityQueryContext,
) =>
@@ -96,8 +97,8 @@ export const timeAndLogCountEventAsync =
const result = await promise;
const endTime = Date.now();
metricsAdapter.logDataManagerLoadEvent({
type: loadType,
metricsAdapter.logDataManagerCountEvent({
type: countType,
isInTransaction: queryContext.isInTransaction(),
entityClassName,
duration: endTime - startTime,
@@ -28,8 +28,14 @@ export enum EntityMetricsLoadType {
* Knex loader load using loadPageAsync.
*/
LOAD_PAGE,
}
/**
* The type of the count method being called.
*/
export enum EntityMetricsCountType {
/**
* Knex loader count using countBySQLAsync.
* Knex loader count using countBySQLFragmentAsync.
*/
COUNT_SQL,
/**
@@ -68,6 +74,37 @@ export interface EntityMetricsLoadEvent {
count: number;
}
/**
* Event about a single call to an EntityLoader count method. Counts return a
* number rather than entities, so they are reported separately from loads.
*/
export interface EntityMetricsCountEvent {
/**
* EntityMetricsCountType for this count.
*/
type: EntityMetricsCountType;
/**
* Whether this count is within a transaction.
*/
isInTransaction: boolean;
/**
* Class name of the Entity being counted.
*/
entityClassName: string;
/**
* Total duration of this count query.
*/
duration: number;
/**
* The number returned by the count query. No entities are loaded for a count.
*/
count: number;
}
/**
* The type of mutation being performed.
*/
@@ -201,6 +238,13 @@ export interface IEntityMetricsAdapter {
*/
logDataManagerLoadEvent(loadEvent: EntityMetricsLoadEvent): void;
/**
* Called when any count occurs. Counts return a number rather than entities,
* so they are reported separately from loads.
* @param countEvent - info about the count event
*/
logDataManagerCountEvent(countEvent: EntityMetricsCountEvent): void;
/**
* Called when any mutation occurs.
* @param mutationEvent - info about the mutation event
@@ -1,5 +1,6 @@
import type {
EntityMetricsAuthorizationEvent,
EntityMetricsCountEvent,
EntityMetricsLoadEvent,
EntityMetricsMutationEvent,
IEntityMetricsAdapter,
@@ -9,6 +10,7 @@ import type {
export class NoOpEntityMetricsAdapter implements IEntityMetricsAdapter {
logAuthorizationEvent(_authorizationEvent: EntityMetricsAuthorizationEvent): void {}
logDataManagerLoadEvent(_loadEvent: EntityMetricsLoadEvent): void {}
logDataManagerCountEvent(_countEvent: EntityMetricsCountEvent): void {}
logMutatorMutationEvent(_mutationEvent: EntityMetricsMutationEvent): void {}
incrementDataManagerLoadCount(_incrementLoadCountEvent: IncrementLoadCountEvent): void {}
}
@@ -9,7 +9,11 @@ import {
timeAndLogMutationEventAsync,
} from '../EntityMetricsUtils.ts';
import type { IEntityMetricsAdapter } from '../IEntityMetricsAdapter.ts';
import { EntityMetricsLoadType, EntityMetricsMutationType } from '../IEntityMetricsAdapter.ts';
import {
EntityMetricsCountType,
EntityMetricsLoadType,
EntityMetricsMutationType,
} from '../IEntityMetricsAdapter.ts';
describe(timeAndLogLoadEventAsync, () => {
it('returns the result from the wrapped promise and logs', async () => {
@@ -95,7 +99,7 @@ describe(timeAndLogCountEventAsync, () => {
const result = await timeAndLogCountEventAsync(
metricsAdapter,
EntityMetricsLoadType.COUNT_SQL,
EntityMetricsCountType.COUNT_SQL,
'TestEntity',
queryContext,
)(Promise.resolve(42));
@@ -103,9 +107,9 @@ describe(timeAndLogCountEventAsync, () => {
expect(result).toBe(42);
verify(
metricsAdapterMock.logDataManagerLoadEvent(
metricsAdapterMock.logDataManagerCountEvent(
deepEqual({
type: EntityMetricsLoadType.COUNT_SQL,
type: EntityMetricsCountType.COUNT_SQL,
isInTransaction: false,
entityClassName: 'TestEntity',
duration: anyNumber(),
@@ -2,7 +2,13 @@ import type { EntityPrivacyPolicyRuleEvaluationContext } from '../EntityPrivacyP
import type { EntityQueryContext } from '../EntityQueryContext.ts';
import type { ReadonlyEntity } from '../ReadonlyEntity.ts';
import type { ViewerContext } from '../ViewerContext.ts';
import { PrivacyPolicyRule, RuleEvaluationResult } from './PrivacyPolicyRule.ts';
import type { RuleEvaluationOutcome } from './PrivacyPolicyRule.ts';
import {
PrivacyPolicyRule,
RuleEvaluationResult,
normalizeRuleEvaluationOutcome,
skipWithReasons,
} from './PrivacyPolicyRule.ts';
export class AllowIfAllSubRulesAllowPrivacyPolicyRule<
TFields extends object,
@@ -34,17 +40,23 @@ export class AllowIfAllSubRulesAllowPrivacyPolicyRule<
TSelectedFields
>,
entity: TEntity,
): Promise<RuleEvaluationResult> {
): Promise<RuleEvaluationOutcome> {
if (this.subRules.length === 0) {
return RuleEvaluationResult.SKIP;
}
const results = await Promise.all(
this.subRules.map((subRule) =>
subRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
const outcomes = await Promise.all(
this.subRules.map(async (subRule) =>
normalizeRuleEvaluationOutcome(
await subRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
),
),
);
return results.every((result) => result === RuleEvaluationResult.ALLOW)
return outcomes.every((outcome) => outcome.result === RuleEvaluationResult.ALLOW)
? RuleEvaluationResult.ALLOW
: RuleEvaluationResult.SKIP;
: skipWithReasons(
...outcomes
.filter((outcome) => outcome.result !== RuleEvaluationResult.ALLOW)
.flatMap((outcome) => outcome.reasons),
);
}
}
@@ -2,7 +2,13 @@ import type { EntityPrivacyPolicyRuleEvaluationContext } from '../EntityPrivacyP
import type { EntityQueryContext } from '../EntityQueryContext.ts';
import type { ReadonlyEntity } from '../ReadonlyEntity.ts';
import type { ViewerContext } from '../ViewerContext.ts';
import { PrivacyPolicyRule, RuleEvaluationResult } from './PrivacyPolicyRule.ts';
import type { RuleEvaluationOutcome } from './PrivacyPolicyRule.ts';
import {
PrivacyPolicyRule,
RuleEvaluationResult,
normalizeRuleEvaluationOutcome,
skipWithReasons,
} from './PrivacyPolicyRule.ts';
export class AllowIfAnySubRuleAllowsPrivacyPolicyRule<
TFields extends object,
@@ -34,14 +40,16 @@ export class AllowIfAnySubRuleAllowsPrivacyPolicyRule<
TSelectedFields
>,
entity: TEntity,
): Promise<RuleEvaluationResult> {
const results = await Promise.all(
this.subRules.map((subRule) =>
subRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
): Promise<RuleEvaluationOutcome> {
const outcomes = await Promise.all(
this.subRules.map(async (subRule) =>
normalizeRuleEvaluationOutcome(
await subRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
),
),
);
return results.includes(RuleEvaluationResult.ALLOW)
return outcomes.some((outcome) => outcome.result === RuleEvaluationResult.ALLOW)
? RuleEvaluationResult.ALLOW
: RuleEvaluationResult.SKIP;
: skipWithReasons(...outcomes.flatMap((outcome) => outcome.reasons));
}
}
@@ -33,17 +33,27 @@ export interface AllowIfInParentCascadeDeletionDirective<
TParentSelectedFields extends keyof TParentFields = keyof TParentFields,
> {
/**
* Class of parent entity that should trigger a cascade set null update to a field within
* the entity being authorized.
* Class (or classes) of parent entity that should trigger a cascade set null update to a field
* within the entity being authorized. Pass an array when multiple sibling classes backed by the
* same table can be the deletion source.
*/
parentEntityClass: IEntityClass<
TParentFields,
TParentIDField,
TViewerContext,
TParentEntity,
TParentPrivacyPolicy,
TParentSelectedFields
>;
parentEntityClass:
| IEntityClass<
TParentFields,
TParentIDField,
TViewerContext,
TParentEntity,
TParentPrivacyPolicy,
TParentSelectedFields
>
| readonly IEntityClass<
TParentFields,
TParentIDField,
TViewerContext,
TParentEntity,
TParentPrivacyPolicy,
TParentSelectedFields
>[];
/**
* Field of the current entity with references the deleting instace of parentEntityClass.
@@ -138,10 +148,12 @@ export class AllowIfInParentCascadeDeletionPrivacyPolicyRule<
>,
entity: TEntity,
): Promise<RuleEvaluationResult> {
const parentEntityClass = this.directive.parentEntityClass;
const parentEntityClasses = Array.isArray(this.directive.parentEntityClass)
? this.directive.parentEntityClass
: [this.directive.parentEntityClass];
const deleteCause = evaluationContext.cascadingDeleteCause;
if (!deleteCause || !(deleteCause.entity instanceof parentEntityClass)) {
if (!deleteCause || !parentEntityClasses.some((cls) => deleteCause.entity instanceof cls)) {
return RuleEvaluationResult.SKIP;
}
@@ -2,6 +2,7 @@ import type { EntityPrivacyPolicyRuleEvaluationContext } from '../EntityPrivacyP
import type { EntityQueryContext } from '../EntityQueryContext.ts';
import type { ReadonlyEntity } from '../ReadonlyEntity.ts';
import type { ViewerContext } from '../ViewerContext.ts';
import type { RuleEvaluationOutcome } from './PrivacyPolicyRule.ts';
import { PrivacyPolicyRule, RuleEvaluationResult } from './PrivacyPolicyRule.ts';
export class EvaluateIfEntityFieldPredicatePrivacyPolicyRule<
@@ -37,7 +38,7 @@ export class EvaluateIfEntityFieldPredicatePrivacyPolicyRule<
TSelectedFields
>,
entity: TEntity,
): Promise<RuleEvaluationResult> {
): Promise<RuleEvaluationOutcome> {
const fieldValue = entity.getField(this.fieldName);
return this.shouldEvaluatePredicate(fieldValue)
? await this.rule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity)
+50 -1
View File
@@ -20,6 +20,50 @@ export enum RuleEvaluationResult {
ALLOW = 1,
}
/**
* A SKIP or DENY result accompanied by reason codes explaining why the rule did not allow.
*
* @remarks
*
* Reason codes are application-defined and should be enum-like. String enums are recommended:
* their members are assignable to `string`, and the privacy policy's denial handler can
* compare against them to select an end-user-facing message.
*
* Multiple reasons are permitted so that composite rules can surface the reasons of their sub-rules.
*/
export type RuleEvaluationResultWithReasons = {
readonly result: RuleEvaluationResult;
readonly reasons: readonly string[];
};
/**
* What a rule returns from evaluation: a bare RuleEvaluationResult, or a SKIP/DENY with reasons.
*/
export type RuleEvaluationOutcome = RuleEvaluationResult | RuleEvaluationResultWithReasons;
/**
* Construct a SKIP outcome with reason codes.
*/
export function skipWithReasons(...reasons: readonly string[]): RuleEvaluationResultWithReasons {
return { result: RuleEvaluationResult.SKIP, reasons };
}
/**
* Construct a DENY outcome with reason codes.
*/
export function denyWithReasons(...reasons: readonly string[]): RuleEvaluationResultWithReasons {
return { result: RuleEvaluationResult.DENY, reasons };
}
/**
* Normalize a RuleEvaluationOutcome into its result and reasons. A bare result has no reasons.
*/
export function normalizeRuleEvaluationOutcome(
outcome: RuleEvaluationOutcome,
): RuleEvaluationResultWithReasons {
return typeof outcome === 'object' ? outcome : { result: outcome, reasons: [] };
}
/**
* A single unit of which declarative privacy policies are composed, allowing for simple
* expression and testing of authorization logic.
@@ -31,6 +75,11 @@ export enum RuleEvaluationResult {
* rules to return ALLOW or SKIP, explicitly authorizing or deferring authorization to the next
* rule in the privacy policy. If all rules in the policy SKIP, the policy is denied.
*
* A rule may attach reason codes to a SKIP or DENY (see skipWithReasons and denyWithReasons).
* When the policy is denied, the reasons from all skipped rules (or from the denying rule) are
* passed to EntityPrivacyPolicy.getUserFacingDenialReason and attached to the thrown
* EntityNotAuthorizedError.
*
* Returning DENY from a rule is useful in a few notable cases:
* - Preventing a CRUD action on an entity (AlwaysDenyPrivacyPolicyRule)
* - Blocking. For example, a user blocks another user from seeing their posts, and the rule
@@ -54,5 +103,5 @@ export abstract class PrivacyPolicyRule<
TSelectedFields
>,
entity: TEntity,
): Promise<RuleEvaluationResult>;
): Promise<RuleEvaluationOutcome>;
}
@@ -8,6 +8,18 @@ import { AllowIfAllSubRulesAllowPrivacyPolicyRule } from '../AllowIfAllSubRulesA
import { AlwaysAllowPrivacyPolicyRule } from '../AlwaysAllowPrivacyPolicyRule.ts';
import { AlwaysDenyPrivacyPolicyRule } from '../AlwaysDenyPrivacyPolicyRule.ts';
import { AlwaysSkipPrivacyPolicyRule } from '../AlwaysSkipPrivacyPolicyRule.ts';
import type { RuleEvaluationOutcome } from '../PrivacyPolicyRule.ts';
import { PrivacyPolicyRule, skipWithReasons } from '../PrivacyPolicyRule.ts';
class SkipWithReasonRule extends PrivacyPolicyRule<any, any, any, any, any> {
constructor(private readonly reason: string) {
super();
}
async evaluateAsync(): Promise<RuleEvaluationOutcome> {
return skipWithReasons(this.reason);
}
}
describePrivacyPolicyRule(
new AllowIfAllSubRulesAllowPrivacyPolicyRule([
@@ -74,3 +86,24 @@ describePrivacyPolicyRule(new AllowIfAllSubRulesAllowPrivacyPolicyRule([]), {
},
],
});
describePrivacyPolicyRule(
new AllowIfAllSubRulesAllowPrivacyPolicyRule([
new AlwaysAllowPrivacyPolicyRule(),
new SkipWithReasonRule('NOT_OWNER'),
new AlwaysSkipPrivacyPolicyRule(),
new SkipWithReasonRule('NOT_MEMBER'),
]),
{
skipCases: [
{
viewerContext: instance(mock(ViewerContext)),
queryContext: instance(mock(EntityQueryContext)),
evaluationContext:
instance(mock<EntityPrivacyPolicyRuleEvaluationContext<any, any, any, any, any>>()),
entity: anything(),
expectedReasons: ['NOT_OWNER', 'NOT_MEMBER'],
},
],
},
);
@@ -8,6 +8,18 @@ import { AllowIfAnySubRuleAllowsPrivacyPolicyRule } from '../AllowIfAnySubRuleAl
import { AlwaysAllowPrivacyPolicyRule } from '../AlwaysAllowPrivacyPolicyRule.ts';
import { AlwaysDenyPrivacyPolicyRule } from '../AlwaysDenyPrivacyPolicyRule.ts';
import { AlwaysSkipPrivacyPolicyRule } from '../AlwaysSkipPrivacyPolicyRule.ts';
import type { RuleEvaluationOutcome } from '../PrivacyPolicyRule.ts';
import { PrivacyPolicyRule, skipWithReasons } from '../PrivacyPolicyRule.ts';
class SkipWithReasonRule extends PrivacyPolicyRule<any, any, any, any, any> {
constructor(private readonly reason: string) {
super();
}
async evaluateAsync(): Promise<RuleEvaluationOutcome> {
return skipWithReasons(this.reason);
}
}
describePrivacyPolicyRule(
new AllowIfAnySubRuleAllowsPrivacyPolicyRule([
@@ -62,3 +74,23 @@ describePrivacyPolicyRule(
],
},
);
describePrivacyPolicyRule(
new AllowIfAnySubRuleAllowsPrivacyPolicyRule([
new SkipWithReasonRule('NOT_OWNER'),
new AlwaysSkipPrivacyPolicyRule(),
new SkipWithReasonRule('NOT_MEMBER'),
]),
{
skipCases: [
{
viewerContext: instance(mock(ViewerContext)),
queryContext: instance(mock(EntityQueryContext)),
evaluationContext:
instance(mock<EntityPrivacyPolicyRuleEvaluationContext<any, any, any, any, any>>()),
entity: anything(),
expectedReasons: ['NOT_OWNER', 'NOT_MEMBER'],
},
],
},
);
@@ -206,6 +206,108 @@ describePrivacyPolicyRule(
},
);
// Test with array of parent entity classes
class TestSiblingParentEntity extends ReadonlyEntity<ParentFields, 'id', ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
ParentFields,
'id',
ViewerContext,
TestSiblingParentEntity,
TestParentPrivacyPolicy
> {
throw new Error('Not implemented for test');
}
}
const siblingParentEntityMock = mock(TestSiblingParentEntity);
when(siblingParentEntityMock.getID()).thenReturn('5');
const siblingParentEntity = instance(siblingParentEntityMock);
Object.setPrototypeOf(siblingParentEntity, TestSiblingParentEntity.prototype);
const childEntityForSiblingMock = mock(TestChildEntity);
when(childEntityForSiblingMock.getField('parent_id')).thenReturn('5');
const childEntityForSiblingNullifiedMock = mock(TestChildEntity);
when(childEntityForSiblingNullifiedMock.getField('parent_id')).thenReturn(null);
describePrivacyPolicyRule(
new AllowIfInParentCascadeDeletionPrivacyPolicyRule<
ChildFields,
'id',
ViewerContext,
TestChildEntity,
ParentFields,
'id',
TestParentEntity,
TestParentPrivacyPolicy
>({
fieldIdentifyingParentEntity: 'parent_id',
parentEntityClass: [TestParentEntity, TestSiblingParentEntity],
}),
{
allowCases: [
// first class in array: parent id matches, field not yet nullified
{
viewerContext: instance(mock(ViewerContext)),
queryContext: instance(mock(EntityQueryContext)),
evaluationContext: {
action: anything(),
previousValue: instance(childEntityMock),
cascadingDeleteCause: {
entity: parentEntity,
cascadingDeleteCause: null,
},
},
entity: instance(childEntityMock),
},
// second class in array: sibling parent id matches, field not yet nullified
{
viewerContext: instance(mock(ViewerContext)),
queryContext: instance(mock(EntityQueryContext)),
evaluationContext: {
action: anything(),
previousValue: instance(childEntityForSiblingMock),
cascadingDeleteCause: {
entity: siblingParentEntity,
cascadingDeleteCause: null,
},
},
entity: instance(childEntityForSiblingMock),
},
// second class in array: field nullified, previous value matches
{
viewerContext: instance(mock(ViewerContext)),
queryContext: instance(mock(EntityQueryContext)),
evaluationContext: {
action: anything(),
previousValue: instance(childEntityForSiblingMock),
cascadingDeleteCause: {
entity: siblingParentEntity,
cascadingDeleteCause: null,
},
},
entity: instance(childEntityForSiblingNullifiedMock),
},
],
skipCases: [
// unrelated entity class not in array
{
viewerContext: instance(mock(ViewerContext)),
queryContext: instance(mock(EntityQueryContext)),
evaluationContext: {
action: anything(),
previousValue: null,
cascadingDeleteCause: {
entity: instance(unrelatedOtherEntityMock),
cascadingDeleteCause: null,
},
},
entity: instance(childEntityMock),
},
],
},
);
// Test with custom lookup field (parentEntityLookupByField)
const parentEntityWithNameMock = mock(TestParentEntity);
when(parentEntityWithNameMock.getField('name')).thenReturn('test-name');
@@ -5,7 +5,10 @@ import type { EntityQueryContext } from '../../EntityQueryContext.ts';
import type { ReadonlyEntity } from '../../ReadonlyEntity.ts';
import type { ViewerContext } from '../../ViewerContext.ts';
import type { PrivacyPolicyRule } from '../../rules/PrivacyPolicyRule.ts';
import { RuleEvaluationResult } from '../../rules/PrivacyPolicyRule.ts';
import {
RuleEvaluationResult,
normalizeRuleEvaluationOutcome,
} from '../../rules/PrivacyPolicyRule.ts';
export interface Case<
TFields extends Record<string, any>,
@@ -24,6 +27,12 @@ export interface Case<
TSelectedFields
>;
entity: TEntity;
/**
* For skip and deny cases: the exact reason codes the rule must attach to its result, in order.
* Omit to assert only the result. An empty array asserts that no reasons were attached.
* Ignored for allow cases.
*/
expectedReasons?: readonly string[];
}
export type CaseMap<
@@ -61,9 +70,15 @@ export const describePrivacyPolicyRuleWithAsyncTestCase = <
test.each(Array.from(allowCases.keys()))('%p', async (caseKey) => {
const { viewerContext, queryContext, evaluationContext, entity } =
await allowCases.get(caseKey)!();
await expect(
privacyPolicyRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
).resolves.toEqual(RuleEvaluationResult.ALLOW);
const outcome = await privacyPolicyRule.evaluateAsync(
viewerContext,
queryContext,
evaluationContext,
entity,
);
expect(normalizeRuleEvaluationOutcome(outcome).result).toEqual(
RuleEvaluationResult.ALLOW,
);
});
});
}
@@ -71,11 +86,20 @@ export const describePrivacyPolicyRuleWithAsyncTestCase = <
if (skipCases && skipCases.size > 0) {
describe('skip cases', () => {
test.each(Array.from(skipCases.keys()))('%p', async (caseKey) => {
const { viewerContext, queryContext, evaluationContext, entity } =
const { viewerContext, queryContext, evaluationContext, entity, expectedReasons } =
await skipCases.get(caseKey)!();
await expect(
privacyPolicyRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
).resolves.toEqual(RuleEvaluationResult.SKIP);
const outcome = normalizeRuleEvaluationOutcome(
await privacyPolicyRule.evaluateAsync(
viewerContext,
queryContext,
evaluationContext,
entity,
),
);
expect(outcome.result).toEqual(RuleEvaluationResult.SKIP);
if (expectedReasons !== undefined) {
expect(outcome.reasons).toEqual(expectedReasons);
}
});
});
}
@@ -83,11 +107,20 @@ export const describePrivacyPolicyRuleWithAsyncTestCase = <
if (denyCases && denyCases.size > 0) {
describe('deny cases', () => {
test.each(Array.from(denyCases.keys()))('%p', async (caseKey) => {
const { viewerContext, queryContext, evaluationContext, entity } =
const { viewerContext, queryContext, evaluationContext, entity, expectedReasons } =
await denyCases.get(caseKey)!();
await expect(
privacyPolicyRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
).resolves.toEqual(RuleEvaluationResult.DENY);
const outcome = normalizeRuleEvaluationOutcome(
await privacyPolicyRule.evaluateAsync(
viewerContext,
queryContext,
evaluationContext,
entity,
),
);
expect(outcome.result).toEqual(RuleEvaluationResult.DENY);
if (expectedReasons !== undefined) {
expect(outcome.reasons).toEqual(expectedReasons);
}
});
});
}
+2 -2
View File
@@ -21,8 +21,8 @@
"groupName": "CI workflows"
},
{
"groupName": "eslint",
"matchPackageNames": ["/^eslint/"]
"groupName": "lint",
"matchPackageNames": ["/^eslint/", "/^oxlint/", "/^prettier/", "/^oxfmt/"]
},
{
"groupName": "type definitions",
+2 -2
View File
@@ -1,13 +1,13 @@
version: '2'
services:
redis:
image: redis@sha256:0c341492924cad6f5483f9133e43bd6c51ecdecbcadfac5b51657393b6a7936c
image: redis@sha256:6f81e8915c60b065a524e6967e0ad1c639ba6efa84d669f823683ea04d9150ee
ports:
- '${REDIS_PORT}:6379'
volumes:
- redis_data:/data
postgres:
image: postgres:18@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
image: postgres:18@sha256:5a5a84b19854a9ffaa54082c166ff4ec27473a361e496e5ea167f298f2da9722
ports:
- '${PGPORT}:5432'
volumes:
+15
View File
@@ -8,6 +8,21 @@
"entryPoints": ["src/index.ts"]
},
"out": "doc",
"treatWarningsAsErrors": true,
"highlightLanguages": [
"bash",
"console",
"css",
"html",
"javascript",
"json",
"jsonc",
"json5",
"sql",
"tsx",
"typescript",
"yaml"
],
"exclude": [
"**/__tests__/**/*.ts",
"**/__integration-tests__/**/*.ts",
Generated
+1160 -1034
View File
File diff suppressed because it is too large. Load diff