@last1id/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/CHANGELOG.md +8 -0
- package/README.md +38 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +85 -0
- package/package.json +26 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @last1id/sdk
|
|
2
|
+
|
|
3
|
+
Zero-to-login SDK for Last1 ID OIDC Authorization Code + PKCE.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @last1id/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createPkcePair, buildAuthorizeUrl, exchangeCode } from "@last1id/sdk";
|
|
15
|
+
|
|
16
|
+
const pkce = await createPkcePair();
|
|
17
|
+
const authorizeUrl = buildAuthorizeUrl(
|
|
18
|
+
{
|
|
19
|
+
issuerUrl: "https://id.last1.com",
|
|
20
|
+
clientId: process.env.LAST1_CLIENT_ID!,
|
|
21
|
+
redirectUri: "https://app.example.com/auth/callback",
|
|
22
|
+
scopes: ["openid", "profile", "offline_access"]
|
|
23
|
+
},
|
|
24
|
+
pkce
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Redirect user to authorizeUrl, then exchange code in callback route.
|
|
28
|
+
const tokenSet = await exchangeCode({
|
|
29
|
+
issuerUrl: "https://id.last1.com",
|
|
30
|
+
clientId: process.env.LAST1_CLIENT_ID!,
|
|
31
|
+
clientSecret: process.env.LAST1_CLIENT_SECRET!,
|
|
32
|
+
redirectUri: "https://app.example.com/auth/callback",
|
|
33
|
+
code: "AUTH_CODE",
|
|
34
|
+
codeVerifier: pkce.codeVerifier
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> `createPkcePair()` uses Web Crypto and returns a Promise. It works in modern browsers and Node 20+.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface PkcePair {
|
|
2
|
+
codeVerifier: string;
|
|
3
|
+
codeChallenge: string;
|
|
4
|
+
codeChallengeMethod: "S256";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface AuthorizeOptions {
|
|
8
|
+
issuerUrl: string;
|
|
9
|
+
clientId: string;
|
|
10
|
+
redirectUri: string;
|
|
11
|
+
scopes?: string[];
|
|
12
|
+
state?: string;
|
|
13
|
+
nonce?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TokenExchangeOptions {
|
|
17
|
+
issuerUrl: string;
|
|
18
|
+
clientId: string;
|
|
19
|
+
redirectUri: string;
|
|
20
|
+
code: string;
|
|
21
|
+
codeVerifier: string;
|
|
22
|
+
clientSecret?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RefreshTokenOptions {
|
|
26
|
+
issuerUrl: string;
|
|
27
|
+
clientId: string;
|
|
28
|
+
refreshToken: string;
|
|
29
|
+
clientSecret?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export declare function createPkcePair(): Promise<PkcePair>;
|
|
33
|
+
export declare function fetchDiscovery(issuerUrl: string): Promise<Record<string, unknown>>;
|
|
34
|
+
export declare function buildAuthorizeUrl(options: AuthorizeOptions, pkce: PkcePair): string;
|
|
35
|
+
export declare function exchangeCode(options: TokenExchangeOptions): Promise<Record<string, unknown>>;
|
|
36
|
+
export declare function refreshToken(options: RefreshTokenOptions): Promise<Record<string, unknown>>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
function getWebCrypto() {
|
|
2
|
+
const c = globalThis.crypto;
|
|
3
|
+
if (!c?.getRandomValues || !c?.subtle) {
|
|
4
|
+
throw new Error("Web Crypto API is required (Node 20+ or modern browser)");
|
|
5
|
+
}
|
|
6
|
+
return c;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function base64UrlEncode(bytes) {
|
|
10
|
+
if (typeof Buffer !== "undefined") {
|
|
11
|
+
return Buffer.from(bytes).toString("base64url");
|
|
12
|
+
}
|
|
13
|
+
let binary = "";
|
|
14
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
15
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function randomToken(bytes = 32) {
|
|
19
|
+
const arr = new Uint8Array(bytes);
|
|
20
|
+
getWebCrypto().getRandomValues(arr);
|
|
21
|
+
return base64UrlEncode(arr);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function createPkcePair() {
|
|
25
|
+
const codeVerifier = randomToken(48);
|
|
26
|
+
const input = new TextEncoder().encode(codeVerifier);
|
|
27
|
+
const digest = await getWebCrypto().subtle.digest("SHA-256", input);
|
|
28
|
+
const codeChallenge = base64UrlEncode(new Uint8Array(digest));
|
|
29
|
+
return { codeVerifier, codeChallenge, codeChallengeMethod: "S256" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function fetchDiscovery(issuerUrl) {
|
|
33
|
+
const issuer = issuerUrl.replace(/\/$/, "");
|
|
34
|
+
const response = await fetch(`${issuer}/.well-known/openid-configuration`);
|
|
35
|
+
if (!response.ok) throw new Error(`Discovery failed (${response.status})`);
|
|
36
|
+
return response.json();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildAuthorizeUrl(options, pkce) {
|
|
40
|
+
const params = new URLSearchParams({
|
|
41
|
+
response_type: "code",
|
|
42
|
+
client_id: options.clientId,
|
|
43
|
+
redirect_uri: options.redirectUri,
|
|
44
|
+
scope: (options.scopes?.length ? options.scopes : ["openid", "profile", "offline_access"]).join(" "),
|
|
45
|
+
state: options.state ?? randomToken(24),
|
|
46
|
+
nonce: options.nonce ?? randomToken(24),
|
|
47
|
+
code_challenge: pkce.codeChallenge,
|
|
48
|
+
code_challenge_method: pkce.codeChallengeMethod
|
|
49
|
+
});
|
|
50
|
+
return `${options.issuerUrl.replace(/\/$/, "")}/oauth/authorize?${params.toString()}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function exchangeCode(options) {
|
|
54
|
+
const body = new URLSearchParams({
|
|
55
|
+
grant_type: "authorization_code",
|
|
56
|
+
code: options.code,
|
|
57
|
+
redirect_uri: options.redirectUri,
|
|
58
|
+
client_id: options.clientId,
|
|
59
|
+
code_verifier: options.codeVerifier
|
|
60
|
+
});
|
|
61
|
+
if (options.clientSecret) body.set("client_secret", options.clientSecret);
|
|
62
|
+
const response = await fetch(`${options.issuerUrl.replace(/\/$/, "")}/oauth/token`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
65
|
+
body
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) throw new Error(`Token exchange failed (${response.status})`);
|
|
68
|
+
return response.json();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function refreshToken(options) {
|
|
72
|
+
const body = new URLSearchParams({
|
|
73
|
+
grant_type: "refresh_token",
|
|
74
|
+
refresh_token: options.refreshToken,
|
|
75
|
+
client_id: options.clientId
|
|
76
|
+
});
|
|
77
|
+
if (options.clientSecret) body.set("client_secret", options.clientSecret);
|
|
78
|
+
const response = await fetch(`${options.issuerUrl.replace(/\/$/, "")}/oauth/token`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
81
|
+
body
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) throw new Error(`Refresh token failed (${response.status})`);
|
|
84
|
+
return response.json();
|
|
85
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@last1id/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-to-login OIDC SDK for Last1 ID",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"oidc",
|
|
22
|
+
"oauth2",
|
|
23
|
+
"pkce",
|
|
24
|
+
"last1"
|
|
25
|
+
]
|
|
26
|
+
}
|