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.
Files changed (209) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +14 -10
  3. package/manifest.v1.schema.json +61 -2
  4. package/package.json +19 -5
  5. package/site/content/docs/ai/index.mdx +1 -1
  6. package/site/content/docs/ai/mcp.mdx +10 -1
  7. package/site/content/docs/ai/skills.mdx +9 -9
  8. package/site/content/docs/elements/ai.mdx +1 -1
  9. package/site/content/docs/elements/clock.mdx +1 -1
  10. package/site/content/docs/elements/flow.mdx +25 -1
  11. package/site/content/docs/elements/gate.mdx +3 -2
  12. package/site/content/docs/elements/store.mdx +289 -341
  13. package/site/content/docs/elements/vault.mdx +5 -5
  14. package/site/content/docs/get-started/basic-usage.mdx +3 -10
  15. package/site/content/docs/get-started/index.mdx +1 -1
  16. package/site/content/docs/get-started/installation.mdx +2 -3
  17. package/site/content/docs/get-started/introduction.mdx +58 -121
  18. package/site/content/docs/get-started/meta.json +9 -1
  19. package/site/content/docs/get-started/project-structure.mdx +4 -11
  20. package/site/content/docs/get-started/testing.mdx +328 -0
  21. package/site/content/docs/get-started/why.mdx +93 -71
  22. package/site/content/docs/index.mdx +44 -11
  23. package/site/content/docs/meta.json +8 -5
  24. package/site/content/docs/plugins/apple.mdx +151 -0
  25. package/site/content/docs/plugins/discord.mdx +139 -0
  26. package/site/content/docs/plugins/facebook.mdx +134 -0
  27. package/site/content/docs/plugins/figma.mdx +138 -0
  28. package/site/content/docs/plugins/github.mdx +138 -0
  29. package/site/content/docs/plugins/google.mdx +153 -0
  30. package/site/content/docs/plugins/index.mdx +47 -1
  31. package/site/content/docs/plugins/meta.json +10 -0
  32. package/site/content/docs/plugins/microsoft.mdx +151 -0
  33. package/site/content/docs/plugins/oauth.mdx +188 -0
  34. package/site/content/docs/plugins/x.mdx +125 -0
  35. package/site/content/docs/providers/index.mdx +2 -0
  36. package/site/content/docs/recipes/index.mdx +2 -0
  37. package/site/content/docs/reference/cli.md +3 -2
  38. package/site/content/docs/reference/client.mdx +58 -1
  39. package/site/content/docs/reference/configuration.mdx +2 -4
  40. package/site/content/docs/reference/fx.mdx +3 -1
  41. package/site/content/docs/reference/index.mdx +0 -5
  42. package/site/content/docs/reference/meta.json +2 -2
  43. package/site/content/docs/reference/okid.mdx +137 -0
  44. package/src/auth/bindings.ts +1 -1
  45. package/src/auth/config.ts +9 -0
  46. package/src/auth/identity-sql.ts +314 -0
  47. package/src/auth/identity.ts +140 -2
  48. package/src/auth/index.ts +17 -1
  49. package/src/auth/method-context.ts +3 -0
  50. package/src/auth/oauth-as/cimd.ts +132 -0
  51. package/src/auth/oauth-as/crypto.test.ts +101 -0
  52. package/src/auth/oauth-as/crypto.ts +393 -0
  53. package/src/auth/oauth-as/errors.ts +68 -0
  54. package/src/auth/oauth-as/http.test.ts +419 -0
  55. package/src/auth/oauth-as/http.ts +842 -0
  56. package/src/auth/oauth-as/stores.ts +61 -0
  57. package/src/auth/oauth-as/tables.ts +142 -0
  58. package/src/auth/tables.ts +0 -11
  59. package/src/bench/README.md +83 -0
  60. package/src/bench/REPORT.md +176 -0
  61. package/src/bench/g01-rls-stamp.bench.ts +194 -0
  62. package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
  63. package/src/bench/g03-signal-once.bench.ts +157 -0
  64. package/src/bench/g03-signal-reconnect.bench.ts +254 -0
  65. package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
  66. package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
  67. package/src/bench/g05-sustained-full.bench.ts +265 -0
  68. package/src/bench/g06-mixed-load.bench.ts +260 -0
  69. package/src/bench/g07-vault-crypto.bench.ts +100 -0
  70. package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
  71. package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
  72. package/src/bench/g08-store-kv-durable.bench.ts +133 -0
  73. package/src/bench/g08-store-sql.bench.ts +178 -0
  74. package/src/bench/g09-journal-sustained.bench.ts +203 -0
  75. package/src/bench/g10-observability-contention.bench.ts +246 -0
  76. package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
  77. package/src/bench/g13-elements.bench.ts +427 -0
  78. package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
  79. package/src/bench/g15-postgres-degradation.bench.ts +264 -0
  80. package/src/bench/g16-live-query-fanout.bench.ts +206 -0
  81. package/src/bench/lib/event-loop-lag.ts +26 -0
  82. package/src/bench/lib/infra.ts +60 -0
  83. package/src/bench/lib/report.ts +52 -0
  84. package/src/bench/lib/rss-sampler.ts +61 -0
  85. package/src/bench/lib/signal-pg.ts +88 -0
  86. package/src/bench/load-app.ts +337 -0
  87. package/src/bench/load-child.ts +108 -0
  88. package/src/bench/smoke.bench.ts +43 -0
  89. package/src/cli/competitor-mention-removal.test.ts +28 -0
  90. package/src/cli/doctor-fd.ts +117 -0
  91. package/src/cli/doctor.test.ts +192 -0
  92. package/src/cli/doctor.ts +129 -1
  93. package/src/client/create.ts +95 -1
  94. package/src/client/index.ts +9 -2
  95. package/src/client/transport.ts +11 -4
  96. package/src/client/use-live-query.ts +154 -0
  97. package/src/client-react/index.ts +15 -1
  98. package/src/client-react/live-resource.ts +246 -0
  99. package/src/client-react/use-live-query.test.ts +475 -0
  100. package/src/client-react/use-live-query.ts +530 -0
  101. package/src/compiler/extract.test.ts +518 -0
  102. package/src/compiler/extract.ts +386 -19
  103. package/src/console/server/invoke-user-flow.ts +2 -1
  104. package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-De7Lc2JC.js} +1 -1
  105. package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
  106. package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
  107. package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.js} +1 -1
  108. package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
  109. package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-4rHOePuE.js} +1 -1
  110. package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.js} +1 -1
  111. package/src/console/ui-next/dist/index.html +1 -1
  112. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
  113. package/src/drivers/cdc-outbox.ts +389 -0
  114. package/src/drivers/memory.ts +20 -0
  115. package/src/drivers/oauth-apple.ts +156 -0
  116. package/src/drivers/oauth-discord.ts +79 -0
  117. package/src/drivers/oauth-facebook.ts +80 -0
  118. package/src/drivers/oauth-figma.ts +116 -0
  119. package/src/drivers/oauth-github.ts +92 -0
  120. package/src/drivers/oauth-google.ts +142 -0
  121. package/src/drivers/oauth-microsoft.ts +174 -0
  122. package/src/drivers/oauth-oidc.ts +293 -0
  123. package/src/drivers/oauth-shared.ts +326 -0
  124. package/src/drivers/oauth-types.ts +159 -0
  125. package/src/drivers/oauth-x.ts +77 -0
  126. package/src/drivers/oauth2-common.ts +95 -0
  127. package/src/drivers/oauth2-token.ts +61 -0
  128. package/src/drivers/pg-rls-row-passes.ts +251 -0
  129. package/src/drivers/pg-rls.ts +2 -0
  130. package/src/drivers/postgres.ts +45 -2
  131. package/src/drivers/signal-postgres.ts +2 -1
  132. package/src/elements/channel/runtime.ts +29 -2
  133. package/src/elements/channel.test.ts +52 -0
  134. package/src/elements/gate/boot.ts +29 -2
  135. package/src/elements/store/emit-drizzle.ts +147 -14
  136. package/src/elements/store/field-ddl.test.ts +118 -0
  137. package/src/elements/store/field-types.test.ts +455 -0
  138. package/src/elements/store/list-query.golden.json +777 -0
  139. package/src/elements/store/list-query.parity.test.ts +396 -0
  140. package/src/elements/store/list-query.ts +792 -0
  141. package/src/elements/store/live-default.test.ts +136 -0
  142. package/src/elements/store/live-http.test.ts +160 -0
  143. package/src/elements/store/live-isolation.test.ts +291 -0
  144. package/src/elements/store/live-query-runtime.test.ts +323 -0
  145. package/src/elements/store/live-query-runtime.ts +403 -0
  146. package/src/elements/store/live-query-server.test.ts +377 -0
  147. package/src/elements/store/live-query-server.ts +102 -0
  148. package/src/elements/store/live-query.ts +97 -0
  149. package/src/elements/store/resource.ts +189 -680
  150. package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
  151. package/src/elements/store/schema-decl.ts +539 -41
  152. package/src/elements/store/sql-rls-stamp.test.ts +27 -0
  153. package/src/elements/store/sql-session.ts +297 -35
  154. package/src/elements/store/table.ts +102 -21
  155. package/src/elements/store.test.ts +3 -1
  156. package/src/elements/store.ts +12 -1
  157. package/src/elements/vault/chaos-child.ts +74 -1
  158. package/src/elements/vault/chaos.test.ts +4 -2
  159. package/src/elements/vault/storage.ts +4 -2
  160. package/src/index.ts +5 -2
  161. package/src/kernel/app-auth.ts +1 -0
  162. package/src/kernel/app.ts +116 -2
  163. package/src/kernel/auth-sharing.test.ts +196 -0
  164. package/src/kernel/boot.test.ts +3 -3
  165. package/src/kernel/errors.ts +8 -0
  166. package/src/kernel/fx.test.ts +1 -0
  167. package/src/kernel/fx.ts +14 -2
  168. package/src/kernel/horizontal-child.ts +2 -1
  169. package/src/kernel/http-resource.ts +33 -7
  170. package/src/kernel/identity-host-persist.test.ts +119 -0
  171. package/src/kernel/instance-id.ts +4 -2
  172. package/src/kernel/journal.ts +2 -1
  173. package/src/kernel/mcp-tool.test.ts +95 -0
  174. package/src/kernel/on.ts +9 -0
  175. package/src/kernel/realtime-bind.ts +326 -0
  176. package/src/kernel/resource-live.ts +117 -0
  177. package/src/kernel/triggers.ts +86 -4
  178. package/src/manifest/diff.ts +37 -0
  179. package/src/manifest/types.ts +64 -2
  180. package/src/okid.bench.test.ts +64 -0
  181. package/src/okid.test.ts +338 -0
  182. package/src/okid.ts +245 -0
  183. package/src/plugins/anonymous.ts +19 -1
  184. package/src/plugins/auth/shared.ts +15 -0
  185. package/src/plugins/index.ts +2 -0
  186. package/src/plugins/magic-link.ts +10 -8
  187. package/src/plugins/mcp-oauth.ts +208 -0
  188. package/src/plugins/oauth/flow-store.ts +117 -0
  189. package/src/plugins/oauth/link.ts +69 -0
  190. package/src/plugins/oauth/shared.ts +108 -0
  191. package/src/plugins/oauth/token-vault.ts +100 -0
  192. package/src/plugins/oauth.security.test.ts +535 -0
  193. package/src/plugins/oauth.ts +532 -0
  194. package/src/plugins/otp.ts +48 -6
  195. package/src/plugins/passkey.ts +20 -1
  196. package/src/plugins/two-factor.ts +11 -0
  197. package/src/plugins/username.ts +40 -7
  198. package/src/release/build-lib.ts +7 -1
  199. package/src/release/measure.ts +1 -0
  200. package/src/release/official-plugins.ts +4 -1
  201. package/src/runs/collect.ts +2 -1
  202. package/src/runs/drivers/files.ts +2 -1
  203. package/src/test/create-test-app.ts +114 -5
  204. package/src/test/export-bundle.test.ts +33 -0
  205. package/src/test/live-signals.test.ts +83 -0
  206. package/src/test/tenant-isolation.test.ts +175 -0
  207. package/src/testing.ts +26 -0
  208. package/src/upgrade/codemods.ts +1 -1
  209. package/site/content/docs/reference/migrating-environments.mdx +0 -158
