okengine 0.3.6 → 0.5.0
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 +2 -0
- package/package.json +14 -12
- package/site/content/docs/ai/index.mdx +24 -0
- package/site/content/docs/ai/llms-txt.mdx +3 -0
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/ai/skills.mdx +5 -3
- package/site/content/docs/console/gates.mdx +46 -8
- package/site/content/docs/console/index.mdx +54 -0
- package/site/content/docs/console/meta.json +1 -0
- package/site/content/docs/elements/ai.mdx +2 -0
- package/site/content/docs/elements/channel.mdx +2 -0
- package/site/content/docs/elements/clock.mdx +1 -4
- package/site/content/docs/elements/flow.mdx +4 -10
- package/site/content/docs/elements/gate.mdx +189 -48
- package/site/content/docs/elements/index.mdx +45 -0
- package/site/content/docs/elements/meta.json +1 -1
- package/site/content/docs/elements/signal.mdx +1 -5
- package/site/content/docs/elements/store.mdx +27 -6
- package/site/content/docs/elements/vault.mdx +10 -11
- package/site/content/docs/get-started/basic-usage.mdx +79 -43
- package/site/content/docs/get-started/index.mdx +33 -0
- package/site/content/docs/get-started/installation.mdx +95 -43
- package/site/content/docs/get-started/introduction.mdx +128 -75
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/why.mdx +141 -0
- package/site/content/docs/index.mdx +9 -31
- package/site/content/docs/plugins/anonymous.mdx +95 -0
- package/site/content/docs/plugins/compression.mdx +2 -2
- package/site/content/docs/plugins/cors.mdx +2 -2
- package/site/content/docs/plugins/csrf.mdx +2 -2
- package/site/content/docs/plugins/email-otp.mdx +111 -0
- package/site/content/docs/plugins/{security-headers.mdx → headers.mdx} +2 -2
- package/site/content/docs/plugins/index.mdx +69 -0
- package/site/content/docs/plugins/ip-allowlist.mdx +1 -2
- package/site/content/docs/plugins/magic-link.mdx +112 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +8 -8
- package/site/content/docs/plugins/meta.json +10 -1
- package/site/content/docs/plugins/passkey.mdx +128 -0
- package/site/content/docs/plugins/phone-number.mdx +111 -0
- package/site/content/docs/plugins/two-factor.mdx +116 -0
- package/site/content/docs/plugins/username.mdx +117 -0
- package/site/content/docs/reference/client.mdx +331 -0
- package/site/content/docs/reference/configuration.mdx +1 -1
- package/site/content/docs/reference/environment-variables.mdx +5 -3
- package/site/content/docs/reference/fx.mdx +36 -8
- package/site/content/docs/reference/index.mdx +45 -0
- package/site/content/docs/reference/meta.json +11 -1
- package/site/content/docs/reference/plugins.mdx +25 -14
- package/src/auth/auth.test.ts +20 -2
- package/src/auth/bindings.ts +439 -0
- package/src/auth/breach-check.ts +112 -0
- package/src/auth/config.ts +288 -0
- package/src/auth/cookies.ts +123 -0
- package/src/auth/gate-auth.test.ts +379 -0
- package/src/auth/identity.ts +190 -0
- package/src/auth/index.ts +117 -1
- package/src/auth/method-context.ts +33 -0
- package/src/auth/operator.ts +27 -1
- package/src/auth/password-policy.test.ts +126 -0
- package/src/auth/password-policy.ts +77 -0
- package/src/auth/plugin.ts +62 -4
- package/src/auth/rate.ts +45 -0
- package/src/auth/schema.ts +260 -0
- package/src/auth/secondary-storage.ts +37 -0
- package/src/auth/sessions.ts +58 -1
- package/src/auth/tables.ts +4 -0
- package/src/auth/verification.ts +78 -0
- package/src/cli/competitor-mention-removal.test.ts +117 -0
- package/src/cli/dev.test.ts +3 -3
- package/src/cli/dev.ts +20 -0
- package/src/cli/meilisearch-local.test.ts +69 -0
- package/src/cli/meilisearch-local.ts +188 -0
- package/src/cli/schema.ts +95 -23
- package/src/client/auth.ts +120 -0
- package/src/client-react/index.ts +93 -0
- package/src/compiler/aot.test.ts +2 -1
- package/src/compiler/extract.ts +19 -0
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +5 -1
- package/src/compiler/response.ts +16 -2
- package/src/console/server/app.ts +10 -6
- package/src/console/server/auth-rate.test.ts +3 -3
- package/src/console/server/bind.ts +12 -1
- package/src/console/server/channels.test.ts +1 -1
- package/src/console/server/console-gates.ts +14 -0
- package/src/console/server/console.test.ts +6 -6
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/server/flows.ts +2 -0
- package/src/console/server/gates.ts +8 -1
- package/src/console/server/operator-db.test.ts +4 -4
- package/src/console/server/operator-db.ts +22 -4
- package/src/console/server/security.gate.test.ts +3 -3
- package/src/console/server/store.test.ts +1 -1
- package/src/console/server/store.ts +11 -1
- package/src/console/ui/dist/assets/index-CjxwRGVv.js +10 -0
- package/src/console/ui/dist/assets/panel-access-BGv45snf.js +64 -0
- package/src/console/ui/dist/assets/{panel-ai-D_m6WQI8.js → panel-ai-B2S7LEii.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-CKnXFyUx.js → panel-architecture-D7UJh91v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-DCDd4WAC.js → panel-channels-9T3ybqRu.js} +1 -1
- package/src/console/ui/dist/assets/panel-clock-Cb1UXGRQ.js +1 -0
- package/src/console/ui/dist/assets/{panel-diff-cdonmH8c.js → panel-diff-DmYbKWmN.js} +1 -1
- package/src/console/ui/dist/assets/panel-flows-PiHwT55z.js +48 -0
- package/src/console/ui/dist/assets/{panel-gates-B5eTE8XH.js → panel-gates-BQGYXvjT.js} +1 -1
- package/src/console/ui/dist/assets/panel-overview-BBnRO18l.js +1 -0
- package/src/console/ui/dist/assets/{panel-plugins-Cj7DK1er.js → panel-plugins-D0PsmVw2.js} +1 -1
- package/src/console/ui/dist/assets/panel-runs-CWuRDe0r.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-whmDXIg3.js → panel-signals-Bbg4ewpP.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-CEMHLvaw.js → panel-store-CPCbsDRa.js} +1 -1
- package/src/console/ui/dist/assets/panel-traces-DVAzuA_S.js +1 -0
- package/src/console/ui/dist/assets/{panel-vault-C9wjbki8.js → panel-vault-D1_MvOmo.js} +1 -1
- package/src/console/ui/dist/assets/{rolldown-runtime-CNC7AqOf.js → rolldown-runtime-B0Z9INg1.js} +1 -1
- package/src/console/ui/dist/index.html +2 -2
- package/src/console/ui/gates/fixture.ts +4 -0
- package/src/console/ui/gates/types.ts +2 -0
- package/src/console/ui/shell/client.ts +1 -0
- package/src/docker/compose.ts +5 -0
- package/src/docker/docker.test.ts +41 -0
- package/src/docker/recipes/index.ts +10 -2
- package/src/docker/recipes/meilisearch.ts +31 -0
- package/src/drivers/conformance.test.ts +16 -1
- package/src/drivers/conformance.ts +40 -3
- package/src/drivers/index.ts +14 -2
- package/src/drivers/libsql.ts +4 -4
- package/src/drivers/meilisearch.integration.test.ts +77 -0
- package/src/drivers/meilisearch.test.ts +181 -0
- package/src/drivers/meilisearch.ts +208 -0
- package/src/drivers/memory.ts +4 -4
- package/src/drivers/pgvector.ts +6 -6
- package/src/drivers/types.ts +93 -12
- package/src/drivers/vault-driver-removal.test.ts +6 -0
- package/src/drivers/vault-types.ts +4 -4
- package/src/elements/ai/runtime.ts +6 -0
- package/src/elements/ai.test.ts +22 -0
- package/src/elements/gate/boot.ts +136 -0
- package/src/elements/gate/config.ts +69 -0
- package/src/elements/gate/declare.ts +51 -1
- package/src/elements/gate/runtime.ts +3 -1
- package/src/elements/gate.test.ts +77 -0
- package/src/elements/gate.ts +20 -1
- package/src/elements/index.ts +8 -0
- package/src/elements/store/index-boot.test.ts +49 -7
- package/src/elements/store/runtime.ts +50 -15
- package/src/elements/store.ts +2 -0
- package/src/elements/vault.test.ts +27 -4
- package/src/elements/vault.ts +1 -1
- package/src/index.ts +15 -0
- package/src/kernel/app.ts +253 -32
- package/src/kernel/boot-bind/store.test.ts +9 -0
- package/src/kernel/boot-bind/store.ts +30 -2
- package/src/kernel/boot.test.ts +40 -3
- package/src/kernel/boot.ts +8 -0
- package/src/kernel/call.test.ts +46 -2
- package/src/kernel/concurrency.test.ts +58 -0
- package/src/kernel/concurrency.ts +48 -0
- package/src/kernel/edge.test.ts +3 -3
- package/src/kernel/flow.test.ts +2 -2
- package/src/kernel/fx.test.ts +12 -2
- package/src/kernel/fx.ts +97 -5
- package/src/kernel/hooks.test.ts +4 -4
- package/src/kernel/index.ts +22 -1
- package/src/kernel/pipeline.test.ts +12 -8
- package/src/kernel/pipeline.ts +23 -4
- package/src/kernel/plugin/decorate.test.ts +3 -3
- package/src/kernel/plugin/scoping.test.ts +3 -3
- package/src/kernel/plugin-elements.test.ts +51 -0
- package/src/kernel/plugin-needs.test.ts +83 -0
- package/src/kernel/plugin-needs.ts +129 -0
- package/src/kernel/plugin.ts +101 -0
- package/src/kernel/redacted.ts +74 -0
- package/src/kernel/registry-isolation.test.ts +5 -5
- package/src/kernel/registry.ts +102 -3
- package/src/kernel/router.ts +3 -3
- package/src/manifest/types.ts +2 -0
- package/src/plugins/anonymous.ts +58 -0
- package/src/plugins/auth/shared.ts +121 -0
- package/src/plugins/auth-methods.test.ts +176 -0
- package/src/plugins/compression.test.ts +5 -5
- package/src/plugins/config-source.test.ts +1 -1
- package/src/plugins/cors.test.ts +16 -10
- package/src/plugins/csrf.test.ts +1 -1
- package/src/plugins/email-otp.ts +161 -0
- package/src/plugins/index.ts +31 -0
- package/src/plugins/ip-allowlist.test.ts +19 -9
- package/src/plugins/magic-link.ts +163 -0
- package/src/plugins/maintenance-mode.test.ts +9 -5
- package/src/plugins/passkey.ts +216 -0
- package/src/plugins/phone-number.ts +149 -0
- package/src/plugins/security-headers.test.ts +14 -14
- package/src/plugins/two-factor.ts +249 -0
- package/src/plugins/username.ts +148 -0
- package/src/runs/runs.test.ts +6 -2
- package/src/runtime/primitives.ts +37 -4
- package/src/runtime/serve.test.ts +3 -2
- package/src/runtime/types.ts +25 -2
- package/src/test/create-test-app.test.ts +1 -1
- package/src/test/create-test-app.ts +4 -1
- package/src/test/provisions.integration.test.ts +2 -2
- package/site/content/docs/get-started/comparison.mdx +0 -65
- package/src/console/ui/dist/assets/index-CrKMmO__.js +0 -10
- package/src/console/ui/dist/assets/panel-access-C0J2D-a2.js +0 -64
- package/src/console/ui/dist/assets/panel-clock-DjGGFPzr.js +0 -1
- package/src/console/ui/dist/assets/panel-flows-DlCU5zjA.js +0 -45
- package/src/console/ui/dist/assets/panel-overview-BsFvDdts.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-C0gmnoYL.js +0 -1
- package/src/console/ui/dist/assets/panel-traces-BDiAuVSK.js +0 -1
- package/src/drivers/vault-infisical.ts +0 -57
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable password breach check — optional Have I Been Pwned k-anonymity helper.
|
|
3
|
+
*
|
|
4
|
+
* HIBP Pwned Passwords range API (no API key):
|
|
5
|
+
* `GET https://api.pwnedpasswords.com/range/{5-char-SHA1-prefix}`
|
|
6
|
+
* with required `User-Agent`. Client matches the remaining suffix locally.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Returns `true` when the password should be rejected as breached. */
|
|
10
|
+
export type BreachCheckFn = (password: string) => Promise<boolean>;
|
|
11
|
+
|
|
12
|
+
/** Behaviour when the breach-check network call fails. */
|
|
13
|
+
export type BreachCheckErrorMode = "reject" | "allow";
|
|
14
|
+
|
|
15
|
+
/** Options for {@link createHibpBreachCheck}. */
|
|
16
|
+
export interface HibpBreachCheckOptions {
|
|
17
|
+
/** Required identifying User-Agent (HIBP terms). */
|
|
18
|
+
readonly userAgent: string;
|
|
19
|
+
/** Injectable fetch (tests). Defaults to global `fetch`. */
|
|
20
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
21
|
+
/** Send `Add-Padding: true` (recommended). Default true. */
|
|
22
|
+
readonly padding?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* When the range API errors / is unreachable.
|
|
25
|
+
* Default `"reject"` (fail closed while breach check is enabled).
|
|
26
|
+
*/
|
|
27
|
+
readonly onError?: BreachCheckErrorMode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const HIBP_RANGE_URL = "https://api.pwnedpasswords.com/range/";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* SHA-1 hex digest of a UTF-8 string (uppercase).
|
|
34
|
+
*
|
|
35
|
+
* @param password - Plaintext
|
|
36
|
+
*/
|
|
37
|
+
export async function sha1HexUpper(password: string): Promise<string> {
|
|
38
|
+
const data = new TextEncoder().encode(password);
|
|
39
|
+
const digest = await crypto.subtle.digest("SHA-1", data);
|
|
40
|
+
return [...new Uint8Array(digest)]
|
|
41
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
42
|
+
.join("")
|
|
43
|
+
.toUpperCase();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a Have I Been Pwned k-anonymity breach checker.
|
|
48
|
+
*
|
|
49
|
+
* Never sends the password or full hash — only the first 5 hex chars of SHA-1.
|
|
50
|
+
*
|
|
51
|
+
* @param options - User-Agent and fetch knobs
|
|
52
|
+
*/
|
|
53
|
+
export function createHibpBreachCheck(options: HibpBreachCheckOptions): BreachCheckFn {
|
|
54
|
+
const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
55
|
+
const onError = options.onError ?? "reject";
|
|
56
|
+
const padding = options.padding !== false;
|
|
57
|
+
|
|
58
|
+
return async (password) => {
|
|
59
|
+
const full = await sha1HexUpper(password);
|
|
60
|
+
const prefix = full.slice(0, 5);
|
|
61
|
+
const suffix = full.slice(5);
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetchFn(`${HIBP_RANGE_URL}${prefix}`, {
|
|
64
|
+
headers: {
|
|
65
|
+
"User-Agent": options.userAgent,
|
|
66
|
+
...(padding ? { "Add-Padding": "true" } : {}),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
if (onError === "allow") return false;
|
|
71
|
+
throw new BreachCheckError(`HIBP range HTTP ${res.status}`);
|
|
72
|
+
}
|
|
73
|
+
const body = await res.text();
|
|
74
|
+
for (const line of body.split("\n")) {
|
|
75
|
+
const [hashSuffix, countStr] = line.trim().split(":");
|
|
76
|
+
if (!hashSuffix || hashSuffix.length === 0) continue;
|
|
77
|
+
const count = Number(countStr ?? "0");
|
|
78
|
+
// Padded rows always have count 0 — discard.
|
|
79
|
+
if (count === 0) continue;
|
|
80
|
+
if (hashSuffix.toUpperCase() === suffix) return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err instanceof BreachCheckError) throw err;
|
|
85
|
+
if (onError === "allow") return false;
|
|
86
|
+
throw new BreachCheckError(err instanceof Error ? err.message : "HIBP range request failed");
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run an optional breach check. No-op when `check` is omitted.
|
|
93
|
+
*
|
|
94
|
+
* @param password - Plaintext
|
|
95
|
+
* @param check - Pluggable checker (`true` = breached)
|
|
96
|
+
*/
|
|
97
|
+
export async function assertNotBreached(
|
|
98
|
+
password: string,
|
|
99
|
+
check: BreachCheckFn | undefined,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
if (!check) return;
|
|
102
|
+
const breached = await check(password);
|
|
103
|
+
if (breached) throw new BreachCheckError("password appears in a known breach");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Breach check rejected the password or failed closed. */
|
|
107
|
+
export class BreachCheckError extends Error {
|
|
108
|
+
constructor(message: string) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.name = "BreachCheckError";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate auth options — resolved from `oke({ gate: { auth } })`.
|
|
3
|
+
*
|
|
4
|
+
* Better Auth is inspiration for shape only; this is OKE-native.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PasswordHashOptions } from "../runtime/types.ts";
|
|
8
|
+
import type { SessionStore } from "./sessions.ts";
|
|
9
|
+
import type { BreachCheckFn } from "./breach-check.ts";
|
|
10
|
+
import type { PasswordPolicyOptions } from "./password-policy.ts";
|
|
11
|
+
import {
|
|
12
|
+
resolveAuthSchema,
|
|
13
|
+
type AuthModelOptions,
|
|
14
|
+
type AuthSchemaOptions,
|
|
15
|
+
type ResolvedAuthSchema,
|
|
16
|
+
} from "./schema.ts";
|
|
17
|
+
import { sessionCryptoFromAuthOptions, type AuthSessionOptions } from "./plugin.ts";
|
|
18
|
+
|
|
19
|
+
/** Email + password method knobs. */
|
|
20
|
+
export interface EmailAndPasswordOptions {
|
|
21
|
+
readonly enabled?: boolean;
|
|
22
|
+
/** Require verified email before sign-in (default false). */
|
|
23
|
+
readonly requireEmailVerification?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Account-linking policy (Phase 1 stub — enforced later with OAuth). */
|
|
27
|
+
export interface AccountLinkingOptions {
|
|
28
|
+
readonly enabled?: boolean;
|
|
29
|
+
readonly trustedProviders?: readonly string[];
|
|
30
|
+
readonly allowDifferentEmails?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Opt-in cookie session mirror (Phase 1a). Bearer remains default. */
|
|
34
|
+
export interface AuthCookieOptions {
|
|
35
|
+
readonly enabled?: boolean;
|
|
36
|
+
/** Cookie name prefix (default `oke`). */
|
|
37
|
+
readonly prefix?: string;
|
|
38
|
+
readonly secure?: boolean;
|
|
39
|
+
/** Share across subdomains (`.example.com`). */
|
|
40
|
+
readonly crossSubdomain?: boolean;
|
|
41
|
+
readonly domain?: string;
|
|
42
|
+
readonly sameSite?: "strict" | "lax" | "none";
|
|
43
|
+
readonly path?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Secondary hot-path storage (Phase 1a) — uses `store.kv` when configured. */
|
|
47
|
+
export interface AuthSecondaryStorageOptions {
|
|
48
|
+
readonly enabled?: boolean;
|
|
49
|
+
/** Kv namespace prefix (default `auth:`). */
|
|
50
|
+
readonly prefix?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Lifecycle hooks (Phase 1a) — fx-safe, no side-channel I/O. */
|
|
54
|
+
export interface AuthDatabaseHooks {
|
|
55
|
+
readonly user?: {
|
|
56
|
+
readonly create?: {
|
|
57
|
+
readonly before?: (user: Record<string, unknown>) => unknown | Promise<unknown>;
|
|
58
|
+
readonly after?: (user: Record<string, unknown>) => void | Promise<void>;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
readonly session?: {
|
|
62
|
+
readonly create?: {
|
|
63
|
+
readonly after?: (session: Record<string, unknown>) => void | Promise<void>;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
readonly account?: {
|
|
67
|
+
readonly create?: {
|
|
68
|
+
readonly after?: (account: Record<string, unknown>) => void | Promise<void>;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Session options under `gate.auth.session` (extends plugin session + schema). */
|
|
74
|
+
export interface GateAuthSessionOptions extends AuthSessionOptions, AuthModelOptions {
|
|
75
|
+
/**
|
|
76
|
+
* Max age (ms) from session creation for "fresh" step-up policies.
|
|
77
|
+
* Default 24h. `fx.auth` / gate policies can require freshness.
|
|
78
|
+
*/
|
|
79
|
+
readonly freshAgeMs?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Public `gate.auth` bag. */
|
|
83
|
+
export interface GateAuthOptions extends AuthSchemaOptions {
|
|
84
|
+
/** HMAC secret for access tokens. Required in prod; minted in dev when omitted. */
|
|
85
|
+
readonly secret?: string;
|
|
86
|
+
/** HTTP base path for auth Flows (default `/auth`). */
|
|
87
|
+
readonly basePath?: string;
|
|
88
|
+
/**
|
|
89
|
+
* When `false`, skip materializing `/auth/*` HTTP Bindings
|
|
90
|
+
* (embedding / Console — secret + tables only). Default `true`.
|
|
91
|
+
*/
|
|
92
|
+
readonly http?: boolean;
|
|
93
|
+
/** Audience stamped on access tokens (default `oke-app`). */
|
|
94
|
+
readonly audience?: string;
|
|
95
|
+
readonly emailAndPassword?: EmailAndPasswordOptions;
|
|
96
|
+
readonly session?: GateAuthSessionOptions;
|
|
97
|
+
readonly accountLinking?: AccountLinkingOptions;
|
|
98
|
+
readonly password?: PasswordHashOptions;
|
|
99
|
+
readonly passwordPolicy?: PasswordPolicyOptions;
|
|
100
|
+
readonly breachCheck?: BreachCheckFn;
|
|
101
|
+
/** Opt-in HttpOnly cookie mirror (Phase 1a). */
|
|
102
|
+
readonly cookies?: AuthCookieOptions;
|
|
103
|
+
/** Opt-in secondary kv storage (Phase 1a). */
|
|
104
|
+
readonly secondaryStorage?: AuthSecondaryStorageOptions;
|
|
105
|
+
/** Lifecycle hooks (Phase 1a). */
|
|
106
|
+
readonly hooks?: AuthDatabaseHooks;
|
|
107
|
+
/**
|
|
108
|
+
* Pre-built session store (Console / tests). Created when omitted.
|
|
109
|
+
* Not part of the public app DX — escape hatch for embedding.
|
|
110
|
+
*/
|
|
111
|
+
readonly sessions?: SessionStore;
|
|
112
|
+
/** Injectable clock. */
|
|
113
|
+
readonly now?: () => number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Resolved account-linking stub. */
|
|
117
|
+
export interface ResolvedAccountLinking {
|
|
118
|
+
readonly enabled: boolean;
|
|
119
|
+
readonly trustedProviders: readonly string[];
|
|
120
|
+
readonly allowDifferentEmails: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Resolved cookie options. */
|
|
124
|
+
export interface ResolvedAuthCookies {
|
|
125
|
+
readonly enabled: boolean;
|
|
126
|
+
readonly prefix: string;
|
|
127
|
+
readonly secure: boolean;
|
|
128
|
+
readonly crossSubdomain: boolean;
|
|
129
|
+
readonly domain: string | undefined;
|
|
130
|
+
readonly sameSite: "strict" | "lax" | "none";
|
|
131
|
+
readonly path: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Fully resolved Gate auth config. */
|
|
135
|
+
export interface ResolvedGateAuth {
|
|
136
|
+
readonly enabled: true;
|
|
137
|
+
readonly secret: string;
|
|
138
|
+
readonly secretMinted: boolean;
|
|
139
|
+
readonly basePath: string;
|
|
140
|
+
/** When false, no `/auth/*` Bindings (secret / tables / Bearer only). */
|
|
141
|
+
readonly http: boolean;
|
|
142
|
+
readonly audience: string;
|
|
143
|
+
readonly emailAndPassword: {
|
|
144
|
+
readonly enabled: boolean;
|
|
145
|
+
readonly requireEmailVerification: boolean;
|
|
146
|
+
};
|
|
147
|
+
readonly session: {
|
|
148
|
+
readonly accessTtlMs: number | undefined;
|
|
149
|
+
readonly refreshTtlMs: number | undefined;
|
|
150
|
+
readonly idleTtlMs: number | undefined;
|
|
151
|
+
readonly absoluteTtlMs: number | undefined;
|
|
152
|
+
readonly singleSessionPerUser: boolean;
|
|
153
|
+
readonly freshAgeMs: number;
|
|
154
|
+
};
|
|
155
|
+
readonly accountLinking: ResolvedAccountLinking;
|
|
156
|
+
readonly schema: ResolvedAuthSchema;
|
|
157
|
+
readonly password: PasswordHashOptions | undefined;
|
|
158
|
+
readonly passwordPolicy: PasswordPolicyOptions | undefined;
|
|
159
|
+
readonly breachCheck: BreachCheckFn | undefined;
|
|
160
|
+
readonly cookies: ResolvedAuthCookies;
|
|
161
|
+
readonly secondaryStorage: {
|
|
162
|
+
readonly enabled: boolean;
|
|
163
|
+
readonly prefix: string;
|
|
164
|
+
};
|
|
165
|
+
readonly hooks: AuthDatabaseHooks | undefined;
|
|
166
|
+
readonly sessions: SessionStore | undefined;
|
|
167
|
+
readonly now: (() => number) | undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Default fresh-age window (24h). */
|
|
171
|
+
export const DEFAULT_FRESH_AGE_MS = 24 * 60 * 60 * 1000;
|
|
172
|
+
|
|
173
|
+
/** Options for {@link resolveGateAuth}. */
|
|
174
|
+
export interface ResolveGateAuthOptions {
|
|
175
|
+
readonly auth: GateAuthOptions;
|
|
176
|
+
/** Boot / app env (`prod` fails without secret). */
|
|
177
|
+
readonly env?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Mint a development HMAC secret (never used in production boots).
|
|
182
|
+
*/
|
|
183
|
+
export function mintDevAuthSecret(): string {
|
|
184
|
+
const bytes = new Uint8Array(32);
|
|
185
|
+
crypto.getRandomValues(bytes);
|
|
186
|
+
return `oke_dev_${Buffer.from(bytes).toString("base64url")}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resolve `gate.auth` — fails in prod when `secret` is missing.
|
|
191
|
+
*
|
|
192
|
+
* @param options - Auth bag + env
|
|
193
|
+
*/
|
|
194
|
+
export function resolveGateAuth(options: ResolveGateAuthOptions): ResolvedGateAuth {
|
|
195
|
+
const { auth, env } = options;
|
|
196
|
+
const isProd = env === "prod" || env === "production";
|
|
197
|
+
let secret = auth.secret;
|
|
198
|
+
let secretMinted = false;
|
|
199
|
+
if (!secret || secret.length === 0) {
|
|
200
|
+
if (isProd) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"gate.auth: secret is required in production (set gate.auth.secret or OKE_AUTH_SECRET)",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
secret = mintDevAuthSecret();
|
|
206
|
+
secretMinted = true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const sessionOpts = auth.session ?? {};
|
|
210
|
+
const crypto = sessionCryptoFromAuthOptions({
|
|
211
|
+
accessTtlMs: sessionOpts.accessTtlMs,
|
|
212
|
+
refreshTtlMs: sessionOpts.refreshTtlMs,
|
|
213
|
+
session: sessionOpts,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const schema = resolveAuthSchema({
|
|
217
|
+
user: auth.user,
|
|
218
|
+
account: auth.account,
|
|
219
|
+
session: auth.session,
|
|
220
|
+
refreshToken: auth.refreshToken,
|
|
221
|
+
verification: auth.verification,
|
|
222
|
+
roles: auth.roles,
|
|
223
|
+
apiKeys: auth.apiKeys,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const basePath = normalizeBasePath(auth.basePath ?? "/auth");
|
|
227
|
+
const cookies = auth.cookies ?? {};
|
|
228
|
+
const secondary = auth.secondaryStorage ?? {};
|
|
229
|
+
const linking = auth.accountLinking ?? {};
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
enabled: true,
|
|
233
|
+
secret,
|
|
234
|
+
secretMinted,
|
|
235
|
+
basePath,
|
|
236
|
+
http: auth.http !== false,
|
|
237
|
+
audience: auth.audience ?? "oke-app",
|
|
238
|
+
emailAndPassword: {
|
|
239
|
+
enabled: auth.emailAndPassword?.enabled === true,
|
|
240
|
+
requireEmailVerification: auth.emailAndPassword?.requireEmailVerification === true,
|
|
241
|
+
},
|
|
242
|
+
session: {
|
|
243
|
+
accessTtlMs: crypto.accessTtlMs,
|
|
244
|
+
refreshTtlMs: crypto.refreshTtlMs,
|
|
245
|
+
idleTtlMs: crypto.idleTtlMs,
|
|
246
|
+
absoluteTtlMs: crypto.absoluteTtlMs,
|
|
247
|
+
singleSessionPerUser: crypto.singleSessionPerUser === true,
|
|
248
|
+
freshAgeMs: sessionOpts.freshAgeMs ?? DEFAULT_FRESH_AGE_MS,
|
|
249
|
+
},
|
|
250
|
+
accountLinking: {
|
|
251
|
+
enabled: linking.enabled === true,
|
|
252
|
+
trustedProviders: linking.trustedProviders ?? ["credential"],
|
|
253
|
+
allowDifferentEmails: linking.allowDifferentEmails === true,
|
|
254
|
+
},
|
|
255
|
+
schema,
|
|
256
|
+
password: auth.password,
|
|
257
|
+
passwordPolicy: auth.passwordPolicy,
|
|
258
|
+
breachCheck: auth.breachCheck,
|
|
259
|
+
cookies: {
|
|
260
|
+
enabled: cookies.enabled === true,
|
|
261
|
+
prefix: cookies.prefix ?? "oke",
|
|
262
|
+
secure: cookies.secure !== false,
|
|
263
|
+
crossSubdomain: cookies.crossSubdomain === true,
|
|
264
|
+
domain: cookies.domain,
|
|
265
|
+
sameSite: cookies.sameSite ?? "lax",
|
|
266
|
+
path: cookies.path ?? "/",
|
|
267
|
+
},
|
|
268
|
+
secondaryStorage: {
|
|
269
|
+
enabled: secondary.enabled === true,
|
|
270
|
+
prefix: secondary.prefix ?? "auth:",
|
|
271
|
+
},
|
|
272
|
+
hooks: auth.hooks,
|
|
273
|
+
sessions: auth.sessions,
|
|
274
|
+
now: auth.now,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Normalize auth base path (`/auth`, no trailing slash except root).
|
|
280
|
+
*
|
|
281
|
+
* @param path - Raw base path
|
|
282
|
+
*/
|
|
283
|
+
export function normalizeBasePath(path: string): string {
|
|
284
|
+
const trimmed = path.trim();
|
|
285
|
+
if (trimmed === "" || trimmed === "/") return "/auth";
|
|
286
|
+
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
287
|
+
return withSlash.replace(/\/+$/, "") || "/auth";
|
|
288
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in auth cookie helpers (Phase 1a) — Bearer remains the default transport.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Console's cookie→Bearer pattern for app Gate auth when enabled.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ResolvedAuthCookies } from "./config.ts";
|
|
8
|
+
|
|
9
|
+
/** Cookie names derived from prefix. */
|
|
10
|
+
export interface AuthCookieNames {
|
|
11
|
+
readonly access: string;
|
|
12
|
+
readonly refresh: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Cookie names for a resolved cookie config.
|
|
17
|
+
*
|
|
18
|
+
* @param cookies - Resolved cookie options
|
|
19
|
+
*/
|
|
20
|
+
export function authCookieNames(cookies: ResolvedAuthCookies): AuthCookieNames {
|
|
21
|
+
const p = cookies.prefix;
|
|
22
|
+
return {
|
|
23
|
+
access: `${p}.session_token`,
|
|
24
|
+
refresh: `${p}.refresh_token`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build `Set-Cookie` header values for access + refresh tokens.
|
|
30
|
+
*
|
|
31
|
+
* @param cookies - Resolved cookie options
|
|
32
|
+
* @param tokens - Issued tokens
|
|
33
|
+
* @param maxAgeSec - Max-Age for refresh (access uses shorter when provided)
|
|
34
|
+
*/
|
|
35
|
+
export function buildAuthSetCookies(
|
|
36
|
+
cookies: ResolvedAuthCookies,
|
|
37
|
+
tokens: { readonly accessToken: string; readonly refreshToken: string },
|
|
38
|
+
maxAgeSec: { readonly access: number; readonly refresh: number },
|
|
39
|
+
): string[] {
|
|
40
|
+
if (!cookies.enabled) return [];
|
|
41
|
+
const names = authCookieNames(cookies);
|
|
42
|
+
return [
|
|
43
|
+
serializeCookie(names.access, tokens.accessToken, cookies, maxAgeSec.access),
|
|
44
|
+
serializeCookie(names.refresh, tokens.refreshToken, cookies, maxAgeSec.refresh),
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build clearing `Set-Cookie` headers (logout).
|
|
50
|
+
*
|
|
51
|
+
* @param cookies - Resolved cookie options
|
|
52
|
+
*/
|
|
53
|
+
export function clearAuthSetCookies(cookies: ResolvedAuthCookies): string[] {
|
|
54
|
+
if (!cookies.enabled) return [];
|
|
55
|
+
const names = authCookieNames(cookies);
|
|
56
|
+
return [
|
|
57
|
+
serializeCookie(names.access, "", cookies, 0),
|
|
58
|
+
serializeCookie(names.refresh, "", cookies, 0),
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract Bearer-equivalent token from Cookie header when cookies enabled.
|
|
64
|
+
* Prefers access cookie; falls back to refresh only when caller asks.
|
|
65
|
+
*
|
|
66
|
+
* @param cookieHeader - Raw `Cookie` header
|
|
67
|
+
* @param cookies - Resolved cookie options
|
|
68
|
+
*/
|
|
69
|
+
export function tokenFromCookieHeader(
|
|
70
|
+
cookieHeader: string | null | undefined,
|
|
71
|
+
cookies: ResolvedAuthCookies,
|
|
72
|
+
): string | undefined {
|
|
73
|
+
if (!cookies.enabled || !cookieHeader) return undefined;
|
|
74
|
+
const names = authCookieNames(cookies);
|
|
75
|
+
const map = parseCookieHeader(cookieHeader);
|
|
76
|
+
return map.get(names.access) || undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function serializeCookie(
|
|
80
|
+
name: string,
|
|
81
|
+
value: string,
|
|
82
|
+
cookies: ResolvedAuthCookies,
|
|
83
|
+
maxAge: number,
|
|
84
|
+
): string {
|
|
85
|
+
const parts = [
|
|
86
|
+
`${name}=${encodeURIComponent(value)}`,
|
|
87
|
+
`Path=${cookies.path}`,
|
|
88
|
+
`Max-Age=${Math.max(0, Math.floor(maxAge))}`,
|
|
89
|
+
"HttpOnly",
|
|
90
|
+
`SameSite=${capitalizeSameSite(cookies.sameSite)}`,
|
|
91
|
+
];
|
|
92
|
+
if (cookies.secure) parts.push("Secure");
|
|
93
|
+
const domain = cookies.domain ?? (cookies.crossSubdomain ? undefined : undefined);
|
|
94
|
+
if (domain) parts.push(`Domain=${domain}`);
|
|
95
|
+
return parts.join("; ");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function capitalizeSameSite(v: "strict" | "lax" | "none"): string {
|
|
99
|
+
if (v === "none") return "None";
|
|
100
|
+
if (v === "lax") return "Lax";
|
|
101
|
+
return "Strict";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parse a Cookie header into a map.
|
|
106
|
+
*
|
|
107
|
+
* @param header - Raw Cookie header
|
|
108
|
+
*/
|
|
109
|
+
export function parseCookieHeader(header: string): Map<string, string> {
|
|
110
|
+
const out = new Map<string, string>();
|
|
111
|
+
for (const part of header.split(";")) {
|
|
112
|
+
const idx = part.indexOf("=");
|
|
113
|
+
if (idx <= 0) continue;
|
|
114
|
+
const k = part.slice(0, idx).trim();
|
|
115
|
+
const v = part.slice(idx + 1).trim();
|
|
116
|
+
try {
|
|
117
|
+
out.set(k, decodeURIComponent(v));
|
|
118
|
+
} catch {
|
|
119
|
+
out.set(k, v);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|