okengine 0.17.2 → 0.18.3
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 +9 -5
- package/manifest.v1.schema.json +61 -2
- package/package.json +18 -4
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/store.mdx +287 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/installation.mdx +1 -2
- package/site/content/docs/get-started/introduction.mdx +54 -110
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +94 -70
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/plugins/apple.mdx +151 -0
- package/site/content/docs/plugins/discord.mdx +139 -0
- package/site/content/docs/plugins/facebook.mdx +134 -0
- package/site/content/docs/plugins/figma.mdx +138 -0
- package/site/content/docs/plugins/github.mdx +138 -0
- package/site/content/docs/plugins/google.mdx +153 -0
- package/site/content/docs/plugins/index.mdx +47 -1
- package/site/content/docs/plugins/meta.json +10 -0
- package/site/content/docs/plugins/microsoft.mdx +151 -0
- package/site/content/docs/plugins/oauth.mdx +188 -0
- package/site/content/docs/plugins/x.mdx +125 -0
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/client.mdx +58 -1
- package/site/content/docs/reference/configuration.mdx +2 -4
- package/site/content/docs/reference/fx.mdx +3 -1
- package/site/content/docs/reference/index.mdx +0 -5
- package/site/content/docs/reference/meta.json +2 -2
- package/site/content/docs/reference/okid.mdx +137 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/config.ts +9 -0
- package/src/auth/identity-sql.ts +314 -0
- package/src/auth/identity.ts +140 -2
- package/src/auth/index.ts +17 -1
- package/src/auth/method-context.ts +3 -0
- package/src/auth/oauth-as/cimd.ts +132 -0
- package/src/auth/oauth-as/crypto.test.ts +101 -0
- package/src/auth/oauth-as/crypto.ts +393 -0
- package/src/auth/oauth-as/errors.ts +68 -0
- package/src/auth/oauth-as/http.test.ts +419 -0
- package/src/auth/oauth-as/http.ts +842 -0
- package/src/auth/oauth-as/stores.ts +61 -0
- package/src/auth/oauth-as/tables.ts +142 -0
- package/src/auth/tables.ts +0 -11
- package/src/bench/README.md +83 -0
- package/src/bench/REPORT.md +176 -0
- package/src/bench/g01-rls-stamp.bench.ts +194 -0
- package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
- package/src/bench/g03-signal-once.bench.ts +157 -0
- package/src/bench/g03-signal-reconnect.bench.ts +254 -0
- package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
- package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
- package/src/bench/g05-sustained-full.bench.ts +265 -0
- package/src/bench/g06-mixed-load.bench.ts +260 -0
- package/src/bench/g07-vault-crypto.bench.ts +100 -0
- package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
- package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
- package/src/bench/g08-store-kv-durable.bench.ts +133 -0
- package/src/bench/g08-store-sql.bench.ts +178 -0
- package/src/bench/g09-journal-sustained.bench.ts +203 -0
- package/src/bench/g10-observability-contention.bench.ts +246 -0
- package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
- package/src/bench/g13-elements.bench.ts +427 -0
- package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
- package/src/bench/g15-postgres-degradation.bench.ts +264 -0
- package/src/bench/g16-live-query-fanout.bench.ts +206 -0
- package/src/bench/lib/event-loop-lag.ts +26 -0
- package/src/bench/lib/infra.ts +60 -0
- package/src/bench/lib/report.ts +52 -0
- package/src/bench/lib/rss-sampler.ts +61 -0
- package/src/bench/lib/signal-pg.ts +88 -0
- package/src/bench/load-app.ts +337 -0
- package/src/bench/load-child.ts +108 -0
- package/src/bench/smoke.bench.ts +43 -0
- package/src/cli/competitor-mention-removal.test.ts +28 -0
- package/src/cli/doctor-fd.ts +117 -0
- package/src/cli/doctor.test.ts +192 -0
- package/src/cli/doctor.ts +129 -1
- package/src/client/create.ts +95 -1
- package/src/client/index.ts +9 -2
- package/src/client/transport.ts +11 -4
- package/src/client/use-live-query.ts +154 -0
- package/src/client-react/index.ts +15 -1
- package/src/client-react/live-resource.ts +246 -0
- package/src/client-react/use-live-query.test.ts +475 -0
- package/src/client-react/use-live-query.ts +530 -0
- package/src/compiler/extract.test.ts +518 -0
- package/src/compiler/extract.ts +386 -19
- package/src/console/server/invoke-user-flow.ts +2 -1
- package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-DY4N6nnk.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-CsPDMrVM.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-CcTDXHuz.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-CKR595wP.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
- package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-CpPFFKyE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-BsMf-9_W.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
- package/src/drivers/cdc-outbox.ts +389 -0
- package/src/drivers/memory.ts +20 -0
- package/src/drivers/oauth-apple.ts +156 -0
- package/src/drivers/oauth-discord.ts +79 -0
- package/src/drivers/oauth-facebook.ts +80 -0
- package/src/drivers/oauth-figma.ts +116 -0
- package/src/drivers/oauth-github.ts +92 -0
- package/src/drivers/oauth-google.ts +142 -0
- package/src/drivers/oauth-microsoft.ts +174 -0
- package/src/drivers/oauth-oidc.ts +293 -0
- package/src/drivers/oauth-shared.ts +326 -0
- package/src/drivers/oauth-types.ts +159 -0
- package/src/drivers/oauth-x.ts +77 -0
- package/src/drivers/oauth2-common.ts +95 -0
- package/src/drivers/oauth2-token.ts +61 -0
- package/src/drivers/pg-rls-row-passes.ts +251 -0
- package/src/drivers/pg-rls.ts +2 -0
- package/src/drivers/postgres.ts +45 -2
- package/src/drivers/signal-postgres.ts +2 -1
- package/src/elements/channel/runtime.ts +29 -2
- package/src/elements/channel.test.ts +52 -0
- package/src/elements/gate/boot.ts +29 -2
- package/src/elements/store/emit-drizzle.ts +147 -14
- package/src/elements/store/field-ddl.test.ts +118 -0
- package/src/elements/store/field-types.test.ts +455 -0
- package/src/elements/store/list-query.golden.json +777 -0
- package/src/elements/store/list-query.parity.test.ts +396 -0
- package/src/elements/store/list-query.ts +792 -0
- package/src/elements/store/live-default.test.ts +136 -0
- package/src/elements/store/live-http.test.ts +160 -0
- package/src/elements/store/live-isolation.test.ts +291 -0
- package/src/elements/store/live-query-runtime.test.ts +323 -0
- package/src/elements/store/live-query-runtime.ts +403 -0
- package/src/elements/store/live-query-server.test.ts +377 -0
- package/src/elements/store/live-query-server.ts +102 -0
- package/src/elements/store/live-query.ts +97 -0
- package/src/elements/store/resource.ts +189 -680
- package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
- package/src/elements/store/schema-decl.ts +539 -41
- package/src/elements/store/sql-rls-stamp.test.ts +27 -0
- package/src/elements/store/sql-session.ts +297 -35
- package/src/elements/store/table.ts +102 -21
- package/src/elements/store.test.ts +3 -1
- package/src/elements/store.ts +12 -1
- package/src/elements/vault/chaos-child.ts +74 -1
- package/src/elements/vault/chaos.test.ts +4 -2
- package/src/elements/vault/storage.ts +4 -2
- package/src/index.ts +4 -1
- package/src/kernel/app-auth.ts +1 -0
- package/src/kernel/app.ts +116 -2
- package/src/kernel/auth-sharing.test.ts +196 -0
- package/src/kernel/boot.test.ts +3 -3
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +14 -2
- package/src/kernel/horizontal-child.ts +2 -1
- package/src/kernel/http-resource.ts +33 -7
- package/src/kernel/identity-host-persist.test.ts +119 -0
- package/src/kernel/instance-id.ts +4 -2
- package/src/kernel/journal.ts +2 -1
- package/src/kernel/mcp-tool.test.ts +95 -0
- package/src/kernel/on.ts +9 -0
- package/src/kernel/realtime-bind.ts +326 -0
- package/src/kernel/resource-live.ts +117 -0
- package/src/kernel/triggers.ts +86 -4
- package/src/manifest/diff.ts +37 -0
- package/src/manifest/types.ts +64 -2
- package/src/okid.bench.test.ts +64 -0
- package/src/okid.test.ts +338 -0
- package/src/okid.ts +245 -0
- package/src/plugins/anonymous.ts +19 -1
- package/src/plugins/auth/shared.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +10 -8
- package/src/plugins/mcp-oauth.ts +208 -0
- package/src/plugins/oauth/flow-store.ts +117 -0
- package/src/plugins/oauth/link.ts +69 -0
- package/src/plugins/oauth/shared.ts +108 -0
- package/src/plugins/oauth/token-vault.ts +100 -0
- package/src/plugins/oauth.security.test.ts +535 -0
- package/src/plugins/oauth.ts +532 -0
- package/src/plugins/otp.ts +48 -6
- package/src/plugins/passkey.ts +20 -1
- package/src/plugins/two-factor.ts +11 -0
- package/src/plugins/username.ts +40 -7
- package/src/release/build-lib.ts +7 -1
- package/src/release/measure.ts +1 -0
- package/src/release/official-plugins.ts +4 -1
- package/src/runs/collect.ts +2 -1
- package/src/runs/drivers/files.ts +2 -1
- package/src/test/create-test-app.ts +114 -5
- package/src/test/export-bundle.test.ts +33 -0
- package/src/test/live-signals.test.ts +83 -0
- package/src/test/tenant-isolation.test.ts +175 -0
- package/src/testing.ts +26 -0
- package/src/upgrade/codemods.ts +1 -1
- package/site/content/docs/reference/migrating-environments.mdx +0 -158
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "OAuth"
|
|
3
|
+
description: "Official plugin — social sign-in with eight providers under /auth, Authorization Code + PKCE only."
|
|
4
|
+
icon: "FingerprintPattern"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`oauth()` adds social login to Gate auth. Start at
|
|
9
|
+
`/auth/oauth/{provider}/start`, approve, then land on
|
|
10
|
+
`/auth/oauth/callback/{provider}` with a session — eight providers.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Enable `gate.auth`, then `.plug(oauth({ providers: { ... } }))`. Every
|
|
14
|
+
provider runs Authorization Code + PKCE with an exact registered redirect URI
|
|
15
|
+
— implicit and password grants do not exist here.
|
|
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 { oauth } from "okengine/plugins";
|
|
28
|
+
|
|
29
|
+
export const app = oke({
|
|
30
|
+
name: "shop",
|
|
31
|
+
env: "dev",
|
|
32
|
+
gate: { auth: {} },
|
|
33
|
+
}).plug(
|
|
34
|
+
oauth({
|
|
35
|
+
baseUrl: "https://app.example.com",
|
|
36
|
+
providers: {
|
|
37
|
+
google: { enabled: true },
|
|
38
|
+
github: { enabled: true },
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`baseUrl` synthesizes `{baseUrl}/auth/oauth/callback/{provider}` — register
|
|
45
|
+
that exact string. Client secrets resolve from Vault
|
|
46
|
+
(`OAUTH_GOOGLE_CLIENT_SECRET`, `OAUTH_GITHUB_CLIENT_SECRET`, …).
|
|
47
|
+
|
|
48
|
+
</Step>
|
|
49
|
+
|
|
50
|
+
<Step>
|
|
51
|
+
### Start the flow
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
const { data } = await api.auth.oauthStart({ provider: "google" });
|
|
55
|
+
// redirect the browser to data.authorizationUrl
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The response carries `authorizationUrl`, `expiresInMs`, and the provider echo.
|
|
59
|
+
The flow row (state hash, PKCE verifier, nonce) lives for ten minutes.
|
|
60
|
+
|
|
61
|
+
</Step>
|
|
62
|
+
|
|
63
|
+
<Step>
|
|
64
|
+
### Callback issues a session
|
|
65
|
+
|
|
66
|
+
The provider redirects to your callback route; the flow verifies state,
|
|
67
|
+
exchanges the code, and signs the person in:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const body = {
|
|
71
|
+
accessToken: "...",
|
|
72
|
+
refreshToken: "...",
|
|
73
|
+
userId: "usr_...",
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A brand-new visitor gets a fresh account. Someone whose email already belongs
|
|
78
|
+
to another credential is refused with `email_in_use`.
|
|
79
|
+
|
|
80
|
+
</Step>
|
|
81
|
+
|
|
82
|
+
</Steps>
|
|
83
|
+
|
|
84
|
+
## Providers
|
|
85
|
+
|
|
86
|
+
| Provider | Shape | Identity source | Email verified |
|
|
87
|
+
| --------- | ------ | ------------------------ | ------------------------------------ |
|
|
88
|
+
| Google | OIDC | JWKS-verified ID token | `email_verified` claim |
|
|
89
|
+
| Apple | OIDC | JWKS-verified ID token | strict parse — `"false"` stays false |
|
|
90
|
+
| Microsoft | OIDC | JWKS-verified ID token | `email_verified` claim |
|
|
91
|
+
| GitHub | OAuth2 | `/user` + `/user/emails` | primary `verified` flag |
|
|
92
|
+
| Discord | OAuth2 | `/users/@me` | `verified` flag |
|
|
93
|
+
| X | OAuth2 | `/2/users/me` | never — no trustworthy signal |
|
|
94
|
+
| Facebook | OAuth2 | Graph `/me` | never — no trustworthy signal |
|
|
95
|
+
| Figma | OAuth2 | `/v1/me` | never — no verification field |
|
|
96
|
+
|
|
97
|
+
Each provider has its own page under this category — endpoints, scopes, setup,
|
|
98
|
+
and trust rules.
|
|
99
|
+
|
|
100
|
+
## Security model
|
|
101
|
+
|
|
102
|
+
| Threat | Defense |
|
|
103
|
+
| ---------------------------------- | ----------------------------------------------------------------------- |
|
|
104
|
+
| Code interception | PKCE S256 on every provider |
|
|
105
|
+
| CSRF / forged callbacks | single-use `state`, SHA-256-hashed at rest |
|
|
106
|
+
| Mix-up across providers (RFC 9700) | per-provider callback routes + issuer pinning on every assertion |
|
|
107
|
+
| Unverified-email takeover | `linkOrProvision` refuses `email_in_use` without an authenticated owner |
|
|
108
|
+
| Redirect manipulation | byte-exact stored `redirect_uri` at token exchange |
|
|
109
|
+
|
|
110
|
+
**Consequence:** an attacker who completes a social login claiming your email
|
|
111
|
+
gains nothing — the account stays untouched unless they already own a session
|
|
112
|
+
for it.
|
|
113
|
+
|
|
114
|
+
## Options
|
|
115
|
+
|
|
116
|
+
| Option | Type | Default | Meaning |
|
|
117
|
+
| --------------- | ------------------- | -------- | ---------------------------------------- |
|
|
118
|
+
| `providers` | map | `{}` | Per-provider config, disabled by default |
|
|
119
|
+
| `baseUrl` | `string` | — | Origin for synthesized callback URIs |
|
|
120
|
+
| `secret` | `string` | active\* | HMAC secret (\*from `gate.auth`) |
|
|
121
|
+
| `sessions` | `SessionStore` | active\* | Session store |
|
|
122
|
+
| `identities` | `IdentityStore` | active\* | Shared user store |
|
|
123
|
+
| `verifications` | `VerificationStore` | new | Flow-state store |
|
|
124
|
+
| `fetch` | `typeof fetch` | global | Injectable transport (tests) |
|
|
125
|
+
|
|
126
|
+
Per-provider config: `enabled`, `clientId`, `redirectUri`, `scopes`,
|
|
127
|
+
`storeProviderTokens`, plus Microsoft `tenant` and Apple `teamId` / `keyId`.
|
|
128
|
+
|
|
129
|
+
## Surfaces
|
|
130
|
+
|
|
131
|
+
| Flow | Path | Gate |
|
|
132
|
+
| --------------------- | ------------------------------------------ | ------------------------ |
|
|
133
|
+
| `auth.oauthStart` | `POST /auth/oauth/{provider}/start` | `gate.public` + otp rate |
|
|
134
|
+
| `auth.oauthCallback` | `GET+POST /auth/oauth/callback/{provider}` | `gate.public` + otp rate |
|
|
135
|
+
| `auth.oauthLinkStart` | `POST /auth/oauth/{provider}/link` | session + bearer |
|
|
136
|
+
|
|
137
|
+
The GET + POST callback pair exists because Apple posts its response instead of
|
|
138
|
+
redirecting.
|
|
139
|
+
|
|
140
|
+
## Troubleshooting
|
|
141
|
+
|
|
142
|
+
<Accordions>
|
|
143
|
+
<Accordion title="Boot fails listing OAUTH_…_CLIENT_SECRET">
|
|
144
|
+
|
|
145
|
+
Every enabled provider needs its secret in Vault before boot. Seed
|
|
146
|
+
`OAUTH_{PROVIDER}_CLIENT_SECRET` (Apple uses `OAUTH_APPLE_PRIVATE_KEY`) through
|
|
147
|
+
your Vault driver or `.env.local`.
|
|
148
|
+
|
|
149
|
+
</Accordion>
|
|
150
|
+
<Accordion title="Callback returns invalid_state">
|
|
151
|
+
|
|
152
|
+
The state was consumed, expired (ten-minute TTL), or minted by a different
|
|
153
|
+
provider's start call. Restart from `/start`.
|
|
154
|
+
|
|
155
|
+
</Accordion>
|
|
156
|
+
<Accordion title="Callback returns issuer_mismatch">
|
|
157
|
+
|
|
158
|
+
The ID token's issuer does not match the provider that started the flow — the
|
|
159
|
+
signature was valid but the token came from elsewhere. This rejection is the
|
|
160
|
+
mix-up defense working; retry the real provider.
|
|
161
|
+
|
|
162
|
+
</Accordion>
|
|
163
|
+
<Accordion title="Callback returns email_in_use">
|
|
164
|
+
|
|
165
|
+
The provider returned an email already registered to another account without
|
|
166
|
+
proof you own that account. Sign in with the original method first, then link
|
|
167
|
+
via `POST /auth/oauth/{provider}/link`.
|
|
168
|
+
|
|
169
|
+
</Accordion>
|
|
170
|
+
</Accordions>
|
|
171
|
+
|
|
172
|
+
## Learn more
|
|
173
|
+
|
|
174
|
+
- [Gate](/docs/elements/gate) — `gate.auth`
|
|
175
|
+
- [Passkey](/docs/plugins/passkey) — phishing-resistant alternative
|
|
176
|
+
- [Client](/docs/reference/client) — calling `/auth` from the browser
|
|
177
|
+
|
|
178
|
+
## Next
|
|
179
|
+
|
|
180
|
+
<Cards>
|
|
181
|
+
<Card title="Google" description="OIDC reference provider." href="/docs/plugins/google" />
|
|
182
|
+
<Card
|
|
183
|
+
title="GitHub"
|
|
184
|
+
description="OAuth2 with verified-email lookup."
|
|
185
|
+
href="/docs/plugins/github"
|
|
186
|
+
/>
|
|
187
|
+
<Card title="Passkey" description="WebAuthn-shaped passkeys." href="/docs/plugins/passkey" />
|
|
188
|
+
</Cards>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "X"
|
|
3
|
+
description: "Official plugin — X (Twitter) sign-in as a PKCE public client with confidential-client secrets left out."
|
|
4
|
+
icon: "X"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
X is the one provider where `oauth()` behaves as a **public client**: the
|
|
9
|
+
token exchange carries the code verifier but no secret. PKCE is the only
|
|
10
|
+
proof the callback is yours.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Enable OAuth 2.0 in the X Developer Portal, set up your exact callback URI, and request
|
|
14
|
+
`users.email` access if you need addresses. No `OAUTH_X_CLIENT_SECRET` exists — none is read.
|
|
15
|
+
</Callout>
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
<Steps>
|
|
20
|
+
|
|
21
|
+
<Step>
|
|
22
|
+
### Configure the app
|
|
23
|
+
|
|
24
|
+
X Developer Portal → Project → User authentication settings → **Set up**.
|
|
25
|
+
Choose _Web App_, enable OAuth 2.0, and add
|
|
26
|
+
`https://app.example.com/auth/oauth/callback/x` as a callback URI.
|
|
27
|
+
|
|
28
|
+
</Step>
|
|
29
|
+
|
|
30
|
+
<Step>
|
|
31
|
+
### Plug it
|
|
32
|
+
|
|
33
|
+
```typescript title="src/app.ts"
|
|
34
|
+
import { oke } from "okengine";
|
|
35
|
+
import { oauth } from "okengine/plugins";
|
|
36
|
+
|
|
37
|
+
export const app = oke({
|
|
38
|
+
name: "shop",
|
|
39
|
+
env: "dev",
|
|
40
|
+
gate: { auth: {} },
|
|
41
|
+
}).plug(
|
|
42
|
+
oauth({
|
|
43
|
+
baseUrl: "https://app.example.com",
|
|
44
|
+
providers: {
|
|
45
|
+
x: { enabled: true },
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
</Step>
|
|
52
|
+
|
|
53
|
+
</Steps>
|
|
54
|
+
|
|
55
|
+
That is the whole setup — no Vault contract for this provider.
|
|
56
|
+
|
|
57
|
+
## How identity works
|
|
58
|
+
|
|
59
|
+
| Aspect | Behavior |
|
|
60
|
+
| -------------- | ------------------------------------------------------------------------------------------- |
|
|
61
|
+
| Token exchange | `POST https://api.x.com/2/oauth2/token` with the code verifier, no secret |
|
|
62
|
+
| Profile | `GET /2/users/me?user.fields=id,name,username,confirmed_email` |
|
|
63
|
+
| Subject | the string `id` inside `data` |
|
|
64
|
+
| Email trust | **always unverified** — `confirmed_email` gates API access, it does not attest verification |
|
|
65
|
+
|
|
66
|
+
**Consequence:** an X email never claims an existing account during sign-up.
|
|
67
|
+
It can attach to a fresh account or to an existing one you already control via
|
|
68
|
+
authenticated linking.
|
|
69
|
+
|
|
70
|
+
Default scopes: `users.read`, `tweet.read`.
|
|
71
|
+
|
|
72
|
+
## Options
|
|
73
|
+
|
|
74
|
+
| Option | Type | Default | Meaning |
|
|
75
|
+
| --------------------- | ---------- | --------------- | ---------------------------------- |
|
|
76
|
+
| `enabled` | `boolean` | `false` | Turn the provider on |
|
|
77
|
+
| `clientId` | `string` | Vault/env\* | `\*OAUTH_X_CLIENT_ID` |
|
|
78
|
+
| `redirectUri` | `string` | `{baseUrl}…/x` | Exact registered URI |
|
|
79
|
+
| `scopes` | `string[]` | driver defaults | Extra scopes (`offline.access`, …) |
|
|
80
|
+
| `storeProviderTokens` | `boolean` | `false` | Keep tokens in Vault |
|
|
81
|
+
|
|
82
|
+
## Surfaces
|
|
83
|
+
|
|
84
|
+
| Flow | Path |
|
|
85
|
+
| -------- | --------------------------------- |
|
|
86
|
+
| Start | `POST /auth/oauth/x/start` |
|
|
87
|
+
| Callback | `GET+POST /auth/oauth/callback/x` |
|
|
88
|
+
| Link | `POST /auth/oauth/x/link` |
|
|
89
|
+
|
|
90
|
+
## Troubleshooting
|
|
91
|
+
|
|
92
|
+
<Accordions>
|
|
93
|
+
<Accordion title="invalid_request on token exchange">
|
|
94
|
+
|
|
95
|
+
Callback URIs must match exactly and the code must be fresh. X codes are
|
|
96
|
+
single-use and short-lived; flow rows expire after ten minutes too.
|
|
97
|
+
|
|
98
|
+
</Accordion>
|
|
99
|
+
<Accordion title="No email ever arrives">
|
|
100
|
+
|
|
101
|
+
`confirmed_email` requires elevated access plus the `users.email` scope. Even
|
|
102
|
+
then the address stays unverified by design — see the trust table above.
|
|
103
|
+
|
|
104
|
+
</Accordion>
|
|
105
|
+
<Accordion title="403 forbidden">
|
|
106
|
+
|
|
107
|
+
Your app tier lacks user-context authentication, or the requested scope set
|
|
108
|
+
exceeds what the portal grants.
|
|
109
|
+
|
|
110
|
+
</Accordion>
|
|
111
|
+
</Accordions>
|
|
112
|
+
|
|
113
|
+
## Learn more
|
|
114
|
+
|
|
115
|
+
- [OAuth](/docs/plugins/oauth) — shared flows and security model
|
|
116
|
+
- [Facebook](/docs/plugins/facebook) — also never-verified emails
|
|
117
|
+
- [Vault](/docs/elements/vault) — optional token storage
|
|
118
|
+
|
|
119
|
+
## Next
|
|
120
|
+
|
|
121
|
+
<Cards>
|
|
122
|
+
<Card title="Facebook" description="Never-verified emails." href="/docs/plugins/facebook" />
|
|
123
|
+
<Card title="Figma" description="Basic-auth token endpoint." href="/docs/plugins/figma" />
|
|
124
|
+
<Card title="Discord" description="Nullable emails." href="/docs/plugins/discord" />
|
|
125
|
+
</Cards>
|
|
@@ -27,7 +27,8 @@ oke test # bun test with PGLite posture (NODE_ENV=test,
|
|
|
27
27
|
oke mode # removed — prints error; use oke dev (Compose) / oke test (PGLite)
|
|
28
28
|
|
|
29
29
|
oke start # runs exactly what production runs (this is the Docker CMD)
|
|
30
|
-
oke doctor # verify secrets, ports, drivers, tenancy, schema drift
|
|
30
|
+
oke doctor # verify secrets, ports, drivers, tenancy, schema drift, FD headroom,
|
|
31
|
+
# CDC outbox backlog/retention, live fan-out pressure
|
|
31
32
|
oke console claim-code # print setup claim code from .oke/claim-code (after Console boot)
|
|
32
33
|
# → `bun run dev:console` boots kernel + Vite with a fixed
|
|
33
34
|
# operator (`dev@oke.dev` / `Okengine123!`) — Sign in (prefilled)
|
|
@@ -141,7 +142,7 @@ the hero (not once per process).
|
|
|
141
142
|
that `@electric-sql/pglite` resolves, then forwards argv to `bun test`.
|
|
142
143
|
|
|
143
144
|
`oke mode` and `oke dev --local` / `-l` are removed — they exit with an error.
|
|
144
|
-
|
|
145
|
+
Rename the env keys in `oke.config.ts` to `dev` / `test` / `prod`.
|
|
145
146
|
|
|
146
147
|
### create-oke
|
|
147
148
|
|
|
@@ -172,7 +172,8 @@ const { data } = await api.auth.signInEmail({ email, password });
|
|
|
172
172
|
if (data) session.set(data);
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
React: `useSession(api, session)
|
|
175
|
+
React: `useSession(api, session)`, `useLive(api, signal, input?)`, and
|
|
176
|
+
`useLiveQuery({ api, listFlow, live, query? })` from `okengine/client-react`.
|
|
176
177
|
|
|
177
178
|
| Step | What happens |
|
|
178
179
|
| ---------------------- | ---------------------------------------------------------------- |
|
|
@@ -205,6 +206,7 @@ A 401 with no `{ data, error }` body becomes `TransportError` with `data.status:
|
|
|
205
206
|
| `AUTH_ERROR_CODES` | `okengine/client/auth` | Common auth Flow / gate codes |
|
|
206
207
|
| `useSession(api, session?)` | `okengine/client-react` | React status + `auth.me` |
|
|
207
208
|
| `useLive(api, signal, …)` | `okengine/client-react` | React `events` / `latest` / `isConnected` |
|
|
209
|
+
| `useLiveQuery({ … })` | `okengine/client-react` | Live list + optimistic `mutate` over a resource |
|
|
208
210
|
|
|
209
211
|
Core `okengine/client` stays under the size budget — helpers are separate exports. Not in
|
|
210
212
|
core today: cookie jars or plugin `.client()` decorations. Browser apps: also see
|
|
@@ -305,6 +307,61 @@ const { events, latest, error, isConnected } = useLive(
|
|
|
305
307
|
);
|
|
306
308
|
```
|
|
307
309
|
|
|
310
|
+
### Live queries (`store.resource({ live: true })`)
|
|
311
|
+
|
|
312
|
+
When a resource opts into `live: true`, the compiler mounts `GET <path>/live` next to the CRUD
|
|
313
|
+
verbs. That route streams **classified** row events — not a shared tape — so each subscriber only
|
|
314
|
+
sees rows that still pass their RLS stamp + list filters:
|
|
315
|
+
|
|
316
|
+
| `kind` | Meaning |
|
|
317
|
+
| --------- | -------------------------------------------------------------- |
|
|
318
|
+
| `upsert` | Row visible under stamp + query — merge/replace by primary key |
|
|
319
|
+
| `revoked` | Row left visibility (`reason: "rls"` \| `"query"`) — remove |
|
|
320
|
+
| `delete` | Row deleted in CDC — remove |
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
import { useLiveQuery } from "okengine/client-react";
|
|
324
|
+
|
|
325
|
+
const { data, error, isLoading, isConnected, isReconnecting, refetch, mutate } = useLiveQuery({
|
|
326
|
+
api,
|
|
327
|
+
listFlow: api.tasks.list,
|
|
328
|
+
query: { status: "open" },
|
|
329
|
+
live: { method: "GET", path: "/tasks/live" }, // from app.$routes
|
|
330
|
+
options: {
|
|
331
|
+
enabled: session.status === "authenticated", // default true — idle when false
|
|
332
|
+
refreshKey: tenantId, // identity change → full re-subscribe
|
|
333
|
+
onAuthRefresh: onAuthRefreshed, // auth.refresh() → new snapshot + replay
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await mutate(
|
|
338
|
+
api.tasks.update,
|
|
339
|
+
{ id, status: "done" },
|
|
340
|
+
{
|
|
341
|
+
optimistic: (rows) => rows.map((r) => (r.id === id ? { ...r, status: "done" } : r)),
|
|
342
|
+
pkOf: (input) => input.id,
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Every `mutate` call generates a client UUID sent as the `X-Oke-Mutation-Id`
|
|
348
|
+
header — the server echoes it onto that write's CDC events, so:
|
|
349
|
+
|
|
350
|
+
- Your own late SSE echoes never double-apply (pending-set dedupe).
|
|
351
|
+
- Reconnects replay-guard by event `seq` (`isReplayedEvent`).
|
|
352
|
+
- Manual `refetch()` re-runs only the HTTP list read; reconnects always do a
|
|
353
|
+
full subscribe-protocol cycle (new snapshot + replay).
|
|
354
|
+
|
|
355
|
+
| State | Meaning |
|
|
356
|
+
| ---------------- | ------------------------------------------------------------------- |
|
|
357
|
+
| `isLoading` | Waiting for the first snapshot — no data yet |
|
|
358
|
+
| `isConnected` | SSE stream is open |
|
|
359
|
+
| `isReconnecting` | Stream dropped after a successful load; reconnect backoff in flight |
|
|
360
|
+
|
|
361
|
+
**Consequence:** optimistic patches roll back automatically when the Flow returns
|
|
362
|
+
`error !== null`. Server CDC / the successful response clear the override so the next upsert is
|
|
363
|
+
authoritative.
|
|
364
|
+
|
|
308
365
|
## Exports
|
|
309
366
|
|
|
310
367
|
| Export | Kind | Role |
|
|
@@ -28,8 +28,7 @@ export default defineConfig({
|
|
|
28
28
|
| `prod` | Production deploy | Shared durable backends |
|
|
29
29
|
|
|
30
30
|
Missing `dev` pins fill from `prod` (`fillDevFromProd`). Only `dev` / `test` / `prod` keys are
|
|
31
|
-
valid —
|
|
32
|
-
`local` / `docker` maps.
|
|
31
|
+
valid — rename `local` → `dev` and `docker` → `prod` if you still have the old maps.
|
|
33
32
|
|
|
34
33
|
## Quick start
|
|
35
34
|
|
|
@@ -246,7 +245,7 @@ Domain schema sync for `oke db push | generate | migrate` (Drizzle). Unrelated t
|
|
|
246
245
|
|
|
247
246
|
<Accordion title='oke.config: … uses removed driver "sqlite"'>
|
|
248
247
|
The `sqlite` driver is gone. Pin `postgres` for `dev`/`prod` and `pglite` for `test`. Edit the
|
|
249
|
-
file by hand —
|
|
248
|
+
file by hand — rename the env keys to `dev` / `test` / `prod` first.
|
|
250
249
|
</Accordion>
|
|
251
250
|
|
|
252
251
|
<Accordion title='drivers.store.sql.test must be "pglite"'>
|
|
@@ -268,7 +267,6 @@ Domain schema sync for `oke db push | generate | migrate` (Drizzle). Unrelated t
|
|
|
268
267
|
|
|
269
268
|
## Learn more
|
|
270
269
|
|
|
271
|
-
- [Migrating environments](/docs/reference/migrating-environments) — four-key maps → `dev`/`test`/`prod`
|
|
272
270
|
- [CLI Reference](/docs/reference/cli) — `oke dev` · `oke test` · `oke db`
|
|
273
271
|
- [Environment Variables](/docs/reference/environment-variables) — URL and secret resolution
|
|
274
272
|
- [Store](/docs/elements/store) — what the store drivers back
|
|
@@ -230,7 +230,9 @@ Returning a plain value instead answers 200 with `{ data: value, error: null }`
|
|
|
230
230
|
| `fx.log.debug/info/warn/error(msg, data?)` | Redacting — secrets print as `***` |
|
|
231
231
|
| `fx.t(key, values?)` | ICU MessageFormat — active locale → `i18n.default` → key |
|
|
232
232
|
| `fx.locale` | Active locale (`Accept-Language` matched to `i18n.locales`) |
|
|
233
|
-
| `fx.id()` |
|
|
233
|
+
| `fx.id()` | OKID — 21-char native id from `okengine/okid` |
|
|
234
|
+
|
|
235
|
+
See [OKID](/docs/reference/okid) for generation options and the entropy model.
|
|
234
236
|
|
|
235
237
|
`fx.t` uses [ICU MessageFormat](https://unicode-org.github.io/icu/userguide/format_parse/messages/)
|
|
236
238
|
(FormatJS): `{name}`, plurals, `select` / `selectordinal`, rich-text tags.
|
|
@@ -15,11 +15,6 @@ Dense tables and command lists. Reach for these when you already know what you a
|
|
|
15
15
|
description="Every option in oke.config.ts."
|
|
16
16
|
href="/docs/reference/configuration"
|
|
17
17
|
/>
|
|
18
|
-
<Card
|
|
19
|
-
title="Migrating environments"
|
|
20
|
-
description="local/docker maps → dev/test/prod."
|
|
21
|
-
href="/docs/reference/migrating-environments"
|
|
22
|
-
/>
|
|
23
18
|
<Card title="fx" description="The complete fx surface and effects." href="/docs/reference/fx" />
|
|
24
19
|
<Card
|
|
25
20
|
title="i18n"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "OKID"
|
|
3
|
+
description: "OKE's native id generator — compact, URL-safe, cryptographically random ids from okengine/okid, with an opt-in time-sortable and alphabet-controlled variant."
|
|
4
|
+
icon: "Hash"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Using `okengine/okid` gives you an id for any primary key, request trace, or event that is short (`okid()` is 21 characters), URL-safe, and random from a cryptographic source. Turn to it when a plain UUID string is more than you need; your app already generates them wherever `defaultFn(id)` is used.
|
|
9
|
+
|
|
10
|
+
<Callout title="The one rule">
|
|
11
|
+
Use OKID for identity, never for secrets. An id is enumerable by design, so anything you hand to
|
|
12
|
+
an untrusted client must be a token from the Vault, not an OKID.
|
|
13
|
+
</Callout>
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
<Steps>
|
|
18
|
+
|
|
19
|
+
<Step>
|
|
20
|
+
### Install nothing — it is exported by the package
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { okid } from "okengine/okid";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
</Step>
|
|
27
|
+
|
|
28
|
+
<Step>
|
|
29
|
+
### Generate an id
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const userId = okid();
|
|
33
|
+
const requestId = okid(16);
|
|
34
|
+
const eventKey = okid({ sortable: true });
|
|
35
|
+
const inviteCode = okid({ lookAlikes: false, uppercase: false });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
</Step>
|
|
39
|
+
|
|
40
|
+
<Step>
|
|
41
|
+
### Store it anywhere a string fits
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// field.id() is shorthand for "default generation id" — currently OK ID.
|
|
45
|
+
field.id().primaryKey();
|
|
46
|
+
// Or pin OK ID explicitly:
|
|
47
|
+
field.okid().primaryKey();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The same 21-character id lands in your SQL primary keys, KV keys, and trace ids.
|
|
51
|
+
|
|
52
|
+
</Step>
|
|
53
|
+
|
|
54
|
+
</Steps>
|
|
55
|
+
|
|
56
|
+
## Reference
|
|
57
|
+
|
|
58
|
+
| Call | Result | Notes |
|
|
59
|
+
| -------------------------------------------------- | ----------------------------------------- | --------------------------------------- |
|
|
60
|
+
| `okid()` | 21-char URL-safe id, 126 bits of entropy | 64-char alphabet, `a-zA-Z0-9-_` |
|
|
61
|
+
| `okid(length)` | id of exactly `length` characters | integer between 8 and 128 |
|
|
62
|
+
| `okid({ length })` | options form, id of `length` | default 21 |
|
|
63
|
+
| `okid({ sortable })` | time-prefixed id, 8 prefix + `length − 8` | lexicographic order ≈ creation order |
|
|
64
|
+
| `okid({ numbers, lowercase, uppercase, symbols })` | charset control | each group defaults to on |
|
|
65
|
+
| `okid({ lookAlikes })` | confusable-char control | `lookAlikes: false` drops `1lI0Oouv5Ss` |
|
|
66
|
+
|
|
67
|
+
### Options
|
|
68
|
+
|
|
69
|
+
| Option | Type | Default | Meaning |
|
|
70
|
+
| ------------ | --------- | ------- | ----------------------------------------------------------- |
|
|
71
|
+
| `length` | `number` | `21` | total id length in characters, between 8 and 128 |
|
|
72
|
+
| `sortable` | `boolean` | `false` | prefix an 8-char epoch-ms timestamp so sorting follows time |
|
|
73
|
+
| `numbers` | `boolean` | `true` | include `0-9` |
|
|
74
|
+
| `lowercase` | `boolean` | `true` | include `a-z` |
|
|
75
|
+
| `uppercase` | `boolean` | `true` | include `A-Z` |
|
|
76
|
+
| `symbols` | `boolean` | `true` | include `-` and `_` |
|
|
77
|
+
| `lookAlikes` | `boolean` | `true` | include confusable chars `1lI0Oouv5Ss`; set `false` to drop |
|
|
78
|
+
|
|
79
|
+
### Exported constants
|
|
80
|
+
|
|
81
|
+
| Constant | Value | Meaning |
|
|
82
|
+
| -------------------------- | ----------------------------------------- | -------------------------------- |
|
|
83
|
+
| `OKID_ALPHABET` | `a-zA-Z0-9-_` | default, Base64URL order |
|
|
84
|
+
| `OKID_SORTABLE_ALPHABET` | alphabet sorted by code unit (same chars) | used by the sortable encoder |
|
|
85
|
+
| `OKID_LOOKALIKE_CHARS` | `1lI0Oouv5Ss` | dropped when `lookAlikes: false` |
|
|
86
|
+
| `OKID_DEFAULT_LENGTH` | `21` | default length |
|
|
87
|
+
| `OKID_MIN_LENGTH` | `8` | shortest non-sortable length |
|
|
88
|
+
| `OKID_MAX_LENGTH` | `128` | longest length |
|
|
89
|
+
| `OKID_SORTABLE_MIN_LENGTH` | `16` | shortest sortable length (8+8) |
|
|
90
|
+
|
|
91
|
+
## Collision resistance
|
|
92
|
+
|
|
93
|
+
Every character is drawn uniformly from the alphabet with `crypto.getRandomValues()`. Because it uses an unbiased character selection (never modulo), each character carries exactly `log2(alphabet)` bits of entropy. At the default 21 characters over 64 symbols, that is 126 bits — the birthday-bound collision probability across one billion ids is on the order of `10⁻²¹`. You do not need a UUID for collision resistance; this is where a UUID is stronger only because it is a different format, not a different amount of randomness.
|
|
94
|
+
|
|
95
|
+
**Consequence:** two ids minted at the same millisecond are still distinct — the timestamp prefix never replaces entropy, it prefixes it.
|
|
96
|
+
|
|
97
|
+
## Sortable ids
|
|
98
|
+
|
|
99
|
+
`sortable: true` prepends 48 bits of `Date.now()` encoded in exactly 8 characters, in an alphabet whose sort order matches time order. Sorting a batch of these ids reproduces the creation order across milliseconds.
|
|
100
|
+
|
|
101
|
+
**Consequence:** a sortable id embeds its creation time (millisecond precision), so keep them out of public, enumerable surfaces. Clock skew distorts order but can never produce a duplicate — the tail stays random.
|
|
102
|
+
|
|
103
|
+
## Alphabet control
|
|
104
|
+
|
|
105
|
+
Turning groups off shrinks the alphabet. With a non-power-of-two alphabet, OKID uses rejection sampling instead of modulo, so every remaining character stays equally likely — the output never becomes measurably biased.
|
|
106
|
+
|
|
107
|
+
**Consequence:** smaller alphabets mean fewer bits per character. `lookAlikes: false` alone drops the default entropy only slightly (126 → ~120 bits); dropping whole groups costs more. Choose the smallest alphabet that fits the human-transcription use case.
|
|
108
|
+
|
|
109
|
+
## Under the hood
|
|
110
|
+
|
|
111
|
+
The generator is a pure function: no counters, no process or machine fingerprint, no shared mutable state. It is safe to call concurrently from any number of workers, and every id uses only the bytes it needs — no hidden timestamp, no hidden prefix.
|
|
112
|
+
|
|
113
|
+
## Troubleshooting
|
|
114
|
+
|
|
115
|
+
<Accordions>
|
|
116
|
+
<Accordion title="I get an error for an empty alphabet">
|
|
117
|
+
|
|
118
|
+
Passing `numbers: false, lowercase: false, uppercase: false, symbols: false` at the same time throws a `RangeError` with the message `okid: alphabet is empty — enable at least one character group`. Re-enable at least one group, or don't use the option object and rely on the default alphabet.
|
|
119
|
+
|
|
120
|
+
</Accordion>
|
|
121
|
+
<Accordion title="I get a RangeError for a length">
|
|
122
|
+
|
|
123
|
+
`okid(0)`, `okid(-1)`, `okid(7)`, `okid(129)`, and non-integer lengths throw a `RangeError`. Sortable ids have a higher floor: passing a sortable length below 16 throws. Keep lengths between 8 and 128 (16–128 for sortable).
|
|
124
|
+
|
|
125
|
+
</Accordion>
|
|
126
|
+
<Accordion title="My ids are not sortable by the alphabet order I expected">
|
|
127
|
+
|
|
128
|
+
The default alphabet order is not lexicographic; `_` sorts between uppercase and lowercase. When `sortable` is on, ids use the code-point-ordered alphabet, so plain string comparison matches time order. Do not customize the alphabet in sortable mode — the option exists exactly because the default order is not trustworthy for ordering.
|
|
129
|
+
|
|
130
|
+
</Accordion>
|
|
131
|
+
</Accordions>
|
|
132
|
+
|
|
133
|
+
## References
|
|
134
|
+
|
|
135
|
+
- [Store](/docs/elements/store) — `defaultFn(id)` in table declarations delegates to `okid()`
|
|
136
|
+
- [fx](/docs/reference/fx) — `fx.id()` in the runtime uses `okid()`
|
|
137
|
+
- [Clock](/docs/elements/clock) — process `instanceId` (`inst-<okid>`) is an OKID
|
package/src/auth/bindings.ts
CHANGED
|
@@ -175,7 +175,7 @@ export function createAuthHttpBindings(
|
|
|
175
175
|
} = {},
|
|
176
176
|
): AuthHttpMaterialization {
|
|
177
177
|
const sessions = options.sessions ?? config.sessions ?? createSessionStore();
|
|
178
|
-
const identities = options.identities ?? createIdentityStore();
|
|
178
|
+
const identities = options.identities ?? config.identities ?? createIdentityStore();
|
|
179
179
|
const loginAttempts = createLoginAttemptBag();
|
|
180
180
|
const rateGates = createAuthRateGates(options.rateLimitEnabled !== false);
|
|
181
181
|
const policyGates = createAuthPolicyGates(config);
|
package/src/auth/config.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { PasswordHashOptions } from "../runtime/types.ts";
|
|
|
8
8
|
import type { SessionStore } from "./sessions.ts";
|
|
9
9
|
import { createApiKeyStore, type ApiKeyStore } from "./api-keys.ts";
|
|
10
10
|
import type { BreachCheckFn } from "./breach-check.ts";
|
|
11
|
+
import { createIdentityStore, type IdentityStore } from "./identity.ts";
|
|
11
12
|
import type { PasswordPolicyOptions } from "./password-policy.ts";
|
|
12
13
|
import {
|
|
13
14
|
resolveAuthSchema,
|
|
@@ -116,6 +117,11 @@ export interface GateAuthOptions extends AuthSchemaOptions {
|
|
|
116
117
|
* Not part of the public app DX — escape hatch for embedding.
|
|
117
118
|
*/
|
|
118
119
|
readonly sessions?: SessionStore;
|
|
120
|
+
/**
|
|
121
|
+
* Shared identity / credential store. Created automatically when omitted.
|
|
122
|
+
* All Gate auth method plugins resolve against this one store by default.
|
|
123
|
+
*/
|
|
124
|
+
readonly identities?: IdentityStore;
|
|
119
125
|
/**
|
|
120
126
|
* Shared API key store. Created automatically when omitted
|
|
121
127
|
* (`gate.auth.secret` is the HMAC pepper).
|
|
@@ -186,6 +192,8 @@ export interface ResolvedGateAuth {
|
|
|
186
192
|
readonly hooks: AuthDatabaseHooks | undefined;
|
|
187
193
|
readonly sessions: SessionStore | undefined;
|
|
188
194
|
readonly apiKeyStore: ApiKeyStore;
|
|
195
|
+
/** Shared identity store — the single user/credential source across Gate auth + method plugins. */
|
|
196
|
+
readonly identities: IdentityStore;
|
|
189
197
|
readonly tenant: ResolvedTenantAuth | undefined;
|
|
190
198
|
readonly tenantStore: TenantStore | undefined;
|
|
191
199
|
readonly now: (() => number) | undefined;
|
|
@@ -296,6 +304,7 @@ export function resolveGateAuth(options: ResolveGateAuthOptions): ResolvedGateAu
|
|
|
296
304
|
hooks: auth.hooks,
|
|
297
305
|
sessions: auth.sessions,
|
|
298
306
|
apiKeyStore: auth.apiKeyStore ?? createApiKeyStore({ pepper: secret }),
|
|
307
|
+
identities: auth.identities ?? createIdentityStore(),
|
|
299
308
|
tenant: auth.tenant ? resolveTenantAuth(auth.tenant) : undefined,
|
|
300
309
|
tenantStore: auth.tenant ? (auth.tenantStore ?? createTenantStore()) : auth.tenantStore,
|
|
301
310
|
now: auth.now,
|