@auth-craft/client 0.1.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/README.md +315 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy.d.ts +75 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +95 -0
- package/dist/proxy.js.map +1 -0
- package/dist/types.test-d.d.ts +2 -0
- package/dist/types.test-d.d.ts.map +1 -0
- package/dist/types.test-d.js +26 -0
- package/dist/types.test-d.js.map +1 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# @auth-craft/client
|
|
2
|
+
|
|
3
|
+
Typed, contract-driven frontend client for Auth Craft.
|
|
4
|
+
|
|
5
|
+
Every method is generated from `@auth-craft/api-contract` — the **same** endpoint
|
|
6
|
+
declarations the backend routes validate against. So the client's paths, request
|
|
7
|
+
bodies, query params, path params **and response types cannot drift** from the
|
|
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 (via `@auth-craft/transport`,
|
|
10
|
+
absorbed from FetchGuard), protecting them from XSS.
|
|
11
|
+
|
|
12
|
+
> Migrating from the old `@auth-craft/frontend-sdk`? Jump to
|
|
13
|
+
> [Migration](#migration-from-auth-craftfrontend-sdk).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @auth-craft/client
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Bundler note (Vite)
|
|
24
|
+
|
|
25
|
+
The transport runs a Web Worker. With Vite, exclude it from pre-bundling:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// vite.config.ts
|
|
29
|
+
export default defineConfig({
|
|
30
|
+
optimizeDeps: { exclude: ['@auth-craft/transport'] }
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If you bundle the SDK to plain JS (e.g. consuming from `dist/`), pass a
|
|
35
|
+
`workerFactory` so the worker URL resolves in your app (see [Config](#config)).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { createAuthCraftClient } from '@auth-craft/client'
|
|
43
|
+
|
|
44
|
+
const client = createAuthCraftClient({
|
|
45
|
+
baseUrl: 'https://auth.example.com',
|
|
46
|
+
strategy: 'cookie-auth', // or 'body-auth'
|
|
47
|
+
allowedDomains: ['auth.example.com']
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// One-time init — boots the Worker and restores any existing session.
|
|
51
|
+
await client.init()
|
|
52
|
+
|
|
53
|
+
// Log in. Returns a discriminated result: tokens | challenge | mfa.
|
|
54
|
+
const r = await client.auth.authenticate({
|
|
55
|
+
body: { identityType: 'email', identityValue: 'user@example.com', credentialType: 'password', credentialValue: 'secret' }
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (r.ok) {
|
|
59
|
+
// session established → call authenticated endpoints
|
|
60
|
+
const me = await client.auth.me()
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## The call shape
|
|
67
|
+
|
|
68
|
+
Every generated method takes **one argument object** with up to three keys —
|
|
69
|
+
always the same shape, so you never have to remember a per-method signature:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
client.<group>.<method>({
|
|
73
|
+
params?: { ...pathParams }, // required only when the path has :params
|
|
74
|
+
body?: { ...requestBody }, // present only when the endpoint has a body
|
|
75
|
+
query?: { ...queryParams } // present only when the endpoint has a query
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- `params` is **required** when the path contains a segment like `:tenantId`.
|
|
80
|
+
- `body` is typed from the contract's request schema (missing/extra fields are a
|
|
81
|
+
compile error).
|
|
82
|
+
- `query` is always optional.
|
|
83
|
+
|
|
84
|
+
Every method returns `Promise<Result<T>>` from
|
|
85
|
+
[`ts-micro-result`](https://www.npmjs.com/package/ts-micro-result):
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const r = await client.auth.me()
|
|
89
|
+
if (r.ok) {
|
|
90
|
+
console.log(r.data.displayName) // typed from the contract response schema
|
|
91
|
+
} else {
|
|
92
|
+
console.error(r.errors[0].code) // backend error codes, forwarded as-is
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Config
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
interface AuthCraftClientConfig {
|
|
102
|
+
/** Base URL of the Auth Craft backend (no trailing slash needed). */
|
|
103
|
+
baseUrl: string
|
|
104
|
+
/** Refresh-token delivery: 'cookie-auth' (httpOnly cookie) or 'body-auth'. */
|
|
105
|
+
strategy: 'cookie-auth' | 'body-auth'
|
|
106
|
+
/** Hosts the Worker may attach tokens to (anti-exfiltration allow-list). */
|
|
107
|
+
allowedDomains: string[]
|
|
108
|
+
/** Refresh this many ms before access-token expiry (proactive). */
|
|
109
|
+
refreshEarlyMs?: number
|
|
110
|
+
/** Headers added to every request (e.g. an edge-gate secret). */
|
|
111
|
+
defaultHeaders?: Record<string, string>
|
|
112
|
+
/** Custom Worker factory — required when consuming the SDK as compiled JS. */
|
|
113
|
+
workerFactory?: () => Worker
|
|
114
|
+
/**
|
|
115
|
+
* Auth scope → sent as the `X-Auth-Context` header on every request.
|
|
116
|
+
* 'system' = admin, 'tenant' = tenant-scoped, omit/'customer' = default.
|
|
117
|
+
*/
|
|
118
|
+
context?: 'customer' | 'tenant' | 'system'
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Method groups
|
|
125
|
+
|
|
126
|
+
`client.<group>.<method>(...)`. Authenticated unless marked **public**.
|
|
127
|
+
|
|
128
|
+
### `auth`
|
|
129
|
+
| Method | HTTP | Notes |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `authenticate({ body })` | POST /auth/authenticate | **public**, login — returns tokens / challenge / mfa |
|
|
132
|
+
| `register({ body })` | POST /auth/register | **public** — returns `{ userId, verification? }` (does NOT log in) |
|
|
133
|
+
| `completeChallenge({ body })` | POST /auth/complete-challenge | **public**, login — finishes a 2-step credential challenge |
|
|
134
|
+
| `completeMfaChallenge({ body })` | POST /auth/complete-mfa-challenge | **public**, login — finishes MFA |
|
|
135
|
+
| `issueMfaChallenge({ body })` | POST /auth/issue-mfa-challenge | **public** — send an MFA code (SMS/email) |
|
|
136
|
+
| `me()` | GET /auth/me | current user profile |
|
|
137
|
+
| `selectTenant({ body })` | POST /auth/select-tenant | switch tenant context (re-issues tokens) |
|
|
138
|
+
| `logout()` | POST /auth/logout | clears the session |
|
|
139
|
+
|
|
140
|
+
> `refresh` is handled **automatically** by the transport (proactive, before
|
|
141
|
+
> expiry). You don't call it.
|
|
142
|
+
|
|
143
|
+
### `password`
|
|
144
|
+
`changePassword({ body })` · `forgotPassword({ body })` *(public)* · `resetPassword({ body })` *(public)*
|
|
145
|
+
|
|
146
|
+
### `verification`
|
|
147
|
+
`verifyCode({ body })` *(public)* · `resendVerification({ body })` *(public)*
|
|
148
|
+
|
|
149
|
+
### `session`
|
|
150
|
+
`listSessions({ query? })` · `deleteSession({ body })` · `revokeToken({ body })`
|
|
151
|
+
|
|
152
|
+
### `mfa`
|
|
153
|
+
`setupAuthenticator({ body })` · `verifyAuthenticator({ body })` · `listAuthenticators({ query? })` · `removeAuthenticator({ body })` · `updateMfaSettings({ body })`
|
|
154
|
+
|
|
155
|
+
> MFA verify: the code goes **inside `data`**, e.g.
|
|
156
|
+
> `verifyAuthenticator({ body: { method: 'totp', data: { code: '123456' } } })`.
|
|
157
|
+
|
|
158
|
+
### `oauth`
|
|
159
|
+
`oauthState()` *(public)* · `oauthCallback({ body })` *(public, login)* · `linkProvider({ body })` · `unlinkProvider({ body })` · `getProviders()`
|
|
160
|
+
|
|
161
|
+
### `users`
|
|
162
|
+
`updateProfile({ body })`
|
|
163
|
+
|
|
164
|
+
### `tenant`
|
|
165
|
+
All scoped under `/tenants/:tenantId/...` — **`params.tenantId` is required**:
|
|
166
|
+
|
|
167
|
+
`getTenantProfile({ params })` · `updateTenantProfile({ params, body })` · `createInvite({ params, body })` · `listInvites({ params, query? })` · `acceptInvite({ params, body })` · `declineInvite({ params, body })` · `revokeInvite({ params, body })` · `listMembers({ params, query? })` · `updateMember({ params, body })` · `removeMember({ params, body })`
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
await client.tenant.listMembers({ params: { tenantId: 'org-123' } })
|
|
171
|
+
await client.tenant.createInvite({
|
|
172
|
+
params: { tenantId: 'org-123' },
|
|
173
|
+
body: { inviteeEmail: 'new@example.com', roles: ['member'], authPerms: ['tenant.member.view'] }
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Login: handling challenge & MFA
|
|
180
|
+
|
|
181
|
+
A login call (`authenticate`, `completeChallenge`, `oauthCallback`) resolves to a
|
|
182
|
+
**discriminated union** — tokens, a credential challenge, or an MFA requirement.
|
|
183
|
+
Use the exported type guards to branch:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { isAuthTokens, isChallengeRequired, isMfaRequired } from '@auth-craft/api-contract'
|
|
187
|
+
|
|
188
|
+
const r = await client.auth.authenticate({
|
|
189
|
+
body: { identityType: 'email', identityValue: 'user@example.com', credentialType: 'otp' }
|
|
190
|
+
})
|
|
191
|
+
if (!r.ok) { /* real error */ return }
|
|
192
|
+
|
|
193
|
+
if (isChallengeRequired(r.data)) {
|
|
194
|
+
// e.g. emailed OTP — collect the code, then:
|
|
195
|
+
const done = await client.auth.completeChallenge({
|
|
196
|
+
body: { challengeId: r.data.challenge.data.challengeId as string, code: userCode }
|
|
197
|
+
})
|
|
198
|
+
} else if (isMfaRequired(r.data)) {
|
|
199
|
+
// collect MFA code for one of r.data.mfa.methods, then:
|
|
200
|
+
const done = await client.auth.completeMfaChallenge({
|
|
201
|
+
body: { challengeId: r.data.mfa.challengeId, method: r.data.mfa.methods[0], code: userCode }
|
|
202
|
+
})
|
|
203
|
+
} else if (isAuthTokens(r.data)) {
|
|
204
|
+
// session established
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Scopes (customer / tenant / system)
|
|
211
|
+
|
|
212
|
+
Set `context` so the backend reads the right refresh-token cookie / scope:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const adminClient = createAuthCraftClient({ /* ... */, context: 'system' })
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
This stamps `X-Auth-Context: system` on every request. (In a deployment with the
|
|
219
|
+
Cloudflare BFF gateway, the gateway is the source of truth for scope; the header
|
|
220
|
+
is the local-dev convention.)
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Lifecycle
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
await client.init() // boot Worker; idempotent enough to call once at startup
|
|
228
|
+
client.destroy() // terminate the Worker and clear in-memory state
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Why it can't drift
|
|
234
|
+
|
|
235
|
+
| Concern | Guaranteed by |
|
|
236
|
+
|---|---|
|
|
237
|
+
| Path / path params | shared `contract.path`; a CI test diffs every contract path against the live route registry |
|
|
238
|
+
| HTTP method | same registry diff (method + path) |
|
|
239
|
+
| Request body / query | `InferInput` of the contract schema — wrong/missing fields are a compile error |
|
|
240
|
+
| Response shape | `InferOutput` of the contract response schema; a CI test fails if a data endpoint lacks one |
|
|
241
|
+
| Method coverage | methods are **generated** from the contract group (`buildClient` iterates its keys) — a new endpoint appears automatically; there is no hand-maintained list to fall behind |
|
|
242
|
+
| Server reads the same shape | the backend route's `ctx.body`/`query`/`params` are typed from the **same** contract schema; renaming a field breaks the client call site *and* the route handler in one `tsc` run |
|
|
243
|
+
| Per-method transform bugs | there are none — the envelope→`Result<T>` step lives once in the transport |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Migration from `@auth-craft/frontend-sdk`
|
|
248
|
+
|
|
249
|
+
The old `@auth-craft/frontend-sdk` was hand-written (per-method paths/types), which
|
|
250
|
+
let request/response shapes drift from the server. It is **removed**. This client
|
|
251
|
+
replaces it. The package name and factory change, and every call uses the uniform
|
|
252
|
+
`{ body / params / query }` shape.
|
|
253
|
+
|
|
254
|
+
### Setup
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
// OLD
|
|
258
|
+
import { createAuthCraft } from '@auth-craft/frontend-sdk'
|
|
259
|
+
const client = createAuthCraft({ baseUrl, strategy, allowedDomains })
|
|
260
|
+
await client.init()
|
|
261
|
+
|
|
262
|
+
// NEW
|
|
263
|
+
import { createAuthCraftClient } from '@auth-craft/client'
|
|
264
|
+
const client = createAuthCraftClient({ baseUrl, strategy, allowedDomains })
|
|
265
|
+
await client.init()
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Call-site changes (the important part)
|
|
269
|
+
|
|
270
|
+
| Old | New |
|
|
271
|
+
|---|---|
|
|
272
|
+
| `client.auth.authenticate(req)` | `client.auth.authenticate({ body: req })` |
|
|
273
|
+
| `client.auth.me()` | `client.auth.me()` *(unchanged)* |
|
|
274
|
+
| `client.password.change(req)` | `client.password.changePassword({ body: req })` |
|
|
275
|
+
| `client.password.forgot(req)` | `client.password.forgotPassword({ body: req })` |
|
|
276
|
+
| `client.password.reset(req)` | `client.password.resetPassword({ body: req })` |
|
|
277
|
+
| `client.verification.verify(req)` | `client.verification.verifyCode({ body: req })` |
|
|
278
|
+
| `client.verification.resend(req)` | `client.verification.resendVerification({ body: req })` |
|
|
279
|
+
| `client.session.list(includeRevoked)` | `client.session.listSessions({ query: { includeRevoked: 'true' } })` |
|
|
280
|
+
| `client.session.delete(req)` | `client.session.deleteSession({ body: req })` |
|
|
281
|
+
| `client.session.revoke(req)` | `client.session.revokeToken({ body: req })` |
|
|
282
|
+
| `client.mfa.setup(req)` | `client.mfa.setupAuthenticator({ body: req })` |
|
|
283
|
+
| `client.mfa.verify({ method, code })` | `client.mfa.verifyAuthenticator({ body: { method, data: { code } } })` ⚠️ code in `data` |
|
|
284
|
+
| `client.mfa.list(opts)` | `client.mfa.listAuthenticators({ query: opts })` |
|
|
285
|
+
| `client.mfa.delete({ method, id })` | `client.mfa.removeAuthenticator({ body: { method, authenticatorId } })` ⚠️ `authenticatorId` |
|
|
286
|
+
| `client.mfa.updateSettings(req)` | `client.mfa.updateMfaSettings({ body: req })` |
|
|
287
|
+
| `client.oauth.callback(req)` | `client.oauth.oauthCallback({ body: req })` |
|
|
288
|
+
| `client.oauth.link(req)` | `client.oauth.linkProvider({ body: req })` ⚠️ no `accessToken` field |
|
|
289
|
+
| `client.oauth.unlink(req)` | `client.oauth.unlinkProvider({ body: req })` |
|
|
290
|
+
| `client.oauth.listProviders()` | `client.oauth.getProviders()` |
|
|
291
|
+
| *(none)* | `client.oauth.oauthState()` — **new**, required for the CSRF redirect flow |
|
|
292
|
+
| `client.user.updateProfile(req)` | `client.users.updateProfile({ body: req })` |
|
|
293
|
+
| `client.tenant.createInvite(req)` | `client.tenant.createInvite({ params: { tenantId }, body })` ⚠️ tenantId in path |
|
|
294
|
+
| `client.tenant.listMembers()` | `client.tenant.listMembers({ params: { tenantId } })` ⚠️ tenantId in path |
|
|
295
|
+
| `client.tenant.getProfile()` | `client.tenant.getTenantProfile({ params: { tenantId } })` |
|
|
296
|
+
| `client.tenant.checkPendingInvites(req)` | **removed** — no public route (internal only) |
|
|
297
|
+
|
|
298
|
+
### Behaviour changes you must be aware of
|
|
299
|
+
|
|
300
|
+
- **Tenant routes are path-scoped.** Every tenant method now requires
|
|
301
|
+
`params.tenantId`. The old client called `/tenant/...` (no id) and 404'd.
|
|
302
|
+
- **MFA codes go in `data`**, and the field is `authenticatorId` (not `id`).
|
|
303
|
+
- **OAuth never accepts `accessToken`** — the backend rejects it (token
|
|
304
|
+
substitution). Use `code` or `credential`. For the redirect/code flow, call
|
|
305
|
+
`oauth.oauthState()` first and forward the returned `state` to the provider.
|
|
306
|
+
- **Permissions are string arrays** (`roles`, `authPerms`), not numeric masks.
|
|
307
|
+
- **Login returns a union** — branch with `isAuthTokens` / `isChallengeRequired`
|
|
308
|
+
/ `isMfaRequired` (see [above](#login-handling-challenge--mfa)). The old client
|
|
309
|
+
returned ad-hoc shapes per path.
|
|
310
|
+
- **`refresh` is automatic** — remove any manual refresh calls.
|
|
311
|
+
|
|
312
|
+
### Errors
|
|
313
|
+
|
|
314
|
+
Both old and new return `Result<T>` from `ts-micro-result`, so `if (r.ok) { r.data } else { r.errors }`
|
|
315
|
+
is unchanged. The new client forwards the backend's error codes verbatim.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @auth-craft/client — contract-driven typed client.
|
|
3
|
+
*
|
|
4
|
+
* Wires the absorbed transport (FetchGuard) to the shared api-contract, so the
|
|
5
|
+
* client's methods are generated from — and type-checked against — the same
|
|
6
|
+
* endpoint declarations the backend routes use. No hand-written per-method
|
|
7
|
+
* paths/types: a route change in the contract re-types the call sites here.
|
|
8
|
+
*
|
|
9
|
+
* Module groups mirror the route groups: auth, password, verification, session,
|
|
10
|
+
* mfa, oauth, users, tenant. Tenant methods take `{ params: { tenantId }, ... }`
|
|
11
|
+
* because the routes are scoped under /tenants/:tenantId/...
|
|
12
|
+
*/
|
|
13
|
+
import { authContract, mfaContract, oauthContract, passwordContract, sessionContract, tenantsContract, usersContract, verificationContract } from '@auth-craft/api-contract';
|
|
14
|
+
import { type ContractClient } from './proxy';
|
|
15
|
+
export interface AuthCraftClientConfig {
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
strategy: 'cookie-auth' | 'body-auth';
|
|
18
|
+
allowedDomains: string[];
|
|
19
|
+
refreshEarlyMs?: number;
|
|
20
|
+
defaultHeaders?: Record<string, string>;
|
|
21
|
+
workerFactory?: () => Worker;
|
|
22
|
+
/** Auth context scope → X-Auth-Context header. */
|
|
23
|
+
context?: 'customer' | 'tenant' | 'system';
|
|
24
|
+
}
|
|
25
|
+
export declare class AuthCraftClient {
|
|
26
|
+
private fg;
|
|
27
|
+
private readonly config;
|
|
28
|
+
auth: ContractClient<typeof authContract>;
|
|
29
|
+
password: ContractClient<typeof passwordContract>;
|
|
30
|
+
verification: ContractClient<typeof verificationContract>;
|
|
31
|
+
session: ContractClient<typeof sessionContract>;
|
|
32
|
+
mfa: ContractClient<typeof mfaContract>;
|
|
33
|
+
oauth: ContractClient<typeof oauthContract>;
|
|
34
|
+
users: ContractClient<typeof usersContract>;
|
|
35
|
+
tenant: ContractClient<typeof tenantsContract>;
|
|
36
|
+
constructor(config: AuthCraftClientConfig);
|
|
37
|
+
init(): Promise<void>;
|
|
38
|
+
private wireGroups;
|
|
39
|
+
buildUrl(path: string): string;
|
|
40
|
+
destroy(): void;
|
|
41
|
+
}
|
|
42
|
+
export declare function createAuthCraftClient(config: AuthCraftClientConfig): AuthCraftClient;
|
|
43
|
+
export { buildClient } from './proxy';
|
|
44
|
+
export type { ContractClient, EndpointMethod, AuthResult } from './proxy';
|
|
45
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,YAAY,EACZ,WAAW,EACX,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,aAAa,EACb,oBAAoB,EACrB,MAAM,0BAA0B,CAAA;AAEjC,OAAO,EAAe,KAAK,cAAc,EAAE,MAAM,SAAS,CAAA;AAE1D,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,aAAa,GAAG,WAAW,CAAA;IACrC,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,aAAa,CAAC,EAAE,MAAM,MAAM,CAAA;IAC5B,kDAAkD;IAClD,OAAO,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAA;CAC3C;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,EAAE,CAAgC;IAC1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAE9C,IAAI,EAAG,cAAc,CAAC,OAAO,YAAY,CAAC,CAAA;IAC1C,QAAQ,EAAG,cAAc,CAAC,OAAO,gBAAgB,CAAC,CAAA;IAClD,YAAY,EAAG,cAAc,CAAC,OAAO,oBAAoB,CAAC,CAAA;IAC1D,OAAO,EAAG,cAAc,CAAC,OAAO,eAAe,CAAC,CAAA;IAChD,GAAG,EAAG,cAAc,CAAC,OAAO,WAAW,CAAC,CAAA;IACxC,KAAK,EAAG,cAAc,CAAC,OAAO,aAAa,CAAC,CAAA;IAC5C,KAAK,EAAG,cAAc,CAAC,OAAO,aAAa,CAAC,CAAA;IAC5C,MAAM,EAAG,cAAc,CAAC,OAAO,eAAe,CAAC,CAAA;gBAEnC,MAAM,EAAE,qBAAqB;IAInC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB3B,OAAO,CAAC,UAAU;IAYlB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAK9B,OAAO,IAAI,IAAI;CAIhB;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,eAAe,CAEpF;AAED,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACrC,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @auth-craft/client — contract-driven typed client.
|
|
3
|
+
*
|
|
4
|
+
* Wires the absorbed transport (FetchGuard) to the shared api-contract, so the
|
|
5
|
+
* client's methods are generated from — and type-checked against — the same
|
|
6
|
+
* endpoint declarations the backend routes use. No hand-written per-method
|
|
7
|
+
* paths/types: a route change in the contract re-types the call sites here.
|
|
8
|
+
*
|
|
9
|
+
* Module groups mirror the route groups: auth, password, verification, session,
|
|
10
|
+
* mfa, oauth, users, tenant. Tenant methods take `{ params: { tenantId }, ... }`
|
|
11
|
+
* because the routes are scoped under /tenants/:tenantId/...
|
|
12
|
+
*/
|
|
13
|
+
import { authContract, mfaContract, oauthContract, passwordContract, sessionContract, tenantsContract, usersContract, verificationContract } from '@auth-craft/api-contract';
|
|
14
|
+
import { createClient } from '@auth-craft/transport';
|
|
15
|
+
import { buildClient } from './proxy';
|
|
16
|
+
export class AuthCraftClient {
|
|
17
|
+
fg = null;
|
|
18
|
+
config;
|
|
19
|
+
auth;
|
|
20
|
+
password;
|
|
21
|
+
verification;
|
|
22
|
+
session;
|
|
23
|
+
mfa;
|
|
24
|
+
oauth;
|
|
25
|
+
users;
|
|
26
|
+
tenant;
|
|
27
|
+
constructor(config) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
}
|
|
30
|
+
async init() {
|
|
31
|
+
const headers = this.config.context && this.config.context !== 'customer'
|
|
32
|
+
? { ...this.config.defaultHeaders, 'X-Auth-Context': this.config.context }
|
|
33
|
+
: this.config.defaultHeaders;
|
|
34
|
+
this.fg = createClient({
|
|
35
|
+
provider: {
|
|
36
|
+
type: this.config.strategy,
|
|
37
|
+
refreshUrl: this.buildUrl('/auth/refresh'),
|
|
38
|
+
loginUrl: this.buildUrl('/auth/authenticate'),
|
|
39
|
+
logoutUrl: this.buildUrl('/auth/logout'),
|
|
40
|
+
...(headers ? { headers } : {})
|
|
41
|
+
},
|
|
42
|
+
allowedDomains: this.config.allowedDomains,
|
|
43
|
+
...(this.config.refreshEarlyMs !== undefined ? { refreshEarlyMs: this.config.refreshEarlyMs } : {}),
|
|
44
|
+
...(headers ? { defaultHeaders: headers } : {}),
|
|
45
|
+
...(this.config.workerFactory ? { workerFactory: this.config.workerFactory } : {})
|
|
46
|
+
});
|
|
47
|
+
await this.fg.whenReady();
|
|
48
|
+
this.wireGroups(this.fg);
|
|
49
|
+
}
|
|
50
|
+
wireGroups(fg) {
|
|
51
|
+
const url = (p) => this.buildUrl(p);
|
|
52
|
+
this.auth = buildClient(fg, authContract, url);
|
|
53
|
+
this.password = buildClient(fg, passwordContract, url);
|
|
54
|
+
this.verification = buildClient(fg, verificationContract, url);
|
|
55
|
+
this.session = buildClient(fg, sessionContract, url);
|
|
56
|
+
this.mfa = buildClient(fg, mfaContract, url);
|
|
57
|
+
this.oauth = buildClient(fg, oauthContract, url);
|
|
58
|
+
this.users = buildClient(fg, usersContract, url);
|
|
59
|
+
this.tenant = buildClient(fg, tenantsContract, url);
|
|
60
|
+
}
|
|
61
|
+
buildUrl(path) {
|
|
62
|
+
const p = path.startsWith('/') ? path : `/${path}`;
|
|
63
|
+
return `${this.config.baseUrl}${p}`;
|
|
64
|
+
}
|
|
65
|
+
destroy() {
|
|
66
|
+
this.fg?.destroy();
|
|
67
|
+
this.fg = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function createAuthCraftClient(config) {
|
|
71
|
+
return new AuthCraftClient(config);
|
|
72
|
+
}
|
|
73
|
+
export { buildClient } from './proxy';
|
|
74
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
import type { Endpoint, PathParams } from '@auth-craft/api-contract';
|
|
18
|
+
import { type FetchGuardClient } from '@auth-craft/transport';
|
|
19
|
+
import { type Result } from 'ts-micro-result';
|
|
20
|
+
import type { InferInput, InferOutput } from 'valibot';
|
|
21
|
+
export interface AuthResult {
|
|
22
|
+
authenticated: boolean;
|
|
23
|
+
user?: unknown;
|
|
24
|
+
expiresAt?: number | null;
|
|
25
|
+
}
|
|
26
|
+
/** Whether an endpoint has any `:param` segments. */
|
|
27
|
+
type HasParams<P extends string> = PathParams<P> extends never ? false : true;
|
|
28
|
+
/** The `params` argument, required only when the path has params. */
|
|
29
|
+
type ParamsArg<P extends string> = HasParams<P> extends true ? {
|
|
30
|
+
params: Record<PathParams<P>, string | number>;
|
|
31
|
+
} : {
|
|
32
|
+
params?: undefined;
|
|
33
|
+
};
|
|
34
|
+
/** Body argument, present only when the endpoint declares a body schema. */
|
|
35
|
+
type BodyArg<E> = E extends {
|
|
36
|
+
body: infer S;
|
|
37
|
+
} ? {
|
|
38
|
+
body: InferInput<S extends import('valibot').GenericSchema ? S : never>;
|
|
39
|
+
} : {
|
|
40
|
+
body?: undefined;
|
|
41
|
+
};
|
|
42
|
+
/** Query argument, present only when the endpoint declares a query schema. */
|
|
43
|
+
type QueryArg<E> = E extends {
|
|
44
|
+
query: infer S;
|
|
45
|
+
} ? {
|
|
46
|
+
query?: InferInput<S extends import('valibot').GenericSchema ? S : never>;
|
|
47
|
+
} : {
|
|
48
|
+
query?: undefined;
|
|
49
|
+
};
|
|
50
|
+
/** The single call argument object for a generated method. */
|
|
51
|
+
type CallArg<E extends Endpoint> = ParamsArg<E['path']> & BodyArg<E> & QueryArg<E>;
|
|
52
|
+
/** The resolved return type of a generated method. */
|
|
53
|
+
type CallReturn<E extends Endpoint> = E extends {
|
|
54
|
+
kind: 'data';
|
|
55
|
+
} ? E extends {
|
|
56
|
+
response: infer R;
|
|
57
|
+
} ? Result<InferOutput<R extends import('valibot').GenericSchema ? R : never>> : Result<unknown> : E extends {
|
|
58
|
+
kind: 'login';
|
|
59
|
+
} ? E extends {
|
|
60
|
+
response: infer R;
|
|
61
|
+
} ? Result<InferOutput<R extends import('valibot').GenericSchema ? R : never>> : Result<AuthResult> : Result<AuthResult>;
|
|
62
|
+
/** Does this arg shape need any argument at all? */
|
|
63
|
+
type NeedsArg<E extends Endpoint> = HasParams<E['path']> extends true ? true : E extends {
|
|
64
|
+
body: unknown;
|
|
65
|
+
} ? true : false;
|
|
66
|
+
export type EndpointMethod<E extends Endpoint> = NeedsArg<E> extends true ? (arg: CallArg<E>) => Promise<CallReturn<E>> : (arg?: CallArg<E>) => Promise<CallReturn<E>>;
|
|
67
|
+
export type ContractClient<C extends Record<string, Endpoint>> = {
|
|
68
|
+
[K in keyof C]: EndpointMethod<C[K]>;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Build a typed client object from a contract group.
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildClient<C extends Record<string, Endpoint>>(transport: FetchGuardClient, contract: C, buildUrl: (path: string) => string): ContractClient<C>;
|
|
74
|
+
export {};
|
|
75
|
+
//# sourceMappingURL=proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
import { fillPath } from '@auth-craft/api-contract';
|
|
18
|
+
import { deserializeResult } from '@auth-craft/transport';
|
|
19
|
+
import { err, ok } from 'ts-micro-result';
|
|
20
|
+
/**
|
|
21
|
+
* Build a typed client object from a contract group.
|
|
22
|
+
*/
|
|
23
|
+
export function buildClient(transport, contract, buildUrl) {
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const key of Object.keys(contract)) {
|
|
26
|
+
const ep = contract[key];
|
|
27
|
+
out[key] = async (arg) => {
|
|
28
|
+
const path = fillPath(ep.path, arg?.params);
|
|
29
|
+
const url = buildUrl(path) + buildQueryString(arg?.query);
|
|
30
|
+
const body = arg?.body;
|
|
31
|
+
const requiresAuth = ep.public !== true;
|
|
32
|
+
switch (ep.kind) {
|
|
33
|
+
case 'data':
|
|
34
|
+
return transport.callJson(ep.method, url, body, { requiresAuth });
|
|
35
|
+
case 'login':
|
|
36
|
+
return dispatchLogin(transport, body, url);
|
|
37
|
+
case 'logout':
|
|
38
|
+
return transport.logout(body);
|
|
39
|
+
case 'exchange':
|
|
40
|
+
return transport.exchangeToken(url, {
|
|
41
|
+
...(body !== undefined ? { payload: body } : {})
|
|
42
|
+
});
|
|
43
|
+
default:
|
|
44
|
+
return err({ code: 'UNKNOWN_KIND', message: `Unknown endpoint kind for '${key}'.` });
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Dispatch a login-kind endpoint and normalize the outcome.
|
|
52
|
+
*
|
|
53
|
+
* FetchGuard's login() resolves the AuthResult when a session is established. A
|
|
54
|
+
* 2-step login (credential challenge / MFA required) comes back as a non-error
|
|
55
|
+
* HTTP body that FetchGuard surfaces via `meta.params.body`. We deserialize that
|
|
56
|
+
* envelope so the caller gets the discriminated login result (tokens | challenge
|
|
57
|
+
* | mfa) instead of an opaque "not authenticated" — matching the old SDK's
|
|
58
|
+
* behaviour, but in ONE place instead of per method.
|
|
59
|
+
*/
|
|
60
|
+
async function dispatchLogin(transport, body, url) {
|
|
61
|
+
const result = await transport.login(body, url);
|
|
62
|
+
// Session established → return the tokens/auth-state shape.
|
|
63
|
+
if (result.ok && result.data.authenticated) {
|
|
64
|
+
return ok({
|
|
65
|
+
accessToken: '',
|
|
66
|
+
expiresAt: result.data.expiresAt ?? 0
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// Not authenticated: the backend body may carry a challenge / MFA requirement
|
|
70
|
+
// (a legitimate 2-step step), or a real error. Deserialize and forward it.
|
|
71
|
+
if (!result.ok) {
|
|
72
|
+
const params = result.meta?.params;
|
|
73
|
+
if (params?.body) {
|
|
74
|
+
const parsed = deserializeResult(params.body);
|
|
75
|
+
// Forward whatever the backend said — challenge/mfa (ok) or error (err).
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
return err(result.errors);
|
|
79
|
+
}
|
|
80
|
+
// ok but not authenticated and no body to interpret — surface a generic error.
|
|
81
|
+
return err({ code: 'LOGIN_FAILED', message: 'Authentication did not establish a session' });
|
|
82
|
+
}
|
|
83
|
+
/** Serialize a query object to a `?a=1&b=2` string (skips undefined). */
|
|
84
|
+
function buildQueryString(query) {
|
|
85
|
+
if (!query)
|
|
86
|
+
return '';
|
|
87
|
+
const params = new URLSearchParams();
|
|
88
|
+
for (const [k, v] of Object.entries(query)) {
|
|
89
|
+
if (v !== undefined && v !== null)
|
|
90
|
+
params.set(k, String(v));
|
|
91
|
+
}
|
|
92
|
+
const qs = params.toString();
|
|
93
|
+
return qs ? `?${qs}` : '';
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.test-d.d.ts","sourceRoot":"","sources":["../src/types.test-d.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level POC: the generated client methods are typed FROM the contract.
|
|
3
|
+
* Correct calls compile; wrong calls must fail (verified by @ts-expect-error).
|
|
4
|
+
* This is the mechanical guarantee — a contract change re-types these call sites.
|
|
5
|
+
*
|
|
6
|
+
* This file is type-checked (tsc --noEmit), not executed.
|
|
7
|
+
*/
|
|
8
|
+
import { authContract } from '@auth-craft/api-contract';
|
|
9
|
+
// ✓ me: no body, no params → callable with no argument
|
|
10
|
+
void client.me();
|
|
11
|
+
// ✓ authenticate: body inferred from the contract schema
|
|
12
|
+
void client.authenticate({
|
|
13
|
+
body: { identityType: 'email', identityValue: 'a@b.com', credentialType: 'password', credentialValue: 'x' }
|
|
14
|
+
});
|
|
15
|
+
// ✓ selectTenant: body { tenantId }
|
|
16
|
+
void client.selectTenant({ body: { tenantId: 't1' } });
|
|
17
|
+
// ✗ authenticate missing required fields → MUST error
|
|
18
|
+
// @ts-expect-error identityValue + credentialType are required
|
|
19
|
+
void client.authenticate({ body: { identityType: 'email' } });
|
|
20
|
+
// ✗ selectTenant wrong field name → MUST error
|
|
21
|
+
// @ts-expect-error 'tenant' is not a valid field
|
|
22
|
+
void client.selectTenant({ body: { tenant: 't1' } });
|
|
23
|
+
// ✗ me takes no body → MUST error
|
|
24
|
+
// @ts-expect-error me accepts no argument with body
|
|
25
|
+
void client.me({ body: { x: 1 } });
|
|
26
|
+
//# sourceMappingURL=types.test-d.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.test-d.js","sourceRoot":"","sources":["../src/types.test-d.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAKvD,uDAAuD;AACvD,KAAK,MAAM,CAAC,EAAE,EAAE,CAAA;AAEhB,yDAAyD;AACzD,KAAK,MAAM,CAAC,YAAY,CAAC;IACvB,IAAI,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,EAAE;CAC5G,CAAC,CAAA;AAEF,oCAAoC;AACpC,KAAK,MAAM,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;AAEtD,sDAAsD;AACtD,+DAA+D;AAC/D,KAAK,MAAM,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,CAAC,CAAA;AAE7D,+CAA+C;AAC/C,iDAAiD;AACjD,KAAK,MAAM,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;AAEpD,kCAAkC;AAClC,oDAAoD;AACpD,KAAK,MAAM,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@auth-craft/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Contract-driven typed frontend client for Auth Craft. Methods are generated from @auth-craft/api-contract over the @auth-craft/transport (Web-Worker token isolation), so routes and client cannot drift.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist", "README.md"],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsc --watch",
|
|
18
|
+
"clean": "rm -rf dist",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "vitest run"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@auth-craft/api-contract": "workspace:*",
|
|
24
|
+
"@auth-craft/transport": "workspace:*",
|
|
25
|
+
"ts-micro-result": "^3.3.0",
|
|
26
|
+
"valibot": "^1.4.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^6.0.3",
|
|
30
|
+
"vitest": "^4.1.8"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT"
|
|
33
|
+
}
|