@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 ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial publishable SDK skeleton.
6
+ - PKCE helper support.
7
+ - Authorize URL generation.
8
+ - Token exchange and refresh helpers.
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+.
@@ -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
+ }