@sentropic/auth-ui 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,12 +45,13 @@ npm install @sentropic/auth-ui
45
45
 
46
46
  | Export path | Contents |
47
47
  | --- | --- |
48
- | `@sentropic/auth-ui` | `AuthUiTransport`, `AuthUiSession`, `AuthUiError`, `AuthUiLabels`, `createDefaultAuthUiLabels`, `createFrenchAuthUiLabels`, `createDefaultAuthUiBranding`, `assertAuthUiTransport`, `normalizeAuthEmail`, `createAuthUiError`, `createDefaultFetchTransport`, WebAuthn helpers |
48
+ | `@sentropic/auth-ui` | `AuthUiTransport`, `AuthUiSession`, `AuthUiError`, `AuthUiLabels`, `createDefaultAuthUiLabels`, `createFrenchAuthUiLabels`, `createDefaultAuthUiBranding`, `assertAuthUiTransport`, `normalizeAuthEmail`, `createAuthUiError`, `createDefaultFetchTransport`, WebAuthn helpers; **+ OAuth (0.3.0)**: `createOAuthClient`, `OAuthConsentTransport`, `OAuthConsentDetails`, `OAuthConsentDecision`, `OAuthConsentLabels`, `createDefaultOAuthConsentLabels`, `createFrenchOAuthConsentLabels` |
49
49
  | `@sentropic/auth-ui/components/AuthLogin.svelte` | Passkey login screen (discoverable credentials, lost-device path) |
50
50
  | `@sentropic/auth-ui/components/AuthRegister.svelte` | Email-code → passkey registration; optional `skipEmailVerification` for hosts that own pre-auth |
51
51
  | `@sentropic/auth-ui/components/AuthMagicLinkVerify.svelte` | Verifies a magic-link token from a host-supplied source |
52
52
  | `@sentropic/auth-ui/components/AuthDevices.svelte` | Lists / renames / revokes registered passkeys |
53
53
  | `@sentropic/auth-ui/components/AuthDevicePair.svelte` | Approves a device-code pairing (`approveDevicePairing` contract) |
54
+ | `@sentropic/auth-ui/components/OAuthConsent.svelte` | OAuth2 consent screen (since 0.3.0) |
54
55
 
55
56
  All five components accept a `labels?: Partial<AuthUiLabels>` prop so hosts can override copy without forking. Each takes a `transport: AuthUiTransport` and one or more host callbacks (`onLoggedIn`, `onRegistered`, `onVerified`, `onPaired`, `onUnauthorized`, `onError`). Visual customisation flows through CSS custom properties (`--auth-primary`, `--auth-bg`, `--auth-text`, `--auth-radius`, `--auth-font-family`, …) and slots (`no-account`, `register-new-device`, `back-to-login`, `back-to-devices`, `pair-cta`, `add-device`, `login-link`, `cancel`).
56
57
 
@@ -142,9 +143,87 @@ See `tests/example-admin-fetch-transport.test.ts` for a full walkthrough.
142
143
  - **French labels**: `createFrenchAuthUiLabels(overrides?)` ships a complete FR baseline; partial overrides keep the unchanged keys.
143
144
  - **Post-login redirects**: never built into the components — call `goto(returnUrl)` / `location.assign(...)` from `onLoggedIn` / `onRegistered` / `onVerified` / `onRedirect`. `AuthMagicLinkVerify` exposes `redirectDelayMs` (defaults to 1000 ms) so the success screen has time to render before the host redirect fires.
144
145
 
145
- ## Backend coupling (BR-39b)
146
+ ## OAuth Consent + RP Client Helper (since 0.3.0)
146
147
 
147
- `@sentropic/auth-hono` provides reusable Hono route factories that match this transport shape 1:1. It is **not required** to adopt this UI package — any backend that exposes the routes listed above will work. BR-39b removes the duplicated backend code from Sentropic, but the UI package was usable before that landed.
148
+ ### OAuthConsent component
149
+
150
+ `<OAuthConsent />` renders the consent screen displayed to users when an external RP requests access.
151
+
152
+ Props:
153
+
154
+ | Prop | Type | Description |
155
+ | --- | --- | --- |
156
+ | `state` | `string` | Sealed OAuth state token from the authorize redirect query param |
157
+ | `transport` | `OAuthConsentTransport` | Calls the IdP to fetch consent details and submit the decision |
158
+ | `labels` | `Partial<OAuthConsentLabels>` | Override EN defaults (or use `createFrenchOAuthConsentLabels()`) |
159
+ | `onRedirect` | `(url: string) => void` | Called with the RP redirect URL after approve or deny |
160
+ | `onError` | `(error: unknown) => void` | Called on transport errors |
161
+
162
+ Named slots: `branding` (client logo / name override), `scope-description` (per-scope explanations), `footer` (custom legal copy).
163
+
164
+ ```svelte
165
+ <script lang="ts">
166
+ import { goto } from '$app/navigation';
167
+ import OAuthConsent from '@sentropic/auth-ui/components/OAuthConsent.svelte';
168
+ import { createSentropicOAuthConsentTransport } from '$lib/services/oauth-transport';
169
+ import { page } from '$app/stores';
170
+
171
+ const state = $page.url.searchParams.get('state') ?? '';
172
+ const transport = createSentropicOAuthConsentTransport();
173
+ </script>
174
+
175
+ <OAuthConsent {state} {transport} onRedirect={(url) => goto(url)} />
176
+ ```
177
+
178
+ ### createOAuthClient helper
179
+
180
+ `createOAuthClient` handles discovery, PKCE generation, code exchange, token revocation, and optional DPoP proof generation for RP-side code in the browser.
181
+
182
+ ```ts
183
+ import { createOAuthClient } from '@sentropic/auth-ui';
184
+
185
+ const client = createOAuthClient({
186
+ issuer: 'https://api.example.com',
187
+ clientId: 'my-rp',
188
+ redirectUri: 'https://myapp.example.com/auth/callback',
189
+ scopes: ['openid', 'profile', 'email'],
190
+ // Optional: enable DPoP (RFC 9449)
191
+ // dpop: { generateKeyPair: ..., store: ... }
192
+ });
193
+
194
+ // Build the authorization URL (redirects user to IdP)
195
+ const { url, codeVerifier, state, nonce } = await client.startAuthorization();
196
+ location.assign(url);
197
+
198
+ // On callback page: exchange code for tokens
199
+ const tokens = await client.exchangeCode(code, codeVerifier);
200
+
201
+ // Fetch user claims
202
+ const userInfo = await client.userInfo(tokens.access_token);
203
+
204
+ // Revoke access token when done
205
+ await client.revoke(tokens.access_token);
206
+ ```
207
+
208
+ The client caches the OIDC discovery document on first call. When `dpop` is enabled it generates an Ed25519 keypair via SubtleCrypto, stores it through the injected `store` adapter, and attaches a signed DPoP proof header to every token/userinfo/revoke request.
209
+
210
+ ### Downstream RP example (immo, diag, paas)
211
+
212
+ ```ts
213
+ // In a downstream SvelteKit app that uses Sentropic as its IdP
214
+ import { createOAuthClient } from '@sentropic/auth-ui';
215
+
216
+ export const oauthClient = createOAuthClient({
217
+ issuer: import.meta.env.VITE_SENTROPIC_API_URL, // e.g. https://api.sentropic.io
218
+ clientId: import.meta.env.VITE_OAUTH_CLIENT_ID,
219
+ redirectUri: `${location.origin}/auth/callback`,
220
+ scopes: ['openid', 'profile', 'email'],
221
+ });
222
+ ```
223
+
224
+ ## Backend coupling (BR-39b / BR-39c)
225
+
226
+ `@sentropic/auth-hono` provides reusable Hono route factories that match this transport shape 1:1. It is **not required** to adopt this UI package — any backend that exposes the routes listed above will work. BR-39b removes the duplicated backend code from Sentropic; BR-39c adds the OAuth2/OIDC IdP surface consumed by `<OAuthConsent />` and `createOAuthClient`.
148
227
 
