okengine 0.6.1 → 0.8.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 -2
- package/README.md +73 -181
- package/package.json +3 -3
- package/site/content/docs/console/clock.mdx +25 -5
- package/site/content/docs/console/signals.mdx +13 -4
- package/site/content/docs/elements/ai.mdx +29 -2
- package/site/content/docs/elements/channel.mdx +42 -25
- package/site/content/docs/elements/clock.mdx +59 -25
- package/site/content/docs/elements/flow.mdx +8 -2
- package/site/content/docs/elements/gate.mdx +8 -1
- package/site/content/docs/elements/signal.mdx +173 -29
- package/site/content/docs/elements/store.mdx +595 -73
- package/site/content/docs/elements/vault.mdx +15 -5
- package/site/content/docs/get-started/basic-usage.mdx +13 -6
- package/site/content/docs/get-started/installation.mdx +52 -14
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/plugins/magic-link.mdx +42 -0
- package/site/content/docs/plugins/phone-number.mdx +78 -17
- package/site/content/docs/plugins/two-factor.mdx +1 -0
- package/site/content/docs/reference/cli.md +32 -2
- package/site/content/docs/reference/configuration.mdx +15 -13
- package/site/content/docs/reference/environment-variables.mdx +32 -18
- package/site/content/docs/reference/errors.mdx +12 -11
- package/site/content/docs/reference/fx.mdx +15 -13
- package/site/content/docs/reference/security.md +3 -1
- package/src/cli/ai-setup/ai-setup.test.ts +144 -0
- package/src/cli/ai-setup/apply.ts +214 -0
- package/src/cli/ai-setup/catalog.ts +263 -0
- package/src/cli/ai-setup/detect-ollama.ts +166 -0
- package/src/cli/ai-setup/index.ts +228 -0
- package/src/cli/ai-setup/prompts.ts +649 -0
- package/src/cli/ai-setup/recommend.test.ts +100 -0
- package/src/cli/ai-setup/recommend.ts +203 -0
- package/src/cli/ai.ts +38 -0
- package/src/cli/db-auto-push.test.ts +6 -1
- package/src/cli/db-auto-push.ts +8 -3
- package/src/cli/db-seed.ts +359 -0
- package/src/cli/db.test.ts +341 -3
- package/src/cli/db.ts +75 -8
- package/src/cli/dev-db-push.test.ts +59 -0
- package/src/cli/dev.test.ts +40 -0
- package/src/cli/dev.ts +55 -1
- package/src/cli/docker-clean.test.ts +179 -0
- package/src/cli/docker-clean.ts +406 -0
- package/src/cli/docker.ts +16 -4
- package/src/cli/ensure-drizzle-config.ts +1 -1
- package/src/cli/hero-meta.test.ts +6 -5
- package/src/cli/hero-meta.ts +7 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +22 -0
- package/src/cli/load-config.ts +7 -2
- package/src/cli/openbao-bootstrap.test.ts +1 -0
- package/src/cli/openbao-bootstrap.ts +9 -2
- package/src/cli/registry.ts +116 -2
- package/src/client/live-gap.test.ts +35 -0
- package/src/client/transport.test.ts +21 -0
- package/src/client/transport.ts +27 -5
- package/src/compiler/effects-infer.ts +1 -0
- package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
- package/src/config/define-config.test.ts +6 -6
- package/src/config/index.ts +5 -1
- package/src/console/server/console.test.ts +45 -0
- package/src/console/server/flows.ts +39 -9
- package/src/console/server/i18n.ts +41 -0
- package/src/console/server/vault.ts +12 -38
- package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
- package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
- package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
- package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
- package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
- package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
- package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
- package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
- package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
- package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
- package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
- package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/shell/components/ui.tsx +5 -1
- package/src/console/ui/shell/setup/Wizard.tsx +26 -5
- package/src/docker/cleanup.test.ts +193 -0
- package/src/docker/cleanup.ts +355 -0
- package/src/docker/docker.test.ts +4 -0
- package/src/docker/index.ts +19 -0
- package/src/docker/recipes/ollama.ts +10 -5
- package/src/drivers/ai-ollama.ts +45 -2
- package/src/drivers/ai-providers.test.ts +31 -0
- package/src/drivers/channel-sently.test.ts +8 -0
- package/src/drivers/channel-taqnyat-mail.ts +34 -0
- package/src/drivers/channel-types.ts +71 -0
- package/src/drivers/clock-postgres.test.ts +258 -0
- package/src/drivers/clock-postgres.ts +410 -0
- package/src/drivers/index.ts +21 -0
- package/src/drivers/journal-postgres.test.ts +175 -0
- package/src/drivers/journal-postgres.ts +492 -0
- package/src/drivers/signal-engine.ts +98 -25
- package/src/drivers/signal-nats.ts +4 -4
- package/src/drivers/signal-postgres.ts +158 -64
- package/src/drivers/signal-redis.ts +4 -4
- package/src/drivers/signal-types.ts +61 -2
- package/src/drivers/vault-driver-removal.test.ts +5 -1
- package/src/elements/channel/runtime.ts +51 -0
- package/src/elements/channel.test.ts +71 -0
- package/src/elements/clock/chaos-child.ts +389 -0
- package/src/elements/clock/chaos.test.ts +204 -0
- package/src/elements/clock/durable.ts +7 -0
- package/src/elements/clock/reconcile.ts +110 -0
- package/src/elements/clock/runtime.ts +13 -0
- package/src/elements/clock.test.ts +164 -2
- package/src/elements/clock.ts +2 -1
- package/src/elements/gate/runtime.ts +6 -0
- package/src/elements/signal/chaos-child.ts +41 -4
- package/src/elements/signal/declare.ts +4 -1
- package/src/elements/signal/delivery-modes.test.ts +179 -0
- package/src/elements/signal/key-ordering.test.ts +306 -0
- package/src/elements/signal/lease-reclaim.test.ts +123 -0
- package/src/elements/signal/optional-emit.test.ts +76 -0
- package/src/elements/signal/order-lifecycle.test.ts +130 -0
- package/src/elements/signal/orphan-messages.test.ts +152 -0
- package/src/elements/signal/runtime.ts +11 -4
- package/src/elements/signal/schema-emit.test.ts +107 -0
- package/src/elements/signal.test.ts +89 -5
- package/src/elements/store/files-fx.ts +104 -0
- package/src/elements/store/files-image.test.ts +251 -0
- package/src/elements/store/files-image.ts +494 -0
- package/src/elements/store/runtime.ts +60 -0
- package/src/elements/store/seed.test.ts +27 -0
- package/src/elements/store/seed.ts +68 -0
- package/src/elements/store/sql-session.test.ts +39 -0
- package/src/elements/store/sql-session.ts +55 -0
- package/src/elements/store/upsert-app.test.ts +103 -0
- package/src/elements/store.ts +29 -0
- package/src/elements/vault/boot-chain.ts +150 -0
- package/src/elements/vault/runtime.ts +8 -0
- package/src/index.ts +15 -0
- package/src/kernel/app.ts +168 -15
- package/src/kernel/boot-bind/channel.test.ts +16 -0
- package/src/kernel/boot-bind/channel.ts +13 -0
- package/src/kernel/boot-bind/clock.ts +63 -7
- package/src/kernel/boot-bind/gate.ts +71 -3
- package/src/kernel/boot-bind/honor-config.test.ts +348 -0
- package/src/kernel/boot-bind/journal.ts +89 -0
- package/src/kernel/boot-bind/signal.ts +64 -7
- package/src/kernel/boot-bind/vault.ts +27 -8
- package/src/kernel/boot.test.ts +6 -4
- package/src/kernel/boot.ts +63 -15
- package/src/kernel/concurrency.ts +1 -1
- package/src/kernel/errors.ts +8 -2
- package/src/kernel/fx.test.ts +19 -0
- package/src/kernel/fx.ts +153 -9
- package/src/kernel/index.ts +6 -0
- package/src/kernel/journal-boot.test.ts +397 -0
- package/src/kernel/journal-suspend.ts +35 -0
- package/src/kernel/journal.test.ts +142 -0
- package/src/kernel/journal.ts +202 -27
- package/src/plugins/auth-methods.security.test.ts +10 -7
- package/src/plugins/phone-number.ts +67 -10
- package/src/plugins/taqnyat.live.test.ts +174 -0
- package/src/runtime/dev-request-log.test.ts +20 -1
- package/src/runtime/dev-request-log.ts +38 -0
- package/src/term.test.ts +15 -0
- package/src/term.ts +8 -3
- package/src/console/ui/dist/assets/index-CjxwRGVv.js +0 -10
package/src/kernel/boot.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* 1. Vault — resolve every declared secret (list all gaps at once)
|
|
6
6
|
* 2. Store — bind drivers per environment; open connections
|
|
7
7
|
* 3. Signals — register declarations; start consumers
|
|
8
|
-
* 4. Clocks — reconcile into the Store
|
|
8
|
+
* 4. Clocks — reconcile into the Store
|
|
9
|
+
* 4b. Journal — bind the durable-run store (SKIP LOCKED + lease when shared)
|
|
10
|
+
* 4c. Scheduler — tick clocks + resume due durable runs (leader election)
|
|
9
11
|
* 5. Channel — bind channel runtime
|
|
10
12
|
* 6. AI — bind AI runtime
|
|
11
13
|
* 7. Runs — open the runs store
|
|
@@ -33,6 +35,7 @@ import type {
|
|
|
33
35
|
VaultRuntime,
|
|
34
36
|
VaultSecretDecl,
|
|
35
37
|
} from "../elements/vault.ts";
|
|
38
|
+
import type { JournalRuntime } from "./boot-bind/journal.ts";
|
|
36
39
|
import type { CreateRunsRuntimeOptions, RunsRuntime } from "../runs/index.ts";
|
|
37
40
|
import { createCapabilityToken, type CapabilityToken } from "./capability.ts";
|
|
38
41
|
import type { AnyFlowDef } from "./flow.ts";
|
|
@@ -48,6 +51,8 @@ export interface ElementRuntimes {
|
|
|
48
51
|
readonly channel?: ChannelRuntime;
|
|
49
52
|
readonly ai?: AiRuntime;
|
|
50
53
|
readonly runs?: RunsRuntime;
|
|
54
|
+
/** Pre-bound durable-run journal (skips driver resolution). */
|
|
55
|
+
readonly journal?: JournalRuntime;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
/** Declarations + options consumed by {@link bootApplication}. */
|
|
@@ -112,8 +117,23 @@ export interface BootOptions {
|
|
|
112
117
|
* @param payload - Payload
|
|
113
118
|
*/
|
|
114
119
|
readonly onSignal?: (signal: string, payload: unknown) => void | Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Resume due durable runs — called on every scheduler tick when any flow
|
|
122
|
+
* declares `durable: true` (claimDueSleep on the shared journal store).
|
|
123
|
+
*/
|
|
124
|
+
readonly onDurableResume?: () => void | Promise<void>;
|
|
125
|
+
/** Durable-run lease duration ms (default 30_000 — matches Signal claims). */
|
|
126
|
+
readonly journalLeaseMs?: number;
|
|
115
127
|
/** Injectable clock for test / frozen harnesses. */
|
|
116
128
|
readonly now?: () => number;
|
|
129
|
+
/**
|
|
130
|
+
* Test-only client injection for binders that open redis / signal redis.
|
|
131
|
+
* Production apps leave this unset.
|
|
132
|
+
*/
|
|
133
|
+
readonly clients?: {
|
|
134
|
+
readonly kv?: import("../drivers/types.ts").KvClientLike;
|
|
135
|
+
readonly signalRedis?: import("../drivers/signal-types.ts").SignalRedisClientLike;
|
|
136
|
+
};
|
|
117
137
|
/** Instance id for leader election. */
|
|
118
138
|
readonly instanceId?: string;
|
|
119
139
|
/**
|
|
@@ -136,6 +156,8 @@ export interface BootResult {
|
|
|
136
156
|
readonly channel?: ChannelRuntime;
|
|
137
157
|
readonly ai?: AiRuntime;
|
|
138
158
|
readonly runs?: RunsRuntime;
|
|
159
|
+
/** Durable-run journal (present when any flow declares `durable: true`). */
|
|
160
|
+
readonly journal?: JournalRuntime;
|
|
139
161
|
/** Per-flow capability tokens minted from declared effects. */
|
|
140
162
|
readonly capabilities: ReadonlyMap<string, CapabilityToken>;
|
|
141
163
|
/** Stop the background scheduler (if started). */
|
|
@@ -154,6 +176,8 @@ export interface ElementNeeds {
|
|
|
154
176
|
readonly channel: boolean;
|
|
155
177
|
readonly ai: boolean;
|
|
156
178
|
readonly runs: boolean;
|
|
179
|
+
/** Durable-run journal — any flow with `durable: true`. */
|
|
180
|
+
readonly journal: boolean;
|
|
157
181
|
}
|
|
158
182
|
|
|
159
183
|
/**
|
|
@@ -174,9 +198,11 @@ export function resolveElementNeeds(options: BootOptions): ElementNeeds {
|
|
|
174
198
|
let channel = pre.channel !== undefined || options.channel !== undefined;
|
|
175
199
|
let ai = pre.ai !== undefined || options.ai !== undefined;
|
|
176
200
|
let runs = pre.runs !== undefined || options.runs !== undefined;
|
|
201
|
+
let journal = pre.journal !== undefined;
|
|
177
202
|
|
|
178
203
|
const considerFlow = (f: AnyFlowDef): void => {
|
|
179
204
|
const e = f.effects;
|
|
205
|
+
if (f.durable === true) journal = true;
|
|
180
206
|
if ((e?.reads?.length ?? 0) > 0 || (e?.writes?.length ?? 0) > 0) {
|
|
181
207
|
store = true;
|
|
182
208
|
}
|
|
@@ -208,7 +234,7 @@ export function resolveElementNeeds(options: BootOptions): ElementNeeds {
|
|
|
208
234
|
signal = true;
|
|
209
235
|
}
|
|
210
236
|
|
|
211
|
-
return { vault, store, signal, clock, gate, channel, ai, runs };
|
|
237
|
+
return { vault, store, signal, clock, gate, channel, ai, runs, journal };
|
|
212
238
|
}
|
|
213
239
|
|
|
214
240
|
/**
|
|
@@ -264,6 +290,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
264
290
|
type StoreBind = typeof import("./boot-bind/store.ts");
|
|
265
291
|
type SignalBind = typeof import("./boot-bind/signal.ts");
|
|
266
292
|
type ClockBind = typeof import("./boot-bind/clock.ts");
|
|
293
|
+
type JournalBind = typeof import("./boot-bind/journal.ts");
|
|
267
294
|
type GateBind = typeof import("./boot-bind/gate.ts");
|
|
268
295
|
type ChannelBind = typeof import("./boot-bind/channel.ts");
|
|
269
296
|
type AiBind = typeof import("./boot-bind/ai.ts");
|
|
@@ -272,6 +299,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
272
299
|
let storeBind: StoreBind | undefined;
|
|
273
300
|
let signalBind: SignalBind | undefined;
|
|
274
301
|
let clockBind: ClockBind | undefined;
|
|
302
|
+
let journalBind: JournalBind | undefined;
|
|
275
303
|
let gateBind: GateBind | undefined;
|
|
276
304
|
let channelBind: ChannelBind | undefined;
|
|
277
305
|
let aiBind: AiBind | undefined;
|
|
@@ -298,6 +326,13 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
298
326
|
}),
|
|
299
327
|
);
|
|
300
328
|
}
|
|
329
|
+
if (needs.journal && !pre.journal) {
|
|
330
|
+
binderLoads.push(
|
|
331
|
+
loadBind<JournalBind>("journal").then((m) => {
|
|
332
|
+
journalBind = m;
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
301
336
|
if (needs.gate && !pre.gate) {
|
|
302
337
|
binderLoads.push(
|
|
303
338
|
loadBind<GateBind>("gate").then((m) => {
|
|
@@ -344,7 +379,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
344
379
|
let signal = pre.signal;
|
|
345
380
|
if (needs.signal) {
|
|
346
381
|
if (!signal) {
|
|
347
|
-
signal = await signalBind!.bindSignal(options, env, now);
|
|
382
|
+
signal = await signalBind!.bindSignal(options, env, now, docker);
|
|
348
383
|
} else {
|
|
349
384
|
// Pre-built: still register decls / start if needed.
|
|
350
385
|
for (const decl of options.signals ?? []) {
|
|
@@ -356,27 +391,37 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
356
391
|
}
|
|
357
392
|
}
|
|
358
393
|
|
|
359
|
-
// 4. Clocks
|
|
394
|
+
// 4. Clocks
|
|
360
395
|
let clock = pre.clock;
|
|
361
|
-
let schedulerTimer: ReturnType<typeof setInterval> | undefined;
|
|
362
396
|
if (needs.clock) {
|
|
363
397
|
const bound = await clockBind!.bindClock(options, env, now, clock);
|
|
364
398
|
clock = bound.clock;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 4b. Journal — durable-run store (shared + leased when a driver is bound).
|
|
402
|
+
let journal = pre.journal;
|
|
403
|
+
if (needs.journal && !journal) {
|
|
404
|
+
journal = (await journalBind!.bindJournal(options, env)).journal;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 4c. Scheduler — one timer drives clock ticks and durable-run resume.
|
|
408
|
+
let schedulerTimer: ReturnType<typeof setInterval> | undefined;
|
|
409
|
+
const startScheduler = options.startScheduler ?? env !== "test";
|
|
410
|
+
if (startScheduler && (clock !== undefined || journal !== undefined)) {
|
|
411
|
+
const period = options.schedulerIntervalMs ?? 1000;
|
|
412
|
+
const clockRt = clock;
|
|
413
|
+
const durableResume = options.onDurableResume;
|
|
414
|
+
schedulerTimer = setInterval(() => {
|
|
415
|
+
if (clockRt) void clockRt.tick();
|
|
416
|
+
if (journal && durableResume) void durableResume();
|
|
417
|
+
}, period);
|
|
418
|
+
schedulerTimer.unref?.();
|
|
374
419
|
}
|
|
375
420
|
|
|
376
421
|
// Gate (before AI)
|
|
377
422
|
let gate = pre.gate;
|
|
378
423
|
if (needs.gate && !gate) {
|
|
379
|
-
gate = await gateBind!.bindGate(options, now);
|
|
424
|
+
gate = await gateBind!.bindGate(options, now, env, docker);
|
|
380
425
|
}
|
|
381
426
|
|
|
382
427
|
// 5. Channel
|
|
@@ -419,6 +464,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
419
464
|
channel,
|
|
420
465
|
ai,
|
|
421
466
|
runs,
|
|
467
|
+
journal,
|
|
422
468
|
capabilities,
|
|
423
469
|
stopScheduler() {
|
|
424
470
|
if (schedulerTimer !== undefined) {
|
|
@@ -434,6 +480,8 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
434
480
|
await signal?.close();
|
|
435
481
|
await vault?.close();
|
|
436
482
|
await runs?.flush();
|
|
483
|
+
const journalStore = journal?.store as { close?: () => Promise<void> } | undefined;
|
|
484
|
+
await journalStore?.close?.();
|
|
437
485
|
},
|
|
438
486
|
};
|
|
439
487
|
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
linkAbort,
|
|
15
15
|
withAbortSignal,
|
|
16
16
|
} from "./abort-scope.ts";
|
|
17
|
-
import { isJournalSuspend } from "./journal.ts";
|
|
17
|
+
import { isJournalSuspend } from "./journal-suspend.ts";
|
|
18
18
|
|
|
19
19
|
/** A unit of work started under an abort scope. */
|
|
20
20
|
export type FxThunk<T> = () => T | Promise<T>;
|
package/src/kernel/errors.ts
CHANGED
|
@@ -143,14 +143,20 @@ export const OKE_ERRORS = {
|
|
|
143
143
|
fix: 'Add "{resource}" to this flow\'s effects.calls.',
|
|
144
144
|
},
|
|
145
145
|
/**
|
|
146
|
-
* Emit target has no subscriber (unified-theory §21
|
|
147
|
-
*
|
|
146
|
+
* Emit target has no subscriber (unified-theory §21).
|
|
147
|
+
* Thrown at emit when `optional` is false and nobody is subscribed.
|
|
148
148
|
*/
|
|
149
149
|
ORPHAN_EMIT: {
|
|
150
150
|
code: 1042,
|
|
151
151
|
cause: 'Flow "{flow}" emits signal "{resource}" with no subscriber.',
|
|
152
152
|
fix: "Add `on({resource}, …)` or mark the signal `{ optional: true }`.",
|
|
153
153
|
},
|
|
154
|
+
/** Emit payload failed the signal's declared Standard Schema. */
|
|
155
|
+
SIGNAL_SCHEMA: {
|
|
156
|
+
code: 1043,
|
|
157
|
+
cause: '"{resource}": {detail}',
|
|
158
|
+
fix: "Fix schema payload.",
|
|
159
|
+
},
|
|
154
160
|
/**
|
|
155
161
|
* Domain table/column missing under docker/prod (migrations not applied).
|
|
156
162
|
* Store/DDL band starts at 1100.
|
package/src/kernel/fx.test.ts
CHANGED
|
@@ -141,6 +141,12 @@ describe("fx — wholesale swap", () => {
|
|
|
141
141
|
async send() {
|
|
142
142
|
return { ok: true };
|
|
143
143
|
},
|
|
144
|
+
async sendOtp() {
|
|
145
|
+
return { ok: true };
|
|
146
|
+
},
|
|
147
|
+
async verifyOtp() {
|
|
148
|
+
return { ok: true };
|
|
149
|
+
},
|
|
144
150
|
async ask() {
|
|
145
151
|
return {};
|
|
146
152
|
},
|
|
@@ -258,6 +264,19 @@ describe("errors — registry", () => {
|
|
|
258
264
|
expect(err.message).toContain("→");
|
|
259
265
|
});
|
|
260
266
|
|
|
267
|
+
test("OKE1043 is reserved for signal schema emit failures", () => {
|
|
268
|
+
const def = OKE_ERRORS.SIGNAL_SCHEMA;
|
|
269
|
+
expect(def.code).toBe(1043);
|
|
270
|
+
expect(lookupOkeError(1043)).toEqual(def);
|
|
271
|
+
const err = new OkeError(def, {
|
|
272
|
+
resource: "order-placed",
|
|
273
|
+
detail: "total: Expected number, received string",
|
|
274
|
+
});
|
|
275
|
+
expect(err.message).toContain("OKE1043");
|
|
276
|
+
expect(err.message).toContain("order-placed");
|
|
277
|
+
expect(err.message).toContain("https://oke.omqkhafi.dev/e/1043");
|
|
278
|
+
});
|
|
279
|
+
|
|
261
280
|
test("fx.fail returns a value, not an exception", () => {
|
|
262
281
|
const fx = createFx({ flow: "x", effects: {} });
|
|
263
282
|
const result = fx.fail(
|
package/src/kernel/fx.ts
CHANGED
|
@@ -11,7 +11,14 @@
|
|
|
11
11
|
|
|
12
12
|
import type { Effects, ResourceRef } from "../manifest/types.ts";
|
|
13
13
|
import type {
|
|
14
|
+
FilesStoreDecl,
|
|
15
|
+
FilesStoreFxHandle,
|
|
16
|
+
IndexStoreDecl,
|
|
17
|
+
IndexStoreFxHandle,
|
|
18
|
+
KvStoreDecl,
|
|
19
|
+
KvStoreFxHandle,
|
|
14
20
|
SelectOrderBuilder,
|
|
21
|
+
SqlStoreDecl,
|
|
15
22
|
StoreDecl,
|
|
16
23
|
StoreHandle,
|
|
17
24
|
StoreRuntime,
|
|
@@ -19,6 +26,7 @@ import type {
|
|
|
19
26
|
} from "../elements/store.ts";
|
|
20
27
|
import type { SqlRow } from "../drivers/types.ts";
|
|
21
28
|
import type { SignalRuntime } from "../elements/signal.ts";
|
|
29
|
+
import type { SignalEmitOptions } from "../drivers/signal-types.ts";
|
|
22
30
|
import type { VaultRuntime } from "../elements/vault.ts";
|
|
23
31
|
import type { ChannelRuntime } from "../elements/channel.ts";
|
|
24
32
|
import type { AiRuntime } from "../elements/ai.ts";
|
|
@@ -227,6 +235,36 @@ export interface FxSendOptions {
|
|
|
227
235
|
readonly acceptLanguage?: string;
|
|
228
236
|
}
|
|
229
237
|
|
|
238
|
+
/** Options for {@link Fx.sendOtp} (provider-managed SMS OTP). */
|
|
239
|
+
export interface FxSendOtpOptions {
|
|
240
|
+
/** Recipient phone number (E.164). */
|
|
241
|
+
readonly to: string;
|
|
242
|
+
/** Unique id for this verification flow (required again on verify). */
|
|
243
|
+
readonly requestId: string;
|
|
244
|
+
/** Message language (`en` or `ar`). */
|
|
245
|
+
readonly lang?: "en" | "ar";
|
|
246
|
+
/** Optional note appended to the OTP SMS. */
|
|
247
|
+
readonly note?: string;
|
|
248
|
+
/** Sender id override. */
|
|
249
|
+
readonly from?: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Options for {@link Fx.verifyOtp}. */
|
|
253
|
+
export interface FxVerifyOtpOptions {
|
|
254
|
+
/** Recipient phone number (same as send). */
|
|
255
|
+
readonly to: string;
|
|
256
|
+
/** Same {@link FxSendOtpOptions.requestId} used when sending. */
|
|
257
|
+
readonly requestId: string;
|
|
258
|
+
/** OTP code the user entered. */
|
|
259
|
+
readonly code: string;
|
|
260
|
+
/** Message language (`en` or `ar`). */
|
|
261
|
+
readonly lang?: "en" | "ar";
|
|
262
|
+
/** Sender id override. */
|
|
263
|
+
readonly from?: string;
|
|
264
|
+
/** Optional note. */
|
|
265
|
+
readonly note?: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
230
268
|
/** Options for {@link Fx.ask}. */
|
|
231
269
|
export interface FxAskOptions {
|
|
232
270
|
readonly via?: readonly NamedRef[];
|
|
@@ -285,18 +323,24 @@ export interface Fx {
|
|
|
285
323
|
* Open a store handle for `ref` (capability checked on each op).
|
|
286
324
|
*
|
|
287
325
|
* When a {@link CreateFxOptions.storeRuntime} is bound and `ref` is a
|
|
288
|
-
*
|
|
326
|
+
* facet declaration, returns the driver-backed handle for that facet.
|
|
327
|
+
* String / `{ ref }` forms return the in-memory stub (tests).
|
|
289
328
|
*
|
|
290
329
|
* @param ref - Store resource ref, named handle, or store declaration
|
|
291
330
|
*/
|
|
331
|
+
store(ref: SqlStoreDecl): SqlStoreHandle;
|
|
332
|
+
store(ref: KvStoreDecl): KvStoreFxHandle;
|
|
333
|
+
store(ref: FilesStoreDecl): FilesStoreFxHandle;
|
|
334
|
+
store(ref: IndexStoreDecl): IndexStoreFxHandle;
|
|
292
335
|
store(ref: NamedRef | { readonly ref: ResourceRef } | StoreDecl): FxStoreHandle;
|
|
293
336
|
/**
|
|
294
337
|
* Emit a signal (records `emit`).
|
|
295
338
|
*
|
|
296
339
|
* @param signal - Signal name or handle
|
|
297
340
|
* @param payload - Payload
|
|
341
|
+
* @param options - Optional emit options (`key` for per-key once ordering)
|
|
298
342
|
*/
|
|
299
|
-
emit(signal: NamedRef, payload?: unknown): Promise<void>;
|
|
343
|
+
emit(signal: NamedRef, payload?: unknown, options?: SignalEmitOptions): Promise<void>;
|
|
300
344
|
/**
|
|
301
345
|
* Call another flow (records `call`). Stub returns `undefined`.
|
|
302
346
|
*
|
|
@@ -325,6 +369,22 @@ export interface Fx {
|
|
|
325
369
|
* @param opts - Recipient / data
|
|
326
370
|
*/
|
|
327
371
|
send(template: NamedRef, opts?: FxSendOptions): Promise<{ ok: true }>;
|
|
372
|
+
/**
|
|
373
|
+
* Send a provider-managed SMS OTP (records `send` on `sms-otp`).
|
|
374
|
+
*
|
|
375
|
+
* Vendor extra (Taqnyat Verify API) — requires a bound SMS driver that
|
|
376
|
+
* supports provider-managed OTP. Dry-run records would-have-fired without
|
|
377
|
+
* contacting the provider.
|
|
378
|
+
*
|
|
379
|
+
* @param opts - Recipient + requestId (+ lang / note / from)
|
|
380
|
+
*/
|
|
381
|
+
sendOtp(opts: FxSendOtpOptions): Promise<{ ok: true }>;
|
|
382
|
+
/**
|
|
383
|
+
* Verify a provider-managed SMS OTP code (records `send` on `sms-otp`).
|
|
384
|
+
*
|
|
385
|
+
* @param opts - Recipient + requestId + code (+ lang / note / from)
|
|
386
|
+
*/
|
|
387
|
+
verifyOtp(opts: FxVerifyOtpOptions): Promise<{ ok: true }>;
|
|
328
388
|
/**
|
|
329
389
|
* Ask an AI prompt (records `ask`). Stub returns `{}`.
|
|
330
390
|
*
|
|
@@ -835,6 +895,13 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
835
895
|
return h.exists(table, idOrWhere);
|
|
836
896
|
});
|
|
837
897
|
},
|
|
898
|
+
upsert(table, matchOn, values, upsertOptions) {
|
|
899
|
+
return gated("write", ref, async () => {
|
|
900
|
+
refuseDryRunWrite();
|
|
901
|
+
const h = await ensure();
|
|
902
|
+
return h.upsert(table, matchOn, values, upsertOptions);
|
|
903
|
+
});
|
|
904
|
+
},
|
|
838
905
|
increment(table, id, column, by) {
|
|
839
906
|
return gated("write", ref, async () => {
|
|
840
907
|
refuseDryRunWrite();
|
|
@@ -870,10 +937,25 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
870
937
|
} as SqlStoreHandle;
|
|
871
938
|
}
|
|
872
939
|
|
|
940
|
+
function storeHandle(ref: SqlStoreDecl): SqlStoreHandle;
|
|
941
|
+
function storeHandle(ref: KvStoreDecl): KvStoreFxHandle;
|
|
942
|
+
function storeHandle(ref: FilesStoreDecl): FilesStoreFxHandle;
|
|
943
|
+
function storeHandle(ref: IndexStoreDecl): IndexStoreFxHandle;
|
|
944
|
+
function storeHandle(ref: NamedRef | { readonly ref: ResourceRef } | StoreDecl): FxStoreHandle;
|
|
873
945
|
function storeHandle(ref: NamedRef | { readonly ref: ResourceRef } | StoreDecl): FxStoreHandle {
|
|
874
946
|
const runtime = options.storeRuntime;
|
|
875
|
-
if (
|
|
947
|
+
if (typeof ref === "object" && ref !== null && "facet" in ref) {
|
|
876
948
|
const decl = ref;
|
|
949
|
+
// SQL physics cannot run on the in-memory stub (insert(table).values ≠ stub insert(row)).
|
|
950
|
+
// Without a runtime, fail loudly — never return a stub missing upsert/select/….
|
|
951
|
+
if (!runtime) {
|
|
952
|
+
if (decl.facet === "sql") {
|
|
953
|
+
throw new Error(
|
|
954
|
+
`fx.store("${decl.ref}"): no store runtime — boot the app (stores / flow effects) before using SQL handles`,
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
return stubStoreHandle(decl.ref);
|
|
958
|
+
}
|
|
877
959
|
const cache: { handle?: StoreHandle } = {};
|
|
878
960
|
const open = async () => {
|
|
879
961
|
if (!cache.handle) {
|
|
@@ -892,7 +974,28 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
892
974
|
});
|
|
893
975
|
}
|
|
894
976
|
|
|
895
|
-
|
|
977
|
+
if (decl.facet === "files") {
|
|
978
|
+
const ref = decl.ref;
|
|
979
|
+
return runtime.openFilesFx(
|
|
980
|
+
decl as Extract<StoreDecl, { facet: "files" }>,
|
|
981
|
+
{
|
|
982
|
+
effects: options.effects ?? {},
|
|
983
|
+
revealPii: options.revealPii,
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
gate: gated,
|
|
987
|
+
refuseDryRunWrite: () => {
|
|
988
|
+
if (isDryRun()) {
|
|
989
|
+
throw new DryRunWriteIsolationError(
|
|
990
|
+
`Driver-backed store "${ref}" cannot isolate writes during dry-run; dry-run refused rather than risk a double-write.`,
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
},
|
|
994
|
+
},
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// KV / index — thin gated wrappers preserving driver methods.
|
|
896
999
|
const baseRef = decl.ref;
|
|
897
1000
|
return new Proxy({} as StoreHandle, {
|
|
898
1001
|
get(_t, prop) {
|
|
@@ -1007,14 +1110,12 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1007
1110
|
};
|
|
1008
1111
|
|
|
1009
1112
|
const fx: Fx = {
|
|
1010
|
-
store
|
|
1011
|
-
|
|
1012
|
-
},
|
|
1013
|
-
emit(signal, payload) {
|
|
1113
|
+
store: storeHandle,
|
|
1114
|
+
emit(signal, payload, emitOptions) {
|
|
1014
1115
|
const name = resolveName(signal);
|
|
1015
1116
|
return gated("emit", name, async () => {
|
|
1016
1117
|
if (options.signalRuntime) {
|
|
1017
|
-
await options.signalRuntime.emit(name, payload);
|
|
1118
|
+
await options.signalRuntime.emit(name, payload, emitOptions);
|
|
1018
1119
|
}
|
|
1019
1120
|
});
|
|
1020
1121
|
},
|
|
@@ -1069,6 +1170,49 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1069
1170
|
return { ok: true as const };
|
|
1070
1171
|
});
|
|
1071
1172
|
},
|
|
1173
|
+
sendOtp(opts) {
|
|
1174
|
+
return gated("send", "sms-otp", async () => {
|
|
1175
|
+
if (isDryRun()) {
|
|
1176
|
+
recordWouldHaveFired("send", "sms-otp");
|
|
1177
|
+
return { ok: true as const };
|
|
1178
|
+
}
|
|
1179
|
+
if (!options.channelRuntime) {
|
|
1180
|
+
throw new Error(
|
|
1181
|
+
"fx.sendOtp needs a bound Channel — declare channel and set drivers.channel.sms (e.g. taqnyat)",
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
await options.channelRuntime.sendOtp({
|
|
1185
|
+
to: opts.to,
|
|
1186
|
+
requestId: opts.requestId,
|
|
1187
|
+
...(opts.lang ? { lang: opts.lang } : {}),
|
|
1188
|
+
...(opts.note ? { note: opts.note } : {}),
|
|
1189
|
+
...(opts.from ? { from: opts.from } : {}),
|
|
1190
|
+
});
|
|
1191
|
+
return { ok: true as const };
|
|
1192
|
+
});
|
|
1193
|
+
},
|
|
1194
|
+
verifyOtp(opts) {
|
|
1195
|
+
return gated("send", "sms-otp", async () => {
|
|
1196
|
+
if (isDryRun()) {
|
|
1197
|
+
recordWouldHaveFired("send", "sms-otp");
|
|
1198
|
+
return { ok: true as const };
|
|
1199
|
+
}
|
|
1200
|
+
if (!options.channelRuntime) {
|
|
1201
|
+
throw new Error(
|
|
1202
|
+
"fx.verifyOtp needs a bound Channel — declare channel and set drivers.channel.sms (e.g. taqnyat)",
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
await options.channelRuntime.verifyOtp({
|
|
1206
|
+
to: opts.to,
|
|
1207
|
+
requestId: opts.requestId,
|
|
1208
|
+
code: opts.code,
|
|
1209
|
+
...(opts.lang ? { lang: opts.lang } : {}),
|
|
1210
|
+
...(opts.from ? { from: opts.from } : {}),
|
|
1211
|
+
...(opts.note ? { note: opts.note } : {}),
|
|
1212
|
+
});
|
|
1213
|
+
return { ok: true as const };
|
|
1214
|
+
});
|
|
1215
|
+
},
|
|
1072
1216
|
ask(prompt, input, opts) {
|
|
1073
1217
|
const name = resolveName(prompt);
|
|
1074
1218
|
return gated("ask", name, async () => {
|
package/src/kernel/index.ts
CHANGED
|
@@ -167,10 +167,16 @@ export {
|
|
|
167
167
|
createJournal,
|
|
168
168
|
createMemoryJournalStore,
|
|
169
169
|
createFileJournalStore,
|
|
170
|
+
hasJournalLease,
|
|
171
|
+
isJournalLeaseBusy,
|
|
170
172
|
isJournalSuspend,
|
|
173
|
+
JournalLeaseBusy,
|
|
171
174
|
JournalSuspend,
|
|
175
|
+
JOURNAL_DEFAULT_LEASE_MS,
|
|
172
176
|
type Journal,
|
|
173
177
|
type JournalEntry,
|
|
178
|
+
type JournalLeaseOptions,
|
|
179
|
+
type JournalLeaseStore,
|
|
174
180
|
type JournalRun,
|
|
175
181
|
type JournalRunStatus,
|
|
176
182
|
type JournalSession,
|