@alien-id/sso 1.0.25

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 ADDED
@@ -0,0 +1,87 @@
1
+ # @alien_org/sso-sdk-core
2
+
3
+ Core TypeScript client for [Alien SSO](https://alien.org) authentication. Provides OIDC-compatible authentication with blockchain and TEE backing.
4
+
5
+ ## ⚠️ Alpha Version Notice
6
+
7
+ **This is an early alpha version.** The SDK is under active development and may contain bugs or undergo breaking changes. Use with caution in production environments.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @alien_org/sso-sdk-core
13
+ ```
14
+
15
+ ## Features
16
+
17
+ - ✅ **TypeScript-first** with full type safety
18
+ - ✅ **Runtime validation** via Zod schemas
19
+ - ✅ **PKCE support** for secure authorization
20
+ - ✅ **Dual exports**: ESM and CJS
21
+ - ✅ **Zero UI dependencies** - use in any JavaScript environment
22
+ - ✅ **Storage management** for tokens and session data
23
+
24
+ ## Documentation
25
+
26
+ 📚 **Full documentation at [dev.alien.org/docs](https://dev.alien.org/docs)**
27
+
28
+ - **[Integration Guide](https://dev.alien.org/docs/sso-guide/core-integration)** - Complete integration walkthrough
29
+ - **[API Reference](https://dev.alien.org/docs/sso-api-reference/api-reference-core)** - Detailed API documentation
30
+ - **[What is Alien Session?](https://dev.alien.org/docs/what-is-alien-session)** - Session architecture explained
31
+ - **[Demo App](https://dev.alien.org/docs/sso-demo-app)** - Example application
32
+
33
+ ### React Integration
34
+
35
+ If you're using React, check out [@alien_org/sso-sdk-react](https://www.npmjs.com/package/@alien_org/sso-sdk-react) for hooks and pre-built components:
36
+
37
+ ```bash
38
+ npm install @alien_org/sso-sdk-react
39
+ ```
40
+
41
+ ## Authentication Flow
42
+
43
+ 1. **Generate deeplink** → Display QR code or redirect
44
+ 2. **User authenticates** in Alien mobile app
45
+ 3. **Poll for completion** → Get authorization code
46
+ 4. **Exchange code** → Receive access token
47
+ 5. **Verify token** → Validate with server (optional)
48
+
49
+ ## Storage
50
+
51
+ The SDK uses browser storage for session management:
52
+
53
+ - **localStorage**: `alien-sso_access_token` - Access token
54
+ - **sessionStorage**: `alien-sso_code_verifier` - PKCE code verifier
55
+
56
+ ## Getting a Provider Address
57
+
58
+ Register your application at the [Developer Portal](https://dev.alien.org/dashboard) to get your provider credentials.
59
+
60
+ ## TypeScript Support
61
+
62
+ Includes full TypeScript declarations with Zod runtime validation:
63
+
64
+ ```typescript
65
+ import type {
66
+ AlienSsoClientConfig,
67
+ AuthorizeResponse,
68
+ PollResponse,
69
+ ExchangeCodeResponse,
70
+ TokenInfo
71
+ } from '@alien_org/sso-sdk-core';
72
+ ```
73
+
74
+ ## Browser Support
75
+
76
+ - Modern browsers with ES2020+ support
77
+ - Chrome, Firefox, Safari, Edge (latest versions)
78
+
79
+ ## License
80
+
81
+ MIT
82
+
83
+ ## Links
84
+
85
+ - [Documentation](https://dev.alien.org/docs)
86
+ - [GitHub Repository](https://github.com/alien-org/sso-sdk-js)
87
+ - [NPM Package](https://www.npmjs.com/package/@alien_org/sso-sdk-core)
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var h=(u,e,o)=>new Promise((r,s)=>{var a=l=>{try{c(o.next(l))}catch(S){s(S)}},n=l=>{try{c(o.throw(l))}catch(S){s(S)}},c=l=>l.done?r(l.value):Promise.resolve(l.value).then(a,n);c((o=o.apply(u,e)).next())});Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("zod/v4-mini"),I=require("js-sha256"),y=t.z.object({deep_link:t.z.string(),polling_code:t.z.string(),expired_at:t.z.number()}),T=t.z.object({polling_code:t.z.string()}),j=["pending","authorized","rejected","expired"],x=t.z.enum(j),z=t.z.object({status:x,authorization_code:t.z.optional(t.z.string())}),m=t.z.object({access_token:t.z.string(),token_type:t.z.string(),expires_in:t.z.number(),id_token:t.z.optional(t.z.string()),refresh_token:t.z.string()}),R=t.z.object({sub:t.z.string()}),A=t.z.object({iss:t.z.string(),sub:t.z.string(),aud:t.z.union([t.z.string(),t.z.array(t.z.string())]),exp:t.z.number(),iat:t.z.number(),nonce:t.z.optional(t.z.string()),auth_time:t.z.optional(t.z.number())}),E=m;function _(u){return btoa(u).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function w(u){let e=u.replace(/-/g,"+").replace(/_/g,"/");for(;e.length%4;)e+="=";return atob(e)}const v="https://sso.alien.com",P=5e3,i="alien-sso_",g=i+"refresh_token",p=i+"token_expiry",f=(u,e)=>new URL(e,u).toString(),b=t.z.object({ssoBaseUrl:t.z.url(),providerAddress:t.z.string(),pollingInterval:t.z.optional(t.z.number())}),d=class d{constructor(e){this.config=b.parse(e),this.ssoBaseUrl=this.config.ssoBaseUrl||v,this.providerAddress=this.config.providerAddress,this.pollingInterval=this.config.pollingInterval||P}generateCodeVerifier(e=128){let o;const r=typeof window!="undefined"&&window.crypto;if(r&&r.getRandomValues)o=new Uint8Array(e),r.getRandomValues(o);else{o=new Uint8Array(e);for(let a=0;a<e;a++)o[a]=Math.floor(Math.random()*256)}let s="";for(let a=0;a<o.length;a++)s+=String.fromCharCode(o[a]);return _(s)}generateCodeChallenge(e){const o=I.sha256.array(e),r=String.fromCharCode(...o);return _(r)}generateDeeplink(){return h(this,null,function*(){const e=this.generateCodeVerifier(),o=this.generateCodeChallenge(e);sessionStorage.setItem(i+"code_verifier",e);const r=new URLSearchParams({response_type:"code",response_mode:"json",client_id:this.providerAddress,scope:"openid",code_challenge:o,code_challenge_method:"S256"}),s=`${this.config.ssoBaseUrl}/oauth/authorize?${r.toString()}`,a=yield fetch(s,{method:"GET"});if(!a.ok){const c=yield a.json().catch(()=>({error:a.statusText}));throw new Error(`Authorize failed: ${c.error_description||c.error||a.statusText}`)}const n=yield a.json();return y.parse(n)})}pollAuth(e){return h(this,null,function*(){const o={polling_code:e};T.parse(o);const r=yield fetch(f(this.config.ssoBaseUrl,"/oauth/poll"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});if(!r.ok)throw new Error(`Poll failed: ${r.statusText}`);const s=yield r.json();return z.parse(s)})}exchangeToken(e){return h(this,null,function*(){const o=sessionStorage.getItem(i+"code_verifier");if(!o)throw new Error("Missing code verifier.");const r=new URLSearchParams({grant_type:"authorization_code",code:e,client_id:this.providerAddress,code_verifier:o}),s=yield fetch(f(this.config.ssoBaseUrl,"/oauth/token"),{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:r.toString()});if(!s.ok){const l=yield s.json().catch(()=>({error:s.statusText}));throw new Error(`Token exchange failed: ${l.error_description||l.error||s.statusText}`)}const a=yield s.json(),n=m.parse(a);localStorage.setItem(i+"access_token",n.access_token),n.id_token&&localStorage.setItem(i+"id_token",n.id_token),localStorage.setItem(g,n.refresh_token);const c=Date.now()+n.expires_in*1e3;return localStorage.setItem(p,c.toString()),sessionStorage.removeItem(i+"code_verifier"),n})}verifyAuth(){return h(this,null,function*(){return this.withAutoRefresh(()=>h(this,null,function*(){const e=this.getAccessToken();if(!e)return null;const o=yield fetch(f(this.config.ssoBaseUrl,"/oauth/userinfo"),{method:"GET",headers:{Authorization:`Bearer ${e}`}});if(!o.ok){if(o.status===401){const s=new Error("Unauthorized");throw s.response={status:401},s}return null}const r=yield o.json();return R.parse(r)}))})}getAccessToken(){return localStorage.getItem(i+"access_token")}getIdToken(){return localStorage.getItem(i+"id_token")}getAuthData(){const e=this.getIdToken()||this.getAccessToken();if(!e)return null;const o=e.split(".");if(o.length!==3)return null;let r;try{const n=w(o[0]);r=JSON.parse(n)}catch(n){return null}if(r.alg!=="RS256"||r.typ!=="JWT")return null;let s;try{const n=JSON.parse(w(o[1]));s=A.parse(n)}catch(n){return null}return(Array.isArray(s.aud)?s.aud:[s.aud]).includes(this.providerAddress)?s:null}getSubject(){const e=this.getAuthData();return(e==null?void 0:e.sub)||null}isTokenExpired(){const e=this.getAuthData();return e?Date.now()/1e3>e.exp:!0}logout(){localStorage.removeItem(i+"access_token"),localStorage.removeItem(i+"id_token"),localStorage.removeItem(g),localStorage.removeItem(p),sessionStorage.removeItem(i+"code_verifier")}getRefreshToken(){return localStorage.getItem(g)}hasRefreshToken(){return!!this.getRefreshToken()}isAccessTokenExpired(){const e=localStorage.getItem(p);if(!e)return!0;const o=parseInt(e,10),r=Date.now(),s=300*1e3;return r>=o-s}refreshAccessToken(){return h(this,null,function*(){return d.refreshPromise||(d.refreshPromise=this.doRefreshAccessToken().finally(()=>{d.refreshPromise=null})),d.refreshPromise})}doRefreshAccessToken(){return h(this,null,function*(){const e=this.getRefreshToken();if(!e)throw new Error("No refresh token available");const o=new URLSearchParams({grant_type:"refresh_token",refresh_token:e,client_id:this.providerAddress}),r=yield fetch(f(this.config.ssoBaseUrl,"/oauth/token"),{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:o.toString()});if(!r.ok){const c=yield r.json().catch(()=>({error:r.statusText}));throw this.logout(),new Error(`Token refresh failed: ${c.error_description||c.error||r.statusText}`)}const s=yield r.json(),a=m.parse(s);localStorage.setItem(i+"access_token",a.access_token),a.id_token&&localStorage.setItem(i+"id_token",a.id_token),localStorage.setItem(g,a.refresh_token);const n=Date.now()+a.expires_in*1e3;return localStorage.setItem(p,n.toString()),a})}withAutoRefresh(e,o=1){return h(this,null,function*(){var r,s,a;try{return yield e()}catch(n){if((((r=n==null?void 0:n.response)==null?void 0:r.status)===401||((s=n==null?void 0:n.message)==null?void 0:s.includes("401"))||((a=n==null?void 0:n.message)==null?void 0:a.includes("Unauthorized")))&&o>0&&this.hasRefreshToken())try{return yield this.refreshAccessToken(),yield e()}catch(l){throw n}throw n}})}};d.refreshPromise=null;let k=d;exports.AlienSsoClient=k;exports.AlienSsoClientSchema=b;exports.AuthorizeResponseSchema=y;exports.ExchangeCodeResponseSchema=E;exports.PollRequestSchema=T;exports.PollResponseSchema=z;exports.TokenInfoSchema=A;exports.TokenResponseSchema=m;exports.UserInfoResponseSchema=R;
@@ -0,0 +1,186 @@
1
+ import { z } from 'zod/v4-mini';
2
+
3
+ export declare class AlienSsoClient {
4
+ readonly config: AlienSsoClientConfig;
5
+ readonly pollingInterval: number;
6
+ readonly ssoBaseUrl: string;
7
+ readonly providerAddress: string;
8
+ private static refreshPromise;
9
+ constructor(config: AlienSsoClientConfig);
10
+ private generateCodeVerifier;
11
+ private generateCodeChallenge;
12
+ /**
13
+ * Initiates OAuth2 authorization flow with response_mode=json for SPA
14
+ * GET /oauth/authorize?response_type=code&response_mode=json&...
15
+ */
16
+ generateDeeplink(): Promise<AuthorizeResponse>;
17
+ /**
18
+ * Polls for authorization completion
19
+ * POST /oauth/poll
20
+ */
21
+ pollAuth(pollingCode: string): Promise<PollResponse>;
22
+ /**
23
+ * Exchanges authorization code for tokens
24
+ * POST /oauth/token (application/x-www-form-urlencoded)
25
+ * Returns both access_token and id_token
26
+ */
27
+ exchangeToken(authorizationCode: string): Promise<TokenResponse>;
28
+ /**
29
+ * Verifies authentication by calling userinfo endpoint
30
+ * GET /oauth/userinfo
31
+ * Automatically refreshes token on 401 if refresh token is available
32
+ */
33
+ verifyAuth(): Promise<UserInfoResponse | null>;
34
+ /**
35
+ * Gets stored access token
36
+ */
37
+ getAccessToken(): string | null;
38
+ /**
39
+ * Gets stored ID token
40
+ */
41
+ getIdToken(): string | null;
42
+ /**
43
+ * Decodes and validates JWT token to extract claims
44
+ * Works with both access_token and id_token (EdDSA signed)
45
+ */
46
+ getAuthData(): TokenInfo | null;
47
+ /**
48
+ * Gets the subject (user identifier) from the token
49
+ */
50
+ getSubject(): string | null;
51
+ /**
52
+ * Checks if the current token is expired
53
+ */
54
+ isTokenExpired(): boolean;
55
+ /**
56
+ * Clears all stored authentication data
57
+ */
58
+ logout(): void;
59
+ /**
60
+ * Gets stored refresh token
61
+ */
62
+ getRefreshToken(): string | null;
63
+ /**
64
+ * Checks if a refresh token is available
65
+ */
66
+ hasRefreshToken(): boolean;
67
+ /**
68
+ * Checks if the access token is expired or will expire soon (within 5 minutes)
69
+ */
70
+ isAccessTokenExpired(): boolean;
71
+ /**
72
+ * Refreshes the access token using the stored refresh token
73
+ * POST /oauth/token with grant_type=refresh_token
74
+ * Uses singleton pattern to prevent concurrent refresh requests (race condition)
75
+ */
76
+ refreshAccessToken(): Promise<TokenResponse>;
77
+ /**
78
+ * Internal method that performs the actual token refresh
79
+ */
80
+ private doRefreshAccessToken;
81
+ /**
82
+ * Executes a function that makes an authenticated request
83
+ * Automatically refreshes token and retries on 401 error
84
+ */
85
+ withAutoRefresh<T>(requestFn: () => Promise<T>, maxRetries?: number): Promise<T>;
86
+ }
87
+
88
+ export declare type AlienSsoClientConfig = z.infer<typeof AlienSsoClientSchema>;
89
+
90
+ export declare const AlienSsoClientSchema: z.ZodMiniObject<{
91
+ ssoBaseUrl: z.ZodMiniURL;
92
+ providerAddress: z.ZodMiniString<string>;
93
+ pollingInterval: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
94
+ }, z.core.$strip>;
95
+
96
+ export declare type AuthorizeResponse = z.infer<typeof AuthorizeResponseSchema>;
97
+
98
+ /**
99
+ * Authorize response schema (for response_mode=json)
100
+ * GET /oauth/authorize?response_mode=json&...
101
+ */
102
+ export declare const AuthorizeResponseSchema: z.ZodMiniObject<{
103
+ deep_link: z.ZodMiniString<string>;
104
+ polling_code: z.ZodMiniString<string>;
105
+ expired_at: z.ZodMiniNumber<number>;
106
+ }, z.core.$strip>;
107
+
108
+ export declare type ExchangeCodeResponse = TokenResponse;
109
+
110
+ export declare const ExchangeCodeResponseSchema: z.ZodMiniObject<{
111
+ access_token: z.ZodMiniString<string>;
112
+ token_type: z.ZodMiniString<string>;
113
+ expires_in: z.ZodMiniNumber<number>;
114
+ id_token: z.ZodMiniOptional<z.ZodMiniString<string>>;
115
+ refresh_token: z.ZodMiniString<string>;
116
+ }, z.core.$strip>;
117
+
118
+ export declare interface JWTHeader {
119
+ alg: string;
120
+ typ: string;
121
+ kid?: string;
122
+ }
123
+
124
+ export declare type PollRequest = z.infer<typeof PollRequestSchema>;
125
+
126
+ /**
127
+ * Poll request/response schema
128
+ * POST /oauth/poll
129
+ */
130
+ export declare const PollRequestSchema: z.ZodMiniObject<{
131
+ polling_code: z.ZodMiniString<string>;
132
+ }, z.core.$strip>;
133
+
134
+ export declare type PollResponse = z.infer<typeof PollResponseSchema>;
135
+
136
+ export declare const PollResponseSchema: z.ZodMiniObject<{
137
+ status: z.ZodMiniEnum<{
138
+ pending: "pending";
139
+ authorized: "authorized";
140
+ rejected: "rejected";
141
+ expired: "expired";
142
+ }>;
143
+ authorization_code: z.ZodMiniOptional<z.ZodMiniString<string>>;
144
+ }, z.core.$strip>;
145
+
146
+ export declare type TokenInfo = z.infer<typeof TokenInfoSchema>;
147
+
148
+ /**
149
+ * Token info schema (parsed from JWT)
150
+ * Standard OIDC claims
151
+ */
152
+ export declare const TokenInfoSchema: z.ZodMiniObject<{
153
+ iss: z.ZodMiniString<string>;
154
+ sub: z.ZodMiniString<string>;
155
+ aud: z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniArray<z.ZodMiniString<string>>]>;
156
+ exp: z.ZodMiniNumber<number>;
157
+ iat: z.ZodMiniNumber<number>;
158
+ nonce: z.ZodMiniOptional<z.ZodMiniString<string>>;
159
+ auth_time: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
160
+ }, z.core.$strip>;
161
+
162
+ export declare type TokenResponse = z.infer<typeof TokenResponseSchema>;
163
+
164
+ /**
165
+ * Token exchange response schema (OAuth2 standard)
166
+ * POST /oauth/token
167
+ */
168
+ export declare const TokenResponseSchema: z.ZodMiniObject<{
169
+ access_token: z.ZodMiniString<string>;
170
+ token_type: z.ZodMiniString<string>;
171
+ expires_in: z.ZodMiniNumber<number>;
172
+ id_token: z.ZodMiniOptional<z.ZodMiniString<string>>;
173
+ refresh_token: z.ZodMiniString<string>;
174
+ }, z.core.$strip>;
175
+
176
+ export declare type UserInfoResponse = z.infer<typeof UserInfoResponseSchema>;
177
+
178
+ /**
179
+ * UserInfo response schema
180
+ * GET /oauth/userinfo
181
+ */
182
+ export declare const UserInfoResponseSchema: z.ZodMiniObject<{
183
+ sub: z.ZodMiniString<string>;
184
+ }, z.core.$strip>;
185
+
186
+ export { }
@@ -0,0 +1,355 @@
1
+ var h = (u, e, o) => new Promise((r, s) => {
2
+ var a = (l) => {
3
+ try {
4
+ c(o.next(l));
5
+ } catch (m) {
6
+ s(m);
7
+ }
8
+ }, n = (l) => {
9
+ try {
10
+ c(o.throw(l));
11
+ } catch (m) {
12
+ s(m);
13
+ }
14
+ }, c = (l) => l.done ? r(l.value) : Promise.resolve(l.value).then(a, n);
15
+ c((o = o.apply(u, e)).next());
16
+ });
17
+ import { z as t } from "zod/v4-mini";
18
+ import { sha256 as y } from "js-sha256";
19
+ const T = t.object({
20
+ deep_link: t.string(),
21
+ polling_code: t.string(),
22
+ expired_at: t.number()
23
+ }), b = t.object({
24
+ polling_code: t.string()
25
+ }), A = ["pending", "authorized", "rejected", "expired"], I = t.enum(A), R = t.object({
26
+ status: I,
27
+ authorization_code: t.optional(t.string())
28
+ }), k = t.object({
29
+ access_token: t.string(),
30
+ token_type: t.string(),
31
+ expires_in: t.number(),
32
+ id_token: t.optional(t.string()),
33
+ // Optional - not returned on refresh_token grant
34
+ refresh_token: t.string()
35
+ }), x = t.object({
36
+ sub: t.string()
37
+ }), j = t.object({
38
+ iss: t.string(),
39
+ sub: t.string(),
40
+ aud: t.union([t.string(), t.array(t.string())]),
41
+ exp: t.number(),
42
+ iat: t.number(),
43
+ nonce: t.optional(t.string()),
44
+ auth_time: t.optional(t.number())
45
+ }), z = k;
46
+ function _(u) {
47
+ return btoa(u).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
48
+ }
49
+ function w(u) {
50
+ let e = u.replace(/-/g, "+").replace(/_/g, "/");
51
+ for (; e.length % 4; )
52
+ e += "=";
53
+ return atob(e);
54
+ }
55
+ const E = "https://sso.alien.com", v = 5e3, i = "alien-sso_", g = i + "refresh_token", p = i + "token_expiry", f = (u, e) => new URL(e, u).toString(), U = t.object({
56
+ ssoBaseUrl: t.url(),
57
+ providerAddress: t.string(),
58
+ pollingInterval: t.optional(t.number())
59
+ }), d = class d {
60
+ constructor(e) {
61
+ this.config = U.parse(e), this.ssoBaseUrl = this.config.ssoBaseUrl || E, this.providerAddress = this.config.providerAddress, this.pollingInterval = this.config.pollingInterval || v;
62
+ }
63
+ generateCodeVerifier(e = 128) {
64
+ let o;
65
+ const r = typeof window != "undefined" && window.crypto;
66
+ if (r && r.getRandomValues)
67
+ o = new Uint8Array(e), r.getRandomValues(o);
68
+ else {
69
+ o = new Uint8Array(e);
70
+ for (let a = 0; a < e; a++)
71
+ o[a] = Math.floor(Math.random() * 256);
72
+ }
73
+ let s = "";
74
+ for (let a = 0; a < o.length; a++)
75
+ s += String.fromCharCode(o[a]);
76
+ return _(s);
77
+ }
78
+ generateCodeChallenge(e) {
79
+ const o = y.array(e), r = String.fromCharCode(...o);
80
+ return _(r);
81
+ }
82
+ /**
83
+ * Initiates OAuth2 authorization flow with response_mode=json for SPA
84
+ * GET /oauth/authorize?response_type=code&response_mode=json&...
85
+ */
86
+ generateDeeplink() {
87
+ return h(this, null, function* () {
88
+ const e = this.generateCodeVerifier(), o = this.generateCodeChallenge(e);
89
+ sessionStorage.setItem(i + "code_verifier", e);
90
+ const r = new URLSearchParams({
91
+ response_type: "code",
92
+ response_mode: "json",
93
+ client_id: this.providerAddress,
94
+ scope: "openid",
95
+ code_challenge: o,
96
+ code_challenge_method: "S256"
97
+ }), s = `${this.config.ssoBaseUrl}/oauth/authorize?${r.toString()}`, a = yield fetch(s, {
98
+ method: "GET"
99
+ });
100
+ if (!a.ok) {
101
+ const c = yield a.json().catch(() => ({ error: a.statusText }));
102
+ throw new Error(`Authorize failed: ${c.error_description || c.error || a.statusText}`);
103
+ }
104
+ const n = yield a.json();
105
+ return T.parse(n);
106
+ });
107
+ }
108
+ /**
109
+ * Polls for authorization completion
110
+ * POST /oauth/poll
111
+ */
112
+ pollAuth(e) {
113
+ return h(this, null, function* () {
114
+ const o = {
115
+ polling_code: e
116
+ };
117
+ b.parse(o);
118
+ const r = yield fetch(f(this.config.ssoBaseUrl, "/oauth/poll"), {
119
+ method: "POST",
120
+ headers: {
121
+ "Content-Type": "application/json"
122
+ },
123
+ body: JSON.stringify(o)
124
+ });
125
+ if (!r.ok)
126
+ throw new Error(`Poll failed: ${r.statusText}`);
127
+ const s = yield r.json();
128
+ return R.parse(s);
129
+ });
130
+ }
131
+ /**
132
+ * Exchanges authorization code for tokens
133
+ * POST /oauth/token (application/x-www-form-urlencoded)
134
+ * Returns both access_token and id_token
135
+ */
136
+ exchangeToken(e) {
137
+ return h(this, null, function* () {
138
+ const o = sessionStorage.getItem(i + "code_verifier");
139
+ if (!o) throw new Error("Missing code verifier.");
140
+ const r = new URLSearchParams({
141
+ grant_type: "authorization_code",
142
+ code: e,
143
+ client_id: this.providerAddress,
144
+ code_verifier: o
145
+ }), s = yield fetch(
146
+ f(this.config.ssoBaseUrl, "/oauth/token"),
147
+ {
148
+ method: "POST",
149
+ headers: {
150
+ "Content-Type": "application/x-www-form-urlencoded"
151
+ },
152
+ body: r.toString()
153
+ }
154
+ );
155
+ if (!s.ok) {
156
+ const l = yield s.json().catch(() => ({ error: s.statusText }));
157
+ throw new Error(`Token exchange failed: ${l.error_description || l.error || s.statusText}`);
158
+ }
159
+ const a = yield s.json(), n = k.parse(a);
160
+ localStorage.setItem(i + "access_token", n.access_token), n.id_token && localStorage.setItem(i + "id_token", n.id_token), localStorage.setItem(g, n.refresh_token);
161
+ const c = Date.now() + n.expires_in * 1e3;
162
+ return localStorage.setItem(p, c.toString()), sessionStorage.removeItem(i + "code_verifier"), n;
163
+ });
164
+ }
165
+ /**
166
+ * Verifies authentication by calling userinfo endpoint
167
+ * GET /oauth/userinfo
168
+ * Automatically refreshes token on 401 if refresh token is available
169
+ */
170
+ verifyAuth() {
171
+ return h(this, null, function* () {
172
+ return this.withAutoRefresh(() => h(this, null, function* () {
173
+ const e = this.getAccessToken();
174
+ if (!e)
175
+ return null;
176
+ const o = yield fetch(
177
+ f(this.config.ssoBaseUrl, "/oauth/userinfo"),
178
+ {
179
+ method: "GET",
180
+ headers: {
181
+ Authorization: `Bearer ${e}`
182
+ }
183
+ }
184
+ );
185
+ if (!o.ok) {
186
+ if (o.status === 401) {
187
+ const s = new Error("Unauthorized");
188
+ throw s.response = { status: 401 }, s;
189
+ }
190
+ return null;
191
+ }
192
+ const r = yield o.json();
193
+ return x.parse(r);
194
+ }));
195
+ });
196
+ }
197
+ /**
198
+ * Gets stored access token
199
+ */
200
+ getAccessToken() {
201
+ return localStorage.getItem(i + "access_token");
202
+ }
203
+ /**
204
+ * Gets stored ID token
205
+ */
206
+ getIdToken() {
207
+ return localStorage.getItem(i + "id_token");
208
+ }
209
+ /**
210
+ * Decodes and validates JWT token to extract claims
211
+ * Works with both access_token and id_token (EdDSA signed)
212
+ */
213
+ getAuthData() {
214
+ const e = this.getIdToken() || this.getAccessToken();
215
+ if (!e) return null;
216
+ const o = e.split(".");
217
+ if (o.length !== 3)
218
+ return null;
219
+ let r;
220
+ try {
221
+ const n = w(o[0]);
222
+ r = JSON.parse(n);
223
+ } catch (n) {
224
+ return null;
225
+ }
226
+ if (r.alg !== "RS256" || r.typ !== "JWT")
227
+ return null;
228
+ let s;
229
+ try {
230
+ const n = JSON.parse(w(o[1]));
231
+ s = j.parse(n);
232
+ } catch (n) {
233
+ return null;
234
+ }
235
+ return (Array.isArray(s.aud) ? s.aud : [s.aud]).includes(this.providerAddress) ? s : null;
236
+ }
237
+ /**
238
+ * Gets the subject (user identifier) from the token
239
+ */
240
+ getSubject() {
241
+ const e = this.getAuthData();
242
+ return (e == null ? void 0 : e.sub) || null;
243
+ }
244
+ /**
245
+ * Checks if the current token is expired
246
+ */
247
+ isTokenExpired() {
248
+ const e = this.getAuthData();
249
+ return e ? Date.now() / 1e3 > e.exp : !0;
250
+ }
251
+ /**
252
+ * Clears all stored authentication data
253
+ */
254
+ logout() {
255
+ localStorage.removeItem(i + "access_token"), localStorage.removeItem(i + "id_token"), localStorage.removeItem(g), localStorage.removeItem(p), sessionStorage.removeItem(i + "code_verifier");
256
+ }
257
+ /**
258
+ * Gets stored refresh token
259
+ */
260
+ getRefreshToken() {
261
+ return localStorage.getItem(g);
262
+ }
263
+ /**
264
+ * Checks if a refresh token is available
265
+ */
266
+ hasRefreshToken() {
267
+ return !!this.getRefreshToken();
268
+ }
269
+ /**
270
+ * Checks if the access token is expired or will expire soon (within 5 minutes)
271
+ */
272
+ isAccessTokenExpired() {
273
+ const e = localStorage.getItem(p);
274
+ if (!e) return !0;
275
+ const o = parseInt(e, 10), r = Date.now(), s = 300 * 1e3;
276
+ return r >= o - s;
277
+ }
278
+ /**
279
+ * Refreshes the access token using the stored refresh token
280
+ * POST /oauth/token with grant_type=refresh_token
281
+ * Uses singleton pattern to prevent concurrent refresh requests (race condition)
282
+ */
283
+ refreshAccessToken() {
284
+ return h(this, null, function* () {
285
+ return d.refreshPromise || (d.refreshPromise = this.doRefreshAccessToken().finally(() => {
286
+ d.refreshPromise = null;
287
+ })), d.refreshPromise;
288
+ });
289
+ }
290
+ /**
291
+ * Internal method that performs the actual token refresh
292
+ */
293
+ doRefreshAccessToken() {
294
+ return h(this, null, function* () {
295
+ const e = this.getRefreshToken();
296
+ if (!e)
297
+ throw new Error("No refresh token available");
298
+ const o = new URLSearchParams({
299
+ grant_type: "refresh_token",
300
+ refresh_token: e,
301
+ client_id: this.providerAddress
302
+ }), r = yield fetch(
303
+ f(this.config.ssoBaseUrl, "/oauth/token"),
304
+ {
305
+ method: "POST",
306
+ headers: {
307
+ "Content-Type": "application/x-www-form-urlencoded"
308
+ },
309
+ body: o.toString()
310
+ }
311
+ );
312
+ if (!r.ok) {
313
+ const c = yield r.json().catch(() => ({ error: r.statusText }));
314
+ throw this.logout(), new Error(`Token refresh failed: ${c.error_description || c.error || r.statusText}`);
315
+ }
316
+ const s = yield r.json(), a = k.parse(s);
317
+ localStorage.setItem(i + "access_token", a.access_token), a.id_token && localStorage.setItem(i + "id_token", a.id_token), localStorage.setItem(g, a.refresh_token);
318
+ const n = Date.now() + a.expires_in * 1e3;
319
+ return localStorage.setItem(p, n.toString()), a;
320
+ });
321
+ }
322
+ /**
323
+ * Executes a function that makes an authenticated request
324
+ * Automatically refreshes token and retries on 401 error
325
+ */
326
+ withAutoRefresh(e, o = 1) {
327
+ return h(this, null, function* () {
328
+ var r, s, a;
329
+ try {
330
+ return yield e();
331
+ } catch (n) {
332
+ if ((((r = n == null ? void 0 : n.response) == null ? void 0 : r.status) === 401 || ((s = n == null ? void 0 : n.message) == null ? void 0 : s.includes("401")) || ((a = n == null ? void 0 : n.message) == null ? void 0 : a.includes("Unauthorized"))) && o > 0 && this.hasRefreshToken())
333
+ try {
334
+ return yield this.refreshAccessToken(), yield e();
335
+ } catch (l) {
336
+ throw n;
337
+ }
338
+ throw n;
339
+ }
340
+ });
341
+ }
342
+ };
343
+ d.refreshPromise = null;
344
+ let S = d;
345
+ export {
346
+ S as AlienSsoClient,
347
+ U as AlienSsoClientSchema,
348
+ T as AuthorizeResponseSchema,
349
+ z as ExchangeCodeResponseSchema,
350
+ b as PollRequestSchema,
351
+ R as PollResponseSchema,
352
+ j as TokenInfoSchema,
353
+ k as TokenResponseSchema,
354
+ x as UserInfoResponseSchema
355
+ };
@@ -0,0 +1 @@
1
+ (function(c,e){typeof exports=="object"&&typeof module!="undefined"?e(exports,require("zod/v4-mini"),require("js-sha256")):typeof define=="function"&&define.amd?define(["exports","zod/v4-mini","js-sha256"],e):(c=typeof globalThis!="undefined"?globalThis:c||self,e(c.AlienSsoCore={},c.Zod,c.jsSha256))})(this,(function(c,e,f){"use strict";var h=(c,e,f)=>new Promise((k,m)=>{var T=l=>{try{g(f.next(l))}catch(p){m(p)}},z=l=>{try{g(f.throw(l))}catch(p){m(p)}},g=l=>l.done?k(l.value):Promise.resolve(l.value).then(T,z);g((f=f.apply(c,e)).next())});const k=e.z.object({deep_link:e.z.string(),polling_code:e.z.string(),expired_at:e.z.number()}),m=e.z.object({polling_code:e.z.string()}),T=["pending","authorized","rejected","expired"],z=e.z.enum(T),g=e.z.object({status:z,authorization_code:e.z.optional(e.z.string())}),l=e.z.object({access_token:e.z.string(),token_type:e.z.string(),expires_in:e.z.number(),id_token:e.z.optional(e.z.string()),refresh_token:e.z.string()}),p=e.z.object({sub:e.z.string()}),b=e.z.object({iss:e.z.string(),sub:e.z.string(),aud:e.z.union([e.z.string(),e.z.array(e.z.string())]),exp:e.z.number(),iat:e.z.number(),nonce:e.z.optional(e.z.string()),auth_time:e.z.optional(e.z.number())}),P=l;function I(S){return btoa(S).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function j(S){let t=S.replace(/-/g,"+").replace(/_/g,"/");for(;t.length%4;)t+="=";return atob(t)}const x="https://sso.alien.com",U=5e3,i="alien-sso_",_=i+"refresh_token",w=i+"token_expiry",y=(S,t)=>new URL(t,S).toString(),E=e.z.object({ssoBaseUrl:e.z.url(),providerAddress:e.z.string(),pollingInterval:e.z.optional(e.z.number())}),d=class d{constructor(t){this.config=E.parse(t),this.ssoBaseUrl=this.config.ssoBaseUrl||x,this.providerAddress=this.config.providerAddress,this.pollingInterval=this.config.pollingInterval||U}generateCodeVerifier(t=128){let o;const r=typeof window!="undefined"&&window.crypto;if(r&&r.getRandomValues)o=new Uint8Array(t),r.getRandomValues(o);else{o=new Uint8Array(t);for(let a=0;a<t;a++)o[a]=Math.floor(Math.random()*256)}let s="";for(let a=0;a<o.length;a++)s+=String.fromCharCode(o[a]);return I(s)}generateCodeChallenge(t){const o=f.sha256.array(t),r=String.fromCharCode(...o);return I(r)}generateDeeplink(){return h(this,null,function*(){const t=this.generateCodeVerifier(),o=this.generateCodeChallenge(t);sessionStorage.setItem(i+"code_verifier",t);const r=new URLSearchParams({response_type:"code",response_mode:"json",client_id:this.providerAddress,scope:"openid",code_challenge:o,code_challenge_method:"S256"}),s=`${this.config.ssoBaseUrl}/oauth/authorize?${r.toString()}`,a=yield fetch(s,{method:"GET"});if(!a.ok){const u=yield a.json().catch(()=>({error:a.statusText}));throw new Error(`Authorize failed: ${u.error_description||u.error||a.statusText}`)}const n=yield a.json();return k.parse(n)})}pollAuth(t){return h(this,null,function*(){const o={polling_code:t};m.parse(o);const r=yield fetch(y(this.config.ssoBaseUrl,"/oauth/poll"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});if(!r.ok)throw new Error(`Poll failed: ${r.statusText}`);const s=yield r.json();return g.parse(s)})}exchangeToken(t){return h(this,null,function*(){const o=sessionStorage.getItem(i+"code_verifier");if(!o)throw new Error("Missing code verifier.");const r=new URLSearchParams({grant_type:"authorization_code",code:t,client_id:this.providerAddress,code_verifier:o}),s=yield fetch(y(this.config.ssoBaseUrl,"/oauth/token"),{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:r.toString()});if(!s.ok){const R=yield s.json().catch(()=>({error:s.statusText}));throw new Error(`Token exchange failed: ${R.error_description||R.error||s.statusText}`)}const a=yield s.json(),n=l.parse(a);localStorage.setItem(i+"access_token",n.access_token),n.id_token&&localStorage.setItem(i+"id_token",n.id_token),localStorage.setItem(_,n.refresh_token);const u=Date.now()+n.expires_in*1e3;return localStorage.setItem(w,u.toString()),sessionStorage.removeItem(i+"code_verifier"),n})}verifyAuth(){return h(this,null,function*(){return this.withAutoRefresh(()=>h(this,null,function*(){const t=this.getAccessToken();if(!t)return null;const o=yield fetch(y(this.config.ssoBaseUrl,"/oauth/userinfo"),{method:"GET",headers:{Authorization:`Bearer ${t}`}});if(!o.ok){if(o.status===401){const s=new Error("Unauthorized");throw s.response={status:401},s}return null}const r=yield o.json();return p.parse(r)}))})}getAccessToken(){return localStorage.getItem(i+"access_token")}getIdToken(){return localStorage.getItem(i+"id_token")}getAuthData(){const t=this.getIdToken()||this.getAccessToken();if(!t)return null;const o=t.split(".");if(o.length!==3)return null;let r;try{const n=j(o[0]);r=JSON.parse(n)}catch(n){return null}if(r.alg!=="RS256"||r.typ!=="JWT")return null;let s;try{const n=JSON.parse(j(o[1]));s=b.parse(n)}catch(n){return null}return(Array.isArray(s.aud)?s.aud:[s.aud]).includes(this.providerAddress)?s:null}getSubject(){const t=this.getAuthData();return(t==null?void 0:t.sub)||null}isTokenExpired(){const t=this.getAuthData();return t?Date.now()/1e3>t.exp:!0}logout(){localStorage.removeItem(i+"access_token"),localStorage.removeItem(i+"id_token"),localStorage.removeItem(_),localStorage.removeItem(w),sessionStorage.removeItem(i+"code_verifier")}getRefreshToken(){return localStorage.getItem(_)}hasRefreshToken(){return!!this.getRefreshToken()}isAccessTokenExpired(){const t=localStorage.getItem(w);if(!t)return!0;const o=parseInt(t,10),r=Date.now(),s=300*1e3;return r>=o-s}refreshAccessToken(){return h(this,null,function*(){return d.refreshPromise||(d.refreshPromise=this.doRefreshAccessToken().finally(()=>{d.refreshPromise=null})),d.refreshPromise})}doRefreshAccessToken(){return h(this,null,function*(){const t=this.getRefreshToken();if(!t)throw new Error("No refresh token available");const o=new URLSearchParams({grant_type:"refresh_token",refresh_token:t,client_id:this.providerAddress}),r=yield fetch(y(this.config.ssoBaseUrl,"/oauth/token"),{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:o.toString()});if(!r.ok){const u=yield r.json().catch(()=>({error:r.statusText}));throw this.logout(),new Error(`Token refresh failed: ${u.error_description||u.error||r.statusText}`)}const s=yield r.json(),a=l.parse(s);localStorage.setItem(i+"access_token",a.access_token),a.id_token&&localStorage.setItem(i+"id_token",a.id_token),localStorage.setItem(_,a.refresh_token);const n=Date.now()+a.expires_in*1e3;return localStorage.setItem(w,n.toString()),a})}withAutoRefresh(t,o=1){return h(this,null,function*(){var r,s,a;try{return yield t()}catch(n){if((((r=n==null?void 0:n.response)==null?void 0:r.status)===401||((s=n==null?void 0:n.message)==null?void 0:s.includes("401"))||((a=n==null?void 0:n.message)==null?void 0:a.includes("Unauthorized")))&&o>0&&this.hasRefreshToken())try{return yield this.refreshAccessToken(),yield t()}catch(R){throw n}throw n}})}};d.refreshPromise=null;let A=d;c.AlienSsoClient=A,c.AlienSsoClientSchema=E,c.AuthorizeResponseSchema=k,c.ExchangeCodeResponseSchema=P,c.PollRequestSchema=m,c.PollResponseSchema=g,c.TokenInfoSchema=b,c.TokenResponseSchema=l,c.UserInfoResponseSchema=p,Object.defineProperty(c,Symbol.toStringTag,{value:"Module"})}));
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@alien-id/sso",
3
+ "version": "1.0.25",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/alien-id/sso-sdk-js.git"
7
+ },
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.esm.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.esm.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "vite build",
24
+ "dev": "vite build --watch",
25
+ "test": "jest",
26
+ "test:unit": "jest tests/unit",
27
+ "test:integration": "jest tests/integration",
28
+ "test:watch": "jest --watch",
29
+ "release": "npm publish --access public",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "devDependencies": {
33
+ "@types/express": "^5.0.3",
34
+ "@types/jest": "^30.0.0",
35
+ "@types/node": "^24.3.0",
36
+ "@typescript-eslint/eslint-plugin": "^8.41.0",
37
+ "@typescript-eslint/parser": "^8.41.0",
38
+ "cross-fetch": "^4.1.0",
39
+ "eslint": "^9.34.0",
40
+ "eslint-config-prettier": "^10.1.8",
41
+ "eslint-plugin-prettier": "^5.5.4",
42
+ "express": "^5.1.0",
43
+ "jest": "^30.1.2",
44
+ "jest-environment-jsdom": "^30.1.2",
45
+ "nock": "^14.0.10",
46
+ "prettier": "^3.6.2",
47
+ "ts-jest": "^29.4.1",
48
+ "turbo": "^2.5.6",
49
+ "vite": "^7.1.12",
50
+ "vite-plugin-dts": "^4.5.4"
51
+ },
52
+ "dependencies": {
53
+ "js-sha256": "^0.11.1",
54
+ "zod": "^4.1.5"
55
+ }
56
+ }