Five surfaces (browser, office, vscode, cli, mcp) each hand-rolled the SAME
IAM OAuth2+PKCE flow — and one drifted and broke: vscode built
hanzo.id/oauth/authorize and iam.hanzo.ai/oauth/token, both MISSING the
/v1/iam prefix (a 404 — the same class of bug as the browser login fix),
plus it POSTed JSON where IAM requires form-urlencoded and sent a
client_secret on a public PKCE client. That is the opposite of one way.
Decomplected into ONE core — packages/auth (@hanzo/auth):
- config.ts canonical HIP-0111 endpoints (authorize/token/userinfo ALL
carry /v1/iam) + the one-client-per-surface registry
(hanzo-browser/office/vscode/cli). URL assembly lives once.
- pkce.ts the one PKCE (S256), platform-CSPRNG + WebCrypto.
- oauth.ts buildAuthorizeURL / exchangeCode / refreshTokens / userinfo —
pure fetch, form-encoded per RFC 6749.
- flow.ts login / getValidToken (transparent refresh) / logout /
currentUser, written ONCE against TokenStore + Opener. A
surface supplies only those two thin adapters — no surface
re-implements the flow.
23 tests: the drift guard (every path has /v1/iam), client registry, PKCE
RFC-7636 vector, wire shapes, and the full flow with fake adapters.
Migrate office onto it: roamingSettings TokenStore + Dialog-API Opener;
delete office's duplicate pkce.ts + IAM config (now the core's). API-key
path kept. 22 tests; the core bundles into the task pane.
Fix vscode standalone-auth: all three endpoints → ${host}/v1/iam/oauth/*,
token calls form-urlencoded, OIDC scopes, drop the secret on the public
PKCE client. Compiles clean.
CI now gates @hanzo/auth. Browser/mcp/tools already use canonical paths;
migrating them to import the core is the tracked next step (each needs its
own bundler wiring) — the ONE way now exists and has two consumers.
40 lines
1.8 KiB
TypeScript
40 lines
1.8 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { authorizeURL, tokenURL, userinfoURL, CLIENTS, IAM_API_PATH } from '../src/config';
|
|
import { authClientFor } from '../src/index';
|
|
|
|
describe('canonical HIP-0111 endpoints — the drift guard', () => {
|
|
it('authorize/token/userinfo ALL carry the /v1/iam prefix', () => {
|
|
// This is the exact bug that shipped in vscode: hanzo.id/oauth/authorize,
|
|
// missing /v1/iam. Pin every path so it can never regress.
|
|
expect(authorizeURL()).toBe('https://hanzo.id/v1/iam/oauth/authorize');
|
|
expect(tokenURL()).toBe('https://iam.hanzo.ai/v1/iam/oauth/token');
|
|
expect(userinfoURL()).toBe('https://iam.hanzo.ai/v1/iam/oauth/userinfo');
|
|
expect(IAM_API_PATH).toBe('/v1/iam');
|
|
});
|
|
|
|
it('a custom issuer keeps the /v1/iam prefix', () => {
|
|
expect(authorizeURL('https://id.acme.test')).toBe('https://id.acme.test/v1/iam/oauth/authorize');
|
|
expect(tokenURL('https://iam.acme.test/')).toBe('https://iam.acme.test/v1/iam/oauth/token');
|
|
});
|
|
});
|
|
|
|
describe('surface client registry — one client per surface', () => {
|
|
it('every surface has a distinct hanzo-<surface> id', () => {
|
|
expect(CLIENTS.browser.id).toBe('hanzo-browser');
|
|
expect(CLIENTS.office.id).toBe('hanzo-office');
|
|
expect(CLIENTS.vscode.id).toBe('hanzo-vscode');
|
|
expect(CLIENTS.cli.id).toBe('hanzo-cli');
|
|
const ids = Object.values(CLIENTS).map((c) => c.id);
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
});
|
|
|
|
it('authClientFor resolves id + redirect and honors overrides', () => {
|
|
const c = authClientFor('office');
|
|
expect(c.clientId).toBe('hanzo-office');
|
|
expect(c.redirectUri).toBe('https://office.hanzo.ai/auth-callback.html');
|
|
expect(authClientFor('office', { redirectUri: 'https://localhost:3000/cb' }).redirectUri).toBe(
|
|
'https://localhost:3000/cb',
|
|
);
|
|
});
|
|
});
|