herald-auth-web 0.5.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 +231 -0
- package/dist/index.d.ts +772 -0
- package/dist/index.global.js +8 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +1526 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# herald-auth-web
|
|
2
|
+
|
|
3
|
+
Official Herald browser JavaScript SDK for **third-party web integration**.
|
|
4
|
+
|
|
5
|
+
Framework-agnostic (React, Vue, or plain HTML), **zero runtime dependencies**
|
|
6
|
+
(native `fetch` + `WebCrypto` + `localStorage`). Wraps Herald's browser Bearer
|
|
7
|
+
authentication lifecycle — register, email verification, password reset, login
|
|
8
|
+
(with TOTP / passkey second factors and passwordless email-OTP), silent access
|
|
9
|
+
token refresh, logout, and status.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install herald-auth-web
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The package is **ESM-only** (`import`/`export`). Modern bundlers (Vite, webpack 5+,
|
|
18
|
+
Next.js, esbuild, Rollup) and ESM CDNs (esm.sh) consume it directly. It targets
|
|
19
|
+
the browser — there is no CommonJS build (Node/server use is a separate
|
|
20
|
+
server SDK's job).
|
|
21
|
+
|
|
22
|
+
### CDN / `<script>` (no build step)
|
|
23
|
+
|
|
24
|
+
A minified IIFE bundle exposing a `Herald` global is published for third-party
|
|
25
|
+
pages that integrate via a script tag:
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<script src="https://unpkg.com/herald-auth-web"></script>
|
|
29
|
+
<script>
|
|
30
|
+
const client = Herald.createHeraldClient({
|
|
31
|
+
baseUrl: 'https://auth.example.com',
|
|
32
|
+
realmId: '<your-realm>',
|
|
33
|
+
clientId: '<your-client-app>',
|
|
34
|
+
})
|
|
35
|
+
</script>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The `unpkg`/`jsdelivr` package fields resolve to `dist/index.global.js`, so the
|
|
39
|
+
bare URL above works; `https://cdn.jsdelivr.net/npm/herald-auth-web` works too.
|
|
40
|
+
|
|
41
|
+
> Building from source requires regenerating the typed client from the Herald
|
|
42
|
+
> backend: `npm run generate-api` (needs `cargo` + the backend), then `npm run build`.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { createHeraldClient, HeraldError } from 'herald-auth-web'
|
|
49
|
+
|
|
50
|
+
const client = createHeraldClient({
|
|
51
|
+
baseUrl: 'https://auth.example.com', // Herald API origin
|
|
52
|
+
realmId: '<your-realm>',
|
|
53
|
+
clientId: '<your-client-app>',
|
|
54
|
+
onSessionChange: (event) => {
|
|
55
|
+
if (event.type === 'session-expired') {
|
|
56
|
+
// redirect to your login page
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Register
|
|
62
|
+
await client.register({ email: 'user@example.com', password: '••••••••' })
|
|
63
|
+
|
|
64
|
+
// Email verification / password reset: the backend sends an email with a link
|
|
65
|
+
// that 302-redirects to your pre-registered Client App page. The SDK only
|
|
66
|
+
// triggers the send:
|
|
67
|
+
await client.triggerVerifyEmail({ email: 'user@example.com' })
|
|
68
|
+
|
|
69
|
+
// Login — multi-branch result (discriminate on `kind`)
|
|
70
|
+
const result = await client.login({ email: 'user@example.com', password: '••••••••' })
|
|
71
|
+
switch (result.kind) {
|
|
72
|
+
case 'success':
|
|
73
|
+
// logged in; result.session
|
|
74
|
+
break
|
|
75
|
+
case 'requires-second-factor': {
|
|
76
|
+
// result.secondFactors ⊆ ['totp', 'passkey']; result.tempToken
|
|
77
|
+
const final = await client.verifyTotp({ tempToken: result.tempToken, code: '123456' })
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
case 'consent-required':
|
|
81
|
+
// render result.agreements, then re-call login({ ..., agreements: result.agreements })
|
|
82
|
+
break
|
|
83
|
+
case 'oauth-redirect':
|
|
84
|
+
window.location.href = result.redirectTo
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Authenticated requests automatically inject Bearer + silently refresh on 401.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Origin pre-registration (CORS)
|
|
92
|
+
|
|
93
|
+
Herald's CORS policy matches the request origin against the Client App's
|
|
94
|
+
`allowed_origins` **exactly**. Before the SDK can call the API from your page,
|
|
95
|
+
add your page origin (scheme + host + port, e.g. `https://app.example.com`) to
|
|
96
|
+
the Client App's `allowed_origins` in the Herald console.
|
|
97
|
+
|
|
98
|
+
A non-registered origin surfaces as a `HeraldError { kind: 'network' }` (the
|
|
99
|
+
browser cannot distinguish a CORS rejection from a generic network failure).
|
|
100
|
+
|
|
101
|
+
## Turnstile
|
|
102
|
+
|
|
103
|
+
If the realm enforces Cloudflare Turnstile, pass the Turnstile token via the
|
|
104
|
+
`turnstileToken` field of each method payload.
|
|
105
|
+
|
|
106
|
+
## Token storage model
|
|
107
|
+
|
|
108
|
+
- **Access token** — held **only in memory** (never persisted). A page reload
|
|
109
|
+
clears it; the SDK silently refreshes it on the next request.
|
|
110
|
+
- **Refresh token** — stored via a pluggable `TokenStorage` interface; the
|
|
111
|
+
**default is `localStorage`**. The backend rotates it on every refresh and
|
|
112
|
+
revokes the whole family on reuse detection.
|
|
113
|
+
|
|
114
|
+
Tradeoff: a refresh token in `localStorage` is readable by XSS. This matches the
|
|
115
|
+
Herald own-frontend risk posture and is mitigated by server-side rotation +
|
|
116
|
+
reuse detection + absolute TTL + short-lived access tokens. For higher security,
|
|
117
|
+
inject `memoryStorage()` (no persistence across reloads) or a custom adapter:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { createHeraldClient, memoryStorage } from 'herald-auth-web'
|
|
121
|
+
|
|
122
|
+
const client = createHeraldClient({
|
|
123
|
+
baseUrl: 'https://auth.example.com',
|
|
124
|
+
realmId: '<realm>',
|
|
125
|
+
clientId: '<client-app>',
|
|
126
|
+
storage: memoryStorage(),
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## SSR / Node
|
|
131
|
+
|
|
132
|
+
`createHeraldClient` throws `HeraldError { kind: 'ssr-no-storage' }` when no
|
|
133
|
+
`storage` adapter is injected and `localStorage` is unavailable (e.g. SSR/Node).
|
|
134
|
+
Inject an explicit adapter in non-browser environments.
|
|
135
|
+
|
|
136
|
+
## Passkey login
|
|
137
|
+
|
|
138
|
+
Passkey login is two steps with a browser WebAuthn assertion in between:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { performPasskeyAssertion } from 'herald-auth-web'
|
|
142
|
+
|
|
143
|
+
// 1FA passkey login
|
|
144
|
+
const begin = await client.passkey.loginBegin({})
|
|
145
|
+
const assertion = await performPasskeyAssertion(begin.options)
|
|
146
|
+
const result = await client.passkey.loginFinish({ authToken: begin.authToken, assertion })
|
|
147
|
+
|
|
148
|
+
// 2FA passkey login (after a `requires-second-factor` result):
|
|
149
|
+
// const begin = await client.passkey.loginBegin({ tempToken })
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Passkey RP isolation requires your page origin to match the Client App's
|
|
153
|
+
pre-registered origin.
|
|
154
|
+
|
|
155
|
+
## Passwordless email-OTP login
|
|
156
|
+
|
|
157
|
+
Email-OTP is an independent **passwordless first factor** (not a second factor).
|
|
158
|
+
`send` resolves a discriminated result: the two 409 control-flow outcomes —
|
|
159
|
+
`consent_required` (auto-register consent gate; render `agreements` and re-send
|
|
160
|
+
with them) and `email_not_registered` (auto-register off) — arrive as
|
|
161
|
+
`{ kind: 'conflict' }` instead of throwing:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const sent = await client.loginWithEmailOtp.send({ email: 'user@example.com' })
|
|
165
|
+
if (sent.kind === 'conflict' && sent.code === 'consent_required') {
|
|
166
|
+
// render sent.agreements (each entry carries the raw backend summary on
|
|
167
|
+
// `.raw` for display), then re-send with the accepted pairs:
|
|
168
|
+
await client.loginWithEmailOtp.send({
|
|
169
|
+
email: 'user@example.com',
|
|
170
|
+
agreements: sent.agreements.map(({ agreementType, versionId }) => ({ agreementType, versionId })),
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Verify applies the issued token set on success (same as login).
|
|
175
|
+
const result = await client.loginWithEmailOtp.verify({ email: 'user@example.com', code: '123456' })
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Error handling
|
|
179
|
+
|
|
180
|
+
Every method rejects with a `HeraldError`. Branch on the stable `kind`:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
try {
|
|
184
|
+
await client.login({ email, password })
|
|
185
|
+
} catch (e) {
|
|
186
|
+
if (e instanceof HeraldError) {
|
|
187
|
+
switch (e.kind) {
|
|
188
|
+
case 'unauthorized': // bad credentials
|
|
189
|
+
case 'rate-limited': // 429
|
|
190
|
+
case 'validation': // 400
|
|
191
|
+
case 'network': // fetch failed / CORS
|
|
192
|
+
// ...
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Out of scope
|
|
199
|
+
|
|
200
|
+
This SDK wraps only the `/login` direct-signed `CustomUserUi` credential class
|
|
201
|
+
(no PKCE → FirstParty), and the authentication lifecycle. Server-side resource
|
|
202
|
+
management, high-risk operations (password change, authenticator management,
|
|
203
|
+
account deletion), and framework-specific adapters are separate concerns.
|
|
204
|
+
|
|
205
|
+
## First-party consumption (Herald's own frontend)
|
|
206
|
+
|
|
207
|
+
Herald's own frontend also consumes this SDK as its token engine
|
|
208
|
+
(DEC-js-sdk-013). On top of the third-party surface above it uses the additive
|
|
209
|
+
first-party bridge:
|
|
210
|
+
|
|
211
|
+
- `client.tokens.getAccessToken()` / `setTokens({ accessToken, refreshToken,
|
|
212
|
+
clientId? })` / `clear()` / `bindClientId(clientId)` — inspect / inject the
|
|
213
|
+
token family owned by the host app (e.g. after its own PKCE exchange or
|
|
214
|
+
switch-client, which stay in the host per the scope decision above);
|
|
215
|
+
- `client.refresh()` — the public single-flight refresh (shares its in-flight
|
|
216
|
+
promise with the 401 interceptor);
|
|
217
|
+
- `login` / `passkey.loginBegin` accept an optional OAuth context
|
|
218
|
+
(`oauthClientId`/`redirectUri`/`state`, or the `oauth` object) which is
|
|
219
|
+
passed through untouched; the backend answers with `redirectTo` and the
|
|
220
|
+
caller completes the exchange itself;
|
|
221
|
+
- `consent-required` results carry the raw agreement summary on each entry's
|
|
222
|
+
`raw` field for host apps that render the consent list.
|
|
223
|
+
|
|
224
|
+
## Regenerating the client
|
|
225
|
+
|
|
226
|
+
The typed HTTP client is generated from the Herald backend OpenAPI spec:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
npm run generate-api # cargo export-openapi + @hey-api/openapi-ts
|
|
230
|
+
npm run build
|
|
231
|
+
```
|