okengine 0.21.0 → 0.22.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 +4 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +10 -0
- package/package.json +22 -25
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.mdx +1 -1
- package/site/content/docs/elements/channel/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- package/site/content/docs/reference/configuration.mdx +2 -2
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +28 -15
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/dev.test.ts +105 -2
- package/src/cli/dev.ts +37 -2
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/index.ts +9 -1
- package/src/cli/load-config.images.test.ts +10 -10
- package/src/cli/load-config.ts +1 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/create.ts +31 -27
- package/src/client/live.ts +44 -101
- package/src/client/sse.ts +20 -66
- package/src/client/stream.ts +25 -67
- package/src/client/transport.test.ts +225 -0
- package/src/client/transport.ts +158 -90
- package/src/client/types.ts +29 -3
- package/src/client/wire.ts +119 -0
- package/src/compiler/aot.ts +3 -32
- package/src/compiler/dynamic.ts +13 -11
- package/src/compiler/effects-infer.ts +726 -16
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +113 -12
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- package/src/compiler/interpret.ts +45 -0
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/ui-next/dist/assets/{access-page-tIbsiphz.js → access-page-Ze6ckjdd.js} +2 -2
- package/src/console/ui-next/dist/assets/agent-disclosure-Bkohc_8X.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-BCC-DxKT.js → cache-glyph-rL0lHxat.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-jOmYISdc.js → call-pii-button-Deo4PP18.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DC2xNaAb.js → collapsible-CoJ6amHf.js} +1 -1
- package/src/console/ui-next/dist/assets/{copy-inline-button-CAYD18cr.js → copy-inline-button-CWG5_ZOh.js} +1 -1
- package/src/console/ui-next/dist/assets/{detail-header-DVWNjWjg.js → detail-header-b_ZzIsmU.js} +1 -1
- package/src/console/ui-next/dist/assets/{dropdown-menu-4h2LOVXM.js → dropdown-menu-nwXEO1Ac.js} +1 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +1 -0
- package/src/console/ui-next/dist/assets/{explorer-empty-CJs5A-wm.js → explorer-empty-C8sSCqYT.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-gT1lsWQK.js → flows-page-D2E7BtKK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-C2GZEJNI.js → highlighted-json-CPYBzDh0.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BdYjcIrD.js → http-method-DTPevKmM.js} +1 -1
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +4 -0
- package/src/console/ui-next/dist/assets/{replica-lag-C4QdAF7J.js → replica-lag-BM27lXld.js} +3 -3
- package/src/console/ui-next/dist/assets/{request-meta-C43DHyld.js → request-meta-BjFT7DNl.js} +1 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +41 -0
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-Dcpo4Us_.js → trace-detail-sheet-Cmitq3da.js} +2 -2
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CKJTuv43.js → tree-expand-toggle-CbDIB-7x.js} +2 -2
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-CT4-gajj.js → use-vault-list-Cs45Czkt.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-BwZ9YjTW.js → vault-page-BOMr0go9.js} +2 -2
- package/src/console/ui-next/dist/index.html +2 -2
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +9 -0
- package/src/docker/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/stack-id.test.ts +1 -1
- package/src/drivers/journal-postgres.ts +168 -3
- package/src/drivers/meilisearch.ts +2 -0
- package/src/drivers/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- package/src/elements/store/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/sql-session.ts +20 -4
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +4 -0
- package/src/i18n/catalogs/en.ts +4 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/app.ts +200 -20
- package/src/kernel/auto-cache.test.ts +6 -6
- package/src/kernel/boot-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +19 -1
- package/src/kernel/builtin-errors.ts +8 -0
- package/src/kernel/client-descriptor.test.ts +78 -0
- package/src/kernel/client-descriptor.ts +24 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- package/src/kernel/errors-compiler.ts +20 -0
- package/src/kernel/errors-text.ts +99 -0
- package/src/kernel/errors.ts +90 -138
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx-sql-handle.ts +305 -0
- package/src/kernel/fx.ts +42 -330
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +56 -0
- package/src/kernel/json-result.ts +59 -0
- package/src/kernel/project-out.ts +6 -1
- package/src/manifest/types.ts +8 -0
- package/src/okid-extended.ts +175 -0
- package/src/okid-shared.ts +103 -0
- package/src/okid.ts +30 -213
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/build-lib.ts +9 -0
- package/src/release/http-graph.test.ts +25 -0
- package/src/release/http-graph.ts +90 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +105 -14
- package/src/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-CSKumwS2.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-CwoV56jn.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/index-Cul17AcV.js +0 -63
- package/src/console/ui-next/dist/assets/observability-page-oY9vdYBk.js +0 -4
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-CL0D9dOq.js +0 -41
- package/src/console/ui-next/dist/assets/units-page-1PlT19ft.js +0 -1
|
@@ -405,7 +405,7 @@ test("creates a user", async () => {
|
|
|
405
405
|
bun test # [!code focus]
|
|
406
406
|
```
|
|
407
407
|
|
|
408
|
-
`createTestApp` swaps every driver behind `fx` for a deterministic one.
|
|
408
|
+
`createTestApp` swaps every driver behind `fx` for a deterministic one. It stamps Manifest tokens. Inline flows that are not an app pass `capability: "open"`.
|
|
409
409
|
|
|
410
410
|
</Step>
|
|
411
411
|
|
|
@@ -762,7 +762,7 @@ return await fx.ask(triage, { message }); // [!code focus]
|
|
|
762
762
|
|
|
763
763
|
## Learn more
|
|
764
764
|
|
|
765
|
-
- [Routing](/docs/
|
|
765
|
+
- [Routing](/docs/understand/routing) — when to omit the path, `list` / `create` / `get` filenames
|
|
766
766
|
- [Flow](/docs/elements/flow) — one species; `fx` is the door
|
|
767
767
|
- [Signal](/docs/elements/signal) — `once` / `broadcast` / `live`
|
|
768
768
|
- [Store](/docs/elements/store) — SQL, KV, files, search
|
|
@@ -775,7 +775,7 @@ return await fx.ask(triage, { message }); // [!code focus]
|
|
|
775
775
|
<Card
|
|
776
776
|
title="Routing"
|
|
777
777
|
description="File-tree stamps, many-routes files, and reserved leaves."
|
|
778
|
-
href="/docs/
|
|
778
|
+
href="/docs/understand/routing"
|
|
779
779
|
/>
|
|
780
780
|
<Card title="Flow" description="on(trigger) → do through fx." href="/docs/elements/flow" />
|
|
781
781
|
<Card title="Signal" description="Move work off the request." href="/docs/elements/signal" />
|
|
@@ -11,8 +11,8 @@ import { join } from "node:path";
|
|
|
11
11
|
|
|
12
12
|
const ROOT = join(import.meta.dir, "../..");
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
const FORBIDDEN_HONO = ["Ho", "no"].join("");
|
|
14
|
+
/** Word-boundary router peer — `-F` false-positives on `Honored` / `canHonor`. */
|
|
15
|
+
const FORBIDDEN_HONO = ["\\b", "Ho", "no", "\\b"].join("");
|
|
16
16
|
/** Forbidden peer name. */
|
|
17
17
|
const FORBIDDEN_ELYSIA = ["Ely", "sia"].join("");
|
|
18
18
|
/** Forbidden peer name (covers Enc + ore.ts). */
|
|
@@ -104,7 +104,7 @@ function assertZeroGitGrepRegex(pattern: string, ignorePrefixes: readonly string
|
|
|
104
104
|
|
|
105
105
|
describe("named competitor mention removal gate", () => {
|
|
106
106
|
test("tracked tree has zero mentions of the thin multi-runtime router peer", () => {
|
|
107
|
-
|
|
107
|
+
assertZeroGitGrepRegex(FORBIDDEN_HONO);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
test("tracked tree has zero mentions of the Bun-first peer framework", () => {
|
package/src/cli/dev.test.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* context and exercises `oke.manifest.get` over HTTP on :6535.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
10
10
|
import { mkdir, mkdtemp, readdir } from "node:fs/promises";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { join, resolve } from "node:path";
|
|
@@ -17,9 +17,19 @@ import {
|
|
|
17
17
|
} from "../compiler/generate-adopt.ts";
|
|
18
18
|
import type { Manifest } from "../manifest/types.ts";
|
|
19
19
|
import type { WideEvent } from "../runs/types.ts";
|
|
20
|
+
import type { ConsoleAppHandle } from "../console/server/app.ts";
|
|
21
|
+
import { createConsoleState } from "../console/server/state.ts";
|
|
20
22
|
import { isDataEnvelope, MCP_DATA_KIND } from "../mcp/data.ts";
|
|
21
23
|
import { mintMcpSession } from "../mcp/session.ts";
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
CLIENT_REGEN_DEBOUNCE_MS,
|
|
26
|
+
isClientRegenWatchPath,
|
|
27
|
+
isFlowsTreeWatchPath,
|
|
28
|
+
runDev,
|
|
29
|
+
type DevOptions,
|
|
30
|
+
type DevSession,
|
|
31
|
+
type DevWatchFn,
|
|
32
|
+
} from "./dev.ts";
|
|
23
33
|
import { mcpContextFromConsole } from "./mcp-from-console.ts";
|
|
24
34
|
|
|
25
35
|
/** Repo public entry — absolute import so temp apps need no install. */
|
|
@@ -803,6 +813,99 @@ describe("isFlowsTreeWatchPath", () => {
|
|
|
803
813
|
});
|
|
804
814
|
});
|
|
805
815
|
|
|
816
|
+
describe("isClientRegenWatchPath", () => {
|
|
817
|
+
test("flow files and schema declares regenerate the client; unrelated files do not", () => {
|
|
818
|
+
expect(isClientRegenWatchPath("flows/notes/get.ts")).toBe(true);
|
|
819
|
+
expect(isClientRegenWatchPath("schema.ts")).toBe(true);
|
|
820
|
+
expect(isClientRegenWatchPath("db/schema/links.ts")).toBe(true);
|
|
821
|
+
expect(isClientRegenWatchPath("lib/util.ts")).toBe(false);
|
|
822
|
+
expect(isClientRegenWatchPath("flows/index.ts")).toBe(false);
|
|
823
|
+
expect(isClientRegenWatchPath("flows/notes/get.ts.tmp")).toBe(false);
|
|
824
|
+
expect(isClientRegenWatchPath("db/drizzle/schema.ts")).toBe(false);
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
describe("oke dev client regen watch", () => {
|
|
829
|
+
let session: DevSession | undefined;
|
|
830
|
+
|
|
831
|
+
afterEach(() => {
|
|
832
|
+
session?.stop();
|
|
833
|
+
session = undefined;
|
|
834
|
+
mock.restore();
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
test("three flow saves within 50ms regenerate the client once", async () => {
|
|
838
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-dev-regen-"));
|
|
839
|
+
await mkdir(join(dir, "src"), { recursive: true });
|
|
840
|
+
await Bun.write(join(dir, "src/app.ts"), "export {};\n");
|
|
841
|
+
|
|
842
|
+
const extractMod = await import("../compiler/extract.ts");
|
|
843
|
+
const originalExtract = extractMod.extractManifest;
|
|
844
|
+
let extracts = 0;
|
|
845
|
+
mock.module(resolve(import.meta.dir, "../compiler/extract.ts"), () => ({
|
|
846
|
+
...extractMod,
|
|
847
|
+
extractManifest: (opts: Parameters<typeof originalExtract>[0]) => {
|
|
848
|
+
extracts++;
|
|
849
|
+
return originalExtract(opts);
|
|
850
|
+
},
|
|
851
|
+
}));
|
|
852
|
+
|
|
853
|
+
let regens = 0;
|
|
854
|
+
let listener: Parameters<DevWatchFn>[2] | undefined;
|
|
855
|
+
const result = await runDev({
|
|
856
|
+
cwd: dir,
|
|
857
|
+
stdinIsTTY: false,
|
|
858
|
+
appPort: 0,
|
|
859
|
+
consolePort: 0,
|
|
860
|
+
mcpPort: 0,
|
|
861
|
+
...stubSurfaces(),
|
|
862
|
+
serveConsole: async () => ({
|
|
863
|
+
port: 0,
|
|
864
|
+
stop() {},
|
|
865
|
+
console: {
|
|
866
|
+
state: createConsoleState({ cwd: dir, secret: SECRET }),
|
|
867
|
+
} as ConsoleAppHandle,
|
|
868
|
+
}),
|
|
869
|
+
startApp: async () => ({
|
|
870
|
+
port: 0,
|
|
871
|
+
url: new URL("http://127.0.0.1:9"),
|
|
872
|
+
stop() {},
|
|
873
|
+
}),
|
|
874
|
+
syncAdoptBarrel: async () => [],
|
|
875
|
+
regenClient: async () => {
|
|
876
|
+
regens++;
|
|
877
|
+
},
|
|
878
|
+
watchFs: (_path, _options, next) => {
|
|
879
|
+
listener = next;
|
|
880
|
+
return { close() {} };
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
expect(result.code).toBe(0);
|
|
885
|
+
session = result.session;
|
|
886
|
+
const boot = regens;
|
|
887
|
+
expect(boot).toBe(1);
|
|
888
|
+
const extractsAfterBoot = extracts;
|
|
889
|
+
expect(extractsAfterBoot).toBeGreaterThan(0);
|
|
890
|
+
if (!listener) throw new Error("watcher not started");
|
|
891
|
+
|
|
892
|
+
listener("change", "flows/notes/get.ts");
|
|
893
|
+
listener("change", "flows/notes/list.ts");
|
|
894
|
+
listener("change", "flows/notes/[id]/delete.ts");
|
|
895
|
+
await Bun.sleep(50);
|
|
896
|
+
expect(regens).toBe(boot);
|
|
897
|
+
expect(extracts).toBe(extractsAfterBoot);
|
|
898
|
+
|
|
899
|
+
await Bun.sleep(CLIENT_REGEN_DEBOUNCE_MS + 80);
|
|
900
|
+
expect(regens).toBe(boot + 1);
|
|
901
|
+
expect(extracts).toBe(extractsAfterBoot + 1);
|
|
902
|
+
|
|
903
|
+
listener("change", "lib/util.ts");
|
|
904
|
+
await Bun.sleep(CLIENT_REGEN_DEBOUNCE_MS + 80);
|
|
905
|
+
expect(regens).toBe(boot + 1);
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
|
|
806
909
|
describe("oke dev Docker-first", () => {
|
|
807
910
|
test("dryRun always plans compose (no mode ask)", async () => {
|
|
808
911
|
const dir = await mkdtemp(join(tmpdir(), "oke-dev-always-compose-"));
|
package/src/cli/dev.ts
CHANGED
|
@@ -1417,15 +1417,26 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
1417
1417
|
|
|
1418
1418
|
const watchFs: DevWatchFn =
|
|
1419
1419
|
options.watchFs ?? ((path, watchOptions, listener) => watch(path, watchOptions, listener));
|
|
1420
|
+
// Trailing timer, always re-armed. A burst resets the window and still
|
|
1421
|
+
// fires once after it goes quiet — a timestamp check that only `return`s
|
|
1422
|
+
// drops the save and never regenerates.
|
|
1423
|
+
const clientRegen = createDebouncedRunner(() => {
|
|
1424
|
+
void regen(appUrl);
|
|
1425
|
+
}, CLIENT_REGEN_DEBOUNCE_MS);
|
|
1426
|
+
const manifestRefresh = createDebouncedRunner(() => {
|
|
1427
|
+
void refreshManifestInto(consoleState);
|
|
1428
|
+
}, CLIENT_REGEN_DEBOUNCE_MS);
|
|
1420
1429
|
const watcher = watchFs(resolve(cwd, "src"), { recursive: true }, (_event, filename) => {
|
|
1421
1430
|
const rel = (filename?.toString() ?? "").replace(/\\/g, "/");
|
|
1422
1431
|
if (isFlowsTreeWatchPath(rel)) {
|
|
1423
1432
|
void syncAdoptBarrel(cwd);
|
|
1424
1433
|
}
|
|
1425
|
-
|
|
1434
|
+
if (isClientRegenWatchPath(rel)) {
|
|
1435
|
+
clientRegen.trigger();
|
|
1436
|
+
}
|
|
1426
1437
|
// Only live-extract when the host did not pin a Manifest (tests).
|
|
1427
1438
|
if (options.manifest === undefined) {
|
|
1428
|
-
|
|
1439
|
+
manifestRefresh.trigger();
|
|
1429
1440
|
}
|
|
1430
1441
|
if (autoPushEnabled && isDomainSchemaWatchPath(filename?.toString())) {
|
|
1431
1442
|
lastSchemaFilename = filename?.toString() ?? "schema.ts";
|
|
@@ -1444,6 +1455,8 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
1444
1455
|
stopComposeHealthWatch = null;
|
|
1445
1456
|
bootBoard.stop();
|
|
1446
1457
|
autoPushRunner.cancel();
|
|
1458
|
+
clientRegen.cancel();
|
|
1459
|
+
manifestRefresh.cancel();
|
|
1447
1460
|
watcher.close();
|
|
1448
1461
|
const host = attachedHost;
|
|
1449
1462
|
attachedHost = null;
|
|
@@ -1613,6 +1626,28 @@ export function isFlowsTreeWatchPath(rel: string): boolean {
|
|
|
1613
1626
|
return true;
|
|
1614
1627
|
}
|
|
1615
1628
|
|
|
1629
|
+
/** Quiet window before one `oke-client.d.ts` regen after a burst of saves. */
|
|
1630
|
+
export const CLIENT_REGEN_DEBOUNCE_MS = 200;
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* True when a `src/` watcher event can change the typed client
|
|
1634
|
+
* (flow files, or a schema declare module those flows import).
|
|
1635
|
+
*
|
|
1636
|
+
* Editor temp files and the adopt barrel are excluded so a regen cannot
|
|
1637
|
+
* retrigger itself.
|
|
1638
|
+
*
|
|
1639
|
+
* @param rel - Watcher filename (POSIX or Windows)
|
|
1640
|
+
*/
|
|
1641
|
+
export function isClientRegenWatchPath(rel: string): boolean {
|
|
1642
|
+
const posix = rel.replace(/\\/g, "/");
|
|
1643
|
+
if (posix.endsWith(".tmp")) return false;
|
|
1644
|
+
if (isFlowsTreeWatchPath(posix)) return true;
|
|
1645
|
+
if (/(^|\/)drizzle\//.test(posix)) return false;
|
|
1646
|
+
if (/(^|\/)schema(\.decl)?\.tsx?$/.test(posix)) return true;
|
|
1647
|
+
if (/(^|\/)schema\/.+\.tsx?$/.test(posix)) return true;
|
|
1648
|
+
return false;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1616
1651
|
/**
|
|
1617
1652
|
* Frame a schema-sync failure for the `oke dev` loop: warn loud for developer
|
|
1618
1653
|
* bugs, soft-skip only known environmental gaps. Never crashes the session.
|
package/src/cli/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Bare `oke` (TTY) → interactive Ink TUI. `oke <command>` runs directly.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { VaultAuditConfig } from "../elements/vault/types.ts";
|
|
8
9
|
import { aiCli } from "./ai.ts";
|
|
9
10
|
import { branchCli } from "./branch.ts";
|
|
10
11
|
import { buildCli } from "./build.ts";
|
|
@@ -28,6 +29,7 @@ import { stackCli } from "./stack.ts";
|
|
|
28
29
|
import { startCli } from "./start.ts";
|
|
29
30
|
import { testCli } from "./test.ts";
|
|
30
31
|
import { upgradeCli } from "./upgrade.ts";
|
|
32
|
+
import { loadOkeConfig } from "./load-config.ts";
|
|
31
33
|
import { vaultCli } from "./vault-cmd.ts";
|
|
32
34
|
|
|
33
35
|
const rawArgv = process.argv.slice(2);
|
|
@@ -90,7 +92,13 @@ if (cmd === "client" && sub === "add") {
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
if (cmd === "vault") {
|
|
93
|
-
|
|
95
|
+
let audit: VaultAuditConfig | undefined;
|
|
96
|
+
try {
|
|
97
|
+
audit = (await loadOkeConfig()).config.vault?.audit;
|
|
98
|
+
} catch {
|
|
99
|
+
audit = undefined;
|
|
100
|
+
}
|
|
101
|
+
process.exit(await vaultCli(sub ? [sub, ...rest] : rest, audit === undefined ? {} : { audit }));
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
if (cmd === "docker") {
|
|
@@ -18,19 +18,19 @@ describe("flattenImagesConfig", () => {
|
|
|
18
18
|
sql: "postgres:18-alpine",
|
|
19
19
|
kv: "redis:8-alpine",
|
|
20
20
|
files: "rustfs/rustfs:1.0.0",
|
|
21
|
-
index: "getmeili/meilisearch:v1.
|
|
21
|
+
index: "getmeili/meilisearch:v1.54",
|
|
22
22
|
},
|
|
23
|
-
channel: { email: "axllent/mailpit:v1.31.
|
|
24
|
-
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.
|
|
23
|
+
channel: { email: "axllent/mailpit:v1.31.2" },
|
|
24
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59",
|
|
25
25
|
proxy: "caddy:2-alpine",
|
|
26
26
|
}),
|
|
27
27
|
).toEqual({
|
|
28
28
|
"store.sql": "postgres:18-alpine",
|
|
29
29
|
"store.kv": "redis:8-alpine",
|
|
30
30
|
"store.files": "rustfs/rustfs:1.0.0",
|
|
31
|
-
"store.index": "getmeili/meilisearch:v1.
|
|
32
|
-
"channel.email": "axllent/mailpit:v1.31.
|
|
33
|
-
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.
|
|
31
|
+
"store.index": "getmeili/meilisearch:v1.54",
|
|
32
|
+
"channel.email": "axllent/mailpit:v1.31.2",
|
|
33
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59",
|
|
34
34
|
proxy: "caddy:2-alpine",
|
|
35
35
|
});
|
|
36
36
|
});
|
|
@@ -53,7 +53,7 @@ describe("defaultImagesFromConfig", () => {
|
|
|
53
53
|
});
|
|
54
54
|
expect(images).toEqual({
|
|
55
55
|
"store.sql": "postgres:18-alpine",
|
|
56
|
-
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.
|
|
56
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59",
|
|
57
57
|
"store.kv": "redis:8-alpine",
|
|
58
58
|
});
|
|
59
59
|
});
|
|
@@ -68,7 +68,7 @@ describe("defaultImagesFromConfig", () => {
|
|
|
68
68
|
},
|
|
69
69
|
});
|
|
70
70
|
expect(images["store.sql"]).toBe("pgvector/pgvector:pg17");
|
|
71
|
-
expect(images.pgdog).toBe("ghcr.io/pgdogdev/pgdog:v0.1.
|
|
71
|
+
expect(images.pgdog).toBe("ghcr.io/pgdogdev/pgdog:v0.1.59");
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
test("clock postgres alone pulls store.sql image", () => {
|
|
@@ -79,7 +79,7 @@ describe("defaultImagesFromConfig", () => {
|
|
|
79
79
|
});
|
|
80
80
|
expect(images).toEqual({
|
|
81
81
|
"store.sql": "postgres:18-alpine",
|
|
82
|
-
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.
|
|
82
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59",
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
85
|
|
|
@@ -91,7 +91,7 @@ describe("defaultImagesFromConfig", () => {
|
|
|
91
91
|
});
|
|
92
92
|
expect(images).toEqual({
|
|
93
93
|
"store.sql": "postgres:18-alpine",
|
|
94
|
-
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.
|
|
94
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59",
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
|
package/src/cli/load-config.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { Manifest } from "../manifest/types.ts";
|
|
|
15
15
|
/** Default image pins when `images` is omitted but prod drivers need containers. */
|
|
16
16
|
const DEFAULT_SQL_IMAGE = "postgres:18-alpine";
|
|
17
17
|
const DEFAULT_PGVECTOR_IMAGE = "pgvector/pgvector:pg17";
|
|
18
|
-
const DEFAULT_PGDOG_IMAGE = "ghcr.io/pgdogdev/pgdog:v0.1.
|
|
18
|
+
const DEFAULT_PGDOG_IMAGE = "ghcr.io/pgdogdev/pgdog:v0.1.59";
|
|
19
19
|
const DEFAULT_KV_IMAGE = "redis:8-alpine";
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -282,6 +282,29 @@ describe("oke vault audit", () => {
|
|
|
282
282
|
await h.close();
|
|
283
283
|
}
|
|
284
284
|
});
|
|
285
|
+
|
|
286
|
+
test("audit verify refuses a webhook sink", async () => {
|
|
287
|
+
const sql = createMemoryVaultSql();
|
|
288
|
+
const errors: string[] = [];
|
|
289
|
+
const original = console.error;
|
|
290
|
+
console.error = (...args: unknown[]) => {
|
|
291
|
+
errors.push(args.map((part) => String(part)).join(" "));
|
|
292
|
+
};
|
|
293
|
+
try {
|
|
294
|
+
const code = await vaultCli(["audit", "verify"], {
|
|
295
|
+
sql,
|
|
296
|
+
env: {},
|
|
297
|
+
stdinIsTTY: false,
|
|
298
|
+
audit: { sink: "webhook", webhookUrl: "https://audit.example/hook" },
|
|
299
|
+
write: () => undefined,
|
|
300
|
+
});
|
|
301
|
+
expect(code).toBe(1);
|
|
302
|
+
expect(errors.join("\n")).toContain('audit.sink "db"');
|
|
303
|
+
} finally {
|
|
304
|
+
console.error = original;
|
|
305
|
+
await sql.close();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
285
308
|
});
|
|
286
309
|
|
|
287
310
|
describe("oke vault backup / restore", () => {
|
package/src/cli/vault-cmd.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { SqlConnection } from "../drivers/types.ts";
|
|
|
24
24
|
import { openBuiltinVaultAdapter, VAULT_MASTER_KEY_ENV } from "../drivers/vault-builtin.ts";
|
|
25
25
|
import { parseDotenv, formatDotenv } from "../drivers/vault-dotenv-parse.ts";
|
|
26
26
|
import type { BuiltinVaultAdapter } from "../elements/vault/builtin-adapter.ts";
|
|
27
|
+
import type { VaultAuditConfig } from "../elements/vault/types.ts";
|
|
27
28
|
import { promptHidden, readStdinSecure } from "./vault-secure-input.ts";
|
|
28
29
|
|
|
29
30
|
/** Env var for the *new* master key when resuming an interrupted `rotate-master`. */
|
|
@@ -70,6 +71,11 @@ export interface VaultCmdOptions {
|
|
|
70
71
|
* closes a connection it did not open.
|
|
71
72
|
*/
|
|
72
73
|
readonly sql?: SqlConnection;
|
|
74
|
+
/**
|
|
75
|
+
* `vault.audit` from `oke.config.ts`. The `oke` binary loads this; tests
|
|
76
|
+
* pass it directly. Omitted means the SQL hash chain.
|
|
77
|
+
*/
|
|
78
|
+
readonly audit?: VaultAuditConfig;
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
/**
|
|
@@ -332,6 +338,7 @@ async function withVault<T>(
|
|
|
332
338
|
masterKey,
|
|
333
339
|
...(flags.url === undefined ? {} : { url: flags.url }),
|
|
334
340
|
...(options.sql === undefined ? {} : { connection: options.sql }),
|
|
341
|
+
...(options.audit === undefined ? {} : { audit: options.audit }),
|
|
335
342
|
});
|
|
336
343
|
try {
|
|
337
344
|
return await run(opened.adapter);
|
package/src/client/create.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Untriggered flows fall back to `POST /_oke/{unit}/{flow}` RPC.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { createTransport, type Transport } from "./transport.ts";
|
|
15
|
+
import { createTransport, transportEnvelope, type Transport } from "./transport.ts";
|
|
16
16
|
import { asThenableIterable, attachPager } from "./pager.ts";
|
|
17
17
|
import {
|
|
18
18
|
flattenLiveRoutes,
|
|
@@ -33,6 +33,7 @@ import type {
|
|
|
33
33
|
LiveHandlers,
|
|
34
34
|
ResolveApp,
|
|
35
35
|
} from "./types.ts";
|
|
36
|
+
import { methodAndPath, walkContracts } from "./wire.ts";
|
|
36
37
|
|
|
37
38
|
/** App-shaped value that carries a runtime `$routes` table from typed adopt. */
|
|
38
39
|
export interface AppWithRoutes {
|
|
@@ -169,19 +170,11 @@ function mergeHeaders(base: ClientHeaders | undefined, over: ClientHeaders): Cli
|
|
|
169
170
|
export function flattenRoutes(
|
|
170
171
|
$routes: ClientRouteMap | undefined,
|
|
171
172
|
): ClientOptions["routes"] | undefined {
|
|
172
|
-
if (!$routes) return undefined;
|
|
173
173
|
const out: Record<string, { readonly method: string; readonly path: string }> = {};
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const method = "method" in contract ? contract.method : undefined;
|
|
179
|
-
const path = "path" in contract ? contract.path : undefined;
|
|
180
|
-
if (typeof method === "string" && typeof path === "string") {
|
|
181
|
-
out[`${unit}.${flow}`] = { method, path };
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
174
|
+
walkContracts($routes, (unit, flow, contract) => {
|
|
175
|
+
const route = methodAndPath(contract);
|
|
176
|
+
if (route) out[`${unit}.${flow}`] = route;
|
|
177
|
+
});
|
|
185
178
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
186
179
|
}
|
|
187
180
|
|
|
@@ -196,25 +189,19 @@ function proxy(transport: Transport, path: readonly string[], ctx: ProxyCtx): un
|
|
|
196
189
|
if (path.length < 2) {
|
|
197
190
|
const message = `Incomplete path: api.${path.join(".") || "?"}(…)`;
|
|
198
191
|
return attachPager(
|
|
199
|
-
|
|
200
|
-
data: null,
|
|
201
|
-
error: {
|
|
202
|
-
code: "TransportError" as const,
|
|
203
|
-
message,
|
|
204
|
-
data: { message },
|
|
205
|
-
},
|
|
206
|
-
},
|
|
192
|
+
transportEnvelope(message),
|
|
207
193
|
(nextInput) => invoke(nextInput, callOpts),
|
|
208
194
|
input,
|
|
209
195
|
);
|
|
210
196
|
}
|
|
211
197
|
const unit = path[0]!;
|
|
212
198
|
const flow = path.slice(1).join(".");
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
199
|
+
const headers = ctx.perCallHeaders.read();
|
|
200
|
+
const result = await transport.call(
|
|
201
|
+
`${unit}/${flow}`,
|
|
202
|
+
input,
|
|
203
|
+
callOpts ? { headers, ...callOpts } : headers,
|
|
204
|
+
);
|
|
218
205
|
return attachPager(result, (nextInput) => invoke(nextInput, callOpts), input);
|
|
219
206
|
};
|
|
220
207
|
const call = (a?: unknown, b?: unknown): unknown => {
|
|
@@ -351,11 +338,28 @@ function isCallOpts(value: unknown): value is CallOpts {
|
|
|
351
338
|
if ("signal" in v) {
|
|
352
339
|
return v.signal === undefined || v.signal instanceof AbortSignal;
|
|
353
340
|
}
|
|
341
|
+
const keyOk =
|
|
342
|
+
"idempotencyKey" in v && (typeof v.idempotencyKey === "string" || v.idempotencyKey === false);
|
|
343
|
+
if (("retry" in v && typeof v.retry === "boolean") || keyOk) {
|
|
344
|
+
return Object.keys(v).every(
|
|
345
|
+
(k) => k === "response" || k === "signal" || k === "retry" || k === "idempotencyKey",
|
|
346
|
+
);
|
|
347
|
+
}
|
|
354
348
|
return false;
|
|
355
349
|
}
|
|
356
350
|
|
|
357
|
-
/** Per-call options on a Flow invoke (binary decode
|
|
351
|
+
/** Per-call options on a Flow invoke (binary decode, abort, retry opt-in). */
|
|
358
352
|
export interface CallOpts {
|
|
359
353
|
readonly response?: "json" | "blob" | "arrayBuffer";
|
|
360
354
|
readonly signal?: AbortSignal;
|
|
355
|
+
/**
|
|
356
|
+
* Repeat this call under the client `retry` policy even when it sends
|
|
357
|
+
* no idempotency key.
|
|
358
|
+
*/
|
|
359
|
+
readonly retry?: boolean;
|
|
360
|
+
/**
|
|
361
|
+
* Stable `Idempotency-Key`, or `false` to send none.
|
|
362
|
+
* See {@link ClientCallOpts.idempotencyKey}.
|
|
363
|
+
*/
|
|
364
|
+
readonly idempotencyKey?: string | false;
|
|
361
365
|
}
|