149
228
  ## Versioning
150
229
 
@@ -1,5 +1,6 @@
1
1
  export * from './errors.js';
2
2
  export * from './labels.js';
3
+ export * from './oauth-consent.js';
3
4
  export * from './transport.js';
4
5
  export * from './transport-fetch.js';
5
6
  export * from './transport-types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC"}
package/dist/contracts.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './errors.js';
2
2
  export * from './labels.js';
3
+ export * from './oauth-consent.js';
3
4
  export * from './transport.js';
4
5
  export * from './transport-fetch.js';
5
6
  export * from './transport-types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from './contracts.js';
2
+ export * from './oauth-client.js';
3
+ export * from './oauth-consent.js';
2
4
  export * from './webauthn.js';
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from './contracts.js';
2
+ export * from './oauth-client.js';
3
+ export * from './oauth-consent.js';
2
4
  export * from './webauthn.js';
3
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC"}
package/dist/labels.d.ts CHANGED
@@ -1,5 +1,8 @@
1
+ import type { OAuthConsentLabels } from './oauth-consent.js';
1
2
  import type { AuthUiBranding, AuthUiLabels } from './types.js';
2
3
  export declare const createDefaultAuthUiLabels: (overrides?: Partial<AuthUiLabels>) => AuthUiLabels;
3
4
  export declare const createFrenchAuthUiLabels: (overrides?: Partial<AuthUiLabels>) => AuthUiLabels;
4
5
  export declare const createDefaultAuthUiBranding: (overrides?: Partial<AuthUiBranding>) => AuthUiBranding;
6
+ export declare const createDefaultOAuthConsentLabels: (overrides?: Partial<OAuthConsentLabels>) => OAuthConsentLabels;
7
+ export declare const createFrenchOAuthConsentLabels: (overrides?: Partial<OAuthConsentLabels>) => OAuthConsentLabels;
5
8
  //# sourceMappingURL=labels.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"labels.d.ts","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/D,eAAO,MAAM,yBAAyB,eAAe,QAAQ,YAAY,CAAC,KAAQ,YAoFhF,CAAC;AAEH,eAAO,MAAM,wBAAwB,eAAe,QAAQ,YAAY,CAAC,KAAQ,YAmF7E,CAAC;AAEL,eAAO,MAAM,2BAA2B,eAAe,QAAQ,cAAc,CAAC,KAAQ,cAYrF,CAAC"}
1
+ {"version":3,"file":"labels.d.ts","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/D,eAAO,MAAM,yBAAyB,eAAe,QAAQ,YAAY,CAAC,KAAQ,YAoFhF,CAAC;AAEH,eAAO,MAAM,wBAAwB,eAAe,QAAQ,YAAY,CAAC,KAAQ,YAmF7E,CAAC;AAEL,eAAO,MAAM,2BAA2B,eAAe,QAAQ,cAAc,CAAC,KAAQ,cAYrF,CAAC;AAEF,eAAO,MAAM,+BAA+B,eAC/B,QAAQ,kBAAkB,CAAC,KACrC,kBAiBD,CAAC;AAEH,eAAO,MAAM,8BAA8B,eAC9B,QAAQ,kBAAkB,CAAC,KACrC,kBAiBC,CAAC"}
package/dist/labels.js CHANGED
@@ -177,4 +177,39 @@ export const createDefaultAuthUiBranding = (overrides = {}) => {
177
177
  }
178
178
  return branding;
179
179
  };
180
+ export const createDefaultOAuthConsentLabels = (overrides = {}) => ({
181
+ approve: 'Approve',
182
+ approving: 'Approving...',
183
+ deny: 'Deny',
184
+ denying: 'Denying...',
185
+ errorGeneric: 'Unable to load the authorization request.',
186
+ loading: 'Loading authorization request...',
187
+ redirectUriLabel: 'Redirect destination',
188
+ scopeDescriptions: {
189
+ email: 'Access your email address and email verification status.',
190
+ openid: 'Confirm your identity with this account.',
191
+ profile: 'Access your profile name.',
192
+ ...(overrides.scopeDescriptions ?? {}),
193
+ },
194
+ scopesTitle: 'This application requests access to:',
195
+ title: 'Authorize application',
196
+ ...overrides,
197
+ });
198
+ export const createFrenchOAuthConsentLabels = (overrides = {}) => createDefaultOAuthConsentLabels({
199
+ approve: 'Autoriser',
200
+ approving: 'Autorisation...',
201
+ deny: 'Refuser',
202
+ denying: 'Refus...',
203
+ errorGeneric: "Impossible de charger la demande d'autorisation.",
204
+ loading: "Chargement de la demande d'autorisation...",
205
+ redirectUriLabel: 'Destination de redirection',
206
+ scopeDescriptions: {
207
+ email: "Accéder à votre adresse email et à son statut de vérification.",
208
+ openid: 'Confirmer votre identité avec ce compte.',
209
+ profile: 'Accéder au nom de votre profil.',
210
+ },
211
+ scopesTitle: 'Cette application demande accès à :',
212
+ title: "Autoriser l'application",
213
+ ...overrides,
214
+ });
180
215
  //# sourceMappingURL=labels.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"labels.js","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"labels.js","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ export type OAuthFetchLike = (input: string, init?: RequestInit) => Promise<Response>;
