@baliola/auth-sdk 0.2.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 +76 -0
- package/LICENSE +37 -0
- package/README.md +305 -0
- package/dist/authError-C5g5jP5l.d.ts +102 -0
- package/dist/authError-DbEZJnC4.js +214 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2 -0
- package/dist/client-BcyoQ5Cj.js +504 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.js +2 -0
- package/dist/index-G-gqveGn.d.ts +142 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/requests-eTORIjY5.d.ts +42 -0
- package/dist/session-Cs_P7ojF.d.ts +49 -0
- package/dist/sessionStore-BDdEpbL8.d.ts +35 -0
- package/dist/sessionStore-DD6lON9W.js +50 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.js +2 -0
- package/dist/tokens-BXrPLi5B.d.ts +29 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +1 -0
- package/package.json +60 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@baliola/auth-sdk` are documented here.
|
|
4
|
+
|
|
5
|
+
## 0.2.0
|
|
6
|
+
|
|
7
|
+
Distribution change and clean-ups; no method-level API changes.
|
|
8
|
+
|
|
9
|
+
- chore(release): publish to **public npm** (`https://registry.npmjs.org`) instead of GitHub Packages. Installing no longer requires a Personal Access Token, an `.npmrc` registry override, or SSO authorization — plain `bun add @baliola/auth-sdk` works in every environment.
|
|
10
|
+
- docs: rewrite `README.md`, `docs/install.md`, `docs/ciSetup.md`, and `docs/troubleshooting.md` to reflect public-npm install. CI snippets no longer need build secrets.
|
|
11
|
+
- Consumers previously installing from GitHub Packages should remove any `@baliola:registry=https://npm.pkg.github.com` entries from `~/.npmrc` and project-local `.npmrc` files before upgrading.
|
|
12
|
+
- fix(types): realign `AccessTokenPayload` with the server's current JWT shape. The flat `projectId?: string | null` / `projectName?: string | null` pair is replaced by `projects: AccessTokenPayloadProject[]` (each entry carries `{ id, name, clientId }`). Type-only change — affects consumers that decode the JWT manually and reference this type. A scoped login (`clientId` supplied) yields a single-entry array; a central login yields every project the account has active roles in.
|
|
13
|
+
- chore!: remove the `./internal` subpath export and the `internal/` source folder (`createValidateCache`, `newIdempotencyKey`, `consumeHeaders`). Those helpers targeted the retired `/internal/api-keys/*` endpoints and the static "internal token" concept. Service-to-service callers now authenticate with an admin-tier API key (`baliola_admin_…`) against `/admin/api-keys/*`; cache + idempotency helpers for that surface will ship in a dedicated admin SDK.
|
|
14
|
+
|
|
15
|
+
## 0.1.0
|
|
16
|
+
|
|
17
|
+
Breaking redesign of the auth API and SDK surface to eliminate dev confusion between the two email flows. Old method names and endpoint paths are removed; no aliases.
|
|
18
|
+
|
|
19
|
+
### Breaking changes
|
|
20
|
+
|
|
21
|
+
- feat(sdk)!: replace the flat method surface with namespaced flows — `auth.emailOtp.*`, `auth.emailPassword.*`, `auth.google.*`. The namespaces match the DB provider names (`email_otp`, `email_password`).
|
|
22
|
+
- feat(sdk)!: rename verb-overloaded methods so each one's purpose is unambiguous at the call site:
|
|
23
|
+
- `auth.requestEmailOtp` → `auth.emailOtp.sendLoginCode`
|
|
24
|
+
- `auth.verifyEmailOtp` → `auth.emailOtp.verifyLoginCode`
|
|
25
|
+
- `auth.register` → `auth.emailPassword.register`
|
|
26
|
+
- `auth.verifyRegistration` → `auth.emailPassword.verifyRegistrationCode`
|
|
27
|
+
- `auth.loginWithPassword` → `auth.emailPassword.login`
|
|
28
|
+
- `auth.setPassword` → `auth.emailPassword.setPassword`
|
|
29
|
+
- `auth.changePassword` → `auth.emailPassword.changePassword`
|
|
30
|
+
- `auth.loginWithGoogle` → `auth.google.login`
|
|
31
|
+
- feat(sdk)!: verify-code methods take `{ email, code }` instead of `{ email, otp }`.
|
|
32
|
+
- feat(sdk)!: `auth.emailPassword.register` no longer branches on `'otp_sent' | 'linked'` — it always sends a verification OTP and returns `{ otp: OtpInfo }`. The single-step "linked" path is removed.
|
|
33
|
+
- feat(sdk)!: send/resend/register success responses replace `expiresInMinutes` with a structured `OtpInfo` payload — `expiresInSeconds`, `expiresAt`, `canResendInSeconds`, `resendsRemaining`.
|
|
34
|
+
- feat(sdk)!: backend endpoint paths renamed to match the namespaces: `POST /auth/email-otp/send-login-code`, `/auth/email-otp/verify-login-code`, `/auth/email-otp/resend-login-code`, `/auth/email-password/register`, `/auth/email-password/verify-registration-code`, `/auth/email-password/resend-registration-code`, etc.
|
|
35
|
+
|
|
36
|
+
### New features
|
|
37
|
+
|
|
38
|
+
- feat(sdk): add typed error subclasses under `AuthError` — `OtpInvalidError` (with `attemptsRemaining`, `canResendInSeconds`), `OtpExpiredError`, `MaxAttemptsError` (with `retryAfterSeconds`), `NoPendingOtpError`, `ResendCooldownError` (with `canResendInSeconds`), `MaxResendsError`, `InvalidCredentialsError`, `NoPasswordSetError`, `AccountSuspendedError`, `EmailAlreadyHasPasswordError`, `CaptchaFailedError`, `RateLimitedError` (with `retryAfterSeconds`, `retryAfterHuman`), `InvalidPasswordError` (with `reason`), `InvalidEmailError`. Branch on `instanceof` for TS narrowing, or `err.code` for stable string matching. Backend responses now carry the structured context fields via `error.details` so the SDK lifts them into the typed properties.
|
|
39
|
+
- feat(sdk): add dedicated resend methods — `auth.emailOtp.resendLoginCode` and `auth.emailPassword.resendRegistrationCode`. Each enforces its own resend cooldown (`OTP_RESEND_COOLDOWN_SECONDS`, default 60s) and per-OTP cap (`OTP_MAX_RESENDS`, default 3) on top of the per-OTP attempts limit.
|
|
40
|
+
- feat(sdk): add `captchaToken` option on every public unauthenticated entrypoint (`sendLoginCode`, `resendLoginCode`, `register`, `resendRegistrationCode`, `login`). Backend verifies via Cloudflare Turnstile when `TURNSTILE_SECRET_KEY` is set; dev bypass when unset; server-to-server clients can be allowlisted via `CAPTCHA_BYPASS_CLIENT_IDS`.
|
|
41
|
+
- feat(sdk): `auth.emailOtp.sendLoginCode` response includes `flow: 'login' | 'signup'` and `methods: ('password' | 'passwordless')[]` so the UI can adapt to the account state without an extra round-trip.
|
|
42
|
+
|
|
43
|
+
### Internal
|
|
44
|
+
|
|
45
|
+
- refactor(sdk): backend collapses the two OTP namespaces (`otp:*` and `email_verify:*`) into a single Redis record per email, stamped with `purpose: 'passwordless' | 'register_password'`. Latest-wins on conflicts; the pending bcrypt hash for password registration is held inside the record until verify, so no account/provider rows are written before email ownership is proven.
|
|
46
|
+
- docs(sdk): rewrite README around namespaced flows + typed error taxonomy; rewrite `docs/concepts.md` with sequence diagrams for each flow and the shared OTP-store model.
|
|
47
|
+
|
|
48
|
+
## 0.0.5
|
|
49
|
+
|
|
50
|
+
- feat(sdk): add cross-tab session sync via `BroadcastChannel` — when one tab logs in, logs out, or refreshes the access token, every other tab on the same origin updates its in-memory mirror in real time.
|
|
51
|
+
- feat(sdk): add `auth.fetch({ requireAuth: true })` per-call option that throws `AuthError(401)` before sending when no session is loaded.
|
|
52
|
+
- feat(sdk): add `auth.fetch({ disableRefresh: true })` per-call option that skips both proactive and reactive refresh, letting a 401 propagate to the caller.
|
|
53
|
+
|
|
54
|
+
## 0.0.4
|
|
55
|
+
|
|
56
|
+
- feat(sdk): add `changePassword({ currentPassword, newPassword })` paired with `POST /auth/email-password/change-password` server route — verifies the current password, rotates the bcrypt hash, revokes every other session for the account (caller stays signed in), and sends a confirmation email out-of-band.
|
|
57
|
+
- chore(auth): relax password policy to require only 8+ characters and at least one uppercase letter (number and special-character requirements dropped).
|
|
58
|
+
|
|
59
|
+
## 0.0.3
|
|
60
|
+
|
|
61
|
+
- chore(sdk): bump version to 0.0.3
|
|
62
|
+
- feat(sdk): expose `Project.allowedOrigins` on `AuthSession`
|
|
63
|
+
|
|
64
|
+
## 0.0.2
|
|
65
|
+
|
|
66
|
+
- chore(sdk): bump version to 0.0.2
|
|
67
|
+
- refactor(sdk): drop pre-release version labels from public JSDoc
|
|
68
|
+
- docs(sdk): inline install in README and move roadmap to ROADMAP.md
|
|
69
|
+
- docs(sdk): add user install, troubleshooting, and CI setup guides
|
|
70
|
+
- docs(sdk): add README with usage and configuration reference
|
|
71
|
+
- test(sdk): add unit tests for client, transport, and store
|
|
72
|
+
- feat(sdk): implement auth client with auto-refresh, session mirror, and consumer fetch helper
|
|
73
|
+
|
|
74
|
+
## 0.0.1
|
|
75
|
+
|
|
76
|
+
- feat(sdk): scaffold `@baliola/auth-sdk` package config
|
package/LICENSE
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Copyright (c) 2026 Baliola. All rights reserved.
|
|
2
|
+
|
|
3
|
+
PROPRIETARY AND CONFIDENTIAL
|
|
4
|
+
|
|
5
|
+
This software, including its source code, compiled output, type definitions,
|
|
6
|
+
documentation, and any associated materials (collectively, the "Software"), is
|
|
7
|
+
the proprietary property of Baliola. All rights, title, and interest in and to
|
|
8
|
+
the Software are and shall remain the exclusive property of Baliola.
|
|
9
|
+
|
|
10
|
+
Access to the Software is granted only to parties expressly authorized by
|
|
11
|
+
Baliola, and solely under the terms of a separate written agreement with
|
|
12
|
+
Baliola. No rights or licenses are granted by implication, estoppel, or
|
|
13
|
+
otherwise.
|
|
14
|
+
|
|
15
|
+
Without Baliola's prior written consent, the following are prohibited:
|
|
16
|
+
|
|
17
|
+
1. Redistribution of the Software, in whole or in part, in source or
|
|
18
|
+
compiled form, to any third party.
|
|
19
|
+
2. Modification, adaptation, translation, or creation of derivative works
|
|
20
|
+
based on the Software.
|
|
21
|
+
3. Reverse engineering, decompilation, or disassembly of the Software,
|
|
22
|
+
except to the extent such restriction is expressly prohibited by
|
|
23
|
+
applicable law.
|
|
24
|
+
4. Sublicensing, leasing, renting, selling, or otherwise transferring any
|
|
25
|
+
rights in the Software.
|
|
26
|
+
5. Removing, altering, or obscuring any copyright, trademark, or other
|
|
27
|
+
proprietary notices appearing in the Software.
|
|
28
|
+
|
|
29
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
30
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
31
|
+
FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NONINFRINGEMENT. IN NO EVENT
|
|
32
|
+
SHALL BALIOLA, ITS AFFILIATES, OR CONTRIBUTORS BE LIABLE FOR ANY CLAIM,
|
|
33
|
+
DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR
|
|
34
|
+
OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
35
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
36
|
+
|
|
37
|
+
For licensing inquiries, contact: <info@baliola.io>
|
package/README.md
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# @baliola/auth-sdk
|
|
2
|
+
|
|
3
|
+
Client SDK for the Baliola Auth service. Two clearly separated email flows (passwordless OTP, password + verification OTP) plus Google login, with typed methods, typed errors, auto-refresh, and cross-tab sync.
|
|
4
|
+
|
|
5
|
+
For deeper detail see [docs/concepts.md](./docs/concepts.md).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun add @baliola/auth-sdk
|
|
11
|
+
# or: npm install @baliola/auth-sdk
|
|
12
|
+
# or: pnpm add @baliola/auth-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Published to public npm under the `@baliola` scope — no registry config or auth token required.
|
|
16
|
+
|
|
17
|
+
**Runtime**: Bun (any recent), Node 20.3+, Chrome 116+, Safari 17+, Firefox 124+. Zero runtime deps. ESM only.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createAuthClient, localStorageStore, OtpInvalidError } from '@baliola/auth-sdk';
|
|
23
|
+
|
|
24
|
+
const auth = createAuthClient({
|
|
25
|
+
baseUrl: 'https://baliola-auth.baliola.dev',
|
|
26
|
+
clientId: 'your-project-client-id', // omit for a central multi-project login
|
|
27
|
+
store: localStorageStore(), // omit for in-memory (server-side / tests)
|
|
28
|
+
});
|
|
29
|
+
await auth.loadSession(); // rehydrate from store at app boot
|
|
30
|
+
|
|
31
|
+
// Passwordless login
|
|
32
|
+
const { otp } = await auth.emailOtp.sendLoginCode({
|
|
33
|
+
email: 'user@example.com',
|
|
34
|
+
captchaToken: turnstileToken,
|
|
35
|
+
});
|
|
36
|
+
showOtpForm({ resendIn: otp.canResendInSeconds });
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const session = await auth.emailOtp.verifyLoginCode({
|
|
40
|
+
email: 'user@example.com',
|
|
41
|
+
code: '123456',
|
|
42
|
+
});
|
|
43
|
+
} catch (e) {
|
|
44
|
+
if (e instanceof OtpInvalidError) {
|
|
45
|
+
showError(`Wrong code. ${e.attemptsRemaining} tries left.`);
|
|
46
|
+
} else throw e;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
auth.isAuthenticated(); // true
|
|
50
|
+
await auth.logout();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## API
|
|
54
|
+
|
|
55
|
+
The SDK is namespaced by flow so that each method's purpose is unambiguous at the call site.
|
|
56
|
+
|
|
57
|
+
### `auth.emailOtp` — passwordless flow
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
auth.emailOtp.sendLoginCode({ email, captchaToken });
|
|
61
|
+
// → { flow: 'login'|'signup', methods: ('password'|'passwordless')[], otp: OtpInfo }
|
|
62
|
+
|
|
63
|
+
auth.emailOtp.verifyLoginCode({ email, code });
|
|
64
|
+
// → AuthSession (also populates the session mirror)
|
|
65
|
+
|
|
66
|
+
auth.emailOtp.resendLoginCode({ email, captchaToken });
|
|
67
|
+
// → { otp: OtpInfo }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### `auth.emailPassword` — password flow
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
auth.emailPassword.register({ email, password, captchaToken });
|
|
74
|
+
// → { otp: OtpInfo } (sends a verification OTP; bcrypt hash held server-side)
|
|
75
|
+
|
|
76
|
+
auth.emailPassword.verifyRegistrationCode({ email, code });
|
|
77
|
+
// → AuthSession (finalizes the account; populates the session mirror)
|
|
78
|
+
|
|
79
|
+
auth.emailPassword.resendRegistrationCode({ email, captchaToken });
|
|
80
|
+
// → { otp: OtpInfo }
|
|
81
|
+
|
|
82
|
+
auth.emailPassword.login({ email, password, captchaToken });
|
|
83
|
+
// → AuthSession
|
|
84
|
+
|
|
85
|
+
// Authenticated:
|
|
86
|
+
auth.emailPassword.setPassword({ password });
|
|
87
|
+
auth.emailPassword.changePassword({ currentPassword, newPassword });
|
|
88
|
+
// ↑ revokes every other session for the account on success
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `auth.google`
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// idToken is obtained from Google Sign-In on the frontend.
|
|
95
|
+
auth.google.login({ idToken }); // → AuthSession
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Session lifecycle
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
await auth.refresh(); // → AuthSession (manual; SDK also refreshes proactively/reactively)
|
|
102
|
+
await auth.logout(); // → void
|
|
103
|
+
|
|
104
|
+
auth.getSession(); // AuthSession | null (sync)
|
|
105
|
+
auth.isAuthenticated(); // boolean (sync)
|
|
106
|
+
await auth.loadSession(); // rehydrate from store at app boot
|
|
107
|
+
|
|
108
|
+
const unsub = auth.onSessionChange((session) => {
|
|
109
|
+
/* AuthSession | null */
|
|
110
|
+
});
|
|
111
|
+
auth.onSessionChange(handler, { immediate: false }); // future-only
|
|
112
|
+
unsub();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `auth.fetch` — call your own backend
|
|
116
|
+
|
|
117
|
+
`Authorization: Bearer …` is attached automatically; `X-Session-ID` is **never** sent to consumer URLs. 401 → `/auth/refresh` → retry-once happens transparently.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const res = await auth.fetch('https://api.example/things', {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
body: JSON.stringify({ ping: 1 }),
|
|
123
|
+
signal: ctrl.signal,
|
|
124
|
+
requireAuth: true, // throw AuthError(401) before sending if no session
|
|
125
|
+
disableRefresh: true, // skip both proactive + reactive refresh for this call
|
|
126
|
+
});
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
/* res.status, res.json(), etc. */
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`auth.fetch` returns the `Response` for any consumer-URL non-2xx. It only throws `AuthError` when the SDK's own `/auth/refresh` sub-call fails.
|
|
133
|
+
|
|
134
|
+
### Lifecycle
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
auth.close(); // release cross-tab BroadcastChannel; optional
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Errors
|
|
141
|
+
|
|
142
|
+
Every error subclass extends `AuthError`. Branch on `instanceof` (preferred for TS narrowing) or `err.code`.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import {
|
|
146
|
+
AuthError,
|
|
147
|
+
OtpInvalidError,
|
|
148
|
+
OtpExpiredError,
|
|
149
|
+
MaxAttemptsError,
|
|
150
|
+
ResendCooldownError,
|
|
151
|
+
MaxResendsError,
|
|
152
|
+
NoPendingOtpError,
|
|
153
|
+
InvalidCredentialsError,
|
|
154
|
+
NoPasswordSetError,
|
|
155
|
+
AccountSuspendedError,
|
|
156
|
+
EmailAlreadyHasPasswordError,
|
|
157
|
+
CaptchaFailedError,
|
|
158
|
+
RateLimitedError,
|
|
159
|
+
InvalidPasswordError,
|
|
160
|
+
InvalidEmailError,
|
|
161
|
+
} from '@baliola/auth-sdk';
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await auth.emailOtp.verifyLoginCode({ email, code });
|
|
165
|
+
} catch (e) {
|
|
166
|
+
if (e instanceof OtpInvalidError)
|
|
167
|
+
return show(`${e.attemptsRemaining} tries left, resend in ${e.canResendInSeconds}s`);
|
|
168
|
+
if (e instanceof OtpExpiredError) return showResendPrompt();
|
|
169
|
+
if (e instanceof MaxAttemptsError) return show(`Too many tries. Wait ${e.retryAfterSeconds}s`);
|
|
170
|
+
if (e instanceof RateLimitedError) return show(`Rate limited. ${e.retryAfterHuman}`);
|
|
171
|
+
throw e;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| Error class | `code` | Status | Carries |
|
|
176
|
+
| ------------------------------ | ---------------------------- | ------ | ------------------------------------------ |
|
|
177
|
+
| `OtpInvalidError` | `otp_invalid` | 400 | `attemptsRemaining`, `canResendInSeconds` |
|
|
178
|
+
| `OtpExpiredError` | `otp_expired` | 400 | — |
|
|
179
|
+
| `MaxAttemptsError` | `max_attempts_exceeded` | 429 | `retryAfterSeconds` |
|
|
180
|
+
| `NoPendingOtpError` | `no_pending_otp` | 400 | — |
|
|
181
|
+
| `ResendCooldownError` | `resend_cooldown` | 429 | `canResendInSeconds` |
|
|
182
|
+
| `MaxResendsError` | `max_resends_exceeded` | 429 | — |
|
|
183
|
+
| `InvalidCredentialsError` | `invalid_credentials` | 401 | — |
|
|
184
|
+
| `NoPasswordSetError` | `no_password_set` | 400 | — |
|
|
185
|
+
| `AccountSuspendedError` | `account_suspended` | 403 | — |
|
|
186
|
+
| `EmailAlreadyHasPasswordError` | `email_already_has_password` | 409 | — |
|
|
187
|
+
| `CaptchaFailedError` | `captcha_failed` | 400 | — |
|
|
188
|
+
| `InvalidPasswordError` | `invalid_password` | 400 | `reason: 'too_short'\|'no_uppercase'\|...` |
|
|
189
|
+
| `InvalidEmailError` | `invalid_email` | 400 | — |
|
|
190
|
+
| `RateLimitedError` | (legacy 429) | 429 | `retryAfterSeconds`, `retryAfterHuman` |
|
|
191
|
+
| `AuthError` (base) | varies | varies | `status: 0` for network/timeout |
|
|
192
|
+
|
|
193
|
+
## Stores
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { memoryStore, localStorageStore, type SessionStore } from '@baliola/auth-sdk';
|
|
197
|
+
|
|
198
|
+
memoryStore(); // default — per-process, no persistence
|
|
199
|
+
localStorageStore({ key? }); // browser only, persists across reloads
|
|
200
|
+
|
|
201
|
+
const cookieStore: SessionStore = {
|
|
202
|
+
get() { /* AuthSession | null */ },
|
|
203
|
+
set(s) { /* persist or clear when s === null */ },
|
|
204
|
+
};
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Cross-tab sync (login / logout / refresh) is automatic via `BroadcastChannel`, regardless of store choice.
|
|
208
|
+
|
|
209
|
+
## Configuration
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
createAuthClient({
|
|
213
|
+
baseUrl: 'https://baliola-auth.baliola.dev', // required, no trailing slash
|
|
214
|
+
clientId: 'your-project-client-id', // optional; scopes the session to one project. Omit for a central login that carries every project the account has active roles in.
|
|
215
|
+
store: memoryStore(), // default; or localStorageStore() / custom
|
|
216
|
+
fetch: globalThis.fetch, // override for tests / interceptors
|
|
217
|
+
timeoutMs: 15_000, // per-request, default 15s
|
|
218
|
+
proactiveRefresh: { leadTimeMs: 60_000 }, // or `false` to disable; default 60s lead
|
|
219
|
+
onError: (e, source) => console.error('[auth-sdk]', source, e),
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`clientId` is constructor-only — there is no per-call override. To switch projects, create a new client.
|
|
224
|
+
|
|
225
|
+
## Decoding the JWT
|
|
226
|
+
|
|
227
|
+
The SDK does not parse the access token; consumers that need claims (e.g. server-side authorization in a Node service) decode it with their own library. The exported `AccessTokenPayload` type matches the server's current claim shape.
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { decodeJwt } from 'jose';
|
|
231
|
+
import type { AccessTokenPayload } from '@baliola/auth-sdk';
|
|
232
|
+
|
|
233
|
+
const session = auth.getSession();
|
|
234
|
+
if (session) {
|
|
235
|
+
const claims = decodeJwt<AccessTokenPayload>(session.accessToken);
|
|
236
|
+
// claims.accountId, claims.email
|
|
237
|
+
// claims.projects: { id, name, clientId }[] // single entry for a scoped login; many for a central login
|
|
238
|
+
// claims.roles?: string[]; claims.permissions?: string[]
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Sample response shapes
|
|
243
|
+
|
|
244
|
+
**`auth.emailOtp.sendLoginCode` success:**
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"flow": "signup",
|
|
249
|
+
"methods": ["passwordless"],
|
|
250
|
+
"otp": {
|
|
251
|
+
"expiresInSeconds": 300,
|
|
252
|
+
"expiresAt": "2026-05-04T12:05:00.000Z",
|
|
253
|
+
"canResendInSeconds": 60,
|
|
254
|
+
"resendsRemaining": 3
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**`auth.emailPassword.register` / resend success:**
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"otp": {
|
|
264
|
+
"expiresInSeconds": 300,
|
|
265
|
+
"expiresAt": "...",
|
|
266
|
+
"canResendInSeconds": 60,
|
|
267
|
+
"resendsRemaining": 3
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**`auth.emailOtp.verifyLoginCode` / `auth.emailPassword.verifyRegistrationCode` / `auth.emailPassword.login` / `auth.google.login` success (`AuthSession`):**
|
|
273
|
+
|
|
274
|
+
```json
|
|
275
|
+
{
|
|
276
|
+
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
|
|
277
|
+
"refreshToken": "session-uuid",
|
|
278
|
+
"expiresIn": 3600,
|
|
279
|
+
"account": { "id": "...", "email": "user@example.com", "status": "active" },
|
|
280
|
+
"project": { "id": "...", "name": "...", "displayName": null, "allowedOrigins": [...] },
|
|
281
|
+
"roles": ["user"],
|
|
282
|
+
"permissions": []
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Verify-code failure (e.g. `OtpInvalidError`):**
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{
|
|
290
|
+
"message": "Invalid verification code. 2 attempt(s) remaining.",
|
|
291
|
+
"data": null,
|
|
292
|
+
"error": {
|
|
293
|
+
"code": "otp_invalid",
|
|
294
|
+
"details": {
|
|
295
|
+
"name": "OtpInvalidError",
|
|
296
|
+
"attemptsRemaining": 2,
|
|
297
|
+
"canResendInSeconds": 30
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
Proprietary — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//#region src/errors/authError.d.ts
|
|
2
|
+
type AuthErrorInit = {
|
|
3
|
+
status: number; /** Stable machine-readable code (e.g. 'otp_invalid'). May be empty for network failures. */
|
|
4
|
+
code?: string;
|
|
5
|
+
serverMessage: string;
|
|
6
|
+
serverName?: string;
|
|
7
|
+
details?: unknown;
|
|
8
|
+
cause?: unknown;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Base class for every error thrown by the SDK. Concrete error subclasses
|
|
12
|
+
* (`OtpInvalidError`, `RateLimitedError`, etc.) extend this. Consumers can
|
|
13
|
+
* branch on either `instanceof` (preferred for TS narrowing) or `err.code`
|
|
14
|
+
* (when narrowing by string).
|
|
15
|
+
*
|
|
16
|
+
* - `status`: HTTP status from the server, or `0` for network/timeout failures.
|
|
17
|
+
* - `code`: stable machine-readable code (see error subclasses for values).
|
|
18
|
+
* - `serverMessage`: server's `message` field (for display or logs).
|
|
19
|
+
* - `serverName`: server's error class name from `error.details.name`.
|
|
20
|
+
* - `details`: server's `error.details` as-is (carries context fields like
|
|
21
|
+
* `attemptsRemaining`, `canResendInSeconds`, `retryAfterSeconds`).
|
|
22
|
+
* - `cause`: original underlying error (e.g. the `TypeError` from a network failure).
|
|
23
|
+
*/
|
|
24
|
+
declare class AuthError extends Error {
|
|
25
|
+
readonly status: number;
|
|
26
|
+
readonly code: string;
|
|
27
|
+
readonly serverMessage: string;
|
|
28
|
+
readonly serverName?: string;
|
|
29
|
+
readonly details?: unknown;
|
|
30
|
+
readonly cause?: unknown;
|
|
31
|
+
constructor(init: AuthErrorInit);
|
|
32
|
+
}
|
|
33
|
+
declare class OtpInvalidError extends AuthError {
|
|
34
|
+
readonly attemptsRemaining: number;
|
|
35
|
+
readonly canResendInSeconds: number;
|
|
36
|
+
constructor(init: AuthErrorInit & {
|
|
37
|
+
attemptsRemaining: number;
|
|
38
|
+
canResendInSeconds: number;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
declare class OtpExpiredError extends AuthError {
|
|
42
|
+
constructor(init: AuthErrorInit);
|
|
43
|
+
}
|
|
44
|
+
declare class MaxAttemptsError extends AuthError {
|
|
45
|
+
readonly retryAfterSeconds: number;
|
|
46
|
+
constructor(init: AuthErrorInit & {
|
|
47
|
+
retryAfterSeconds: number;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
declare class NoPendingOtpError extends AuthError {
|
|
51
|
+
constructor(init: AuthErrorInit);
|
|
52
|
+
}
|
|
53
|
+
declare class ResendCooldownError extends AuthError {
|
|
54
|
+
readonly canResendInSeconds: number;
|
|
55
|
+
constructor(init: AuthErrorInit & {
|
|
56
|
+
canResendInSeconds: number;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
declare class MaxResendsError extends AuthError {
|
|
60
|
+
constructor(init: AuthErrorInit);
|
|
61
|
+
}
|
|
62
|
+
declare class InvalidCredentialsError extends AuthError {
|
|
63
|
+
constructor(init: AuthErrorInit);
|
|
64
|
+
}
|
|
65
|
+
declare class NoPasswordSetError extends AuthError {
|
|
66
|
+
constructor(init: AuthErrorInit);
|
|
67
|
+
}
|
|
68
|
+
declare class AccountSuspendedError extends AuthError {
|
|
69
|
+
constructor(init: AuthErrorInit);
|
|
70
|
+
}
|
|
71
|
+
declare class EmailAlreadyHasPasswordError extends AuthError {
|
|
72
|
+
constructor(init: AuthErrorInit);
|
|
73
|
+
}
|
|
74
|
+
declare class CaptchaFailedError extends AuthError {
|
|
75
|
+
constructor(init: AuthErrorInit);
|
|
76
|
+
}
|
|
77
|
+
declare class RateLimitedError extends AuthError {
|
|
78
|
+
readonly retryAfterSeconds: number;
|
|
79
|
+
readonly retryAfterHuman: string;
|
|
80
|
+
constructor(init: AuthErrorInit & {
|
|
81
|
+
retryAfterSeconds: number;
|
|
82
|
+
retryAfterHuman: string;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
declare class InvalidPasswordError extends AuthError {
|
|
86
|
+
readonly reason: 'too_short' | 'no_uppercase' | 'too_weak' | 'unknown';
|
|
87
|
+
constructor(init: AuthErrorInit & {
|
|
88
|
+
reason: 'too_short' | 'no_uppercase' | 'too_weak' | 'unknown';
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
declare class InvalidEmailError extends AuthError {
|
|
92
|
+
constructor(init: AuthErrorInit);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Map a backend error response onto the right AuthError subclass.
|
|
96
|
+
* Falls back to `AuthError` (with status + code preserved) for unknown codes.
|
|
97
|
+
*/
|
|
98
|
+
declare function authErrorFromResponse(response: Response): Promise<AuthError>;
|
|
99
|
+
/** Network/timeout failures land here. */
|
|
100
|
+
declare function authErrorFromFetchFailure(cause: unknown, isTimeout?: boolean): AuthError;
|
|
101
|
+
//#endregion
|
|
102
|
+
export { authErrorFromFetchFailure as _, EmailAlreadyHasPasswordError as a, InvalidPasswordError as c, NoPasswordSetError as d, NoPendingOtpError as f, ResendCooldownError as g, RateLimitedError as h, CaptchaFailedError as i, MaxAttemptsError as l, OtpInvalidError as m, AuthError as n, InvalidCredentialsError as o, OtpExpiredError as p, AuthErrorInit as r, InvalidEmailError as s, AccountSuspendedError as t, MaxResendsError as u, authErrorFromResponse as v };
|