companion-oauth-pkce-integration.test.mjs
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠ breaking
1 day ago
| 1 | /** |
| 2 | * Tier 2 — INTEGRATION: the pure functions composed across the real OAuth/PKCE sequence |
| 3 | * (no I/O): pair → authorization URL → parse callback params → validate response → token request |
| 4 | * → validate token response → refresh decision. Asserts the pieces fit and the state/PKCE bindings |
| 5 | * survive a round-trip through a URL. |
| 6 | */ |
| 7 | import { describe, it } from 'node:test'; |
| 8 | import assert from 'node:assert/strict'; |
| 9 | import crypto from 'node:crypto'; |
| 10 | import { |
| 11 | createPkcePair, |
| 12 | createOAuthState, |
| 13 | buildAuthorizationUrl, |
| 14 | validateAuthorizationResponse, |
| 15 | buildTokenRequest, |
| 16 | validateTokenResponse, |
| 17 | decideTokenRefresh, |
| 18 | } from '../lib/companion-oauth-pkce.mjs'; |
| 19 | |
| 20 | const AUTH_EP = 'https://knowtation.store/authorize'; |
| 21 | const TOKEN_EP = 'https://knowtation.store/token'; |
| 22 | const ISSUER = 'https://knowtation.store'; |
| 23 | const CLIENT_ID = 'companion-public-client'; |
| 24 | const REDIRECT = 'http://127.0.0.1:49321/callback'; |
| 25 | const SCOPES = ['vault:read', 'vault:write']; |
| 26 | |
| 27 | describe('Integration — authorization request round-trips through a URL', () => { |
| 28 | it('the challenge, state, and redirect survive serialization into the auth URL', () => { |
| 29 | const { codeChallenge } = createPkcePair(); |
| 30 | const state = createOAuthState(); |
| 31 | const url = new URL(buildAuthorizationUrl({ |
| 32 | authorizationEndpoint: AUTH_EP, clientId: CLIENT_ID, redirectUri: REDIRECT, |
| 33 | scopes: SCOPES, state, codeChallenge, |
| 34 | })); |
| 35 | assert.equal(url.searchParams.get('code_challenge'), codeChallenge); |
| 36 | assert.equal(url.searchParams.get('state'), state); |
| 37 | assert.equal(url.searchParams.get('redirect_uri'), REDIRECT); |
| 38 | }); |
| 39 | }); |
| 40 | |
| 41 | describe('Integration — callback validation feeds the token request', () => { |
| 42 | it('a matching-state callback yields a code that buildTokenRequest consumes with the verifier', () => { |
| 43 | const { codeVerifier, codeChallenge } = createPkcePair(); |
| 44 | const state = createOAuthState(); |
| 45 | // The auth server (simulated) verifies S256(verifier) === challenge and returns a code. |
| 46 | assert.equal(crypto.createHash('sha256').update(codeVerifier, 'ascii').digest('base64url'), codeChallenge); |
| 47 | const callbackParams = { code: 'srv-issued-code', state, iss: ISSUER }; |
| 48 | |
| 49 | const resp = validateAuthorizationResponse({ params: callbackParams, expectedState: state, expectedIssuer: ISSUER }); |
| 50 | assert.equal(resp.ok, true); |
| 51 | |
| 52 | const tokenReq = buildTokenRequest({ |
| 53 | tokenEndpoint: TOKEN_EP, clientId: CLIENT_ID, code: resp.code, codeVerifier, redirectUri: REDIRECT, |
| 54 | }); |
| 55 | assert.equal(tokenReq.bodyParams.code, 'srv-issued-code'); |
| 56 | assert.equal(tokenReq.bodyParams.code_verifier, codeVerifier); |
| 57 | }); |
| 58 | }); |
| 59 | |
| 60 | describe('Integration — token response drives the refresh decision', () => { |
| 61 | it('a validated token response gives an expiry the refresh decision can act on', () => { |
| 62 | const tr = validateTokenResponse({ access_token: 'jwt', token_type: 'Bearer', expires_in: 3600, refresh_token: 'r' }); |
| 63 | assert.equal(tr.ok, true); |
| 64 | const now = 1_000_000; |
| 65 | const expiresAt = now + tr.expiresIn * 1000; |
| 66 | assert.equal(decideTokenRefresh({ expiresAt, now, skewMs: 30_000 }), 'valid'); |
| 67 | assert.equal(decideTokenRefresh({ expiresAt, now: expiresAt - 10, skewMs: 30_000 }), 'refresh'); |
| 68 | assert.equal(decideTokenRefresh({ expiresAt, now: expiresAt + 1, refreshExpiresAt: expiresAt + 100 }), 'refresh'); |
| 69 | assert.equal(decideTokenRefresh({ expiresAt, now: expiresAt + 1000, refreshExpiresAt: expiresAt + 100 }), 'reauth'); |
| 70 | }); |
| 71 | }); |
| 72 | |
| 73 | describe('Integration — a denied authorization aborts before any token request', () => { |
| 74 | it('an access_denied callback never produces a code', () => { |
| 75 | const state = createOAuthState(); |
| 76 | const resp = validateAuthorizationResponse({ params: { error: 'access_denied', state }, expectedState: state }); |
| 77 | assert.equal(resp.ok, false); |
| 78 | assert.equal(resp.errorCode, 'access_denied'); |
| 79 | assert.equal(resp.code, undefined); |
| 80 | }); |
| 81 | }); |
File History
2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠
1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6
docs: accept Calendar Events v0 spec with Phase 0 security …
Human
2 days ago