2
+ export interface OAuthDiscoveryDocument {
3
+ authorization_endpoint: string;
4
+ revocation_endpoint?: string;
5
+ token_endpoint: string;
6
+ userinfo_endpoint?: string;
7
+ }
8
+ export interface OAuthTokenResponse {
9
+ access_token: string;
10
+ token_type: 'Bearer' | 'DPoP' | (string & {});
11
+ expires_in?: number;
12
+ id_token?: string;
13
+ scope?: string;
14
+ }
15
+ export interface OAuthAuthorizationRequest {
16
+ codeChallenge: string;
17
+ codeVerifier: string;
18
+ nonce?: string;
19
+ state?: string;
20
+ url: string;
21
+ }
22
+ export interface OAuthStartAuthorizationInput {
23
+ codeVerifier?: string;
24
+ nonce?: string;
25
+ state?: string;
26
+ }
27
+ export interface OAuthDpopKeyPair {
28
+ privateKey?: CryptoKey;
29
+ publicJwk: JsonWebKey;
30
+ sign?: (data: Uint8Array) => Promise<ArrayBuffer | Uint8Array>;
31
+ }
32
+ export interface OAuthDpopStore {
33
+ get(): Promise<OAuthDpopKeyPair | null> | OAuthDpopKeyPair | null;
34
+ set(keyPair: OAuthDpopKeyPair): Promise<void> | void;
35
+ }
36
+ export interface OAuthDpopOptions {
37
+ generateKeyPair?: () => Promise<OAuthDpopKeyPair>;
38
+ jti?: () => string;
39
+ now?: () => Date;
40
+ store: OAuthDpopStore;
41
+ }
42
+ export interface CreateOAuthClientOptions {
43
+ clientId: string;
44
+ dpop?: OAuthDpopOptions;
45
+ fetch?: OAuthFetchLike;
46
+ issuer: string;
47
+ redirectUri: string;
48
+ scopes: string[];
49
+ }
50
+ export interface OAuthClient {
51
+ exchangeCode(code: string, codeVerifier: string): Promise<OAuthTokenResponse>;
52
+ revoke(token: string): Promise<void>;
53
+ startAuthorization(input?: OAuthStartAuthorizationInput): Promise<OAuthAuthorizationRequest>;
54
+ userInfo<T = Record<string, unknown>>(token: string): Promise<T>;
55
+ }
56
+ export declare const createOAuthClient: (options: CreateOAuthClientOptions) => OAuthClient;
57
+ //# sourceMappingURL=oauth-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-client.d.ts","sourceRoot":"","sources":["../src/oauth-client.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEtF,MAAM,WAAW,sBAAsB;IACrC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,QAAQ,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,4BAA4B;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,SAAS,EAAE,UAAU,CAAC;IACtB,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,WAAW,GAAG,UAAU,CAAC,CAAC;CAChE;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAAG,gBAAgB,GAAG,IAAI,CAAC;IAClE,GAAG,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,gBAAgB;IAC/B,eAAe,CAAC,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAClD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,cAAc,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC9E,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,kBAAkB,CAAC,KAAK,CAAC,EAAE,4BAA4B,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAC7F,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAClE;AAED,eAAO,MAAM,iBAAiB,YAAa,wBAAwB,KAAG,WAiHrE,CAAC"}
@@ -0,0 +1,218 @@
1
+ import { createAuthUiError } from './errors.js';
2
+ export const createOAuthClient = (options) => {
3
+ const issuer = options.issuer.replace(/\/+$/u, '');
4
+ const fetchImpl = options.fetch ?? (typeof fetch === 'function' ? fetch.bind(globalThis) : undefined);
5
+ if (!fetchImpl) {
6
+ throw createAuthUiError('invalid_input', 'No global fetch found; pass options.fetch when constructing the OAuth client.');
7
+ }
8
+ let discoveryPromise = null;
9
+ const getDiscovery = () => {
10
+ discoveryPromise ??= requestJson(fetchImpl, `${issuer}/.well-known/openid-configuration`, { headers: { Accept: 'application/json' }, method: 'GET' });
11
+ return discoveryPromise;
12
+ };
13
+ return {
14
+ async startAuthorization(input = {}) {
15
+ const [discovery, dpopJkt] = await Promise.all([
16
+ getDiscovery(),
17
+ options.dpop ? getDpopJkt(options.dpop) : Promise.resolve(null),
18
+ ]);
19
+ const codeVerifier = input.codeVerifier ?? generateCodeVerifier();
20
+ const codeChallenge = await sha256Base64url(codeVerifier);
21
+ const url = new URL(discovery.authorization_endpoint);
22
+ url.searchParams.set('response_type', 'code');
23
+ url.searchParams.set('client_id', options.clientId);
24
+ url.searchParams.set('redirect_uri', options.redirectUri);
25
+ url.searchParams.set('scope', options.scopes.join(' '));
26
+ url.searchParams.set('code_challenge', codeChallenge);
27
+ url.searchParams.set('code_challenge_method', 'S256');
28
+ if (input.state)
29
+ url.searchParams.set('state', input.state);
30
+ if (input.nonce)
31
+ url.searchParams.set('nonce', input.nonce);
32
+ if (dpopJkt)
33
+ url.searchParams.set('dpop_jkt', dpopJkt);
34
+ return {
35
+ codeChallenge,
36
+ codeVerifier,
37
+ nonce: input.nonce,
38
+ state: input.state,
39
+ url: url.toString(),
40
+ };
41
+ },
42
+ async exchangeCode(code, codeVerifier) {
43
+ const discovery = await getDiscovery();
44
+ const headers = {
45
+ Accept: 'application/json',
46
+ 'Content-Type': 'application/x-www-form-urlencoded',
47
+ };
48
+ if (options.dpop) {
49
+ headers.DPoP = await createDpopProof(options.dpop, {
50
+ htm: 'POST',
51
+ htu: discovery.token_endpoint,
52
+ });
53
+ }
54
+ return requestJson(fetchImpl, discovery.token_endpoint, {
55
+ body: formBody({
56
+ client_id: options.clientId,
57
+ code,
58
+ code_verifier: codeVerifier,
59
+ grant_type: 'authorization_code',
60
+ redirect_uri: options.redirectUri,
61
+ }),
62
+ headers,
63
+ method: 'POST',
64
+ });
65
+ },
66
+ async userInfo(token) {
67
+ const discovery = await getDiscovery();
68
+ if (!discovery.userinfo_endpoint) {
69
+ throw createAuthUiError('invalid_input', 'OAuth discovery document is missing userinfo_endpoint.');
70
+ }
71
+ const headers = {
72
+ Accept: 'application/json',
73
+ Authorization: `${options.dpop ? 'DPoP' : 'Bearer'} ${token}`,
74
+ };
75
+ if (options.dpop) {
76
+ headers.DPoP = await createDpopProof(options.dpop, {
77
+ accessToken: token,
78
+ htm: 'GET',
79
+ htu: discovery.userinfo_endpoint,
80
+ });
81
+ }
82
+ return requestJson(fetchImpl, discovery.userinfo_endpoint, { headers, method: 'GET' });
83
+ },
84
+ async revoke(token) {
85
+ const discovery = await getDiscovery();
86
+ if (!discovery.revocation_endpoint) {
87
+ throw createAuthUiError('invalid_input', 'OAuth discovery document is missing revocation_endpoint.');
88
+ }
89
+ const headers = {
90
+ Accept: 'application/json',
91
+ 'Content-Type': 'application/x-www-form-urlencoded',
92
+ };
93
+ if (options.dpop) {
94
+ headers.DPoP = await createDpopProof(options.dpop, {
95
+ accessToken: token,
96
+ htm: 'POST',
97
+ htu: discovery.revocation_endpoint,
98
+ });
99
+ }
100
+ await requestJson(fetchImpl, discovery.revocation_endpoint, {
101
+ body: formBody({ token }),
102
+ headers,
103
+ method: 'POST',
104
+ });
105
+ },
106
+ };
107
+ };
108
+ const requestJson = async (fetchImpl, url, init) => {
109
+ let response;
110
+ try {
111
+ response = await fetchImpl(url, init);
112
+ }
113
+ catch (cause) {
114
+ throw createAuthUiError('transport_error', 'Network request failed', { retryable: true, cause });
115
+ }
116
+ const payload = await safeJson(response);
117
+ if (!response.ok) {
118
+ throw createAuthUiError('transport_error', extractErrorMessage(payload, `Request failed with status ${response.status}`), {
119
+ cause: payload,
120
+ retryable: response.status >= 500,
121
+ });
122
+ }
123
+ return payload;
124
+ };
125
+ const safeJson = async (response) => {
126
+ const text = await response.text();
127
+ if (!text)
128
+ return null;
129
+ try {
130
+ return JSON.parse(text);
131
+ }
132
+ catch {
133
+ return text;
134
+ }
135
+ };
136
+ const extractErrorMessage = (payload, fallback) => {
137
+ if (payload && typeof payload === 'object') {
138
+ const record = payload;
139
+ if (typeof record.error === 'string' && record.error)
140
+ return record.error;
141
+ if (typeof record.message === 'string' && record.message)
142
+ return record.message;
143
+ }
144
+ return fallback;
145
+ };
146
+ const createDpopProof = async (options, input) => {
147
+ const keyPair = await getDpopKeyPair(options);
148
+ const header = base64urlJson({
149
+ alg: 'EdDSA',
150
+ jwk: keyPair.publicJwk,
151
+ typ: 'dpop+jwt',
152
+ });
153
+ const payload = base64urlJson({
154
+ ...(input.accessToken ? { ath: await sha256Base64url(input.accessToken) } : {}),
155
+ htm: input.htm,
156
+ htu: input.htu,
157
+ iat: Math.floor((options.now?.() ?? new Date()).getTime() / 1000),
158
+ jti: options.jti?.() ?? randomToken(16),
159
+ });
160
+ const signingInput = `${header}.${payload}`;
161
+ const signature = keyPair.sign
162
+ ? await keyPair.sign(textEncoder.encode(signingInput))
163
+ : await crypto.subtle.sign('Ed25519', requirePrivateKey(keyPair), textEncoder.encode(signingInput));
164
+ return `${signingInput}.${base64urlEncode(new Uint8Array(signature))}`;
165
+ };
166
+ const getDpopKeyPair = async (options) => {
167
+ const existing = await options.store.get();
168
+ if (existing)
169
+ return existing;
170
+ const generated = options.generateKeyPair ? await options.generateKeyPair() : await generateDefaultDpopKeyPair();
171
+ await options.store.set(generated);
172
+ return generated;
173
+ };
174
+ const getDpopJkt = async (options) => sha256Base64url(canonicalizeJwk((await getDpopKeyPair(options)).publicJwk));
175
+ const generateDefaultDpopKeyPair = async () => {
176
+ const keyPair = await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']);
177
+ return {
178
+ privateKey: keyPair.privateKey,
179
+ publicJwk: await crypto.subtle.exportKey('jwk', keyPair.publicKey),
180
+ };
181
+ };
182
+ const requirePrivateKey = (keyPair) => {
183
+ if (!keyPair.privateKey) {
184
+ throw createAuthUiError('invalid_input', 'OAuth DPoP key pair is missing private key material.');
185
+ }
186
+ return keyPair.privateKey;
187
+ };
188
+ const canonicalizeJwk = (jwk) => {
189
+ const keys = ['crv', 'kty', 'x', 'e', 'n'].filter((key) => typeof jwk[key] === 'string');
190
+ return `{${keys.sort().map((key) => `"${key}":"${jwk[key]}"`).join(',')}}`;
191
+ };
192
+ const formBody = (input) => {
193
+ const form = new URLSearchParams();
194
+ for (const [key, value] of Object.entries(input)) {
195
+ form.set(key, value);
196
+ }
197
+ return form.toString();
198
+ };
199
+ const generateCodeVerifier = () => randomToken(32);
200
+ const randomToken = (byteLength) => {
201
+ const bytes = new Uint8Array(byteLength);
202
+ crypto.getRandomValues(bytes);
203
+ return base64urlEncode(bytes);
204
+ };
205
+ const sha256Base64url = async (value) => {
206
+ const digest = await crypto.subtle.digest('SHA-256', textEncoder.encode(value));
207
+ return base64urlEncode(new Uint8Array(digest));
208
+ };
209
+ const base64urlJson = (value) => base64urlEncode(textEncoder.encode(JSON.stringify(value)));
210
+ const base64urlEncode = (bytes) => {
211
+ let binary = '';
212
+ for (const byte of bytes) {
213
+ binary += String.fromCharCode(byte);
214
+ }
215
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '');
216
+ };
217
+ const textEncoder = new TextEncoder();
218
+ //# sourceMappingURL=oauth-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-client.js","sourceRoot":"","sources":["../src/oauth-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ export type OAuthConsentDecision = 'approve' | 'deny';
2
+ export interface OAuthConsentDetails {
3
+ clientName: string;
4
+ redirectUri: string;
5
+ scopes: string[];
6
+ }
7
+ export interface OAuthConsentTransport {
8
+ getConsent(input: {
9
+ state: string;
10
+ }): Promise<OAuthConsentDetails>;
11
+ submitConsentDecision(input: {
12
+ decision: OAuthConsentDecision;
13
+ state: string;
14
+ }): Promise<{
15
+ redirectTo: string;
16
+ }>;
17
+ }
18
+ export interface OAuthConsentLabels {
19
+ approve: string;
20
+ approving: string;
21
+ deny: string;
22
+ denying: string;
23
+ errorGeneric: string;
24
+ loading: string;
25
+ redirectUriLabel: string;
26
+ scopeDescriptions: Record<string, string>;
27
+ scopesTitle: string;
28
+ title: string;
29
+ }
30
+ //# sourceMappingURL=oauth-consent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-consent.d.ts","sourceRoot":"","sources":["../src/oauth-consent.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,MAAM,CAAC;AAEtD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACnE,qBAAqB,CAAC,KAAK,EAAE;QAC3B,QAAQ,EAAE,oBAAoB,CAAC;QAC/B,KAAK,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=oauth-consent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-consent.js","sourceRoot":"","sources":["../src/oauth-consent.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentropic/auth-ui",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Reusable Svelte authentication UI contracts and browser passkey helpers for Sentropic-compatible apps.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,6 +32,16 @@
32
32
  "svelte": "./src/transport-fetch.ts",
