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
|
@@ -56,6 +56,7 @@ describe("SqlStoreHandle — no relational query surface (path b)", () => {
|
|
|
56
56
|
"findById",
|
|
57
57
|
"delete",
|
|
58
58
|
"exists",
|
|
59
|
+
"upsert",
|
|
59
60
|
"increment",
|
|
60
61
|
"raw",
|
|
61
62
|
"count",
|
|
@@ -133,3 +134,41 @@ describe("SqlStoreHandle — orderBy / limit select chain", () => {
|
|
|
133
134
|
await conn.close();
|
|
134
135
|
});
|
|
135
136
|
});
|
|
137
|
+
|
|
138
|
+
describe("SqlStoreHandle — upsert", () => {
|
|
139
|
+
test("default inserts once then already-existed without touching the row", async () => {
|
|
140
|
+
const { handle, conn } = await openHandle();
|
|
141
|
+
const first = await handle.upsert(
|
|
142
|
+
posts,
|
|
143
|
+
{ id: "welcome" },
|
|
144
|
+
{ id: "welcome", title: "Hello", createdAt: 1 },
|
|
145
|
+
);
|
|
146
|
+
expect(first.status).toBe("upserted");
|
|
147
|
+
|
|
148
|
+
const second = await handle.upsert(
|
|
149
|
+
posts,
|
|
150
|
+
{ id: "welcome" },
|
|
151
|
+
{ id: "welcome", title: "Changed", createdAt: 2 },
|
|
152
|
+
);
|
|
153
|
+
expect(second.status).toBe("already-existed");
|
|
154
|
+
|
|
155
|
+
const row = await handle.findById(posts, "welcome");
|
|
156
|
+
expect(row).toEqual({ id: "welcome", title: "Hello", createdAt: 1 });
|
|
157
|
+
await conn.close();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("onExisting update changes matched columns", async () => {
|
|
161
|
+
const { handle, conn } = await openHandle();
|
|
162
|
+
await handle.upsert(posts, { id: "n1" }, { id: "n1", title: "one", createdAt: 10 });
|
|
163
|
+
const updated = await handle.upsert(
|
|
164
|
+
posts,
|
|
165
|
+
{ id: "n1" },
|
|
166
|
+
{ id: "n1", title: "two", createdAt: 20 },
|
|
167
|
+
{ onExisting: "update" },
|
|
168
|
+
);
|
|
169
|
+
expect(updated.status).toBe("changed");
|
|
170
|
+
const row = await handle.findById(posts, "n1");
|
|
171
|
+
expect(row).toEqual({ id: "n1", title: "two", createdAt: 20 });
|
|
172
|
+
await conn.close();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -237,6 +237,21 @@ export interface SqlStoreHandle {
|
|
|
237
237
|
* @param idOrWhere - Primary key string, or column equality map
|
|
238
238
|
*/
|
|
239
239
|
exists(table: TableHandle | unknown, idOrWhere: string | WhereMap): Promise<boolean>;
|
|
240
|
+
/**
|
|
241
|
+
* Insert when no row matches `matchOn`; never touches an existing match
|
|
242
|
+
* unless `options.onExisting` is `"update"`.
|
|
243
|
+
*
|
|
244
|
+
* @param table - Table
|
|
245
|
+
* @param matchOn - Equality map or Drizzle condition identifying the row
|
|
246
|
+
* @param values - Row values for insert (and optional update)
|
|
247
|
+
* @param options - Per-call opt-in to update an existing match
|
|
248
|
+
*/
|
|
249
|
+
upsert(
|
|
250
|
+
table: TableHandle | unknown,
|
|
251
|
+
matchOn: WhereMap | unknown,
|
|
252
|
+
values: SqlRow,
|
|
253
|
+
options?: { readonly onExisting?: "update" },
|
|
254
|
+
): Promise<UpsertResult>;
|
|
240
255
|
/**
|
|
241
256
|
* Atomically add `by` to `column` on the PK row.
|
|
242
257
|
*
|
|
@@ -276,6 +291,14 @@ export interface SqlStoreHandle {
|
|
|
276
291
|
ensureTable(table: TableHandle): Promise<void>;
|
|
277
292
|
}
|
|
278
293
|
|
|
294
|
+
/** Outcome of {@link SqlStoreHandle.upsert}. */
|
|
295
|
+
export type UpsertStatus = "upserted" | "changed" | "already-existed";
|
|
296
|
+
|
|
297
|
+
/** Result envelope for {@link SqlStoreHandle.upsert}. */
|
|
298
|
+
export interface UpsertResult {
|
|
299
|
+
readonly status: UpsertStatus;
|
|
300
|
+
}
|
|
301
|
+
|
|
279
302
|
/** Options for {@link SqlStoreHandle.page}. */
|
|
280
303
|
export interface SqlPageOptions {
|
|
281
304
|
/** Filter condition (equality map or Drizzle SQL). */
|
|
@@ -617,6 +640,38 @@ export function createSqlStoreHandle(
|
|
|
617
640
|
return rows.length > 0;
|
|
618
641
|
},
|
|
619
642
|
|
|
643
|
+
async upsert(table, matchOn, values, upsertOptions) {
|
|
644
|
+
await ensureFromMeta(table);
|
|
645
|
+
const name = resolveTableName(table);
|
|
646
|
+
const compiled = compileTableWhere(table, matchOn);
|
|
647
|
+
if (!compiled.clause) {
|
|
648
|
+
throw new Error("upsert() requires at least one matchOn predicate");
|
|
649
|
+
}
|
|
650
|
+
const found = await query(
|
|
651
|
+
`SELECT 1 AS "ok" FROM ${quoteIdent(name)} WHERE ${compiled.clause} LIMIT 1`,
|
|
652
|
+
compiled.params,
|
|
653
|
+
);
|
|
654
|
+
if (found.length === 0) {
|
|
655
|
+
const prepared = prepareInsertRow(table, values);
|
|
656
|
+
const cols = Object.keys(prepared);
|
|
657
|
+
const placeholders = cols.map(() => "?").join(", ");
|
|
658
|
+
const colList = cols.map(quoteIdent).join(", ");
|
|
659
|
+
const params = cols.map((c) => prepared[c]);
|
|
660
|
+
await exec(`INSERT INTO ${quoteIdent(name)} (${colList}) VALUES (${placeholders})`, params);
|
|
661
|
+
return { status: "upserted" as const };
|
|
662
|
+
}
|
|
663
|
+
if (upsertOptions?.onExisting !== "update") {
|
|
664
|
+
return { status: "already-existed" as const };
|
|
665
|
+
}
|
|
666
|
+
const prepared = prepareUpdateRow(table, values);
|
|
667
|
+
const setEntries = Object.entries(prepared);
|
|
668
|
+
if (setEntries.length === 0) return { status: "changed" as const };
|
|
669
|
+
const setSql = setEntries.map(([col]) => `${quoteIdent(col)} = ?`).join(", ");
|
|
670
|
+
const params = [...setEntries.map(([, v]) => v), ...compiled.params];
|
|
671
|
+
await exec(`UPDATE ${quoteIdent(name)} SET ${setSql} WHERE ${compiled.clause}`, params);
|
|
672
|
+
return { status: "changed" as const };
|
|
673
|
+
},
|
|
674
|
+
|
|
620
675
|
async increment(table, idValue, column, by = 1) {
|
|
621
676
|
await ensureFromMeta(table);
|
|
622
677
|
const name = resolveTableName(table);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real app-boot path for fx.store(db).upsert — must work through
|
|
3
|
+
* oke() → createTestApp → app.fetch(), not only the isolated SqlStoreHandle.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { gate } from "../gate.ts";
|
|
9
|
+
import { oke } from "../../kernel/app.ts";
|
|
10
|
+
import { createFx } from "../../kernel/fx.ts";
|
|
11
|
+
import { flow, resetFlowSeq } from "../../kernel/flow.ts";
|
|
12
|
+
import { on, resetBindings } from "../../kernel/on.ts";
|
|
13
|
+
import { http } from "../../kernel/triggers.ts";
|
|
14
|
+
import { createTestApp } from "../../test/create-test-app.ts";
|
|
15
|
+
import { field, id, now, store } from "../store.ts";
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
resetBindings();
|
|
19
|
+
resetFlowSeq();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const notes = store.schema.table("notes", {
|
|
23
|
+
id: field.text().primaryKey().defaultFn(id),
|
|
24
|
+
title: field.text().notNull(),
|
|
25
|
+
body: field.text().notNull(),
|
|
26
|
+
createdAt: field.integer().notNull().defaultFn(now),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const UpsertIn = z.object({
|
|
30
|
+
id: z.string(),
|
|
31
|
+
title: z.string(),
|
|
32
|
+
body: z.string(),
|
|
33
|
+
onExisting: z.enum(["update"]).optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const UpsertOut = z.object({
|
|
37
|
+
status: z.enum(["upserted", "changed", "already-existed"]),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("fx.store(db).upsert via app.fetch (real boot)", () => {
|
|
41
|
+
test("abstract schema table: upserted → already-existed → changed", async () => {
|
|
42
|
+
resetBindings();
|
|
43
|
+
resetFlowSeq();
|
|
44
|
+
|
|
45
|
+
const db = store.sql("app", { schema: { notes } });
|
|
46
|
+
|
|
47
|
+
const seed = on(
|
|
48
|
+
http.post("/seed").gate(gate.public),
|
|
49
|
+
flow({
|
|
50
|
+
name: "notes.seed",
|
|
51
|
+
in: UpsertIn,
|
|
52
|
+
out: UpsertOut,
|
|
53
|
+
effects: { writes: ["sql:app"], reads: ["sql:app"] },
|
|
54
|
+
do: async (input, fx) =>
|
|
55
|
+
fx
|
|
56
|
+
.store(db)
|
|
57
|
+
.upsert(
|
|
58
|
+
notes,
|
|
59
|
+
{ id: input.id },
|
|
60
|
+
{ id: input.id, title: input.title, body: input.body, createdAt: 1 },
|
|
61
|
+
input.onExisting ? { onExisting: input.onExisting } : undefined,
|
|
62
|
+
),
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const app = oke({
|
|
67
|
+
name: "upsert-app-boot",
|
|
68
|
+
gate: { policies: [gate.public] },
|
|
69
|
+
}).adopt({ seed });
|
|
70
|
+
Object.assign(app.$options, { env: "test", stores: [db], unguardedHttp: "allow" });
|
|
71
|
+
await createTestApp(app);
|
|
72
|
+
|
|
73
|
+
async function post(body: unknown) {
|
|
74
|
+
const res = await app.fetch(
|
|
75
|
+
new Request("http://localhost/seed", {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "content-type": "application/json" },
|
|
78
|
+
body: JSON.stringify(body),
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
expect(res.status).toBe(200);
|
|
82
|
+
const json = (await res.json()) as { data: { status: string } };
|
|
83
|
+
return json.data.status;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
expect(await post({ id: "welcome", title: "Hello", body: "one" })).toBe("upserted");
|
|
87
|
+
expect(await post({ id: "welcome", title: "Changed", body: "two" })).toBe("already-existed");
|
|
88
|
+
expect(
|
|
89
|
+
await post({
|
|
90
|
+
id: "welcome",
|
|
91
|
+
title: "Updated",
|
|
92
|
+
body: "three",
|
|
93
|
+
onExisting: "update",
|
|
94
|
+
}),
|
|
95
|
+
).toBe("changed");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("SQL StoreDecl without store runtime fails loudly (not stub upsert-missing)", async () => {
|
|
99
|
+
const db = store.sql("app", { schema: { notes } });
|
|
100
|
+
const fx = createFx({ flow: "orphan" });
|
|
101
|
+
expect(() => fx.store(db)).toThrow(/no store runtime/);
|
|
102
|
+
});
|
|
103
|
+
});
|
package/src/elements/store.ts
CHANGED
|
@@ -91,6 +91,9 @@ export type {
|
|
|
91
91
|
ResolvedListConfig,
|
|
92
92
|
} from "./store/resource.ts";
|
|
93
93
|
|
|
94
|
+
export { defineSeed, normalizeSeedFns, resolveSeedCategory } from "./store/seed.ts";
|
|
95
|
+
export type { SeedCategory, SeedDef, SeedFn, SeedFns } from "./store/seed.ts";
|
|
96
|
+
|
|
94
97
|
export { createSqlStoreHandle, resolvePkColumn } from "./store/sql-session.ts";
|
|
95
98
|
export type {
|
|
96
99
|
SqlStoreHandle,
|
|
@@ -102,6 +105,8 @@ export type {
|
|
|
102
105
|
InsertValuesBuilder,
|
|
103
106
|
SqlSessionOptions,
|
|
104
107
|
SqlPageOptions,
|
|
108
|
+
UpsertResult,
|
|
109
|
+
UpsertStatus,
|
|
105
110
|
WhereMap,
|
|
106
111
|
} from "./store/sql-session.ts";
|
|
107
112
|
|
|
@@ -122,3 +127,27 @@ export type {
|
|
|
122
127
|
|
|
123
128
|
export { fileKeyWarnings, contentAddressedKey, projectFileKeys } from "./store/files-policy.ts";
|
|
124
129
|
export type { FileKeyWarning } from "./store/files-policy.ts";
|
|
130
|
+
|
|
131
|
+
export {
|
|
132
|
+
createFilesImagePipeline,
|
|
133
|
+
putImageToBucket,
|
|
134
|
+
variantObjectKey,
|
|
135
|
+
resolveFilesImageCtorOptions,
|
|
136
|
+
DEFAULT_FILES_IMAGE_MAX_PIXELS,
|
|
137
|
+
} from "./store/files-image.ts";
|
|
138
|
+
export type {
|
|
139
|
+
FilesImageMeta,
|
|
140
|
+
FilesImageOptions,
|
|
141
|
+
FilesImageResizeOptions,
|
|
142
|
+
FilesImageModulateOptions,
|
|
143
|
+
FilesImageJpegOptions,
|
|
144
|
+
FilesImagePngOptions,
|
|
145
|
+
FilesImageWebpOptions,
|
|
146
|
+
FilesImageQualityOptions,
|
|
147
|
+
FilesImageFormat,
|
|
148
|
+
FilesImagePipeline,
|
|
149
|
+
FilesImageBucketAccess,
|
|
150
|
+
ImageVariantSpec,
|
|
151
|
+
PutImageOptions,
|
|
152
|
+
PutImageResult,
|
|
153
|
+
} from "./store/files-image.ts";
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Vault resolution-chain assembly for app boot and Console.
|
|
3
|
+
*
|
|
4
|
+
* Order (first hit wins): process.env → .env.local → compose .env.docker →
|
|
5
|
+
* backend layer (`env` seed / openbao / memory / managed).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import type { ConfigEnv } from "../../config/index.ts";
|
|
11
|
+
import {
|
|
12
|
+
envVaultDriver,
|
|
13
|
+
managedVaultDriver,
|
|
14
|
+
memoryVaultDriver,
|
|
15
|
+
openbaoVaultDriver,
|
|
16
|
+
} from "../../drivers/index.ts";
|
|
17
|
+
import { parseDotenv } from "../../drivers/vault-dotenv-parse.ts";
|
|
18
|
+
import type { VaultDriverId } from "../../drivers/vault-types.ts";
|
|
19
|
+
import { resolveComposeEnvPath } from "./chain.ts";
|
|
20
|
+
import type { VaultChainLayer } from "./runtime.ts";
|
|
21
|
+
|
|
22
|
+
/** Options for {@link buildVaultBootChain}. */
|
|
23
|
+
export interface BuildVaultBootChainOptions {
|
|
24
|
+
/** Project root (defaults to `process.cwd()`). */
|
|
25
|
+
readonly cwd?: string;
|
|
26
|
+
/** Resolved `drivers.vault` id for the active env. */
|
|
27
|
+
readonly driverId: VaultDriverId;
|
|
28
|
+
/** Active config env — controls fail-loud vs soft fallback for openbao. */
|
|
29
|
+
readonly env?: ConfigEnv;
|
|
30
|
+
/** Seed secrets for the terminal memory / managed layer. */
|
|
31
|
+
readonly seed?: Readonly<Record<string, string>>;
|
|
32
|
+
/**
|
|
33
|
+
* When `openbao` is selected but credentials are missing:
|
|
34
|
+
* - `"throw"` — fail loud (default for `prod` / `docker`)
|
|
35
|
+
* - `"memory"` — soft-fallback to a seeded memory bag (Console / local)
|
|
36
|
+
*/
|
|
37
|
+
readonly missingOpenbao?: "throw" | "memory";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Normalize a config label to a {@link VaultDriverId}.
|
|
42
|
+
*
|
|
43
|
+
* Accepts legacy `"dotenv"` as an alias for `"env"`.
|
|
44
|
+
*
|
|
45
|
+
* @param raw - Config string
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeVaultDriverId(raw: string): VaultDriverId {
|
|
48
|
+
if (raw === "dotenv") return "env";
|
|
49
|
+
if (raw === "env" || raw === "openbao" || raw === "memory" || raw === "managed") {
|
|
50
|
+
return raw;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(
|
|
53
|
+
`oke boot: unknown vault driver "${raw}" (expected env · openbao · memory · managed)`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the standard Vault resolution chain for app boot / Console.
|
|
59
|
+
*
|
|
60
|
+
* @param options - Driver id + cwd + seed
|
|
61
|
+
*/
|
|
62
|
+
export function buildVaultBootChain(options: BuildVaultBootChainOptions): VaultChainLayer[] {
|
|
63
|
+
const cwd = options.cwd ?? process.cwd();
|
|
64
|
+
const seed = options.seed ?? {};
|
|
65
|
+
const composeEnv = resolveComposeEnvPath(cwd);
|
|
66
|
+
|
|
67
|
+
let composeText = "";
|
|
68
|
+
if (existsSync(composeEnv.path)) {
|
|
69
|
+
composeText = readFileSync(composeEnv.path, "utf8").toString();
|
|
70
|
+
}
|
|
71
|
+
const envMap = parseDotenv(composeText);
|
|
72
|
+
const openbaoUrl = process.env.OKE_VAULT_URL ?? envMap.get("OKE_VAULT_URL");
|
|
73
|
+
const openbaoToken = process.env.OKE_VAULT_TOKEN ?? envMap.get("OKE_VAULT_TOKEN");
|
|
74
|
+
const openbaoMount = process.env.OKE_VAULT_MOUNT ?? envMap.get("OKE_VAULT_MOUNT") ?? "secret";
|
|
75
|
+
|
|
76
|
+
const envLayers: VaultChainLayer[] = [
|
|
77
|
+
{ driver: envVaultDriver, source: "process.env" },
|
|
78
|
+
{
|
|
79
|
+
driver: envVaultDriver,
|
|
80
|
+
source: ".env.local",
|
|
81
|
+
options: { path: resolve(cwd, ".env.local") },
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
driver: envVaultDriver,
|
|
85
|
+
source: composeEnv.source,
|
|
86
|
+
options: { path: composeEnv.path },
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
switch (options.driverId) {
|
|
91
|
+
case "memory":
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
driver: memoryVaultDriver,
|
|
95
|
+
source: "driver",
|
|
96
|
+
options: { secrets: seed },
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
case "managed":
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
driver: managedVaultDriver,
|
|
103
|
+
source: "driver",
|
|
104
|
+
options: { secrets: seed },
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
case "env":
|
|
108
|
+
return [
|
|
109
|
+
...envLayers,
|
|
110
|
+
{
|
|
111
|
+
driver: memoryVaultDriver,
|
|
112
|
+
source: "driver",
|
|
113
|
+
options: { secrets: seed },
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
case "openbao": {
|
|
117
|
+
if (openbaoUrl && openbaoToken) {
|
|
118
|
+
return [
|
|
119
|
+
...envLayers,
|
|
120
|
+
{
|
|
121
|
+
driver: openbaoVaultDriver,
|
|
122
|
+
source: "driver",
|
|
123
|
+
options: { url: openbaoUrl, token: openbaoToken, mount: openbaoMount },
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
const env = options.env ?? "local";
|
|
128
|
+
const missing =
|
|
129
|
+
options.missingOpenbao ?? (env === "prod" || env === "docker" ? "throw" : "memory");
|
|
130
|
+
if (missing === "throw") {
|
|
131
|
+
throw new Error(
|
|
132
|
+
'oke boot: vault driver "openbao" needs OKE_VAULT_URL and OKE_VAULT_TOKEN ' +
|
|
133
|
+
"(did `oke dev -d` bootstrap OpenBao / write docker/.env.docker?)",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return [
|
|
137
|
+
...envLayers,
|
|
138
|
+
{
|
|
139
|
+
driver: memoryVaultDriver,
|
|
140
|
+
source: "driver",
|
|
141
|
+
options: { secrets: seed },
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
default: {
|
|
146
|
+
const _exhaustive: never = options.driverId;
|
|
147
|
+
throw new Error(`oke boot: unhandled vault driver ${String(_exhaustive)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -87,6 +87,11 @@ export interface CreateVaultRuntimeOptions {
|
|
|
87
87
|
export interface VaultRuntime {
|
|
88
88
|
/** Declared contracts by name. */
|
|
89
89
|
readonly contracts: ReadonlyMap<string, VaultSecretDecl>;
|
|
90
|
+
/**
|
|
91
|
+
* Driver ids of each chain layer in order (`env` · `openbao` · `memory` · `managed`).
|
|
92
|
+
* Available before {@link boot}.
|
|
93
|
+
*/
|
|
94
|
+
readonly chainDriverIds: readonly string[];
|
|
90
95
|
/**
|
|
91
96
|
* Open every chain layer, merge (first wins), validate all contracts,
|
|
92
97
|
* register redaction. Throws {@link VaultBootError} listing every gap.
|
|
@@ -245,8 +250,11 @@ export function createVaultRuntime(options: CreateVaultRuntimeOptions = {}): Vau
|
|
|
245
250
|
booted = true;
|
|
246
251
|
}
|
|
247
252
|
|
|
253
|
+
const chainDriverIds = (options.chain ?? []).map((layer) => layer.driver.id);
|
|
254
|
+
|
|
248
255
|
return {
|
|
249
256
|
contracts,
|
|
257
|
+
chainDriverIds,
|
|
250
258
|
get booted() {
|
|
251
259
|
return booted;
|
|
252
260
|
},
|
package/src/index.ts
CHANGED
|
@@ -65,6 +65,9 @@ export {
|
|
|
65
65
|
now,
|
|
66
66
|
defineTable,
|
|
67
67
|
field,
|
|
68
|
+
defineSeed,
|
|
69
|
+
normalizeSeedFns,
|
|
70
|
+
resolveSeedCategory,
|
|
68
71
|
createStoreRuntime,
|
|
69
72
|
type StoreDecl,
|
|
70
73
|
type StoreRuntime,
|
|
@@ -72,6 +75,12 @@ export {
|
|
|
72
75
|
type TableHandle,
|
|
73
76
|
type SchemaTableDecl,
|
|
74
77
|
type SchemaColumnDecl,
|
|
78
|
+
type SeedDef,
|
|
79
|
+
type SeedFn,
|
|
80
|
+
type SeedFns,
|
|
81
|
+
type SeedCategory,
|
|
82
|
+
type UpsertResult,
|
|
83
|
+
type UpsertStatus,
|
|
75
84
|
} from "./elements/store.ts";
|
|
76
85
|
|
|
77
86
|
export {
|
|
@@ -152,7 +161,13 @@ export {
|
|
|
152
161
|
createJournal,
|
|
153
162
|
createMemoryJournalStore,
|
|
154
163
|
createFileJournalStore,
|
|
164
|
+
hasJournalLease,
|
|
165
|
+
isJournalLeaseBusy,
|
|
166
|
+
JournalLeaseBusy,
|
|
167
|
+
JOURNAL_DEFAULT_LEASE_MS,
|
|
155
168
|
type Journal,
|
|
169
|
+
type JournalLeaseOptions,
|
|
170
|
+
type JournalLeaseStore,
|
|
156
171
|
type JournalStore,
|
|
157
172
|
type JournalRun,
|
|
158
173
|
} from "./kernel/journal.ts";
|