feat!: Harper v5 support; fix withOAuthValidation on Resource API v2#43
feat!: Harper v5 support; fix withOAuthValidation on Resource API v2#43
Conversation
BREAKING: drops support for harperdb v4. Use the v1.x branch for
v4-compatible releases (v1.x line).
Harper v5 migration:
- Rename peer/dev dep `harperdb` → `harper` (`>=5.0.0` / `^5.0.0`)
- Update all imports `from 'harperdb'` → `from 'harper'` (src/types.ts,
src/lib/resource.ts, src/lib/handlers.ts, docs/api-reference.md)
- Update Bun preload mock target (`mock.module('harper', ...)`)
- Guard on `scope.resources` / `scope.server` once at
`handleApplication` entry — the Harper v5 `Scope` type marks these
optional but they're assigned in the constructor; fail loudly if
the invariant is ever violated, then use locally-narrowed consts
throughout (no scattered `?.` or `!`)
- Update `npm run test:coverage` path pattern accordingly
- Update CLAUDE.md + package.json keyword ordering
withOAuthValidation fix (closes #33):
- Old code found the request via `args.find((arg) => arg?.session)`,
which is a v4/legacy pattern. Resource API v2 methods receive
`(target, data)` — no request in args. The wrapper silently passed
through without validating.
- Drop the args-based lookup; read the request from `this.getContext()`.
- Add test/lib/withOAuthValidation.test.js with 7 tests covering:
valid-session passthrough, requireAuth 401, requireAuth-false
passthrough, custom onValidationError, stale-provider clearing,
non-HTTP method passthrough, and no-context fallthrough.
Version bump to 2.0.0 is deferred until we're ready to cut the release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI `npm ci` failed because the lockfile referenced bufferutil@4.1.0, utf-8-validate@5.0.10, and node-gyp-build@4.8.4 transitively but lacked entries for them. Happens when `npm install` partially resolves optional peer deps. Clean regen resolves. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. Missing tests for validation failure paths — the core cases the fix was supposed to addressFile: |
| assert.equal(calls.length, 1); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Two test cases are needed here to cover the failure paths introduced by the fix:
| }); | |
| describe('fallthrough: no context, no args with session', () => { | |
| it('passes through when the resource has no getContext and no request is in args', async () => { | |
| // Simulates a method called without v2 context and without a legacy request arg | |
| const calls = []; | |
| const resource = { | |
| async get(target, data) { | |
| calls.push({ target, data }); | |
| return { status: 200 }; | |
| }, | |
| }; | |
| const wrapped = withOAuthValidation(resource, { providers: mockProviders, logger: mockLogger }); | |
| const result = await wrapped.get({ path: '/noop' }, 'irrelevant'); | |
| assert.equal(result.status, 200); | |
| assert.equal(calls.length, 1); | |
| }); | |
| }); | |
| describe('validation failure paths (the scenarios the fix was written to address)', () => { | |
| it('returns 401 when requireAuth is true and the session token is expired with no refresh token', async () => { | |
| const expiredSession = makeSession({ | |
| oauth: { | |
| provider: 'github', | |
| accessToken: 'old-token', | |
| refreshToken: undefined, | |
| expiresAt: Date.now() - 1000, // already expired | |
| }, | |
| }); | |
| const context = { session: expiredSession }; | |
| const calls = []; | |
| const resource = makeV2Resource((method) => { | |
| calls.push({ method }); | |
| return { status: 200 }; | |
| }, context); | |
| const wrapped = withOAuthValidation(resource, { | |
| providers: mockProviders, | |
| logger: mockLogger, | |
| requireAuth: true, | |
| }); | |
| const result = await wrapped.get({ path: '/protected' }); | |
| assert.equal(result.status, 401); | |
| assert.equal(calls.length, 0, 'underlying method must not be called for expired session'); | |
| }); | |
| it('returns 401 when requireAuth is true and the session provider is not in the registry', async () => { | |
| const context = { | |
| session: makeSession({ oauth: { provider: 'ghost-provider', accessToken: 'stale' } }), | |
| }; | |
| const calls = []; | |
| const resource = makeV2Resource((method) => { | |
| calls.push({ method }); | |
| return { status: 200 }; | |
| }, context); | |
| const wrapped = withOAuthValidation(resource, { | |
| providers: mockProviders, // only has 'github' | |
| logger: mockLogger, | |
| requireAuth: true, | |
| }); | |
| const result = await wrapped.get({ path: '/protected' }); | |
| assert.equal(result.status, 401); | |
| assert.equal(calls.length, 0, 'underlying method must not be called when provider is unknown'); | |
| }); | |
| }); | |
| }); |
|
Closing in favor of a split: (A) Harper v5 migration, (B) #33 withOAuthValidation fix layered on top of A. Splitting keeps each PR within the reviewer's turn budget and preserves the option to backport the fix to the v1.x branch independently. Branch |
Summary
Two tightly-related changes bundled per Nathan's direction: migrate the plugin to Harper v5 (
harpernpm package, v5Scopetype, Resource API v2 conventions) and fix #33 (withOAuthValidationsilently passing Resource API v2 requests).BREAKING
harper >=5.0.0.v1.xbranch; future v4 bug fixes should PR there.2.0.0is deferred until we're ready to cut the release.Harper v5 migration
harperdb→harper(peer>=5.0.0, dev^5.0.0)from 'harperdb'→from 'harper'(src/types.ts, src/lib/resource.ts, src/lib/handlers.ts, docs/api-reference.md)mock.module('harper', ...)handleApplicationentry: guard once onscope.resources/scope.server(Harper v5 types mark these optional but they're always assigned by Scope's constructor — fail loudly if the invariant is ever violated, then use locally-narrowedconsts throughout instead of scattering?.or!)npm run test:coveragepath pattern updatedCLAUDE.md+package.jsonkeywords updated#33 fix: withOAuthValidation on Resource API v2
const request = args.find((arg) => arg?.session !== undefined);— a v4/legacy pattern. Resource API v2 method signatures arereceive(target, data)/get(target)etc.; the request is NOT in the args. The wrapper silently passed through without validating.this.getContext().test/lib/withOAuthValidation.test.js— new, 7 tests covering: valid-session passthrough,requireAuth401,requireAuth: falsepassthrough, customonValidationError, stale-provider clearing, non-HTTP method passthrough, no-context fallthrough.Prereqs already done
v1.xbranch cut from pre-migration main (for future v4 bug fixes)ai-review-logAI_REVIEW_LOG_TOKENsecret configuredTest plan
bun test— 429 pass, 2 skip, 0 fail (22 files)npm run lintpassesnpm run format:checkpasses (.claude/settings.local.json warn is gitignored local-only)Closes #33