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
package/src/kernel/fx.test.ts
CHANGED
package/src/kernel/fx.ts
CHANGED
|
@@ -72,6 +72,7 @@ import type { RunsRuntime } from "../runs/runtime.ts";
|
|
|
72
72
|
import type { RunsRow, WideEvent } from "../runs/types.ts";
|
|
73
73
|
import type { RunWindowStats, SloBreach } from "../runs/window.ts";
|
|
74
74
|
import type { MessageCatalogs } from "../i18n/messages.ts";
|
|
75
|
+
import { okid } from "../okid.ts";
|
|
75
76
|
import { lazyRequire } from "./lazy-require.ts";
|
|
76
77
|
import type { AppMessageKey, MessageValues } from "../i18n/types.ts";
|
|
77
78
|
|
|
@@ -853,6 +854,14 @@ export interface Fx {
|
|
|
853
854
|
* Outside `all` / `race`, a never-aborted signal.
|
|
854
855
|
*/
|
|
855
856
|
readonly signal: AbortSignal;
|
|
857
|
+
/**
|
|
858
|
+
* Resolved RLS identity for this invocation (`undefined` when bypassed,
|
|
859
|
+
* operator-plane, or unauthenticated on an un-gated trigger). The live
|
|
860
|
+
* query stack subscribes SSE streams under this exact stamp so per-event
|
|
861
|
+
* visibility re-checks are Postgres-authoritative — identical to what a
|
|
862
|
+
* read through {@link Fx.store} would see.
|
|
863
|
+
*/
|
|
864
|
+
readonly rlsIdentity: import("../drivers/pg-rls.ts").RlsIdentity | undefined;
|
|
856
865
|
/**
|
|
857
866
|
* Run thunks in parallel. On first rejection, abort sibling branches and
|
|
858
867
|
* rethrow. Pass thunks (not started Promises) so abort scopes exist first.
|
|
@@ -1197,7 +1206,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1197
1206
|
},
|
|
1198
1207
|
insert(row: Record<string, unknown>): Promise<{ id: string }> {
|
|
1199
1208
|
return gated("write", ref, () => {
|
|
1200
|
-
const id = typeof row.id === "string" ? row.id :
|
|
1209
|
+
const id = typeof row.id === "string" ? row.id : okid();
|
|
1201
1210
|
table().set(id, { ...row, id });
|
|
1202
1211
|
return { id };
|
|
1203
1212
|
});
|
|
@@ -2088,7 +2097,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
2088
2097
|
},
|
|
2089
2098
|
locale,
|
|
2090
2099
|
id() {
|
|
2091
|
-
return
|
|
2100
|
+
return okid();
|
|
2092
2101
|
},
|
|
2093
2102
|
auth,
|
|
2094
2103
|
operator,
|
|
@@ -2130,6 +2139,9 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
2130
2139
|
get signal(): AbortSignal {
|
|
2131
2140
|
return currentAbortSignal();
|
|
2132
2141
|
},
|
|
2142
|
+
get rlsIdentity() {
|
|
2143
|
+
return rlsInvokeContext().rls;
|
|
2144
|
+
},
|
|
2133
2145
|
all(thunks) {
|
|
2134
2146
|
return fxAll(thunks);
|
|
2135
2147
|
},
|
|
@@ -38,6 +38,7 @@ import { http } from "./triggers.ts";
|
|
|
38
38
|
import { GATE_KV_NAMESPACE } from "./boot-bind/gate.ts";
|
|
39
39
|
import { createBunRuntime } from "../runtime/bun.ts";
|
|
40
40
|
import { installGracefulShutdown } from "./graceful-shutdown.ts";
|
|
41
|
+
import { okid } from "../okid.ts";
|
|
41
42
|
|
|
42
43
|
const mode = process.argv[2];
|
|
43
44
|
if (mode !== "serve") {
|
|
@@ -139,7 +140,7 @@ const rate = flow("horizontal.rate", {
|
|
|
139
140
|
});
|
|
140
141
|
const write = flow("horizontal.write", {
|
|
141
142
|
do: async (_input, fx) => {
|
|
142
|
-
const id =
|
|
143
|
+
const id = okid();
|
|
143
144
|
await fx
|
|
144
145
|
.store(db)
|
|
145
146
|
.insert(writesTable)
|
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
type HttpTrigger,
|
|
14
14
|
type ResourceFlowBag,
|
|
15
15
|
type ResourceMount,
|
|
16
|
+
type SignalSource,
|
|
16
17
|
} from "./triggers.ts";
|
|
18
|
+
import { registerPendingResourceLiveMount } from "./resource-live.ts";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Mount a CRUD resource at `path`: `list`/`create` on the base, `get` /
|
|
@@ -31,16 +33,40 @@ export function httpResource<P extends string>(
|
|
|
31
33
|
const id = `${path}/:id` as `${P}/:id`;
|
|
32
34
|
const verb = <M extends HttpMethod>(method: M, p: P | `${P}/:id`): HttpTrigger<M> =>
|
|
33
35
|
createHttpTrigger(method, p, gates);
|
|
36
|
+
// Live exposure — synthesized from the resource's live surface so SSE
|
|
37
|
+
// rides the same gates as the CRUD verbs.
|
|
38
|
+
const liveFlow = (ops as { readonly live?: { readonly signal: string; readonly flow: unknown } })
|
|
39
|
+
.live;
|
|
40
|
+
// `store.resource(…, { live: omitted })` puts a pending surface on the ops
|
|
41
|
+
// bag. `http.resource` records the mount slot; `oke()` drains it only when
|
|
42
|
+
// the project-wide `store.live` flag is on.
|
|
43
|
+
const pendingSignal = (ops as { readonly pendingLive?: { readonly signalName: string } })
|
|
44
|
+
.pendingLive?.signalName;
|
|
45
|
+
if (pendingSignal !== undefined && liveFlow === undefined) {
|
|
46
|
+
registerPendingResourceLiveMount({ path, gates, signalName: pendingSignal });
|
|
47
|
+
}
|
|
48
|
+
const mounts = [
|
|
49
|
+
{ trigger: verb("GET", path), flow: ops.list },
|
|
50
|
+
{ trigger: verb("POST", path), flow: ops.create },
|
|
51
|
+
{ trigger: verb("GET", id), flow: ops.get },
|
|
52
|
+
{ trigger: verb("PATCH", id), flow: ops.update },
|
|
53
|
+
{ trigger: verb("DELETE", id), flow: ops.remove },
|
|
54
|
+
...(liveFlow !== undefined
|
|
55
|
+
? [
|
|
56
|
+
{
|
|
57
|
+
trigger: createHttpTrigger("GET" as const, `${path}/live`, gates, {
|
|
58
|
+
name: liveFlow.signal,
|
|
59
|
+
} as SignalSource),
|
|
60
|
+
flow: liveFlow.flow,
|
|
61
|
+
},
|
|
62
|
+
]
|
|
63
|
+
: []),
|
|
64
|
+
];
|
|
34
65
|
const mount: ResourceMount = {
|
|
35
66
|
[resourceMountBrand]: true,
|
|
36
67
|
gates,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{ trigger: verb("POST", path), flow: ops.create },
|
|
40
|
-
{ trigger: verb("GET", id), flow: ops.get },
|
|
41
|
-
{ trigger: verb("PATCH", id), flow: ops.update },
|
|
42
|
-
{ trigger: verb("DELETE", id), flow: ops.remove },
|
|
43
|
-
],
|
|
68
|
+
...(liveFlow !== undefined ? { live: { signal: liveFlow.signal, flow: liveFlow.flow } } : {}),
|
|
69
|
+
mounts,
|
|
44
70
|
gate: createGateAttach((next) => httpResource(path, ops, next), gates),
|
|
45
71
|
public() {
|
|
46
72
|
return httpResource(path, ops, [...gates, GATE_PUBLIC_NAME]);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host identity durability — standalone `oke()` (no Console).
|
|
3
|
+
*
|
|
4
|
+
* Creates a user via the real `/auth/sign-up/email` path, stops, boots a fresh
|
|
5
|
+
* app against the same SQL URL, and resolves the same user on `/auth/sign-in/email`.
|
|
6
|
+
* Production uses postgres + `DATABASE_URL`; this suite uses file-backed
|
|
7
|
+
* PGlite at that same URL so the restart is hermetic.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
11
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { AUTH_TABLES } from "../auth/tables.ts";
|
|
15
|
+
import { gate } from "../elements/gate.ts";
|
|
16
|
+
import { resetStores, store } from "../elements/store/declare.ts";
|
|
17
|
+
import { oke } from "./app.ts";
|
|
18
|
+
import { resetFlowSeq } from "./flow.ts";
|
|
19
|
+
import { resetBindings } from "./on.ts";
|
|
20
|
+
|
|
21
|
+
const PEPPER = "oke-host-identity-persist-pepper";
|
|
22
|
+
const member = gate.policy("member", ({ auth }) => !!auth.verified);
|
|
23
|
+
|
|
24
|
+
function resetIsolated(): void {
|
|
25
|
+
resetBindings();
|
|
26
|
+
resetFlowSeq();
|
|
27
|
+
resetStores();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
resetIsolated();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("host identity persist via store.sql()", () => {
|
|
35
|
+
test("standalone oke() persists users+credentials across restart; sign-in resolves on a fresh store", async () => {
|
|
36
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-host-identity-"));
|
|
37
|
+
const sqlUrl = join(dir, "pgdata");
|
|
38
|
+
const prevDb = process.env.DATABASE_URL;
|
|
39
|
+
const prevPglite = process.env.OKE_PGLITE_URL;
|
|
40
|
+
process.env.DATABASE_URL = sqlUrl;
|
|
41
|
+
process.env.OKE_PGLITE_URL = sqlUrl;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
resetIsolated();
|
|
45
|
+
const db1 = store.sql("app", { schema: {} });
|
|
46
|
+
const first = oke({
|
|
47
|
+
name: "host-identity-first",
|
|
48
|
+
stores: [db1],
|
|
49
|
+
gate: {
|
|
50
|
+
auth: { secret: PEPPER, emailAndPassword: { enabled: true } },
|
|
51
|
+
policies: [member],
|
|
52
|
+
},
|
|
53
|
+
env: "test",
|
|
54
|
+
startScheduler: false,
|
|
55
|
+
});
|
|
56
|
+
await first.boot({ env: "test", startScheduler: false });
|
|
57
|
+
|
|
58
|
+
const signUp = await first.fetch(
|
|
59
|
+
new Request("http://localhost/auth/sign-up/email", {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "content-type": "application/json" },
|
|
62
|
+
body: JSON.stringify({ email: "persist@example.com", password: "CorrectHorse1!" }),
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
expect(signUp.status).toBe(200);
|
|
66
|
+
const signedUp = (await signUp.json()) as {
|
|
67
|
+
data: { accessToken: string; userId: string };
|
|
68
|
+
};
|
|
69
|
+
expect(signedUp.data.userId).toBeTruthy();
|
|
70
|
+
|
|
71
|
+
const firstStore = first.bootResult?.store;
|
|
72
|
+
await first.stop();
|
|
73
|
+
await firstStore?.close();
|
|
74
|
+
|
|
75
|
+
resetIsolated();
|
|
76
|
+
const db2 = store.sql("app", { schema: {} });
|
|
77
|
+
const second = oke({
|
|
78
|
+
name: "host-identity-second",
|
|
79
|
+
stores: [db2],
|
|
80
|
+
gate: {
|
|
81
|
+
auth: { secret: PEPPER, emailAndPassword: { enabled: true } },
|
|
82
|
+
policies: [member],
|
|
83
|
+
},
|
|
84
|
+
env: "test",
|
|
85
|
+
startScheduler: false,
|
|
86
|
+
});
|
|
87
|
+
await second.boot({ env: "test", startScheduler: false });
|
|
88
|
+
|
|
89
|
+
const signIn = await second.fetch(
|
|
90
|
+
new Request("http://localhost/auth/sign-in/email", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "content-type": "application/json" },
|
|
93
|
+
body: JSON.stringify({ email: "persist@example.com", password: "CorrectHorse1!" }),
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
expect(signIn.status).toBe(200);
|
|
97
|
+
const session = (await signIn.json()) as { data: { userId: string } };
|
|
98
|
+
expect(session.data.userId).toBe(signedUp.data.userId);
|
|
99
|
+
|
|
100
|
+
const conn = await second.bootResult?.store?.primarySql();
|
|
101
|
+
const identities = await conn?.query(`SELECT id, email FROM ${AUTH_TABLES.identities}`);
|
|
102
|
+
expect(identities?.some((r) => r.email === "persist@example.com")).toBe(true);
|
|
103
|
+
const credentials = await conn?.query(
|
|
104
|
+
`SELECT user_id, provider, provider_account_id FROM ${AUTH_TABLES.credentials}`,
|
|
105
|
+
);
|
|
106
|
+
expect(credentials?.some((r) => r.provider === "credential")).toBe(true);
|
|
107
|
+
|
|
108
|
+
const secondStore = second.bootResult?.store;
|
|
109
|
+
await second.stop();
|
|
110
|
+
await secondStore?.close();
|
|
111
|
+
} finally {
|
|
112
|
+
if (prevDb === undefined) delete process.env.DATABASE_URL;
|
|
113
|
+
else process.env.DATABASE_URL = prevDb;
|
|
114
|
+
if (prevPglite === undefined) delete process.env.OKE_PGLITE_URL;
|
|
115
|
+
else process.env.OKE_PGLITE_URL = prevPglite;
|
|
116
|
+
await rm(dir, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
}, 20_000);
|
|
119
|
+
});
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
* and the fleet registry.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { okid } from "../okid.ts";
|
|
7
|
+
|
|
6
8
|
/** Prefix for process instance ids minted at boot. */
|
|
7
9
|
export const INSTANCE_ID_PREFIX = "inst-";
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
|
-
* Mint a process instance id (`inst-<
|
|
12
|
+
* Mint a process instance id (`inst-<okid>`).
|
|
11
13
|
*/
|
|
12
14
|
export function mintInstanceId(): string {
|
|
13
|
-
return `${INSTANCE_ID_PREFIX}${
|
|
15
|
+
return `${INSTANCE_ID_PREFIX}${okid()}`;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
package/src/kernel/journal.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { mkdir } from "node:fs/promises";
|
|
11
11
|
import { dirname } from "node:path";
|
|
12
|
+
import { okid } from "../okid.ts";
|
|
12
13
|
import { JournalSuspend } from "./journal-suspend.ts";
|
|
13
14
|
|
|
14
15
|
export { JournalSuspend, isJournalSuspend } from "./journal-suspend.ts";
|
|
@@ -452,7 +453,7 @@ export interface Journal {
|
|
|
452
453
|
*/
|
|
453
454
|
export function createJournal(options: CreateJournalOptions): Journal {
|
|
454
455
|
const now = options.now ?? (() => Date.now());
|
|
455
|
-
const newId = options.id ?? (() =>
|
|
456
|
+
const newId = options.id ?? (() => okid());
|
|
456
457
|
const lease = options.lease;
|
|
457
458
|
const coordinated = lease !== undefined && hasJournalLease(options.store);
|
|
458
459
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool trigger — name validation, gated registration lookup,
|
|
3
|
+
* duplicate-name boot failure (OKE1018), gate posture, and Manifest stamping.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test, beforeEach } from "bun:test";
|
|
7
|
+
import { GateBootError } from "../elements/gate/boot.ts";
|
|
8
|
+
import { extractFromSources } from "../compiler/extract.ts";
|
|
9
|
+
import { gate } from "../elements/gate.ts";
|
|
10
|
+
import { oke } from "./app.ts";
|
|
11
|
+
import { flow, resetFlowSeq } from "./flow.ts";
|
|
12
|
+
import { on, resetBindings } from "./on.ts";
|
|
13
|
+
import { mcp } from "./triggers.ts";
|
|
14
|
+
|
|
15
|
+
const member = gate.policy("member", ({ auth }) => !!auth.verified);
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
resetBindings();
|
|
19
|
+
resetFlowSeq();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("mcp.tool(name)", () => {
|
|
23
|
+
test("empty or missing name throws TypeError", () => {
|
|
24
|
+
expect(() => mcp.tool("")).toThrow(/name is required/);
|
|
25
|
+
expect(() => mcp.tool(" ")).toThrow(/name is required/);
|
|
26
|
+
expect(() => (mcp.tool as (name?: string) => ReturnType<typeof mcp.tool>)()).toThrow(
|
|
27
|
+
/name is required/,
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("gated registration is indexed by exact tool name", () => {
|
|
32
|
+
const toolFlow = on(
|
|
33
|
+
mcp.tool("unit.action").gate(member),
|
|
34
|
+
flow("unit.action", {
|
|
35
|
+
plane: "user",
|
|
36
|
+
do: () => ({ ok: true }),
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(toolFlow.$trigger?.kind).toBe("mcp");
|
|
41
|
+
expect(toolFlow.$trigger && toolFlow.$trigger.kind === "mcp" && toolFlow.$trigger.name).toBe(
|
|
42
|
+
"unit.action",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const app = oke({ name: "t", autoBoot: false });
|
|
46
|
+
expect(app.resolveMcpTool("unit.action")).toBe(toolFlow);
|
|
47
|
+
expect(app.resolveMcpTool("other.action")).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("duplicate tool name fails OKE1018 at construction", () => {
|
|
51
|
+
on(
|
|
52
|
+
mcp.tool("bookings.create").gate(member),
|
|
53
|
+
flow("bookings.create", { plane: "user", do: () => ({ ok: true }) }),
|
|
54
|
+
);
|
|
55
|
+
on(
|
|
56
|
+
mcp.tool("bookings.create").gate(member),
|
|
57
|
+
flow("bookings.also", { plane: "user", do: () => ({ ok: true }) }),
|
|
58
|
+
);
|
|
59
|
+
expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1018/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("ungated mcp.tool passes on() but fails gate posture at boot", async () => {
|
|
63
|
+
on(
|
|
64
|
+
mcp.tool("bookings.create"),
|
|
65
|
+
flow("bookings.create", { plane: "user", do: () => ({ ok: true }) }),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const app = oke({ name: "t", autoBoot: false });
|
|
69
|
+
expect(app.resolveMcpTool("bookings.create")).toBeUndefined();
|
|
70
|
+
await expect(app.boot()).rejects.toThrow(GateBootError);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("mcp.tool — Manifest", () => {
|
|
75
|
+
test("trigger.mcp.name and gates are stamped on the flow", async () => {
|
|
76
|
+
const source = `
|
|
77
|
+
import { on, flow, gate, mcp } from "okengine";
|
|
78
|
+
|
|
79
|
+
export const member = gate.policy("member", ({ auth }) => !!auth.verified);
|
|
80
|
+
|
|
81
|
+
export const createBooking = on(
|
|
82
|
+
mcp.tool("bookings.create").gate(member),
|
|
83
|
+
flow("bookings.create", {
|
|
84
|
+
plane: "user",
|
|
85
|
+
do: () => ({ ok: true }),
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
`;
|
|
89
|
+
const manifest = await extractFromSources({ "src/flows/bookings/index.ts": source });
|
|
90
|
+
const flowDef = manifest.flows?.["bookings.create"];
|
|
91
|
+
expect(flowDef?.trigger).toEqual({ mcp: { name: "bookings.create" } });
|
|
92
|
+
expect(flowDef?.gates).toEqual(["member"]);
|
|
93
|
+
expect(flowDef?.plane).toBe("user");
|
|
94
|
+
});
|
|
95
|
+
});
|
package/src/kernel/on.ts
CHANGED
|
@@ -91,6 +91,9 @@ export function on(
|
|
|
91
91
|
};
|
|
92
92
|
const ops: Record<string, unknown> = {};
|
|
93
93
|
for (const { trigger, flow } of triggerOrMount.mounts) {
|
|
94
|
+
// Live SSE exposure binds through the normal `on()` path below (its
|
|
95
|
+
// trigger carries a liveSignal) — it is not one of the five CRUD ops.
|
|
96
|
+
if (trigger.liveSignal !== undefined) continue;
|
|
94
97
|
const key = trigger.path.endsWith("/:id")
|
|
95
98
|
? byIdVerb[trigger.method]
|
|
96
99
|
: baseVerb[trigger.method];
|
|
@@ -100,6 +103,12 @@ export function on(
|
|
|
100
103
|
on(trigger, flow as FlowDef<any, any, any, any, Trigger | undefined>);
|
|
101
104
|
ops[key] = flow;
|
|
102
105
|
}
|
|
106
|
+
// Live surface — the `<path>/live` mount carries a liveSignal; bind it
|
|
107
|
+
// through `on()` so SSE synthesis + exposure-uniqueness checks run once.
|
|
108
|
+
const liveMount = triggerOrMount.mounts.find((m) => m.trigger.liveSignal !== undefined);
|
|
109
|
+
if (liveMount !== undefined && isFlow(liveMount.flow)) {
|
|
110
|
+
on(liveMount.trigger, liveMount.flow as FlowDef<any, any, any, any, Trigger | undefined>);
|
|
111
|
+
}
|
|
103
112
|
return ops as unknown as ResourceFlowBag;
|
|
104
113
|
}
|
|
105
114
|
const asHttp =
|