33
33
  "import": "./src/transport-fetch.ts"
34
34
  },
35
+ "./oauth-client": {
36
+ "types": "./src/oauth-client.ts",
37
+ "svelte": "./src/oauth-client.ts",
38
+ "import": "./src/oauth-client.ts"
39
+ },
40
+ "./oauth-consent": {
41
+ "types": "./src/oauth-consent.ts",
42
+ "svelte": "./src/oauth-consent.ts",
43
+ "import": "./src/oauth-consent.ts"
44
+ },
35
45
  "./components/AuthLogin.svelte": {
36
46
  "types": "./src/components/AuthLogin.svelte.d.ts",
37
47
  "svelte": "./src/components/AuthLogin.svelte",
@@ -56,6 +66,11 @@
56
66
  "types": "./src/components/AuthDevicePair.svelte.d.ts",
57
67
  "svelte": "./src/components/AuthDevicePair.svelte",
58
68
  "import": "./src/components/AuthDevicePair.svelte"
69
+ },
70
+ "./components/OAuthConsent.svelte": {
71
+ "types": "./src/components/OAuthConsent.svelte.d.ts",
72
+ "svelte": "./src/components/OAuthConsent.svelte",
73
+ "import": "./src/components/OAuthConsent.svelte"
59
74
  }
