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,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>
@@ -0,0 +1,138 @@
1
+ ---
2
+ title: "Figma"
3
+ description: "Official plugin — Figma sign-in with HTTP Basic client auth and no email verification signal."
4
+ icon: "PenTool"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ Figma is OAuth2 with two twists: the token endpoint uses **HTTP Basic**
9
+ instead of a body field, and the API exposes **no verification flag** — so
10
+ emails are always unverified.
11
+
12
+ <Callout title="The one rule">
13
+ Register your exact callback URI in the Figma app, seed the client secret, and expect unverified
14
+ emails — new accounts only, never takeovers.
15
+ </Callout>
16
+
17
+ ## Quick start
18
+
19
+ <Steps>
20
+
21
+ <Step>
22
+ ### Create an app
23
+
24
+ Figma → Settings → Security → **Personal access tokens / OAuth apps** → create
25
+ an OAuth app. Add `https://app.example.com/auth/oauth/callback/figma` as a
26
+ callback URL.
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
+ figma: { 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_FIGMA_CLIENT_SECRET=...
59
+ ```
60
+
61
+ The driver sends it as `Authorization: Basic base64(client_id:client_secret)`
62
+ on the token call.
63
+
64
+ </Step>
65
+
66
+ </Steps>
67
+
68
+ ## How identity works
69
+
70
+ | Aspect | Behavior |
71
+ | -------------- | --------------------------------------------------------------------------------- |
72
+ | Token exchange | `POST https://api.figma.com/v1/oauth/token` with the Basic header + PKCE verifier |
73
+ | Profile | `GET https://api.figma.com/v1/me` |
74
+ | Subject | `id`, falling back to `handle` when absent |
75
+ | Email trust | **always unverified** — no verification field exists |
76
+
77
+ Default scopes: `file_read`. Trim this to what you actually need; sign-in
78
+ itself requires nothing beyond defaults.
79
+
80
+ ## Options
81
+
82
+ | Option | Type | Default | Meaning |
83
+ | --------------------- | ---------- | ------------------ | ------------------------- |
84
+ | `enabled` | `boolean` | `false` | Turn the provider on |
85
+ | `clientId` | `string` | Vault/env\* | `\*OAUTH_FIGMA_CLIENT_ID` |
86
+ | `redirectUri` | `string` | `{baseUrl}…/figma` | Exact registered URI |
87
+ | `scopes` | `string[]` | driver defaults | Extra scopes |
88
+ | `storeProviderTokens` | `boolean` | `false` | Keep tokens in Vault |
89
+
90
+ ## Surfaces
91
+
92
+ | Flow | Path |
93
+ | -------- | ------------------------------------- |
94
+ | Start | `POST /auth/oauth/figma/start` |
95
+ | Callback | `GET+POST /auth/oauth/callback/figma` |
96
+ | Link | `POST /auth/oauth/figma/link` |
97
+
98
+ ## Troubleshooting
99
+
100
+ <Accordions>
101
+ <Accordion title="401 on token exchange">
102
+
103
+ The Basic header is built from _your_ pair — a mismatched id/secret rotation
104
+ or a secret seeded under the wrong provider key. Check
105
+ `OAUTH_FIGMA_CLIENT_SECRET`.
106
+
107
+ </Accordion>
108
+ <Accordion title="invalid_grant">
109
+
110
+ Codes are single-use and expire quickly. Flow rows are too — restart from
111
+ `/start`.
112
+
113
+ </Accordion>
114
+ <Accordion title="Email missing on the profile">
115
+
116
+ Some Figma accounts expose no email. The session signs in without one;
117
+ nothing else changes.
118
+
119
+ </Accordion>
120
+ </Accordions>
121
+
122
+ ## Learn more
123
+
124
+ - [OAuth](/docs/plugins/oauth) — shared flows and security model
125
+ - [GitHub](/docs/plugins/github) — OAuth2 with real verification signal
126
+ - [Vault](/docs/elements/vault) — where secrets live
127
+
128
+ ## Next
129
+
130
+ <Cards>
131
+ <Card title="Google" description="OIDC reference provider." href="/docs/plugins/google" />
132
+ <Card
133
+ title="GitHub"
134
+ description="OAuth2 with verified-email lookup."
135
+ href="/docs/plugins/github"
136
+ />
137
+ <Card title="X" description="PKCE public client." href="/docs/plugins/x" />
138
+ </Cards>
@@ -0,0 +1,138 @@
1
+ ---
2
+ title: "GitHub"
3
+ description: "Official plugin — GitHub sign-in with primary-email lookup from the emails API."
4
+ icon: "GitBranch"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ GitHub is OAuth2 without discovery or ID tokens, so `oauth()` builds the
9
+ identity assertion from two REST calls: `/user` for the account and
10
+ `/user/emails` for the address that actually matters.
11
+
12
+ <Callout title="The one rule">
13
+ Request the `user:email` scope. The public profile email is usually null; the verified primary
14
+ address only exists on the emails endpoint.
15
+ </Callout>
16
+
17
+ ## Quick start
18
+
19
+ <Steps>
20
+
21
+ <Step>
22
+ ### Create an OAuth App
23
+
24
+ GitHub → Settings → Developer settings → OAuth Apps → **New OAuth App**.
25
+ Authorization callback URL:
26
+ `https://app.example.com/auth/oauth/callback/github`.
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
+ github: { 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_GITHUB_CLIENT_SECRET=...
59
+ ```
60
+
61
+ </Step>
62
+
63
+ </Steps>
64
+
65
+ ## How identity works
66
+
67
+ | Aspect | Behavior |
68
+ | -------------- | ------------------------------------------------------------------------------ |
69
+ | Token exchange | form POST to `github.com/login/oauth/access_token`, JSON accepted |
70
+ | Profile | `GET https://api.github.com/user` (numeric `id` is the subject) |
71
+ | Email | `GET https://api.github.com/user/emails`; picks the entry with `primary: true` |
72
+ | Email trust | verified only when the selected entry has an explicit `verified: true` |
73
+
74
+ **Consequence:** a GitHub account whose primary email is unverified never
75
+ claims that address during sign-up — the flow provisions without verified
76
+ status instead of risking someone else's inbox.
77
+
78
+ Default scopes: `read:user`, `user:email`. PKCE parameters are sent; GitHub
79
+ ignores them but the protection stays uniform across providers.
80
+
81
+ ## Options
82
+
83
+ | Option | Type | Default | Meaning |
84
+ | --------------------- | ---------- | ------------------- | -------------------------- |
85
+ | `enabled` | `boolean` | `false` | Turn the provider on |
86
+ | `clientId` | `string` | Vault/env\* | `\*OAUTH_GITHUB_CLIENT_ID` |
87
+ | `redirectUri` | `string` | `{baseUrl}…/github` | 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/github/start` |
96
+ | Callback | `GET+POST /auth/oauth/callback/github` |
97
+ | Link | `POST /auth/oauth/github/link` |
98
+
99
+ ## Troubleshooting
100
+
101
+ <Accordions>
102
+ <Accordion title="Sign-in succeeds with no email">
103
+
104
+ The token lacks `user:email` (custom scopes dropped it) or the account has no
105
+ verified addresses. The session is valid; add the scope for future flows.
106
+
107
+ </Accordion>
108
+ <Accordion title="401 on /user/emails">
109
+
110
+ App permissions changed or the user revoked the grant under Settings →
111
+ Applications. The driver degrades to no email rather than failing sign-in.
112
+
113
+ </Accordion>
114
+ <Accordion title="redirect_uri mismatch">
115
+
116
+ GitHub compares the registered callback URL exactly. Re-copy the value your
117
+ config produces.
118
+
119
+ </Accordion>
120
+ </Accordions>
121
+
122
+ ## Learn more
123
+
124
+ - [OAuth](/docs/plugins/oauth) — shared flows and security model
125
+ - [Discord](/docs/plugins/discord) — same OAuth2 shape, nullable email
126
+ - [Vault](/docs/elements/vault) — where secrets live
127
+
128
+ ## Next
129
+
130
+ <Cards>
131
+ <Card
132
+ title="Discord"
133
+ description="Verified flag with nullable email."
134
+ href="/docs/plugins/discord"
135
+ />
136
+ <Card title="Google" description="OIDC reference provider." href="/docs/plugins/google" />
137
+ <Card title="X" description="PKCE public client." href="/docs/plugins/x" />
138
+ </Cards>