okengine 0.17.2 → 0.18.4
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/AGENTS.md +5 -3
- package/README.md +14 -10
- package/manifest.v1.schema.json +61 -2
- package/package.json +19 -5
- package/site/content/docs/ai/index.mdx +1 -1
- package/site/content/docs/ai/mcp.mdx +10 -1
- package/site/content/docs/ai/skills.mdx +9 -9
- package/site/content/docs/elements/ai.mdx +1 -1
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/gate.mdx +3 -2
- package/site/content/docs/elements/store.mdx +289 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/basic-usage.mdx +3 -10
- package/site/content/docs/get-started/index.mdx +1 -1
- package/site/content/docs/get-started/installation.mdx +2 -3
- package/site/content/docs/get-started/introduction.mdx +58 -121
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/project-structure.mdx +4 -11
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +93 -71
- package/site/content/docs/index.mdx +44 -11
- package/site/content/docs/meta.json +8 -5
- 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/providers/index.mdx +2 -0
- package/site/content/docs/recipes/index.mdx +2 -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-De7Lc2JC.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.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-4rHOePuE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.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 +5 -2
- 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
|
@@ -8,6 +8,8 @@ source: "docs/spec/unified-theory.md"
|
|
|
8
8
|
Every provider below speaks a protocol oke already drives — Postgres wire or Redis wire.
|
|
9
9
|
Hand the connection string to that driver; no new driver id, no Flow code changes.
|
|
10
10
|
|
|
11
|
+
These managed providers are real, working infrastructure choices behind the same two templates (`standard`, `advanced`) and the same eight elements — not alternative products or separate frameworks.
|
|
12
|
+
|
|
11
13
|
<Callout title="The one rule">
|
|
12
14
|
Vendor choice lives in a connection URL (`DATABASE_URL` / `REDIS_URL`) — never in
|
|
13
15
|
`drivers.store.sql` / `drivers.store.kv`, which only ever say `postgres` or `redis`.
|
|
@@ -8,6 +8,8 @@ source: "docs/spec/unified-theory.md"
|
|
|
8
8
|
Every recipe below is already wired into `oke docker` — pin an image in `oke.config.ts`
|
|
9
9
|
and get env, healthcheck, and a connection URL for free.
|
|
10
10
|
|
|
11
|
+
These recipes are real, working infrastructure choices behind the same two templates (`standard`, `advanced`) and the same eight elements — not alternative products or separate frameworks.
|
|
12
|
+
|
|
11
13
|
<Callout title="The one rule">
|
|
12
14
|
Vendor choice lives in `images[…]` — never in `drivers.*`, which only ever say protocol ids
|
|
13
15
|
(`postgres`, `redis`, `s3`, `smtp`, `meilisearch`, `openai-compatible`, `ollama`, …).
|
|
@@ -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,
|