okengine 0.17.2 → 0.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -3
- package/README.md +14 -10
- package/manifest.v1.schema.json +61 -2
- package/package.json +19 -5
- package/site/content/docs/ai/index.mdx +1 -1
- package/site/content/docs/ai/mcp.mdx +10 -1
- package/site/content/docs/ai/skills.mdx +9 -9
- package/site/content/docs/elements/ai.mdx +1 -1
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/gate.mdx +3 -2
- package/site/content/docs/elements/store.mdx +289 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/basic-usage.mdx +3 -10
- package/site/content/docs/get-started/index.mdx +1 -1
- package/site/content/docs/get-started/installation.mdx +2 -3
- package/site/content/docs/get-started/introduction.mdx +58 -121
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/project-structure.mdx +4 -11
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +93 -71
- package/site/content/docs/index.mdx +44 -11
- package/site/content/docs/meta.json +8 -5
- package/site/content/docs/plugins/apple.mdx +151 -0
- package/site/content/docs/plugins/discord.mdx +139 -0
- package/site/content/docs/plugins/facebook.mdx +134 -0
- package/site/content/docs/plugins/figma.mdx +138 -0
- package/site/content/docs/plugins/github.mdx +138 -0
- package/site/content/docs/plugins/google.mdx +153 -0
- package/site/content/docs/plugins/index.mdx +47 -1
- package/site/content/docs/plugins/meta.json +10 -0
- package/site/content/docs/plugins/microsoft.mdx +151 -0
- package/site/content/docs/plugins/oauth.mdx +188 -0
- package/site/content/docs/plugins/x.mdx +125 -0
- package/site/content/docs/providers/index.mdx +2 -0
- package/site/content/docs/recipes/index.mdx +2 -0
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/client.mdx +58 -1
- package/site/content/docs/reference/configuration.mdx +2 -4
- package/site/content/docs/reference/fx.mdx +3 -1
- package/site/content/docs/reference/index.mdx +0 -5
- package/site/content/docs/reference/meta.json +2 -2
- package/site/content/docs/reference/okid.mdx +137 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/config.ts +9 -0
- package/src/auth/identity-sql.ts +314 -0
- package/src/auth/identity.ts +140 -2
- package/src/auth/index.ts +17 -1
- package/src/auth/method-context.ts +3 -0
- package/src/auth/oauth-as/cimd.ts +132 -0
- package/src/auth/oauth-as/crypto.test.ts +101 -0
- package/src/auth/oauth-as/crypto.ts +393 -0
- package/src/auth/oauth-as/errors.ts +68 -0
- package/src/auth/oauth-as/http.test.ts +419 -0
- package/src/auth/oauth-as/http.ts +842 -0
- package/src/auth/oauth-as/stores.ts +61 -0
- package/src/auth/oauth-as/tables.ts +142 -0
- package/src/auth/tables.ts +0 -11
- package/src/bench/README.md +83 -0
- package/src/bench/REPORT.md +176 -0
- package/src/bench/g01-rls-stamp.bench.ts +194 -0
- package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
- package/src/bench/g03-signal-once.bench.ts +157 -0
- package/src/bench/g03-signal-reconnect.bench.ts +254 -0
- package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
- package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
- package/src/bench/g05-sustained-full.bench.ts +265 -0
- package/src/bench/g06-mixed-load.bench.ts +260 -0
- package/src/bench/g07-vault-crypto.bench.ts +100 -0
- package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
- package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
- package/src/bench/g08-store-kv-durable.bench.ts +133 -0
- package/src/bench/g08-store-sql.bench.ts +178 -0
- package/src/bench/g09-journal-sustained.bench.ts +203 -0
- package/src/bench/g10-observability-contention.bench.ts +246 -0
- package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
- package/src/bench/g13-elements.bench.ts +427 -0
- package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
- package/src/bench/g15-postgres-degradation.bench.ts +264 -0
- package/src/bench/g16-live-query-fanout.bench.ts +206 -0
- package/src/bench/lib/event-loop-lag.ts +26 -0
- package/src/bench/lib/infra.ts +60 -0
- package/src/bench/lib/report.ts +52 -0
- package/src/bench/lib/rss-sampler.ts +61 -0
- package/src/bench/lib/signal-pg.ts +88 -0
- package/src/bench/load-app.ts +337 -0
- package/src/bench/load-child.ts +108 -0
- package/src/bench/smoke.bench.ts +43 -0
- package/src/cli/competitor-mention-removal.test.ts +28 -0
- package/src/cli/doctor-fd.ts +117 -0
- package/src/cli/doctor.test.ts +192 -0
- package/src/cli/doctor.ts +129 -1
- package/src/client/create.ts +95 -1
- package/src/client/index.ts +9 -2
- package/src/client/transport.ts +11 -4
- package/src/client/use-live-query.ts +154 -0
- package/src/client-react/index.ts +15 -1
- package/src/client-react/live-resource.ts +246 -0
- package/src/client-react/use-live-query.test.ts +475 -0
- package/src/client-react/use-live-query.ts +530 -0
- package/src/compiler/extract.test.ts +518 -0
- package/src/compiler/extract.ts +386 -19
- package/src/console/server/invoke-user-flow.ts +2 -1
- package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-De7Lc2JC.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
- package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-4rHOePuE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
- package/src/drivers/cdc-outbox.ts +389 -0
- package/src/drivers/memory.ts +20 -0
- package/src/drivers/oauth-apple.ts +156 -0
- package/src/drivers/oauth-discord.ts +79 -0
- package/src/drivers/oauth-facebook.ts +80 -0
- package/src/drivers/oauth-figma.ts +116 -0
- package/src/drivers/oauth-github.ts +92 -0
- package/src/drivers/oauth-google.ts +142 -0
- package/src/drivers/oauth-microsoft.ts +174 -0
- package/src/drivers/oauth-oidc.ts +293 -0
- package/src/drivers/oauth-shared.ts +326 -0
- package/src/drivers/oauth-types.ts +159 -0
- package/src/drivers/oauth-x.ts +77 -0
- package/src/drivers/oauth2-common.ts +95 -0
- package/src/drivers/oauth2-token.ts +61 -0
- package/src/drivers/pg-rls-row-passes.ts +251 -0
- package/src/drivers/pg-rls.ts +2 -0
- package/src/drivers/postgres.ts +45 -2
- package/src/drivers/signal-postgres.ts +2 -1
- package/src/elements/channel/runtime.ts +29 -2
- package/src/elements/channel.test.ts +52 -0
- package/src/elements/gate/boot.ts +29 -2
- package/src/elements/store/emit-drizzle.ts +147 -14
- package/src/elements/store/field-ddl.test.ts +118 -0
- package/src/elements/store/field-types.test.ts +455 -0
- package/src/elements/store/list-query.golden.json +777 -0
- package/src/elements/store/list-query.parity.test.ts +396 -0
- package/src/elements/store/list-query.ts +792 -0
- package/src/elements/store/live-default.test.ts +136 -0
- package/src/elements/store/live-http.test.ts +160 -0
- package/src/elements/store/live-isolation.test.ts +291 -0
- package/src/elements/store/live-query-runtime.test.ts +323 -0
- package/src/elements/store/live-query-runtime.ts +403 -0
- package/src/elements/store/live-query-server.test.ts +377 -0
- package/src/elements/store/live-query-server.ts +102 -0
- package/src/elements/store/live-query.ts +97 -0
- package/src/elements/store/resource.ts +189 -680
- package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
- package/src/elements/store/schema-decl.ts +539 -41
- package/src/elements/store/sql-rls-stamp.test.ts +27 -0
- package/src/elements/store/sql-session.ts +297 -35
- package/src/elements/store/table.ts +102 -21
- package/src/elements/store.test.ts +3 -1
- package/src/elements/store.ts +12 -1
- package/src/elements/vault/chaos-child.ts +74 -1
- package/src/elements/vault/chaos.test.ts +4 -2
- package/src/elements/vault/storage.ts +4 -2
- package/src/index.ts +5 -2
- package/src/kernel/app-auth.ts +1 -0
- package/src/kernel/app.ts +116 -2
- package/src/kernel/auth-sharing.test.ts +196 -0
- package/src/kernel/boot.test.ts +3 -3
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +14 -2
- package/src/kernel/horizontal-child.ts +2 -1
- package/src/kernel/http-resource.ts +33 -7
- package/src/kernel/identity-host-persist.test.ts +119 -0
- package/src/kernel/instance-id.ts +4 -2
- package/src/kernel/journal.ts +2 -1
- package/src/kernel/mcp-tool.test.ts +95 -0
- package/src/kernel/on.ts +9 -0
- package/src/kernel/realtime-bind.ts +326 -0
- package/src/kernel/resource-live.ts +117 -0
- package/src/kernel/triggers.ts +86 -4
- package/src/manifest/diff.ts +37 -0
- package/src/manifest/types.ts +64 -2
- package/src/okid.bench.test.ts +64 -0
- package/src/okid.test.ts +338 -0
- package/src/okid.ts +245 -0
- package/src/plugins/anonymous.ts +19 -1
- package/src/plugins/auth/shared.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +10 -8
- package/src/plugins/mcp-oauth.ts +208 -0
- package/src/plugins/oauth/flow-store.ts +117 -0
- package/src/plugins/oauth/link.ts +69 -0
- package/src/plugins/oauth/shared.ts +108 -0
- package/src/plugins/oauth/token-vault.ts +100 -0
- package/src/plugins/oauth.security.test.ts +535 -0
- package/src/plugins/oauth.ts +532 -0
- package/src/plugins/otp.ts +48 -6
- package/src/plugins/passkey.ts +20 -1
- package/src/plugins/two-factor.ts +11 -0
- package/src/plugins/username.ts +40 -7
- package/src/release/build-lib.ts +7 -1
- package/src/release/measure.ts +1 -0
- package/src/release/official-plugins.ts +4 -1
- package/src/runs/collect.ts +2 -1
- package/src/runs/drivers/files.ts +2 -1
- package/src/test/create-test-app.ts +114 -5
- package/src/test/export-bundle.test.ts +33 -0
- package/src/test/live-signals.test.ts +83 -0
- package/src/test/tenant-isolation.test.ts +175 -0
- package/src/testing.ts +26 -0
- package/src/upgrade/codemods.ts +1 -1
- package/site/content/docs/reference/migrating-environments.mdx +0 -158
|
@@ -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>
|