authfyio-javascript 0.2.1
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 +21 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +204 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# authfyio-javascript
|
|
2
|
+
|
|
3
|
+
Framework-free JavaScript SDK for Authfyio — browser sessions, auth state subscription, and admin calls.
|
|
4
|
+
|
|
5
|
+
> Part of [Authfyio](https://authfyio.com) — a self-hostable authentication platform.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install authfyio-javascript
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
See the full guide at **https://authfyio.com/docs**.
|
|
16
|
+
|
|
17
|
+
Point the SDK at your Authfyio instance via the same-origin proxy (`/api/af`) or set `AF_API_BASE_URL` for server-side calls.
|
|
18
|
+
|
|
19
|
+
## License
|
|
20
|
+
|
|
21
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-free JavaScript SDK — works with Vite, Webpack, Rollup, ESM
|
|
3
|
+
* CDN, or plain `<script type="module">`.
|
|
4
|
+
*
|
|
5
|
+
* import { createAuthfyio } from 'authfyio-javascript';
|
|
6
|
+
* const kolay = createAuthfyio({ baseUrl: 'https://fapi.example.com' });
|
|
7
|
+
* kolay.onAuthChange((auth) => {
|
|
8
|
+
* if (auth.isSignedIn) document.body.dataset.user = auth.userId!;
|
|
9
|
+
* });
|
|
10
|
+
* await kolay.signInEmailPassword('a@b.com', '…');
|
|
11
|
+
*
|
|
12
|
+
* Sessions are driven by the two-cookie Authfyio model — this SDK reads
|
|
13
|
+
* `__session` on every tick to stay in sync and calls `/sessions/refresh`
|
|
14
|
+
* before the JWT expires.
|
|
15
|
+
*/
|
|
16
|
+
export type SessionClaims = {
|
|
17
|
+
sid: string;
|
|
18
|
+
sub: string;
|
|
19
|
+
env: string;
|
|
20
|
+
org?: string;
|
|
21
|
+
org_role?: string;
|
|
22
|
+
/** Permission keys held in the active org (e.g. 'org:sys_billing:manage'). */
|
|
23
|
+
org_perms?: string[];
|
|
24
|
+
iat?: number;
|
|
25
|
+
exp?: number;
|
|
26
|
+
};
|
|
27
|
+
export type HasCheck = {
|
|
28
|
+
role?: string;
|
|
29
|
+
permission?: string;
|
|
30
|
+
};
|
|
31
|
+
export type AuthState = {
|
|
32
|
+
isSignedIn: boolean;
|
|
33
|
+
userId: string | null;
|
|
34
|
+
sessionId: string | null;
|
|
35
|
+
orgId: string | null;
|
|
36
|
+
orgRole: string | null;
|
|
37
|
+
claims: SessionClaims | null;
|
|
38
|
+
jwt: string | null;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Browser-side default. Assumes a same-origin proxy mounted at `/api/af`
|
|
42
|
+
* (e.g. `authfyio-nextjs/proxy` for Next.js apps). Override `baseUrl`
|
|
43
|
+
* if you mount the proxy elsewhere or call api.authfyio.com directly
|
|
44
|
+
* from a same-eTLD+1 host.
|
|
45
|
+
*/
|
|
46
|
+
export declare const DEFAULT_BASE_URL = "/api/af";
|
|
47
|
+
export type AuthfyioBrowserOptions = {
|
|
48
|
+
baseUrl?: string;
|
|
49
|
+
autoRefresh?: boolean;
|
|
50
|
+
refreshSkewSeconds?: number;
|
|
51
|
+
};
|
|
52
|
+
export type AuthfyioBrowser = {
|
|
53
|
+
state(): AuthState;
|
|
54
|
+
onAuthChange(listener: (state: AuthState) => void): () => void;
|
|
55
|
+
refresh(): Promise<AuthState>;
|
|
56
|
+
signInEmailPassword(email: string, password: string): Promise<AuthState>;
|
|
57
|
+
signUpEmailPassword(email: string, password: string): Promise<AuthState>;
|
|
58
|
+
signInMagicLink(email: string): Promise<void>;
|
|
59
|
+
signOut(): Promise<void>;
|
|
60
|
+
oauthRedirectUrl(provider: 'google' | 'github'): string;
|
|
61
|
+
/** Raw JWT for attaching to your own API requests. */
|
|
62
|
+
getToken(): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Synchronous gating helper. Reads the JWT's `org_role` / `org_perms`
|
|
65
|
+
* claims so it works without a network round-trip. Returns false when
|
|
66
|
+
* the user has no active org (for permission checks) or no matching
|
|
67
|
+
* role.
|
|
68
|
+
*/
|
|
69
|
+
has(check: HasCheck): boolean;
|
|
70
|
+
};
|
|
71
|
+
export declare function createAuthfyio(opts?: AuthfyioBrowserOptions): AuthfyioBrowser;
|
|
72
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;IAC7B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAE1C,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,IAAI,SAAS,CAAC;IACnB,YAAY,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC/D,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC;IAC9B,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACzE,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACzE,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;IACxD,sDAAsD;IACtD,QAAQ,IAAI,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;OAKG;IACH,GAAG,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC;CAC/B,CAAC;AAEF,wBAAgB,cAAc,CAAC,IAAI,GAAE,sBAA2B,GAAG,eAAe,CAmIjF"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-free JavaScript SDK — works with Vite, Webpack, Rollup, ESM
|
|
3
|
+
* CDN, or plain `<script type="module">`.
|
|
4
|
+
*
|
|
5
|
+
* import { createAuthfyio } from 'authfyio-javascript';
|
|
6
|
+
* const kolay = createAuthfyio({ baseUrl: 'https://fapi.example.com' });
|
|
7
|
+
* kolay.onAuthChange((auth) => {
|
|
8
|
+
* if (auth.isSignedIn) document.body.dataset.user = auth.userId!;
|
|
9
|
+
* });
|
|
10
|
+
* await kolay.signInEmailPassword('a@b.com', '…');
|
|
11
|
+
*
|
|
12
|
+
* Sessions are driven by the two-cookie Authfyio model — this SDK reads
|
|
13
|
+
* `__session` on every tick to stay in sync and calls `/sessions/refresh`
|
|
14
|
+
* before the JWT expires.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Browser-side default. Assumes a same-origin proxy mounted at `/api/af`
|
|
18
|
+
* (e.g. `authfyio-nextjs/proxy` for Next.js apps). Override `baseUrl`
|
|
19
|
+
* if you mount the proxy elsewhere or call api.authfyio.com directly
|
|
20
|
+
* from a same-eTLD+1 host.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_BASE_URL = '/api/af';
|
|
23
|
+
export function createAuthfyio(opts = {}) {
|
|
24
|
+
const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
25
|
+
const listeners = new Set();
|
|
26
|
+
let current = readState();
|
|
27
|
+
let refreshTimer = null;
|
|
28
|
+
function readState() {
|
|
29
|
+
const jwt = readSessionCookie();
|
|
30
|
+
if (!jwt)
|
|
31
|
+
return empty();
|
|
32
|
+
try {
|
|
33
|
+
const claims = decodeJwt(jwt);
|
|
34
|
+
return {
|
|
35
|
+
isSignedIn: true,
|
|
36
|
+
userId: claims.sub ?? null,
|
|
37
|
+
sessionId: claims.sid ?? null,
|
|
38
|
+
orgId: claims.org ?? null,
|
|
39
|
+
orgRole: claims.org_role ?? null,
|
|
40
|
+
claims,
|
|
41
|
+
jwt,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return empty();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function emit() {
|
|
49
|
+
const next = readState();
|
|
50
|
+
if (!shallowEqual(next, current)) {
|
|
51
|
+
current = next;
|
|
52
|
+
for (const fn of listeners)
|
|
53
|
+
fn(current);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
current = next;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function scheduleRefresh() {
|
|
60
|
+
if (refreshTimer)
|
|
61
|
+
clearTimeout(refreshTimer);
|
|
62
|
+
if (opts.autoRefresh === false)
|
|
63
|
+
return;
|
|
64
|
+
if (!current.isSignedIn || !current.claims?.exp)
|
|
65
|
+
return;
|
|
66
|
+
const skew = opts.refreshSkewSeconds ?? 15;
|
|
67
|
+
const msUntil = Math.max(0, current.claims.exp * 1000 - Date.now() - skew * 1000);
|
|
68
|
+
refreshTimer = setTimeout(async () => {
|
|
69
|
+
try {
|
|
70
|
+
await api.refresh();
|
|
71
|
+
}
|
|
72
|
+
catch { }
|
|
73
|
+
}, msUntil);
|
|
74
|
+
}
|
|
75
|
+
const api = {
|
|
76
|
+
state: () => current,
|
|
77
|
+
onAuthChange(fn) {
|
|
78
|
+
listeners.add(fn);
|
|
79
|
+
fn(current);
|
|
80
|
+
return () => listeners.delete(fn);
|
|
81
|
+
},
|
|
82
|
+
async refresh() {
|
|
83
|
+
await fetch(`${baseUrl}/v1/auth/sessions/refresh`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
credentials: 'include',
|
|
86
|
+
});
|
|
87
|
+
emit();
|
|
88
|
+
scheduleRefresh();
|
|
89
|
+
return current;
|
|
90
|
+
},
|
|
91
|
+
async signInEmailPassword(email, password) {
|
|
92
|
+
const res = await fetch(`${baseUrl}/v1/auth/sign-in/email-password`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
credentials: 'include',
|
|
95
|
+
headers: { 'content-type': 'application/json' },
|
|
96
|
+
body: JSON.stringify({ email, password }),
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok)
|
|
99
|
+
throw new Error('invalid_credentials');
|
|
100
|
+
emit();
|
|
101
|
+
scheduleRefresh();
|
|
102
|
+
return current;
|
|
103
|
+
},
|
|
104
|
+
async signUpEmailPassword(email, password) {
|
|
105
|
+
const res = await fetch(`${baseUrl}/v1/auth/sign-up/email-password`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
credentials: 'include',
|
|
108
|
+
headers: { 'content-type': 'application/json' },
|
|
109
|
+
body: JSON.stringify({ email, password }),
|
|
110
|
+
});
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
const body = (await res.json().catch(() => null));
|
|
113
|
+
throw new Error(body?.message ?? 'signup_failed');
|
|
114
|
+
}
|
|
115
|
+
emit();
|
|
116
|
+
scheduleRefresh();
|
|
117
|
+
return current;
|
|
118
|
+
},
|
|
119
|
+
async signInMagicLink(email) {
|
|
120
|
+
await fetch(`${baseUrl}/v1/auth/sign-in/magic-link`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
credentials: 'include',
|
|
123
|
+
headers: { 'content-type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({ email }),
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
async signOut() {
|
|
128
|
+
await fetch(`${baseUrl}/v1/auth/sign-out`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
credentials: 'include',
|
|
131
|
+
});
|
|
132
|
+
emit();
|
|
133
|
+
if (refreshTimer)
|
|
134
|
+
clearTimeout(refreshTimer);
|
|
135
|
+
},
|
|
136
|
+
oauthRedirectUrl(provider) {
|
|
137
|
+
return `${baseUrl}/v1/auth/oauth/${provider}/authorize`;
|
|
138
|
+
},
|
|
139
|
+
getToken() {
|
|
140
|
+
return current.jwt;
|
|
141
|
+
},
|
|
142
|
+
has(check) {
|
|
143
|
+
if (!current.isSignedIn || !current.claims)
|
|
144
|
+
return false;
|
|
145
|
+
if (check.role && current.claims.org_role !== check.role)
|
|
146
|
+
return false;
|
|
147
|
+
if (check.permission) {
|
|
148
|
+
if (!current.claims.org)
|
|
149
|
+
return false;
|
|
150
|
+
const perms = Array.isArray(current.claims.org_perms) ? current.claims.org_perms : [];
|
|
151
|
+
if (!perms.includes(check.permission))
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
scheduleRefresh();
|
|
158
|
+
// Cheap drift detection — re-check cookies every 30s in case another tab
|
|
159
|
+
// (or a direct server-side rotation) changed the JWT.
|
|
160
|
+
if (typeof window !== 'undefined') {
|
|
161
|
+
setInterval(emit, 30_000);
|
|
162
|
+
}
|
|
163
|
+
return api;
|
|
164
|
+
}
|
|
165
|
+
function empty() {
|
|
166
|
+
return {
|
|
167
|
+
isSignedIn: false,
|
|
168
|
+
userId: null,
|
|
169
|
+
sessionId: null,
|
|
170
|
+
orgId: null,
|
|
171
|
+
orgRole: null,
|
|
172
|
+
claims: null,
|
|
173
|
+
jwt: null,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function shallowEqual(a, b) {
|
|
177
|
+
return (a.isSignedIn === b.isSignedIn &&
|
|
178
|
+
a.userId === b.userId &&
|
|
179
|
+
a.sessionId === b.sessionId &&
|
|
180
|
+
a.orgId === b.orgId &&
|
|
181
|
+
a.orgRole === b.orgRole &&
|
|
182
|
+
a.jwt === b.jwt);
|
|
183
|
+
}
|
|
184
|
+
function readSessionCookie() {
|
|
185
|
+
if (typeof document === 'undefined')
|
|
186
|
+
return null;
|
|
187
|
+
for (const pair of document.cookie.split(';')) {
|
|
188
|
+
const eq = pair.indexOf('=');
|
|
189
|
+
if (eq <= 0)
|
|
190
|
+
continue;
|
|
191
|
+
const name = pair.slice(0, eq).trim();
|
|
192
|
+
if (name !== '__session')
|
|
193
|
+
continue;
|
|
194
|
+
return decodeURIComponent(pair.slice(eq + 1).trim());
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
function decodeJwt(token) {
|
|
199
|
+
const [, payload] = token.split('.');
|
|
200
|
+
const b64 = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
201
|
+
const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
|
|
202
|
+
const json = atob(padded);
|
|
203
|
+
return JSON.parse(json);
|
|
204
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "authfyio-javascript",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Framework-free JavaScript SDK for Authfyio — browser sessions, auth state subscription, and admin calls.",
|
|
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
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.build.json",
|
|
21
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"authfyio",
|
|
26
|
+
"auth",
|
|
27
|
+
"javascript",
|
|
28
|
+
"browser",
|
|
29
|
+
"sdk"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://authfyio.com/docs",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/authfyio/authfyio.git"
|
|
38
|
+
}
|
|
39
|
+
}
|