@productcraft/auth 0.6.0 → 0.8.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 +73 -5
- package/dist/index.cjs +566 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +963 -157
- package/dist/index.d.ts +963 -157
- package/dist/index.js +558 -9
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -13,9 +13,8 @@ Server-side only. For React / Next.js apps, call this from your backend (BFF pat
|
|
|
13
13
|
```ts
|
|
14
14
|
import { Auth } from "@productcraft/auth";
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
});
|
|
16
|
+
// Reads PRODUCTCRAFT_API_KEY (and PRODUCTCRAFT_AUTH_APP) from the environment
|
|
17
|
+
const auth = new Auth();
|
|
19
18
|
```
|
|
20
19
|
|
|
21
20
|
The SDK splits into three caller contexts.
|
|
@@ -217,6 +216,56 @@ await consumer.auth.signinWithProvider({
|
|
|
217
216
|
|
|
218
217
|
No SDK upgrade required — the per-provider TS enum widens automatically with the next spec refresh.
|
|
219
218
|
|
|
219
|
+
## Passkeys (WebAuthn)
|
|
220
|
+
|
|
221
|
+
Passkeys are switched on per app in the console (relying-party id + the
|
|
222
|
+
site origin in `allowed_redirect_origins`). Once they are, each ceremony
|
|
223
|
+
is one call on `auth.consumer(appSlug).passkeys`. The helpers run
|
|
224
|
+
**in the browser** — they drive `navigator.credentials` — but importing
|
|
225
|
+
the package anywhere is safe; nothing touches a browser global until you
|
|
226
|
+
call them.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { Auth, isPasskeySupported, PasskeyError } from "@productcraft/auth";
|
|
230
|
+
|
|
231
|
+
if (!isPasskeySupported()) hidePasskeyButton(); // false in Node and in insecure contexts
|
|
232
|
+
|
|
233
|
+
// Enrol — signed-in user adds a passkey as an MFA factor
|
|
234
|
+
const me = new Auth({ auth: { type: "bearer", token: accessToken } }).consumer("acme");
|
|
235
|
+
const { factor, recovery_codes } = await me.passkeys.enroll({ label: "MacBook" });
|
|
236
|
+
// show recovery_codes once — they are not retrievable later
|
|
237
|
+
|
|
238
|
+
// Passwordless sign-in — no password, no email; discoverable credential
|
|
239
|
+
const anon = new Auth().consumer("acme");
|
|
240
|
+
const session = await anon.passkeys.signIn({ mediation: "conditional" }); // amr: ["webauthn"]
|
|
241
|
+
|
|
242
|
+
// MFA step of a password sign-in — when signin() answered { mfa_required, mfa_token, factors }
|
|
243
|
+
const session2 = await anon.passkeys.completeMfaChallenge({ mfa_token: signin.mfa_token });
|
|
244
|
+
|
|
245
|
+
// Step-up inside a live session
|
|
246
|
+
const { amr, mfa_at } = await me.passkeys.stepUp();
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Each helper does the options call → `navigator.credentials.create()` /
|
|
250
|
+
`.get()` → verify call, with every buffer base64url-encoded in both
|
|
251
|
+
directions and the `public_key` members renamed to the WebAuthn spec's
|
|
252
|
+
camelCase (the Auth wire is snake_case). Browser-side failures reject
|
|
253
|
+
with `PasskeyError` (`code`: `unsupported`, `cancelled`, `timeout`,
|
|
254
|
+
`invalid_state`, `security`, `no_credential`, `unknown`; `cause` holds
|
|
255
|
+
the DOM error); server-side failures stay `AuthHttpError`, so a 401 on
|
|
256
|
+
a forged or replayed assertion is distinguishable from the user
|
|
257
|
+
dismissing the prompt. Pass `signal: AbortSignal.timeout(ms)` to bound
|
|
258
|
+
the prompt yourself.
|
|
259
|
+
|
|
260
|
+
The encoding layer is exported for anyone who wants the pieces:
|
|
261
|
+
`toB64u`, `fromB64u`, `decodeOptions`, `encodeCredential`, plus
|
|
262
|
+
`createPasskeyCredential(public_key)` / `getPasskeyCredential(public_key)`
|
|
263
|
+
which wrap `navigator.credentials` on their own.
|
|
264
|
+
|
|
265
|
+
`signIn()` never sends an identifier: `/auth/passkey/options` takes none
|
|
266
|
+
by design (it would be an account-existence oracle), so sign-in relies on
|
|
267
|
+
a discoverable (resident) credential.
|
|
268
|
+
|
|
220
269
|
## JWT verification
|
|
221
270
|
|
|
222
271
|
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.
|
|
@@ -260,6 +309,8 @@ try {
|
|
|
260
309
|
|
|
261
310
|
Behind the scenes: JWKS fetched once, cached in-memory (10 min TTL by default), singleflighted so 100 concurrent verifies do 1 fetch, auto-refetched if a token's `kid` isn't in the cached JWKS (rotation handling). Issuer + audience are checked against `scope.acceptedIssuers` + `scope.expectedAudience` automatically.
|
|
262
311
|
|
|
312
|
+
**Refresh tokens are refused.** A token carrying `token_use: "refresh"` (or the older `type: "refresh"`) throws `JwtRefreshTokenError` (code `ERR_JWT_REFRESH_TOKEN`) even when its signature, issuer and audience are valid: a refresh token is only good at the refresh endpoint, never as an access token. `createAuthJwtStrategy` from `@productcraft/auth-passport` refuses them too. If you call `jwtVerify` yourself (below), check `isRefreshToken(payload)` and reject.
|
|
313
|
+
|
|
263
314
|
### Building blocks for `jose`, NestJS guards, Hono, etc.
|
|
264
315
|
|
|
265
316
|
`scope.jwks.getKey` is a [jose-compatible](https://github.com/panva/jose) key resolver — pass it anywhere a `GetKeyFunction` is expected.
|
|
@@ -279,7 +330,7 @@ For the per-app boundary, the JWKS is the cryptographic gate (only the app's key
|
|
|
279
330
|
|
|
280
331
|
### Passport integration
|
|
281
332
|
|
|
282
|
-
Use the companion package [`@productcraft/auth-passport`](
|
|
333
|
+
Use the companion package [`@productcraft/auth-passport`](https://github.com/clauderanelagh/productcraft-node/tree/main/packages/auth-passport):
|
|
283
334
|
|
|
284
335
|
```bash
|
|
285
336
|
npm install @productcraft/auth-passport passport-jwt
|
|
@@ -318,11 +369,28 @@ import {
|
|
|
318
369
|
} from "@productcraft/auth";
|
|
319
370
|
```
|
|
320
371
|
|
|
372
|
+
## Credentials from the environment
|
|
373
|
+
|
|
374
|
+
Server-side, omit `auth` and the client reads your workspace key from the environment:
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
# .env
|
|
378
|
+
PRODUCTCRAFT_API_KEY=pcft_live_...
|
|
379
|
+
PRODUCTCRAFT_AUTH_APP=acme # default slug for auth.consumer()
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
- An explicit `auth` always wins over the environment.
|
|
383
|
+
- Hosts are never read from the environment. Override them in code with `baseUrl`.
|
|
384
|
+
- This is a server-only fallback. Never put a key in `NEXT_PUBLIC_*` or any other variable a bundler inlines into browser code.
|
|
385
|
+
- `auth.app(appId)` and the workspace methods (`apps`, `stats`) use the explicit `auth`, else `PRODUCTCRAFT_API_KEY`.
|
|
386
|
+
- `auth.consumer()` takes its slug from `PRODUCTCRAFT_AUTH_APP` when you omit it, and **never** reads a credential from the environment: consumer routes act for an end user, so only an explicit `auth` is sent. PAK-only consumer routes (`signup_requires_pak` sign-ups, tenants, code mint, factors) therefore need `new Auth({ auth: { type: "apiKey", key: process.env.MY_APP_PAK! } })`.
|
|
387
|
+
|
|
321
388
|
## Configuration
|
|
322
389
|
|
|
323
390
|
```ts
|
|
324
391
|
new Auth({
|
|
325
392
|
// Auth credential the SDK presents to Auth
|
|
393
|
+
// Omit to use PRODUCTCRAFT_API_KEY from the environment
|
|
326
394
|
auth: { type: "apiKey", key: "pcft_live_..." }
|
|
327
395
|
| { type: "bearer", token: "eyJ..." }
|
|
328
396
|
| { type: "cookie", value: "auth_token=..." },
|
|
@@ -343,4 +411,4 @@ When the spec changes, the nightly `spec-refresh` workflow re-runs codegen and o
|
|
|
343
411
|
|
|
344
412
|
## License
|
|
345
413
|
|
|
346
|
-
[MIT](
|
|
414
|
+
[MIT](https://github.com/clauderanelagh/productcraft-node/blob/main/LICENSE).
|