60
75
  },
61
76
  "files": [
@@ -76,6 +91,7 @@
76
91
  "test": "vitest run tests"
77
92
  },
78
93
  "peerDependencies": {
94
+ "@sentropic/auth-hono": "^0.3.0",
79
95
  "@simplewebauthn/browser": "^13.2.2",
80
96
  "svelte": "^5.0.0"
81
97
  },
@@ -0,0 +1,240 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import {
4
+ createDefaultOAuthConsentLabels,
5
+ type AuthUiError,
6
+ type OAuthConsentDetails,
7
+ type OAuthConsentLabels,
8
+ type OAuthConsentTransport,
9
+ } from '../contracts.js';
10
+ import { createAuthUiError } from '../errors.js';
11
+
12
+ interface Props {
13
+ labels?: Partial<OAuthConsentLabels>;
14
+ onError?: (error: AuthUiError) => void;
15
+ onRedirect?: (url: string) => void;
16
+ state: string;
17
+ transport: OAuthConsentTransport;
18
+ }
19
+
20
+ let { labels, onError, onRedirect, state: consentState, transport }: Props = $props();
21
+
22
+ const resolvedLabels = $derived(createDefaultOAuthConsentLabels(labels ?? {}));
23
+
24
+ let details = $state<OAuthConsentDetails | null>(null);
25
+ let error = $state('');
26
+ let loading = $state(true);
27
+ let submitting = $state<'approve' | 'deny' | null>(null);
28
+
29
+ onMount(loadConsent);
30
+
31
+ async function loadConsent(): Promise<void> {
32
+ loading = true;
33
+ error = '';
34
+ try {
35
+ details = await transport.getConsent({ state: consentState });
36
+ } catch (cause) {
37
+ handleError(createAuthUiError('transport_error', resolvedLabels.errorGeneric, { cause }));
38
+ } finally {
39
+ loading = false;
40
+ }
41
+ }
42
+
43
+ async function submit(decision: 'approve' | 'deny'): Promise<void> {
44
+ submitting = decision;
45
+ error = '';
46
+ try {
47
+ const result = await transport.submitConsentDecision({ decision, state: consentState });
48
+ onRedirect?.(result.redirectTo);
49
+ } catch (cause) {
50
+ handleError(createAuthUiError('transport_error', resolvedLabels.errorGeneric, { cause }));
51
+ } finally {
52
+ submitting = null;
53
+ }
54
+ }
55
+
56
+ function handleError(err: AuthUiError): void {
57
+ error = err.message;
58
+ onError?.(err);
59
+ }
60
+ </script>
61
+
62
+ <div class="auth-ui-oauth-consent">
63
+ <slot name="branding"></slot>
64
+
65
+ {#if loading}
66
+ <div class="auth-ui-loading" role="status">
67
+ <div class="auth-ui-spinner" aria-hidden="true"></div>
68
+ <p class="auth-ui-loading__label">{resolvedLabels.loading}</p>
69
+ </div>
70
+ {:else if error}
71
+ <div class="auth-ui-alert auth-ui-alert--error" role="alert">{error}</div>
72
+ {:else if details}
73
+ <header class="auth-ui-header">
74
+ <h2 class="auth-ui-title">{resolvedLabels.title}</h2>
75
+ <p class="auth-ui-subtitle">{details.clientName}</p>
76
+ </header>
77
+
78
+ <section class="auth-ui-section" aria-labelledby="oauth-consent-scopes">
79
+ <h3 id="oauth-consent-scopes" class="auth-ui-section__title">{resolvedLabels.scopesTitle}</h3>
80
+ <ul class="auth-ui-scope-list">
81
+ {#each details.scopes as scope (scope)}
82
+ <li class="auth-ui-scope-list__item">
83
+ <strong>{scope}</strong>
84
+ <slot name="scope-description" scope={scope}>
85
+ <span>{resolvedLabels.scopeDescriptions[scope] ?? scope}</span>
86
+ </slot>
87
+ </li>
88
+ {/each}
89
+ </ul>
90
+ </section>
91
+
92
+ <section class="auth-ui-section">
93
+ <h3 class="auth-ui-section__title">{resolvedLabels.redirectUriLabel}</h3>
94
+ <p class="auth-ui-redirect-uri">{details.redirectUri}</p>
95
+ </section>
96
+
97
+ <div class="auth-ui-actions">
98
+ <button
99
+ type="button"
100
+ class="auth-ui-button auth-ui-button--primary"
101
+ disabled={submitting !== null}
102
+ onclick={() => submit('approve')}
103
+ >
104
+ {submitting === 'approve' ? resolvedLabels.approving : resolvedLabels.approve}
105
+ </button>
106
+ <button
107
+ type="button"
108
+ class="auth-ui-button auth-ui-button--secondary"
109
+ disabled={submitting !== null}
110
+ onclick={() => submit('deny')}
111
+ >
112
+ {submitting === 'deny' ? resolvedLabels.denying : resolvedLabels.deny}
113
+ </button>
114
+ </div>
115
+
116
+ <slot name="footer"></slot>
117
+ {/if}
118
+ </div>
119
+
120
+ <style>
121
+ .auth-ui-oauth-consent {
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: 1.5rem;
125
+ max-width: 32rem;
126
+ margin: 0 auto;
127
+ padding: 2rem 1rem;
128
+ font-family: var(--auth-font-family, system-ui, -apple-system, sans-serif);
129
+ color: var(--auth-text, #111827);
130
+ }
131
+ .auth-ui-header {
132
+ display: flex;
133
+ flex-direction: column;
134
+ gap: 0.25rem;
135
+ text-align: center;
136
+ }
137
+ .auth-ui-title {
138
+ margin: 0;
139
+ font-size: 1.5rem;
140
+ font-weight: 700;
141
+ }
142
+ .auth-ui-subtitle {
143
+ margin: 0;
144
+ font-size: 0.95rem;
145
+ color: var(--auth-muted, #6b7280);
146
+ }
147
+ .auth-ui-section {
148
+ display: flex;
149
+ flex-direction: column;
150
+ gap: 0.75rem;
151
+ }
152
+ .auth-ui-section__title {
153
+ margin: 0;
154
+ font-size: 0.875rem;
155
+ font-weight: 600;
156
+ }
157
+ .auth-ui-scope-list {
158
+ display: flex;
159
+ flex-direction: column;
160
+ gap: 0.75rem;
161
+ margin: 0;
162
+ padding: 0;
163
+ list-style: none;
164
+ }
165
+ .auth-ui-scope-list__item {
166
+ display: flex;
167
+ flex-direction: column;
168
+ gap: 0.25rem;
169
+ padding: 0.75rem;
170
+ border: 1px solid var(--auth-border, #e5e7eb);
171
+ border-radius: var(--auth-radius, 0.375rem);
172
+ font-size: 0.875rem;
173
+ }
174
+ .auth-ui-scope-list__item span {
175
+ color: var(--auth-muted, #6b7280);
176
+ }
177
+ .auth-ui-redirect-uri {
178
+ margin: 0;
179
+ overflow-wrap: anywhere;
180
+ font-size: 0.875rem;
181
+ color: var(--auth-muted, #6b7280);
182
+ }
183
+ .auth-ui-actions {
184
+ display: grid;
185
+ grid-template-columns: 1fr 1fr;
186
+ gap: 0.75rem;
187
+ }
188
+ .auth-ui-button {
189
+ padding: 0.625rem 1rem;
190
+ border: none;
191
+ border-radius: var(--auth-radius, 0.375rem);
192
+ font-size: 0.875rem;
193
+ font-weight: 500;
194
+ cursor: pointer;
195
+ }
196
+ .auth-ui-button:disabled {
197
+ opacity: 0.5;
198
+ cursor: not-allowed;
199
+ }
200
+ .auth-ui-button--primary {
201
+ background: var(--auth-primary, #4f46e5);
202
+ color: var(--auth-primary-text, #ffffff);
203
+ }
204
+ .auth-ui-button--secondary {
205
+ background: var(--auth-surface-muted, #f3f4f6);
206
+ color: var(--auth-text, #111827);
207
+ }
208
+ .auth-ui-alert {
209
+ padding: 0.75rem 1rem;
210
+ border-radius: var(--auth-radius, 0.375rem);
211
+ font-size: 0.875rem;
212
+ }
213
+ .auth-ui-alert--error {
214
+ background: var(--auth-error-bg, #fef2f2);
215
+ color: var(--auth-error-text, #991b1b);
216
+ }
217
+ .auth-ui-loading {
218
+ text-align: center;
219
+ padding: 3rem 0;
220
+ }
221
+ .auth-ui-spinner {
222
+ display: inline-block;
223
+ width: 3rem;
224
+ height: 3rem;
225
+ border: 2px solid transparent;
226
+ border-bottom-color: var(--auth-primary, #4f46e5);
227
+ border-radius: 50%;
228
+ animation: auth-ui-spin 0.75s linear infinite;
229
+ }
230
+ .auth-ui-loading__label {
231
+ margin-top: 1rem;
232
+ font-size: 0.875rem;
233
+ color: var(--auth-muted, #6b7280);
234
+ }
235
+ @keyframes auth-ui-spin {
236
+ to {
237
+ transform: rotate(360deg);
238
+ }
239
+ }
240
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { Component } from 'svelte';
2
+ import type {
3
+ AuthUiError,
4
+ OAuthConsentLabels,
5
+ OAuthConsentTransport,
6
+ } from '../contracts.js';
7
+
8
+ export interface OAuthConsentProps {
9
+ labels?: Partial<OAuthConsentLabels>;
10
+ onError?: (error: AuthUiError) => void;
11
+ onRedirect?: (url: string) => void;
12
+ state: string;
13
+ transport: OAuthConsentTransport;
14
+ }
15
+
16
+ declare const OAuthConsent: Component<OAuthConsentProps>;
17
+ export default OAuthConsent;
package/src/contracts.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './errors.js';
2
2
  export * from './labels.js';
3
+ export * from './oauth-consent.js';
3
4
  export * from './transport.js';
4
5
  export * from './transport-fetch.js';
5
6
  export * from './transport-types.js';
package/src/index.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from './contracts.js';
2
+ export * from './oauth-client.js';
3
+ export * from './oauth-consent.js';
2
4
  export * from './webauthn.js';
package/src/labels.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { OAuthConsentLabels } from './oauth-consent.js';
1
2
  import type { AuthUiBranding, AuthUiLabels } from './types.js';
2
3
 
3
4
  export const createDefaultAuthUiLabels = (overrides: Partial<AuthUiLabels> = {}): AuthUiLabels => ({
@@ -184,3 +185,45 @@ export const createDefaultAuthUiBranding = (overrides: Partial<AuthUiBranding> =
184
185
 
185
186
  return branding;
186
187
  };
188
+
189
+ export const createDefaultOAuthConsentLabels = (
190
+ overrides: Partial<OAuthConsentLabels> = {},
191
+ ): OAuthConsentLabels => ({
192
+ approve: 'Approve',
193
+ approving: 'Approving...',
194
+ deny: 'Deny',
195
+ denying: 'Denying...',
196
+ errorGeneric: 'Unable to load the authorization request.',
197
+ loading: 'Loading authorization request...',
198
+ redirectUriLabel: 'Redirect destination',
199
+ scopeDescriptions: {
200
+ email: 'Access your email address and email verification status.',
201
+ openid: 'Confirm your identity with this account.',
202
+ profile: 'Access your profile name.',
203
+ ...(overrides.scopeDescriptions ?? {}),
204
+ },
205
+ scopesTitle: 'This application requests access to:',
206
+ title: 'Authorize application',
207
+ ...overrides,
208
+ });
209
+
210
+ export const createFrenchOAuthConsentLabels = (
211
+ overrides: Partial<OAuthConsentLabels> = {},
212
+ ): OAuthConsentLabels =>
213
+ createDefaultOAuthConsentLabels({
214
+ approve: 'Autoriser',
215
+ approving: 'Autorisation...',
216
+ deny: 'Refuser',
217
+ denying: 'Refus...',
218
+ errorGeneric: "Impossible de charger la demande d'autorisation.",
219
+ loading: "Chargement de la demande d'autorisation...",
220
+ redirectUriLabel: 'Destination de redirection',
221
+ scopeDescriptions: {
222
+ email: "Accéder à votre adresse email et à son statut de vérification.",
223
+ openid: 'Confirmer votre identité avec ce compte.',
224
+ profile: 'Accéder au nom de votre profil.',
225
+ },
226
+ scopesTitle: 'Cette application demande accès à :',
227
+ title: "Autoriser l'application",
228
+ ...overrides,
229
+ });
@@ -0,0 +1,312 @@
1
+ import { createAuthUiError } from './errors.js';
2
+
3
+ export type OAuthFetchLike = (input: string, init?: RequestInit) => Promise<Response>;
4
+
5
+ export interface OAuthDiscoveryDocument {
6
+ authorization_endpoint: string;
7
+ revocation_endpoint?: string;
8
+ token_endpoint: string;
9
+ userinfo_endpoint?: string;
10
+ }
11
+
12
+ export interface OAuthTokenResponse {
13
+ access_token: string;
14
+ token_type: 'Bearer' | 'DPoP' | (string & {});
15
+ expires_in?: number;
16
+ id_token?: string;
17
+ scope?: string;
18
+ }
19
+
20
+ export interface OAuthAuthorizationRequest {
21
+ codeChallenge: string;
22
+ codeVerifier: string;
23
+ nonce?: string;
24
+ state?: string;
25
+ url: string;
26
+ }
27
+
28
+ export interface OAuthStartAuthorizationInput {
29
+ codeVerifier?: string;
30
+ nonce?: string;
31
+ state?: string;
32
+ }
33
+
34
+ export interface OAuthDpopKeyPair {
35
+ privateKey?: CryptoKey;
36
+ publicJwk: JsonWebKey;
37
+ sign?: (data: Uint8Array) => Promise<ArrayBuffer | Uint8Array>;
38
+ }
39
+
40
+ export interface OAuthDpopStore {
41
+ get(): Promise<OAuthDpopKeyPair | null> | OAuthDpopKeyPair | null;
42
+ set(keyPair: OAuthDpopKeyPair): Promise<void> | void;
43
+ }
44
+
45
+ export interface OAuthDpopOptions {
46
+ generateKeyPair?: () => Promise<OAuthDpopKeyPair>;
47
+ jti?: () => string;
48
+ now?: () => Date;
49
+ store: OAuthDpopStore;
50
+ }
51
+
52
+ export interface CreateOAuthClientOptions {
53
+ clientId: string;
54
+ dpop?: OAuthDpopOptions;
55
+ fetch?: OAuthFetchLike;
56
+ issuer: string;
57
+ redirectUri: string;
58
+ scopes: string[];
59
+ }
60
+
61
+ export interface OAuthClient {
62
+ exchangeCode(code: string, codeVerifier: string): Promise<OAuthTokenResponse>;
63
+ revoke(token: string): Promise<void>;
64
+ startAuthorization(input?: OAuthStartAuthorizationInput): Promise<OAuthAuthorizationRequest>;
65
+ userInfo<T = Record<string, unknown>>(token: string): Promise<T>;
66
+ }
67
+
68
+ export const createOAuthClient = (options: CreateOAuthClientOptions): OAuthClient => {
69
+ const issuer = options.issuer.replace(/\/+$/u, '');
70
+ const fetchImpl = options.fetch ?? (typeof fetch === 'function' ? fetch.bind(globalThis) : undefined);
71
+ if (!fetchImpl) {
72
+ throw createAuthUiError('invalid_input', 'No global fetch found; pass options.fetch when constructing the OAuth client.');
73
+ }
74
+
75
+ let discoveryPromise: Promise<OAuthDiscoveryDocument> | null = null;
76
+ const getDiscovery = (): Promise<OAuthDiscoveryDocument> => {
77
+ discoveryPromise ??= requestJson<OAuthDiscoveryDocument>(
78
+ fetchImpl,
79
+ `${issuer}/.well-known/openid-configuration`,
80
+ { headers: { Accept: 'application/json' }, method: 'GET' }
81
+ );
82
+ return discoveryPromise;
83
+ };
84
+
85
+ return {
86
+ async startAuthorization(input = {}) {
87
+ const [discovery, dpopJkt] = await Promise.all([
88
+ getDiscovery(),
89
+ options.dpop ? getDpopJkt(options.dpop) : Promise.resolve<string | null>(null),
90
+ ]);
91
+ const codeVerifier = input.codeVerifier ?? generateCodeVerifier();
92
+ const codeChallenge = await sha256Base64url(codeVerifier);
93
+ const url = new URL(discovery.authorization_endpoint);
94
+ url.searchParams.set('response_type', 'code');
95
+ url.searchParams.set('client_id', options.clientId);
96
+ url.searchParams.set('redirect_uri', options.redirectUri);
97
+ url.searchParams.set('scope', options.scopes.join(' '));
98
+ url.searchParams.set('code_challenge', codeChallenge);
99
+ url.searchParams.set('code_challenge_method', 'S256');
100
+ if (input.state) url.searchParams.set('state', input.state);
101
+ if (input.nonce) url.searchParams.set('nonce', input.nonce);
102
+ if (dpopJkt) url.searchParams.set('dpop_jkt', dpopJkt);
103
+
104
+ return {
105
+ codeChallenge,
106
+ codeVerifier,
107
+ nonce: input.nonce,
108
+ state: input.state,
109
+ url: url.toString(),
110
+ };
111
+ },
112
+
113
+ async exchangeCode(code, codeVerifier) {
114
+ const discovery = await getDiscovery();
115
+ const headers: Record<string, string> = {
116
+ Accept: 'application/json',
117
+ 'Content-Type': 'application/x-www-form-urlencoded',
118
+ };
119
+ if (options.dpop) {
120
+ headers.DPoP = await createDpopProof(options.dpop, {
121
+ htm: 'POST',
122
+ htu: discovery.token_endpoint,
123
+ });
124
+ }
125
+
126
+ return requestJson<OAuthTokenResponse>(fetchImpl, discovery.token_endpoint, {
127
+ body: formBody({
128
+ client_id: options.clientId,
129
+ code,
130
+ code_verifier: codeVerifier,
131
+ grant_type: 'authorization_code',
132
+ redirect_uri: options.redirectUri,
133
+ }),
134
+ headers,
135
+ method: 'POST',
136
+ });
137
+ },
138
+
139
+ async userInfo(token) {
140
+ const discovery = await getDiscovery();
141
+ if (!discovery.userinfo_endpoint) {
142
+ throw createAuthUiError('invalid_input', 'OAuth discovery document is missing userinfo_endpoint.');
143
+ }
144
+ const headers: Record<string, string> = {
145
+ Accept: 'application/json',
146
+ Authorization: `${options.dpop ? 'DPoP' : 'Bearer'} ${token}`,
147
+ };
148
+ if (options.dpop) {
149
+ headers.DPoP = await createDpopProof(options.dpop, {
150
+ accessToken: token,
151
+ htm: 'GET',
152
+ htu: discovery.userinfo_endpoint,
153
+ });
154
+ }
155
+ return requestJson(fetchImpl, discovery.userinfo_endpoint, { headers, method: 'GET' });
156
+ },
157
+
158
+ async revoke(token) {
159
+ const discovery = await getDiscovery();
160
+ if (!discovery.revocation_endpoint) {
161
+ throw createAuthUiError('invalid_input', 'OAuth discovery document is missing revocation_endpoint.');
162
+ }
163
+ const headers: Record<string, string> = {
164
+ Accept: 'application/json',
165
+ 'Content-Type': 'application/x-www-form-urlencoded',
166
+ };
167
+ if (options.dpop) {
168
+ headers.DPoP = await createDpopProof(options.dpop, {
169
+ accessToken: token,
170
+ htm: 'POST',
171
+ htu: discovery.revocation_endpoint,
172
+ });
173
+ }
174
+ await requestJson(fetchImpl, discovery.revocation_endpoint, {
175
+ body: formBody({ token }),
176
+ headers,
177
+ method: 'POST',
178
+ });
179
+ },
180
+ };
181
+ };
182
+
183
+ const requestJson = async <T>(
184
+ fetchImpl: OAuthFetchLike,
185
+ url: string,
186
+ init: RequestInit
187
+ ): Promise<T> => {
188
+ let response: Response;
189
+ try {
190
+ response = await fetchImpl(url, init);
191
+ } catch (cause) {
192
+ throw createAuthUiError('transport_error', 'Network request failed', { retryable: true, cause });
193
+ }
194
+
195
+ const payload = await safeJson(response);
196
+ if (!response.ok) {
197
+ throw createAuthUiError('transport_error', extractErrorMessage(payload, `Request failed with status ${response.status}`), {
198
+ cause: payload,
199
+ retryable: response.status >= 500,
200
+ });
201
+ }
202
+
203
+ return payload as T;
204
+ };
205
+
206
+ const safeJson = async (response: Response): Promise<unknown> => {
207
+ const text = await response.text();
208
+ if (!text) return null;
209
+ try {
210
+ return JSON.parse(text);
211
+ } catch {
212
+ return text;
213
+ }
214
+ };
215
+
216
+ const extractErrorMessage = (payload: unknown, fallback: string): string => {
217
+ if (payload && typeof payload === 'object') {
218
+ const record = payload as Record<string, unknown>;
219
+ if (typeof record.error === 'string' && record.error) return record.error;
220
+ if (typeof record.message === 'string' && record.message) return record.message;
221
+ }
222
+ return fallback;
223
+ };
224
+
225
+ const createDpopProof = async (
226
+ options: OAuthDpopOptions,
227
+ input: { accessToken?: string; htm: string; htu: string }
228
+ ): Promise<string> => {
229
+ const keyPair = await getDpopKeyPair(options);
230
+ const header = base64urlJson({
231
+ alg: 'EdDSA',
232
+ jwk: keyPair.publicJwk,
233
+ typ: 'dpop+jwt',
234
+ });
235
+ const payload = base64urlJson({
236
+ ...(input.accessToken ? { ath: await sha256Base64url(input.accessToken) } : {}),
237
+ htm: input.htm,
238
+ htu: input.htu,
239
+ iat: Math.floor((options.now?.() ?? new Date()).getTime() / 1000),
240
+ jti: options.jti?.() ?? randomToken(16),
241
+ });
242
+ const signingInput = `${header}.${payload}`;
243
+ const signature = keyPair.sign
244
+ ? await keyPair.sign(textEncoder.encode(signingInput))
245
+ : await crypto.subtle.sign('Ed25519', requirePrivateKey(keyPair), textEncoder.encode(signingInput));
246
+ return `${signingInput}.${base64urlEncode(new Uint8Array(signature))}`;
247
+ };
248
+
249
+ const getDpopKeyPair = async (options: OAuthDpopOptions): Promise<OAuthDpopKeyPair> => {
250
+ const existing = await options.store.get();
251
+ if (existing) return existing;
252
+ const generated = options.generateKeyPair ? await options.generateKeyPair() : await generateDefaultDpopKeyPair();
253
+ await options.store.set(generated);
254
+ return generated;
255
+ };
256
+
257
+ const getDpopJkt = async (options: OAuthDpopOptions): Promise<string> =>
258
+ sha256Base64url(canonicalizeJwk((await getDpopKeyPair(options)).publicJwk));
259
+
260
+ const generateDefaultDpopKeyPair = async (): Promise<OAuthDpopKeyPair> => {
261
+ const keyPair = await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']);
262
+ return {
263
+ privateKey: keyPair.privateKey,
264
+ publicJwk: await crypto.subtle.exportKey('jwk', keyPair.publicKey),
265
+ };
266
+ };
267
+
268
+ const requirePrivateKey = (keyPair: OAuthDpopKeyPair): CryptoKey => {
269
+ if (!keyPair.privateKey) {
270
+ throw createAuthUiError('invalid_input', 'OAuth DPoP key pair is missing private key material.');
271
+ }
272
+ return keyPair.privateKey;
273
+ };
274
+
275
+ const canonicalizeJwk = (jwk: JsonWebKey): string => {
276
+ const keys = ['crv', 'kty', 'x', 'e', 'n'].filter((key) => typeof (jwk as Record<string, unknown>)[key] === 'string');
277
+ return `{${keys.sort().map((key) => `"${key}":"${(jwk as Record<string, string>)[key]}"`).join(',')}}`;
278
+ };
279
+
280
+ const formBody = (input: Record<string, string>): string => {
281
+ const form = new URLSearchParams();
282
+ for (const [key, value] of Object.entries(input)) {
283
+ form.set(key, value);
284
+ }
285
+ return form.toString();
286
+ };
287
+
288
+ const generateCodeVerifier = (): string => randomToken(32);
289
+
290
+ const randomToken = (byteLength: number): string => {
291
+ const bytes = new Uint8Array(byteLength);
292
+ crypto.getRandomValues(bytes);
293
+ return base64urlEncode(bytes);
294
+ };
295
+
296
+ const sha256Base64url = async (value: string): Promise<string> => {
297
+ const digest = await crypto.subtle.digest('SHA-256', textEncoder.encode(value));
298
+ return base64urlEncode(new Uint8Array(digest));
299
+ };
300
+
301
+ const base64urlJson = (value: unknown): string =>
302
+ base64urlEncode(textEncoder.encode(JSON.stringify(value)));
303
+
304
+ const base64urlEncode = (bytes: Uint8Array): string => {
305
+ let binary = '';
306
+ for (const byte of bytes) {
307
+ binary += String.fromCharCode(byte);
308
+ }
309
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '');
310
+ };
311
+
312
+ const textEncoder = new TextEncoder();
@@ -0,0 +1,28 @@
1
+ export type OAuthConsentDecision = 'approve' | 'deny';
2
+
3
+ export interface OAuthConsentDetails {
4
+ clientName: string;
5
+ redirectUri: string;
6
+ scopes: string[];
7
+ }
8
+
9
+ export interface OAuthConsentTransport {
10
+ getConsent(input: { state: string }): Promise<OAuthConsentDetails>;
11
+ submitConsentDecision(input: {
12
+ decision: OAuthConsentDecision;
13
+ state: string;
14
+ }): Promise<{ redirectTo: string }>;
15
+ }
16
+
17
+ export interface OAuthConsentLabels {
18
+ approve: string;
19
+ approving: string;
20
+ deny: string;
21
+ denying: string;
22
+ errorGeneric: string;
23
+ loading: string;
24
+ redirectUriLabel: string;
25
+ scopeDescriptions: Record<string, string>;
26
+ scopesTitle: string;
27
+ title: string;
28
+ }