okengine 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/site/content/docs/ai/index.mdx +24 -0
- package/site/content/docs/ai/llms-txt.mdx +3 -0
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/console/gates.mdx +46 -8
- package/site/content/docs/console/index.mdx +54 -0
- package/site/content/docs/console/meta.json +1 -0
- package/site/content/docs/elements/gate.mdx +187 -48
- package/site/content/docs/elements/index.mdx +45 -0
- package/site/content/docs/elements/meta.json +1 -1
- package/site/content/docs/get-started/basic-usage.mdx +4 -3
- package/site/content/docs/get-started/index.mdx +33 -0
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/why.mdx +10 -10
- package/site/content/docs/index.mdx +9 -31
- package/site/content/docs/plugins/anonymous.mdx +95 -0
- package/site/content/docs/plugins/compression.mdx +2 -2
- package/site/content/docs/plugins/cors.mdx +2 -2
- package/site/content/docs/plugins/csrf.mdx +2 -2
- package/site/content/docs/plugins/email-otp.mdx +111 -0
- package/site/content/docs/plugins/{security-headers.mdx → headers.mdx} +11 -11
- package/site/content/docs/plugins/index.mdx +69 -0
- package/site/content/docs/plugins/magic-link.mdx +112 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +8 -8
- package/site/content/docs/plugins/meta.json +10 -1
- package/site/content/docs/plugins/passkey.mdx +140 -0
- package/site/content/docs/plugins/phone-number.mdx +111 -0
- package/site/content/docs/plugins/two-factor.mdx +117 -0
- package/site/content/docs/plugins/username.mdx +117 -0
- package/site/content/docs/reference/client.mdx +331 -0
- package/site/content/docs/reference/fx.mdx +20 -5
- package/site/content/docs/reference/index.mdx +45 -0
- package/site/content/docs/reference/meta.json +11 -1
- package/site/content/docs/reference/plugins.mdx +25 -14
- package/src/auth/auth.test.ts +20 -2
- package/src/auth/bindings.ts +439 -0
- package/src/auth/breach-check.ts +112 -0
- package/src/auth/config.ts +288 -0
- package/src/auth/constant-time.ts +22 -0
- package/src/auth/cookies.ts +123 -0
- package/src/auth/gate-auth.test.ts +379 -0
- package/src/auth/identity.ts +190 -0
- package/src/auth/index.ts +119 -1
- package/src/auth/method-context.ts +33 -0
- package/src/auth/operator.ts +27 -1
- package/src/auth/password-policy.test.ts +126 -0
- package/src/auth/password-policy.ts +77 -0
- package/src/auth/plugin.ts +62 -4
- package/src/auth/rate.ts +45 -0
- package/src/auth/schema.ts +260 -0
- package/src/auth/secondary-storage.ts +37 -0
- package/src/auth/sessions.ts +58 -1
- package/src/auth/tables.ts +4 -0
- package/src/auth/verification.ts +78 -0
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/dev.test.ts +3 -3
- package/src/cli/schema.ts +95 -23
- package/src/client/auth.ts +120 -0
- package/src/client-react/index.ts +93 -0
- package/src/compiler/aot.test.ts +2 -1
- package/src/compiler/extract.ts +19 -0
- package/src/compiler/response.ts +16 -2
- package/src/console/server/app.ts +10 -6
- package/src/console/server/auth-rate.test.ts +3 -3
- package/src/console/server/bind.ts +12 -1
- package/src/console/server/channels.test.ts +1 -1
- package/src/console/server/console-gates.ts +14 -0
- package/src/console/server/console.test.ts +6 -6
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/server/flows.ts +2 -0
- package/src/console/server/gates.ts +8 -1
- package/src/console/server/operator-db.test.ts +4 -4
- package/src/console/server/operator-db.ts +22 -4
- package/src/console/server/security.gate.test.ts +3 -3
- package/src/console/ui/gates/fixture.ts +4 -0
- package/src/console/ui/gates/types.ts +2 -0
- package/src/console/ui/shell/client.ts +1 -0
- package/src/elements/gate/boot.ts +136 -0
- package/src/elements/gate/config.ts +69 -0
- package/src/elements/gate/declare.ts +51 -1
- package/src/elements/gate/runtime.ts +3 -1
- package/src/elements/gate.test.ts +77 -0
- package/src/elements/gate.ts +20 -1
- package/src/elements/index.ts +8 -0
- package/src/index.ts +11 -0
- package/src/kernel/app.ts +253 -32
- package/src/kernel/boot.test.ts +40 -3
- package/src/kernel/boot.ts +8 -0
- package/src/kernel/call.test.ts +46 -2
- package/src/kernel/edge.test.ts +3 -3
- package/src/kernel/flow.test.ts +2 -2
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +60 -0
- package/src/kernel/hooks.test.ts +4 -4
- package/src/kernel/index.ts +12 -0
- package/src/kernel/pipeline.test.ts +12 -8
- package/src/kernel/pipeline.ts +23 -4
- package/src/kernel/plugin/decorate.test.ts +3 -3
- package/src/kernel/plugin/scoping.test.ts +3 -3
- package/src/kernel/plugin-elements.test.ts +51 -0
- package/src/kernel/plugin-needs.test.ts +83 -0
- package/src/kernel/plugin-needs.ts +129 -0
- package/src/kernel/plugin.ts +101 -0
- package/src/kernel/registry-isolation.test.ts +5 -5
- package/src/kernel/registry.ts +102 -3
- package/src/manifest/types.ts +2 -0
- package/src/plugins/anonymous.ts +58 -0
- package/src/plugins/auth/shared.ts +121 -0
- package/src/plugins/auth-methods.security.test.ts +762 -0
- package/src/plugins/auth-methods.test.ts +176 -0
- package/src/plugins/compression.test.ts +5 -5
- package/src/plugins/compression.ts +1 -1
- package/src/plugins/config-source.test.ts +12 -12
- package/src/plugins/config-source.ts +2 -2
- package/src/plugins/cors.test.ts +16 -10
- package/src/plugins/cors.ts +1 -1
- package/src/plugins/csrf.test.ts +1 -1
- package/src/plugins/email-otp.ts +161 -0
- package/src/plugins/{security-headers.test.ts → headers.test.ts} +26 -26
- package/src/plugins/headers.ts +240 -41
- package/src/plugins/index.ts +42 -3
- package/src/plugins/ip-allowlist.test.ts +19 -9
- package/src/plugins/magic-link.ts +163 -0
- package/src/plugins/maintenance-mode.test.ts +9 -5
- package/src/plugins/passkey-webauthn.ts +217 -0
- package/src/plugins/passkey.ts +282 -0
- package/src/plugins/phone-number.ts +149 -0
- package/src/plugins/response-headers.ts +54 -0
- package/src/plugins/two-factor.ts +253 -0
- package/src/plugins/username.ts +148 -0
- package/src/runs/runs.test.ts +6 -2
- package/src/runtime/primitives.ts +37 -4
- package/src/runtime/serve.test.ts +3 -2
- package/src/runtime/types.ts +25 -2
- package/src/test/create-test-app.test.ts +1 -1
- package/src/test/create-test-app.ts +4 -1
- package/src/test/provisions.integration.test.ts +1 -1
- package/src/plugins/security-headers.ts +0 -255
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Passkey"
|
|
3
|
+
description: "Official plugin — WebAuthn register and authenticate under /auth with signature and origin checks."
|
|
4
|
+
icon: "FingerprintPattern"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`passkey()` adds register and authenticate Flows for WebAuthn credentials (`oke_passkeys`).
|
|
9
|
+
Options return a challenge; register and authenticate verify `clientDataJSON` origin + challenge,
|
|
10
|
+
`authenticatorData` rpId hash, and an ECDSA P-256 signature against the stored SPKI public key.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Enable `gate.auth`, then `.plug(passkey())`. Registration needs a Bearer session; authenticate is
|
|
14
|
+
public. Post the full ceremony fields — presence of a stored credential alone never issues a
|
|
15
|
+
session.
|
|
16
|
+
</Callout>
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
<Steps>
|
|
21
|
+
|
|
22
|
+
<Step>
|
|
23
|
+
### Plug it
|
|
24
|
+
|
|
25
|
+
```typescript title="src/app.ts"
|
|
26
|
+
import { oke } from "okengine";
|
|
27
|
+
import { passkey } from "okengine/plugins";
|
|
28
|
+
|
|
29
|
+
export const app = oke({
|
|
30
|
+
name: "shop",
|
|
31
|
+
env: "local",
|
|
32
|
+
gate: { auth: {} },
|
|
33
|
+
}).plug(passkey({ origins: ["http://localhost", "https://localhost"] }));
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
</Step>
|
|
37
|
+
|
|
38
|
+
<Step>
|
|
39
|
+
### Register (session required)
|
|
40
|
+
|
|
41
|
+
Wire Bearer on `createClient` (`auth.getToken` / `memorySession`) — calls take input only.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const opts = await api.auth.passkeyRegisterOptions({});
|
|
45
|
+
// opts.data: { challenge, rpId, userId }
|
|
46
|
+
|
|
47
|
+
await api.auth.passkeyRegister({
|
|
48
|
+
credentialId: "...", // base64url
|
|
49
|
+
publicKey: "...", // base64url SPKI (ECDSA P-256)
|
|
50
|
+
userId: opts.data!.userId,
|
|
51
|
+
challenge: opts.data!.challenge,
|
|
52
|
+
clientDataJSON: "...", // base64url JSON { type, challenge, origin }
|
|
53
|
+
authenticatorData: "...", // base64url
|
|
54
|
+
signature: "...", // base64url ECDSA over authData || SHA-256(clientDataJSON)
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Paths: `POST /auth/passkey/register/options`, `POST /auth/passkey/register`.
|
|
59
|
+
|
|
60
|
+
</Step>
|
|
61
|
+
|
|
62
|
+
<Step>
|
|
63
|
+
### Authenticate
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const opts = await api.auth.passkeyAuthenticateOptions({});
|
|
67
|
+
const { data } = await api.auth.passkeyAuthenticate({
|
|
68
|
+
credentialId: "...",
|
|
69
|
+
challenge: opts.data!.challenge,
|
|
70
|
+
clientDataJSON: "...",
|
|
71
|
+
authenticatorData: "...",
|
|
72
|
+
signature: "...",
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Paths: `POST /auth/passkey/authenticate/options`, `POST /auth/passkey/authenticate`.
|
|
77
|
+
Challenges are single-use; wrong origin → `invalid_origin`; bad signature → `invalid_credentials`.
|
|
78
|
+
|
|
79
|
+
</Step>
|
|
80
|
+
|
|
81
|
+
</Steps>
|
|
82
|
+
|
|
83
|
+
## Options
|
|
84
|
+
|
|
85
|
+
| Option | Type | Default | Meaning |
|
|
86
|
+
| ------------ | ------------------- | ------------------------------------------ | -------------------------------- |
|
|
87
|
+
| `secret` | `string` | active\* | HMAC secret (\*from `gate.auth`) |
|
|
88
|
+
| `sessions` | `SessionStore` | active\* | Session store |
|
|
89
|
+
| `now` | `() => number` | `Date.now` | Injectable clock |
|
|
90
|
+
| `passkeys` | `PasskeyStore` | new | Credential → user mapping |
|
|
91
|
+
| `challenges` | `VerificationStore` | new | Registration / auth challenges |
|
|
92
|
+
| `rpId` | `string` | `"localhost"` | Relying party id |
|
|
93
|
+
| `origins` | `string[]` | `["http://localhost","https://localhost"]` | Allowed `clientDataJSON.origin` |
|
|
94
|
+
|
|
95
|
+
## Surfaces
|
|
96
|
+
|
|
97
|
+
| Flow | Path | Gate |
|
|
98
|
+
| --------------------------------- | ----------------------------------------- | ------------------------ |
|
|
99
|
+
| `auth.passkeyRegisterOptions` | `POST /auth/passkey/register/options` | session + bearer |
|
|
100
|
+
| `auth.passkeyRegister` | `POST /auth/passkey/register` | session + bearer |
|
|
101
|
+
| `auth.passkeyAuthenticateOptions` | `POST /auth/passkey/authenticate/options` | `gate.public` + otp rate |
|
|
102
|
+
| `auth.passkeyAuthenticate` | `POST /auth/passkey/authenticate` | `gate.public` + otp rate |
|
|
103
|
+
|
|
104
|
+
**Consequence:** a stolen `credentialId` without the private key cannot mint a session. Set
|
|
105
|
+
`origins` to your real app origins before production.
|
|
106
|
+
|
|
107
|
+
## Troubleshooting
|
|
108
|
+
|
|
109
|
+
<Accordions>
|
|
110
|
+
<Accordion title="register fails with unauthenticated">
|
|
111
|
+
|
|
112
|
+
Sign in with another method first. `userId` in the body must match the Bearer session.
|
|
113
|
+
|
|
114
|
+
</Accordion>
|
|
115
|
+
<Accordion title="authenticate returns invalid_origin">
|
|
116
|
+
|
|
117
|
+
`clientDataJSON.origin` must be in `passkey({ origins })`. Default allows only localhost HTTP/S.
|
|
118
|
+
|
|
119
|
+
</Accordion>
|
|
120
|
+
<Accordion title="authenticate returns invalid_credentials">
|
|
121
|
+
|
|
122
|
+
Unknown `credentialId`, consumed/expired challenge, bad signature, or rpId hash mismatch.
|
|
123
|
+
Re-run authenticate options for a fresh challenge, then sign with the enrolled private key.
|
|
124
|
+
|
|
125
|
+
</Accordion>
|
|
126
|
+
</Accordions>
|
|
127
|
+
|
|
128
|
+
## Learn more
|
|
129
|
+
|
|
130
|
+
- [Two-factor](/docs/plugins/two-factor) — TOTP step-up
|
|
131
|
+
- [Gate](/docs/elements/gate) — `gate.auth`
|
|
132
|
+
- [Client](/docs/reference/client) — calling `/auth` from the browser
|
|
133
|
+
|
|
134
|
+
## Next
|
|
135
|
+
|
|
136
|
+
<Cards>
|
|
137
|
+
<Card title="Two-factor" description="TOTP enable / verify." href="/docs/plugins/two-factor" />
|
|
138
|
+
<Card title="Gate" description="Builtin auth and policies." href="/docs/elements/gate" />
|
|
139
|
+
<Card title="Anonymous" description="Guest sessions." href="/docs/plugins/anonymous" />
|
|
140
|
+
</Cards>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Phone number"
|
|
3
|
+
description: "Official plugin — E.164 phone OTP request and verify under /auth."
|
|
4
|
+
icon: "Smartphone"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`phoneNumber()` signs people in with a phone number in E.164 form (`+15551234567`). Codes are
|
|
9
|
+
6 digits, hashed, 10 minutes, 5 attempts — same security shape as email OTP.
|
|
10
|
+
|
|
11
|
+
<Callout title="The one rule">
|
|
12
|
+
Enable `gate.auth`, then `.plug(phoneNumber())`. Reject non-E.164 numbers. SMS Channel delivery is
|
|
13
|
+
not wired yet — use `exposeDevOtp` locally. Prefer stronger methods when phone is the only factor.
|
|
14
|
+
</Callout>
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
<Steps>
|
|
19
|
+
|
|
20
|
+
<Step>
|
|
21
|
+
### Plug it
|
|
22
|
+
|
|
23
|
+
```typescript title="src/app.ts"
|
|
24
|
+
import { oke } from "okengine";
|
|
25
|
+
import { phoneNumber } from "okengine/plugins";
|
|
26
|
+
|
|
27
|
+
export const app = oke({
|
|
28
|
+
name: "shop",
|
|
29
|
+
env: "local",
|
|
30
|
+
gate: { auth: {} },
|
|
31
|
+
}).plug(phoneNumber({ exposeDevOtp: true }));
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
</Step>
|
|
35
|
+
|
|
36
|
+
<Step>
|
|
37
|
+
### Request a code
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const { data, error } = await api.auth.requestPhoneOtp({ phone: "+15551234567" });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`POST /auth/phone/request`. Invalid E.164 → `AuthFailed` / `invalid_phone`.
|
|
44
|
+
|
|
45
|
+
</Step>
|
|
46
|
+
|
|
47
|
+
<Step>
|
|
48
|
+
### Verify
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const { data } = await api.auth.verifyPhoneOtp({
|
|
52
|
+
phone: "+15551234567",
|
|
53
|
+
otp: data!.devOtp!,
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`POST /auth/phone/verify` — first success creates a phone → `userId` mapping.
|
|
58
|
+
|
|
59
|
+
</Step>
|
|
60
|
+
|
|
61
|
+
</Steps>
|
|
62
|
+
|
|
63
|
+
## Options
|
|
64
|
+
|
|
65
|
+
| Option | Type | Default | Meaning |
|
|
66
|
+
| --------------- | ------------------- | -------- | --------------------------------------- |
|
|
67
|
+
| `secret` | `string` | active\* | HMAC secret (\*from `gate.auth`) |
|
|
68
|
+
| `sessions` | `SessionStore` | active\* | Session store |
|
|
69
|
+
| `ttlMs` | `number` | 10m | Challenge lifetime |
|
|
70
|
+
| `exposeDevOtp` | `boolean` | `false` | Include raw OTP in the request response |
|
|
71
|
+
| `phones` | `PhoneStore` | new | Phone → userId map |
|
|
72
|
+
| `verifications` | `VerificationStore` | new | Challenge store |
|
|
73
|
+
|
|
74
|
+
## Surfaces
|
|
75
|
+
|
|
76
|
+
| Flow | Path | Gate |
|
|
77
|
+
| ---------------------- | -------------------------- | ------------------------ |
|
|
78
|
+
| `auth.requestPhoneOtp` | `POST /auth/phone/request` | `gate.public` + otp rate |
|
|
79
|
+
| `auth.verifyPhoneOtp` | `POST /auth/phone/verify` | `gate.public` + otp rate |
|
|
80
|
+
|
|
81
|
+
E.164 pattern: `^\+[1-9]\d{7,14}$`.
|
|
82
|
+
|
|
83
|
+
## Troubleshooting
|
|
84
|
+
|
|
85
|
+
<Accordions>
|
|
86
|
+
<Accordion title="invalid_phone">
|
|
87
|
+
|
|
88
|
+
Number must start with `+` and a non-zero country digit, then 7–14 more digits. No spaces or
|
|
89
|
+
dashes.
|
|
90
|
+
|
|
91
|
+
</Accordion>
|
|
92
|
+
<Accordion title="No SMS arrived">
|
|
93
|
+
|
|
94
|
+
v1 does not send SMS. Use `exposeDevOtp` in local/test, or deliver via your own Channel Flow.
|
|
95
|
+
|
|
96
|
+
</Accordion>
|
|
97
|
+
</Accordions>
|
|
98
|
+
|
|
99
|
+
## Learn more
|
|
100
|
+
|
|
101
|
+
- [Email OTP](/docs/plugins/email-otp) — email variant of the same OTP rules
|
|
102
|
+
- [Gate](/docs/elements/gate) — `gate.auth`
|
|
103
|
+
- [Channel](/docs/elements/channel) — when you wire SMS yourself
|
|
104
|
+
|
|
105
|
+
## Next
|
|
106
|
+
|
|
107
|
+
<Cards>
|
|
108
|
+
<Card title="Email OTP" description="Six-digit email codes." href="/docs/plugins/email-otp" />
|
|
109
|
+
<Card title="Two-factor" description="TOTP step-up." href="/docs/plugins/two-factor" />
|
|
110
|
+
<Card title="Gate" description="Builtin auth and policies." href="/docs/elements/gate" />
|
|
111
|
+
</Cards>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Two-factor"
|
|
3
|
+
description: "Official plugin — TOTP enable, verify, and disable for an authenticated session."
|
|
4
|
+
icon: "LockKeyhole"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`twoFactor()` adds RFC 6238 TOTP (HMAC-SHA1, 6 digits, 30s) after someone already has a session.
|
|
9
|
+
Enable returns a secret, `otpauth://` URL, and recovery codes; verify mints a new session.
|
|
10
|
+
TOTP codes are compared in constant time (±1 step window).
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Enable `gate.auth`, then `.plug(twoFactor())`. Enable and disable need a Bearer session —
|
|
14
|
+
verify is public and takes `{ userId, code }` after password (or other) sign-in.
|
|
15
|
+
</Callout>
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
<Steps>
|
|
20
|
+
|
|
21
|
+
<Step>
|
|
22
|
+
### Plug it
|
|
23
|
+
|
|
24
|
+
```typescript title="src/app.ts"
|
|
25
|
+
import { oke } from "okengine";
|
|
26
|
+
import { twoFactor } from "okengine/plugins";
|
|
27
|
+
|
|
28
|
+
export const app = oke({
|
|
29
|
+
name: "shop",
|
|
30
|
+
env: "local",
|
|
31
|
+
gate: { auth: {} },
|
|
32
|
+
}).plug(twoFactor());
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
</Step>
|
|
36
|
+
|
|
37
|
+
<Step>
|
|
38
|
+
### Enable (session required)
|
|
39
|
+
|
|
40
|
+
Wire Bearer on `createClient` (`auth.getToken` / `memorySession`) — calls take input only.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
const { data } = await api.auth.twoFactorEnable({});
|
|
44
|
+
// data.secret, data.otpauthUrl, data.recoveryCodes
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`POST /auth/two-factor/enable` — writes `oke_two_factor`. Store recovery codes once; they are
|
|
48
|
+
shown only here.
|
|
49
|
+
|
|
50
|
+
</Step>
|
|
51
|
+
|
|
52
|
+
<Step>
|
|
53
|
+
### Verify a code
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
const { data } = await api.auth.twoFactorVerify({ userId, code: "123456" });
|
|
57
|
+
// hybrid session tokens — recovery codes also accepted once each
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`POST /auth/two-factor/verify`. Missing / disabled factor or bad code → `AuthFailed` /
|
|
61
|
+
`invalid_credentials`. Disable: `api.auth.twoFactorDisable` (Bearer via `auth.getToken`).
|
|
62
|
+
|
|
63
|
+
</Step>
|
|
64
|
+
|
|
65
|
+
</Steps>
|
|
66
|
+
|
|
67
|
+
## Options
|
|
68
|
+
|
|
69
|
+
| Option | Type | Default | Meaning |
|
|
70
|
+
| ---------- | ---------------- | ---------- | -------------------------------- |
|
|
71
|
+
| `secret` | `string` | active\* | HMAC secret (\*from `gate.auth`) |
|
|
72
|
+
| `sessions` | `SessionStore` | active\* | Session store |
|
|
73
|
+
| `now` | `() => number` | `Date.now` | Injectable clock |
|
|
74
|
+
| `factors` | `TwoFactorStore` | new | Per-user TOTP + recovery hashes |
|
|
75
|
+
| `issuer` | `string` | `"oke"` | Label in `otpauth://` URLs |
|
|
76
|
+
|
|
77
|
+
## Surfaces
|
|
78
|
+
|
|
79
|
+
| Flow | Path | Gate |
|
|
80
|
+
| ----------------------- | ------------------------------- | ------------------------ |
|
|
81
|
+
| `auth.twoFactorEnable` | `POST /auth/two-factor/enable` | session + bearer |
|
|
82
|
+
| `auth.twoFactorVerify` | `POST /auth/two-factor/verify` | `gate.public` + otp rate |
|
|
83
|
+
| `auth.twoFactorDisable` | `POST /auth/two-factor/disable` | session + bearer |
|
|
84
|
+
|
|
85
|
+
**Consequence:** verify issues a full session for `userId` — use it as the step after first-factor
|
|
86
|
+
sign-in when the account has 2FA enabled.
|
|
87
|
+
|
|
88
|
+
## Troubleshooting
|
|
89
|
+
|
|
90
|
+
<Accordions>
|
|
91
|
+
<Accordion title="twoFactorEnable returns AuthFailed unauthenticated">
|
|
92
|
+
|
|
93
|
+
Wire a session into `createClient` (`auth.getToken` / `memorySession`) from email/password
|
|
94
|
+
(or another method) first — enable is gated on Bearer.
|
|
95
|
+
|
|
96
|
+
</Accordion>
|
|
97
|
+
<Accordion title="verify always fails">
|
|
98
|
+
|
|
99
|
+
Code must be six digits. Check clock skew (±1 window), and that you enrolled the `secret` from
|
|
100
|
+
enable. A recovery code works once, then is consumed.
|
|
101
|
+
|
|
102
|
+
</Accordion>
|
|
103
|
+
</Accordions>
|
|
104
|
+
|
|
105
|
+
## Learn more
|
|
106
|
+
|
|
107
|
+
- [Passkey](/docs/plugins/passkey) — WebAuthn register / authenticate
|
|
108
|
+
- [Gate](/docs/elements/gate) — session + policies
|
|
109
|
+
- [Username](/docs/plugins/username) — first factor to enroll against
|
|
110
|
+
|
|
111
|
+
## Next
|
|
112
|
+
|
|
113
|
+
<Cards>
|
|
114
|
+
<Card title="Passkey" description="WebAuthn register and assert." href="/docs/plugins/passkey" />
|
|
115
|
+
<Card title="Gate" description="Builtin auth and policies." href="/docs/elements/gate" />
|
|
116
|
+
<Card title="Email OTP" description="Passwordless email codes." href="/docs/plugins/email-otp" />
|
|
117
|
+
</Cards>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Username"
|
|
3
|
+
description: "Official plugin — username + password sign-up and sign-in under /auth, plugged onto gate.auth."
|
|
4
|
+
icon: "UserRound"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`username()` lets people register and sign in with a username instead of email. It adds two public
|
|
9
|
+
Flows under `/auth` and contributes the `oke_usernames` table.
|
|
10
|
+
|
|
11
|
+
<Callout title="The one rule">
|
|
12
|
+
Enable `gate.auth` first, then `.plug(username())`. The plugin `.needs("auth")` and joins the HTTP
|
|
13
|
+
router via Bindings — not registry metadata alone.
|
|
14
|
+
</Callout>
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
<Steps>
|
|
19
|
+
|
|
20
|
+
<Step>
|
|
21
|
+
### Plug it
|
|
22
|
+
|
|
23
|
+
```typescript title="src/app.ts"
|
|
24
|
+
import { oke } from "okengine";
|
|
25
|
+
import { username } from "okengine/plugins";
|
|
26
|
+
|
|
27
|
+
export const app = oke({
|
|
28
|
+
name: "shop",
|
|
29
|
+
env: "local",
|
|
30
|
+
gate: { auth: {} },
|
|
31
|
+
}).plug(username());
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
</Step>
|
|
35
|
+
|
|
36
|
+
<Step>
|
|
37
|
+
### Sign up
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const { data, error } = await api.auth.signUpUsername({
|
|
41
|
+
username: "ali",
|
|
42
|
+
password: "CorrectHorse1",
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`POST /auth/sign-up/username` — usernames are normalized to lowercase; allowed pattern
|
|
47
|
+
`[a-z0-9._-]{3,64}`.
|
|
48
|
+
|
|
49
|
+
</Step>
|
|
50
|
+
|
|
51
|
+
<Step>
|
|
52
|
+
### Sign in
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
const { data } = await api.auth.signInUsername({
|
|
56
|
+
username: "ali",
|
|
57
|
+
password: "CorrectHorse1",
|
|
58
|
+
});
|
|
59
|
+
// data: accessToken, refreshToken, accessExpiresAt, userId
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Unknown username and bad password both return `AuthFailed` with
|
|
63
|
+
`reason: "invalid_credentials"` (enumeration-safe).
|
|
64
|
+
|
|
65
|
+
</Step>
|
|
66
|
+
|
|
67
|
+
</Steps>
|
|
68
|
+
|
|
69
|
+
## Options
|
|
70
|
+
|
|
71
|
+
| Option | Type | Default | Meaning |
|
|
72
|
+
| ----------- | --------------- | ---------- | ----------------------------------------------------------- |
|
|
73
|
+
| `secret` | `string` | active\* | HMAC secret (\*from `gate.auth` when plugged after `oke()`) |
|
|
74
|
+
| `sessions` | `SessionStore` | active\* | Session store shared with Gate auth |
|
|
75
|
+
| `now` | `() => number` | `Date.now` | Injectable clock |
|
|
76
|
+
| `usernames` | `UsernameStore` | new map | Shared in-memory credential store |
|
|
77
|
+
|
|
78
|
+
## Surfaces
|
|
79
|
+
|
|
80
|
+
| Flow | Path | Gate |
|
|
81
|
+
| --------------------- | ----------------------------- | ---------------------------- |
|
|
82
|
+
| `auth.signUpUsername` | `POST /auth/sign-up/username` | `gate.public` + sign-up rate |
|
|
83
|
+
| `auth.signInUsername` | `POST /auth/sign-in/username` | `gate.public` + sign-in rate |
|
|
84
|
+
|
|
85
|
+
## Troubleshooting
|
|
86
|
+
|
|
87
|
+
<Accordions>
|
|
88
|
+
<Accordion title="plugin boot failed — needs "auth"">
|
|
89
|
+
|
|
90
|
+
Set `oke({ gate: { auth: { … } } })` before `.plug(username())`.
|
|
91
|
+
|
|
92
|
+
</Accordion>
|
|
93
|
+
<Accordion title="AuthFailed invalid_credentials on sign-up">
|
|
94
|
+
|
|
95
|
+
Username taken, or it fails the `[a-z0-9._-]{3,64}` pattern after lowercasing. Same error shape
|
|
96
|
+
on purpose — do not treat it as "exists" in the UI.
|
|
97
|
+
|
|
98
|
+
</Accordion>
|
|
99
|
+
</Accordions>
|
|
100
|
+
|
|
101
|
+
## Learn more
|
|
102
|
+
|
|
103
|
+
- [Gate](/docs/elements/gate) — `gate.auth` and posture
|
|
104
|
+
- [Plugins](/docs/plugins) — all auth method plugins
|
|
105
|
+
- [Client](/docs/reference/client) — `createClient` + `memorySession`
|
|
106
|
+
|
|
107
|
+
## Next
|
|
108
|
+
|
|
109
|
+
<Cards>
|
|
110
|
+
<Card
|
|
111
|
+
title="Anonymous"
|
|
112
|
+
description="Session without a password."
|
|
113
|
+
href="/docs/plugins/anonymous"
|
|
114
|
+
/>
|
|
115
|
+
<Card title="Gate" description="Builtin auth and policies." href="/docs/elements/gate" />
|
|
116
|
+
<Card title="Magic link" description="Email link sign-in." href="/docs/plugins/magic-link" />
|
|
117
|
+
</Cards>
|