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.
Files changed (197) hide show
  1. package/README.md +9 -5
  2. package/manifest.v1.schema.json +61 -2
  3. package/package.json +18 -4
  4. package/site/content/docs/elements/clock.mdx +1 -1
  5. package/site/content/docs/elements/flow.mdx +25 -1
  6. package/site/content/docs/elements/store.mdx +287 -341
  7. package/site/content/docs/elements/vault.mdx +5 -5
  8. package/site/content/docs/get-started/installation.mdx +1 -2
  9. package/site/content/docs/get-started/introduction.mdx +54 -110
  10. package/site/content/docs/get-started/meta.json +9 -1
  11. package/site/content/docs/get-started/testing.mdx +328 -0
  12. package/site/content/docs/get-started/why.mdx +94 -70
  13. package/site/content/docs/index.mdx +1 -1
  14. package/site/content/docs/plugins/apple.mdx +151 -0
  15. package/site/content/docs/plugins/discord.mdx +139 -0
  16. package/site/content/docs/plugins/facebook.mdx +134 -0
  17. package/site/content/docs/plugins/figma.mdx +138 -0
  18. package/site/content/docs/plugins/github.mdx +138 -0
  19. package/site/content/docs/plugins/google.mdx +153 -0
  20. package/site/content/docs/plugins/index.mdx +47 -1
  21. package/site/content/docs/plugins/meta.json +10 -0
  22. package/site/content/docs/plugins/microsoft.mdx +151 -0
  23. package/site/content/docs/plugins/oauth.mdx +188 -0
  24. package/site/content/docs/plugins/x.mdx +125 -0
  25. package/site/content/docs/reference/cli.md +3 -2
  26. package/site/content/docs/reference/client.mdx +58 -1
  27. package/site/content/docs/reference/configuration.mdx +2 -4
  28. package/site/content/docs/reference/fx.mdx +3 -1
  29. package/site/content/docs/reference/index.mdx +0 -5
  30. package/site/content/docs/reference/meta.json +2 -2
  31. package/site/content/docs/reference/okid.mdx +137 -0
  32. package/src/auth/bindings.ts +1 -1
  33. package/src/auth/config.ts +9 -0
  34. package/src/auth/identity-sql.ts +314 -0
  35. package/src/auth/identity.ts +140 -2
  36. package/src/auth/index.ts +17 -1
  37. package/src/auth/method-context.ts +3 -0
  38. package/src/auth/oauth-as/cimd.ts +132 -0
  39. package/src/auth/oauth-as/crypto.test.ts +101 -0
  40. package/src/auth/oauth-as/crypto.ts +393 -0
  41. package/src/auth/oauth-as/errors.ts +68 -0
  42. package/src/auth/oauth-as/http.test.ts +419 -0
  43. package/src/auth/oauth-as/http.ts +842 -0
  44. package/src/auth/oauth-as/stores.ts +61 -0
  45. package/src/auth/oauth-as/tables.ts +142 -0
  46. package/src/auth/tables.ts +0 -11
  47. package/src/bench/README.md +83 -0
  48. package/src/bench/REPORT.md +176 -0
  49. package/src/bench/g01-rls-stamp.bench.ts +194 -0
  50. package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
  51. package/src/bench/g03-signal-once.bench.ts +157 -0
  52. package/src/bench/g03-signal-reconnect.bench.ts +254 -0
  53. package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
  54. package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
  55. package/src/bench/g05-sustained-full.bench.ts +265 -0
  56. package/src/bench/g06-mixed-load.bench.ts +260 -0
  57. package/src/bench/g07-vault-crypto.bench.ts +100 -0
  58. package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
  59. package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
  60. package/src/bench/g08-store-kv-durable.bench.ts +133 -0
  61. package/src/bench/g08-store-sql.bench.ts +178 -0
  62. package/src/bench/g09-journal-sustained.bench.ts +203 -0
  63. package/src/bench/g10-observability-contention.bench.ts +246 -0
  64. package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
  65. package/src/bench/g13-elements.bench.ts +427 -0
  66. package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
  67. package/src/bench/g15-postgres-degradation.bench.ts +264 -0
  68. package/src/bench/g16-live-query-fanout.bench.ts +206 -0
  69. package/src/bench/lib/event-loop-lag.ts +26 -0
  70. package/src/bench/lib/infra.ts +60 -0
  71. package/src/bench/lib/report.ts +52 -0
  72. package/src/bench/lib/rss-sampler.ts +61 -0
  73. package/src/bench/lib/signal-pg.ts +88 -0
  74. package/src/bench/load-app.ts +337 -0
  75. package/src/bench/load-child.ts +108 -0
  76. package/src/bench/smoke.bench.ts +43 -0
  77. package/src/cli/competitor-mention-removal.test.ts +28 -0
  78. package/src/cli/doctor-fd.ts +117 -0
  79. package/src/cli/doctor.test.ts +192 -0
  80. package/src/cli/doctor.ts +129 -1
  81. package/src/client/create.ts +95 -1
  82. package/src/client/index.ts +9 -2
  83. package/src/client/transport.ts +11 -4
  84. package/src/client/use-live-query.ts +154 -0
  85. package/src/client-react/index.ts +15 -1
  86. package/src/client-react/live-resource.ts +246 -0
  87. package/src/client-react/use-live-query.test.ts +475 -0
  88. package/src/client-react/use-live-query.ts +530 -0
  89. package/src/compiler/extract.test.ts +518 -0
  90. package/src/compiler/extract.ts +386 -19
  91. package/src/console/server/invoke-user-flow.ts +2 -1
  92. package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-DY4N6nnk.js} +1 -1
  93. package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-CsPDMrVM.js} +1 -1
  94. package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-CcTDXHuz.js} +3 -3
  95. package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-CKR595wP.js} +1 -1
  96. package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
  97. package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-CpPFFKyE.js} +1 -1
  98. package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-BsMf-9_W.js} +1 -1
  99. package/src/console/ui-next/dist/index.html +1 -1
  100. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
  101. package/src/drivers/cdc-outbox.ts +389 -0
  102. package/src/drivers/memory.ts +20 -0
  103. package/src/drivers/oauth-apple.ts +156 -0
  104. package/src/drivers/oauth-discord.ts +79 -0
  105. package/src/drivers/oauth-facebook.ts +80 -0
  106. package/src/drivers/oauth-figma.ts +116 -0
  107. package/src/drivers/oauth-github.ts +92 -0
  108. package/src/drivers/oauth-google.ts +142 -0
  109. package/src/drivers/oauth-microsoft.ts +174 -0
  110. package/src/drivers/oauth-oidc.ts +293 -0
  111. package/src/drivers/oauth-shared.ts +326 -0
  112. package/src/drivers/oauth-types.ts +159 -0
  113. package/src/drivers/oauth-x.ts +77 -0
  114. package/src/drivers/oauth2-common.ts +95 -0
  115. package/src/drivers/oauth2-token.ts +61 -0
  116. package/src/drivers/pg-rls-row-passes.ts +251 -0
  117. package/src/drivers/pg-rls.ts +2 -0
  118. package/src/drivers/postgres.ts +45 -2
  119. package/src/drivers/signal-postgres.ts +2 -1
  120. package/src/elements/channel/runtime.ts +29 -2
  121. package/src/elements/channel.test.ts +52 -0
  122. package/src/elements/gate/boot.ts +29 -2
  123. package/src/elements/store/emit-drizzle.ts +147 -14
  124. package/src/elements/store/field-ddl.test.ts +118 -0
  125. package/src/elements/store/field-types.test.ts +455 -0
  126. package/src/elements/store/list-query.golden.json +777 -0
  127. package/src/elements/store/list-query.parity.test.ts +396 -0
  128. package/src/elements/store/list-query.ts +792 -0
  129. package/src/elements/store/live-default.test.ts +136 -0
  130. package/src/elements/store/live-http.test.ts +160 -0
  131. package/src/elements/store/live-isolation.test.ts +291 -0
  132. package/src/elements/store/live-query-runtime.test.ts +323 -0
  133. package/src/elements/store/live-query-runtime.ts +403 -0
  134. package/src/elements/store/live-query-server.test.ts +377 -0
  135. package/src/elements/store/live-query-server.ts +102 -0
  136. package/src/elements/store/live-query.ts +97 -0
  137. package/src/elements/store/resource.ts +189 -680
  138. package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
  139. package/src/elements/store/schema-decl.ts +539 -41
  140. package/src/elements/store/sql-rls-stamp.test.ts +27 -0
  141. package/src/elements/store/sql-session.ts +297 -35
  142. package/src/elements/store/table.ts +102 -21
  143. package/src/elements/store.test.ts +3 -1
  144. package/src/elements/store.ts +12 -1
  145. package/src/elements/vault/chaos-child.ts +74 -1
  146. package/src/elements/vault/chaos.test.ts +4 -2
  147. package/src/elements/vault/storage.ts +4 -2
  148. package/src/index.ts +4 -1
  149. package/src/kernel/app-auth.ts +1 -0
  150. package/src/kernel/app.ts +116 -2
  151. package/src/kernel/auth-sharing.test.ts +196 -0
  152. package/src/kernel/boot.test.ts +3 -3
  153. package/src/kernel/errors.ts +8 -0
  154. package/src/kernel/fx.test.ts +1 -0
  155. package/src/kernel/fx.ts +14 -2
  156. package/src/kernel/horizontal-child.ts +2 -1
  157. package/src/kernel/http-resource.ts +33 -7
  158. package/src/kernel/identity-host-persist.test.ts +119 -0
  159. package/src/kernel/instance-id.ts +4 -2
  160. package/src/kernel/journal.ts +2 -1
  161. package/src/kernel/mcp-tool.test.ts +95 -0
  162. package/src/kernel/on.ts +9 -0
  163. package/src/kernel/realtime-bind.ts +326 -0
  164. package/src/kernel/resource-live.ts +117 -0
  165. package/src/kernel/triggers.ts +86 -4
  166. package/src/manifest/diff.ts +37 -0
  167. package/src/manifest/types.ts +64 -2
  168. package/src/okid.bench.test.ts +64 -0
  169. package/src/okid.test.ts +338 -0
  170. package/src/okid.ts +245 -0
  171. package/src/plugins/anonymous.ts +19 -1
  172. package/src/plugins/auth/shared.ts +15 -0
  173. package/src/plugins/index.ts +2 -0
  174. package/src/plugins/magic-link.ts +10 -8
  175. package/src/plugins/mcp-oauth.ts +208 -0
  176. package/src/plugins/oauth/flow-store.ts +117 -0
  177. package/src/plugins/oauth/link.ts +69 -0
  178. package/src/plugins/oauth/shared.ts +108 -0
  179. package/src/plugins/oauth/token-vault.ts +100 -0
  180. package/src/plugins/oauth.security.test.ts +535 -0
  181. package/src/plugins/oauth.ts +532 -0
  182. package/src/plugins/otp.ts +48 -6
  183. package/src/plugins/passkey.ts +20 -1
  184. package/src/plugins/two-factor.ts +11 -0
  185. package/src/plugins/username.ts +40 -7
  186. package/src/release/build-lib.ts +7 -1
  187. package/src/release/measure.ts +1 -0
  188. package/src/release/official-plugins.ts +4 -1
  189. package/src/runs/collect.ts +2 -1
  190. package/src/runs/drivers/files.ts +2 -1
  191. package/src/test/create-test-app.ts +114 -5
  192. package/src/test/export-bundle.test.ts +33 -0
  193. package/src/test/live-signals.test.ts +83 -0
  194. package/src/test/tenant-isolation.test.ts +175 -0
  195. package/src/testing.ts +26 -0
  196. package/src/upgrade/codemods.ts +1 -1
  197. package/site/content/docs/reference/migrating-environments.mdx +0 -158
