login-hub-sdk 0.1.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 +52 -0
- package/dist/client.d.ts +24 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +34 -0
- package/dist/client.js.map +1 -0
- package/dist/cookies.d.ts +13 -0
- package/dist/cookies.d.ts.map +1 -0
- package/dist/cookies.js +31 -0
- package/dist/cookies.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -0
- package/dist/http.d.ts +3 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +22 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/next.d.ts +57 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +119 -0
- package/dist/next.js.map +1 -0
- package/dist/token.d.ts +22 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +50 -0
- package/dist/token.js.map +1 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/url.d.ts +22 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +32 -0
- package/dist/url.js.map +1 -0
- package/dist/userinfo.d.ts +5 -0
- package/dist/userinfo.d.ts.map +1 -0
- package/dist/userinfo.js +9 -0
- package/dist/userinfo.js.map +1 -0
- package/dist/verify.d.ts +12 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +33 -0
- package/dist/verify.js.map +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# login-hub-sdk
|
|
2
|
+
|
|
3
|
+
Thin, framework-agnostic TypeScript client for the login-hub OAuth2
|
|
4
|
+
authorization server. Implements `docs/api-contract.md` exactly — no OAuth
|
|
5
|
+
flow code to write, no 3rd-party provider SDKs to touch.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Published as a **private** package on GitHub Packages. See `PUBLISHING.md`
|
|
10
|
+
for the `.npmrc`/auth setup a consuming app needs.
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "dependencies": { "login-hub-sdk": "^0.1.0" } }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For local development against this monorepo you can still use a file
|
|
17
|
+
reference instead: `"file:../path/to/packages/sdk"`.
|
|
18
|
+
|
|
19
|
+
## Core API (`login-hub-sdk`)
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createLoginUrl, exchangeCodeForToken, verifyAccessToken, getUserInfo, refreshAccessToken, logout, LoginHubClient } from "login-hub-sdk";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- `createLoginUrl(config, { redirectUri, state, appName? })` — pure URL builder for `GET /authorize`.
|
|
26
|
+
- `exchangeCodeForToken(config, { code, redirectUri })` — `POST /token` (authorization_code). **Server-only** (needs `clientSecret`).
|
|
27
|
+
- `refreshAccessToken(config, refreshToken)` — `POST /token` (refresh_token, rotates).
|
|
28
|
+
- `verifyAccessToken(config, accessToken)` — stateless JWKS verification (`jose`), no network call to hub beyond a cached JWKS fetch.
|
|
29
|
+
- `getUserInfo(config, accessToken)` — `GET /userinfo` (always-fresh DB values).
|
|
30
|
+
- `logout(config, refreshToken)` — `POST /logout`.
|
|
31
|
+
- `LoginHubClient` — class wrapping all of the above under one configured instance.
|
|
32
|
+
|
|
33
|
+
## Next.js helpers (`login-hub-sdk/next`)
|
|
34
|
+
|
|
35
|
+
Built on Web-standard `Request`/`Response`/`Headers` — works in Node and Edge
|
|
36
|
+
runtimes, no hard dependency on the `next` package.
|
|
37
|
+
|
|
38
|
+
- `loginRedirect(config, { redirectUri, appName? })` → `Response` (302 + state cookie).
|
|
39
|
+
- `handleCallback(config, request, { redirectUri, successPath? })` → `Response` (verifies state, exchanges code, sets session cookies).
|
|
40
|
+
- `getSession(config, cookieStore)` → `{ claims, accessToken } | null` (works with Next's `cookies()`).
|
|
41
|
+
- `refreshSession(config, request)` → new tokens + `Response` carrying updated `Set-Cookie` headers.
|
|
42
|
+
- `logoutResponse(config, request, { redirectTo? })` → revokes + clears cookies.
|
|
43
|
+
|
|
44
|
+
See `examples/next` for full wiring (3 route files + a page reading the session).
|
|
45
|
+
|
|
46
|
+
## Test
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install
|
|
50
|
+
npm test # vitest: URL building, token exchange (mocked fetch), JWKS verification, cookies
|
|
51
|
+
npm run build # tsc -> dist/
|
|
52
|
+
```
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type CreateLoginUrlOptions } from "./url.js";
|
|
2
|
+
import { type VerifyAccessTokenOptions } from "./verify.js";
|
|
3
|
+
import type { HubTokenClaims, HubUser, LoginHubConfig, TokenResponse } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Convenience wrapper bundling every SDK call under one configured instance.
|
|
6
|
+
* Equivalent to calling the standalone functions with `config` each time —
|
|
7
|
+
* use whichever style fits your codebase.
|
|
8
|
+
*/
|
|
9
|
+
export declare class LoginHubClient {
|
|
10
|
+
private readonly config;
|
|
11
|
+
constructor(config: LoginHubConfig);
|
|
12
|
+
createLoginUrl(options: CreateLoginUrlOptions): string;
|
|
13
|
+
exchangeCodeForToken(params: {
|
|
14
|
+
code: string;
|
|
15
|
+
redirectUri: string;
|
|
16
|
+
}): Promise<TokenResponse>;
|
|
17
|
+
refreshAccessToken(refreshToken: string): Promise<TokenResponse>;
|
|
18
|
+
logout(refreshToken: string): Promise<{
|
|
19
|
+
ok: true;
|
|
20
|
+
}>;
|
|
21
|
+
getUserInfo(accessToken: string): Promise<HubUser>;
|
|
22
|
+
verifyAccessToken(accessToken: string, options?: VerifyAccessTokenOptions): Promise<HubTokenClaims>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAGtE,OAAO,EAAqB,KAAK,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEzF;;;;GAIG;AACH,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,cAAc;IAEnD,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM;IAItD,oBAAoB,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,CAAC;IAI3F,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAIhE,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,CAAC;IAInD,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIlD,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,wBAAwB,GAAG,OAAO,CAAC,cAAc,CAAC;CAGpG"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createLoginUrl } from "./url.js";
|
|
2
|
+
import { exchangeCodeForToken, logout, refreshAccessToken } from "./token.js";
|
|
3
|
+
import { getUserInfo } from "./userinfo.js";
|
|
4
|
+
import { verifyAccessToken } from "./verify.js";
|
|
5
|
+
/**
|
|
6
|
+
* Convenience wrapper bundling every SDK call under one configured instance.
|
|
7
|
+
* Equivalent to calling the standalone functions with `config` each time —
|
|
8
|
+
* use whichever style fits your codebase.
|
|
9
|
+
*/
|
|
10
|
+
export class LoginHubClient {
|
|
11
|
+
config;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
}
|
|
15
|
+
createLoginUrl(options) {
|
|
16
|
+
return createLoginUrl(this.config, options);
|
|
17
|
+
}
|
|
18
|
+
exchangeCodeForToken(params) {
|
|
19
|
+
return exchangeCodeForToken(this.config, params);
|
|
20
|
+
}
|
|
21
|
+
refreshAccessToken(refreshToken) {
|
|
22
|
+
return refreshAccessToken(this.config, refreshToken);
|
|
23
|
+
}
|
|
24
|
+
logout(refreshToken) {
|
|
25
|
+
return logout(this.config, refreshToken);
|
|
26
|
+
}
|
|
27
|
+
getUserInfo(accessToken) {
|
|
28
|
+
return getUserInfo(this.config, accessToken);
|
|
29
|
+
}
|
|
30
|
+
verifyAccessToken(accessToken, options) {
|
|
31
|
+
return verifyAccessToken(this.config, accessToken, options);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAA8B,MAAM,UAAU,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAiC,MAAM,aAAa,CAAC;AAG/E;;;;GAIG;AACH,MAAM,OAAO,cAAc;IACI;IAA7B,YAA6B,MAAsB;QAAtB,WAAM,GAAN,MAAM,CAAgB;IAAG,CAAC;IAEvD,cAAc,CAAC,OAA8B;QAC3C,OAAO,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC;IAED,oBAAoB,CAAC,MAA6C;QAChE,OAAO,oBAAoB,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnD,CAAC;IAED,kBAAkB,CAAC,YAAoB;QACrC,OAAO,kBAAkB,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,CAAC,YAAoB;QACzB,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC3C,CAAC;IAED,WAAW,CAAC,WAAmB;QAC7B,OAAO,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC/C,CAAC;IAED,iBAAiB,CAAC,WAAmB,EAAE,OAAkC;QACvE,OAAO,iBAAiB,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;IAC9D,CAAC;CACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CookieOptions {
|
|
2
|
+
maxAge?: number;
|
|
3
|
+
path?: string;
|
|
4
|
+
httpOnly?: boolean;
|
|
5
|
+
secure?: boolean;
|
|
6
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
7
|
+
}
|
|
8
|
+
/** Minimal `Set-Cookie` serializer — kept dependency-free so the SDK doesn't
|
|
9
|
+
* force consumers onto a specific `cookie` package version. */
|
|
10
|
+
export declare function serializeCookie(name: string, value: string, options?: CookieOptions): string;
|
|
11
|
+
/** Parses a raw `Cookie` request header into a name→value map. */
|
|
12
|
+
export declare function parseCookieHeader(header: string | null | undefined): Record<string, string>;
|
|
13
|
+
//# sourceMappingURL=cookies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../src/cookies.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;CACtC;AAED;+DAC+D;AAC/D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAQhG;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAW3F"}
|
package/dist/cookies.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Minimal `Set-Cookie` serializer — kept dependency-free so the SDK doesn't
|
|
2
|
+
* force consumers onto a specific `cookie` package version. */
|
|
3
|
+
export function serializeCookie(name, value, options = {}) {
|
|
4
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
5
|
+
if (options.maxAge !== undefined)
|
|
6
|
+
parts.push(`Max-Age=${Math.floor(options.maxAge)}`);
|
|
7
|
+
parts.push(`Path=${options.path ?? "/"}`);
|
|
8
|
+
if (options.httpOnly !== false)
|
|
9
|
+
parts.push("HttpOnly");
|
|
10
|
+
if (options.secure)
|
|
11
|
+
parts.push("Secure");
|
|
12
|
+
parts.push(`SameSite=${options.sameSite ?? "Lax"}`);
|
|
13
|
+
return parts.join("; ");
|
|
14
|
+
}
|
|
15
|
+
/** Parses a raw `Cookie` request header into a name→value map. */
|
|
16
|
+
export function parseCookieHeader(header) {
|
|
17
|
+
const out = {};
|
|
18
|
+
if (!header)
|
|
19
|
+
return out;
|
|
20
|
+
for (const pair of header.split(";")) {
|
|
21
|
+
const idx = pair.indexOf("=");
|
|
22
|
+
if (idx === -1)
|
|
23
|
+
continue;
|
|
24
|
+
const key = pair.slice(0, idx).trim();
|
|
25
|
+
const value = pair.slice(idx + 1).trim();
|
|
26
|
+
if (key)
|
|
27
|
+
out[key] = decodeURIComponent(value);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=cookies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.js","sourceRoot":"","sources":["../src/cookies.ts"],"names":[],"mappings":"AAQA;+DAC+D;AAC/D,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,KAAa,EAAE,UAAyB,EAAE;IACtF,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,IAAI,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACvD,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACtF,KAAK,CAAC,IAAI,CAAC,QAAQ,OAAO,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC;IAC1C,IAAI,OAAO,CAAC,QAAQ,KAAK,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACvD,IAAI,OAAO,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACzC,KAAK,CAAC,IAAI,CAAC,YAAY,OAAO,CAAC,QAAQ,IAAI,KAAK,EAAE,CAAC,CAAC;IACpD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,iBAAiB,CAAC,MAAiC;IACjE,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,IAAI,CAAC,MAAM;QAAE,OAAO,GAAG,CAAC;IACxB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,GAAG,KAAK,CAAC,CAAC;YAAE,SAAS;QACzB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACzC,IAAI,GAAG;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HubErrorBody } from "./types.js";
|
|
2
|
+
/** Thrown for any non-2xx response from the hub, and for JWT verification
|
|
3
|
+
* failures. `error`/`errorDescription` mirror api-contract.md §5's enum. */
|
|
4
|
+
export declare class LoginHubError extends Error {
|
|
5
|
+
readonly error: string;
|
|
6
|
+
readonly errorDescription?: string;
|
|
7
|
+
readonly status?: number;
|
|
8
|
+
constructor(body: HubErrorBody, status?: number);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;4EAC4E;AAC5E,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;gBAEb,IAAI,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM;CAOhD"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Thrown for any non-2xx response from the hub, and for JWT verification
|
|
2
|
+
* failures. `error`/`errorDescription` mirror api-contract.md §5's enum. */
|
|
3
|
+
export class LoginHubError extends Error {
|
|
4
|
+
error;
|
|
5
|
+
errorDescription;
|
|
6
|
+
status;
|
|
7
|
+
constructor(body, status) {
|
|
8
|
+
super(body.errorDescription ?? body.error);
|
|
9
|
+
this.name = "LoginHubError";
|
|
10
|
+
this.error = body.error;
|
|
11
|
+
this.errorDescription = body.errorDescription;
|
|
12
|
+
this.status = status;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA;4EAC4E;AAC5E,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,KAAK,CAAS;IACd,gBAAgB,CAAU;IAC1B,MAAM,CAAU;IAEzB,YAAY,IAAkB,EAAE,MAAe;QAC7C,KAAK,CAAC,IAAI,CAAC,gBAAgB,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,CAAC;QAC9C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF"}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function postJson<T>(fetchImpl: typeof fetch, url: string, body: Record<string, unknown>, headers?: Record<string, string>): Promise<T>;
|
|
2
|
+
export declare function getJson<T>(fetchImpl: typeof fetch, url: string, headers?: Record<string, string>): Promise<T>;
|
|
3
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAGA,wBAAsB,QAAQ,CAAC,CAAC,EAC9B,SAAS,EAAE,OAAO,KAAK,EACvB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,CAAC,CAAC,CAWZ;AAED,wBAAsB,OAAO,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAOnH"}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { LoginHubError } from "./errors.js";
|
|
2
|
+
export async function postJson(fetchImpl, url, body, headers) {
|
|
3
|
+
const res = await fetchImpl(url, {
|
|
4
|
+
method: "POST",
|
|
5
|
+
headers: { "content-type": "application/json", ...headers },
|
|
6
|
+
body: JSON.stringify(body),
|
|
7
|
+
});
|
|
8
|
+
const json = (await res.json().catch(() => ({})));
|
|
9
|
+
if (!res.ok) {
|
|
10
|
+
throw new LoginHubError(json ?? { error: "server_error" }, res.status);
|
|
11
|
+
}
|
|
12
|
+
return json;
|
|
13
|
+
}
|
|
14
|
+
export async function getJson(fetchImpl, url, headers) {
|
|
15
|
+
const res = await fetchImpl(url, { method: "GET", headers });
|
|
16
|
+
const json = (await res.json().catch(() => ({})));
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
throw new LoginHubError(json ?? { error: "server_error" }, res.status);
|
|
19
|
+
}
|
|
20
|
+
return json;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,SAAuB,EACvB,GAAW,EACX,IAA6B,EAC7B,OAAgC;IAEhC,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE;QAC/B,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,GAAG,OAAO,EAAE;QAC3D,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KAC3B,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAY,CAAC;IAC7D,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,aAAa,CAAE,IAAqB,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3F,CAAC;IACD,OAAO,IAAS,CAAC;AACnB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAI,SAAuB,EAAE,GAAW,EAAE,OAAgC;IACrG,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;IAC7D,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAY,CAAC;IAC7D,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,aAAa,CAAE,IAAqB,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3F,CAAC;IACD,OAAO,IAAS,CAAC;AACnB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC"}
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { HubTokenClaims, LoginHubConfig } from "./types.js";
|
|
2
|
+
export declare const SESSION_COOKIE = "hub_session";
|
|
3
|
+
export declare const REFRESH_COOKIE = "hub_refresh";
|
|
4
|
+
export declare const STATE_COOKIE = "hub_state";
|
|
5
|
+
/**
|
|
6
|
+
* Builds the redirect response for your login route (e.g. `app/login/route.ts`):
|
|
7
|
+
* sets a short-lived CSRF `state` cookie and 302s to the hub's `/authorize`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function loginRedirect(config: Pick<LoginHubConfig, "hubUrl" | "clientId">, options: {
|
|
10
|
+
redirectUri: string;
|
|
11
|
+
appName?: string;
|
|
12
|
+
secure?: boolean;
|
|
13
|
+
}): Response;
|
|
14
|
+
/**
|
|
15
|
+
* Handles the callback route (e.g. `app/auth/callback/route.ts`): verifies
|
|
16
|
+
* `state`, exchanges `code` for tokens, sets session cookies, and 302s to
|
|
17
|
+
* `successPath`. Throws on missing/mismatched state or a hub error - wrap in
|
|
18
|
+
* try/catch and redirect to an error page.
|
|
19
|
+
*/
|
|
20
|
+
export declare function handleCallback(config: LoginHubConfig, request: Request, options: {
|
|
21
|
+
redirectUri: string;
|
|
22
|
+
successPath?: string;
|
|
23
|
+
secure?: boolean;
|
|
24
|
+
}): Promise<Response>;
|
|
25
|
+
export interface CookieReader {
|
|
26
|
+
get(name: string): {
|
|
27
|
+
value: string;
|
|
28
|
+
} | undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reads + verifies the session from cookies (works with Next's `cookies()`
|
|
32
|
+
* from `next/headers`, or any object shaped `{ get(name) => {value} }`).
|
|
33
|
+
* Returns `null` if there's no valid session - does *not* attempt refresh
|
|
34
|
+
* (call `refreshSession` explicitly, since that needs to write cookies).
|
|
35
|
+
*/
|
|
36
|
+
export declare function getSession(config: Pick<LoginHubConfig, "hubUrl" | "clientId">, cookies: CookieReader): Promise<{
|
|
37
|
+
claims: HubTokenClaims;
|
|
38
|
+
accessToken: string;
|
|
39
|
+
} | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Uses the refresh cookie to mint a new access token and returns a Response
|
|
42
|
+
* with updated `Set-Cookie` headers (rotation - api-contract §6). Call this
|
|
43
|
+
* from middleware/a route handler when `getSession` returns null but a
|
|
44
|
+
* refresh cookie is present.
|
|
45
|
+
*/
|
|
46
|
+
export declare function refreshSession(config: LoginHubConfig, request: Request, options?: {
|
|
47
|
+
secure?: boolean;
|
|
48
|
+
}): Promise<{
|
|
49
|
+
accessToken: string;
|
|
50
|
+
response: Response;
|
|
51
|
+
} | null>;
|
|
52
|
+
/** Revokes the refresh token at the hub and clears session cookies. */
|
|
53
|
+
export declare function logoutResponse(config: LoginHubConfig, request: Request, options?: {
|
|
54
|
+
redirectTo?: string;
|
|
55
|
+
secure?: boolean;
|
|
56
|
+
}): Promise<Response>;
|
|
57
|
+
//# sourceMappingURL=next.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjE,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAC5C,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAC5C,eAAO,MAAM,YAAY,cAAc,CAAC;AAUxC;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,UAAU,CAAC,EACnD,OAAO,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GACnE,QAAQ,CASV;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GACvE,OAAO,CAAC,QAAQ,CAAC,CAmCnB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CAClD;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,UAAU,CAAC,EACnD,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CASjE;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GAAG,IAAI,CAAC,CAwB7D;AAED,uEAAuE;AACvE,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACtD,OAAO,CAAC,QAAQ,CAAC,CAUnB"}
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js integration helpers. Built on the Web-standard Request/Response/
|
|
3
|
+
* Headers APIs so they work in both App Router Route Handlers (Node + Edge
|
|
4
|
+
* runtime) without importing `next` directly - keeps `next` an optional
|
|
5
|
+
* peer dependency (see package.json).
|
|
6
|
+
*/
|
|
7
|
+
import { createLoginUrl } from "./url.js";
|
|
8
|
+
import { exchangeCodeForToken, logout as hubLogout, refreshAccessToken } from "./token.js";
|
|
9
|
+
import { verifyAccessToken } from "./verify.js";
|
|
10
|
+
import { parseCookieHeader, serializeCookie } from "./cookies.js";
|
|
11
|
+
export const SESSION_COOKIE = "hub_session";
|
|
12
|
+
export const REFRESH_COOKIE = "hub_refresh";
|
|
13
|
+
export const STATE_COOKIE = "hub_state";
|
|
14
|
+
const THIRTY_DAYS_SECONDS = 30 * 24 * 60 * 60;
|
|
15
|
+
function randomState() {
|
|
16
|
+
const bytes = new Uint8Array(24);
|
|
17
|
+
crypto.getRandomValues(bytes);
|
|
18
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Builds the redirect response for your login route (e.g. `app/login/route.ts`):
|
|
22
|
+
* sets a short-lived CSRF `state` cookie and 302s to the hub's `/authorize`.
|
|
23
|
+
*/
|
|
24
|
+
export function loginRedirect(config, options) {
|
|
25
|
+
const state = randomState();
|
|
26
|
+
const url = createLoginUrl(config, { redirectUri: options.redirectUri, state, appName: options.appName });
|
|
27
|
+
const headers = new Headers({ Location: url });
|
|
28
|
+
headers.append("Set-Cookie", serializeCookie(STATE_COOKIE, state, { maxAge: 600, httpOnly: true, secure: options.secure }));
|
|
29
|
+
return new Response(null, { status: 302, headers });
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Handles the callback route (e.g. `app/auth/callback/route.ts`): verifies
|
|
33
|
+
* `state`, exchanges `code` for tokens, sets session cookies, and 302s to
|
|
34
|
+
* `successPath`. Throws on missing/mismatched state or a hub error - wrap in
|
|
35
|
+
* try/catch and redirect to an error page.
|
|
36
|
+
*/
|
|
37
|
+
export async function handleCallback(config, request, options) {
|
|
38
|
+
const reqUrl = new URL(request.url);
|
|
39
|
+
const code = reqUrl.searchParams.get("code");
|
|
40
|
+
const state = reqUrl.searchParams.get("state");
|
|
41
|
+
const error = reqUrl.searchParams.get("error");
|
|
42
|
+
const cookies = parseCookieHeader(request.headers.get("cookie"));
|
|
43
|
+
if (error) {
|
|
44
|
+
throw new Error(`login failed: ${error}`);
|
|
45
|
+
}
|
|
46
|
+
if (!code || !state || state !== cookies[STATE_COOKIE]) {
|
|
47
|
+
throw new Error("invalid or missing OAuth state (possible CSRF or expired session)");
|
|
48
|
+
}
|
|
49
|
+
const tokens = await exchangeCodeForToken(config, { code, redirectUri: options.redirectUri });
|
|
50
|
+
const headers = new Headers({ Location: options.successPath ?? "/" });
|
|
51
|
+
headers.append("Set-Cookie", serializeCookie(SESSION_COOKIE, tokens.accessToken, {
|
|
52
|
+
maxAge: tokens.expiresIn,
|
|
53
|
+
httpOnly: true,
|
|
54
|
+
secure: options.secure,
|
|
55
|
+
}));
|
|
56
|
+
headers.append("Set-Cookie", serializeCookie(REFRESH_COOKIE, tokens.refreshToken, {
|
|
57
|
+
maxAge: THIRTY_DAYS_SECONDS,
|
|
58
|
+
httpOnly: true,
|
|
59
|
+
secure: options.secure,
|
|
60
|
+
}));
|
|
61
|
+
headers.append("Set-Cookie", serializeCookie(STATE_COOKIE, "", { maxAge: 0, secure: options.secure }));
|
|
62
|
+
return new Response(null, { status: 302, headers });
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Reads + verifies the session from cookies (works with Next's `cookies()`
|
|
66
|
+
* from `next/headers`, or any object shaped `{ get(name) => {value} }`).
|
|
67
|
+
* Returns `null` if there's no valid session - does *not* attempt refresh
|
|
68
|
+
* (call `refreshSession` explicitly, since that needs to write cookies).
|
|
69
|
+
*/
|
|
70
|
+
export async function getSession(config, cookies) {
|
|
71
|
+
const accessToken = cookies.get(SESSION_COOKIE)?.value;
|
|
72
|
+
if (!accessToken)
|
|
73
|
+
return null;
|
|
74
|
+
try {
|
|
75
|
+
const claims = await verifyAccessToken(config, accessToken);
|
|
76
|
+
return { claims, accessToken };
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Uses the refresh cookie to mint a new access token and returns a Response
|
|
84
|
+
* with updated `Set-Cookie` headers (rotation - api-contract §6). Call this
|
|
85
|
+
* from middleware/a route handler when `getSession` returns null but a
|
|
86
|
+
* refresh cookie is present.
|
|
87
|
+
*/
|
|
88
|
+
export async function refreshSession(config, request, options = {}) {
|
|
89
|
+
const cookies = parseCookieHeader(request.headers.get("cookie"));
|
|
90
|
+
const refreshToken = cookies[REFRESH_COOKIE];
|
|
91
|
+
if (!refreshToken)
|
|
92
|
+
return null;
|
|
93
|
+
const tokens = await refreshAccessToken(config, refreshToken);
|
|
94
|
+
const headers = new Headers();
|
|
95
|
+
headers.append("Set-Cookie", serializeCookie(SESSION_COOKIE, tokens.accessToken, {
|
|
96
|
+
maxAge: tokens.expiresIn,
|
|
97
|
+
httpOnly: true,
|
|
98
|
+
secure: options.secure,
|
|
99
|
+
}));
|
|
100
|
+
headers.append("Set-Cookie", serializeCookie(REFRESH_COOKIE, tokens.refreshToken, {
|
|
101
|
+
maxAge: THIRTY_DAYS_SECONDS,
|
|
102
|
+
httpOnly: true,
|
|
103
|
+
secure: options.secure,
|
|
104
|
+
}));
|
|
105
|
+
return { accessToken: tokens.accessToken, response: new Response(null, { headers }) };
|
|
106
|
+
}
|
|
107
|
+
/** Revokes the refresh token at the hub and clears session cookies. */
|
|
108
|
+
export async function logoutResponse(config, request, options = {}) {
|
|
109
|
+
const cookies = parseCookieHeader(request.headers.get("cookie"));
|
|
110
|
+
const refreshToken = cookies[REFRESH_COOKIE];
|
|
111
|
+
if (refreshToken) {
|
|
112
|
+
await hubLogout(config, refreshToken).catch(() => undefined);
|
|
113
|
+
}
|
|
114
|
+
const headers = new Headers(options.redirectTo ? { Location: options.redirectTo } : undefined);
|
|
115
|
+
headers.append("Set-Cookie", serializeCookie(SESSION_COOKIE, "", { maxAge: 0, secure: options.secure }));
|
|
116
|
+
headers.append("Set-Cookie", serializeCookie(REFRESH_COOKIE, "", { maxAge: 0, secure: options.secure }));
|
|
117
|
+
return new Response(null, { status: options.redirectTo ? 302 : 200, headers });
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=next.js.map
|
package/dist/next.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next.js","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"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"}
|
package/dist/token.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LoginHubConfig, TokenResponse } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* `POST /token` grant_type=authorization_code — exchanges the code from the
|
|
4
|
+
* hub's redirect for tokens (api-contract §2). Call this from your callback
|
|
5
|
+
* route handler, server-side only.
|
|
6
|
+
*/
|
|
7
|
+
export declare function exchangeCodeForToken(config: LoginHubConfig, params: {
|
|
8
|
+
code: string;
|
|
9
|
+
redirectUri: string;
|
|
10
|
+
}): Promise<TokenResponse>;
|
|
11
|
+
/**
|
|
12
|
+
* `POST /token` grant_type=refresh_token — rotates the refresh token and
|
|
13
|
+
* issues a fresh access token (api-contract §2, §6 rotation). The returned
|
|
14
|
+
* `refreshToken` replaces the old one; the old one is revoked server-side.
|
|
15
|
+
*/
|
|
16
|
+
export declare function refreshAccessToken(config: LoginHubConfig, refreshToken: string): Promise<TokenResponse>;
|
|
17
|
+
/** `POST /logout` — revokes a refresh token (api-contract §2). Access tokens
|
|
18
|
+
* are not revocable; the caller should also clear its own session cookie. */
|
|
19
|
+
export declare function logout(config: LoginHubConfig, refreshToken: string): Promise<{
|
|
20
|
+
ok: true;
|
|
21
|
+
}>;
|
|
22
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAWhE;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,aAAa,CAAC,CAUxB;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAS7G;AAED;6EAC6E;AAC7E,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC,CAQhG"}
|
package/dist/token.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { postJson } from "./http.js";
|
|
2
|
+
function requireSecret(config) {
|
|
3
|
+
if (!config.clientSecret) {
|
|
4
|
+
throw new Error("clientSecret is required for this call - keep it server-side only (BFF model, api-contract.md §0)");
|
|
5
|
+
}
|
|
6
|
+
return config.clientSecret;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* `POST /token` grant_type=authorization_code — exchanges the code from the
|
|
10
|
+
* hub's redirect for tokens (api-contract §2). Call this from your callback
|
|
11
|
+
* route handler, server-side only.
|
|
12
|
+
*/
|
|
13
|
+
export async function exchangeCodeForToken(config, params) {
|
|
14
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
15
|
+
const url = new URL("/token", config.hubUrl).toString();
|
|
16
|
+
return postJson(fetchImpl, url, {
|
|
17
|
+
grant_type: "authorization_code",
|
|
18
|
+
client_id: config.clientId,
|
|
19
|
+
client_secret: requireSecret(config),
|
|
20
|
+
code: params.code,
|
|
21
|
+
redirect_uri: params.redirectUri,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `POST /token` grant_type=refresh_token — rotates the refresh token and
|
|
26
|
+
* issues a fresh access token (api-contract §2, §6 rotation). The returned
|
|
27
|
+
* `refreshToken` replaces the old one; the old one is revoked server-side.
|
|
28
|
+
*/
|
|
29
|
+
export async function refreshAccessToken(config, refreshToken) {
|
|
30
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
31
|
+
const url = new URL("/token", config.hubUrl).toString();
|
|
32
|
+
return postJson(fetchImpl, url, {
|
|
33
|
+
grant_type: "refresh_token",
|
|
34
|
+
client_id: config.clientId,
|
|
35
|
+
client_secret: requireSecret(config),
|
|
36
|
+
refresh_token: refreshToken,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** `POST /logout` — revokes a refresh token (api-contract §2). Access tokens
|
|
40
|
+
* are not revocable; the caller should also clear its own session cookie. */
|
|
41
|
+
export async function logout(config, refreshToken) {
|
|
42
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
43
|
+
const url = new URL("/logout", config.hubUrl).toString();
|
|
44
|
+
return postJson(fetchImpl, url, {
|
|
45
|
+
clientId: config.clientId,
|
|
46
|
+
clientSecret: requireSecret(config),
|
|
47
|
+
refreshToken,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGrC,SAAS,aAAa,CAAC,MAAsB;IAC3C,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,mGAAmG,CACpG,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC,YAAY,CAAC;AAC7B,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAsB,EACtB,MAA6C;IAE7C,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IACxD,OAAO,QAAQ,CAAgB,SAAS,EAAE,GAAG,EAAE;QAC7C,UAAU,EAAE,oBAAoB;QAChC,SAAS,EAAE,MAAM,CAAC,QAAQ;QAC1B,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC;QACpC,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,YAAY,EAAE,MAAM,CAAC,WAAW;KACjC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAsB,EAAE,YAAoB;IACnF,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IACxD,OAAO,QAAQ,CAAgB,SAAS,EAAE,GAAG,EAAE;QAC7C,UAAU,EAAE,eAAe;QAC3B,SAAS,EAAE,MAAM,CAAC,QAAQ;QAC1B,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC;QACpC,aAAa,EAAE,YAAY;KAC5B,CAAC,CAAC;AACL,CAAC;AAED;6EAC6E;AAC7E,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,MAAsB,EAAE,YAAoB;IACvE,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IACzD,OAAO,QAAQ,CAAe,SAAS,EAAE,GAAG,EAAE;QAC5C,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,YAAY,EAAE,aAAa,CAAC,MAAM,CAAC;QACnC,YAAY;KACb,CAAC,CAAC;AACL,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Config shared by every SDK call. Mirrors docs/api-contract.md §1 step 2. */
|
|
2
|
+
export interface LoginHubConfig {
|
|
3
|
+
/** Hub base URL, e.g. "https://login.example.com" (no trailing slash). */
|
|
4
|
+
hubUrl: string;
|
|
5
|
+
/** Client ID issued by `POST /clients`. */
|
|
6
|
+
clientId: string;
|
|
7
|
+
/**
|
|
8
|
+
* Client secret issued by `POST /clients`. Required for /token and /logout
|
|
9
|
+
* calls. **Server-side only** — never ship this to a browser bundle
|
|
10
|
+
* (BFF model, api-contract.md §0).
|
|
11
|
+
*/
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
/** Optional custom fetch implementation (tests, non-global-fetch runtimes). */
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
/** Normalized user object — identical shape for all 3 providers (api-contract §3). */
|
|
17
|
+
export interface HubUser {
|
|
18
|
+
id: string;
|
|
19
|
+
provider: "google" | "naver" | "kakao";
|
|
20
|
+
email: string | null;
|
|
21
|
+
nickname: string;
|
|
22
|
+
avatarUrl: string | null;
|
|
23
|
+
}
|
|
24
|
+
/** `POST /token` response (both grant types), api-contract §2. */
|
|
25
|
+
export interface TokenResponse {
|
|
26
|
+
tokenType: "Bearer";
|
|
27
|
+
accessToken: string;
|
|
28
|
+
expiresIn: number;
|
|
29
|
+
refreshToken: string;
|
|
30
|
+
user: HubUser;
|
|
31
|
+
}
|
|
32
|
+
/** Decoded access-token JWT claims, api-contract §4. */
|
|
33
|
+
export interface HubTokenClaims {
|
|
34
|
+
sub: string;
|
|
35
|
+
iss: string;
|
|
36
|
+
aud: string;
|
|
37
|
+
iat: number;
|
|
38
|
+
exp: number;
|
|
39
|
+
email: string | null;
|
|
40
|
+
nickname: string;
|
|
41
|
+
avatarUrl: string | null;
|
|
42
|
+
provider: "google" | "naver" | "kakao";
|
|
43
|
+
}
|
|
44
|
+
export interface HubErrorBody {
|
|
45
|
+
error: string;
|
|
46
|
+
errorDescription?: string;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,sFAAsF;AACtF,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;IACvC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,QAAQ,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/dist/url.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LoginHubConfig } from "./types.js";
|
|
2
|
+
export interface CreateLoginUrlOptions {
|
|
3
|
+
/** Consumer's own callback route (must be registered in `redirectUris`). */
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
/** CSRF-protection value — generate per-session, store, and compare on callback. */
|
|
6
|
+
state: string;
|
|
7
|
+
/** Optional display name shown as "{appName}에 로그인" on the hub's login screen. */
|
|
8
|
+
appName?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Builds the `GET /authorize` URL to send the browser to (api-contract §1
|
|
12
|
+
* step 3 / §2). This does not make a network call — it's a pure URL builder
|
|
13
|
+
* so callers can `redirect()` or render an `<a href>` with it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createLoginUrl(config: Pick<LoginHubConfig, "hubUrl" | "clientId">, options: CreateLoginUrlOptions): string;
|
|
16
|
+
/**
|
|
17
|
+
* Generates a cryptographically random opaque string suitable for the OAuth
|
|
18
|
+
* `state` param. Works in both Node (>=19, webcrypto global) and edge
|
|
19
|
+
* runtimes (Next.js middleware/edge routes).
|
|
20
|
+
*/
|
|
21
|
+
export declare function generateState(byteLength?: number): string;
|
|
22
|
+
//# sourceMappingURL=url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,qBAAqB;IACpC,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;IACpB,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,UAAU,CAAC,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAO1H;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,UAAU,SAAK,GAAG,MAAM,CAUrD"}
|
package/dist/url.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the `GET /authorize` URL to send the browser to (api-contract §1
|
|
3
|
+
* step 3 / §2). This does not make a network call — it's a pure URL builder
|
|
4
|
+
* so callers can `redirect()` or render an `<a href>` with it.
|
|
5
|
+
*/
|
|
6
|
+
export function createLoginUrl(config, options) {
|
|
7
|
+
const url = new URL("/authorize", config.hubUrl);
|
|
8
|
+
url.searchParams.set("client_id", config.clientId);
|
|
9
|
+
url.searchParams.set("redirect_uri", options.redirectUri);
|
|
10
|
+
url.searchParams.set("state", options.state);
|
|
11
|
+
if (options.appName)
|
|
12
|
+
url.searchParams.set("app_name", options.appName);
|
|
13
|
+
return url.toString();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generates a cryptographically random opaque string suitable for the OAuth
|
|
17
|
+
* `state` param. Works in both Node (>=19, webcrypto global) and edge
|
|
18
|
+
* runtimes (Next.js middleware/edge routes).
|
|
19
|
+
*/
|
|
20
|
+
export function generateState(byteLength = 24) {
|
|
21
|
+
const bytes = new Uint8Array(byteLength);
|
|
22
|
+
const cryptoObj = globalThis.crypto;
|
|
23
|
+
if (!cryptoObj) {
|
|
24
|
+
throw new Error("global crypto is unavailable in this runtime - pass a pre-generated state instead");
|
|
25
|
+
}
|
|
26
|
+
cryptoObj.getRandomValues(bytes);
|
|
27
|
+
let out = "";
|
|
28
|
+
for (const b of bytes)
|
|
29
|
+
out += b.toString(16).padStart(2, "0");
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=url.js.map
|
package/dist/url.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.js","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAAmD,EAAE,OAA8B;IAChH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACjD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACnD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1D,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7C,IAAI,OAAO,CAAC,OAAO;QAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACvE,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,UAAU,GAAG,EAAE;IAC3C,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC;IACzC,MAAM,SAAS,GAAI,UAAoD,CAAC,MAAM,CAAC;IAC/E,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,mFAAmF,CAAC,CAAC;IACvG,CAAC;IACD,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IACjC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,GAAG,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9D,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HubUser, LoginHubConfig } from "./types.js";
|
|
2
|
+
/** `GET /userinfo` — always-fresh user data from the hub DB (api-contract §2),
|
|
3
|
+
* unlike the JWT claims which are a snapshot from token-issue time. */
|
|
4
|
+
export declare function getUserInfo(config: Pick<LoginHubConfig, "hubUrl" | "fetch">, accessToken: string): Promise<HubUser>;
|
|
5
|
+
//# sourceMappingURL=userinfo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"userinfo.d.ts","sourceRoot":"","sources":["../src/userinfo.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE1D;uEACuE;AACvE,wBAAsB,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,OAAO,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAIzH"}
|
package/dist/userinfo.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getJson } from "./http.js";
|
|
2
|
+
/** `GET /userinfo` — always-fresh user data from the hub DB (api-contract §2),
|
|
3
|
+
* unlike the JWT claims which are a snapshot from token-issue time. */
|
|
4
|
+
export async function getUserInfo(config, accessToken) {
|
|
5
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
6
|
+
const url = new URL("/userinfo", config.hubUrl).toString();
|
|
7
|
+
return getJson(fetchImpl, url, { authorization: `Bearer ${accessToken}` });
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=userinfo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"userinfo.js","sourceRoot":"","sources":["../src/userinfo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC;uEACuE;AACvE,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAgD,EAAE,WAAmB;IACrG,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC3D,OAAO,OAAO,CAAU,SAAS,EAAE,GAAG,EAAE,EAAE,aAAa,EAAE,UAAU,WAAW,EAAE,EAAE,CAAC,CAAC;AACtF,CAAC"}
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HubTokenClaims, LoginHubConfig } from "./types.js";
|
|
2
|
+
export interface VerifyAccessTokenOptions {
|
|
3
|
+
/** Override the expected `iss` claim. Defaults to "login-hub" (api-contract §4). */
|
|
4
|
+
issuer?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Stateless JWKS verification of a hub-issued access token (api-contract §4).
|
|
8
|
+
* No network call to the hub beyond the (cached) JWKS fetch — safe to call on
|
|
9
|
+
* every request in a middleware/edge function.
|
|
10
|
+
*/
|
|
11
|
+
export declare function verifyAccessToken(config: Pick<LoginHubConfig, "hubUrl" | "clientId">, accessToken: string, options?: VerifyAccessTokenOptions): Promise<HubTokenClaims>;
|
|
12
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAiBjE,MAAM,WAAW,wBAAwB;IACvC,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,UAAU,CAAC,EACnD,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,cAAc,CAAC,CAczB"}
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
2
|
+
import { LoginHubError } from "./errors.js";
|
|
3
|
+
const DEFAULT_ISSUER = "login-hub";
|
|
4
|
+
// One JWKS fetcher per hubUrl, reused across calls (jose caches keys +
|
|
5
|
+
// handles `kid` rotation internally - api-contract §2 jwks.json note).
|
|
6
|
+
const jwksCache = new Map();
|
|
7
|
+
function getJwks(hubUrl) {
|
|
8
|
+
let jwks = jwksCache.get(hubUrl);
|
|
9
|
+
if (!jwks) {
|
|
10
|
+
jwks = createRemoteJWKSet(new URL("/.well-known/jwks.json", hubUrl));
|
|
11
|
+
jwksCache.set(hubUrl, jwks);
|
|
12
|
+
}
|
|
13
|
+
return jwks;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Stateless JWKS verification of a hub-issued access token (api-contract §4).
|
|
17
|
+
* No network call to the hub beyond the (cached) JWKS fetch — safe to call on
|
|
18
|
+
* every request in a middleware/edge function.
|
|
19
|
+
*/
|
|
20
|
+
export async function verifyAccessToken(config, accessToken, options = {}) {
|
|
21
|
+
const jwks = getJwks(config.hubUrl);
|
|
22
|
+
try {
|
|
23
|
+
const { payload } = await jwtVerify(accessToken, jwks, {
|
|
24
|
+
issuer: options.issuer ?? DEFAULT_ISSUER,
|
|
25
|
+
audience: config.clientId,
|
|
26
|
+
});
|
|
27
|
+
return payload;
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
throw new LoginHubError({ error: "invalid_token", errorDescription: err instanceof Error ? err.message : String(err) }, 401);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAwB,MAAM,MAAM,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,MAAM,cAAc,GAAG,WAAW,CAAC;AAEnC,uEAAuE;AACvE,uEAAuE;AACvE,MAAM,SAAS,GAAG,IAAI,GAAG,EAA2B,CAAC;AAErD,SAAS,OAAO,CAAC,MAAc;IAC7B,IAAI,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,IAAI,GAAG,kBAAkB,CAAC,IAAI,GAAG,CAAC,wBAAwB,EAAE,MAAM,CAAC,CAAC,CAAC;QACrE,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAOD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAmD,EACnD,WAAmB,EACnB,UAAoC,EAAE;IAEtC,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE;YACrD,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,cAAc;YACxC,QAAQ,EAAE,MAAM,CAAC,QAAQ;SAC1B,CAAC,CAAC;QACH,OAAO,OAAoC,CAAC;IAC9C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,aAAa,CACrB,EAAE,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAC9F,GAAG,CACJ,CAAC;IACJ,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "login-hub-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Thin framework-agnostic client for the login-hub OAuth2 authorization server (see docs/api-contract.md).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./next": {
|
|
14
|
+
"types": "./dist/next.d.ts",
|
|
15
|
+
"import": "./dist/next.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/ngsk2784-lab/login.git",
|
|
24
|
+
"directory": "packages/sdk"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"registry": "https://registry.npmjs.org",
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:watch": "vitest"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"next": ">=13"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"next": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"jose": "^6.2.3",
|
|
46
|
+
"qrcode": "^1.5.4",
|
|
47
|
+
"speakeasy": "^2.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.10.5",
|
|
51
|
+
"typescript": "^5.7.3",
|
|
52
|
+
"vitest": "^4.1.10"
|
|
53
|
+
}
|
|
54
|
+
}
|