@authyon/auth 0.1.3
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 +158 -0
- package/dist/index.cjs +501 -0
- package/dist/index.d.cts +403 -0
- package/dist/index.d.ts +403 -0
- package/dist/index.js +468 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# @authyon/auth
|
|
2
|
+
|
|
3
|
+
SDK JS/TS para o [Authyon](https://authyon.com) — autenticação, sessões, multi-tenant e 2FA, com armazenamento e refresh de tokens transparentes.
|
|
4
|
+
|
|
5
|
+
Cobre todos os endpoints públicos documentados em [authyon.com/docs](https://authyon.com/docs), usando a **publishable key** (`pk_...`) — segura para expor no navegador.
|
|
6
|
+
|
|
7
|
+
Para gestão de organização/membros (secret key) e verificação de token no backend, veja [`@authyon/server`](../server).
|
|
8
|
+
|
|
9
|
+
## Instalação
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @authyon/auth
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Uso rápido
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createClient } from "@authyon/auth";
|
|
19
|
+
|
|
20
|
+
const authyon = createClient({ envKey: "pk_live_..." });
|
|
21
|
+
|
|
22
|
+
// Login (com suporte a 2FA)
|
|
23
|
+
const result = await authyon.login({
|
|
24
|
+
email: "alice@acme.com",
|
|
25
|
+
password: "...",
|
|
26
|
+
organizationSlug: "acme", // opcional
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (result.twoFactorRequired) {
|
|
30
|
+
const code = prompt(`Código 2FA (${result.methods.join(", ")})`);
|
|
31
|
+
await authyon.verifyTwoFactor({
|
|
32
|
+
challengeToken: result.challengeToken,
|
|
33
|
+
method: "authenticator",
|
|
34
|
+
code: code!,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Usuário atual — o access token é renovado automaticamente quando necessário
|
|
39
|
+
const user = await authyon.user.me();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Sessão e tokens
|
|
43
|
+
|
|
44
|
+
- Tokens persistem em `localStorage` por padrão (`memoryStorage()` ou um `TokenStorage` próprio via opção `storage`).
|
|
45
|
+
- `getAccessToken()` renova o token automaticamente antes de expirar (refresh token é single-use e rotacionado, com single-flight para evitar corridas).
|
|
46
|
+
- Chamadas autenticadas que retornam 401 fazem um refresh e uma retentativa automática.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const token = await authyon.getAccessToken(); // sempre válido, ou null se deslogado
|
|
50
|
+
|
|
51
|
+
const unsubscribe = authyon.onAuthStateChange((event) => {
|
|
52
|
+
// "signed_in" | "refreshed" | "signed_out"
|
|
53
|
+
console.log(event.type);
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
Métodos de sessão/auth ficam soltos no client; os que giram em torno de um recurso específico ficam agrupados em namespaces (`user`, `organization`, `twoFactor`, `webauthn`, `sso`).
|
|
60
|
+
|
|
61
|
+
| Método | Endpoint |
|
|
62
|
+
| ------------------------------------------------------------------------ | ----------------------- |
|
|
63
|
+
| `register({ email, username, password })` | `POST /auth/register` |
|
|
64
|
+
| `login({ email \| username, password, organizationSlug? })` | `POST /auth/login` |
|
|
65
|
+
| `verifyTwoFactor({ challengeToken, method, code?, webAuthnAssertion? })` | `POST /auth/2fa/verify` |
|
|
66
|
+
| `refresh()` | `POST /auth/refresh` |
|
|
67
|
+
| `logout({ everywhere? })` | `POST /auth/logout` |
|
|
68
|
+
| `introspect(token?)` | `POST /auth/introspect` |
|
|
69
|
+
| `validate(token?)` | `POST /auth/validate` |
|
|
70
|
+
|
|
71
|
+
### `authyon.user`
|
|
72
|
+
|
|
73
|
+
| Método | Endpoint |
|
|
74
|
+
| ----------------------------------------------- | ----------------------------------- |
|
|
75
|
+
| `user.me()` | `GET /auth/me` |
|
|
76
|
+
| `user.sessions()` | `GET /auth/sessions` |
|
|
77
|
+
| `user.revokeSession(sessionId)` | `DELETE /auth/sessions/{id}` |
|
|
78
|
+
| `user.activities(params?)` | `GET /auth/me/activities` |
|
|
79
|
+
| `user.requestPasswordReset(email)` | `POST /auth/password-reset/request` |
|
|
80
|
+
| `user.confirmPasswordReset(token, newPassword)` | `POST /auth/password-reset/confirm` |
|
|
81
|
+
|
|
82
|
+
### `authyon.organization`
|
|
83
|
+
|
|
84
|
+
| Método | Endpoint |
|
|
85
|
+
| ----------------------------------------------------- | ---------------------------------------------- |
|
|
86
|
+
| `organization.list()` | `GET /auth/tenants` |
|
|
87
|
+
| `organization.create(params?)` | `POST /auth/tenants` |
|
|
88
|
+
| `organization.get(organizationId)` | `GET /auth/tenants/{id}` |
|
|
89
|
+
| `organization.rename(organizationId, name)` | `PATCH /auth/tenants/{id}` |
|
|
90
|
+
| `organization.switch(slug)` | `POST /auth/switch-tenant` |
|
|
91
|
+
| `organization.current()` | — (lê `activeOrganization` da sessão em cache) |
|
|
92
|
+
| `organization.members.list(organizationId, params?)` | `GET /auth/tenants/{id}/members` |
|
|
93
|
+
| `organization.members.invite(organizationId, params)` | `POST /auth/tenants/{id}/members` |
|
|
94
|
+
| `organization.members.remove(organizationId, userId)` | `DELETE /auth/tenants/{id}/members/{userId}` |
|
|
95
|
+
| `organization.roles.list(organizationId)` | `GET /auth/tenants/{id}/roles` |
|
|
96
|
+
|
|
97
|
+
### `authyon.twoFactor`
|
|
98
|
+
|
|
99
|
+
| Método | Endpoint |
|
|
100
|
+
| ---------------------------------------------------- | -------------------------------------------- |
|
|
101
|
+
| `twoFactor.status()` | `GET /auth/2fa/status` |
|
|
102
|
+
| `twoFactor.resendEmail(challengeToken)` | `POST /auth/2fa/resend-email` |
|
|
103
|
+
| `twoFactor.setupAuthenticator()` | `POST /auth/2fa/authenticator/setup` |
|
|
104
|
+
| `twoFactor.confirmAuthenticator(code)` | `POST /auth/2fa/authenticator/confirm` |
|
|
105
|
+
| `twoFactor.enableEmail(code?)` | `POST /auth/2fa/email/enable` |
|
|
106
|
+
| `twoFactor.disable(method, currentPassword)` | `POST /auth/2fa/disable` |
|
|
107
|
+
| `twoFactor.regenerateRecoveryCodes(currentPassword)` | `POST /auth/2fa/recovery-codes/regenerate` |
|
|
108
|
+
| `twoFactor.webauthn.registerStart()` | `POST /auth/2fa/webauthn/register/start` |
|
|
109
|
+
| `twoFactor.webauthn.registerFinish(...)` | `POST /auth/2fa/webauthn/register/finish` |
|
|
110
|
+
| `twoFactor.webauthn.credentials()` | `GET /auth/2fa/webauthn/credentials` |
|
|
111
|
+
| `twoFactor.webauthn.renameCredential(id, nickname)` | `PATCH /auth/2fa/webauthn/credentials/{id}` |
|
|
112
|
+
| `twoFactor.webauthn.removeCredential(id, pwd)` | `DELETE /auth/2fa/webauthn/credentials/{id}` |
|
|
113
|
+
| `twoFactor.webauthn.assertionStart(challengeToken)` | `POST /auth/2fa/webauthn/assertion/start` |
|
|
114
|
+
|
|
115
|
+
### `authyon.webauthn` (login sem senha)
|
|
116
|
+
|
|
117
|
+
| Método | Endpoint |
|
|
118
|
+
| --------------------------------- | ---------------------------------- |
|
|
119
|
+
| `webauthn.loginStart(email?)` | `POST /auth/webauthn/login/start` |
|
|
120
|
+
| `webauthn.loginFinish(assertion)` | `POST /auth/webauthn/login/finish` |
|
|
121
|
+
|
|
122
|
+
### `authyon.sso` (login social)
|
|
123
|
+
|
|
124
|
+
| Método | Endpoint |
|
|
125
|
+
| -------------------------------- | ------------------------------------------------------------------------------------- |
|
|
126
|
+
| `sso.providers()` | `GET /auth/sso/providers` |
|
|
127
|
+
| `sso.startUrl(provider, params)` | monta a URL de `GET /auth/sso/{provider}/start` (não faz a chamada — navegue até ela) |
|
|
128
|
+
| `sso.exchange(code)` | `POST /auth/sso/exchange` |
|
|
129
|
+
|
|
130
|
+
## Invalidação de token
|
|
131
|
+
|
|
132
|
+
- **Sessão atual**: `logout()` revoga o refresh token atual; `logout({ everywhere: true })` revoga todos os refresh tokens do usuário.
|
|
133
|
+
- **Uma sessão específica**: `user.revokeSession(sessionId)`, usando o `id` retornado por `user.sessions()` — derruba um dispositivo sem afetar a sessão atual.
|
|
134
|
+
- **Access token**: por ser um JWT stateless, o access token continua "válido" até expirar (`expiresIn`, tipicamente 30 min) mesmo após revogar o refresh token. Para checar revogação em tempo real no seu backend, use `validate()` (cross-checa o estado no banco) em vez de `introspect()`.
|
|
135
|
+
|
|
136
|
+
## Erros
|
|
137
|
+
|
|
138
|
+
Toda resposta não-2xx vira um `AuthyonError` (problem+json). Compare pelo `code` legível por máquina, nunca pelo `title`:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { AuthyonError, ErrorCodes } from "@authyon/auth";
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await authyon.register({ email, password });
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (err instanceof AuthyonError && err.is(ErrorCodes.EmailTaken)) {
|
|
147
|
+
// e-mail já cadastrado
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Build
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm install
|
|
156
|
+
npm run build # dist/ (ESM + CJS + .d.ts via tsup)
|
|
157
|
+
npm run typecheck
|
|
158
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AuthyonClient: () => AuthyonClient,
|
|
24
|
+
AuthyonError: () => AuthyonError,
|
|
25
|
+
ErrorCodes: () => ErrorCodes,
|
|
26
|
+
createClient: () => createClient,
|
|
27
|
+
defaultStorage: () => defaultStorage,
|
|
28
|
+
localStorageAdapter: () => localStorageAdapter,
|
|
29
|
+
memoryStorage: () => memoryStorage
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
|
|
33
|
+
// src/errors.ts
|
|
34
|
+
var AuthyonError = class extends Error {
|
|
35
|
+
constructor(status, body) {
|
|
36
|
+
super(body.detail ?? body.title ?? `Authyon request failed with status ${status}`);
|
|
37
|
+
this.name = "AuthyonError";
|
|
38
|
+
this.status = status;
|
|
39
|
+
this.code = body.code ?? "unknown";
|
|
40
|
+
this.title = body.title ?? "Error";
|
|
41
|
+
this.detail = body.detail;
|
|
42
|
+
}
|
|
43
|
+
is(code) {
|
|
44
|
+
return this.code === code;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var ErrorCodes = {
|
|
48
|
+
EmailTaken: "user.email_taken",
|
|
49
|
+
PasswordWeak: "user.password_weak",
|
|
50
|
+
PasswordPwned: "user.password_pwned"
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/storage.ts
|
|
54
|
+
var STORAGE_KEY = "authyon.session";
|
|
55
|
+
function memoryStorage() {
|
|
56
|
+
let session = null;
|
|
57
|
+
return {
|
|
58
|
+
get: () => session,
|
|
59
|
+
set: (s) => {
|
|
60
|
+
session = s;
|
|
61
|
+
},
|
|
62
|
+
clear: () => {
|
|
63
|
+
session = null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function localStorageAdapter(key = STORAGE_KEY) {
|
|
68
|
+
return {
|
|
69
|
+
get() {
|
|
70
|
+
try {
|
|
71
|
+
const raw = window.localStorage.getItem(key);
|
|
72
|
+
return raw ? JSON.parse(raw) : null;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
set(session) {
|
|
78
|
+
try {
|
|
79
|
+
window.localStorage.setItem(key, JSON.stringify(session));
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
clear() {
|
|
84
|
+
try {
|
|
85
|
+
window.localStorage.removeItem(key);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function defaultStorage() {
|
|
92
|
+
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
|
|
93
|
+
return localStorageAdapter();
|
|
94
|
+
}
|
|
95
|
+
return memoryStorage();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/client.ts
|
|
99
|
+
var DEFAULT_BASE_URL = "https://api.authyon.com";
|
|
100
|
+
var EXPIRY_SKEW_MS = 3e4;
|
|
101
|
+
var AuthyonClient = class {
|
|
102
|
+
constructor(options) {
|
|
103
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
104
|
+
this.refreshInFlight = null;
|
|
105
|
+
// ── Passwordless (passkey) login ─────────────────────────────────────────
|
|
106
|
+
this.webauthn = {
|
|
107
|
+
/** POST /auth/webauthn/login/start — begins a passkey sign-in. */
|
|
108
|
+
loginStart: (email) => this.request("/auth/webauthn/login/start", { method: "POST", body: { email } }),
|
|
109
|
+
/**
|
|
110
|
+
* POST /auth/webauthn/login/finish — completes the passkey ceremony and
|
|
111
|
+
* stores the session.
|
|
112
|
+
*/
|
|
113
|
+
loginFinish: (assertion) => this.request("/auth/webauthn/login/finish", {
|
|
114
|
+
method: "POST",
|
|
115
|
+
body: assertion
|
|
116
|
+
}).then((data) => this.setSession(data, "signed_in"))
|
|
117
|
+
};
|
|
118
|
+
// ── Social sign-in (SSO) ─────────────────────────────────────────────────
|
|
119
|
+
this.sso = {
|
|
120
|
+
/** GET /auth/sso/providers — providers enabled for this environment. */
|
|
121
|
+
providers: () => this.request("/auth/sso/providers"),
|
|
122
|
+
/**
|
|
123
|
+
* Builds the URL to redirect the browser to in order to start a
|
|
124
|
+
* provider's sign-in flow (`GET /auth/sso/{provider}/start`). Navigate
|
|
125
|
+
* to it directly — e.g. `window.location.href = client.sso.startUrl(...)`.
|
|
126
|
+
*/
|
|
127
|
+
startUrl: (provider, params) => {
|
|
128
|
+
const query = new URLSearchParams({ redirect_uri: params.redirectUri });
|
|
129
|
+
if (params.state) query.set("state", params.state);
|
|
130
|
+
if (params.mode) query.set("mode", params.mode);
|
|
131
|
+
return `${this.baseUrl}/auth/sso/${encodeURIComponent(provider)}/start?${query}`;
|
|
132
|
+
},
|
|
133
|
+
/**
|
|
134
|
+
* POST /auth/sso/exchange — swaps the one-time code from the provider
|
|
135
|
+
* callback for tokens and stores the session.
|
|
136
|
+
*/
|
|
137
|
+
exchange: (code) => this.request("/auth/sso/exchange", { method: "POST", body: { code } }).then(
|
|
138
|
+
(data) => this.setSession(data, "signed_in")
|
|
139
|
+
)
|
|
140
|
+
};
|
|
141
|
+
// ── User ─────────────────────────────────────────────────────────────────
|
|
142
|
+
this.user = {
|
|
143
|
+
/** GET /auth/me — fresh profile of the current user. */
|
|
144
|
+
me: () => this.request("/auth/me", { bearer: true }).then(normalizeUser),
|
|
145
|
+
/** GET /auth/sessions — active refresh-token sessions with device/IP data. */
|
|
146
|
+
sessions: () => this.request("/auth/sessions", { bearer: true }),
|
|
147
|
+
/** GET /auth/me/activities — recent account activity for the current user. */
|
|
148
|
+
activities: (params = {}) => this.request(`/auth/me/activities?${toQuery(params)}`, { bearer: true }),
|
|
149
|
+
/**
|
|
150
|
+
* Revokes a single session by id (e.g. one entry from `sessions()`),
|
|
151
|
+
* signing that device out without affecting the current one.
|
|
152
|
+
*
|
|
153
|
+
* ⚠️ Not directly confirmed against the published API reference at the
|
|
154
|
+
* time this SDK was written — `DELETE /auth/sessions/{id}` follows the
|
|
155
|
+
* REST convention the rest of the documented API uses, but verify it
|
|
156
|
+
* against the Authyon dashboard/API reference before relying on it. If
|
|
157
|
+
* the endpoint differs, override via a raw call to your own backend.
|
|
158
|
+
*/
|
|
159
|
+
revokeSession: (sessionId) => this.request(`/auth/sessions/${encodeURIComponent(sessionId)}`, {
|
|
160
|
+
method: "DELETE",
|
|
161
|
+
bearer: true
|
|
162
|
+
}),
|
|
163
|
+
/** POST /auth/password-reset/request — always resolves (no account enumeration). */
|
|
164
|
+
requestPasswordReset: (email) => this.request("/auth/password-reset/request", { method: "POST", body: { email } }),
|
|
165
|
+
/** POST /auth/password-reset/confirm — sets a new password and revokes all refresh tokens. */
|
|
166
|
+
confirmPasswordReset: (token, newPassword) => this.request("/auth/password-reset/confirm", {
|
|
167
|
+
method: "POST",
|
|
168
|
+
body: { token, newPassword }
|
|
169
|
+
})
|
|
170
|
+
};
|
|
171
|
+
// ── Organization ─────────────────────────────────────────────────────────
|
|
172
|
+
this.organization = {
|
|
173
|
+
/** GET /auth/tenants — all organization memberships. */
|
|
174
|
+
list: () => this.request("/auth/tenants", { bearer: true }),
|
|
175
|
+
/**
|
|
176
|
+
* POST /auth/tenants — creates an organization owned by the signed-in
|
|
177
|
+
* user (only available when self-service organization creation is
|
|
178
|
+
* enabled for the environment).
|
|
179
|
+
*/
|
|
180
|
+
create: (params = {}) => this.request("/auth/tenants", { method: "POST", bearer: true, body: params }),
|
|
181
|
+
/** GET /auth/tenants/{organizationId} — fetch one of the user's organizations by id. */
|
|
182
|
+
get: (organizationId) => this.request(`/auth/tenants/${encodeURIComponent(organizationId)}`, { bearer: true }),
|
|
183
|
+
/**
|
|
184
|
+
* PATCH /auth/tenants/{organizationId} — renames the organization.
|
|
185
|
+
* Requires the `tenants:manage` custom permission on it.
|
|
186
|
+
*/
|
|
187
|
+
rename: (organizationId, name) => this.request(`/auth/tenants/${encodeURIComponent(organizationId)}`, {
|
|
188
|
+
method: "PATCH",
|
|
189
|
+
bearer: true,
|
|
190
|
+
body: { name }
|
|
191
|
+
}),
|
|
192
|
+
/** POST /auth/switch-tenant — issues a fresh token scoped to the new organization. */
|
|
193
|
+
switch: (organizationSlug) => this.request("/auth/switch-tenant", {
|
|
194
|
+
method: "POST",
|
|
195
|
+
bearer: true,
|
|
196
|
+
body: { tenantSlug: organizationSlug }
|
|
197
|
+
}).then((data) => this.setSession(data, "refreshed")),
|
|
198
|
+
/** The organization the current session is scoped to, from the cached session — no network call. */
|
|
199
|
+
current: () => this.getSession()?.user?.activeOrganization ?? null,
|
|
200
|
+
members: {
|
|
201
|
+
/** GET /auth/tenants/{organizationId}/members — list an organization's members. */
|
|
202
|
+
list: (organizationId, params = {}) => this.request(
|
|
203
|
+
`/auth/tenants/${encodeURIComponent(organizationId)}/members?${toQuery(params)}`,
|
|
204
|
+
{ bearer: true }
|
|
205
|
+
),
|
|
206
|
+
/** POST /auth/tenants/{organizationId}/members — invite a member by e-mail. */
|
|
207
|
+
invite: (organizationId, params) => this.request(`/auth/tenants/${encodeURIComponent(organizationId)}/members`, {
|
|
208
|
+
method: "POST",
|
|
209
|
+
bearer: true,
|
|
210
|
+
body: params
|
|
211
|
+
}),
|
|
212
|
+
/** DELETE /auth/tenants/{organizationId}/members/{userId} — remove a member. */
|
|
213
|
+
remove: (organizationId, userId) => this.request(
|
|
214
|
+
`/auth/tenants/${encodeURIComponent(organizationId)}/members/${encodeURIComponent(userId)}`,
|
|
215
|
+
{ method: "DELETE", bearer: true }
|
|
216
|
+
)
|
|
217
|
+
},
|
|
218
|
+
roles: {
|
|
219
|
+
/** GET /auth/tenants/{organizationId}/roles — roles available in the organization. */
|
|
220
|
+
list: (organizationId) => this.request(`/auth/tenants/${encodeURIComponent(organizationId)}/roles`, {
|
|
221
|
+
bearer: true
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
// ── Two-factor management (authenticated) ────────────────────────────────
|
|
226
|
+
this.twoFactor = {
|
|
227
|
+
/** GET /auth/2fa/status — enrolled methods and recovery code count. */
|
|
228
|
+
status: () => this.request("/auth/2fa/status", { bearer: true }),
|
|
229
|
+
/** POST /auth/2fa/resend-email — resends the code for an in-flight login challenge. */
|
|
230
|
+
resendEmail: (challengeToken) => this.request("/auth/2fa/resend-email", { method: "POST", body: { challengeToken } }),
|
|
231
|
+
/** POST /auth/2fa/authenticator/setup — returns secret, QR SVG and otpauth URI. */
|
|
232
|
+
setupAuthenticator: () => this.request("/auth/2fa/authenticator/setup", { method: "POST", bearer: true }),
|
|
233
|
+
/** POST /auth/2fa/authenticator/confirm — returns 10 single-use recovery codes. */
|
|
234
|
+
confirmAuthenticator: (code) => this.request("/auth/2fa/authenticator/confirm", {
|
|
235
|
+
method: "POST",
|
|
236
|
+
bearer: true,
|
|
237
|
+
body: { code }
|
|
238
|
+
}),
|
|
239
|
+
/**
|
|
240
|
+
* POST /auth/2fa/email/enable — two-step opt-in for email-based OTP.
|
|
241
|
+
* Call without `code` to receive one by e-mail, then call again with
|
|
242
|
+
* that code to confirm enrolment.
|
|
243
|
+
*/
|
|
244
|
+
enableEmail: (code) => this.request("/auth/2fa/email/enable", { method: "POST", bearer: true, body: { code } }),
|
|
245
|
+
/** POST /auth/2fa/disable — turns off a specific 2FA method (requires current password). */
|
|
246
|
+
disable: (method, currentPassword) => this.request("/auth/2fa/disable", {
|
|
247
|
+
method: "POST",
|
|
248
|
+
bearer: true,
|
|
249
|
+
body: { method, currentPassword }
|
|
250
|
+
}),
|
|
251
|
+
/**
|
|
252
|
+
* POST /auth/2fa/recovery-codes/regenerate — rotates the 10 single-use
|
|
253
|
+
* recovery codes (requires current password).
|
|
254
|
+
*/
|
|
255
|
+
regenerateRecoveryCodes: (currentPassword) => this.request("/auth/2fa/recovery-codes/regenerate", {
|
|
256
|
+
method: "POST",
|
|
257
|
+
bearer: true,
|
|
258
|
+
body: { currentPassword }
|
|
259
|
+
}),
|
|
260
|
+
webauthn: {
|
|
261
|
+
/** POST /auth/2fa/webauthn/register/start — begins passkey enrolment for 2FA. */
|
|
262
|
+
registerStart: () => this.request("/auth/2fa/webauthn/register/start", { method: "POST", bearer: true }),
|
|
263
|
+
/** POST /auth/2fa/webauthn/register/finish — finishes passkey enrolment. */
|
|
264
|
+
registerFinish: (ceremonyToken, attestationJson, nickname) => this.request("/auth/2fa/webauthn/register/finish", {
|
|
265
|
+
method: "POST",
|
|
266
|
+
bearer: true,
|
|
267
|
+
body: { ceremonyToken, attestationJson, nickname }
|
|
268
|
+
}),
|
|
269
|
+
/** GET /auth/2fa/webauthn/credentials — the caller's registered passkeys. */
|
|
270
|
+
credentials: () => this.request("/auth/2fa/webauthn/credentials", { bearer: true }),
|
|
271
|
+
/** PATCH /auth/2fa/webauthn/credentials/{id} — renames a passkey. */
|
|
272
|
+
renameCredential: (id, nickname) => this.request(`/auth/2fa/webauthn/credentials/${encodeURIComponent(id)}`, {
|
|
273
|
+
method: "PATCH",
|
|
274
|
+
bearer: true,
|
|
275
|
+
body: { nickname }
|
|
276
|
+
}),
|
|
277
|
+
/** DELETE /auth/2fa/webauthn/credentials/{id} — removes a passkey (requires current password). */
|
|
278
|
+
removeCredential: (id, currentPassword) => this.request(`/auth/2fa/webauthn/credentials/${encodeURIComponent(id)}`, {
|
|
279
|
+
method: "DELETE",
|
|
280
|
+
bearer: true,
|
|
281
|
+
body: { currentPassword }
|
|
282
|
+
}),
|
|
283
|
+
/**
|
|
284
|
+
* POST /auth/2fa/webauthn/assertion/start — fetches WebAuthn assertion
|
|
285
|
+
* options for an in-flight login challenge (2FA method `"webauthn"`).
|
|
286
|
+
*/
|
|
287
|
+
assertionStart: (challengeToken) => this.request("/auth/2fa/webauthn/assertion/start", {
|
|
288
|
+
method: "POST",
|
|
289
|
+
body: { challengeToken }
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
if (!options.envKey)
|
|
294
|
+
throw new Error("Authyon: `envKey` is required (pk_live_... / pk_test_...)");
|
|
295
|
+
this.envKey = options.envKey;
|
|
296
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
297
|
+
this.storage = options.storage ?? defaultStorage();
|
|
298
|
+
this.autoRefresh = options.autoRefresh ?? true;
|
|
299
|
+
this.fetchImpl = options.fetch ?? fetch.bind(globalThis);
|
|
300
|
+
}
|
|
301
|
+
// ── Session state ────────────────────────────────────────────────────────
|
|
302
|
+
/** Current persisted session, or null when signed out. */
|
|
303
|
+
getSession() {
|
|
304
|
+
return this.storage.get();
|
|
305
|
+
}
|
|
306
|
+
isAuthenticated() {
|
|
307
|
+
return this.getSession() !== null;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Returns a valid access token, refreshing it transparently when it is
|
|
311
|
+
* expired or about to expire. Returns null when signed out.
|
|
312
|
+
*/
|
|
313
|
+
async getAccessToken() {
|
|
314
|
+
const session = this.getSession();
|
|
315
|
+
if (!session) return null;
|
|
316
|
+
if (this.autoRefresh && Date.now() >= session.expiresAt - EXPIRY_SKEW_MS) {
|
|
317
|
+
try {
|
|
318
|
+
return (await this.refresh()).accessToken;
|
|
319
|
+
} catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return session.accessToken;
|
|
324
|
+
}
|
|
325
|
+
/** Subscribe to sign-in / refresh / sign-out events. Returns an unsubscribe fn. */
|
|
326
|
+
onAuthStateChange(listener) {
|
|
327
|
+
this.listeners.add(listener);
|
|
328
|
+
return () => this.listeners.delete(listener);
|
|
329
|
+
}
|
|
330
|
+
emit(event) {
|
|
331
|
+
for (const listener of this.listeners) listener(event);
|
|
332
|
+
}
|
|
333
|
+
setSession(raw, event) {
|
|
334
|
+
const session = {
|
|
335
|
+
...raw,
|
|
336
|
+
user: raw.user ? normalizeUser(raw.user) : void 0,
|
|
337
|
+
expiresAt: Date.now() + raw.expiresIn * 1e3
|
|
338
|
+
};
|
|
339
|
+
this.storage.set(session);
|
|
340
|
+
this.emit(event === "signed_out" ? { type: "signed_out" } : { type: event, session });
|
|
341
|
+
return session;
|
|
342
|
+
}
|
|
343
|
+
clearSession() {
|
|
344
|
+
this.storage.clear();
|
|
345
|
+
this.emit({ type: "signed_out" });
|
|
346
|
+
}
|
|
347
|
+
// ── HTTP core ────────────────────────────────────────────────────────────
|
|
348
|
+
async request(path, options = {}, isRetry = false) {
|
|
349
|
+
const headers = {
|
|
350
|
+
"X-Authyon-Environment": this.envKey,
|
|
351
|
+
...options.headers
|
|
352
|
+
};
|
|
353
|
+
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
354
|
+
if (options.bearer) {
|
|
355
|
+
const token = await this.getAccessToken();
|
|
356
|
+
if (!token)
|
|
357
|
+
throw new AuthyonError(401, { code: "auth.not_authenticated", title: "Not authenticated" });
|
|
358
|
+
headers.Authorization = `Bearer ${token}`;
|
|
359
|
+
}
|
|
360
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
361
|
+
method: options.method ?? "GET",
|
|
362
|
+
headers,
|
|
363
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
364
|
+
});
|
|
365
|
+
if (response.status === 401 && options.bearer && this.autoRefresh && !isRetry && this.getSession()) {
|
|
366
|
+
try {
|
|
367
|
+
await this.refresh();
|
|
368
|
+
} catch {
|
|
369
|
+
this.clearSession();
|
|
370
|
+
throw await this.toError(response);
|
|
371
|
+
}
|
|
372
|
+
return this.request(path, options, true);
|
|
373
|
+
}
|
|
374
|
+
if (!response.ok) throw await this.toError(response);
|
|
375
|
+
if (response.status === 204) return void 0;
|
|
376
|
+
return await response.json();
|
|
377
|
+
}
|
|
378
|
+
async toError(response) {
|
|
379
|
+
let body = {};
|
|
380
|
+
try {
|
|
381
|
+
body = await response.json();
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
384
|
+
return new AuthyonError(response.status, body);
|
|
385
|
+
}
|
|
386
|
+
// ── Auth flows ───────────────────────────────────────────────────────────
|
|
387
|
+
/** POST /auth/register — creates a new user (rate-limited to 20/hour per IP). */
|
|
388
|
+
async register(params) {
|
|
389
|
+
return this.request("/auth/register", { method: "POST", body: params });
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* POST /auth/login — authenticates and stores the session, or returns a
|
|
393
|
+
* 2FA challenge to complete via `verifyTwoFactor()`.
|
|
394
|
+
*/
|
|
395
|
+
async login(params) {
|
|
396
|
+
const { organizationSlug, ...rest } = params;
|
|
397
|
+
const body = organizationSlug ? { ...rest, tenantSlug: organizationSlug } : rest;
|
|
398
|
+
const data = await this.request("/auth/login", {
|
|
399
|
+
method: "POST",
|
|
400
|
+
body
|
|
401
|
+
});
|
|
402
|
+
if (data.twoFactorRequired) {
|
|
403
|
+
return data;
|
|
404
|
+
}
|
|
405
|
+
const session = this.setSession(data, "signed_in");
|
|
406
|
+
return { twoFactorRequired: false, session };
|
|
407
|
+
}
|
|
408
|
+
/** POST /auth/2fa/verify — redeems a 2FA challenge from `login()` and stores the session. */
|
|
409
|
+
async verifyTwoFactor(params) {
|
|
410
|
+
const data = await this.request("/auth/2fa/verify", { method: "POST", body: params });
|
|
411
|
+
return this.setSession(data, "signed_in");
|
|
412
|
+
}
|
|
413
|
+
/** POST /auth/refresh — rotates the single-use refresh token (single-flight). */
|
|
414
|
+
async refresh() {
|
|
415
|
+
if (this.refreshInFlight) return this.refreshInFlight;
|
|
416
|
+
const current = this.getSession();
|
|
417
|
+
if (!current)
|
|
418
|
+
throw new AuthyonError(401, { code: "auth.not_authenticated", title: "Not authenticated" });
|
|
419
|
+
this.refreshInFlight = this.request("/auth/refresh", {
|
|
420
|
+
method: "POST",
|
|
421
|
+
body: { refreshToken: current.refreshToken }
|
|
422
|
+
}).then(
|
|
423
|
+
(data) => this.setSession({ user: current.user, ...data }, "refreshed")
|
|
424
|
+
).catch((error) => {
|
|
425
|
+
if (error instanceof AuthyonError && (error.status === 401 || error.status === 403)) {
|
|
426
|
+
this.clearSession();
|
|
427
|
+
}
|
|
428
|
+
throw error;
|
|
429
|
+
}).finally(() => {
|
|
430
|
+
this.refreshInFlight = null;
|
|
431
|
+
});
|
|
432
|
+
return this.refreshInFlight;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* POST /auth/logout — revokes the current refresh token and clears local
|
|
436
|
+
* state. Pass `{ everywhere: true }` to revoke every session for the user.
|
|
437
|
+
*/
|
|
438
|
+
async logout(options = {}) {
|
|
439
|
+
const session = this.getSession();
|
|
440
|
+
if (session) {
|
|
441
|
+
try {
|
|
442
|
+
if (options.everywhere) {
|
|
443
|
+
await this.request("/auth/logout", { method: "POST", bearer: true });
|
|
444
|
+
} else {
|
|
445
|
+
await this.request("/auth/logout", {
|
|
446
|
+
method: "POST",
|
|
447
|
+
body: { refreshToken: session.refreshToken }
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
this.clearSession();
|
|
454
|
+
}
|
|
455
|
+
// ── Token verification ───────────────────────────────────────────────────
|
|
456
|
+
/** POST /auth/introspect — lightweight token introspection. */
|
|
457
|
+
async introspect(token) {
|
|
458
|
+
const accessToken = token ?? await this.getAccessToken();
|
|
459
|
+
return this.request("/auth/introspect", { method: "POST", body: { token: accessToken } });
|
|
460
|
+
}
|
|
461
|
+
/** POST /auth/validate — recommended: cross-checks DB state, returns user + organization. */
|
|
462
|
+
async validate(token) {
|
|
463
|
+
const accessToken = token ?? await this.getAccessToken();
|
|
464
|
+
const raw = await this.request("/auth/validate", {
|
|
465
|
+
method: "POST",
|
|
466
|
+
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : void 0
|
|
467
|
+
});
|
|
468
|
+
return {
|
|
469
|
+
user: normalizeUser(raw.user),
|
|
470
|
+
organization: raw.organization ?? raw.tenant ?? null
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
function normalizeUser(raw) {
|
|
475
|
+
const { tenants, activeTenant, ...rest } = raw;
|
|
476
|
+
return {
|
|
477
|
+
...rest,
|
|
478
|
+
organizations: raw.organizations ?? tenants,
|
|
479
|
+
activeOrganization: raw.activeOrganization ?? activeTenant ?? null
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function createClient(options) {
|
|
483
|
+
return new AuthyonClient(options);
|
|
484
|
+
}
|
|
485
|
+
function toQuery(params) {
|
|
486
|
+
const query = new URLSearchParams();
|
|
487
|
+
for (const [key, value] of Object.entries(params)) {
|
|
488
|
+
if (value !== void 0) query.set(key, String(value));
|
|
489
|
+
}
|
|
490
|
+
return query.toString();
|
|
491
|
+
}
|
|
492
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
493
|
+
0 && (module.exports = {
|
|
494
|
+
AuthyonClient,
|
|
495
|
+
AuthyonError,
|
|
496
|
+
ErrorCodes,
|
|
497
|
+
createClient,
|
|
498
|
+
defaultStorage,
|
|
499
|
+
localStorageAdapter,
|
|
500
|
+
memoryStorage
|
|
501
|
+
});
|