@auth-craft/client 0.1.0 → 0.1.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 +144 -8
- package/dist/core/core.d.ts +72 -0
- package/dist/core/core.d.ts.map +1 -0
- package/dist/core/core.js +105 -0
- package/dist/core/core.js.map +1 -0
- package/dist/core/proxy.d.ts +79 -0
- package/dist/core/proxy.d.ts.map +1 -0
- package/dist/core/proxy.js +99 -0
- package/dist/core/proxy.js.map +1 -0
- package/dist/index.d.ts +15 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -55
- package/dist/index.js.map +1 -1
- package/dist/transport/client.d.ts +250 -0
- package/dist/transport/client.d.ts.map +1 -0
- package/dist/transport/client.js +940 -0
- package/dist/transport/client.js.map +1 -0
- package/dist/transport/constants.d.ts +9 -0
- package/dist/transport/constants.d.ts.map +1 -0
- package/dist/transport/constants.js +9 -0
- package/dist/transport/constants.js.map +1 -0
- package/dist/transport/error-codes.d.ts +41 -0
- package/dist/transport/error-codes.d.ts.map +1 -0
- package/dist/transport/error-codes.js +38 -0
- package/dist/transport/error-codes.js.map +1 -0
- package/dist/transport/errors.d.ts +55 -0
- package/dist/transport/errors.d.ts.map +1 -0
- package/dist/transport/errors.js +55 -0
- package/dist/transport/errors.js.map +1 -0
- package/dist/transport/helpers.d.ts +135 -0
- package/dist/transport/helpers.d.ts.map +1 -0
- package/dist/transport/helpers.js +185 -0
- package/dist/transport/helpers.js.map +1 -0
- package/dist/transport/index.d.ts +22 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +32 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/messages.d.ts +104 -0
- package/dist/transport/messages.d.ts.map +1 -0
- package/dist/transport/messages.js +24 -0
- package/dist/transport/messages.js.map +1 -0
- package/dist/transport/provider/create-provider.d.ts +38 -0
- package/dist/transport/provider/create-provider.d.ts.map +1 -0
- package/dist/transport/provider/create-provider.js +119 -0
- package/dist/transport/provider/create-provider.js.map +1 -0
- package/dist/transport/provider/parser/body.d.ts +7 -0
- package/dist/transport/provider/parser/body.d.ts.map +1 -0
- package/dist/transport/provider/parser/body.js +17 -0
- package/dist/transport/provider/parser/body.js.map +1 -0
- package/dist/transport/provider/parser/cookie.d.ts +8 -0
- package/dist/transport/provider/parser/cookie.d.ts.map +1 -0
- package/dist/transport/provider/parser/cookie.js +17 -0
- package/dist/transport/provider/parser/cookie.js.map +1 -0
- package/dist/transport/provider/parser/normalize.d.ts +6 -0
- package/dist/transport/provider/parser/normalize.d.ts.map +1 -0
- package/dist/transport/provider/parser/normalize.js +21 -0
- package/dist/transport/provider/parser/normalize.js.map +1 -0
- package/dist/transport/provider/presets.d.ts +31 -0
- package/dist/transport/provider/presets.d.ts.map +1 -0
- package/dist/transport/provider/presets.js +35 -0
- package/dist/transport/provider/presets.js.map +1 -0
- package/dist/transport/provider/register-presets.d.ts +10 -0
- package/dist/transport/provider/register-presets.d.ts.map +1 -0
- package/dist/transport/provider/register-presets.js +32 -0
- package/dist/transport/provider/register-presets.js.map +1 -0
- package/dist/transport/provider/storage/indexeddb.d.ts +26 -0
- package/dist/transport/provider/storage/indexeddb.d.ts.map +1 -0
- package/dist/transport/provider/storage/indexeddb.js +72 -0
- package/dist/transport/provider/storage/indexeddb.js.map +1 -0
- package/dist/transport/provider/strategy/body.d.ts +28 -0
- package/dist/transport/provider/strategy/body.d.ts.map +1 -0
- package/dist/transport/provider/strategy/body.js +73 -0
- package/dist/transport/provider/strategy/body.js.map +1 -0
- package/dist/transport/provider/strategy/cookie.d.ts +29 -0
- package/dist/transport/provider/strategy/cookie.d.ts.map +1 -0
- package/dist/transport/provider/strategy/cookie.js +70 -0
- package/dist/transport/provider/strategy/cookie.js.map +1 -0
- package/dist/transport/result.d.ts +28 -0
- package/dist/transport/result.d.ts.map +1 -0
- package/dist/transport/result.js +87 -0
- package/dist/transport/result.js.map +1 -0
- package/dist/transport/types.d.ts +488 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/types.js +3 -0
- package/dist/transport/types.js.map +1 -0
- package/dist/transport/utils/binary.d.ts +20 -0
- package/dist/transport/utils/binary.d.ts.map +1 -0
- package/dist/transport/utils/binary.js +51 -0
- package/dist/transport/utils/binary.js.map +1 -0
- package/dist/transport/utils/formdata.d.ts +26 -0
- package/dist/transport/utils/formdata.d.ts.map +1 -0
- package/dist/transport/utils/formdata.js +77 -0
- package/dist/transport/utils/formdata.js.map +1 -0
- package/dist/transport/utils/registry.d.ts +26 -0
- package/dist/transport/utils/registry.d.ts.map +1 -0
- package/dist/transport/utils/registry.js +51 -0
- package/dist/transport/utils/registry.js.map +1 -0
- package/dist/transport/worker-post.d.ts +44 -0
- package/dist/transport/worker-post.d.ts.map +1 -0
- package/dist/transport/worker-post.js +104 -0
- package/dist/transport/worker-post.js.map +1 -0
- package/dist/transport/worker.d.ts +10 -0
- package/dist/transport/worker.d.ts.map +1 -0
- package/dist/transport/worker.js +421 -0
- package/dist/transport/worker.js.map +1 -0
- package/dist/types.test-d.js +3 -9
- package/dist/types.test-d.js.map +1 -1
- package/eslint-isolation.mjs +82 -0
- package/package.json +30 -16
package/README.md
CHANGED
|
@@ -6,9 +6,13 @@ Every method is generated from `@auth-craft/api-contract` — the **same** endpo
|
|
|
6
6
|
declarations the backend routes validate against. So the client's paths, request
|
|
7
7
|
bodies, query params, path params **and response types cannot drift** from the
|
|
8
8
|
server: if a route changes and the contract is updated, your call sites re-type
|
|
9
|
-
at build time. Tokens are isolated in a Web Worker (
|
|
9
|
+
at build time. Tokens are isolated in a Web Worker (a vendored transport,
|
|
10
10
|
absorbed from FetchGuard), protecting them from XSS.
|
|
11
11
|
|
|
12
|
+
> **One scope per app?** Import [`@auth-craft/client/core`](#auth-craftclientcore--per-scope-isolation)
|
|
13
|
+
> + only the contract groups you need, so other scopes' code stays out of your
|
|
14
|
+
> bundle. The root export below wires every group.
|
|
15
|
+
|
|
12
16
|
> Migrating from the old `@auth-craft/frontend-sdk`? Jump to
|
|
13
17
|
> [Migration](#migration-from-auth-craftfrontend-sdk).
|
|
14
18
|
|
|
@@ -22,12 +26,12 @@ npm install @auth-craft/client
|
|
|
22
26
|
|
|
23
27
|
### Bundler note (Vite)
|
|
24
28
|
|
|
25
|
-
The
|
|
29
|
+
The client runs a Web Worker. With Vite, exclude it from pre-bundling:
|
|
26
30
|
|
|
27
31
|
```ts
|
|
28
32
|
// vite.config.ts
|
|
29
33
|
export default defineConfig({
|
|
30
|
-
optimizeDeps: { exclude: ['@auth-craft/
|
|
34
|
+
optimizeDeps: { exclude: ['@auth-craft/client'] }
|
|
31
35
|
})
|
|
32
36
|
```
|
|
33
37
|
|
|
@@ -116,6 +120,12 @@ interface AuthCraftClientConfig {
|
|
|
116
120
|
* 'system' = admin, 'tenant' = tenant-scoped, omit/'customer' = default.
|
|
117
121
|
*/
|
|
118
122
|
context?: 'customer' | 'tenant' | 'system'
|
|
123
|
+
/**
|
|
124
|
+
* Subscribe to session state changes (login, logout, refresh, expiry).
|
|
125
|
+
* Fires after `init()`. Bridge it into app state or trigger auto-logout
|
|
126
|
+
* on `authenticated: false`.
|
|
127
|
+
*/
|
|
128
|
+
onAuthStateChanged?: (state: { authenticated: boolean; user?: unknown; expiresAt?: number | null }) => void
|
|
119
129
|
}
|
|
120
130
|
```
|
|
121
131
|
|
|
@@ -176,6 +186,116 @@ await client.tenant.createInvite({
|
|
|
176
186
|
|
|
177
187
|
---
|
|
178
188
|
|
|
189
|
+
## `client.api` — authenticated requests to your own APIs
|
|
190
|
+
|
|
191
|
+
The typed groups above only cover **Auth Craft's** routes. Your app still has its
|
|
192
|
+
own backend (commerce, console, billing, …) that the *same* logged-in user must
|
|
193
|
+
call. `client.api` is that escape hatch: a raw HTTP transport that rides the
|
|
194
|
+
**same Worker-isolated session** — tokens stay out of JS, refresh is shared, and
|
|
195
|
+
you get a deserialized `Result<T>` back.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
// any URL you build — points at YOUR backend, not the auth server
|
|
199
|
+
const r = await client.api.post<{ items: Product[] }>(
|
|
200
|
+
'https://api.example.com/core/products',
|
|
201
|
+
{ name: 'Tee', price: 1900 }
|
|
202
|
+
)
|
|
203
|
+
if (r.ok) r.data.items // typed by the <T> you supply
|
|
204
|
+
|
|
205
|
+
await client.api.get<Plan>('https://api.example.com/core/plans/current')
|
|
206
|
+
await client.api.delete('https://api.example.com/core/team/members/remove', { userId })
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
interface ApiTransport {
|
|
211
|
+
get<T = unknown>(url: string, options?): Promise<Result<T>>
|
|
212
|
+
post<T = unknown>(url: string, body?: unknown, options?): Promise<Result<T>>
|
|
213
|
+
put<T = unknown>(url: string, body?: unknown, options?): Promise<Result<T>>
|
|
214
|
+
patch<T = unknown>(url: string, body?: unknown, options?): Promise<Result<T>>
|
|
215
|
+
delete<T = unknown>(url: string, body?: unknown, options?): Promise<Result<T>>
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- **URLs pass through as-is** — build absolute URLs to whichever backend you
|
|
220
|
+
target. (Unlike the typed groups, `api` does **not** prepend `baseUrl`.)
|
|
221
|
+
- **You supply the type** `<T>` — `api` is unaware of your app's contract. For
|
|
222
|
+
Auth Craft's own routes, prefer the typed groups, which can't drift.
|
|
223
|
+
- Throws if accessed before `init()`.
|
|
224
|
+
|
|
225
|
+
> The escape hatch is deliberately *outside* the contract: your app's routes are
|
|
226
|
+
> not Auth Craft's concern, so they don't belong in `@auth-craft/api-contract`.
|
|
227
|
+
> `api` gives you the authenticated session; you own the routes.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## `@auth-craft/client/core` — per-scope isolation
|
|
232
|
+
|
|
233
|
+
The root export (`createAuthCraftClient`) wires **every** group, so it can't be
|
|
234
|
+
tree-shaken per scope. A web app is almost always **one** scope (storefront =
|
|
235
|
+
customer, console = tenant, admin = system). For those, import the scope-agnostic
|
|
236
|
+
core and wire only the groups you need — other scopes' code then stays out of
|
|
237
|
+
your bundle and your dev call surface:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { createClientCore } from '@auth-craft/client/core'
|
|
241
|
+
import { authContract } from '@auth-craft/api-contract/auth'
|
|
242
|
+
|
|
243
|
+
const core = await createClientCore({
|
|
244
|
+
baseUrl, strategy: 'cookie-auth', allowedDomains, context: 'customer'
|
|
245
|
+
})
|
|
246
|
+
const auth = core.group(authContract) // typed, drift-proof
|
|
247
|
+
await core.api.get('https://api.example.com/core/products') // your own backend
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`core.group(contract)` attaches a typed client for one group; `core.api`,
|
|
251
|
+
`onAuthStateChanged`, `context`, `init()`/`destroy()` work exactly as on the full
|
|
252
|
+
client. A `tenant` console just wires more groups (`@auth-craft/api-contract/tenant`).
|
|
253
|
+
|
|
254
|
+
Verified: a customer app built this way contains **zero** tenant/mfa/oauth route
|
|
255
|
+
code. "Not in the bundle" ≠ "not on disk" — the unused group files still live in
|
|
256
|
+
`node_modules/@auth-craft/api-contract`, just unreferenced. That's isolation at
|
|
257
|
+
the call surface, which is the point.
|
|
258
|
+
|
|
259
|
+
### Keeping it enforced (two guards)
|
|
260
|
+
|
|
261
|
+
1. **Library guard (always on):** a test in this package fails if the core
|
|
262
|
+
(`src/core`) ever imports a contract group or the api-contract root — so the
|
|
263
|
+
isolation can't silently rot for downstream apps.
|
|
264
|
+
|
|
265
|
+
2. **App guard (optional, your linter):** stop an app importing a scope it
|
|
266
|
+
doesn't own.
|
|
267
|
+
|
|
268
|
+
**ESLint** — a helper ships here:
|
|
269
|
+
```ts
|
|
270
|
+
// app/eslint.config.mjs
|
|
271
|
+
import tseslint from 'typescript-eslint'
|
|
272
|
+
import { authCraftIsolation } from '@auth-craft/client/eslint'
|
|
273
|
+
export default tseslint.config(
|
|
274
|
+
authCraftIsolation({ allow: ['auth', 'password', 'verification', 'mfa', 'oauth', 'session', 'users'] })
|
|
275
|
+
)
|
|
276
|
+
```
|
|
277
|
+
Omit a group (e.g. `tenant`) and importing it — or the all-groups root
|
|
278
|
+
`@auth-craft/client`, or the api-contract root — becomes an error.
|
|
279
|
+
|
|
280
|
+
**Biome** — no shipped preset (Biome config is JSON, not composable), but the
|
|
281
|
+
same intent is `noRestrictedImports` in `biome.json`:
|
|
282
|
+
```jsonc
|
|
283
|
+
{
|
|
284
|
+
"linter": { "rules": { "nursery": { "noRestrictedImports": { "level": "error", "options": { "paths": {
|
|
285
|
+
"@auth-craft/client": "Use @auth-craft/client/core + per-group subpaths; the all-groups root can't be tree-shaken.",
|
|
286
|
+
"@auth-craft/api-contract": "Import a focused group (e.g. /auth), not the root.",
|
|
287
|
+
"@auth-craft/api-contract/tenant": "This app's scope does not include 'tenant'."
|
|
288
|
+
} } } } } }
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
(Rule placement — `nursery` vs stable — follows your Biome version.)
|
|
292
|
+
|
|
293
|
+
**Neither linter?** No guard is strictly required: an app that never imports
|
|
294
|
+
`@auth-craft/api-contract/tenant` simply doesn't have it; the guards only catch
|
|
295
|
+
*accidental* imports.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
179
299
|
## Login: handling challenge & MFA
|
|
180
300
|
|
|
181
301
|
A login call (`authenticate`, `completeChallenge`, `oauthCallback`) resolves to a
|
|
@@ -224,10 +344,15 @@ is the local-dev convention.)
|
|
|
224
344
|
## Lifecycle
|
|
225
345
|
|
|
226
346
|
```ts
|
|
227
|
-
await client.init()
|
|
228
|
-
client.destroy()
|
|
347
|
+
await client.init() // boot Worker; idempotent enough to call once at startup
|
|
348
|
+
client.destroy() // terminate the Worker and clear in-memory state
|
|
349
|
+
client.getTransport() // advanced: the raw FetchGuard transport handle
|
|
229
350
|
```
|
|
230
351
|
|
|
352
|
+
> `createAuthCraftClient(config)` returns the client **without** initializing —
|
|
353
|
+
> call `await client.init()` yourself. (`createClientCore(config)` from
|
|
354
|
+
> `@auth-craft/client/core` initializes for you and resolves a ready core.)
|
|
355
|
+
|
|
231
356
|
---
|
|
232
357
|
|
|
233
358
|
## Why it can't drift
|
|
@@ -247,8 +372,10 @@ client.destroy() // terminate the Worker and clear in-memory state
|
|
|
247
372
|
## Migration from `@auth-craft/frontend-sdk`
|
|
248
373
|
|
|
249
374
|
The old `@auth-craft/frontend-sdk` was hand-written (per-method paths/types), which
|
|
250
|
-
let request/response shapes drift from the server.
|
|
251
|
-
|
|
375
|
+
let request/response shapes drift from the server. This client supersedes it: the
|
|
376
|
+
auth methods are now generated from the contract (drift-proof), and raw
|
|
377
|
+
authenticated HTTP moves to [`client.api`](#clientapi--authenticated-requests-to-your-own-apis).
|
|
378
|
+
The package name and factory change, and every auth call uses the uniform
|
|
252
379
|
`{ body / params / query }` shape.
|
|
253
380
|
|
|
254
381
|
### Setup
|
|
@@ -293,7 +420,8 @@ await client.init()
|
|
|
293
420
|
| `client.tenant.createInvite(req)` | `client.tenant.createInvite({ params: { tenantId }, body })` ⚠️ tenantId in path |
|
|
294
421
|
| `client.tenant.listMembers()` | `client.tenant.listMembers({ params: { tenantId } })` ⚠️ tenantId in path |
|
|
295
422
|
| `client.tenant.getProfile()` | `client.tenant.getTenantProfile({ params: { tenantId } })` |
|
|
296
|
-
| `client.
|
|
423
|
+
| `client.api.get/post(...)` *(raw)* | `client.api.get/post(...)` — **unchanged surface**, but on `client.api` (the old SDK exposed these on the root client) |
|
|
424
|
+
| `client.tenant.checkPendingInvites(req)` | not on the typed `tenant` group (no public auth-route). If your app exposes it on its **own** API, call it via `client.api.post('<your-url>', req)`. |
|
|
297
425
|
|
|
298
426
|
### Behaviour changes you must be aware of
|
|
299
427
|
|
|
@@ -308,6 +436,14 @@ await client.init()
|
|
|
308
436
|
/ `isMfaRequired` (see [above](#login-handling-challenge--mfa)). The old client
|
|
309
437
|
returned ad-hoc shapes per path.
|
|
310
438
|
- **`refresh` is automatic** — remove any manual refresh calls.
|
|
439
|
+
- **Raw authenticated requests moved to `client.api`.** The old SDK exposed
|
|
440
|
+
`client.get/post/put/patch/delete` (and `getFetchGuard()`) on the root client
|
|
441
|
+
for hitting your own backend. These now live on [`client.api`](#clientapi--authenticated-requests-to-your-own-apis)
|
|
442
|
+
with the **same signatures** (`Result<T>` in, `Result<T>` out) — just change
|
|
443
|
+
`client.post(...)` → `client.api.post(...)`. Auth-route calls should move to
|
|
444
|
+
the typed groups instead.
|
|
445
|
+
- **`onAuthStateChanged` is unchanged** — still a config callback, same
|
|
446
|
+
`{ authenticated, user?, expiresAt? }` payload; wire your auto-logout there.
|
|
311
447
|
|
|
312
448
|
### Errors
|
|
313
449
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @auth-craft/client/core — scope-agnostic frontend client core.
|
|
3
|
+
*
|
|
4
|
+
* Owns the Web-Worker transport (token isolation + auto-refresh), the session
|
|
5
|
+
* lifecycle, the authenticated raw-HTTP escape hatch (`api`), and a `group()`
|
|
6
|
+
* factory that attaches a typed contract group.
|
|
7
|
+
*
|
|
8
|
+
* It deliberately imports **no** contract group. A per-scope app builds the
|
|
9
|
+
* client and wires only the groups it needs:
|
|
10
|
+
*
|
|
11
|
+
* import { createClientCore } from '@auth-craft/client/core'
|
|
12
|
+
* import { authContract } from '@auth-craft/api-contract/auth'
|
|
13
|
+
*
|
|
14
|
+
* const core = await createClientCore(config)
|
|
15
|
+
* const auth = core.group(authContract) // typed, drift-proof
|
|
16
|
+
* core.api.post('https://api.example.com/...', body) // your own backend
|
|
17
|
+
*
|
|
18
|
+
* Because no tenant/system contract is imported, that app's bundle — and its
|
|
19
|
+
* node_modules call surface — never contains code for other scopes.
|
|
20
|
+
*/
|
|
21
|
+
import type { Endpoint } from '@auth-craft/api-contract/core';
|
|
22
|
+
import type { Result } from 'ts-micro-result';
|
|
23
|
+
import { type FetchGuardClient, type FetchGuardRequestInit, type AuthResult as TransportAuthResult } from '../transport';
|
|
24
|
+
import { type ContractClient } from './proxy';
|
|
25
|
+
/**
|
|
26
|
+
* Authenticated raw-HTTP escape hatch for the host app's own (non-auth-craft)
|
|
27
|
+
* APIs. Rides the same Worker session as the typed groups; returns a
|
|
28
|
+
* deserialized `Result<T>`. URLs pass through as-is.
|
|
29
|
+
*/
|
|
30
|
+
export interface ApiTransport {
|
|
31
|
+
get<T = unknown>(url: string, options?: FetchGuardRequestInit): Promise<Result<T>>;
|
|
32
|
+
post<T = unknown>(url: string, body?: unknown, options?: FetchGuardRequestInit): Promise<Result<T>>;
|
|
33
|
+
put<T = unknown>(url: string, body?: unknown, options?: FetchGuardRequestInit): Promise<Result<T>>;
|
|
34
|
+
patch<T = unknown>(url: string, body?: unknown, options?: FetchGuardRequestInit): Promise<Result<T>>;
|
|
35
|
+
delete<T = unknown>(url: string, body?: unknown, options?: FetchGuardRequestInit): Promise<Result<T>>;
|
|
36
|
+
}
|
|
37
|
+
export interface ClientCoreConfig {
|
|
38
|
+
baseUrl: string;
|
|
39
|
+
strategy: 'cookie-auth' | 'body-auth';
|
|
40
|
+
allowedDomains: string[];
|
|
41
|
+
refreshEarlyMs?: number;
|
|
42
|
+
defaultHeaders?: Record<string, string>;
|
|
43
|
+
workerFactory?: () => Worker;
|
|
44
|
+
/** Auth scope → `X-Auth-Context` header. */
|
|
45
|
+
context?: 'customer' | 'tenant' | 'system';
|
|
46
|
+
/** Session state changes (login/logout/refresh/expiry). Fires after init(). */
|
|
47
|
+
onAuthStateChanged?: (state: TransportAuthResult) => void;
|
|
48
|
+
}
|
|
49
|
+
export declare class ClientCore {
|
|
50
|
+
private fg;
|
|
51
|
+
private readonly config;
|
|
52
|
+
constructor(config: ClientCoreConfig);
|
|
53
|
+
init(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Attach a typed client for one contract group. Call once per group the app
|
|
56
|
+
* uses. The group's URLs are resolved against `baseUrl`.
|
|
57
|
+
*/
|
|
58
|
+
group<C extends Record<string, Endpoint>>(contract: C): ContractClient<C>;
|
|
59
|
+
/**
|
|
60
|
+
* Authenticated raw-HTTP transport for the host app's own APIs. Throws if
|
|
61
|
+
* accessed before `init()`.
|
|
62
|
+
*/
|
|
63
|
+
get api(): ApiTransport;
|
|
64
|
+
buildUrl(path: string): string;
|
|
65
|
+
/** Raw transport handle — for advanced use (e.g. manual subscriptions). */
|
|
66
|
+
getTransport(): FetchGuardClient;
|
|
67
|
+
destroy(): void;
|
|
68
|
+
}
|
|
69
|
+
export declare function createClientCore(config: ClientCoreConfig): Promise<ClientCore>;
|
|
70
|
+
export type { AuthResult, ContractClient, EndpointMethod } from './proxy';
|
|
71
|
+
export { buildClient } from './proxy';
|
|
72
|
+
//# sourceMappingURL=core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,UAAU,IAAI,mBAAmB,EACvC,MAAM,cAAc,CAAC;AACtB,OAAO,EAAe,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAE3D;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACpG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACnG,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACrG,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;CACvG;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,aAAa,GAAG,WAAW,CAAC;IACtC,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;IAC7B,4CAA4C;IAC5C,OAAO,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC3C,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;CAC3D;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,EAAE,CAAiC;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;gBAE9B,MAAM,EAAE,gBAAgB;IAI9B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B3B;;;OAGG;IACH,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC;IAMzE;;;OAGG;IACH,IAAI,GAAG,IAAI,YAAY,CAqBtB;IAED,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAK9B,2EAA2E;IAC3E,YAAY,IAAI,gBAAgB;IAKhC,OAAO,IAAI,IAAI;CAIhB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAIpF;AAED,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @auth-craft/client/core — scope-agnostic frontend client core.
|
|
3
|
+
*
|
|
4
|
+
* Owns the Web-Worker transport (token isolation + auto-refresh), the session
|
|
5
|
+
* lifecycle, the authenticated raw-HTTP escape hatch (`api`), and a `group()`
|
|
6
|
+
* factory that attaches a typed contract group.
|
|
7
|
+
*
|
|
8
|
+
* It deliberately imports **no** contract group. A per-scope app builds the
|
|
9
|
+
* client and wires only the groups it needs:
|
|
10
|
+
*
|
|
11
|
+
* import { createClientCore } from '@auth-craft/client/core'
|
|
12
|
+
* import { authContract } from '@auth-craft/api-contract/auth'
|
|
13
|
+
*
|
|
14
|
+
* const core = await createClientCore(config)
|
|
15
|
+
* const auth = core.group(authContract) // typed, drift-proof
|
|
16
|
+
* core.api.post('https://api.example.com/...', body) // your own backend
|
|
17
|
+
*
|
|
18
|
+
* Because no tenant/system contract is imported, that app's bundle — and its
|
|
19
|
+
* node_modules call surface — never contains code for other scopes.
|
|
20
|
+
*/
|
|
21
|
+
import { createClient, } from '../transport';
|
|
22
|
+
import { buildClient } from './proxy';
|
|
23
|
+
export class ClientCore {
|
|
24
|
+
fg = null;
|
|
25
|
+
config;
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
}
|
|
29
|
+
async init() {
|
|
30
|
+
const headers = this.config.context && this.config.context !== 'customer'
|
|
31
|
+
? { ...this.config.defaultHeaders, 'X-Auth-Context': this.config.context }
|
|
32
|
+
: this.config.defaultHeaders;
|
|
33
|
+
this.fg = createClient({
|
|
34
|
+
provider: {
|
|
35
|
+
type: this.config.strategy,
|
|
36
|
+
refreshUrl: this.buildUrl('/auth/refresh'),
|
|
37
|
+
loginUrl: this.buildUrl('/auth/authenticate'),
|
|
38
|
+
logoutUrl: this.buildUrl('/auth/logout'),
|
|
39
|
+
...(headers ? { headers } : {}),
|
|
40
|
+
},
|
|
41
|
+
allowedDomains: this.config.allowedDomains,
|
|
42
|
+
...(this.config.refreshEarlyMs !== undefined ? { refreshEarlyMs: this.config.refreshEarlyMs } : {}),
|
|
43
|
+
...(headers ? { defaultHeaders: headers } : {}),
|
|
44
|
+
...(this.config.workerFactory ? { workerFactory: this.config.workerFactory } : {}),
|
|
45
|
+
});
|
|
46
|
+
await this.fg.whenReady();
|
|
47
|
+
if (this.config.onAuthStateChanged) {
|
|
48
|
+
this.fg.onAuthStateChanged(this.config.onAuthStateChanged);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Attach a typed client for one contract group. Call once per group the app
|
|
53
|
+
* uses. The group's URLs are resolved against `baseUrl`.
|
|
54
|
+
*/
|
|
55
|
+
group(contract) {
|
|
56
|
+
const fg = this.fg;
|
|
57
|
+
if (!fg)
|
|
58
|
+
throw new Error('ClientCore not initialized. Call init() first.');
|
|
59
|
+
return buildClient(fg, contract, (p) => this.buildUrl(p));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Authenticated raw-HTTP transport for the host app's own APIs. Throws if
|
|
63
|
+
* accessed before `init()`.
|
|
64
|
+
*/
|
|
65
|
+
get api() {
|
|
66
|
+
const fg = this.fg;
|
|
67
|
+
if (!fg)
|
|
68
|
+
throw new Error('ClientCore not initialized. Call init() first.');
|
|
69
|
+
// callJson forbids method/body in options — drop them before forwarding.
|
|
70
|
+
const opts = (o) => {
|
|
71
|
+
if (!o)
|
|
72
|
+
return {};
|
|
73
|
+
const { method: _m, body: _b, ...rest } = o;
|
|
74
|
+
return rest;
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
get: (url, options) => fg.callJson('GET', url, undefined, opts(options)),
|
|
78
|
+
post: (url, body, options) => fg.callJson('POST', url, body, opts(options)),
|
|
79
|
+
put: (url, body, options) => fg.callJson('PUT', url, body, opts(options)),
|
|
80
|
+
patch: (url, body, options) => fg.callJson('PATCH', url, body, opts(options)),
|
|
81
|
+
delete: (url, body, options) => fg.callJson('DELETE', url, body, opts(options)),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
buildUrl(path) {
|
|
85
|
+
const p = path.startsWith('/') ? path : `/${path}`;
|
|
86
|
+
return `${this.config.baseUrl}${p}`;
|
|
87
|
+
}
|
|
88
|
+
/** Raw transport handle — for advanced use (e.g. manual subscriptions). */
|
|
89
|
+
getTransport() {
|
|
90
|
+
if (!this.fg)
|
|
91
|
+
throw new Error('ClientCore not initialized. Call init() first.');
|
|
92
|
+
return this.fg;
|
|
93
|
+
}
|
|
94
|
+
destroy() {
|
|
95
|
+
this.fg?.destroy();
|
|
96
|
+
this.fg = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export async function createClientCore(config) {
|
|
100
|
+
const core = new ClientCore(config);
|
|
101
|
+
await core.init();
|
|
102
|
+
return core;
|
|
103
|
+
}
|
|
104
|
+
export { buildClient } from './proxy';
|
|
105
|
+
//# sourceMappingURL=core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/core/core.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract-driven typed proxy.
|
|
3
|
+
*
|
|
4
|
+
* Given a transport (the absorbed FetchGuard client) and a map of contract
|
|
5
|
+
* endpoints, this builds a typed method per endpoint. The `kind` discriminator
|
|
6
|
+
* decides how each one is dispatched:
|
|
7
|
+
*
|
|
8
|
+
* data → transport.callJson(method, url, body) (auto-bound, typed Result<T>)
|
|
9
|
+
* login → transport.login(body, url) (Worker session bootstrap)
|
|
10
|
+
* logout → transport.logout(body)
|
|
11
|
+
* exchange → transport.exchangeToken(url, { payload })
|
|
12
|
+
*
|
|
13
|
+
* Types flow straight from the contract: the input is inferred from the body
|
|
14
|
+
* schema, the output from the response schema (or AuthResult for login/logout).
|
|
15
|
+
* A route change to the contract therefore re-types every call site at build.
|
|
16
|
+
*
|
|
17
|
+
* Scope-agnostic: imports only `@auth-craft/api-contract/core` (the endpoint
|
|
18
|
+
* type + helpers), never a specific group — so a per-scope app that only wires
|
|
19
|
+
* `auth` never pulls in `tenant`/`system` code through this module.
|
|
20
|
+
*/
|
|
21
|
+
import type { Endpoint, PathParams } from '@auth-craft/api-contract/core';
|
|
22
|
+
import { type Result } from 'ts-micro-result';
|
|
23
|
+
import type { InferInput, InferOutput } from 'valibot';
|
|
24
|
+
import { type FetchGuardClient } from '../transport';
|
|
25
|
+
export interface AuthResult {
|
|
26
|
+
authenticated: boolean;
|
|
27
|
+
user?: unknown;
|
|
28
|
+
expiresAt?: number | null;
|
|
29
|
+
}
|
|
30
|
+
/** Whether an endpoint has any `:param` segments. */
|
|
31
|
+
type HasParams<P extends string> = PathParams<P> extends never ? false : true;
|
|
32
|
+
/** The `params` argument, required only when the path has params. */
|
|
33
|
+
type ParamsArg<P extends string> = HasParams<P> extends true ? {
|
|
34
|
+
params: Record<PathParams<P>, string | number>;
|
|
35
|
+
} : {
|
|
36
|
+
params?: undefined;
|
|
37
|
+
};
|
|
38
|
+
/** Body argument, present only when the endpoint declares a body schema. */
|
|
39
|
+
type BodyArg<E> = E extends {
|
|
40
|
+
body: infer S;
|
|
41
|
+
} ? {
|
|
42
|
+
body: InferInput<S extends import('valibot').GenericSchema ? S : never>;
|
|
43
|
+
} : {
|
|
44
|
+
body?: undefined;
|
|
45
|
+
};
|
|
46
|
+
/** Query argument, present only when the endpoint declares a query schema. */
|
|
47
|
+
type QueryArg<E> = E extends {
|
|
48
|
+
query: infer S;
|
|
49
|
+
} ? {
|
|
50
|
+
query?: InferInput<S extends import('valibot').GenericSchema ? S : never>;
|
|
51
|
+
} : {
|
|
52
|
+
query?: undefined;
|
|
53
|
+
};
|
|
54
|
+
/** The single call argument object for a generated method. */
|
|
55
|
+
type CallArg<E extends Endpoint> = ParamsArg<E['path']> & BodyArg<E> & QueryArg<E>;
|
|
56
|
+
/** The resolved return type of a generated method. */
|
|
57
|
+
type CallReturn<E extends Endpoint> = E extends {
|
|
58
|
+
kind: 'data';
|
|
59
|
+
} ? E extends {
|
|
60
|
+
response: infer R;
|
|
61
|
+
} ? Result<InferOutput<R extends import('valibot').GenericSchema ? R : never>> : Result<unknown> : E extends {
|
|
62
|
+
kind: 'login';
|
|
63
|
+
} ? E extends {
|
|
64
|
+
response: infer R;
|
|
65
|
+
} ? Result<InferOutput<R extends import('valibot').GenericSchema ? R : never>> : Result<AuthResult> : Result<AuthResult>;
|
|
66
|
+
/** Does this arg shape need any argument at all? */
|
|
67
|
+
type NeedsArg<E extends Endpoint> = HasParams<E['path']> extends true ? true : E extends {
|
|
68
|
+
body: unknown;
|
|
69
|
+
} ? true : false;
|
|
70
|
+
export type EndpointMethod<E extends Endpoint> = NeedsArg<E> extends true ? (arg: CallArg<E>) => Promise<CallReturn<E>> : (arg?: CallArg<E>) => Promise<CallReturn<E>>;
|
|
71
|
+
export type ContractClient<C extends Record<string, Endpoint>> = {
|
|
72
|
+
[K in keyof C]: EndpointMethod<C[K]>;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Build a typed client object from a contract group.
|
|
76
|
+
*/
|
|
77
|
+
export declare function buildClient<C extends Record<string, Endpoint>>(transport: FetchGuardClient, contract: C, buildUrl: (path: string) => string): ContractClient<C>;
|
|
78
|
+
export {};
|
|
79
|
+
//# sourceMappingURL=proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/core/proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract-driven typed proxy.
|
|
3
|
+
*
|
|
4
|
+
* Given a transport (the absorbed FetchGuard client) and a map of contract
|
|
5
|
+
* endpoints, this builds a typed method per endpoint. The `kind` discriminator
|
|
6
|
+
* decides how each one is dispatched:
|
|
7
|
+
*
|
|
8
|
+
* data → transport.callJson(method, url, body) (auto-bound, typed Result<T>)
|
|
9
|
+
* login → transport.login(body, url) (Worker session bootstrap)
|
|
10
|
+
* logout → transport.logout(body)
|
|
11
|
+
* exchange → transport.exchangeToken(url, { payload })
|
|
12
|
+
*
|
|
13
|
+
* Types flow straight from the contract: the input is inferred from the body
|
|
14
|
+
* schema, the output from the response schema (or AuthResult for login/logout).
|
|
15
|
+
* A route change to the contract therefore re-types every call site at build.
|
|
16
|
+
*
|
|
17
|
+
* Scope-agnostic: imports only `@auth-craft/api-contract/core` (the endpoint
|
|
18
|
+
* type + helpers), never a specific group — so a per-scope app that only wires
|
|
19
|
+
* `auth` never pulls in `tenant`/`system` code through this module.
|
|
20
|
+
*/
|
|
21
|
+
import { fillPath } from '@auth-craft/api-contract/core';
|
|
22
|
+
import { err, ok } from 'ts-micro-result';
|
|
23
|
+
import { deserializeResult } from '../transport';
|
|
24
|
+
/**
|
|
25
|
+
* Build a typed client object from a contract group.
|
|
26
|
+
*/
|
|
27
|
+
export function buildClient(transport, contract, buildUrl) {
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const key of Object.keys(contract)) {
|
|
30
|
+
const ep = contract[key];
|
|
31
|
+
out[key] = async (arg) => {
|
|
32
|
+
const path = fillPath(ep.path, arg?.params);
|
|
33
|
+
const url = buildUrl(path) + buildQueryString(arg?.query);
|
|
34
|
+
const body = arg?.body;
|
|
35
|
+
const requiresAuth = ep.public !== true;
|
|
36
|
+
switch (ep.kind) {
|
|
37
|
+
case 'data':
|
|
38
|
+
return transport.callJson(ep.method, url, body, { requiresAuth });
|
|
39
|
+
case 'login':
|
|
40
|
+
return dispatchLogin(transport, body, url);
|
|
41
|
+
case 'logout':
|
|
42
|
+
return transport.logout(body);
|
|
43
|
+
case 'exchange':
|
|
44
|
+
return transport.exchangeToken(url, {
|
|
45
|
+
...(body !== undefined ? { payload: body } : {}),
|
|
46
|
+
});
|
|
47
|
+
default:
|
|
48
|
+
return err({ code: 'UNKNOWN_KIND', message: `Unknown endpoint kind for '${key}'.` });
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Dispatch a login-kind endpoint and normalize the outcome.
|
|
56
|
+
*
|
|
57
|
+
* FetchGuard's login() resolves the AuthResult when a session is established. A
|
|
58
|
+
* 2-step login (credential challenge / MFA required) comes back as a non-error
|
|
59
|
+
* HTTP body that FetchGuard surfaces via `meta.params.body`. We deserialize that
|
|
60
|
+
* envelope so the caller gets the discriminated login result (tokens | challenge
|
|
61
|
+
* | mfa) instead of an opaque "not authenticated" — matching the old SDK's
|
|
62
|
+
* behaviour, but in ONE place instead of per method.
|
|
63
|
+
*/
|
|
64
|
+
async function dispatchLogin(transport, body, url) {
|
|
65
|
+
const result = await transport.login(body, url);
|
|
66
|
+
// Session established → return the tokens/auth-state shape.
|
|
67
|
+
if (result.ok && result.data.authenticated) {
|
|
68
|
+
return ok({
|
|
69
|
+
accessToken: '',
|
|
70
|
+
expiresAt: result.data.expiresAt ?? 0,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Not authenticated: the backend body may carry a challenge / MFA requirement
|
|
74
|
+
// (a legitimate 2-step step), or a real error. Deserialize and forward it.
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
const params = result.meta?.params;
|
|
77
|
+
if (params?.body) {
|
|
78
|
+
const parsed = deserializeResult(params.body);
|
|
79
|
+
// Forward whatever the backend said — challenge/mfa (ok) or error (err).
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
return err(result.errors);
|
|
83
|
+
}
|
|
84
|
+
// ok but not authenticated and no body to interpret — surface a generic error.
|
|
85
|
+
return err({ code: 'LOGIN_FAILED', message: 'Authentication did not establish a session' });
|
|
86
|
+
}
|
|
87
|
+
/** Serialize a query object to a `?a=1&b=2` string (skips undefined). */
|
|
88
|
+
function buildQueryString(query) {
|
|
89
|
+
if (!query)
|
|
90
|
+
return '';
|
|
91
|
+
const params = new URLSearchParams();
|
|
92
|
+
for (const [k, v] of Object.entries(query)) {
|
|
93
|
+
if (v !== undefined && v !== null)
|
|
94
|
+
params.set(k, String(v));
|
|
95
|
+
}
|
|
96
|
+
const qs = params.toString();
|
|
97
|
+
return qs ? `?${qs}` : '';
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/core/proxy.ts"],"names":[],"mappings":"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"}
|