@@ -0,0 +1,153 @@
1
+ ---
2
+ title: "Google"
3
+ description: "Official plugin — Google sign-in via OIDC discovery and JWKS-verified ID tokens."
4
+ icon: "Globe"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ Google is the reference OIDC provider for `oauth()`: discovery, ID-token
9
+ signature checks, and issuer pinning all work the textbook way. If you are
10
+ wiring your first social provider, start here.
11
+
12
+ <Callout title="The one rule">
13
+ Register `https://app.example.com/auth/oauth/callback/google` (your exact origin) as an Authorized
14
+ Redirect URI, then enable it with `providers.google.enabled`. The comparison is byte-exact.
15
+ </Callout>
16
+
17
+ ## Quick start
18
+
19
+ <Steps>
20
+
21
+ <Step>
22
+ ### Create OAuth credentials
23
+
24
+ In Google Cloud Console → APIs & Services → Credentials, create an **OAuth
25
+ client ID** of type _Web application_. Add your callback URI under Authorized
26
+ Redirect URIs.
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
+ google: { 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_GOOGLE_CLIENT_SECRET=GOCSPX-...
59
+ ```
60
+
61
+ Boot fails loudly if the secret is missing — the contract is declared by the
62
+ plugin itself.
63
+
64
+ </Step>
65
+
66
+ </Steps>
67
+
68
+ ## How identity works
69
+
70
+ The driver discovers Google's endpoints from
71
+ `https://accounts.google.com/.well-known/openid-configuration` and caches them.
72
+ On callback it verifies the ID token end-to-end:
73
+
74
+ | Check | Rule |
75
+ | --------- | ------------------------------------------------------- |
76
+ | Signature | RS256 / ES256 against Google's published JWKS |
77
+ | Issuer | must equal `https://accounts.google.com` |
78
+ | Audience | must include your client id (`azp` when multi-audience) |
79
+ | Expiry | rejected when stale |
80
+ | Nonce | single-use, bound to the flow row |
81
+
82
+ Email trust follows the OIDC claim: `email_verified: true` marks the address
83
+ verified; anything else — including string `"false"` — stays unverified.
84
+
85
+ Default scopes: `openid`, `email`, `profile`.
86
+
87
+ ## Options
88
+
89
+ | Option | Type | Default | Meaning |
90
+ | --------------------- | ---------- | ------------------- | -------------------------- |
91
+ | `enabled` | `boolean` | `false` | Turn the provider on |
92
+ | `clientId` | `string` | Vault/env\* | `\*OAUTH_GOOGLE_CLIENT_ID` |
93
+ | `redirectUri` | `string` | `{baseUrl}…/google` | Exact registered URI |
94
+ | `scopes` | `string[]` | driver defaults | Extra scopes |
95
+ | `storeProviderTokens` | `boolean` | `false` | Keep tokens in Vault |
96
+
97
+ ## Surfaces
98
+
99
+ | Flow | Path |
100
+ | -------- | -------------------------------------- |
101
+ | Start | `POST /auth/oauth/google/start` |
102
+ | Callback | `GET+POST /auth/oauth/callback/google` |
103
+ | Link | `POST /auth/oauth/google/link` |
104
+
105
+ ## Troubleshooting
106
+
107
+ <Accordions>
108
+ <Accordion title="redirect_uri_mismatch at Google">
109
+
110
+ The registered URI and the sent URI differ. Byte-exact means scheme, host,
111
+ port, path, no trailing slash drift. Copy the value from
112
+ `providers.google.redirectUri` or `baseUrl` synthesis.
113
+
114
+ </Accordion>
115
+ <Accordion title="invalid_client at token exchange">
116
+
117
+ `OAUTH_GOOGLE_CLIENT_SECRET` is missing or stale. Secrets resolve through the
118
+ Vault chain — update `.env.local` or your Vault driver, then restart.
119
+
120
+ </Accordion>
121
+ <Accordion title="aud_mismatch">
122
+
123
+ You are using credentials from a different Google project than the one that
124
+ issued the code, or swapped client ids between environments.
125
+
126
+ </Accordion>
127
+ </Accordions>
128
+
129
+ ## Learn more
130
+
131
+ - [OAuth](/docs/plugins/oauth) — shared flows and security model
132
+ - [Vault](/docs/elements/vault) — where secrets live
133
+ - [Gate](/docs/elements/gate) — `gate.auth`
134
+
135
+ ## Next
136
+
137
+ <Cards>
138
+ <Card
139
+ title="Apple"
140
+ description="form_post + ES256 client-secret JWT."
141
+ href="/docs/plugins/apple"
142
+ />
143
+ <Card
144
+ title="Microsoft"
145
+ description="Entra tenants and issuer templates."
146
+ href="/docs/plugins/microsoft"
147
+ />
148
+ <Card
149
+ title="GitHub"
150
+ description="OAuth2 with verified-email lookup."
151
+ href="/docs/plugins/github"
152
+ />
153
+ </Cards>
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "Plugins"
3
- description: "Official okengine/plugins extensions — authentication, security, operations, and performance."
3
+ description: "Official okengine/plugins extensions — authentication, OAuth, security, operations, and performance."
4
4
  icon: "Puzzle"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
@@ -22,6 +22,52 @@ First-party plugins you `.plug()` onto an app. Each page is one export from `oke
22
22
  <Card title="Passkey" description="WebAuthn-shaped passkeys." href="/docs/plugins/passkey" />
23
23
  </Cards>
24
24
 
25
+ ## OAuth
26
+
27
+ <Cards>
28
+ <Card
29
+ title="OAuth"
30
+ description="Social sign-in — Authorization Code + PKCE for eight providers."
31
+ href="/docs/plugins/oauth"
32
+ />
33
+ <Card
34
+ title="Apple"
35
+ description="Sign in with Apple — form_post + ES256 JWT."
36
+ href="/docs/plugins/apple"
37
+ />
38
+ <Card
39
+ title="Discord"
40
+ description="Discord OAuth2 with nullable email."
41
+ href="/docs/plugins/discord"
42
+ />
43
+ <Card
44
+ title="Facebook"
45
+ description="Facebook Login — never-verified emails."
46
+ href="/docs/plugins/facebook"
47
+ />
48
+ <Card
49
+ title="Figma"
50
+ description="Figma OAuth2 with HTTP Basic token auth."
51
+ href="/docs/plugins/figma"
52
+ />
53
+ <Card
54
+ title="GitHub"
55
+ description="GitHub OAuth2 with verified-email lookup."
56
+ href="/docs/plugins/github"
57
+ />
58
+ <Card
59
+ title="Google"
60
+ description="Google OIDC — JWKS-verified ID tokens."
61
+ href="/docs/plugins/google"
62
+ />
63
+ <Card
64
+ title="Microsoft"
65
+ description="Entra ID OIDC with tenant-aware issuer checks."
66
+ href="/docs/plugins/microsoft"
67
+ />
68
+ <Card title="X" description="X OAuth2 public client — PKCE only." href="/docs/plugins/x" />
69
+ </Cards>
70
+
25
71
  ## Security
26
72
 
27
73
  <Cards>
@@ -10,6 +10,16 @@
10
10
  "otp",
11
11
  "two-factor",
12
12
  "passkey",
13
+ "---OAuth---",
14
+ "oauth",
15
+ "apple",
16
+ "discord",
17
+ "facebook",
18
+ "figma",
19
+ "github",
20
+ "google",
21
+ "microsoft",
22
+ "x",
13
23
  "---Security---",
14
24
  "headers",
15
25
  "cors",
@@ -0,0 +1,151 @@
1
+ ---
2
+ title: "Microsoft"
3
+ description: "Official plugin — Entra ID sign-in across personal, work, and school accounts with tenant-aware issuer checks."
4
+ icon: "LayoutGrid"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ Microsoft (Entra ID) is OIDC with a tenant twist: `common` discovery advertises
9
+ an issuer with the `{tenantid}` placeholder, so the value you pin at start is
10
+ a _template_ validated against the concrete token at callback.
11
+
12
+ <Callout title="The one rule">
13
+ Pick the audience first: `tenant: "common"` for everyone, `organizations` for work/school only,
14
+ `consumers` for personal accounts, or a tenant GUID to lock sign-in to one directory.
15
+ </Callout>
16
+
17
+ ## Quick start
18
+
19
+ <Steps>
20
+
21
+ <Step>
22
+ ### Register an app
23
+
24
+ In the Azure portal → Microsoft Entra ID → App registrations → New
25
+ registration. Choose _Accounts in any organizational directory and personal
26
+ Microsoft accounts_ for `common`. Add your redirect URI under **Web**.
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
+ microsoft: { enabled: true }, // tenant defaults to "common"
46
+ },
47
+ }),
48
+ );
49
+ ```
50
+
51
+ </Step>
52
+
53
+ <Step>
54
+ ### Set the client secret
55
+
56
+ ```text
57
+ # .env.local
58
+ OAUTH_MICROSOFT_CLIENT_SECRET=...
59
+ ```
60
+
61
+ Certificates are not used — the driver authenticates with the shared secret
62
+ form field.
63
+
64
+ </Step>
65
+
66
+ </Steps>
67
+
68
+ ## How identity works
69
+
70
+ | Aspect | Behavior |
71
+ | -------------- | ----------------------------------------------------------------------------------------------------- |
72
+ | Discovery | `{tenant}/oauth2/v2.0/.well-known/openid-configuration`, cached |
73
+ | Issuer pinning | flow stores `https://login.microsoftonline.com/{tenantid}/v2.0`; tokens must match it shape-for-shape |
74
+ | Tenant claim | the token's `tid` must be GUID-shaped and consistent with `iss` |
75
+ | Signature | RS256 against the tenant's published keys |
76
+ | Email trust | OIDC `email_verified` claim |
77
+
78
+ **Consequence:** an ID token minted by a _different_ tenant fails the issuer
79
+ template even though its signature is perfectly valid — the mix-up defense
80
+ survives multi-tenancy.
81
+
82
+ Default scopes: `openid`, `email`, `profile`.
83
+
84
+ ## Options
85
+
86
+ | Option | Type | Default | Meaning |
87
+ | --------------------- | ---------- | ---------------------- | --------------------------------------- |
88
+ | `enabled` | `boolean` | `false` | Turn the provider on |
89
+ | `clientId` | `string` | Vault/env\* | `\*OAUTH_MICROSOFT_CLIENT_ID` |
90
+ | `tenant` | `string` | `"common"` | `organizations`, `consumers`, or a GUID |
91
+ | `redirectUri` | `string` | `{baseUrl}…/microsoft` | Exact registered URI |
92
+ | `scopes` | `string[]` | driver defaults | Extra scopes |
93
+ | `storeProviderTokens` | `boolean` | `false` | Keep tokens in Vault |
94
+
95
+ ## Surfaces
96
+
97
+ | Flow | Path |
98
+ | -------- | ----------------------------------------- |
99
+ | Start | `POST /auth/oauth/microsoft/start` |
100
+ | Callback | `GET+POST /auth/oauth/callback/microsoft` |
101
+ | Link | `POST /auth/oauth/microsoft/link` |
102
+
103
+ ## Troubleshooting
104
+
105
+ <Accordions>
106
+ <Accordion title="issuer_mismatch on every login">
107
+
108
+ Your app registration's _supported account types_ disagree with the configured
109
+ `tenant`. `common` requires multi-tenant + personal accounts; a single-tenant
110
+ GUID needs the matching registration.
111
+
112
+ </Accordion>
113
+ <Accordion title="AADSTS50011 (redirect mismatch)">
114
+
115
+ The reply URL registered in Azure does not byte-match the stored
116
+ `redirectUri`. Re-copy from your plugin config; trailing slashes count.
117
+
118
+ </Accordion>
119
+ <Accordion title="No email arrives">
120
+
121
+ Personal Microsoft accounts can omit email entirely. The flow signs the person
122
+ in without one — exactly like Discord phone-only accounts.
123
+
124
+ </Accordion>
125
+ </Accordions>
126
+
127
+ ## Learn more
128
+
129
+ - [OAuth](/docs/plugins/oauth) — shared flows and security model
130
+ - [Google](/docs/plugins/google) — plain single-issuer OIDC
131
+ - [Vault](/docs/elements/vault) — where secrets live
132
+
133
+ ## Next
134
+
135
+ <Cards>
136
+ <Card
137
+ title="Apple"
138
+ description="form_post + ES256 client-secret JWT."
139
+ href="/docs/plugins/apple"
140
+ />
141
+ <Card
142
+ title="Discord"
143
+ description="Nullable emails, verified flag."
144
+ href="/docs/plugins/discord"
145
+ />
146
+ <Card
147
+ title="GitHub"
148
+ description="OAuth2 with verified-email lookup."
149
+ href="/docs/plugins/github"
150
+ />
151
+ </Cards>
@@ -0,0 +1,188 @@
1
+ ---
2
+ title: "OAuth"
3
+ description: "Official plugin — social sign-in with eight providers under /auth, Authorization Code + PKCE only."
4
+ icon: "FingerprintPattern"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ `oauth()` adds social login to Gate auth. Start at
9
+ `/auth/oauth/{provider}/start`, approve, then land on
10
+ `/auth/oauth/callback/{provider}` with a session — eight providers.
11
+
12
+ <Callout title="The one rule">
13
+ Enable `gate.auth`, then `.plug(oauth({ providers: { ... } }))`. Every
14
+ provider runs Authorization Code + PKCE with an exact registered redirect URI
15
+ — implicit and password grants do not exist here.
16
+ </Callout>
17
+
18
+ ## Quick start
19
+
20
+ <Steps>
21
+
22
+ <Step>
23
+ ### Plug it
24
+
25
+ ```typescript title="src/app.ts"
26
+ import { oke } from "okengine";
27
+ import { oauth } from "okengine/plugins";
28
+
29
+ export const app = oke({
30
+ name: "shop",
31
+ env: "dev",
32
+ gate: { auth: {} },
33
+ }).plug(
34
+ oauth({
35
+ baseUrl: "https://app.example.com",
36
+ providers: {
37
+ google: { enabled: true },
38
+ github: { enabled: true },
39
+ },
40
+ }),
41
+ );
42
+ ```
43
+
44
+ `baseUrl` synthesizes `{baseUrl}/auth/oauth/callback/{provider}` — register
45
+ that exact string. Client secrets resolve from Vault
46
+ (`OAUTH_GOOGLE_CLIENT_SECRET`, `OAUTH_GITHUB_CLIENT_SECRET`, …).
47
+
48
+ </Step>
49
+
50
+ <Step>
51
+ ### Start the flow
52
+
53
+ ```typescript
54
+ const { data } = await api.auth.oauthStart({ provider: "google" });
55
+ // redirect the browser to data.authorizationUrl
56
+ ```
57
+
58
+ The response carries `authorizationUrl`, `expiresInMs`, and the provider echo.
59
+ The flow row (state hash, PKCE verifier, nonce) lives for ten minutes.
60
+
61
+ </Step>
62
+
63
+ <Step>
64
+ ### Callback issues a session
65
+
66
+ The provider redirects to your callback route; the flow verifies state,
67
+ exchanges the code, and signs the person in:
68
+
69
+ ```typescript
70
+ const body = {
71
+ accessToken: "...",
72
+ refreshToken: "...",
73
+ userId: "usr_...",
74
+ };
75
+ ```
76
+
77
+ A brand-new visitor gets a fresh account. Someone whose email already belongs
78
+ to another credential is refused with `email_in_use`.
79
+
80
+ </Step>
81
+
82
+ </Steps>
83
+
84
+ ## Providers
85
+
86
+ | Provider | Shape | Identity source | Email verified |
87
+ | --------- | ------ | ------------------------ | ------------------------------------ |
88
+ | Google | OIDC | JWKS-verified ID token | `email_verified` claim |
89
+ | Apple | OIDC | JWKS-verified ID token | strict parse — `"false"` stays false |
90
+ | Microsoft | OIDC | JWKS-verified ID token | `email_verified` claim |
91
+ | GitHub | OAuth2 | `/user` + `/user/emails` | primary `verified` flag |
92
+ | Discord | OAuth2 | `/users/@me` | `verified` flag |
93
+ | X | OAuth2 | `/2/users/me` | never — no trustworthy signal |
94
+ | Facebook | OAuth2 | Graph `/me` | never — no trustworthy signal |
95
+ | Figma | OAuth2 | `/v1/me` | never — no verification field |
96
+
97
+ Each provider has its own page under this category — endpoints, scopes, setup,
98
+ and trust rules.
99
+
100
+ ## Security model
101
+
102
+ | Threat | Defense |
103
+ | ---------------------------------- | ----------------------------------------------------------------------- |
104
+ | Code interception | PKCE S256 on every provider |
105
+ | CSRF / forged callbacks | single-use `state`, SHA-256-hashed at rest |
106
+ | Mix-up across providers (RFC 9700) | per-provider callback routes + issuer pinning on every assertion |
107
+ | Unverified-email takeover | `linkOrProvision` refuses `email_in_use` without an authenticated owner |
108
+ | Redirect manipulation | byte-exact stored `redirect_uri` at token exchange |
109
+
110
+ **Consequence:** an attacker who completes a social login claiming your email
111
+ gains nothing — the account stays untouched unless they already own a session
112
+ for it.
113
+
114
+ ## Options
115
+
116
+ | Option | Type | Default | Meaning |
117
+ | --------------- | ------------------- | -------- | ---------------------------------------- |
118
+ | `providers` | map | `{}` | Per-provider config, disabled by default |
119
+ | `baseUrl` | `string` | — | Origin for synthesized callback URIs |
120
+ | `secret` | `string` | active\* | HMAC secret (\*from `gate.auth`) |
121
+ | `sessions` | `SessionStore` | active\* | Session store |
122
+ | `identities` | `IdentityStore` | active\* | Shared user store |
123
+ | `verifications` | `VerificationStore` | new | Flow-state store |
124
+ | `fetch` | `typeof fetch` | global | Injectable transport (tests) |
125
+
126
+ Per-provider config: `enabled`, `clientId`, `redirectUri`, `scopes`,
127
+ `storeProviderTokens`, plus Microsoft `tenant` and Apple `teamId` / `keyId`.
128
+
129
+ ## Surfaces
130
+
131
+ | Flow | Path | Gate |
132
+ | --------------------- | ------------------------------------------ | ------------------------ |
133
+ | `auth.oauthStart` | `POST /auth/oauth/{provider}/start` | `gate.public` + otp rate |
134
+ | `auth.oauthCallback` | `GET+POST /auth/oauth/callback/{provider}` | `gate.public` + otp rate |
135
+ | `auth.oauthLinkStart` | `POST /auth/oauth/{provider}/link` | session + bearer |
136
+
137
+ The GET + POST callback pair exists because Apple posts its response instead of
138
+ redirecting.
139
+
140
+ ## Troubleshooting
141
+
142
+ <Accordions>
143
+ <Accordion title="Boot fails listing OAUTH_…_CLIENT_SECRET">
144
+
145
+ Every enabled provider needs its secret in Vault before boot. Seed
146
+ `OAUTH_{PROVIDER}_CLIENT_SECRET` (Apple uses `OAUTH_APPLE_PRIVATE_KEY`) through
147
+ your Vault driver or `.env.local`.
148
+
149
+ </Accordion>
150
+ <Accordion title="Callback returns invalid_state">
151
+
152
+ The state was consumed, expired (ten-minute TTL), or minted by a different
153
+ provider's start call. Restart from `/start`.
154
+
155
+ </Accordion>
156
+ <Accordion title="Callback returns issuer_mismatch">
157
+
158
+ The ID token's issuer does not match the provider that started the flow — the
159
+ signature was valid but the token came from elsewhere. This rejection is the
160
+ mix-up defense working; retry the real provider.
161
+
162
+ </Accordion>
163
+ <Accordion title="Callback returns email_in_use">
164
+
165
+ The provider returned an email already registered to another account without
166
+ proof you own that account. Sign in with the original method first, then link
167
+ via `POST /auth/oauth/{provider}/link`.
168
+
169
+ </Accordion>
170
+ </Accordions>
171
+
172
+ ## Learn more
173
+
174
+ - [Gate](/docs/elements/gate) — `gate.auth`
175
+ - [Passkey](/docs/plugins/passkey) — phishing-resistant alternative
176
+ - [Client](/docs/reference/client) — calling `/auth` from the browser
177
+
178
+ ## Next
179
+
180
+ <Cards>
181
+ <Card title="Google" description="OIDC reference provider." href="/docs/plugins/google" />
182
+ <Card
183
+ title="GitHub"
184
+ description="OAuth2 with verified-email lookup."
185
+ href="/docs/plugins/github"
186
+ />
187
+ <Card title="Passkey" description="WebAuthn-shaped passkeys." href="/docs/plugins/passkey" />
188
+ </Cards>
@@ -0,0 +1,125 @@
1
+ ---
2
+ title: "X"
3
+ description: "Official plugin — X (Twitter) sign-in as a PKCE public client with confidential-client secrets left out."
4
+ icon: "X"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ X is the one provider where `oauth()` behaves as a **public client**: the
9
+ token exchange carries the code verifier but no secret. PKCE is the only
10
+ proof the callback is yours.
11
+
12
+ <Callout title="The one rule">
13
+ Enable OAuth 2.0 in the X Developer Portal, set up your exact callback URI, and request
14
+ `users.email` access if you need addresses. No `OAUTH_X_CLIENT_SECRET` exists — none is read.
15
+ </Callout>
16
+
17
+ ## Quick start
18
+
19
+ <Steps>
20
+
21
+ <Step>
22
+ ### Configure the app
23
+
24
+ X Developer Portal → Project → User authentication settings → **Set up**.
25
+ Choose _Web App_, enable OAuth 2.0, and add
26
+ `https://app.example.com/auth/oauth/callback/x` as a callback URI.
27
+
28
+ </Step>
29
+
30
+ <Step>
31
+ ### Plug it
32
+
33
+ ```typescript title="src/app.ts"
34
+ import { oke } from "okengine";
35
+ import { oauth } from "okengine/plugins";
36
+
37
+ export const app = oke({
38
+ name: "shop",
39
+ env: "dev",
40
+ gate: { auth: {} },
41
+ }).plug(
42
+ oauth({
43
+ baseUrl: "https://app.example.com",
44
+ providers: {
45
+ x: { enabled: true },
46
+ },
47
+ }),
48
+ );
49
+ ```
50
+
51
+ </Step>
52
+
53
+ </Steps>
54
+
55
+ That is the whole setup — no Vault contract for this provider.
56
+
57
+ ## How identity works
58
+
59
+ | Aspect | Behavior |
60
+ | -------------- | ------------------------------------------------------------------------------------------- |
61
+ | Token exchange | `POST https://api.x.com/2/oauth2/token` with the code verifier, no secret |
62
+ | Profile | `GET /2/users/me?user.fields=id,name,username,confirmed_email` |
63
+ | Subject | the string `id` inside `data` |
64
+ | Email trust | **always unverified** — `confirmed_email` gates API access, it does not attest verification |
65
+
66
+ **Consequence:** an X email never claims an existing account during sign-up.
67
+ It can attach to a fresh account or to an existing one you already control via
68
+ authenticated linking.
69
+
70
+ Default scopes: `users.read`, `tweet.read`.
71
+
72
+ ## Options
73
+
74
+ | Option | Type | Default | Meaning |
75
+ | --------------------- | ---------- | --------------- | ---------------------------------- |
76
+ | `enabled` | `boolean` | `false` | Turn the provider on |
77
+ | `clientId` | `string` | Vault/env\* | `\*OAUTH_X_CLIENT_ID` |
78
+ | `redirectUri` | `string` | `{baseUrl}…/x` | Exact registered URI |
79
+ | `scopes` | `string[]` | driver defaults | Extra scopes (`offline.access`, …) |
80
+ | `storeProviderTokens` | `boolean` | `false` | Keep tokens in Vault |
81
+
82
+ ## Surfaces
83
+
84
+ | Flow | Path |
85
+ | -------- | --------------------------------- |
86
+ | Start | `POST /auth/oauth/x/start` |
87
+ | Callback | `GET+POST /auth/oauth/callback/x` |
88
+ | Link | `POST /auth/oauth/x/link` |
89
+
90
+ ## Troubleshooting
91
+
92
+ <Accordions>
93
+ <Accordion title="invalid_request on token exchange">
94
+
95
+ Callback URIs must match exactly and the code must be fresh. X codes are
96
+ single-use and short-lived; flow rows expire after ten minutes too.
97
+
98
+ </Accordion>
99
+ <Accordion title="No email ever arrives">
100
+
101
+ `confirmed_email` requires elevated access plus the `users.email` scope. Even
102
+ then the address stays unverified by design — see the trust table above.
103
+
104
+ </Accordion>
105
+ <Accordion title="403 forbidden">
106
+
107
+ Your app tier lacks user-context authentication, or the requested scope set
108
+ exceeds what the portal grants.
109
+
110
+ </Accordion>
111
+ </Accordions>
112
+
113
+ ## Learn more
114
+
115
+ - [OAuth](/docs/plugins/oauth) — shared flows and security model
116
+ - [Facebook](/docs/plugins/facebook) — also never-verified emails
117
+ - [Vault](/docs/elements/vault) — optional token storage
118
+
119
+ ## Next
120
+
121
+ <Cards>
122
+ <Card title="Facebook" description="Never-verified emails." href="/docs/plugins/facebook" />
123
+ <Card title="Figma" description="Basic-auth token endpoint." href="/docs/plugins/figma" />
124
+ <Card title="Discord" description="Nullable emails." href="/docs/plugins/discord" />
125
+ </Cards>