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
package/src/cli/schema.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `oke schema generate` — core + plugin tables → `schema/oke.ts`.
|
|
2
|
+
* `oke schema generate` — core + auth + plugin tables → `schema/oke.ts`.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { mkdir } from "node:fs/promises";
|
|
7
7
|
import { dirname, resolve } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
resolveAuthSchema,
|
|
10
|
+
type AuthSchemaOptions,
|
|
11
|
+
type ResolvedAuthModel,
|
|
12
|
+
type ResolvedAuthSchema,
|
|
13
|
+
} from "../auth/schema.ts";
|
|
8
14
|
import type { Manifest } from "../manifest/types.ts";
|
|
9
15
|
import { loadManifest } from "./load-config.ts";
|
|
10
16
|
|
|
@@ -25,10 +31,12 @@ export interface SchemaGenerateOptions {
|
|
|
25
31
|
readonly write?: (text: string) => void;
|
|
26
32
|
/** Extra table names (plugins / tests). */
|
|
27
33
|
readonly extraTables?: readonly string[];
|
|
34
|
+
/** Auth schema customization (from `gate.auth` / tests). */
|
|
35
|
+
readonly authSchema?: AuthSchemaOptions | false;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
/**
|
|
31
|
-
* Generate `schema/oke.ts` from Manifest store tables + core
|
|
39
|
+
* Generate `schema/oke.ts` from Manifest store tables + core auth columns.
|
|
32
40
|
*
|
|
33
41
|
* @param options - Manifest / check flag
|
|
34
42
|
*/
|
|
@@ -42,23 +50,41 @@ export async function runSchemaGenerate(options: SchemaGenerateOptions = {}): Pr
|
|
|
42
50
|
if (await file.exists()) manifest = await loadManifest(path);
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
const authSchema =
|
|
54
|
+
options.authSchema === false
|
|
55
|
+
? undefined
|
|
56
|
+
: resolveAuthSchema(options.authSchema ?? loadAuthSchemaFromManifest(manifest));
|
|
57
|
+
|
|
45
58
|
const tables = new Set<string>([
|
|
46
|
-
"oke_roles",
|
|
47
|
-
"oke_role_grants",
|
|
48
|
-
"oke_api_keys",
|
|
49
59
|
"oke_overrides",
|
|
50
60
|
"oke_crons",
|
|
51
61
|
"oke_signal_config",
|
|
52
62
|
"oke_console_prefs",
|
|
53
63
|
...(options.extraTables ?? []),
|
|
54
64
|
]);
|
|
65
|
+
if (authSchema) {
|
|
66
|
+
for (const name of authSchema.tableNames) tables.add(name);
|
|
67
|
+
} else {
|
|
68
|
+
for (const name of [
|
|
69
|
+
"oke_roles",
|
|
70
|
+
"oke_role_grants",
|
|
71
|
+
"oke_api_keys",
|
|
72
|
+
"oke_identities",
|
|
73
|
+
"oke_credentials",
|
|
74
|
+
"oke_sessions",
|
|
75
|
+
"oke_refresh_tokens",
|
|
76
|
+
"oke_verifications",
|
|
77
|
+
]) {
|
|
78
|
+
tables.add(name);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
55
81
|
if (manifest?.stores) {
|
|
56
82
|
for (const store of Object.values(manifest.stores)) {
|
|
57
83
|
for (const t of Object.keys(store.tables ?? {})) tables.add(t);
|
|
58
84
|
}
|
|
59
85
|
}
|
|
60
86
|
|
|
61
|
-
const source = emitSchemaSource([...tables].sort());
|
|
87
|
+
const source = emitSchemaSource([...tables].sort(), authSchema);
|
|
62
88
|
const out = resolve(cwd, options.out ?? SCHEMA_OUT);
|
|
63
89
|
const fp = hashSource(source);
|
|
64
90
|
|
|
@@ -91,26 +117,25 @@ export async function runSchemaGenerate(options: SchemaGenerateOptions = {}): Pr
|
|
|
91
117
|
* @param manifest - Optional manifest
|
|
92
118
|
*/
|
|
93
119
|
export async function schemaFingerprint(cwd: string, manifest?: Manifest): Promise<string> {
|
|
120
|
+
let m = manifest;
|
|
121
|
+
if (!m) {
|
|
122
|
+
const path = resolve(cwd, "oke.manifest.json");
|
|
123
|
+
if (await Bun.file(path).exists()) m = await loadManifest(path);
|
|
124
|
+
}
|
|
125
|
+
const authSchema = resolveAuthSchema(loadAuthSchemaFromManifest(m));
|
|
94
126
|
const tables = new Set<string>([
|
|
95
|
-
"oke_roles",
|
|
96
|
-
"oke_role_grants",
|
|
97
|
-
"oke_api_keys",
|
|
98
127
|
"oke_overrides",
|
|
99
128
|
"oke_crons",
|
|
100
129
|
"oke_signal_config",
|
|
101
130
|
"oke_console_prefs",
|
|
131
|
+
...authSchema.tableNames,
|
|
102
132
|
]);
|
|
103
|
-
let m = manifest;
|
|
104
|
-
if (!m) {
|
|
105
|
-
const path = resolve(cwd, "oke.manifest.json");
|
|
106
|
-
if (await Bun.file(path).exists()) m = await loadManifest(path);
|
|
107
|
-
}
|
|
108
133
|
if (m?.stores) {
|
|
109
134
|
for (const store of Object.values(m.stores)) {
|
|
110
135
|
for (const t of Object.keys(store.tables ?? {})) tables.add(t);
|
|
111
136
|
}
|
|
112
137
|
}
|
|
113
|
-
return hashSource(emitSchemaSource([...tables].sort()));
|
|
138
|
+
return hashSource(emitSchemaSource([...tables].sort(), authSchema));
|
|
114
139
|
}
|
|
115
140
|
|
|
116
141
|
/**
|
|
@@ -150,7 +175,7 @@ export async function schemaCli(args: readonly string[]): Promise<number> {
|
|
|
150
175
|
else if (a === "--help" || a === "-h") {
|
|
151
176
|
console.log(`oke schema generate [--check|-c] [--out|-o schema/oke.ts]
|
|
152
177
|
|
|
153
|
-
Emit core +
|
|
178
|
+
Emit core auth columns + Manifest store tables. Use --check in CI to fail on drift.
|
|
154
179
|
`);
|
|
155
180
|
return 0;
|
|
156
181
|
}
|
|
@@ -159,20 +184,33 @@ Emit core + plugin tables. Use --check in CI to fail on drift.
|
|
|
159
184
|
}
|
|
160
185
|
|
|
161
186
|
/**
|
|
162
|
-
* Emit a
|
|
187
|
+
* Emit a schema module with real auth columns when resolved.
|
|
163
188
|
*
|
|
164
189
|
* @param tables - Table names
|
|
190
|
+
* @param authSchema - Resolved auth schema (optional)
|
|
165
191
|
*/
|
|
166
|
-
export function emitSchemaSource(
|
|
192
|
+
export function emitSchemaSource(
|
|
193
|
+
tables: readonly string[],
|
|
194
|
+
authSchema?: ResolvedAuthSchema,
|
|
195
|
+
): string {
|
|
196
|
+
const byTable = new Map<string, ResolvedAuthModel>();
|
|
197
|
+
if (authSchema) {
|
|
198
|
+
for (const model of Object.values(authSchema.models)) {
|
|
199
|
+
byTable.set(model.tableName, model);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
167
203
|
const decls = tables
|
|
168
|
-
.map(
|
|
169
|
-
(name)
|
|
170
|
-
|
|
171
|
-
|
|
204
|
+
.map((name) => {
|
|
205
|
+
const model = byTable.get(name);
|
|
206
|
+
if (model) return emitAuthTable(model);
|
|
207
|
+
return `/** Generated table handle — wire columns in your app schema. */\nexport const ${camel(name)} = { name: "${name}" } as const;\n`;
|
|
208
|
+
})
|
|
172
209
|
.join("\n");
|
|
210
|
+
|
|
173
211
|
return `/**
|
|
174
212
|
* Generated by \`oke schema generate\` — do not edit.
|
|
175
|
-
* Core
|
|
213
|
+
* Core Gate auth tables + Manifest store tables.
|
|
176
214
|
*/
|
|
177
215
|
|
|
178
216
|
${decls}
|
|
@@ -180,6 +218,40 @@ export const okeTables = [${tables.map((t) => `"${t}"`).join(", ")}] as const;
|
|
|
180
218
|
`;
|
|
181
219
|
}
|
|
182
220
|
|
|
221
|
+
function emitAuthTable(model: ResolvedAuthModel): string {
|
|
222
|
+
const cols = model.columns
|
|
223
|
+
.map((c) => {
|
|
224
|
+
const extras: string[] = [`sqlType: "${c.sqlType}"`];
|
|
225
|
+
if (c.primary) extras.push("primary: true");
|
|
226
|
+
if (c.required) extras.push("required: true");
|
|
227
|
+
if (c.defaultValue !== undefined) {
|
|
228
|
+
extras.push(`defaultValue: ${JSON.stringify(c.defaultValue)}`);
|
|
229
|
+
}
|
|
230
|
+
return ` ${c.logical}: { name: "${c.sqlName}", ${extras.join(", ")} }`;
|
|
231
|
+
})
|
|
232
|
+
.join(",\n");
|
|
233
|
+
return `/** Auth model \`${model.model}\` → \`${model.tableName}\`. */
|
|
234
|
+
export const ${camel(model.tableName)} = {
|
|
235
|
+
name: "${model.tableName}",
|
|
236
|
+
model: "${model.model}",
|
|
237
|
+
columns: {
|
|
238
|
+
${cols}
|
|
239
|
+
},
|
|
240
|
+
} as const;
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Read optional auth schema customization from Manifest extensions.
|
|
246
|
+
*
|
|
247
|
+
* @param manifest - Loaded Manifest
|
|
248
|
+
*/
|
|
249
|
+
function loadAuthSchemaFromManifest(manifest: Manifest | undefined): AuthSchemaOptions {
|
|
250
|
+
if (!manifest) return {};
|
|
251
|
+
const ext = (manifest as { authSchema?: AuthSchemaOptions }).authSchema;
|
|
252
|
+
return ext ?? {};
|
|
253
|
+
}
|
|
254
|
+
|
|
183
255
|
function hashSource(source: string): string {
|
|
184
256
|
return createHash("sha256").update(source).digest("hex");
|
|
185
257
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional auth helpers for {@link createClient} — not a second client factory.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Common auth Flow error codes (server `fail` codes). */
|
|
8
|
+
export const AUTH_ERROR_CODES = {
|
|
9
|
+
AuthFailed: "AuthFailed",
|
|
10
|
+
AuthRateLimited: "AuthRateLimited",
|
|
11
|
+
Unauthorized: "Unauthorized",
|
|
12
|
+
Forbidden: "Forbidden",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
/** In-memory session token bag for SPA / Node clients. */
|
|
16
|
+
export interface MemorySession {
|
|
17
|
+
accessToken: string | null;
|
|
18
|
+
refreshToken: string | null;
|
|
19
|
+
accessExpiresAt: number | null;
|
|
20
|
+
userId: string | null;
|
|
21
|
+
set(tokens: {
|
|
22
|
+
readonly accessToken: string;
|
|
23
|
+
readonly refreshToken: string;
|
|
24
|
+
readonly accessExpiresAt?: number;
|
|
25
|
+
readonly userId?: string;
|
|
26
|
+
}): void;
|
|
27
|
+
clear(): void;
|
|
28
|
+
/** Wire into `createClient` `auth.getToken`. */
|
|
29
|
+
getToken(): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Wire into `createClient` `auth.refresh` — calls the refresh Flow via the client.
|
|
32
|
+
*
|
|
33
|
+
* @param api - Client with `auth.refresh` route (or custom path)
|
|
34
|
+
*/
|
|
35
|
+
refresh(api: {
|
|
36
|
+
auth: {
|
|
37
|
+
refresh: (input: { refreshToken: string }) => Promise<{
|
|
38
|
+
data: {
|
|
39
|
+
accessToken: string;
|
|
40
|
+
refreshToken: string;
|
|
41
|
+
accessExpiresAt?: number;
|
|
42
|
+
userId?: string;
|
|
43
|
+
} | null;
|
|
44
|
+
error: unknown;
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
47
|
+
}): Promise<string | null>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create an in-memory session helper for Bearer `createClient` wiring.
|
|
52
|
+
*/
|
|
53
|
+
export function memorySession(): MemorySession {
|
|
54
|
+
const state: {
|
|
55
|
+
accessToken: string | null;
|
|
56
|
+
refreshToken: string | null;
|
|
57
|
+
accessExpiresAt: number | null;
|
|
58
|
+
userId: string | null;
|
|
59
|
+
} = {
|
|
60
|
+
accessToken: null,
|
|
61
|
+
refreshToken: null,
|
|
62
|
+
accessExpiresAt: null,
|
|
63
|
+
userId: null,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
get accessToken() {
|
|
68
|
+
return state.accessToken;
|
|
69
|
+
},
|
|
70
|
+
set accessToken(v) {
|
|
71
|
+
state.accessToken = v;
|
|
72
|
+
},
|
|
73
|
+
get refreshToken() {
|
|
74
|
+
return state.refreshToken;
|
|
75
|
+
},
|
|
76
|
+
set refreshToken(v) {
|
|
77
|
+
state.refreshToken = v;
|
|
78
|
+
},
|
|
79
|
+
get accessExpiresAt() {
|
|
80
|
+
return state.accessExpiresAt;
|
|
81
|
+
},
|
|
82
|
+
set accessExpiresAt(v) {
|
|
83
|
+
state.accessExpiresAt = v;
|
|
84
|
+
},
|
|
85
|
+
get userId() {
|
|
86
|
+
return state.userId;
|
|
87
|
+
},
|
|
88
|
+
set userId(v) {
|
|
89
|
+
state.userId = v;
|
|
90
|
+
},
|
|
91
|
+
set(tokens) {
|
|
92
|
+
state.accessToken = tokens.accessToken;
|
|
93
|
+
state.refreshToken = tokens.refreshToken;
|
|
94
|
+
state.accessExpiresAt = tokens.accessExpiresAt ?? null;
|
|
95
|
+
state.userId = tokens.userId ?? null;
|
|
96
|
+
},
|
|
97
|
+
clear() {
|
|
98
|
+
state.accessToken = null;
|
|
99
|
+
state.refreshToken = null;
|
|
100
|
+
state.accessExpiresAt = null;
|
|
101
|
+
state.userId = null;
|
|
102
|
+
},
|
|
103
|
+
getToken() {
|
|
104
|
+
return state.accessToken;
|
|
105
|
+
},
|
|
106
|
+
async refresh(api) {
|
|
107
|
+
if (!state.refreshToken) return null;
|
|
108
|
+
const { data, error } = await api.auth.refresh({ refreshToken: state.refreshToken });
|
|
109
|
+
if (error || !data) {
|
|
110
|
+
state.accessToken = null;
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
state.accessToken = data.accessToken;
|
|
114
|
+
state.refreshToken = data.refreshToken;
|
|
115
|
+
state.accessExpiresAt = data.accessExpiresAt ?? null;
|
|
116
|
+
if (data.userId) state.userId = data.userId;
|
|
117
|
+
return data.accessToken;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React helpers for okengine clients — `useSession` over {@link createClient}.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
|
|
8
|
+
import type { MemorySession } from "../client/auth.ts";
|
|
9
|
+
|
|
10
|
+
/** Minimal client surface for session reading. */
|
|
11
|
+
export interface SessionClient {
|
|
12
|
+
auth: {
|
|
13
|
+
me: (input?: unknown) => Promise<{
|
|
14
|
+
data: {
|
|
15
|
+
userId: string;
|
|
16
|
+
email: string;
|
|
17
|
+
name: string;
|
|
18
|
+
emailVerified?: boolean;
|
|
19
|
+
} | null;
|
|
20
|
+
error: unknown;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Session snapshot returned by {@link useSession}. */
|
|
26
|
+
export interface SessionState {
|
|
27
|
+
readonly status: "loading" | "authenticated" | "unauthenticated";
|
|
28
|
+
readonly user: {
|
|
29
|
+
readonly userId: string;
|
|
30
|
+
readonly email: string;
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly emailVerified?: boolean;
|
|
33
|
+
} | null;
|
|
34
|
+
readonly accessToken: string | null;
|
|
35
|
+
refresh(): Promise<void>;
|
|
36
|
+
signOut(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* React hook: read session via `auth.me` + optional {@link memorySession} helper.
|
|
41
|
+
*
|
|
42
|
+
* @param api - Typed client from `createClient`
|
|
43
|
+
* @param session - Optional memory session (Bearer storage)
|
|
44
|
+
*/
|
|
45
|
+
export function useSession(api: SessionClient, session?: MemorySession): SessionState {
|
|
46
|
+
const token = useSyncExternalStore(
|
|
47
|
+
(onStoreChange) => {
|
|
48
|
+
if (!session) return () => {};
|
|
49
|
+
const id = setInterval(onStoreChange, 250);
|
|
50
|
+
return () => clearInterval(id);
|
|
51
|
+
},
|
|
52
|
+
() => session?.accessToken ?? null,
|
|
53
|
+
() => session?.accessToken ?? null,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const [user, setUser] = useState<SessionState["user"]>(null);
|
|
57
|
+
const [status, setStatus] = useState<SessionState["status"]>("loading");
|
|
58
|
+
|
|
59
|
+
const refresh = useCallback(async () => {
|
|
60
|
+
if (!token) {
|
|
61
|
+
setUser(null);
|
|
62
|
+
setStatus("unauthenticated");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
setStatus("loading");
|
|
66
|
+
const { data, error } = await api.auth.me({});
|
|
67
|
+
if (error || !data) {
|
|
68
|
+
setUser(null);
|
|
69
|
+
setStatus("unauthenticated");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setUser(data);
|
|
73
|
+
setStatus("authenticated");
|
|
74
|
+
}, [api, token]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
void refresh();
|
|
78
|
+
}, [refresh]);
|
|
79
|
+
|
|
80
|
+
const signOut = useCallback(() => {
|
|
81
|
+
session?.clear();
|
|
82
|
+
setUser(null);
|
|
83
|
+
setStatus("unauthenticated");
|
|
84
|
+
}, [session]);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
status,
|
|
88
|
+
user,
|
|
89
|
+
accessToken: token,
|
|
90
|
+
refresh,
|
|
91
|
+
signOut,
|
|
92
|
+
};
|
|
93
|
+
}
|
package/src/compiler/aot.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { gate } from "../elements/gate.ts";
|
|
3
4
|
import { oke } from "../kernel/app.ts";
|
|
4
5
|
import { flow, resetFlowSeq } from "../kernel/flow.ts";
|
|
5
6
|
import { on, resetBindings } from "../kernel/on.ts";
|
|
@@ -118,7 +119,7 @@ describe("typed error narrowing end-to-end", () => {
|
|
|
118
119
|
const FlightFull = z.object({ seatsLeft: z.number() });
|
|
119
120
|
|
|
120
121
|
on(
|
|
121
|
-
http.post("/bookings"),
|
|
122
|
+
http.post("/bookings").gate(gate.public),
|
|
122
123
|
flow({
|
|
123
124
|
name: "bookings.create",
|
|
124
125
|
in: Booking,
|
package/src/compiler/extract.ts
CHANGED
|
@@ -671,6 +671,25 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
671
671
|
}
|
|
672
672
|
}
|
|
673
673
|
|
|
674
|
+
if (obj === "gate" && prop === "scope") {
|
|
675
|
+
const scopeName = stringArg(call.arguments[0]);
|
|
676
|
+
if (scopeName) {
|
|
677
|
+
scope.gates[scopeName] = {
|
|
678
|
+
kind: "policy",
|
|
679
|
+
roles: [scopeName],
|
|
680
|
+
scopes: [scopeName],
|
|
681
|
+
};
|
|
682
|
+
const bindingName = enclosingConstName(call, program);
|
|
683
|
+
if (bindingName) {
|
|
684
|
+
scope.gateIds.set(bindingName, scopeName);
|
|
685
|
+
scope.bindings.set(bindingName, {
|
|
686
|
+
kind: "unknown",
|
|
687
|
+
ref: scopeName,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
674
693
|
if (obj === "gate" && prop === "rate") {
|
|
675
694
|
const opts = objectArg(call.arguments[0]);
|
|
676
695
|
const strategy = (stringProp(opts, "strategy") ?? "sliding-window-counter") as RateStrategy;
|
|
@@ -5,7 +5,11 @@ export const chargeBooking = flow({
|
|
|
5
5
|
name: "payments.chargeBooking",
|
|
6
6
|
durable: true,
|
|
7
7
|
do: async ({ orderId }, fx) => {
|
|
8
|
-
|
|
8
|
+
// Journal only serializable step values — a Redacted is not journaled.
|
|
9
|
+
const intent = await fx.step("create-intent", async () => ({
|
|
10
|
+
id: `pi_${orderId}`,
|
|
11
|
+
key: fx.vault(stripeKey).reveal(),
|
|
12
|
+
}));
|
|
9
13
|
await fx.clock.sleep("verify-window", "2m");
|
|
10
14
|
return fx.step("confirm", () => intent);
|
|
11
15
|
},
|
package/src/compiler/response.ts
CHANGED
|
@@ -91,8 +91,22 @@ export function encodeExecuteResult(result: {
|
|
|
91
91
|
}): Response {
|
|
92
92
|
if (result.response) return result.response;
|
|
93
93
|
if (result.failure) return encodeFailure(result.failure);
|
|
94
|
-
if (result.error !== undefined
|
|
95
|
-
|
|
94
|
+
if (result.error !== undefined) {
|
|
95
|
+
if (isFlowFailure(result.error)) {
|
|
96
|
+
return encodeFailure(result.error);
|
|
97
|
+
}
|
|
98
|
+
// Unhandled throws must never look like success (`undefined` → 204).
|
|
99
|
+
return Response.json(
|
|
100
|
+
{
|
|
101
|
+
data: null,
|
|
102
|
+
error: {
|
|
103
|
+
code: "InternalError",
|
|
104
|
+
data: {},
|
|
105
|
+
message: result.error instanceof Error ? result.error.message : "internal error",
|
|
106
|
+
},
|
|
107
|
+
} satisfies FailureEnvelope,
|
|
108
|
+
{ status: 500 },
|
|
109
|
+
);
|
|
96
110
|
}
|
|
97
111
|
return encodeSuccess(result.output);
|
|
98
112
|
}
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* ConsoleApp — operator-plane app built on `createClient<ConsoleApp>`.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { auth } from "../../auth/index.ts";
|
|
6
5
|
import { memorySignalDriver } from "../../drivers/signal-memory.ts";
|
|
7
6
|
import { signal as declareSignal } from "../../elements/signal/declare.ts";
|
|
8
7
|
import { createSignalRuntime } from "../../elements/signal/runtime.ts";
|
|
9
8
|
import { oke, type OkeApp } from "../../kernel/index.ts";
|
|
10
9
|
import { createManifestAiRuntime } from "./ai.ts";
|
|
10
|
+
import { CONSOLE_GATES } from "./console-gates.ts";
|
|
11
11
|
import { createConsoleBindings } from "./flows.ts";
|
|
12
12
|
import { consolePlugin } from "./plugin.ts";
|
|
13
13
|
import {
|
|
@@ -61,14 +61,18 @@ export function createConsoleApp(options: CreateConsoleAppOptions = {}): Console
|
|
|
61
61
|
bindings,
|
|
62
62
|
autoBoot: false,
|
|
63
63
|
fx: { now: state.now },
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
gate: {
|
|
65
|
+
auth: {
|
|
66
|
+
secret: state.secret,
|
|
67
|
+
sessions: state.sessions,
|
|
68
|
+
now: state.now,
|
|
69
|
+
// Console owns `/console/session/*` — no app `/auth/*` surfaces.
|
|
70
|
+
http: false,
|
|
71
|
+
},
|
|
72
|
+
policies: [...CONSOLE_GATES],
|
|
68
73
|
},
|
|
69
74
|
runs: { driver: "memory" },
|
|
70
75
|
})
|
|
71
|
-
.plug(auth({ secret: state.secret }))
|
|
72
76
|
.plug(consolePlugin())
|
|
73
77
|
.adopt({
|
|
74
78
|
console: {
|
|
@@ -53,18 +53,18 @@ describe("auth-rate strategy", () => {
|
|
|
53
53
|
await createOperator(store, {
|
|
54
54
|
email: "ops@example.com",
|
|
55
55
|
name: "Ops",
|
|
56
|
-
password: "
|
|
56
|
+
password: "password1234",
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
const tMissing = performance.now();
|
|
60
|
-
expect(await authenticateOperator(store, "missing@example.com", "
|
|
60
|
+
expect(await authenticateOperator(store, "missing@example.com", "password1234")).toBeNull();
|
|
61
61
|
const missingMs = performance.now() - tMissing;
|
|
62
62
|
|
|
63
63
|
const tBad = performance.now();
|
|
64
64
|
expect(await authenticateOperator(store, "ops@example.com", "wrong-password")).toBeNull();
|
|
65
65
|
const badMs = performance.now() - tBad;
|
|
66
66
|
|
|
67
|
-
expect(await authenticateOperator(store, "ops@example.com", "
|
|
67
|
+
expect(await authenticateOperator(store, "ops@example.com", "password1234")).not.toBeNull();
|
|
68
68
|
|
|
69
69
|
// Both paths pay an argon2 verify — neither should be near-instant.
|
|
70
70
|
expect(missingMs).toBeGreaterThan(5);
|
|
@@ -2,18 +2,29 @@
|
|
|
2
2
|
* Local HTTP bindings that do not touch the global {@link on} registry.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { gate } from "../../elements/gate.ts";
|
|
5
6
|
import type { AnyFlowDef } from "../../kernel/flow.ts";
|
|
6
7
|
import type { Binding } from "../../kernel/on.ts";
|
|
7
8
|
import { normalizeTrigger, type HttpTrigger, type Trigger } from "../../kernel/triggers.ts";
|
|
9
|
+
import { consoleOperatorGate } from "./console-gates.ts";
|
|
10
|
+
import { PUBLIC_CONSOLE_FLOWS } from "./public-flows.ts";
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Bind an HTTP trigger to a flow without registering on the global `on` list.
|
|
14
|
+
* Attaches {@link gate.public} or {@link consoleOperatorGate} when the trigger
|
|
15
|
+
* has no declared auth posture yet.
|
|
11
16
|
*
|
|
12
17
|
* @param trigger - HTTP trigger
|
|
13
18
|
* @param flowDef - Flow definition
|
|
14
19
|
*/
|
|
15
20
|
export function bindHttp(trigger: HttpTrigger, flowDef: AnyFlowDef): Binding {
|
|
16
|
-
const
|
|
21
|
+
const withPosture =
|
|
22
|
+
trigger.gates.length > 0
|
|
23
|
+
? trigger
|
|
24
|
+
: PUBLIC_CONSOLE_FLOWS.has(flowDef.name)
|
|
25
|
+
? trigger.gate(gate.public)
|
|
26
|
+
: trigger.gate(consoleOperatorGate);
|
|
27
|
+
const normalized = normalizeTrigger(withPosture);
|
|
17
28
|
const list = flowDef.triggers as Trigger[];
|
|
18
29
|
list.push(normalized);
|
|
19
30
|
(flowDef as { $trigger: Trigger }).$trigger = normalized;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console HTTP auth-posture gates — public sentinel vs operator policy.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { gate, type PolicyGateDecl } from "../../elements/gate.ts";
|
|
6
|
+
|
|
7
|
+
/** Operator must be present (Console plane). */
|
|
8
|
+
export const consoleOperatorGate: PolicyGateDecl = gate.policy(
|
|
9
|
+
"console:operator",
|
|
10
|
+
({ operator }) => operator.id !== null,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/** Gates registered on the Console app runtime. */
|
|
14
|
+
export const CONSOLE_GATES = [gate.public, consoleOperatorGate] as const;
|
|
@@ -69,7 +69,7 @@ describe("console kernel", () => {
|
|
|
69
69
|
claimCode: code,
|
|
70
70
|
email: "ops@example.com",
|
|
71
71
|
name: "Ops",
|
|
72
|
-
password: "
|
|
72
|
+
password: "password1234",
|
|
73
73
|
}),
|
|
74
74
|
}),
|
|
75
75
|
);
|
|
@@ -89,7 +89,7 @@ describe("console kernel", () => {
|
|
|
89
89
|
claimCode: code,
|
|
90
90
|
email: "other@example.com",
|
|
91
91
|
name: "Other",
|
|
92
|
-
password: "
|
|
92
|
+
password: "password1234",
|
|
93
93
|
}),
|
|
94
94
|
}),
|
|
95
95
|
);
|
|
@@ -127,7 +127,7 @@ describe("console kernel", () => {
|
|
|
127
127
|
claimCode: handle.state.claim.code,
|
|
128
128
|
email: "ops@example.com",
|
|
129
129
|
name: "Ops",
|
|
130
|
-
password: "
|
|
130
|
+
password: "password1234",
|
|
131
131
|
}),
|
|
132
132
|
}),
|
|
133
133
|
);
|
|
@@ -175,7 +175,7 @@ describe("console kernel", () => {
|
|
|
175
175
|
claimCode: handle.state.claim.code,
|
|
176
176
|
email: "ops@example.com",
|
|
177
177
|
name: "Ops",
|
|
178
|
-
password: "
|
|
178
|
+
password: "password1234",
|
|
179
179
|
}),
|
|
180
180
|
}),
|
|
181
181
|
);
|
|
@@ -223,7 +223,7 @@ describe("console kernel", () => {
|
|
|
223
223
|
claimCode: handle.state.claim.code,
|
|
224
224
|
email: "ops@example.com",
|
|
225
225
|
name: "Ops",
|
|
226
|
-
password: "
|
|
226
|
+
password: "password1234",
|
|
227
227
|
}),
|
|
228
228
|
}),
|
|
229
229
|
);
|
|
@@ -317,7 +317,7 @@ describe("console serve security", () => {
|
|
|
317
317
|
claimCode: server.console.state.claim.code,
|
|
318
318
|
email: "ops@example.com",
|
|
319
319
|
name: "Ops",
|
|
320
|
-
password: "
|
|
320
|
+
password: "password1234",
|
|
321
321
|
}),
|
|
322
322
|
}),
|
|
323
323
|
);
|
|
@@ -31,7 +31,7 @@ describe("console flows invoke", () => {
|
|
|
31
31
|
claimCode: code,
|
|
32
32
|
email: "ops@example.com",
|
|
33
33
|
name: "Ops",
|
|
34
|
-
password: "
|
|
34
|
+
password: "password1234",
|
|
35
35
|
}),
|
|
36
36
|
}),
|
|
37
37
|
);
|
|
@@ -100,7 +100,7 @@ describe("console flows invoke", () => {
|
|
|
100
100
|
claimCode: code,
|
|
101
101
|
email: "ops@example.com",
|
|
102
102
|
name: "Ops",
|
|
103
|
-
password: "
|
|
103
|
+
password: "password1234",
|
|
104
104
|
}),
|
|
105
105
|
}),
|
|
106
106
|
);
|