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,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth flow records on the shared verification store — PKCE verifier,
|
|
3
|
+
* state, nonce, expected issuer, exact redirect URI and optional linking
|
|
4
|
+
* subject, keyed `oauth:{provider}:{stateHash}` with SHA-256-hashed state at
|
|
5
|
+
* rest (same hygiene as magic-link/OTP rows). Single-use via consume.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createVerificationStore,
|
|
10
|
+
findActiveVerification,
|
|
11
|
+
consumeVerification,
|
|
12
|
+
hashChallenge,
|
|
13
|
+
putVerification,
|
|
14
|
+
type VerificationStore,
|
|
15
|
+
} from "../../auth/verification.ts";
|
|
16
|
+
import type { OAuthProviderId } from "./shared.ts";
|
|
17
|
+
|
|
18
|
+
/** TTL for a pending OAuth authorization flow. */
|
|
19
|
+
export const OAUTH_FLOW_TTL_MS = 10 * 60_000;
|
|
20
|
+
|
|
21
|
+
/** One pending OAuth authorization-code flow. */
|
|
22
|
+
export interface OAuthFlowRecord {
|
|
23
|
+
readonly provider: OAuthProviderId;
|
|
24
|
+
/** SHA-256 hex of the raw state — the row's secret value. */
|
|
25
|
+
readonly stateHash: string;
|
|
26
|
+
readonly codeVerifier: string;
|
|
27
|
+
/** EXACT redirect URI string registered at the provider. */
|
|
28
|
+
readonly redirectUri: string;
|
|
29
|
+
readonly nonce?: string;
|
|
30
|
+
/** Issuer the flow was initiated against (mix-up anchor). */
|
|
31
|
+
readonly expectedIssuer: string;
|
|
32
|
+
/** Client id used to start the flow (aud check). */
|
|
33
|
+
readonly clientId: string;
|
|
34
|
+
/** Authenticated user linking this login to an existing account. */
|
|
35
|
+
readonly currentUserId?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create (or reuse) a verification store backing OAuth flows.
|
|
40
|
+
*
|
|
41
|
+
* @param existing - Shared store when the host app provides one
|
|
42
|
+
*/
|
|
43
|
+
export function createOAuthFlowStore(existing?: VerificationStore): VerificationStore {
|
|
44
|
+
return existing ?? createVerificationStore();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Persist a new flow record.
|
|
49
|
+
*
|
|
50
|
+
* @param store - Verification store
|
|
51
|
+
* @param record - Flow fields
|
|
52
|
+
* @param now - Epoch-ms
|
|
53
|
+
*/
|
|
54
|
+
export async function putOauthFlow(
|
|
55
|
+
store: VerificationStore,
|
|
56
|
+
record: OAuthFlowRecord,
|
|
57
|
+
now: number,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
putVerification(store, {
|
|
60
|
+
id: `oauth:${record.provider}:${record.stateHash}`,
|
|
61
|
+
identifier: `oauth:${record.provider}`,
|
|
62
|
+
value: record.stateHash,
|
|
63
|
+
expiresAt: now + OAUTH_FLOW_TTL_MS,
|
|
64
|
+
createdAt: now,
|
|
65
|
+
consumedAt: null,
|
|
66
|
+
attempts: 0,
|
|
67
|
+
// Flow fields ride the row's JSON slot (same pattern as channel-neutral OTP).
|
|
68
|
+
email: JSON.stringify(record),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Find the active (non-consumed, non-expired) flow row for `(provider, rawState)`.
|
|
74
|
+
*
|
|
75
|
+
* @param store - Verification store
|
|
76
|
+
* @param provider - Route provider id
|
|
77
|
+
* @param rawState - Raw `state` query/form value
|
|
78
|
+
* @param now - Epoch-ms
|
|
79
|
+
*/
|
|
80
|
+
export async function findOauthFlow(
|
|
81
|
+
store: VerificationStore,
|
|
82
|
+
provider: OAuthProviderId,
|
|
83
|
+
rawState: string,
|
|
84
|
+
now: number,
|
|
85
|
+
): Promise<OAuthFlowRecord | undefined> {
|
|
86
|
+
const stateHash = await hashChallenge(rawState);
|
|
87
|
+
for (const row of store.rows.values()) {
|
|
88
|
+
if (row.identifier !== `oauth:${provider}`) continue;
|
|
89
|
+
if (row.consumedAt !== null || row.expiresAt <= now) continue;
|
|
90
|
+
if (row.value !== stateHash || row.email === undefined || row.email === null) continue;
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(row.email) as OAuthFlowRecord;
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Mark a flow row consumed (single-use — replay returns `invalid_state`).
|
|
102
|
+
*
|
|
103
|
+
* @param store - Verification store
|
|
104
|
+
* @param provider - Route provider id
|
|
105
|
+
* @param rawState - Raw state
|
|
106
|
+
* @param now - Epoch-ms
|
|
107
|
+
*/
|
|
108
|
+
export async function consumeOauthFlow(
|
|
109
|
+
store: VerificationStore,
|
|
110
|
+
provider: OAuthProviderId,
|
|
111
|
+
rawState: string,
|
|
112
|
+
now: number,
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
const stateHash = await hashChallenge(rawState);
|
|
115
|
+
const row = findActiveVerification(store, `oauth:${provider}`, now);
|
|
116
|
+
if (row && row.value === stateHash) consumeVerification(row, now);
|
|
117
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity bridge — every successful OAuth callback routes through
|
|
3
|
+
* `linkOrProvision`; driver assertions never write identities directly.
|
|
4
|
+
* `IdentityError` codes map onto the typed `AuthFailed` error surface.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { IdentityError, linkOrProvision, type IdentityStore } from "../../auth/identity.ts";
|
|
8
|
+
import type { OAuthAssertion } from "../../drivers/oauth-types.ts";
|
|
9
|
+
import type { OAuthProviderId } from "./shared.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Credential provider string namespacing OAuth accounts
|
|
13
|
+
* (`oke_credentials.provider = "oauth:google"` etc.).
|
|
14
|
+
*
|
|
15
|
+
* @param provider - Plugin provider id
|
|
16
|
+
*/
|
|
17
|
+
export function oauthCredentialProvider(provider: OAuthProviderId): string {
|
|
18
|
+
return `oauth:${provider}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Outcome of the identity bridge. */
|
|
22
|
+
export interface OauthLinkResult {
|
|
23
|
+
readonly userId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Route a verified assertion through `linkOrProvision`.
|
|
28
|
+
*
|
|
29
|
+
* GHSA-6g38-8j4p-j3pr defense: `emailVerified` comes verbatim from the
|
|
30
|
+
* driver trust matrix (default false), and `linkOrProvision` refuses to take
|
|
31
|
+
* over an existing email (`email_in_use`) unless `currentUserId` proves the
|
|
32
|
+
* caller already owns the account.
|
|
33
|
+
*
|
|
34
|
+
* @param identities - Shared identity store
|
|
35
|
+
* @param provider - Provider id
|
|
36
|
+
* @param assertion - Verified driver assertion
|
|
37
|
+
* @param currentUserId - Authenticated subject when linking from a session
|
|
38
|
+
*/
|
|
39
|
+
export async function oauthLink(
|
|
40
|
+
identities: IdentityStore,
|
|
41
|
+
provider: OAuthProviderId,
|
|
42
|
+
assertion: OAuthAssertion,
|
|
43
|
+
currentUserId?: string,
|
|
44
|
+
): Promise<OauthLinkResult> {
|
|
45
|
+
const { user } = await linkOrProvision(identities, {
|
|
46
|
+
provider: oauthCredentialProvider(provider),
|
|
47
|
+
providerAccountId: assertion.subject,
|
|
48
|
+
...(assertion.email !== undefined ? { email: assertion.email } : {}),
|
|
49
|
+
emailVerified: assertion.emailVerified,
|
|
50
|
+
...(assertion.name !== undefined ? { name: assertion.name } : {}),
|
|
51
|
+
...(currentUserId !== undefined ? { currentUserId } : {}),
|
|
52
|
+
});
|
|
53
|
+
return { userId: user.id };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Map an {@link IdentityError} code onto a client-safe failure reason.
|
|
58
|
+
*
|
|
59
|
+
* @param err - Identity error
|
|
60
|
+
*/
|
|
61
|
+
export function identityFailureReason(err: unknown): string | undefined {
|
|
62
|
+
if (err instanceof IdentityError) {
|
|
63
|
+
// Enumeration hygiene: never disclose which emails exist.
|
|
64
|
+
if (err.code === "email_in_use" || err.code === "email_conflict") return "email_in_use";
|
|
65
|
+
if (err.code === "identity_not_found") return "invalid_credentials";
|
|
66
|
+
return err.code;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth plugin shared surface: options, provider ids, PKCE minting.
|
|
3
|
+
* Backend-only — never imported by Console UI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { VerificationStore } from "../../auth/verification.ts";
|
|
7
|
+
import type { SessionStore } from "../../auth/sessions.ts";
|
|
8
|
+
import { generateCodeVerifier, codeChallengeS256 } from "../../drivers/oauth-shared.ts";
|
|
9
|
+
import type { OAuthDriverId } from "../../drivers/oauth-types.ts";
|
|
10
|
+
import type { AnyFlowDef } from "../../kernel/flow.ts";
|
|
11
|
+
import { bindAuthHttp } from "../../auth/bindings.ts";
|
|
12
|
+
import { http } from "../auth/shared.ts";
|
|
13
|
+
import type { Binding } from "../../kernel/on.ts";
|
|
14
|
+
|
|
15
|
+
/** Every supported social provider. */
|
|
16
|
+
export const OAUTH_PROVIDER_IDS = [
|
|
17
|
+
"google",
|
|
18
|
+
"apple",
|
|
19
|
+
"microsoft",
|
|
20
|
+
"github",
|
|
21
|
+
"discord",
|
|
22
|
+
"x",
|
|
23
|
+
"facebook",
|
|
24
|
+
"figma",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
/** Supported social providers (same set as {@link OAuthDriverId}). */
|
|
28
|
+
export type OAuthProviderId = OAuthDriverId;
|
|
29
|
+
|
|
30
|
+
/** Per-provider plugin configuration. */
|
|
31
|
+
export interface OAuthProviderConfig {
|
|
32
|
+
/** Enable this provider (default `false`). */
|
|
33
|
+
readonly enabled?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Confidential client id. Public information — inline config is fine;
|
|
36
|
+
* `OAUTH_<PROVIDER>_CLIENT_ID` via Vault/env also resolves at boot.
|
|
37
|
+
*/
|
|
38
|
+
readonly clientId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* EXACT redirect URI string registered at the provider console. Compared
|
|
41
|
+
* byte-for-byte — never derived from the request. Defaults to
|
|
42
|
+
* `${baseUrl}/auth/oauth/callback/{provider}`.
|
|
43
|
+
*/
|
|
44
|
+
readonly redirectUri?: string;
|
|
45
|
+
/** Scopes requested at the provider (driver defaults apply when omitted). */
|
|
46
|
+
readonly scopes?: readonly string[];
|
|
47
|
+
/** Opt-in storage of provider tokens in Vault (encrypted), default off. */
|
|
48
|
+
readonly storeProviderTokens?: boolean;
|
|
49
|
+
/** Microsoft only — tenant selector or GUID (default `common`). */
|
|
50
|
+
readonly tenant?: "common" | "organizations" | "consumers" | string;
|
|
51
|
+
/** Apple only — Developer team id (client-secret JWT `iss`). */
|
|
52
|
+
readonly teamId?: string;
|
|
53
|
+
/** Apple only — Sign in with Apple private-key id (`kid`). */
|
|
54
|
+
readonly keyId?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Options for the {@link oauth} plugin factory. */
|
|
58
|
+
export interface OAuthOptions {
|
|
59
|
+
/** Per-provider configuration; providers default to disabled. */
|
|
60
|
+
readonly providers?: Readonly<Partial<Record<OAuthProviderId, OAuthProviderConfig>>>;
|
|
61
|
+
/**
|
|
62
|
+
* Default app origin used to synthesize per-provider callback redirect
|
|
63
|
+
* URIs (`{baseUrl}/auth/oauth/callback/{provider}`).
|
|
64
|
+
*/
|
|
65
|
+
readonly baseUrl?: string;
|
|
66
|
+
/** HMAC secret; falls back to the active `gate.auth` secret like other methods. */
|
|
67
|
+
readonly secret?: string;
|
|
68
|
+
/** Shared session store (prefer the same store as `gate.auth`). */
|
|
69
|
+
readonly sessions?: SessionStore;
|
|
70
|
+
/** Shared identity/credential store (defaults to `gate.auth.identities`). */
|
|
71
|
+
readonly identities?: import("../../auth/identity.ts").IdentityStore;
|
|
72
|
+
/** Shared verification store backing OAuth flow rows (tests / advanced). */
|
|
73
|
+
readonly verifications?: VerificationStore;
|
|
74
|
+
/** Injectable clock. */
|
|
75
|
+
readonly now?: () => number;
|
|
76
|
+
/** Injectable fetch for every driver (tests — mock IdP). */
|
|
77
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fresh PKCE pair for a new authorization-code flow (RFC 7636 S256).
|
|
82
|
+
*/
|
|
83
|
+
export async function mintPkcePair(): Promise<{ verifier: string; challenge: string }> {
|
|
84
|
+
const verifier = generateCodeVerifier();
|
|
85
|
+
return { verifier, challenge: await codeChallengeS256(verifier) };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Bind an auth Flow to a public **GET** trigger — provider redirects carry
|
|
90
|
+
* `?code=&state=`. The POST twin (Apple form_post) is bound separately.
|
|
91
|
+
*
|
|
92
|
+
* @param path - Absolute route path
|
|
93
|
+
* @param flowDef - Flow to bind
|
|
94
|
+
* @param gates - Optional gate decls (rate limiting)
|
|
95
|
+
*/
|
|
96
|
+
export function bindPublicAuthGet(
|
|
97
|
+
path: string,
|
|
98
|
+
flowDef: AnyFlowDef,
|
|
99
|
+
gates: readonly Parameters<ReturnType<typeof http.get>["gate"]>[0][] = [],
|
|
100
|
+
): Binding {
|
|
101
|
+
return bindAuthHttp(
|
|
102
|
+
http
|
|
103
|
+
.get(path)
|
|
104
|
+
.public()
|
|
105
|
+
.gate(...gates),
|
|
106
|
+
flowDef,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Envelope sealing for opt-in provider-token storage.
|
|
3
|
+
*
|
|
4
|
+
* Vault paths hold one sealed blob per provider; inside, an AES-GCM-sealed
|
|
5
|
+
* map keyed by user id. The seal key is HKDF-derived from the auth secret
|
|
6
|
+
* with a domain-separated info string (never the session HMAC material) —
|
|
7
|
+
* same pattern as `otp-seal`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** HKDF `info` — versioned; bump when the envelope format changes. */
|
|
11
|
+
export const OAUTH_TOKEN_SEAL_HKDF_INFO = "oke-oauth-token-seal-v1";
|
|
12
|
+
|
|
13
|
+
const INFO_BYTES = new TextEncoder().encode(OAUTH_TOKEN_SEAL_HKDF_INFO);
|
|
14
|
+
const EMPTY_SALT = new Uint8Array(0);
|
|
15
|
+
|
|
16
|
+
async function deriveSealKey(secret: string): Promise<CryptoKey> {
|
|
17
|
+
const baseKey = await crypto.subtle.importKey(
|
|
18
|
+
"raw",
|
|
19
|
+
new TextEncoder().encode(secret),
|
|
20
|
+
"HKDF",
|
|
21
|
+
false,
|
|
22
|
+
["deriveKey"],
|
|
23
|
+
);
|
|
24
|
+
return crypto.subtle.deriveKey(
|
|
25
|
+
{ name: "HKDF", hash: "SHA-256", salt: EMPTY_SALT, info: INFO_BYTES },
|
|
26
|
+
baseKey,
|
|
27
|
+
{ name: "AES-GCM", length: 256 },
|
|
28
|
+
false,
|
|
29
|
+
["encrypt", "decrypt"],
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Seal a cleartext payload to `IV || ciphertext+tag`, base64-encoded.
|
|
35
|
+
*
|
|
36
|
+
* @param secret - Root auth secret
|
|
37
|
+
* @param plaintext - Cleartext bytes/string
|
|
38
|
+
*/
|
|
39
|
+
export async function sealTokenBlob(secret: string, plaintext: string): Promise<string> {
|
|
40
|
+
const key = await deriveSealKey(secret);
|
|
41
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
42
|
+
const cipher = await crypto.subtle.encrypt(
|
|
43
|
+
{ name: "AES-GCM", iv },
|
|
44
|
+
key,
|
|
45
|
+
new TextEncoder().encode(plaintext),
|
|
46
|
+
);
|
|
47
|
+
const packed = new Uint8Array(iv.length + cipher.byteLength);
|
|
48
|
+
packed.set(iv, 0);
|
|
49
|
+
packed.set(new Uint8Array(cipher), iv.length);
|
|
50
|
+
return Buffer.from(packed).toString("base64");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Open a blob produced by {@link sealTokenBlob}.
|
|
55
|
+
*
|
|
56
|
+
* @param secret - Root auth secret
|
|
57
|
+
* @param blob - Base64 sealed blob
|
|
58
|
+
*/
|
|
59
|
+
export async function openTokenBlob(secret: string, blob: string): Promise<string> {
|
|
60
|
+
const key = await deriveSealKey(secret);
|
|
61
|
+
const packed = Buffer.from(blob, "base64");
|
|
62
|
+
if (packed.byteLength < 13) throw new Error("oauth-token-seal: malformed blob");
|
|
63
|
+
const iv = new Uint8Array(packed.subarray(0, 12));
|
|
64
|
+
const data = packed.subarray(12);
|
|
65
|
+
const plain = await crypto.subtle.decrypt(
|
|
66
|
+
{ name: "AES-GCM", iv: toArrayBuffer(iv) },
|
|
67
|
+
key,
|
|
68
|
+
toArrayBuffer(data),
|
|
69
|
+
);
|
|
70
|
+
return new TextDecoder().decode(plain);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toArrayBuffer(view: Uint8Array): ArrayBuffer {
|
|
74
|
+
const copy = new Uint8Array(view.byteLength);
|
|
75
|
+
copy.set(view);
|
|
76
|
+
return copy.buffer;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Seal a full per-user token map (the vault path value).
|
|
81
|
+
*
|
|
82
|
+
* @param secret - Root auth secret
|
|
83
|
+
* @param envelope - userId → sealed per-user blob
|
|
84
|
+
*/
|
|
85
|
+
export async function sealTokenEnvelope(secret: string, envelopeJson: string): Promise<string> {
|
|
86
|
+
return sealTokenBlob(secret, envelopeJson);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Open a vault path value into its per-user token map.
|
|
91
|
+
*
|
|
92
|
+
* @param secret - Root auth secret
|
|
93
|
+
* @param sealed - Sealed envelope JSON
|
|
94
|
+
*/
|
|
95
|
+
export async function unsealTokenEnvelope(
|
|
96
|
+
secret: string,
|
|
97
|
+
sealed: string,
|
|
98
|
+
): Promise<Record<string, string>> {
|
|
99
|
+
return JSON.parse(await openTokenBlob(secret, sealed)) as Record<string, string>;
|
|
100
|
+
}
|