@productcraft/auth 0.5.2 → 0.7.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 +78 -4
- package/dist/index.cjs +592 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +970 -156
- package/dist/index.d.ts +970 -156
- package/dist/index.js +585 -6
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -90,8 +90,11 @@ await consumer.auth.signup({
|
|
|
90
90
|
|
|
91
91
|
await consumer.auth.refresh({ refresh_token });
|
|
92
92
|
await consumer.auth.logout({ refresh_token });
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
|
|
94
|
+
// Forgot password. `sendPasswordResetEmail` mints the code AND emails it
|
|
95
|
+
// via Mail — this is what you want for a normal reset flow.
|
|
96
|
+
await consumer.auth.sendPasswordResetEmail({ email });
|
|
97
|
+
await consumer.auth.resetPassword({ code, new_password: "..." });
|
|
95
98
|
|
|
96
99
|
// Sign in with Apple (native iOS flow). See "Federated sign-in" below.
|
|
97
100
|
await consumer.auth.signinWithProvider({
|
|
@@ -116,6 +119,27 @@ await consumer.oauth.clientCredentials({ clientId, clientSecret, scope });
|
|
|
116
119
|
|
|
117
120
|
> **Wire-shape note.** Auth's JSON wire is snake_case, and the SDK returns response objects as-is (e.g. `access_token`, `refresh_token`, `token_type`, `expires_in`). Request DTOs follow the same convention; some convenience fields (`identifier`, `password`, `nonce`) are unaffected, but anywhere the spec uses an underscore the SDK does too.
|
|
118
121
|
|
|
122
|
+
### One-time codes: who sends the email?
|
|
123
|
+
|
|
124
|
+
Verification and password-reset each come in two flavours. **Picking the wrong one is silent** — you get a `2xx` and no email is ever sent — so choose deliberately:
|
|
125
|
+
|
|
126
|
+
| Method | Mints a code | Sends the email | Returns |
|
|
127
|
+
| --- | --- | --- | --- |
|
|
128
|
+
| `auth.requestVerification()` | ✅ | ❌ | `{ code, expires_at }` |
|
|
129
|
+
| `auth.sendVerificationEmail()` | ✅ | ✅ via Mail | `{ expires_at }` |
|
|
130
|
+
| `auth.requestReset()` | ✅ | ❌ | `{ code, expires_at }` |
|
|
131
|
+
| `auth.sendPasswordResetEmail()` | ✅ | ✅ via Mail | `{ expires_at }` |
|
|
132
|
+
|
|
133
|
+
**Use `send*` for a normal flow.** Mail delivers the code using your app's configured sender and template, and you never handle the plaintext code.
|
|
134
|
+
|
|
135
|
+
**Use `request*` only when you are delivering the code yourself** — your own SMTP, an SMS provider, or consuming it in-process (e.g. provisioning a test account). If you call `request*` and drop the returned `code` on the floor, your users get nothing and nothing is logged anywhere, because no send was ever attempted.
|
|
136
|
+
|
|
137
|
+
Each pair is gated by a *separate* PAK permission (`auth.user.verify.create` vs `auth.user.verify.send-email`, and likewise for `password-reset`), so a workspace can enable minting without enabling outbound mail.
|
|
138
|
+
|
|
139
|
+
The `send*` methods fail loudly with a typed `412` when Mail isn't ready — `ENVOI_NOT_ENABLED`, `ENVOI_SENDER_NOT_CONFIGURED`, `ENVOI_TEMPLATE_NOT_CONFIGURED`, `ENVOI_APP_NOT_BOUND_TO_WORKSPACE` — or `503 ENVOI_DISPATCH_FAILED`. If your endpoint returns a uniform response to prevent account enumeration, catch and **log** these rather than discarding them; a swallowed 412 is indistinguishable from the silent failure above.
|
|
140
|
+
|
|
141
|
+
Both `request*` and `send*` return `{}` when the contact doesn't match (or is already verified / unverified as appropriate). That's the anti-enumeration contract, not an error — there is nothing to branch on.
|
|
142
|
+
|
|
119
143
|
## Federated sign-in (Apple, Google)
|
|
120
144
|
|
|
121
145
|
`consumer.auth.signinWithProvider` lets a BFF exchange a provider-issued identity token for an Auth `{ access_token, refresh_token }` pair. The endpoint creates the EndUser on first sign-in and returns the same account on subsequent ones — the SDK is a thin wrapper over Auth's `POST /{appSlug}/v1/auth/oauth/{provider}` which does the heavy lifting (Apple JWKS verification, issuer pinning, audience validation against the app's configured native client ids, server-side nonce replay protection, account creation / linking).
|
|
@@ -193,6 +217,56 @@ await consumer.auth.signinWithProvider({
|
|
|
193
217
|
|
|
194
218
|
No SDK upgrade required — the per-provider TS enum widens automatically with the next spec refresh.
|
|
195
219
|
|
|
220
|
+
## Passkeys (WebAuthn)
|
|
221
|
+
|
|
222
|
+
Passkeys are switched on per app in the console (relying-party id + the
|
|
223
|
+
site origin in `allowed_redirect_origins`). Once they are, each ceremony
|
|
224
|
+
is one call on `auth.consumer(appSlug).passkeys`. The helpers run
|
|
225
|
+
**in the browser** — they drive `navigator.credentials` — but importing
|
|
226
|
+
the package anywhere is safe; nothing touches a browser global until you
|
|
227
|
+
call them.
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { Auth, isPasskeySupported, PasskeyError } from "@productcraft/auth";
|
|
231
|
+
|
|
232
|
+
if (!isPasskeySupported()) hidePasskeyButton(); // false in Node and in insecure contexts
|
|
233
|
+
|
|
234
|
+
// Enrol — signed-in user adds a passkey as an MFA factor
|
|
235
|
+
const me = new Auth({ auth: { type: "bearer", token: accessToken } }).consumer("acme");
|
|
236
|
+
const { factor, recovery_codes } = await me.passkeys.enroll({ label: "MacBook" });
|
|
237
|
+
// show recovery_codes once — they are not retrievable later
|
|
238
|
+
|
|
239
|
+
// Passwordless sign-in — no password, no email; discoverable credential
|
|
240
|
+
const anon = new Auth().consumer("acme");
|
|
241
|
+
const session = await anon.passkeys.signIn({ mediation: "conditional" }); // amr: ["webauthn"]
|
|
242
|
+
|
|
243
|
+
// MFA step of a password sign-in — when signin() answered { mfa_required, mfa_token, factors }
|
|
244
|
+
const session2 = await anon.passkeys.completeMfaChallenge({ mfa_token: signin.mfa_token });
|
|
245
|
+
|
|
246
|
+
// Step-up inside a live session
|
|
247
|
+
const { amr, mfa_at } = await me.passkeys.stepUp();
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Each helper does the options call → `navigator.credentials.create()` /
|
|
251
|
+
`.get()` → verify call, with every buffer base64url-encoded in both
|
|
252
|
+
directions and the `public_key` members renamed to the WebAuthn spec's
|
|
253
|
+
camelCase (the Auth wire is snake_case). Browser-side failures reject
|
|
254
|
+
with `PasskeyError` (`code`: `unsupported`, `cancelled`, `timeout`,
|
|
255
|
+
`invalid_state`, `security`, `no_credential`, `unknown`; `cause` holds
|
|
256
|
+
the DOM error); server-side failures stay `AuthHttpError`, so a 401 on
|
|
257
|
+
a forged or replayed assertion is distinguishable from the user
|
|
258
|
+
dismissing the prompt. Pass `signal: AbortSignal.timeout(ms)` to bound
|
|
259
|
+
the prompt yourself.
|
|
260
|
+
|
|
261
|
+
The encoding layer is exported for anyone who wants the pieces:
|
|
262
|
+
`toB64u`, `fromB64u`, `decodeOptions`, `encodeCredential`, plus
|
|
263
|
+
`createPasskeyCredential(public_key)` / `getPasskeyCredential(public_key)`
|
|
264
|
+
which wrap `navigator.credentials` on their own.
|
|
265
|
+
|
|
266
|
+
`signIn()` never sends an identifier: `/auth/passkey/options` takes none
|
|
267
|
+
by design (it would be an account-existence oracle), so sign-in relies on
|
|
268
|
+
a discoverable (resident) credential.
|
|
269
|
+
|
|
196
270
|
## JWT verification
|
|
197
271
|
|
|
198
272
|
Every Auth app publishes a JWKS at `/{appSlug}/v1/.well-known/jwks.json`. The SDK gives you a verified-claims helper and a jose-compatible JWKS resolver.
|
|
@@ -255,7 +329,7 @@ For the per-app boundary, the JWKS is the cryptographic gate (only the app's key
|
|
|
255
329
|
|
|
256
330
|
### Passport integration
|
|
257
331
|
|
|
258
|
-
Use the companion package [`@productcraft/auth-passport`](
|
|
332
|
+
Use the companion package [`@productcraft/auth-passport`](https://github.com/clauderanelagh/productcraft-node/tree/main/packages/auth-passport):
|
|
259
333
|
|
|
260
334
|
```bash
|
|
261
335
|
npm install @productcraft/auth-passport passport-jwt
|
|
@@ -319,4 +393,4 @@ When the spec changes, the nightly `spec-refresh` workflow re-runs codegen and o
|
|
|
319
393
|
|
|
320
394
|
## License
|
|
321
395
|
|
|
322
|
-
[MIT](
|
|
396
|
+
[MIT](https://github.com/clauderanelagh/productcraft-node/blob/main/LICENSE).
|