Skip to content

Commit aead168

Browse files
os-zhuangclaude
andauthored
ci(validate-deps): replace retired pnpm audit with OSV-Scanner (#2986)
* ci(validate-deps): replace retired pnpm audit with OSV-Scanner npm retired the audit endpoint (HTTP 410, "This endpoint is being retired. Use the bulk advisory endpoint instead.") on 2026-07-13, and pnpm has not migrated to the bulk advisory endpoint. As a result `pnpm audit --audit-level=high` fails unconditionally on every branch and PR, turning the Validate Package Dependencies check permanently red (issue #2974). Replace the step with google/osv-scanner-action (pinned by commit SHA), which reads pnpm-lock.yaml directly against the OSV database and exits non-zero when any advisory matches — restoring a working blocking security gate without depending on the retired npm endpoint. Also update the release hardening checklist in docs/HARDENING.md to use osv-scanner, since `pnpm audit` no longer works locally either. Note: OSV-Scanner gates on any severity rather than only high/critical. Specific advisories can be accepted via an osv-scanner.toml [[IgnoredVulns]] entry instead of lowering the gate. Fixes #2974 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015JoYhorCUhGc8bgFYsErfE * ci(validate-deps): trigger workflow on changes to its own file So edits to the dependency-validation gates are exercised on the PR that introduces them, rather than only when a dependency manifest changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015JoYhorCUhGc8bgFYsErfE * fix(deps): remediate 9 OSV advisories surfaced by the new audit gate The OSV-Scanner gate (replacing the retired `pnpm audit`) blocks on any severity and surfaced 9 pre-existing advisories in pnpm-lock.yaml across 6 packages. Pin each to its patched line, following the existing pnpm-workspace.yaml `overrides` convention: - uuid 8.3.2 -> ^11.1.1 GHSA-w5hq-g745-h8pq (high) - @better-auth/oauth-provider 1.6.23 -> 1.7.0-rc.1 GHSA-p2fr-6hmx-4528 - postcss 8.4.31 -> ^8.5.10 GHSA-qx2v-qp2m-jg93 - cookie 0.6.0 -> 0.7.0 GHSA-pxg6-pf52-xh8x - svelte 5.55.3 -> ^5.55.7 GHSA-9rmh-mm8f-r9h6 + 3 more - @tootallnate/once 1.1.2 -> 2.0.1 GHSA-vpq2-c234-7xj6 (low) @better-auth/oauth-provider mirrors the existing @better-auth/scim pre-release pin (same monorepo, same advisory class). uuid pins to the conservative ^11.1.1 LTS line rather than the latest major. svelte is an optional peer-of-a-peer (better-auth > @sveltejs/kit) that `overrides` alone cannot re-resolve, so it is additionally pinned via a `svelte` devDependency in the root (private) package.json so the peer dedupes onto the patched 5.56.x line; both are documented in pnpm-workspace.yaml. Verified: no vulnerable version remains in the lockfile and `pnpm install --frozen-lockfile` is in sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015JoYhorCUhGc8bgFYsErfE * fix(auth): align better-auth family on 1.7.0-rc.1 + new adapter methods The @better-auth/oauth-provider fix (GHSA-p2fr-6hmx-4528) only ships in the 1.7 line, whose plugins import CLIENT_ASSERTION_TYPE and other symbols that exist only in @better-auth/core 1.7.x. Pinning oauth-provider to 1.7.0-rc.1 while core/better-auth stayed on 1.6.23 made sign-in 500 at runtime ("@better-auth/core/oauth2 does not provide an export named CLIENT_ASSERTION_TYPE"), failing the dogfood regression gate. Pin the ENTIRE better-auth family (better-auth, @better-auth/core, and all @better-auth/* plugins/adapters) to 1.7.0-rc.1 via pnpm-workspace overrides so the stack is internally consistent. better-auth 1.7 also adds two methods to its CustomAdapter contract, now implemented in the ObjectQL adapter as find-then-write mirrors of the existing delete/update methods (ObjectQL has no native atomic primitive): - consumeOne: atomic single-row consume (used for verification tokens on the sign-in path) - incrementOne: guarded field = field + delta counter mutation Verified locally: plugin-auth builds (dts included) and all 440 unit tests pass; frozen-lockfile is in sync; only plugin-auth consumes better-auth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015JoYhorCUhGc8bgFYsErfE * fix(auth): add sys_jwks alg/crv columns for better-auth 1.7 JWKS better-auth 1.7 extends its `jwks` model with two new optional columns — `alg` (signing algorithm, e.g. EdDSA) and `crv` (curve, e.g. Ed25519) — and writes them when minting signing keys. The `sys_jwks` platform object lacked both, so every JWKS insert failed at the driver ("table sys_jwks has no column named alg"), 500ing token signing. Sign-in itself succeeded, but session validation (which touches the JWKS/JWT path) then failed, so every authenticated request returned 401 — surfacing as the dogfood regression gate's "expected 201/403, got 401" failures. Add the two optional `alg`/`crv` text fields to sys_jwks. Legacy rows minted before 1.7 leave them null; better-auth falls back to the configured keyPairConfig.alg (default EdDSA). Verified locally: the full @objectstack/dogfood suite passes (256 tests, 0 failures) with the better-auth family on 1.7.0-rc.1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015JoYhorCUhGc8bgFYsErfE --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 464418e commit aead168

8 files changed

Lines changed: 291 additions & 158 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
fix(auth): align the better-auth family on 1.7.0-rc.1, implement the new adapter methods, and add the new sys_jwks columns (#2974)
7+
8+
Remediating GHSA-p2fr-6hmx-4528 (`@better-auth/oauth-provider`) requires the
9+
1.7 plugin line, which imports `CLIENT_ASSERTION_TYPE` and other symbols that
10+
only exist in `@better-auth/core` 1.7.x — so the whole better-auth family is
11+
pinned to `1.7.0-rc.1` together (mixing a 1.7 plugin with 1.6.23 core 500s on
12+
sign-in). better-auth 1.7 also extends its `CustomAdapter` contract with two
13+
new methods, which the ObjectQL adapter now implements:
14+
15+
- `consumeOne` — atomic single-row consume (find the guarded row, delete it,
16+
return it), used by better-auth for single-use credential consumption
17+
(e.g. verification tokens on the sign-in path).
18+
- `incrementOne` — guarded counter mutation (`field = field + delta` per
19+
`increment` entry plus any absolute `set` values), returning the updated row
20+
or `null` when the guard matches nothing.
21+
22+
Both are find-then-write mirrors of the existing `delete` / `update` methods
23+
(ObjectQL exposes no native atomic primitive) and honour the same core/plugin
24+
field-name bridging.
25+
26+
better-auth 1.7 also extends its `jwks` model with two new optional columns,
27+
`alg` (signing algorithm, e.g. `EdDSA`) and `crv` (curve, e.g. `Ed25519`), and
28+
writes them when minting signing keys. The `sys_jwks` platform object gains the
29+
matching fields — without them every JWKS write failed (`table sys_jwks has no
30+
column named alg`), 500ing token signing and breaking session validation
31+
(sign-in succeeded but every authenticated request 401'd).

.github/workflows/validate-deps.yml

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ on:
88
- '.changeset/config.json'
99
- 'pnpm-workspace.yaml'
1010
- 'scripts/check-changeset-fixed.mjs'
11+
# Re-run when the workflow itself changes, so edits to these gates are
12+
# exercised on the PR that introduces them.
13+
- '.github/workflows/validate-deps.yml'
1114
schedule:
1215
# Run weekly on Monday at 03:00 UTC
1316
- cron: '0 3 * * 1'
@@ -56,11 +59,24 @@ jobs:
5659
- name: Verify Changesets "fixed" group covers every public package
5760
run: node scripts/check-changeset-fixed.mjs
5861

59-
- name: Check for dependency issues
60-
run: |
61-
# Fail the workflow if high-severity vulnerabilities are found
62-
# This enforces security compliance before merging
63-
pnpm audit --audit-level=high
62+
# Fail the workflow if known vulnerabilities are found — enforces
63+
# security compliance before merging.
64+
#
65+
# Previously this ran `pnpm audit --audit-level=high`, but npm retired the
66+
# audit endpoint (HTTP 410, "This endpoint is being retired. Use the bulk
67+
# advisory endpoint instead.") and pnpm has not migrated, so `pnpm audit`
68+
# now fails unconditionally on every branch (see issue #2974). OSV-Scanner
69+
# reads pnpm-lock.yaml directly against the OSV database and exits non-zero
70+
# when any advisory matches, restoring a working blocking gate.
71+
#
72+
# Note: OSV-Scanner blocks on any severity, not just high/critical. To
73+
# accept a specific advisory, add an osv-scanner.toml `[[IgnoredVulns]]`
74+
# entry rather than lowering the gate.
75+
- name: Audit dependencies for known vulnerabilities (OSV-Scanner)
76+
uses: google/osv-scanner-action/osv-scanner-action@8dc09193bb540e09b23da07ad7e30bd33bf87018 # v2.3.8
77+
with:
78+
scan-args: |-
79+
--lockfile=pnpm-lock.yaml
6480
6581
- name: List outdated packages
6682
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'

docs/HARDENING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ Run the production hardening checklist before each release:
223223
- [ ] `revokeSessionsOnPasswordReset: true`
224224
- [ ] CORS origin list is explicit, not `*`
225225
- [ ] `enforceProjectMembership: true` on scoped routes
226-
- [ ] `pnpm audit` clean (no `high`/`critical`)
226+
- [ ] `osv-scanner --lockfile=pnpm-lock.yaml` clean (npm retired the
227+
`pnpm audit` endpoint — see issue #2974; CI enforces this via OSV-Scanner)
227228
- [ ] `pnpm outdated` reviewed
228229
- [ ] Backups: restore tested in the last 30 days
229230
- [ ] Audit log: `sys_audit_log` is append-only at the DB level

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@types/node": "^26.1.1",
4747
"@typescript-eslint/parser": "^8.63.0",
4848
"eslint": "^10.7.0",
49+
"svelte": "^5.55.7",
4950
"tsup": "^8.5.1",
5051
"tsx": "^4.23.0",
5152
"turbo": "^2.10.4",

packages/platform-objects/src/identity/sys-jwks.object.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,23 @@ export const SysJwks = ObjectSchema.create({
5959
description: 'JSON-serialized JWK private key (encrypted at rest)',
6060
}),
6161

62+
// better-auth 1.7 records the key's signing algorithm and (for EdDSA/EC
63+
// keys) its curve alongside the key material, so tokens can advertise the
64+
// correct `alg`/`crv` in the JWKS response. Both are optional — legacy rows
65+
// minted before 1.7 leave them null and better-auth falls back to the
66+
// configured `keyPairConfig.alg` (default `EdDSA`).
67+
alg: Field.text({
68+
label: 'Algorithm',
69+
required: false,
70+
description: 'JWK signing algorithm, e.g. `EdDSA` (better-auth 1.7+)',
71+
}),
72+
73+
crv: Field.text({
74+
label: 'Curve',
75+
required: false,
76+
description: 'JWK curve for EdDSA/EC keys, e.g. `Ed25519` (better-auth 1.7+)',
77+
}),
78+
6279
created_at: Field.datetime({
6380
label: 'Created At',
6481
required: true,

packages/plugins/plugin-auth/src/objectql-adapter.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,55 @@ export function createObjectQLAdapterFactory(rawDataEngine: IDataEngine) {
334334
}
335335
return records.length;
336336
},
337+
338+
// Atomic single-row consume (better-auth 1.7+). ObjectQL has no native
339+
// `DELETE ... RETURNING`, so we find the single guarded row, delete it,
340+
// and return the consumed record — a find-then-write mirror of `delete`.
341+
consumeOne: async <T>(
342+
{ model, where }: { model: string; where: CleanedWhere[] },
343+
): Promise<T | null> => {
344+
const objectName = resolveProtocolName(model);
345+
const bridged = objectName !== model;
346+
const filter = convertWhere(bridged ? remapWhere(where) : where);
347+
348+
const record = await dataEngine.findOne(objectName, { where: filter });
349+
if (!record) return null;
350+
await dataEngine.delete(objectName, { where: { id: record.id } });
351+
const norm = normaliseLegacyDates(model, record);
352+
return (bridged ? remapKeys(norm, snakeToCamel) : norm) as T;
353+
},
354+
355+
// Guarded counter mutation (better-auth 1.7+). ObjectQL has no native
356+
// `SET n = n + $delta ... RETURNING`, so we read the guarded row, apply
357+
// `field = field + delta` for each `increment` entry (negative deltas
358+
// decrement) plus any absolute `set` values, and write it back. `where`
359+
// is both selector and guard, so a non-matching guard returns null.
360+
incrementOne: async <T>(
361+
{ model, where, increment, set }: {
362+
model: string; where: CleanedWhere[];
363+
increment: Record<string, number>; set?: Record<string, unknown>;
364+
},
365+
): Promise<T | null> => {
366+
const objectName = resolveProtocolName(model);
367+
const bridged = objectName !== model;
368+
const filter = convertWhere(bridged ? remapWhere(where) : where);
369+
370+
const record = await dataEngine.findOne(objectName, { where: filter });
371+
if (!record) return null;
372+
373+
const patch: Record<string, any> = {};
374+
for (const [field, delta] of Object.entries(increment)) {
375+
const col = bridged ? camelToSnake(field) : field;
376+
const current = Number((record as Record<string, any>)[col] ?? 0);
377+
patch[col] = current + delta;
378+
}
379+
if (set) Object.assign(patch, bridged ? remapKeys(set, camelToSnake) : set);
380+
381+
const result = await dataEngine.update(objectName, { ...patch, id: record.id });
382+
if (!result) return null;
383+
const norm = normaliseLegacyDates(model, result);
384+
return (bridged ? remapKeys(norm, snakeToCamel) : norm) as T;
385+
},
337386
}),
338387
});
339388
}

0 commit comments

Comments
 (0)