@@ -1,116 +1,140 @@
1
1
  ---
2
2
  title: Why OKE
3
- description: The six seams every TypeScript backend maintains by hand and what OKE derives from one Manifest instead.
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
- Every TypeScript backend works on day one. The bill arrives later: the cache
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
- None of these are router problems. They are **seams** copies of your code's
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
- <Callout title="The one rule">
16
- **All world access goes through `fx`.** What a Flow reads, writes, emits, and reveals is recorded
17
- — so the seams below are derived from one Manifest, not re-typed per project.
18
- </Callout>
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
- ## The six seams
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
- ### The cache that lies
19
+ ### Cache that lies
23
20
 
24
- You add a column to `orders` and update three writers. The hand-bumped cache
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
- **OKE derives:** reads and writes are recorded through `fx`, so invalidation
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
- ### The secret that fails in prod
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
- **OKE derives:** [Vault](/docs/elements/vault) contracts declare the need in
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
- **OKE derives:** the official `okengine/plugins` set ships this glue once —
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
- **OKE derives:** its operator interface reads the Manifest —
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
- ### The permission check in the wrong place
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
- **OKE derives:** declared effects produce a least-privilege matrix — widening
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
- **OKE derives:** drivers are named after protocols (`postgres`, `redis`,
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
- ## The tax is drift
55
+ ---
74
56
 
75
- Every seam above is the same shape: a hand-maintained copy of knowledge the
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
- <DriftBoard />
59
+ ## One Manifest feeds every surface
79
60
 
80
- On the left, versions scatter and stay scattered. On the right, one Manifest
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
- ## The answer's shape
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
- Forty infrastructure concerns collapse into eight elementseach kept only
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
- <CollapseBoard />
73
+ ## The `fx` rule — one door to the world
90
74
 
91
- New infrastructure becomes a **driver** for an existing element, never a
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
- ## Traditional vs OKE
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
- | Seam | Maintained by hand | Derived by OKE |
97
- | ------------------ | --------------------------------------------------------- | ----------------------------------------------------------------------- |
98
- | Behavior model | Endpoints, jobs, consumers, workflows as separate species | One species Flow: `on(Trigger) → Effects` |
99
- | Cache invalidation | Hand-written keys; drift from writers | Derived from effects recorded through `fx` |
100
- | HTTP glue | Middleware copied per repo | Official plugins — `headers`, `cors`, `csrf`, compression, IP allowlist |
101
- | Secrets / config | Env sprawl; fails on first request | Vault contracts; `VaultBootError` at boot |
102
- | Observability | Bolted on; separate source of truth | Console from the Manifest, dev and prod (`:6533`) |
103
- | Permissions | Ad-hoc checks scattered in handlers | Least-privilege matrix from declared effects |
104
- | Local vs prod | Vendor clients; one-off compose | Protocol drivers; vendor in `images`; `oke dev` |
105
- | Client / agents | Separate codegen or hand-kept schemas | Typed client and MCP (`:6535`) from the same Manifest |
82
+ | Inferred from `fx` | What it gives you |
83
+ | ----------------------- | ------------------------------------ |
84
+ | Cache invalidation keys | Automaticfollows 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
- | | Statement |
110
- | --------------- | ------------------------------------------------------------------------------------------------ |
111
- | **Ambition** | The default, most capable TypeScript backend — Bun-first, Web-Standards portable, contract-first |
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>