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
|
@@ -92,6 +92,64 @@ describe("fx.race — structured concurrency", () => {
|
|
|
92
92
|
});
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
describe("fx.using — scoped cleanup", () => {
|
|
96
|
+
test("release runs on success and on thrown error", async () => {
|
|
97
|
+
const fx = createFx({ flow: "t", effects: {} });
|
|
98
|
+
const released: string[] = [];
|
|
99
|
+
|
|
100
|
+
const ok = await fx.using(
|
|
101
|
+
async () => "res-a",
|
|
102
|
+
(r) => {
|
|
103
|
+
released.push(`ok:${r}`);
|
|
104
|
+
},
|
|
105
|
+
async (r) => r.toUpperCase(),
|
|
106
|
+
);
|
|
107
|
+
expect(ok).toBe("RES-A");
|
|
108
|
+
expect(released).toEqual(["ok:res-a"]);
|
|
109
|
+
|
|
110
|
+
await expect(
|
|
111
|
+
fx.using(
|
|
112
|
+
async () => "res-b",
|
|
113
|
+
async (r) => {
|
|
114
|
+
released.push(`err:${r}`);
|
|
115
|
+
},
|
|
116
|
+
async () => {
|
|
117
|
+
throw new Error("boom");
|
|
118
|
+
},
|
|
119
|
+
),
|
|
120
|
+
).rejects.toThrow("boom");
|
|
121
|
+
expect(released).toEqual(["ok:res-a", "err:res-b"]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("release runs when a sibling fx.race winner aborts mid-use", async () => {
|
|
125
|
+
const fx = createFx({ flow: "t", effects: {} });
|
|
126
|
+
let released = false;
|
|
127
|
+
|
|
128
|
+
const winner = await fx.race([
|
|
129
|
+
() =>
|
|
130
|
+
fx.using(
|
|
131
|
+
async () => "lock",
|
|
132
|
+
() => {
|
|
133
|
+
released = true;
|
|
134
|
+
},
|
|
135
|
+
() =>
|
|
136
|
+
new Promise<never>(() => {
|
|
137
|
+
// Stay pending until ambient abort releases us.
|
|
138
|
+
}),
|
|
139
|
+
),
|
|
140
|
+
async () => {
|
|
141
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
142
|
+
return "fast";
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
expect(winner).toBe("fast");
|
|
147
|
+
// Let the abort listener settle.
|
|
148
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
149
|
+
expect(released).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
95
153
|
describe("fx.retry", () => {
|
|
96
154
|
test("retries then succeeds", async () => {
|
|
97
155
|
const fx = createFx({ flow: "t", effects: {} });
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { parseDurationMs } from "../elements/clock/duration.ts";
|
|
9
9
|
import {
|
|
10
10
|
abortableSleep,
|
|
11
|
+
abortError,
|
|
11
12
|
currentAbortSignal,
|
|
12
13
|
isAbortError,
|
|
13
14
|
linkAbort,
|
|
@@ -144,6 +145,53 @@ export async function fxRace<T>(thunks: ReadonlyArray<FxThunk<T>>): Promise<T> {
|
|
|
144
145
|
});
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Scope a resource to a unit of work — `release` runs exactly once when
|
|
150
|
+
* `use` settles or when the ambient abort signal fires, whichever comes
|
|
151
|
+
* first. Uses the same AbortSignal as {@link Fx.signal}; no second
|
|
152
|
+
* cancellation channel.
|
|
153
|
+
*
|
|
154
|
+
* Process-local only: do not journal acquire/release, and do not hold
|
|
155
|
+
* handles across durable park/resume — journal replay returns values and
|
|
156
|
+
* never re-enters step bodies.
|
|
157
|
+
*
|
|
158
|
+
* @param acquire - Open the resource
|
|
159
|
+
* @param release - Cleanup, always run
|
|
160
|
+
* @param use - Work with the resource
|
|
161
|
+
*/
|
|
162
|
+
export async function fxUsing<A, T>(
|
|
163
|
+
acquire: () => A | Promise<A>,
|
|
164
|
+
release: (resource: A) => void | Promise<void>,
|
|
165
|
+
use: (resource: A) => T | Promise<T>,
|
|
166
|
+
): Promise<T> {
|
|
167
|
+
const resource = await acquire();
|
|
168
|
+
const signal = currentAbortSignal();
|
|
169
|
+
let released = false;
|
|
170
|
+
|
|
171
|
+
const releaseOnce = async (): Promise<void> => {
|
|
172
|
+
if (released) return;
|
|
173
|
+
released = true;
|
|
174
|
+
await release(resource);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const onAbort = (): void => {
|
|
178
|
+
void releaseOnce();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (signal.aborted) {
|
|
182
|
+
await releaseOnce();
|
|
183
|
+
throw abortError(signal.reason);
|
|
184
|
+
}
|
|
185
|
+
signal.addEventListener("abort", onAbort);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
return await use(resource);
|
|
189
|
+
} finally {
|
|
190
|
+
signal.removeEventListener("abort", onAbort);
|
|
191
|
+
await releaseOnce();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
147
195
|
/**
|
|
148
196
|
* Retry a thunk with exponential backoff and optional full jitter.
|
|
149
197
|
*
|
package/src/kernel/edge.test.ts
CHANGED
|
@@ -26,7 +26,7 @@ describe("plugin edge handlers", () => {
|
|
|
26
26
|
}
|
|
27
27
|
return undefined;
|
|
28
28
|
});
|
|
29
|
-
const app = oke({ name: "edge" }).plug(edge);
|
|
29
|
+
const app = oke({ autoBoot: false, name: "edge" }).plug(edge);
|
|
30
30
|
|
|
31
31
|
const preflight = await app.fetch(new Request("http://localhost/x", { method: "OPTIONS" }));
|
|
32
32
|
expect(preflight.status).toBe(204);
|
|
@@ -42,13 +42,13 @@ describe("plugin edge handlers", () => {
|
|
|
42
42
|
const answer = plugin("edge-answer", { version: "0.0.1" }).edge(
|
|
43
43
|
() => new Response("answered", { status: 418 }),
|
|
44
44
|
);
|
|
45
|
-
const app = oke({ name: "edge-order" }).plug(pass).plug(answer);
|
|
45
|
+
const app = oke({ autoBoot: false, name: "edge-order" }).plug(pass).plug(answer);
|
|
46
46
|
|
|
47
47
|
const res = await app.fetch(new Request("http://localhost/nowhere", { method: "OPTIONS" }));
|
|
48
48
|
expect(res.status).toBe(418);
|
|
49
49
|
expect(await res.text()).toBe("answered");
|
|
50
50
|
|
|
51
|
-
const noPlugins = oke({ name: "edge-none" });
|
|
51
|
+
const noPlugins = oke({ autoBoot: false, name: "edge-none" });
|
|
52
52
|
const missing = await noPlugins.fetch(
|
|
53
53
|
new Request("http://localhost/nowhere", { method: "OPTIONS" }),
|
|
54
54
|
);
|
package/src/kernel/flow.test.ts
CHANGED
|
@@ -45,7 +45,7 @@ describe("flow — one species", () => {
|
|
|
45
45
|
on(http.post("/work"), shared);
|
|
46
46
|
on(signal, shared);
|
|
47
47
|
|
|
48
|
-
const app = oke({ name: "one-species" });
|
|
48
|
+
const app = oke({ autoBoot: false, name: "one-species" });
|
|
49
49
|
expect(app.bindings).toHaveLength(2);
|
|
50
50
|
expect(app.bindings[0]?.flow).toBe(shared);
|
|
51
51
|
expect(app.bindings[1]?.flow).toBe(shared);
|
|
@@ -115,7 +115,7 @@ describe("five trigger kinds", () => {
|
|
|
115
115
|
}),
|
|
116
116
|
);
|
|
117
117
|
|
|
118
|
-
const app = oke({ name: "five" });
|
|
118
|
+
const app = oke({ autoBoot: false, name: "five" });
|
|
119
119
|
|
|
120
120
|
const res = await app.fetch(new Request("http://localhost/abc", { method: "GET" }));
|
|
121
121
|
expect(res.status).toBe(200);
|
package/src/kernel/fx.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type Fx,
|
|
10
10
|
type FxStubStoreHandle,
|
|
11
11
|
} from "./fx.ts";
|
|
12
|
+
import { Redacted } from "./redacted.ts";
|
|
12
13
|
|
|
13
14
|
/** Narrow stub handle for tests that exercise the in-memory store. */
|
|
14
15
|
function stub(fx: Fx, ref: string): FxStubStoreHandle {
|
|
@@ -99,7 +100,7 @@ describe("fx — effect ledger", () => {
|
|
|
99
100
|
await fx.emit("order-placed", {});
|
|
100
101
|
await fx.send("order-confirmed", { to: "u1" });
|
|
101
102
|
await fx.ask("triage@1", {});
|
|
102
|
-
expect(fx.vault("STRIPE_KEY")).toBe("sk_test");
|
|
103
|
+
expect(fx.vault("STRIPE_KEY").reveal()).toBe("sk_test");
|
|
103
104
|
await fx.call("payments.charge", {});
|
|
104
105
|
|
|
105
106
|
expect(ledger.entries).toHaveLength(7);
|
|
@@ -130,7 +131,7 @@ describe("fx — wholesale swap", () => {
|
|
|
130
131
|
sleep: async () => undefined,
|
|
131
132
|
},
|
|
132
133
|
vault() {
|
|
133
|
-
return "";
|
|
134
|
+
return new Redacted("");
|
|
134
135
|
},
|
|
135
136
|
cache: {
|
|
136
137
|
get: async () => undefined,
|
|
@@ -162,6 +163,7 @@ describe("fx — wholesale swap", () => {
|
|
|
162
163
|
id: () => "fixed-id",
|
|
163
164
|
auth: { userId: "u1", scopes: new Set(["a"]) },
|
|
164
165
|
operator: { id: null },
|
|
166
|
+
principal: { userId: "u1", operatorId: null, scopes: new Set(["a"]) },
|
|
165
167
|
tenant: { id: "t1" },
|
|
166
168
|
fail,
|
|
167
169
|
json: {
|
|
@@ -193,6 +195,14 @@ describe("fx — wholesale swap", () => {
|
|
|
193
195
|
retry(fn) {
|
|
194
196
|
return Promise.resolve().then(fn);
|
|
195
197
|
},
|
|
198
|
+
async using(acquire, release, use) {
|
|
199
|
+
const resource = await acquire();
|
|
200
|
+
try {
|
|
201
|
+
return await use(resource);
|
|
202
|
+
} finally {
|
|
203
|
+
await release(resource);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
196
206
|
};
|
|
197
207
|
|
|
198
208
|
// Any code that accepts `Fx` can run against a total replacement.
|
package/src/kernel/fx.ts
CHANGED
|
@@ -33,7 +33,15 @@ import {
|
|
|
33
33
|
} from "./dry-run.ts";
|
|
34
34
|
import { fail, type FailOptions, type FlowFailure } from "./errors.ts";
|
|
35
35
|
import { currentAbortSignal } from "./abort-scope.ts";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
fxAll,
|
|
38
|
+
fxRace,
|
|
39
|
+
fxRetry,
|
|
40
|
+
fxUsing,
|
|
41
|
+
type FxRetryOptions,
|
|
42
|
+
type FxThunk,
|
|
43
|
+
} from "./concurrency.ts";
|
|
44
|
+
import { maskRedactedDeep, Redacted } from "./redacted.ts";
|
|
37
45
|
import type { JournalSession } from "./journal.ts";
|
|
38
46
|
import type { RunTelemetry } from "./run-telemetry.ts";
|
|
39
47
|
|
|
@@ -67,6 +75,33 @@ export interface FxOperator {
|
|
|
67
75
|
readonly id: string | null;
|
|
68
76
|
}
|
|
69
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Read-only originating identity for audit / attribution.
|
|
80
|
+
* Never consulted by gate evaluation — authorization stays on {@link Fx.auth}.
|
|
81
|
+
*/
|
|
82
|
+
export interface FxPrincipal {
|
|
83
|
+
readonly userId: string | null;
|
|
84
|
+
readonly operatorId: string | null;
|
|
85
|
+
readonly scopes: ReadonlySet<string>;
|
|
86
|
+
readonly verified?: boolean;
|
|
87
|
+
readonly plane?: "user" | "operator";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Freeze a principal snapshot for propagation across {@link Fx.call}.
|
|
92
|
+
*
|
|
93
|
+
* @param p - Live or frozen principal
|
|
94
|
+
*/
|
|
95
|
+
export function freezePrincipal(p: FxPrincipal): FxPrincipal {
|
|
96
|
+
return {
|
|
97
|
+
userId: p.userId,
|
|
98
|
+
operatorId: p.operatorId,
|
|
99
|
+
scopes: new Set(p.scopes),
|
|
100
|
+
...(p.verified !== undefined ? { verified: p.verified } : {}),
|
|
101
|
+
...(p.plane !== undefined ? { plane: p.plane } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
70
105
|
/** Active tenant (multi-tenancy as a dimension of `fx`). */
|
|
71
106
|
export interface FxTenant {
|
|
72
107
|
readonly id: string | null;
|
|
@@ -262,9 +297,13 @@ export interface Fx {
|
|
|
262
297
|
/**
|
|
263
298
|
* Read a vault secret (records `secret`).
|
|
264
299
|
*
|
|
300
|
+
* Returns a {@link Redacted} — printing / logging / serializing it yields a
|
|
301
|
+
* placeholder, never the value. Call `.reveal()` at the one boundary that
|
|
302
|
+
* needs the real value (e.g. passing a credential to a driver).
|
|
303
|
+
*
|
|
265
304
|
* @param secret - Secret name or handle
|
|
266
305
|
*/
|
|
267
|
-
vault(secret: NamedRef): string
|
|
306
|
+
vault(secret: NamedRef): Redacted<string>;
|
|
268
307
|
/** Cache surface. */
|
|
269
308
|
readonly cache: FxCache;
|
|
270
309
|
/**
|
|
@@ -322,6 +361,12 @@ export interface Fx {
|
|
|
322
361
|
readonly auth: FxAuth;
|
|
323
362
|
/** Operator-plane principal. */
|
|
324
363
|
readonly operator: FxOperator;
|
|
364
|
+
/**
|
|
365
|
+
* Read-only originating identity (audit / attribution).
|
|
366
|
+
* On HTTP entry, tracks the resolved principal; across {@link Fx.call},
|
|
367
|
+
* propagates explicitly — never copied into {@link Fx.auth}.
|
|
368
|
+
*/
|
|
369
|
+
readonly principal: FxPrincipal;
|
|
325
370
|
/** Active tenant. */
|
|
326
371
|
readonly tenant: FxTenant;
|
|
327
372
|
/**
|
|
@@ -369,6 +414,20 @@ export interface Fx {
|
|
|
369
414
|
* @param opts - Retry policy
|
|
370
415
|
*/
|
|
371
416
|
retry<T>(fn: FxThunk<T>, opts?: FxRetryOptions): Promise<T>;
|
|
417
|
+
/**
|
|
418
|
+
* Scope a resource to `use` — `release` runs exactly once when `use`
|
|
419
|
+
* settles or the ambient abort signal fires (e.g. a sibling `fx.race`
|
|
420
|
+
* winner). Same-attempt cleanup; not journaled.
|
|
421
|
+
*
|
|
422
|
+
* @param acquire - Open the resource
|
|
423
|
+
* @param release - Cleanup, always run
|
|
424
|
+
* @param use - Work with the resource
|
|
425
|
+
*/
|
|
426
|
+
using<A, T>(
|
|
427
|
+
acquire: () => A | Promise<A>,
|
|
428
|
+
release: (resource: A) => void | Promise<void>,
|
|
429
|
+
use: (resource: A) => T | Promise<T>,
|
|
430
|
+
): Promise<T>;
|
|
372
431
|
}
|
|
373
432
|
|
|
374
433
|
/**
|
|
@@ -393,6 +452,11 @@ export interface CreateFxOptions {
|
|
|
393
452
|
readonly auth?: FxAuth;
|
|
394
453
|
/** Operator principal. */
|
|
395
454
|
readonly operator?: FxOperator;
|
|
455
|
+
/**
|
|
456
|
+
* Frozen originating principal (e.g. propagated across {@link Fx.call}).
|
|
457
|
+
* When omitted, {@link Fx.principal} tracks live {@link Fx.auth} / {@link Fx.operator}.
|
|
458
|
+
*/
|
|
459
|
+
readonly principal?: FxPrincipal;
|
|
396
460
|
/** Tenant. */
|
|
397
461
|
readonly tenant?: FxTenant;
|
|
398
462
|
/** Secret name → value map for `fx.vault`. */
|
|
@@ -496,6 +560,27 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
496
560
|
};
|
|
497
561
|
const operator: FxOperator = options.operator ?? { id: null };
|
|
498
562
|
const tenant: FxTenant = options.tenant ?? { id: null };
|
|
563
|
+
const principal: FxPrincipal =
|
|
564
|
+
options.principal ??
|
|
565
|
+
({
|
|
566
|
+
get userId() {
|
|
567
|
+
return auth.userId;
|
|
568
|
+
},
|
|
569
|
+
get operatorId() {
|
|
570
|
+
return operator.id;
|
|
571
|
+
},
|
|
572
|
+
get scopes() {
|
|
573
|
+
return auth.scopes;
|
|
574
|
+
},
|
|
575
|
+
get verified() {
|
|
576
|
+
return auth.verified;
|
|
577
|
+
},
|
|
578
|
+
get plane() {
|
|
579
|
+
if (operator.id) return "operator" as const;
|
|
580
|
+
if (auth.userId) return "user" as const;
|
|
581
|
+
return undefined;
|
|
582
|
+
},
|
|
583
|
+
} satisfies FxPrincipal);
|
|
499
584
|
const journal = options.journal;
|
|
500
585
|
|
|
501
586
|
async function gated<T>(
|
|
@@ -850,9 +935,12 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
850
935
|
data?: Record<string, unknown>,
|
|
851
936
|
): { message: string; data?: Record<string, unknown> } {
|
|
852
937
|
const vault = options.vaultRuntime;
|
|
853
|
-
|
|
938
|
+
// Redacted<T> never yields the real value, but replace instances with a
|
|
939
|
+
// placeholder so payloads stay plain JSON (and never re-wrap on replay).
|
|
940
|
+
const maskedData = data ? maskRedactedDeep(data) : undefined;
|
|
941
|
+
if (!vault) return { message, data: maskedData };
|
|
854
942
|
const safeMessage = vault.redactString(message);
|
|
855
|
-
const safeData =
|
|
943
|
+
const safeData = maskedData ? (vault.redact(maskedData) as Record<string, unknown>) : undefined;
|
|
856
944
|
return { message: safeMessage, data: safeData };
|
|
857
945
|
}
|
|
858
946
|
|
|
@@ -925,7 +1013,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
925
1013
|
duration: Math.max(0, now() - timestamp),
|
|
926
1014
|
reversibility: reversibilityOf("secret"),
|
|
927
1015
|
});
|
|
928
|
-
return value;
|
|
1016
|
+
return new Redacted(value);
|
|
929
1017
|
},
|
|
930
1018
|
cache,
|
|
931
1019
|
send(template, opts) {
|
|
@@ -1040,6 +1128,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1040
1128
|
},
|
|
1041
1129
|
auth,
|
|
1042
1130
|
operator,
|
|
1131
|
+
principal,
|
|
1043
1132
|
tenant,
|
|
1044
1133
|
fail,
|
|
1045
1134
|
json: {
|
|
@@ -1079,6 +1168,9 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1079
1168
|
retry(fn, opts) {
|
|
1080
1169
|
return fxRetry(fn, opts);
|
|
1081
1170
|
},
|
|
1171
|
+
using(acquire, release, use) {
|
|
1172
|
+
return fxUsing(acquire, release, use);
|
|
1173
|
+
},
|
|
1082
1174
|
};
|
|
1083
1175
|
|
|
1084
1176
|
return { fx, ledger, capability };
|
package/src/kernel/hooks.test.ts
CHANGED
|
@@ -74,7 +74,7 @@ describe("hooks — order and composition", () => {
|
|
|
74
74
|
|
|
75
75
|
on(http.get("/ordered"), f);
|
|
76
76
|
|
|
77
|
-
const app = oke({ name: "hooks-order" })
|
|
77
|
+
const app = oke({ autoBoot: false, name: "hooks-order" })
|
|
78
78
|
.hook("onRequest", () => {
|
|
79
79
|
order.push("app:onRequest");
|
|
80
80
|
})
|
|
@@ -128,7 +128,7 @@ describe("hooks — order and composition", () => {
|
|
|
128
128
|
}),
|
|
129
129
|
);
|
|
130
130
|
|
|
131
|
-
const app = oke({ name: "sc-res" })
|
|
131
|
+
const app = oke({ autoBoot: false, name: "sc-res" })
|
|
132
132
|
.hook("onAuth", () => {
|
|
133
133
|
order.push("onAuth");
|
|
134
134
|
return new Response("denied", { status: 401 });
|
|
@@ -157,7 +157,7 @@ describe("hooks — order and composition", () => {
|
|
|
157
157
|
}),
|
|
158
158
|
);
|
|
159
159
|
|
|
160
|
-
const app = oke({ name: "data-res" }).hook("onResponse", (ctx) => {
|
|
160
|
+
const app = oke({ autoBoot: false, name: "data-res" }).hook("onResponse", (ctx) => {
|
|
161
161
|
seenAtHook = ctx.response;
|
|
162
162
|
if (!ctx.response) return;
|
|
163
163
|
const headers = new Headers(ctx.response.headers);
|
|
@@ -192,7 +192,7 @@ describe("hooks — order and composition", () => {
|
|
|
192
192
|
}),
|
|
193
193
|
);
|
|
194
194
|
|
|
195
|
-
const app = oke({ name: "sc-err" })
|
|
195
|
+
const app = oke({ autoBoot: false, name: "sc-err" })
|
|
196
196
|
.hook("beforeHandle", () => {
|
|
197
197
|
order.push("beforeHandle");
|
|
198
198
|
return fail("Nope", {});
|
package/src/kernel/index.ts
CHANGED
|
@@ -103,6 +103,7 @@ export {
|
|
|
103
103
|
export {
|
|
104
104
|
createFx,
|
|
105
105
|
createFxContext,
|
|
106
|
+
freezePrincipal,
|
|
106
107
|
isJsonResult,
|
|
107
108
|
jsonResultBrand,
|
|
108
109
|
resolveName,
|
|
@@ -118,6 +119,7 @@ export {
|
|
|
118
119
|
type FxJson,
|
|
119
120
|
type FxLog,
|
|
120
121
|
type FxOperator,
|
|
122
|
+
type FxPrincipal,
|
|
121
123
|
type FxRetryOptions,
|
|
122
124
|
type FxSearchOptions,
|
|
123
125
|
type FxSendOptions,
|
|
@@ -137,7 +139,16 @@ export {
|
|
|
137
139
|
withAbortSignal,
|
|
138
140
|
} from "./abort-scope.ts";
|
|
139
141
|
|
|
140
|
-
export {
|
|
142
|
+
export {
|
|
143
|
+
defaultRetryWhen,
|
|
144
|
+
fxAll,
|
|
145
|
+
fxRace,
|
|
146
|
+
fxRetry,
|
|
147
|
+
fxUsing,
|
|
148
|
+
resolveRetryDelayMs,
|
|
149
|
+
} from "./concurrency.ts";
|
|
150
|
+
|
|
151
|
+
export { isRedacted, maskRedactedDeep, REDACTED_PLACEHOLDER, Redacted } from "./redacted.ts";
|
|
141
152
|
|
|
142
153
|
export {
|
|
143
154
|
DryRunWriteIsolationError,
|
|
@@ -235,6 +246,16 @@ export {
|
|
|
235
246
|
type PluginScope,
|
|
236
247
|
} from "./registry.ts";
|
|
237
248
|
|
|
249
|
+
export {
|
|
250
|
+
assertPluginNeeds,
|
|
251
|
+
buildAvailableNeedTokens,
|
|
252
|
+
collectUnmetPluginNeeds,
|
|
253
|
+
PluginNeedsError,
|
|
254
|
+
ELEMENT_NEED_TOKENS,
|
|
255
|
+
type PluginNeedGap,
|
|
256
|
+
type PluginNeedsContext,
|
|
257
|
+
} from "./plugin-needs.ts";
|
|
258
|
+
|
|
238
259
|
export {
|
|
239
260
|
createRouter,
|
|
240
261
|
isUnsupportedByRegExp,
|
|
@@ -79,7 +79,7 @@ describe("pipeline — Unauthorized for anonymous", () => {
|
|
|
79
79
|
const runs = createRunsRuntime({ driver: memoryRunsDriver });
|
|
80
80
|
const app = oke({
|
|
81
81
|
name: "gates-pipeline",
|
|
82
|
-
|
|
82
|
+
gate: { policies: [member] },
|
|
83
83
|
runs,
|
|
84
84
|
env: "test",
|
|
85
85
|
});
|
|
@@ -116,7 +116,7 @@ describe("pipeline — Unauthorized for anonymous", () => {
|
|
|
116
116
|
|
|
117
117
|
const app = oke({
|
|
118
118
|
name: "gates-forbid",
|
|
119
|
-
|
|
119
|
+
gate: { policies: [member, canOrder] },
|
|
120
120
|
env: "test",
|
|
121
121
|
});
|
|
122
122
|
await app.boot({ env: "test", gates: [member, canOrder] });
|
|
@@ -157,7 +157,7 @@ describe("pipeline — evaluated gates on the run", () => {
|
|
|
157
157
|
await runs.open();
|
|
158
158
|
const app = oke({
|
|
159
159
|
name: "gates-eval",
|
|
160
|
-
|
|
160
|
+
gate: { policies: [member, canOrder] },
|
|
161
161
|
runs,
|
|
162
162
|
env: "test",
|
|
163
163
|
});
|
|
@@ -208,9 +208,11 @@ describe("pipeline — Bearer cryptographic verification", () => {
|
|
|
208
208
|
const sessions = createSessionStore();
|
|
209
209
|
const app = oke({
|
|
210
210
|
name: "auth-forge",
|
|
211
|
-
|
|
211
|
+
gate: {
|
|
212
|
+
auth: { secret: "hmac-secret-for-tests", sessions, http: false },
|
|
213
|
+
policies: [member],
|
|
214
|
+
},
|
|
212
215
|
env: "local",
|
|
213
|
-
auth: { secret: "hmac-secret-for-tests", sessions },
|
|
214
216
|
startScheduler: false,
|
|
215
217
|
});
|
|
216
218
|
await app.boot({ env: "local", gates: [member], startScheduler: false });
|
|
@@ -262,9 +264,11 @@ describe("pipeline — Bearer cryptographic verification", () => {
|
|
|
262
264
|
});
|
|
263
265
|
const app = oke({
|
|
264
266
|
name: "auth-expired",
|
|
265
|
-
|
|
267
|
+
gate: {
|
|
268
|
+
auth: { secret, sessions, now: () => nowMs, http: false },
|
|
269
|
+
policies: [member],
|
|
270
|
+
},
|
|
266
271
|
env: "local",
|
|
267
|
-
auth: { secret, sessions, now: () => nowMs },
|
|
268
272
|
elements: { clock: clockRt },
|
|
269
273
|
startScheduler: false,
|
|
270
274
|
});
|
|
@@ -314,7 +318,7 @@ describe("pipeline — Bearer cryptographic verification", () => {
|
|
|
314
318
|
|
|
315
319
|
const app = oke({
|
|
316
320
|
name: "no-inject",
|
|
317
|
-
|
|
321
|
+
gate: { policies: [member] },
|
|
318
322
|
env: "local",
|
|
319
323
|
startScheduler: false,
|
|
320
324
|
});
|
package/src/kernel/pipeline.ts
CHANGED
|
@@ -52,6 +52,13 @@ export interface PipelineDeps {
|
|
|
52
52
|
* @throws On forge / expiry / revoke — pipeline maps to `Unauthorized`
|
|
53
53
|
*/
|
|
54
54
|
readonly verifyBearer?: (token: string) => Promise<ResolvedPrincipal>;
|
|
55
|
+
/**
|
|
56
|
+
* Optional token extraction (e.g. cookie → Bearer when cookies enabled).
|
|
57
|
+
* When omitted, only `Authorization: Bearer` is read.
|
|
58
|
+
*
|
|
59
|
+
* @param request - Incoming HTTP request
|
|
60
|
+
*/
|
|
61
|
+
readonly resolveToken?: (request: Request) => string | undefined;
|
|
55
62
|
/**
|
|
56
63
|
* When true, `ctx.state.principal` / execute `extras.principal` injection
|
|
57
64
|
* is honoured (test harness only). Must be false for production HTTP.
|
|
@@ -158,14 +165,22 @@ export function createElementPipelineHooks(deps: PipelineDeps): {
|
|
|
158
165
|
delete ctx.state.principal;
|
|
159
166
|
}
|
|
160
167
|
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
168
|
+
let token: string | undefined;
|
|
169
|
+
if (deps.resolveToken && ctx.request) {
|
|
170
|
+
token = deps.resolveToken(ctx.request);
|
|
171
|
+
} else {
|
|
172
|
+
const header = ctx.request?.headers.get("authorization");
|
|
173
|
+
if (header?.startsWith("Bearer ")) {
|
|
174
|
+
token = header.slice("Bearer ".length).trim() || undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (token !== undefined) {
|
|
164
179
|
if (!token) {
|
|
165
180
|
return fail("Unauthorized", {});
|
|
166
181
|
}
|
|
167
182
|
if (!deps.verifyBearer) {
|
|
168
|
-
//
|
|
183
|
+
// Token present but no auth binding — refuse rather than trust.
|
|
169
184
|
return fail("Unauthorized", {});
|
|
170
185
|
}
|
|
171
186
|
try {
|
|
@@ -187,6 +202,10 @@ export function createElementPipelineHooks(deps: PipelineDeps): {
|
|
|
187
202
|
|
|
188
203
|
const beforeHandle: HookFn = async (ctx, fxOrErr) => {
|
|
189
204
|
const names = gateNamesOf(ctx.trigger);
|
|
205
|
+
// Empty chain: boot audit should have rejected this when unguardedHttp is
|
|
206
|
+
// "deny". If we still reach here without a gate runtime, skip only for the
|
|
207
|
+
// explicit `autoBoot: false` escape hatch. Public sentinel is a normal
|
|
208
|
+
// named gate in `names`.
|
|
190
209
|
if (names.length === 0 || !deps.gates) return;
|
|
191
210
|
|
|
192
211
|
const policyCtx = policyContextOf(fxOrErr as Fx, ctx);
|
|
@@ -37,7 +37,7 @@ describe("plugin decorate — types and runtime", () => {
|
|
|
37
37
|
}),
|
|
38
38
|
);
|
|
39
39
|
|
|
40
|
-
const app = oke({ name: "decorate" }).plug(audit);
|
|
40
|
+
const app = oke({ autoBoot: false, name: "decorate" }).plug(audit);
|
|
41
41
|
await app.fetch(new Request("http://localhost/x"));
|
|
42
42
|
|
|
43
43
|
expect(seen).toEqual({ enabled: true, retain: "2y" });
|
|
@@ -51,7 +51,7 @@ describe("plugin decorate — types and runtime", () => {
|
|
|
51
51
|
currency: "SAR" as const,
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
const app = oke({ name: "types" }).plug(withAudit).plug(withBilling);
|
|
54
|
+
const app = oke({ autoBoot: false, name: "types" }).plug(withAudit).plug(withBilling);
|
|
55
55
|
|
|
56
56
|
type AppDeco = typeof app extends OkeApp<infer D> ? D : never;
|
|
57
57
|
type HasBoth = ExpectExtends<
|
|
@@ -94,7 +94,7 @@ describe("plugin decorate — types and runtime", () => {
|
|
|
94
94
|
}),
|
|
95
95
|
);
|
|
96
96
|
|
|
97
|
-
const app = oke({ name: "deco-scope" });
|
|
97
|
+
const app = oke({ autoBoot: false, name: "deco-scope" });
|
|
98
98
|
app.unit("orders").plug(plugin("flag", { version: "1.0.0" }).decorate("flag", "orders-only"));
|
|
99
99
|
|
|
100
100
|
await app.fetch(new Request("http://localhost/orders"));
|
|
@@ -48,7 +48,7 @@ describe("plugin scoping — attachment point is the scope", () => {
|
|
|
48
48
|
fired.push("rate-limit");
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
const app = oke({ name: "scoping" });
|
|
51
|
+
const app = oke({ autoBoot: false, name: "scoping" });
|
|
52
52
|
app.unit("orders").plug(rateLimit);
|
|
53
53
|
|
|
54
54
|
await app.fetch(new Request("http://localhost/orders"));
|
|
@@ -87,7 +87,7 @@ describe("plugin scoping — attachment point is the scope", () => {
|
|
|
87
87
|
fired.push("audit");
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
const app = oke({ name: "app-scope" }).plug(audit);
|
|
90
|
+
const app = oke({ autoBoot: false, name: "app-scope" }).plug(audit);
|
|
91
91
|
|
|
92
92
|
await app.fetch(new Request("http://localhost/a"));
|
|
93
93
|
await app.fetch(new Request("http://localhost/b"));
|
|
@@ -121,7 +121,7 @@ describe("plugin scoping — attachment point is the scope", () => {
|
|
|
121
121
|
on(http.get("/marked"), marked);
|
|
122
122
|
on(http.get("/plain"), plain);
|
|
123
123
|
|
|
124
|
-
const app = oke({ name: "flow-scope" });
|
|
124
|
+
const app = oke({ autoBoot: false, name: "flow-scope" });
|
|
125
125
|
|
|
126
126
|
await app.fetch(new Request("http://localhost/marked"));
|
|
127
127
|
await app.fetch(new Request("http://localhost/plain"));
|