okengine 0.3.5 → 0.4.3
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 +6 -0
- package/manifest.v1.schema.json +43 -1
- package/package.json +27 -12
- package/site/content/docs/ai/skills.mdx +11 -7
- package/site/content/docs/console/vault.mdx +4 -0
- package/site/content/docs/elements/ai.mdx +2 -0
- package/site/content/docs/elements/channel.mdx +7 -4
- package/site/content/docs/elements/clock.mdx +2 -4
- package/site/content/docs/elements/flow.mdx +19 -10
- package/site/content/docs/elements/gate.mdx +18 -11
- package/site/content/docs/elements/signal.mdx +9 -12
- package/site/content/docs/elements/store.mdx +69 -8
- package/site/content/docs/elements/vault.mdx +11 -12
- package/site/content/docs/get-started/basic-usage.mdx +76 -41
- 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/plugins/ip-allowlist.mdx +1 -2
- package/site/content/docs/plugins/security-headers.mdx +1 -1
- package/site/content/docs/reference/configuration.mdx +2 -2
- package/site/content/docs/reference/environment-variables.mdx +10 -0
- package/site/content/docs/reference/fx.mdx +56 -3
- package/site/content/docs/reference/plugins.mdx +18 -18
- package/src/cli/competitor-mention-removal.test.ts +117 -0
- 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/compiler/extract.test.ts +63 -0
- package/src/compiler/extract.ts +36 -15
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +5 -1
- package/src/console/server/channels.ts +2 -0
- package/src/console/server/clock.ts +3 -0
- package/src/console/server/flows.ts +13 -0
- package/src/console/server/gates.ts +2 -0
- package/src/console/server/plugins.ts +20 -1
- package/src/console/server/signals.ts +5 -0
- package/src/console/server/store.test.ts +17 -0
- package/src/console/server/store.ts +35 -1
- package/src/console/ui/channels/types.ts +1 -0
- package/src/console/ui/clock/types.ts +1 -0
- package/src/console/ui/display.test.ts +14 -0
- package/src/console/ui/display.ts +9 -0
- 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-9T3ybqRu.js +1 -0
- 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-BQGYXvjT.js +1 -0
- package/src/console/ui/dist/assets/panel-overview-BBnRO18l.js +1 -0
- package/src/console/ui/dist/assets/panel-plugins-D0PsmVw2.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-CWuRDe0r.js +1 -0
- package/src/console/ui/dist/assets/panel-signals-Bbg4ewpP.js +1 -0
- package/src/console/ui/dist/assets/panel-store-CPCbsDRa.js +1 -0
- package/src/console/ui/dist/assets/panel-traces-DVAzuA_S.js +1 -0
- package/src/console/ui/dist/assets/panel-vault-D1_MvOmo.js +1 -0
- 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/types.ts +1 -0
- package/src/console/ui/plugins/fixture.ts +7 -0
- package/src/console/ui/plugins/types.ts +3 -0
- package/src/console/ui/shell/client.ts +3 -0
- package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +7 -2
- package/src/console/ui/shell/panels/clock/ClockPanel.tsx +9 -2
- package/src/console/ui/shell/panels/gates/GatesPanel.tsx +12 -5
- package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +18 -4
- package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +13 -2
- package/src/console/ui/shell/panels/store/StorePanel.tsx +11 -4
- package/src/console/ui/shell/panels/vault/VaultPanel.tsx +9 -3
- package/src/console/ui/signals/types.ts +1 -0
- package/src/console/ui/store/fixture.ts +5 -0
- package/src/console/ui/store/types.ts +2 -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 +26 -0
- package/src/drivers/conformance.ts +40 -3
- package/src/drivers/drizzle-dialect.test.ts +4 -0
- package/src/drivers/drizzle-dialect.ts +8 -4
- package/src/drivers/index.ts +18 -2
- package/src/drivers/libsql.ts +179 -0
- 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/pglite.ts +79 -0
- package/src/drivers/pgvector.ts +60 -25
- package/src/drivers/types.ts +106 -16
- 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/channel/declare.ts +5 -0
- package/src/elements/clock/declare.ts +5 -0
- package/src/elements/clock/durable.ts +7 -1
- package/src/elements/gate/declare.ts +28 -5
- package/src/elements/gate.ts +1 -0
- package/src/elements/signal/declare.ts +5 -0
- package/src/elements/store/declare.ts +10 -2
- package/src/elements/store/index-boot.test.ts +299 -0
- package/src/elements/store/runtime.ts +110 -17
- package/src/elements/store/schema-decl.ts +7 -0
- 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 +4 -0
- package/src/kernel/abort-scope.ts +116 -0
- package/src/kernel/app.ts +12 -2
- package/src/kernel/boot-bind/store.test.ts +68 -1
- package/src/kernel/boot-bind/store.ts +92 -2
- package/src/kernel/concurrency.test.ts +294 -0
- package/src/kernel/concurrency.ts +220 -0
- package/src/kernel/flow.ts +9 -0
- package/src/kernel/fx.test.ts +23 -2
- package/src/kernel/fx.ts +80 -4
- package/src/kernel/index.ts +23 -0
- package/src/kernel/journal.ts +9 -0
- package/src/kernel/plugin/capabilities.test.ts +18 -0
- package/src/kernel/plugin.ts +11 -3
- package/src/kernel/redacted.ts +74 -0
- package/src/kernel/registry.ts +29 -6
- package/src/kernel/router.ts +3 -3
- package/src/manifest/types.ts +21 -0
- package/src/release/measure.ts +4 -0
- package/src/test/provisions.integration.test.ts +1 -1
- package/site/content/docs/get-started/comparison.mdx +0 -65
- package/src/console/ui/dist/assets/index-BWo8R7NR.js +0 -10
- package/src/console/ui/dist/assets/panel-access-C0J2D-a2.js +0 -64
- package/src/console/ui/dist/assets/panel-channels-BOmQ-onL.js +0 -1
- package/src/console/ui/dist/assets/panel-clock-giAq0Ccv.js +0 -1
- package/src/console/ui/dist/assets/panel-flows-DlCU5zjA.js +0 -45
- package/src/console/ui/dist/assets/panel-gates-XclZxWD5.js +0 -1
- package/src/console/ui/dist/assets/panel-overview-BznEOTnb.js +0 -1
- package/src/console/ui/dist/assets/panel-plugins-CcGM1g64.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-CGWNHLR4.js +0 -1
- package/src/console/ui/dist/assets/panel-signals-CNywkdak.js +0 -1
- package/src/console/ui/dist/assets/panel-store-KmTbFHMH.js +0 -1
- package/src/console/ui/dist/assets/panel-traces-DBLx2ilD.js +0 -1
- package/src/console/ui/dist/assets/panel-vault-CEnFc0dk.js +0 -1
- package/src/drivers/vault-infisical.ts +0 -57
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Why OKE
|
|
3
|
+
description: The six seams every TypeScript backend maintains by hand — and what OKE derives from one Manifest instead.
|
|
4
|
+
source: docs/spec/unified-theory.md
|
|
5
|
+
icon: Compass
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Every TypeScript backend works on day one. The bill arrives later: the cache
|
|
9
|
+
that serves last month's schema, the secret that only exists on your laptop,
|
|
10
|
+
the dashboard that has never heard of your new Flow.
|
|
11
|
+
|
|
12
|
+
None of these are router problems. They are **seams** — copies of your code's
|
|
13
|
+
knowledge, kept in places the compiler cannot check, updated by memory.
|
|
14
|
+
|
|
15
|
+
<Callout title="The one rule">
|
|
16
|
+
**All world access goes through `fx`.** What a Flow reads, writes, emits, and reveals is recorded
|
|
17
|
+
— so the seams below are derived from one Manifest, not re-typed per project.
|
|
18
|
+
</Callout>
|
|
19
|
+
|
|
20
|
+
## The six seams
|
|
21
|
+
|
|
22
|
+
### The cache that lies
|
|
23
|
+
|
|
24
|
+
You add a column to `orders` and update three writers. The hand-bumped cache
|
|
25
|
+
key in `checkout` is not one of them. A customer finds it a week later.
|
|
26
|
+
|
|
27
|
+
**OKE derives:** reads and writes are recorded through `fx`, so invalidation
|
|
28
|
+
follows the Flow — there is no separate key to remember.
|
|
29
|
+
|
|
30
|
+
### The secret that fails in prod
|
|
31
|
+
|
|
32
|
+
`STRIPE_KEY` lives in your laptop's `.env`, a README, and a teammate's shell
|
|
33
|
+
history. The deploy boots fine — the first charge request dies at 2am.
|
|
34
|
+
|
|
35
|
+
**OKE derives:** [Vault](/docs/elements/vault) contracts declare the need in
|
|
36
|
+
code; boot resolves every contract and fails loud with every gap listed —
|
|
37
|
+
never halfway.
|
|
38
|
+
|
|
39
|
+
### The glue you rewrite
|
|
40
|
+
|
|
41
|
+
CORS rules, security headers, CSRF tokens, compression — copied from the last
|
|
42
|
+
repo, tweaked, and already drifting from whatever that repo does today.
|
|
43
|
+
|
|
44
|
+
**OKE derives:** the official `okengine/plugins` set ships this glue once —
|
|
45
|
+
shared lifecycle, optional live DB config, nothing to re-copy.
|
|
46
|
+
|
|
47
|
+
### The dashboard that doesn't know you
|
|
48
|
+
|
|
49
|
+
Your observability stack learned your routes from sampled traffic. The Flow
|
|
50
|
+
you deployed an hour ago is invisible until someone wires it by hand.
|
|
51
|
+
|
|
52
|
+
**OKE derives:** the [Console](/docs/console/overview) reads the Manifest —
|
|
53
|
+
flows, effects, traces, architecture — current on every save, in dev and prod
|
|
54
|
+
(`:6533`).
|
|
55
|
+
|
|
56
|
+
### The permission check in the wrong place
|
|
57
|
+
|
|
58
|
+
`if (!user.isAdmin)` sits in handler forty-one of sixty. Which Flows touch
|
|
59
|
+
`payments`? grep answers slowly; review answers never.
|
|
60
|
+
|
|
61
|
+
**OKE derives:** declared effects produce a least-privilege matrix — widening
|
|
62
|
+
access appears in Manifest Diff, not in a diff nobody reads.
|
|
63
|
+
|
|
64
|
+
### Local works, prod doesn't
|
|
65
|
+
|
|
66
|
+
Local runs one vendor client, CI another, prod a third — three glue stories
|
|
67
|
+
for the same database. "Works on my machine" is a driver mismatch.
|
|
68
|
+
|
|
69
|
+
**OKE derives:** drivers are named after protocols (`postgres`, `redis`,
|
|
70
|
+
`s3`), the vendor lives in `images`, and `oke dev --docker` runs the real
|
|
71
|
+
stack locally.
|
|
72
|
+
|
|
73
|
+
## The tax is drift
|
|
74
|
+
|
|
75
|
+
Every seam above is the same shape: a hand-maintained copy of knowledge the
|
|
76
|
+
code already has. Watch one change propagate both ways.
|
|
77
|
+
|
|
78
|
+
<DriftBoard />
|
|
79
|
+
|
|
80
|
+
On the left, versions scatter and stay scattered. On the right, one Manifest
|
|
81
|
+
feeds five surfaces — they cannot disagree, because none of them is a copy.
|
|
82
|
+
|
|
83
|
+
## The answer's shape
|
|
84
|
+
|
|
85
|
+
Forty infrastructure concerns collapse into eight elements — each kept only
|
|
86
|
+
because it has irreducible physics. One change costs up to fifteen seams in
|
|
87
|
+
the zoo; here it always costs two.
|
|
88
|
+
|
|
89
|
+
<CollapseBoard />
|
|
90
|
+
|
|
91
|
+
New infrastructure becomes a **driver** for an existing element, never a
|
|
92
|
+
ninth element — the set of eight is closed.
|
|
93
|
+
|
|
94
|
+
## Traditional vs OKE
|
|
95
|
+
|
|
96
|
+
| Seam | Maintained by hand | Derived by OKE |
|
|
97
|
+
| ------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
98
|
+
| Behavior model | Endpoints, jobs, consumers, workflows as separate species | One species — Flow: `on(Trigger) → Effects` |
|
|
99
|
+
| Cache invalidation | Hand-written keys; drift from writers | Derived from effects recorded through `fx` |
|
|
100
|
+
| HTTP glue | Middleware copied per repo | Official plugins — `securityHeaders`, `cors`, `csrf`, compression, IP allowlist |
|
|
101
|
+
| Secrets / config | Env sprawl; fails on first request | Vault contracts; `VaultBootError` at boot |
|
|
102
|
+
| Observability | Bolted on; separate source of truth | Console from the Manifest, dev and prod (`:6533`) |
|
|
103
|
+
| Permissions | Ad-hoc checks scattered in handlers | Least-privilege matrix from declared effects |
|
|
104
|
+
| Local vs prod | Vendor clients; one-off compose | Protocol drivers; vendor in `images`; `oke dev --docker` |
|
|
105
|
+
| Client / agents | Separate codegen or hand-kept schemas | Typed client and MCP (`:6535`) from the same Manifest |
|
|
106
|
+
|
|
107
|
+
## Ambition, stated plainly
|
|
108
|
+
|
|
109
|
+
| | Statement |
|
|
110
|
+
| --------------- | ------------------------------------------------------------------------------------------------ |
|
|
111
|
+
| **Ambition** | The default, most capable TypeScript backend — Bun-first, Web-Standards portable, contract-first |
|
|
112
|
+
| **Grounded in** | Eight elements, effect inference through `fx`, Gate and Vault, the official plugin set |
|
|
113
|
+
| **Maturity** | **pre-1.0** — published and usable; not independently battle-tested at scale yet |
|
|
114
|
+
|
|
115
|
+
## Learn more
|
|
116
|
+
|
|
117
|
+
- [Introduction](/docs/get-started/introduction) — the one law, eight elements, ten exports
|
|
118
|
+
- [Flow](/docs/elements/flow) — how effects are recorded and inferred
|
|
119
|
+
- [Vault](/docs/elements/vault) — fail-loud secret contracts
|
|
120
|
+
- [Plugins](/docs/reference/plugins) — the official HTTP glue set
|
|
121
|
+
- [Console · Overview](/docs/console/overview) — the Manifest-derived operator UI
|
|
122
|
+
|
|
123
|
+
## Next
|
|
124
|
+
|
|
125
|
+
<Cards>
|
|
126
|
+
<Card
|
|
127
|
+
title="Installation"
|
|
128
|
+
description="Scaffold with create-oke and open the Console."
|
|
129
|
+
href="/docs/get-started/installation"
|
|
130
|
+
/>
|
|
131
|
+
<Card
|
|
132
|
+
title="Basic Usage"
|
|
133
|
+
description="Health Flow, typed client, and bun:test."
|
|
134
|
+
href="/docs/get-started/basic-usage"
|
|
135
|
+
/>
|
|
136
|
+
<Card
|
|
137
|
+
title="Introduction"
|
|
138
|
+
description="The one law, eight elements, ten exports."
|
|
139
|
+
href="/docs/get-started/introduction"
|
|
140
|
+
/>
|
|
141
|
+
</Cards>
|
|
@@ -46,8 +46,7 @@ A client whose IP is not on the list receives `403` with a typed denial:
|
|
|
46
46
|
<Callout type="error">
|
|
47
47
|
Standard reverse proxies **append** to `X-Forwarded-For` — left-side hops are attacker-controlled.
|
|
48
48
|
The plugin trusts the hop `trustedProxyDepth` from the **right** (default `1` = last hop). Set
|
|
49
|
-
this to your real proxy count
|
|
50
|
-
drop-in.
|
|
49
|
+
this to your real proxy count — wrong depth bypasses the allowlist.
|
|
51
50
|
</Callout>
|
|
52
51
|
|
|
53
52
|
## Notes
|
|
@@ -51,7 +51,7 @@ Every [helmet.js](https://helmet.js.org/) middleware maps to an option here —
|
|
|
51
51
|
| `xPoweredBy` | `poweredBy` | Yes — removed; a string sets a decoy value |
|
|
52
52
|
| `xXssProtection` | `xssProtection` | Yes — `0` (disables the legacy buggy auditor) |
|
|
53
53
|
|
|
54
|
-
Beyond parity: headers land on **failures too** (
|
|
54
|
+
Beyond parity: headers land on **failures too** (middleware that only wraps happy paths skips error responses), app-set values win by default, and every option can be driven live from the database (below).
|
|
55
55
|
|
|
56
56
|
## Options
|
|
57
57
|
|
|
@@ -36,10 +36,10 @@ drivers: {
|
|
|
36
36
|
|
|
37
37
|
| Key | Shape | Driver ids (verified) |
|
|
38
38
|
| ------------------ | -------------- | ---------------------------------------------------------------------------- |
|
|
39
|
-
| `store.sql` | env driver map | `sqlite` · `postgres` · `memory`
|
|
39
|
+
| `store.sql` | env driver map | `sqlite` · `postgres` · `libsql` · `pglite` · `memory` |
|
|
40
40
|
| `store.kv` | env driver map | `memory` · `redis` |
|
|
41
41
|
| `store.files` | env driver map | `memory` · `fs` · `s3` |
|
|
42
|
-
| `store.index` | env driver map | `memory` · `pgvector`
|
|
42
|
+
| `store.index` | env driver map | `memory` · `pgvector` · `libsql` · `meilisearch` |
|
|
43
43
|
| `signal` | env driver map | `memory` · `postgres` · `redis` · `nats` |
|
|
44
44
|
| `clock` | env driver map | `memory` · `postgres` · `frozen` |
|
|
45
45
|
| `vault` | env driver map | `dotenv` · `openbao` · `memory` |
|
|
@@ -18,8 +18,18 @@ OKE reads environment variables at boot for connection detail and secrets — ne
|
|
|
18
18
|
| `DATABASE_URL` | Postgres connection (`postgres` driver, drizzle-kit) | `postgres://localhost:5432/oke` |
|
|
19
19
|
| `OKE_STORE_SQL_URL` | Explicit SQL URL override | — |
|
|
20
20
|
| `OKE_SQLITE_URL` | SQLite file path (`sqlite` driver) | `.oke/app.sqlite` |
|
|
21
|
+
| `OKE_LIBSQL_URL` | libSQL URL or file path (`libsql` driver) | `.oke/app.libsql` |
|
|
22
|
+
| `OKE_PGLITE_URL` | PGlite data dir or `memory://` (`pglite` driver) | `.oke/pgdata` |
|
|
21
23
|
| `OKE_SQL_DRIVER` | Force the sql driver id at boot | config map |
|
|
22
24
|
|
|
25
|
+
## Index store
|
|
26
|
+
|
|
27
|
+
| Variable | Used for | Default when unset |
|
|
28
|
+
| --------------------- | --------------------------------------------------- | ------------------ |
|
|
29
|
+
| `OKE_INDEX_DRIVER` | Force the index driver id | config map |
|
|
30
|
+
| `OKE_STORE_INDEX_URL` | Meilisearch base URL (`meilisearch` driver) | — |
|
|
31
|
+
| `OKE_STORE_INDEX_KEY` | Meilisearch API / master key (`meilisearch` driver) | `MEILI_MASTER_KEY` |
|
|
32
|
+
|
|
23
33
|
## KV store
|
|
24
34
|
|
|
25
35
|
| Variable | Used for | Default when unset |
|
|
@@ -41,8 +41,61 @@ See [Store](/docs/elements/store) for the query-builder surface.
|
|
|
41
41
|
| ---------------------------- | ------- | ----------------------------------------------------- |
|
|
42
42
|
| `fx.call(flow, input?)` | `call` | The callee's `out` — runs through the same pipeline |
|
|
43
43
|
| `fx.step(name, fn)` | — | Durable step: replays from the journal, never re-runs |
|
|
44
|
+
| `fx.all([...thunks])` | — | Parallel; first rejection aborts siblings |
|
|
45
|
+
| `fx.race([...thunks])` | — | First settle wins; losers aborted |
|
|
46
|
+
| `fx.retry(fn, opts?)` | — | Exponential backoff + jitter (plain Promise) |
|
|
47
|
+
| `fx.using(acq, rel, use)` | — | `release` runs once on settle or ambient abort |
|
|
48
|
+
| `fx.signal` | — | Ambient `AbortSignal` for the current branch |
|
|
44
49
|
| `fx.fail(code, data, opts?)` | — | Typed failure value (`opts.message` overrides) |
|
|
45
50
|
|
|
51
|
+
## Concurrency and retry
|
|
52
|
+
|
|
53
|
+
Pass **thunks** to `all` / `race` — not already-started Promises — so each branch gets an abort scope before work begins.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
const [user, stock] = await fx.all([
|
|
57
|
+
() => fx.store(db).findById(users, input.userId),
|
|
58
|
+
() => fx.store(db).findById(inventory, input.sku),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const charge = await fx.step("charge", () =>
|
|
62
|
+
fx.retry(() => fx.call(stripeCharge, { amount: input.total }), {
|
|
63
|
+
retries: 3,
|
|
64
|
+
delay: "100ms",
|
|
65
|
+
backoff: 2,
|
|
66
|
+
jitter: true,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| `fx.retry` option | Default | Meaning |
|
|
72
|
+
| ----------------- | ------- | -------------------------------------------- |
|
|
73
|
+
| `retries` | `0` | Extra attempts after the first |
|
|
74
|
+
| `delay` | `50` | Initial backoff — ms number or `"100ms"` |
|
|
75
|
+
| `backoff` | `2` | Multiplier after each retry |
|
|
76
|
+
| `jitter` | `true` | Full jitter on the delay (thundering-herd) |
|
|
77
|
+
| `when` | thrown | Predicate; skips `AbortError` and sleep park |
|
|
78
|
+
|
|
79
|
+
<Callout title="Cooperative cancel">
|
|
80
|
+
Losing branches see `fx.signal` abort. Drivers that do not yet honor the signal may still finish
|
|
81
|
+
in the background — check `fx.signal.aborted` in long user work, and prefer `fx.all` over bare
|
|
82
|
+
`Promise.all`.
|
|
83
|
+
</Callout>
|
|
84
|
+
|
|
85
|
+
`fx.using(acquire, release, use)` scopes a process-local resource to one attempt: `release` runs
|
|
86
|
+
exactly once when `use` settles **or** when the ambient signal aborts (a sibling `fx.race` winner,
|
|
87
|
+
a failing `fx.all` sibling). It is not journaled — do not hold handles across durable park/resume.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const rows = await fx.using(
|
|
91
|
+
() => pool.acquire(),
|
|
92
|
+
(conn) => conn.release(),
|
|
93
|
+
(conn) => conn.query("select …"),
|
|
94
|
+
);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Consequence:** put `fx.retry` inside `fx.step` on durable flows so a completed charge never re-runs on resume. Coarse whole-body retry is `flow({ retry: { … } })` on the same journal session.
|
|
98
|
+
|
|
46
99
|
## Channel
|
|
47
100
|
|
|
48
101
|
| Signature | Records | Notes |
|
|
@@ -62,9 +115,9 @@ AI calls are nondeterministic: journaling is forced on and auto-cache disabled a
|
|
|
62
115
|
|
|
63
116
|
## Vault
|
|
64
117
|
|
|
65
|
-
| Signature | Records | Returns / notes
|
|
66
|
-
| -------------------- | ------- |
|
|
67
|
-
| `fx.vault(contract)` | `read` |
|
|
118
|
+
| Signature | Records | Returns / notes |
|
|
119
|
+
| -------------------- | ------- | ----------------------------------------------------------------------------------------- |
|
|
120
|
+
| `fx.vault(contract)` | `read` | `Redacted<string>` — prints/logs as a placeholder; `.reveal()` at the credential boundary |
|
|
68
121
|
|
|
69
122
|
## Clock
|
|
70
123
|
|
|
@@ -64,22 +64,22 @@ Plugin flows appear in the Manifest, plugin tables land in `schema.generated.ts`
|
|
|
64
64
|
|
|
65
65
|
Every method below exists on both the fluent definition and the boot-time builder the registry records:
|
|
66
66
|
|
|
67
|
-
| Method | Contributes
|
|
68
|
-
| -------------------------------- |
|
|
69
|
-
| `.flow(def)` | An ordinary flow — Manifest, Console, client types
|
|
70
|
-
| `.hook(stage, fn)` | A per-request intercept at one pipeline stage
|
|
71
|
-
| `.edge(fn)` | A handler for HTTP requests that match **no** flow
|
|
72
|
-
| `.decorate(key, value)` | A typed context decoration, visible to flows
|
|
73
|
-
| `.element({ kind, name })` | An element contribution (e.g. `store.sql` facet)
|
|
74
|
-
| `.driver(id, impl)` | A protocol-named driver for an existing element
|
|
75
|
-
| `.image(role, recipe)` | An image recipe for a docker role
|
|
76
|
-
| `.table(name, columns, options)` | A whole DB table, merged into the generated schema
|
|
77
|
-
| `.errors(map)` | Typed errors flows can fail with
|
|
78
|
-
| `.client(name, ext)` | A typed client extension
|
|
79
|
-
| `.consolePanel(panel)` | A Console panel (ESM entry loaded at runtime)
|
|
80
|
-
| `.cli(name, handler)` | An `oke <name>` CLI command
|
|
81
|
-
| `.config(schema)` | A config schema; values live on the plugin identity
|
|
82
|
-
| `.needs(dep)` | A declared dependency (e.g. `"store.kv"`)
|
|
67
|
+
| Method | Contributes |
|
|
68
|
+
| -------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `.flow(def)` | An ordinary flow — Manifest, Console, client types |
|
|
70
|
+
| `.hook(stage, fn)` | A per-request intercept at one pipeline stage |
|
|
71
|
+
| `.edge(fn)` | A handler for HTTP requests that match **no** flow |
|
|
72
|
+
| `.decorate(key, value)` | A typed context decoration, visible to flows |
|
|
73
|
+
| `.element({ kind, name })` | An element contribution (e.g. `store.sql` facet) |
|
|
74
|
+
| `.driver(id, impl)` | A protocol-named driver for an existing element |
|
|
75
|
+
| `.image(role, recipe)` | An image recipe for a docker role |
|
|
76
|
+
| `.table(name, columns, options)` | A whole DB table, merged into the generated schema (`options.description` / `plane` optional) |
|
|
77
|
+
| `.errors(map)` | Typed errors flows can fail with |
|
|
78
|
+
| `.client(name, ext)` | A typed client extension |
|
|
79
|
+
| `.consolePanel(panel)` | A Console panel (ESM entry loaded at runtime) |
|
|
80
|
+
| `.cli(name, handler)` | An `oke <name>` CLI command |
|
|
81
|
+
| `.config(schema)` | A config schema; values live on the plugin identity |
|
|
82
|
+
| `.needs(dep)` | A declared dependency (e.g. `"store.kv"`) |
|
|
83
83
|
|
|
84
84
|
New infrastructure is a **driver** for an existing element, never a ninth element — plugins follow the same law.
|
|
85
85
|
|
|
@@ -194,11 +194,11 @@ A plugin may declare **its own tables** with `field.*` columns, merged into the
|
|
|
194
194
|
plugin("billing", { version: "2.1.0" }).table(
|
|
195
195
|
"invoices",
|
|
196
196
|
{ id: field.text().primaryKey().defaultFn(id) },
|
|
197
|
-
{ plane: "user" },
|
|
197
|
+
{ plane: "user", description: "Customer invoices" },
|
|
198
198
|
);
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
-
Extending an existing **app-owned** table with plugin columns is not supported in v1 — contribute a separate table and reference the app's by key. The optional `plane` metadata (`"operator" | "user" | "shared"`) keeps data-plane isolation intact for privacy tooling.
|
|
201
|
+
Extending an existing **app-owned** table with plugin columns is not supported in v1 — contribute a separate table and reference the app's by key. The optional `plane` metadata (`"operator" | "user" | "shared"`) keeps data-plane isolation intact for privacy tooling. Optional `description` is a human title in the Console (falls back to the table name).
|
|
202
202
|
|
|
203
203
|
## Identity, config, and dependencies
|
|
204
204
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate: named competitor comparisons stay gone from the tracked tree.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors {@link ./doc-staleness.test.ts} — `git grep` must find zero hits
|
|
5
|
+
* (the security-headers decoy header fixture may still use a common
|
|
6
|
+
* `X-Powered-By` value; that path is allow-listed).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
const ROOT = join(import.meta.dir, "../..");
|
|
13
|
+
|
|
14
|
+
/** Forbidden peer name — split so this file does not match itself. */
|
|
15
|
+
const FORBIDDEN_HONO = ["Ho", "no"].join("");
|
|
16
|
+
/** Forbidden peer name. */
|
|
17
|
+
const FORBIDDEN_ELYSIA = ["Ely", "sia"].join("");
|
|
18
|
+
/** Forbidden peer name (covers Enc + ore.ts). */
|
|
19
|
+
const FORBIDDEN_ENCORE = ["Enc", "ore"].join("");
|
|
20
|
+
/** Forbidden peer name. */
|
|
21
|
+
const FORBIDDEN_NESTJS = ["Nest", "JS"].join("");
|
|
22
|
+
/** Forbidden peer name. */
|
|
23
|
+
const FORBIDDEN_FASTIFY = ["Fast", "ify"].join("");
|
|
24
|
+
/** Forbidden peer site — avoid bare `iii` (matches lockfile hashes). */
|
|
25
|
+
const FORBIDDEN_III_DEV = ["iii", ".", "dev"].join("");
|
|
26
|
+
/** Deleted Comparison page path residue. */
|
|
27
|
+
const FORBIDDEN_COMPARISON_PATH = ["get-started/", "comparison"].join("");
|
|
28
|
+
/** Word-boundary classic Node framework — `-F` false-positives on `CallExpression`. */
|
|
29
|
+
const FORBIDDEN_EXPRESS = ["\\b", "Ex", "press", "\\b"].join("");
|
|
30
|
+
|
|
31
|
+
/** Decoy `X-Powered-By` fixture value in the security-headers plugin tests. */
|
|
32
|
+
const EXPRESS_ALLOW = ["src/plugins/security-headers.test.ts:"];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run `git grep -F` and assert zero matches after ignoring allow-listed paths.
|
|
36
|
+
*
|
|
37
|
+
* @param pattern - Fixed-string pattern
|
|
38
|
+
* @param ignorePrefixes - Path prefixes to ignore (`path:line:` hits)
|
|
39
|
+
*/
|
|
40
|
+
function assertZeroGitGrepFixed(pattern: string, ignorePrefixes: readonly string[] = []): void {
|
|
41
|
+
const proc = Bun.spawnSync(["git", "grep", "-F", "-n", "-e", pattern, "--"], {
|
|
42
|
+
cwd: ROOT,
|
|
43
|
+
stdout: "pipe",
|
|
44
|
+
stderr: "pipe",
|
|
45
|
+
});
|
|
46
|
+
// git grep exits 1 when there are no matches.
|
|
47
|
+
if (proc.exitCode === 1) {
|
|
48
|
+
expect(proc.stdout.toString()).toBe("");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const hits = proc.stdout
|
|
52
|
+
.toString()
|
|
53
|
+
.trim()
|
|
54
|
+
.split("\n")
|
|
55
|
+
.filter((line) => line.length > 0)
|
|
56
|
+
.filter((line) => !ignorePrefixes.some((prefix) => line.startsWith(prefix)));
|
|
57
|
+
expect(hits.join("\n")).toBe("");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run `git grep` with a basic regex and assert zero matches after allow-list.
|
|
62
|
+
*
|
|
63
|
+
* @param pattern - Basic regex (no `-F`)
|
|
64
|
+
* @param ignorePrefixes - Path prefixes to ignore (`path:line:` hits)
|
|
65
|
+
*/
|
|
66
|
+
function assertZeroGitGrepRegex(pattern: string, ignorePrefixes: readonly string[] = []): void {
|
|
67
|
+
const proc = Bun.spawnSync(["git", "grep", "-n", "-e", pattern, "--"], {
|
|
68
|
+
cwd: ROOT,
|
|
69
|
+
stdout: "pipe",
|
|
70
|
+
stderr: "pipe",
|
|
71
|
+
});
|
|
72
|
+
if (proc.exitCode === 1) {
|
|
73
|
+
expect(proc.stdout.toString()).toBe("");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const hits = proc.stdout
|
|
77
|
+
.toString()
|
|
78
|
+
.trim()
|
|
79
|
+
.split("\n")
|
|
80
|
+
.filter((line) => line.length > 0)
|
|
81
|
+
.filter((line) => !ignorePrefixes.some((prefix) => line.startsWith(prefix)));
|
|
82
|
+
expect(hits.join("\n")).toBe("");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("named competitor mention removal gate", () => {
|
|
86
|
+
test("tracked tree has zero mentions of the thin multi-runtime router peer", () => {
|
|
87
|
+
assertZeroGitGrepFixed(FORBIDDEN_HONO);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("tracked tree has zero mentions of the Bun-first peer framework", () => {
|
|
91
|
+
assertZeroGitGrepFixed(FORBIDDEN_ELYSIA);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("tracked tree has zero mentions of the Rust-core peer platform", () => {
|
|
95
|
+
assertZeroGitGrepFixed(FORBIDDEN_ENCORE);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("tracked tree has zero mentions of the decorator DI peer", () => {
|
|
99
|
+
assertZeroGitGrepFixed(FORBIDDEN_NESTJS);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("tracked tree has zero mentions of the Node HTTP peer", () => {
|
|
103
|
+
assertZeroGitGrepFixed(FORBIDDEN_FASTIFY);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("tracked tree has zero mentions of the polyglot peer site host", () => {
|
|
107
|
+
assertZeroGitGrepFixed(FORBIDDEN_III_DEV);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("tracked tree has zero deleted Comparison path residue", () => {
|
|
111
|
+
assertZeroGitGrepFixed(FORBIDDEN_COMPARISON_PATH);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("tracked tree has zero classic Node framework peer mentions outside decoy fixture", () => {
|
|
115
|
+
assertZeroGitGrepRegex(FORBIDDEN_EXPRESS, EXPRESS_ALLOW);
|
|
116
|
+
});
|
|
117
|
+
});
|
package/src/cli/dev.ts
CHANGED
|
@@ -339,6 +339,24 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
339
339
|
loadedConfig = null;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
// Local mode + a configured `meilisearch` store.index: bring the server up
|
|
343
|
+
// as a fourth local process (binary on PATH — a documented prerequisite,
|
|
344
|
+
// like Docker for --docker). Docker mode instead relies on the recipe.
|
|
345
|
+
let meili: import("./meilisearch-local.ts").LocalMeilisearchHandle | null = null;
|
|
346
|
+
if (mode === "local") {
|
|
347
|
+
const indexId = resolveDriverId(loadedConfig?.drivers?.store?.index, "local") ?? "memory";
|
|
348
|
+
if (indexId === "meilisearch") {
|
|
349
|
+
try {
|
|
350
|
+
const { startLocalMeilisearch } = await import("./meilisearch-local.ts");
|
|
351
|
+
meili = await startLocalMeilisearch({ cwd });
|
|
352
|
+
write(formatStatusLine(`meilisearch local server on ${meili.url}`));
|
|
353
|
+
} catch (err) {
|
|
354
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
355
|
+
return { code: 1 };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
342
360
|
if (mode === "docker") {
|
|
343
361
|
let images = options.images;
|
|
344
362
|
if (!images) {
|
|
@@ -567,6 +585,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
567
585
|
OKE_KV_DRIVER: stackKvDriver,
|
|
568
586
|
}
|
|
569
587
|
: {}),
|
|
588
|
+
...(meili?.overlay ?? {}),
|
|
570
589
|
};
|
|
571
590
|
process.env.OKE_DEV_REQUEST_LOG = "1";
|
|
572
591
|
|
|
@@ -755,6 +774,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
755
774
|
stopped = true;
|
|
756
775
|
autoPushRunner.cancel();
|
|
757
776
|
watcher.close();
|
|
777
|
+
meili?.stop();
|
|
758
778
|
app.stop();
|
|
759
779
|
consoleServer.stop();
|
|
760
780
|
mcpServer?.stop();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Meilisearch lifecycle — persisted master key material and fail-loud
|
|
3
|
+
* binary resolution. Never spawns a real server here.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
7
|
+
import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
ensureMasterKey,
|
|
12
|
+
MEILISEARCH_STATE_DIR_REL,
|
|
13
|
+
MeilisearchLocalError,
|
|
14
|
+
startLocalMeilisearch,
|
|
15
|
+
} from "./meilisearch-local.ts";
|
|
16
|
+
|
|
17
|
+
const dirs: string[] = [];
|
|
18
|
+
|
|
19
|
+
function tempStateDir(): string {
|
|
20
|
+
const dir = mkdtempSync(join(tmpdir(), "oke-meili-"));
|
|
21
|
+
dirs.push(dir);
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
while (dirs.length > 0) {
|
|
27
|
+
const dir = dirs.pop();
|
|
28
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("ensureMasterKey", () => {
|
|
33
|
+
test("generates once, persists 0600, reuses on next call", () => {
|
|
34
|
+
const stateDir = tempStateDir();
|
|
35
|
+
const first = ensureMasterKey(stateDir);
|
|
36
|
+
const second = ensureMasterKey(stateDir);
|
|
37
|
+
expect(first).toBe(second);
|
|
38
|
+
expect(first.length).toBeGreaterThanOrEqual(32);
|
|
39
|
+
const keyPath = join(stateDir, "master.key");
|
|
40
|
+
expect(statSync(keyPath).mode & 0o777).toBe(0o600);
|
|
41
|
+
expect(statSync(stateDir).mode & 0o777).toBe(0o700);
|
|
42
|
+
expect(readFileSync(keyPath, "utf8").trim()).toBe(first);
|
|
43
|
+
// No leftover temp files.
|
|
44
|
+
expect(readdirSync(stateDir).filter((f) => f.includes(".tmp-"))).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("a too-short persisted key fails loud", () => {
|
|
48
|
+
const stateDir = tempStateDir();
|
|
49
|
+
const keyPath = join(stateDir, "master.key");
|
|
50
|
+
// Write a weak key directly (bypasses the writer) to simulate corruption.
|
|
51
|
+
writeFileSync(keyPath, "short\n");
|
|
52
|
+
expect(() => ensureMasterKey(stateDir)).toThrow(MeilisearchLocalError);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("startLocalMeilisearch", () => {
|
|
57
|
+
test("missing binary fails loud with an install hint (never silent memory)", async () => {
|
|
58
|
+
await expect(
|
|
59
|
+
startLocalMeilisearch({
|
|
60
|
+
binary: "meilisearch-definitely-not-on-path-xyz",
|
|
61
|
+
stateDir: tempStateDir(),
|
|
62
|
+
}),
|
|
63
|
+
).rejects.toThrow(/not found on PATH|never auto-downloads/i);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("state dir rel lives under .oke", () => {
|
|
67
|
+
expect(MEILISEARCH_STATE_DIR_REL).toBe(join(".oke", "meilisearch"));
|
|
68
|
+
});
|
|
69
|
+
});
|