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
|
@@ -1,116 +1,140 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Why OKE
|
|
3
|
-
description:
|
|
3
|
+
description: From forty tools to one species — the Manifest, the `fx` rule, and the eight closed elements.
|
|
4
4
|
source: docs/spec/unified-theory.md
|
|
5
5
|
icon: Compass
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
that serves last month's schema, the secret that only exists on your laptop,
|
|
10
|
-
the dashboard that has never heard of your new Flow.
|
|
8
|
+
## From forty tools to one species
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
knowledge, kept in places the compiler cannot check, updated by memory.
|
|
10
|
+
A booking API, a nightly cleanup job, a receipt email, a row-change hook — in most stacks these are **four frameworks**. In OKE they are **one species** with one shape. Learn the shape once; only the trigger changes.
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
> **The one rule**
|
|
13
|
+
> Every backend behavior is a **Flow**: `on(Trigger) → Effects`. There are no separate species called endpoints, handlers, consumers, jobs, or workflows.
|
|
14
|
+
|
|
15
|
+
## The pain we solve
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
Every TypeScript backend works on day one. The bill arrives later. None of these are router problems — they are **gaps** between what your code knows and what your tooling knows.
|
|
21
18
|
|
|
22
|
-
###
|
|
19
|
+
### Cache that lies
|
|
23
20
|
|
|
24
|
-
You add a column to `orders` and update three writers. The
|
|
25
|
-
key in `checkout` is not one of them. A customer finds it a week later.
|
|
21
|
+
You add a column to `orders` and update three writers. The cache key in `checkout` is not one of them. A customer finds it a week later.
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
follows the Flow — there is no separate key to remember.
|
|
23
|
+
OKE derives: reads and writes are recorded through `fx`, so cache invalidation follows the Flow — there is no separate key to remember.
|
|
29
24
|
|
|
30
|
-
###
|
|
25
|
+
### Secret that fails at 2am
|
|
31
26
|
|
|
32
|
-
`STRIPE_KEY` lives in your laptop's `.env`, a README, and a teammate's shell
|
|
33
|
-
history. The deploy boots fine — the first charge request dies at 2am.
|
|
27
|
+
`STRIPE_KEY` lives in your laptop's `.env`, a README, and a teammate's shell history. The deploy boots fine — the first charge request dies at 2am.
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
code; boot resolves every contract and fails loud with every gap listed —
|
|
37
|
-
never halfway.
|
|
29
|
+
OKE derives: [Vault](/docs/elements/vault) contracts declare the need in code; boot resolves every contract and **fails loud** with every gap listed — never halfway.
|
|
38
30
|
|
|
39
31
|
### The glue you rewrite
|
|
40
32
|
|
|
41
|
-
CORS rules, security headers, CSRF tokens, compression — copied from the last
|
|
42
|
-
repo, tweaked, and already drifting from whatever that repo does today.
|
|
33
|
+
CORS rules, security headers, CSRF tokens, compression — copied from the last repo, tweaked, and already drifting from whatever that repo does today.
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
shared lifecycle, optional live DB config, nothing to re-copy.
|
|
35
|
+
OKE derives: the official `okengine/plugins` set ships this glue once — shared lifecycle, optional live DB config, nothing to re-copy.
|
|
46
36
|
|
|
47
37
|
### The dashboard that doesn't know you
|
|
48
38
|
|
|
49
|
-
Your observability stack learned your routes from sampled traffic. The Flow
|
|
50
|
-
you deployed an hour ago is invisible until someone wires it by hand.
|
|
39
|
+
Your observability stack learned your routes from sampled traffic. The Flow you deployed an hour ago is invisible until someone wires it by hand.
|
|
51
40
|
|
|
52
|
-
|
|
53
|
-
flows, effects, traces, architecture — current on every save, in dev and prod
|
|
54
|
-
(`:6533`).
|
|
41
|
+
OKE derives: its operator interface reads the Manifest — flows, effects, traces, architecture — current on every save, in dev and prod (`:6533`).
|
|
55
42
|
|
|
56
|
-
###
|
|
43
|
+
### Permission check in the wrong place
|
|
57
44
|
|
|
58
|
-
`if (!user.isAdmin)` sits in handler forty-one of sixty. Which Flows touch
|
|
59
|
-
`payments`? grep answers slowly; review answers never.
|
|
45
|
+
`if (!user.isAdmin)` sits in handler forty-one of sixty. Which Flows touch `payments`? grep answers slowly; review answers never.
|
|
60
46
|
|
|
61
|
-
|
|
62
|
-
access appears in Manifest Diff, not in a diff nobody reads.
|
|
47
|
+
OKE derives: declared effects produce a **least-privilege matrix** — widening access appears in Manifest Diff, not in a diff nobody reads.
|
|
63
48
|
|
|
64
49
|
### Local works, prod doesn't
|
|
65
50
|
|
|
66
|
-
Local runs one vendor client, CI another, prod a third — three glue stories
|
|
67
|
-
for the same database. "Works on my machine" is a driver mismatch.
|
|
51
|
+
Local runs one vendor client, CI another, prod a third — three glue stories for the same database.
|
|
68
52
|
|
|
69
|
-
|
|
70
|
-
`s3`), the vendor lives in `images`, and `oke dev` runs the real
|
|
71
|
-
stack locally.
|
|
53
|
+
OKE derives: drivers are named after protocols (`postgres`, `redis`, `s3`), the vendor lives in `images`, and `oke dev` runs the real stack locally.
|
|
72
54
|
|
|
73
|
-
|
|
55
|
+
---
|
|
74
56
|
|
|
75
|
-
Every
|
|
76
|
-
code already has. Watch one change propagate both ways.
|
|
57
|
+
Every gap above is the same shape: knowledge the code already has, duplicated somewhere the compiler cannot check. **The tax is drift.** One change costs fifteen seams to update by hand; in OKE it always costs one.
|
|
77
58
|
|
|
78
|
-
|
|
59
|
+
## One Manifest feeds every surface
|
|
79
60
|
|
|
80
|
-
|
|
81
|
-
feeds five surfaces — they cannot disagree, because none of them is a copy.
|
|
61
|
+
At build time OKE extracts a **Manifest** — a machine-readable description of your system from TypeScript source. You do not maintain a second source of truth; the Manifest is the _single artifact_ from which everything downstream is derived:
|
|
82
62
|
|
|
83
|
-
|
|
63
|
+
| Surface | Port | You maintain? |
|
|
64
|
+
| --------------------------------- | ------------------ | ----------------------------- |
|
|
65
|
+
| Typed client (`okengine/client`) | your app code | No — derived from `Manifest` |
|
|
66
|
+
| Console panels, traces, explorers | `:6533` | No — reads the Manifest |
|
|
67
|
+
| MCP for agents | `:6535` | No — reads the Manifest |
|
|
68
|
+
| Architecture diagram | Console | No — it _is_ the effect graph |
|
|
69
|
+
| Capability matrix + cache keys | compiler / runtime | No — inferred from `fx` |
|
|
84
70
|
|
|
85
|
-
|
|
86
|
-
because it has irreducible physics. One change costs up to fifteen seams in
|
|
87
|
-
the zoo; here it always costs two.
|
|
71
|
+
Because every surface reads the same Manifest, they **cannot disagree** — there is only one source of truth.
|
|
88
72
|
|
|
89
|
-
|
|
73
|
+
## The `fx` rule — one door to the world
|
|
90
74
|
|
|
91
|
-
|
|
92
|
-
ninth element — the set of eight is closed.
|
|
75
|
+
**All world access goes through `fx`.** A Flow that imports `node:fs`, calls `fetch` directly, or uses `Date.now()` is a defect. Effects are **inferred from what a Flow touches through `fx`**, and that inference powers the Manifest, the Console, caching, and durability.
|
|
93
76
|
|
|
94
|
-
|
|
77
|
+
<Callout title="What `fx` records">
|
|
78
|
+
Every read, write, emit, send, ask, secret, and call is captured — so the Manifest knows exactly
|
|
79
|
+
which flows touch `orders` or send PII to a model, without you declaring it.
|
|
80
|
+
</Callout>
|
|
95
81
|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
82
|
+
| Inferred from `fx` | What it gives you |
|
|
83
|
+
| ----------------------- | ------------------------------------ |
|
|
84
|
+
| Cache invalidation keys | Automatic — follows the Flow |
|
|
85
|
+
| Live queries | Built-in subscription model |
|
|
86
|
+
| Least-privilege tokens | Capability matrix from effects |
|
|
87
|
+
| Deterministic tests | Time, clock, and randomness injected |
|
|
88
|
+
| Manifest Diff | Exact change surface on every save |
|
|
89
|
+
|
|
90
|
+
## Eight elements — a closed set
|
|
91
|
+
|
|
92
|
+
Forty infrastructure concerns collapse into eight elements — each kept only because it has **irreducible physics**. New infrastructure becomes a **driver** for an existing element, never a ninth.
|
|
93
|
+
|
|
94
|
+
| Element | Essence | Replaces the zoo |
|
|
95
|
+
| ----------- | ----------------------------- | --------------------------------------------------------- |
|
|
96
|
+
| **Flow** | Behavior | endpoint · handler · consumer · job · workflow · webhook |
|
|
97
|
+
| **Signal** | Data in motion | queue · pub/sub · stream · websocket · SSE · event bus |
|
|
98
|
+
| **Store** | Data at rest | database · cache · KV · file storage · search index |
|
|
99
|
+
| **Clock** | Time | cron · delay · timeout · durable sleep · TTL |
|
|
100
|
+
| **Gate** | Permission to act | auth · session · ABAC · rate limit · quota · feature flag |
|
|
101
|
+
| **Vault** | Protected knowledge | secrets · config · environment |
|
|
102
|
+
| **Channel** | Reaching humans | email · SMS · WhatsApp · push |
|
|
103
|
+
| **AI** | Reaching machine intelligence | model calls · prompts · embeddings · agents · RAG |
|
|
104
|
+
|
|
105
|
+
Drivers are named after **protocols** (`postgres`, `redis`, `s3`) — never vendors. The vendor lives in `images`, and `oke dev` runs the real stack locally.
|
|
106
|
+
|
|
107
|
+
## The shape of a Flow
|
|
108
|
+
|
|
109
|
+
One pipeline. Only the trigger changes between an endpoint, a job, a consumer, and a row hook:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { on, flow, http } from "okengine";
|
|
113
|
+
import { z } from "zod";
|
|
114
|
+
|
|
115
|
+
export const health = on(
|
|
116
|
+
http.get().public(),
|
|
117
|
+
flow({
|
|
118
|
+
out: z.object({ ok: z.literal(true) }),
|
|
119
|
+
do: () => ({ ok: true as const }),
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
| Piece | Role |
|
|
125
|
+
| ------------- | -------------------------------------------------------------- |
|
|
126
|
+
| **Trigger** | How work starts — `http`, a signal, `every`, a row change |
|
|
127
|
+
| **Contracts** | `in`, `out`, typed `errors` — validated before and after `do` |
|
|
128
|
+
| **`do`** | The body — every read, write, emit, and call goes through `fx` |
|
|
129
|
+
| **Effects** | Inferred from those `fx` calls — not hand-annotated |
|
|
130
|
+
|
|
131
|
+
Consequence: **one documentation path, one trace shape, one Flow model, one thing for an AI agent to learn.**
|
|
106
132
|
|
|
107
133
|
## Ambition, stated plainly
|
|
108
134
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
| **Grounded in** | Eight elements, effect inference through `fx`, Gate and Vault, the official plugin set |
|
|
113
|
-
| **Maturity** | **pre-1.0** — published and usable; not independently battle-tested at scale yet |
|
|
135
|
+
> **Ambition**: The default, most capable TypeScript backend — Bun-first, Web-Standards portable, contract-first
|
|
136
|
+
> **Grounded in**: Eight elements, effect inference through `fx`, Gate and Vault, the official plugin set
|
|
137
|
+
> **Maturity**: **pre-1.0** — published and usable; not independently battle-tested at scale yet
|
|
114
138
|
|
|
115
139
|
## Learn more
|
|
116
140
|
|
|
@@ -23,7 +23,7 @@ on(orderPlaced, sendReceipt);
|
|
|
23
23
|
<Card
|
|
24
24
|
title="Get Started"
|
|
25
25
|
description="One law → install → first flows."
|
|
26
|
-
href="/docs/get-started"
|
|
26
|
+
href="/docs/get-started/introduction"
|
|
27
27
|
/>
|
|
28
28
|
<Card title="Elements" description="Flow → AI reference, one page each." href="/docs/elements" />
|
|
29
29
|
<Card
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Apple"
|
|
3
|
+
description: "Official plugin — Sign in with Apple with form_post callbacks and an ES256 client-secret JWT."
|
|
4
|
+
icon: "Apple"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Sign in with Apple is OIDC with three twists: the web flow **posts** its
|
|
9
|
+
response, every exchange needs an ES256 client-secret **JWT you sign**, and
|
|
10
|
+
`email_verified` can arrive as the _string_ `"false"`.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Create a Sign in with Apple key (Team ID, Key ID, `.p8` private key), seed the key in Vault, and
|
|
14
|
+
register your exact callback URI — Apple validates all three on every exchange.
|
|
15
|
+
</Callout>
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
<Steps>
|
|
20
|
+
|
|
21
|
+
<Step>
|
|
22
|
+
### Create a key
|
|
23
|
+
|
|
24
|
+
In the Apple Developer portal: Identifiers → register an App ID with _Sign In
|
|
25
|
+
with Apple_; Keys → create a key with that capability; note the **Team ID**
|
|
26
|
+
and **Key ID**, and download the `.p8` once.
|
|
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
|
+
apple: {
|
|
46
|
+
enabled: true,
|
|
47
|
+
teamId: "ABCDE12345",
|
|
48
|
+
keyId: "XYZ6789012",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
</Step>
|
|
56
|
+
|
|
57
|
+
<Step>
|
|
58
|
+
### Seed the private key
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
# .env.local
|
|
62
|
+
OAUTH_APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
|
|
63
|
+
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEH...
|
|
64
|
+
-----END PRIVATE KEY-----"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The driver mints a fresh ES256 client-secret JWT per exchange (`iss` = team,
|
|
68
|
+
`kid` = key, one-hour life) and discards nothing to disk.
|
|
69
|
+
|
|
70
|
+
</Step>
|
|
71
|
+
|
|
72
|
+
</Steps>
|
|
73
|
+
|
|
74
|
+
## How identity works
|
|
75
|
+
|
|
76
|
+
| Aspect | Behavior |
|
|
77
|
+
| ----------- | ------------------------------------------------------------------------- |
|
|
78
|
+
| Callback | Apple posts `code` + `state` as a form body — both GET and POST are bound |
|
|
79
|
+
| Signature | ES256 against `appleid.apple.com` JWKS |
|
|
80
|
+
| Issuer | must equal `https://appleid.apple.com` |
|
|
81
|
+
| Name | delivered only on first authorization via the form-posted `user` field |
|
|
82
|
+
| Email trust | strict parse — only boolean `true`, `"true"`, or `"1"` count |
|
|
83
|
+
|
|
84
|
+
**Consequence:** the string `"false"` stays unverified. Naive truthiness would
|
|
85
|
+
mark every private-relay email verified — that is the takeover bug class this
|
|
86
|
+
parse exists to close.
|
|
87
|
+
|
|
88
|
+
Default scopes: `name`, `email`.
|
|
89
|
+
|
|
90
|
+
## Options
|
|
91
|
+
|
|
92
|
+
| Option | Type | Default | Meaning |
|
|
93
|
+
| --------------------- | ---------- | ------------------ | --------------------------------------- |
|
|
94
|
+
| `enabled` | `boolean` | `false` | Turn the provider on |
|
|
95
|
+
| `clientId` | `string` | Vault/env\* | `\*OAUTH_APPLE_CLIENT_ID` (Services ID) |
|
|
96
|
+
| `teamId` | `string` | required | Apple Developer Team ID |
|
|
97
|
+
| `keyId` | `string` | required | Private-key identifier |
|
|
98
|
+
| `redirectUri` | `string` | `{baseUrl}…/apple` | Exact registered URI |
|
|
99
|
+
| `scopes` | `string[]` | `name email` | Requested scopes |
|
|
100
|
+
| `storeProviderTokens` | `boolean` | `false` | Keep tokens in Vault |
|
|
101
|
+
|
|
102
|
+
## Surfaces
|
|
103
|
+
|
|
104
|
+
| Flow | Path |
|
|
105
|
+
| -------- | ------------------------------------- |
|
|
106
|
+
| Start | `POST /auth/oauth/apple/start` |
|
|
107
|
+
| Callback | `GET+POST /auth/oauth/callback/apple` |
|
|
108
|
+
| Link | `POST /auth/oauth/apple/link` |
|
|
109
|
+
|
|
110
|
+
## Troubleshooting
|
|
111
|
+
|
|
112
|
+
<Accordions>
|
|
113
|
+
<Accordion title="invalid_client at token exchange">
|
|
114
|
+
|
|
115
|
+
Team ID, Key ID, or the `.p8` does not match the App ID / Services ID you are
|
|
116
|
+
signing for. The JWT is minted fresh per exchange, so fixing the inputs is
|
|
117
|
+
enough — no restart cache to clear.
|
|
118
|
+
|
|
119
|
+
</Accordion>
|
|
120
|
+
<Accordion title="invalid_request mentioning response_mode">
|
|
121
|
+
|
|
122
|
+
Your Services ID must allow the callback you registered. Check the return URLs
|
|
123
|
+
on the Sign in with Apple key configuration.
|
|
124
|
+
|
|
125
|
+
</Accordion>
|
|
126
|
+
<Accordion title="Callback never fires">
|
|
127
|
+
|
|
128
|
+
Browsers post to `/auth/oauth/callback/apple`; make sure proxies do not strip
|
|
129
|
+
form bodies. The route accepts POST with
|
|
130
|
+
`application/x-www-form-urlencoded`.
|
|
131
|
+
|
|
132
|
+
</Accordion>
|
|
133
|
+
</Accordions>
|
|
134
|
+
|
|
135
|
+
## Learn more
|
|
136
|
+
|
|
137
|
+
- [OAuth](/docs/plugins/oauth) — shared flows and security model
|
|
138
|
+
- [Vault](/docs/elements/vault) — seeding `OAUTH_APPLE_PRIVATE_KEY`
|
|
139
|
+
- [Gate](/docs/elements/gate) — `gate.auth`
|
|
140
|
+
|
|
141
|
+
## Next
|
|
142
|
+
|
|
143
|
+
<Cards>
|
|
144
|
+
<Card title="Google" description="OIDC reference provider." href="/docs/plugins/google" />
|
|
145
|
+
<Card
|
|
146
|
+
title="Microsoft"
|
|
147
|
+
description="Entra tenants and issuer templates."
|
|
148
|
+
href="/docs/plugins/microsoft"
|
|
149
|
+
/>
|
|
150
|
+
<Card title="X" description="PKCE public client, never-verified emails." href="/docs/plugins/x" />
|
|
151
|
+
</Cards>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Discord"
|
|
3
|
+
description: "Official plugin — Discord sign-in where email can be real, verified, or legitimately absent."
|
|
4
|
+
icon: "MessageCircle"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Discord is OAuth2 with one quirk: `/users/@me` `email` can be **null**
|
|
9
|
+
(phone-only accounts). `oauth()` signs those people in without an email
|
|
10
|
+
instead of failing them.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Keep the `email` scope in the request (it is on by default). Without it Discord never reports
|
|
14
|
+
`verified: true`, and unverified emails cannot claim existing accounts.
|
|
15
|
+
</Callout>
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
<Steps>
|
|
20
|
+
|
|
21
|
+
<Step>
|
|
22
|
+
### Create an application
|
|
23
|
+
|
|
24
|
+
Discord Developer Portal → Applications → **New Application** → OAuth2. Add a
|
|
25
|
+
redirect under OAuth2 → Redirects:
|
|
26
|
+
`https://app.example.com/auth/oauth/callback/discord`.
|
|
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
|
+
discord: { enabled: true },
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
</Step>
|
|
52
|
+
|
|
53
|
+
<Step>
|
|
54
|
+
### Set the client secret
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
# .env.local
|
|
58
|
+
OAUTH_DISCORD_CLIENT_SECRET=...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
</Step>
|
|
62
|
+
|
|
63
|
+
</Steps>
|
|
64
|
+
|
|
65
|
+
## How identity works
|
|
66
|
+
|
|
67
|
+
| Aspect | Behavior |
|
|
68
|
+
| ----------- | ----------------------------------------------------------------------------------- |
|
|
69
|
+
| Profile | `GET https://discord.com/api/users/@me` (string `id` is the subject) |
|
|
70
|
+
| Email | taken as-is when present; `null` flows through as _no email_ |
|
|
71
|
+
| Email trust | `verified: true` only; the flag silently going missing keeps the address unverified |
|
|
72
|
+
| Name | `global_name`, falling back to `username` |
|
|
73
|
+
|
|
74
|
+
**Consequence:** an integration bug that drops the `verified` field degrades
|
|
75
|
+
to unverified — never to falsely verified. That direction of failure is what
|
|
76
|
+
keeps account takeover off the table.
|
|
77
|
+
|
|
78
|
+
Default scopes: `identify`, `email`. The authorize URL always carries
|
|
79
|
+
`prompt=consent`.
|
|
80
|
+
|
|
81
|
+
## Options
|
|
82
|
+
|
|
83
|
+
| Option | Type | Default | Meaning |
|
|
84
|
+
| --------------------- | ---------- | -------------------- | --------------------------- |
|
|
85
|
+
| `enabled` | `boolean` | `false` | Turn the provider on |
|
|
86
|
+
| `clientId` | `string` | Vault/env\* | `\*OAUTH_DISCORD_CLIENT_ID` |
|
|
87
|
+
| `redirectUri` | `string` | `{baseUrl}…/discord` | Exact registered URI |
|
|
88
|
+
| `scopes` | `string[]` | driver defaults | Extra scopes |
|
|
89
|
+
| `storeProviderTokens` | `boolean` | `false` | Keep tokens in Vault |
|
|
90
|
+
|
|
91
|
+
## Surfaces
|
|
92
|
+
|
|
93
|
+
| Flow | Path |
|
|
94
|
+
| -------- | --------------------------------------- |
|
|
95
|
+
| Start | `POST /auth/oauth/discord/start` |
|
|
96
|
+
| Callback | `GET+POST /auth/oauth/callback/discord` |
|
|
97
|
+
| Link | `POST /auth/oauth/discord/link` |
|
|
98
|
+
|
|
99
|
+
## Troubleshooting
|
|
100
|
+
|
|
101
|
+
<Accordions>
|
|
102
|
+
<Accordion title="Users sign in with no email attached">
|
|
103
|
+
|
|
104
|
+
Phone-only Discord accounts expose `email: null`. The session works; the user
|
|
105
|
+
row simply has no address until they add one at Discord.
|
|
106
|
+
|
|
107
|
+
</Accordion>
|
|
108
|
+
<Accordion title="invalid_oauth2 error code">
|
|
109
|
+
|
|
110
|
+
The client secret was rotated in the portal while old codes were in flight.
|
|
111
|
+
Restart the flow — flow rows are single-use and expire after ten minutes.
|
|
112
|
+
|
|
113
|
+
</Accordion>
|
|
114
|
+
<Accordion title="Email never marked verified">
|
|
115
|
+
|
|
116
|
+
The app lacks the `email` scope or the user has not confirmed their address at
|
|
117
|
+
Discord. Unverified emails provision new accounts but never take over
|
|
118
|
+
existing ones.
|
|
119
|
+
|
|
120
|
+
</Accordion>
|
|
121
|
+
</Accordions>
|
|
122
|
+
|
|
123
|
+
## Learn more
|
|
124
|
+
|
|
125
|
+
- [OAuth](/docs/plugins/oauth) — shared flows and security model
|
|
126
|
+
- [GitHub](/docs/plugins/github) — primary-email lookup pattern
|
|
127
|
+
- [Vault](/docs/elements/vault) — where secrets live
|
|
128
|
+
|
|
129
|
+
## Next
|
|
130
|
+
|
|
131
|
+
<Cards>
|
|
132
|
+
<Card
|
|
133
|
+
title="GitHub"
|
|
134
|
+
description="OAuth2 with verified-email lookup."
|
|
135
|
+
href="/docs/plugins/github"
|
|
136
|
+
/>
|
|
137
|
+
<Card title="Facebook" description="Never-verified emails." href="/docs/plugins/facebook" />
|
|
138
|
+
<Card title="Google" description="OIDC reference provider." href="/docs/plugins/google" />
|
|
139
|
+
</Cards>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Facebook"
|
|
3
|
+
description: "Official plugin — Facebook Login via Graph API with conservative email handling."
|
|
4
|
+
icon: "Share2"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Facebook Login is OAuth2 against the Graph API, and its trust story is the
|
|
9
|
+
simplest one in `oauth()`: the platform offers **no verification signal to
|
|
10
|
+
apps**, so emails from Facebook are treated as unverified — always.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Treat every Facebook-provided address as unverified. The flow provisions new accounts with them
|
|
14
|
+
but refuses to let them claim accounts that already exist.
|
|
15
|
+
</Callout>
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
<Steps>
|
|
20
|
+
|
|
21
|
+
<Step>
|
|
22
|
+
### Create an app
|
|
23
|
+
|
|
24
|
+
Meta for Developers → **Create App** → _Authentication_. Under Facebook Login
|
|
25
|
+
→ Settings, add `https://app.example.com/auth/oauth/callback/facebook` as a
|
|
26
|
+
Valid OAuth Redirect 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
|
+
facebook: { enabled: true },
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
</Step>
|
|
52
|
+
|
|
53
|
+
<Step>
|
|
54
|
+
### Set the client secret
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
# .env.local
|
|
58
|
+
OAUTH_FACEBOOK_CLIENT_SECRET=...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
</Step>
|
|
62
|
+
|
|
63
|
+
</Steps>
|
|
64
|
+
|
|
65
|
+
## How identity works
|
|
66
|
+
|
|
67
|
+
| Aspect | Behavior |
|
|
68
|
+
| -------------- | ------------------------------------------------------------- |
|
|
69
|
+
| Authorize | `www.facebook.com/v21.0/dialog/oauth` |
|
|
70
|
+
| Token exchange | `graph.facebook.com/v21.0/oauth/access_token` |
|
|
71
|
+
| Profile | `GET /me?fields=id,name,email` (the `id` is the subject) |
|
|
72
|
+
| Email | present when the user has one; phone-only accounts have none |
|
|
73
|
+
| Email trust | **always unverified** — no trustworthy provider signal exists |
|
|
74
|
+
|
|
75
|
+
**Consequence:** an attacker completing Facebook login with _your_ email gets
|
|
76
|
+
`email_in_use`, not your session. This exact scenario is the takeover class
|
|
77
|
+
the trust matrix closes.
|
|
78
|
+
|
|
79
|
+
Default scopes: `email`, `public_profile`.
|
|
80
|
+
|
|
81
|
+
## Options
|
|
82
|
+
|
|
83
|
+
| Option | Type | Default | Meaning |
|
|
84
|
+
| --------------------- | ---------- | --------------------- | ------------------------------------- |
|
|
85
|
+
| `enabled` | `boolean` | `false` | Turn the provider on |
|
|
86
|
+
| `clientId` | `string` | Vault/env\* | `\*OAUTH_FACEBOOK_CLIENT_ID` (App ID) |
|
|
87
|
+
| `redirectUri` | `string` | `{baseUrl}…/facebook` | Exact registered URI |
|
|
88
|
+
| `scopes` | `string[]` | driver defaults | Extra scopes |
|
|
89
|
+
| `storeProviderTokens` | `boolean` | `false` | Keep tokens in Vault |
|
|
90
|
+
|
|
91
|
+
## Surfaces
|
|
92
|
+
|
|
93
|
+
| Flow | Path |
|
|
94
|
+
| -------- | ---------------------------------------- |
|
|
95
|
+
| Start | `POST /auth/oauth/facebook/start` |
|
|
96
|
+
| Callback | `GET+POST /auth/oauth/callback/facebook` |
|
|
97
|
+
| Link | `POST /auth/oauth/facebook/link` |
|
|
98
|
+
|
|
99
|
+
## Troubleshooting
|
|
100
|
+
|
|
101
|
+
<Accordions>
|
|
102
|
+
<Accordion title="URL Blocked: redirect_uri">
|
|
103
|
+
|
|
104
|
+
The URI is not on the Valid OAuth Redirect URIs list, or the app is in
|
|
105
|
+
development mode and the user lacks a role. Byte-exact matching applies.
|
|
106
|
+
|
|
107
|
+
</Accordion>
|
|
108
|
+
<Accordion title="Sign-in works but there is no email">
|
|
109
|
+
|
|
110
|
+
The user declined the email permission or has none on file. The flow proceeds
|
|
111
|
+
without an address — same behavior as Discord phone-only accounts.
|
|
112
|
+
|
|
113
|
+
</Accordion>
|
|
114
|
+
<Accordion title="Error code 190 at exchange">
|
|
115
|
+
|
|
116
|
+
The app secret was rotated or the code was replayed. Codes are single-use;
|
|
117
|
+
restart from `/start`.
|
|
118
|
+
|
|
119
|
+
</Accordion>
|
|
120
|
+
</Accordions>
|
|
121
|
+
|
|
122
|
+
## Learn more
|
|
123
|
+
|
|
124
|
+
- [OAuth](/docs/plugins/oauth) — shared flows and security model
|
|
125
|
+
- [X](/docs/plugins/x) — also never-verified emails
|
|
126
|
+
- [Vault](/docs/elements/vault) — where secrets live
|
|
127
|
+
|
|
128
|
+
## Next
|
|
129
|
+
|
|
130
|
+
<Cards>
|
|
131
|
+
<Card title="X" description="PKCE public client." href="/docs/plugins/x" />
|
|
132
|
+
<Card title="Figma" description="Basic-auth token endpoint." href="/docs/plugins/figma" />
|
|
133
|
+
<Card title="Discord" description="Nullable emails." href="/docs/plugins/discord" />
|
|
134
|
+
</Cards>
|