mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
improved module manager's secrets extraction, updated plugins
This commit is contained in:
@@ -50,16 +50,22 @@ export function withPlatformProxy<Env extends CloudflareEnv>(
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
app: async (_env) => {
|
app: async (_env) => {
|
||||||
const env = await getEnv(_env);
|
const env = await getEnv(_env);
|
||||||
|
const binding = use_proxy ? getBinding(env, "D1Database") : undefined;
|
||||||
|
|
||||||
if (config?.app === undefined && use_proxy) {
|
if (config?.app === undefined && use_proxy && binding) {
|
||||||
const binding = getBinding(env, "D1Database");
|
|
||||||
return {
|
return {
|
||||||
connection: d1Sqlite({
|
connection: d1Sqlite({
|
||||||
binding: binding.value,
|
binding: binding.value,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
} else if (typeof config?.app === "function") {
|
} else if (typeof config?.app === "function") {
|
||||||
return config?.app(env);
|
const appConfig = await config?.app(env);
|
||||||
|
if (binding) {
|
||||||
|
appConfig.connection = d1Sqlite({
|
||||||
|
binding: binding.value,
|
||||||
|
}) as any;
|
||||||
|
}
|
||||||
|
return appConfig;
|
||||||
}
|
}
|
||||||
return config?.app || {};
|
return config?.app || {};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export function devFsVitePlugin({
|
|||||||
projectRoot = config.root;
|
projectRoot = config.root;
|
||||||
},
|
},
|
||||||
configureServer(server) {
|
configureServer(server) {
|
||||||
if (!isDev) return;
|
if (!isDev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Intercept stdout to watch for our write requests
|
// Intercept stdout to watch for our write requests
|
||||||
const originalStdoutWrite = process.stdout.write;
|
const originalStdoutWrite = process.stdout.write;
|
||||||
@@ -78,7 +80,10 @@ export function devFsVitePlugin({
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
transform(code, id, options) {
|
transform(code, id, options) {
|
||||||
// Only transform in SSR mode during development
|
// Only transform in SSR mode during development
|
||||||
if (!isDev || !options?.ssr) return;
|
//if (!isDev || !options?.ssr) return;
|
||||||
|
if (!isDev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is the bknd config file
|
// Check if this is the bknd config file
|
||||||
if (id.includes(configFile)) {
|
if (id.includes(configFile)) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { makeAppFromEnv } from "cli/commands/run";
|
|||||||
import { writeFile } from "node:fs/promises";
|
import { writeFile } from "node:fs/promises";
|
||||||
import c from "picocolors";
|
import c from "picocolors";
|
||||||
import { withConfigOptions } from "cli/utils/options";
|
import { withConfigOptions } from "cli/utils/options";
|
||||||
|
import { $console } from "bknd/utils";
|
||||||
|
|
||||||
export const config: CliCommand = (program) => {
|
export const config: CliCommand = (program) => {
|
||||||
withConfigOptions(program.command("config"))
|
withConfigOptions(program.command("config"))
|
||||||
@@ -19,7 +20,14 @@ export const config: CliCommand = (program) => {
|
|||||||
config = getDefaultConfig();
|
config = getDefaultConfig();
|
||||||
} else {
|
} else {
|
||||||
const app = await makeAppFromEnv(options);
|
const app = await makeAppFromEnv(options);
|
||||||
config = app.toJSON(options.secrets);
|
const manager = app.modules;
|
||||||
|
|
||||||
|
if (options.secrets) {
|
||||||
|
$console.warn("Including secrets in output");
|
||||||
|
config = manager.toJSON(true);
|
||||||
|
} else {
|
||||||
|
config = manager.extractSecrets().configs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config = options.pretty ? JSON.stringify(config, null, 2) : JSON.stringify(config);
|
config = options.pretty ? JSON.stringify(config, null, 2) : JSON.stringify(config);
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export { copyAssets } from "./copy-assets";
|
|||||||
export { types } from "./types";
|
export { types } from "./types";
|
||||||
export { mcp } from "./mcp/mcp";
|
export { mcp } from "./mcp/mcp";
|
||||||
export { sync } from "./sync";
|
export { sync } from "./sync";
|
||||||
|
export { secrets } from "./secrets";
|
||||||
|
|||||||
58
app/src/cli/commands/secrets.ts
Normal file
58
app/src/cli/commands/secrets.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import type { CliCommand } from "../types";
|
||||||
|
import { makeAppFromEnv } from "cli/commands/run";
|
||||||
|
import { writeFile } from "node:fs/promises";
|
||||||
|
import c from "picocolors";
|
||||||
|
import { withConfigOptions, type WithConfigOptions } from "cli/utils/options";
|
||||||
|
import { transformObject } from "bknd/utils";
|
||||||
|
import { Option } from "commander";
|
||||||
|
|
||||||
|
export const secrets: CliCommand = (program) => {
|
||||||
|
withConfigOptions(program.command("secrets"))
|
||||||
|
.description("get app secrets")
|
||||||
|
.option("--template", "template output without the actual secrets")
|
||||||
|
.addOption(
|
||||||
|
new Option("--format <format>", "format output").choices(["json", "env"]).default("json"),
|
||||||
|
)
|
||||||
|
.option("--out <file>", "output file")
|
||||||
|
.action(
|
||||||
|
async (
|
||||||
|
options: WithConfigOptions<{ template: string; format: "json" | "env"; out: string }>,
|
||||||
|
) => {
|
||||||
|
const app = await makeAppFromEnv(options);
|
||||||
|
const manager = app.modules;
|
||||||
|
|
||||||
|
let secrets = manager.extractSecrets().secrets;
|
||||||
|
if (options.template) {
|
||||||
|
secrets = transformObject(secrets, () => "");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info("");
|
||||||
|
if (options.out) {
|
||||||
|
if (options.format === "env") {
|
||||||
|
await writeFile(
|
||||||
|
options.out,
|
||||||
|
Object.entries(secrets)
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join("\n"),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await writeFile(options.out, JSON.stringify(secrets, null, 2));
|
||||||
|
}
|
||||||
|
console.info(`Secrets written to ${c.cyan(options.out)}`);
|
||||||
|
} else {
|
||||||
|
if (options.format === "env") {
|
||||||
|
console.info(
|
||||||
|
c.cyan(
|
||||||
|
Object.entries(secrets)
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join("\n"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.info(secrets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.info("");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { objectEach, transformObject, McpServer, type s } from "bknd/utils";
|
import { objectEach, transformObject, McpServer, type s, SecretSchema, setPath } from "bknd/utils";
|
||||||
import { DebugLogger } from "core/utils/DebugLogger";
|
import { DebugLogger } from "core/utils/DebugLogger";
|
||||||
import { Guard } from "auth/authorize/Guard";
|
import { Guard } from "auth/authorize/Guard";
|
||||||
import { env } from "core/env";
|
import { env } from "core/env";
|
||||||
@@ -15,6 +15,7 @@ import { AppData } from "data/AppData";
|
|||||||
import { AppFlows } from "flows/AppFlows";
|
import { AppFlows } from "flows/AppFlows";
|
||||||
import { AppMedia } from "media/AppMedia";
|
import { AppMedia } from "media/AppMedia";
|
||||||
import type { PartialRec } from "core/types";
|
import type { PartialRec } from "core/types";
|
||||||
|
import { mergeWith, pick } from "lodash-es";
|
||||||
|
|
||||||
export type { ModuleBuildContext };
|
export type { ModuleBuildContext };
|
||||||
|
|
||||||
@@ -207,9 +208,43 @@ export class ModuleManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extractSecrets() {
|
||||||
|
const moduleConfigs = structuredClone(this.configs());
|
||||||
|
const secrets = this.options?.secrets || ({} as any);
|
||||||
|
const extractedKeys: string[] = [];
|
||||||
|
|
||||||
|
for (const [key, module] of Object.entries(this.modules)) {
|
||||||
|
const config = moduleConfigs[key];
|
||||||
|
const schema = module.getSchema();
|
||||||
|
|
||||||
|
const extracted = [...schema.walk({ data: config })].filter(
|
||||||
|
(n) => n.schema instanceof SecretSchema,
|
||||||
|
);
|
||||||
|
|
||||||
|
//console.log("extracted", key, extracted, config);
|
||||||
|
for (const n of extracted) {
|
||||||
|
const path = [key, ...n.instancePath].join(".");
|
||||||
|
|
||||||
|
if (typeof n.data === "string" && n.data.length > 0) {
|
||||||
|
extractedKeys.push(path);
|
||||||
|
secrets[path] = n.data;
|
||||||
|
setPath(moduleConfigs, path, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
configs: moduleConfigs,
|
||||||
|
secrets: pick(secrets, extractedKeys),
|
||||||
|
extractedKeys,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
protected async setConfigs(configs: ModuleConfigs): Promise<void> {
|
protected async setConfigs(configs: ModuleConfigs): Promise<void> {
|
||||||
this.logger.log("setting configs");
|
this.logger.log("setting configs");
|
||||||
for await (const [key, config] of Object.entries(configs)) {
|
for await (const [key, config] of Object.entries(configs)) {
|
||||||
|
if (!(key in this.modules)) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// setting "noEmit" to true, to not force listeners to update
|
// setting "noEmit" to true, to not force listeners to update
|
||||||
const result = await this.modules[key].schema().set(config as any, true);
|
const result = await this.modules[key].schema().set(config as any, true);
|
||||||
@@ -226,6 +261,21 @@ export class ModuleManager {
|
|||||||
this.createModules(this.options?.initial ?? {});
|
this.createModules(this.options?.initial ?? {});
|
||||||
await this.buildModules();
|
await this.buildModules();
|
||||||
|
|
||||||
|
// if secrets were provided, extract, merge and build again
|
||||||
|
const provided_secrets = this.options?.secrets ?? {};
|
||||||
|
if (Object.keys(provided_secrets).length > 0) {
|
||||||
|
const { configs, secrets, extractedKeys } = this.extractSecrets();
|
||||||
|
|
||||||
|
for (const key of extractedKeys) {
|
||||||
|
if (key in provided_secrets) {
|
||||||
|
setPath(configs, key, secrets[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.setConfigs(configs);
|
||||||
|
await this.buildModules();
|
||||||
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,34 +165,6 @@ export class DbModuleManager extends ModuleManager {
|
|||||||
return { configs, secrets };
|
return { configs, secrets };
|
||||||
}
|
}
|
||||||
|
|
||||||
extractSecrets() {
|
|
||||||
const moduleConfigs = structuredClone(this.configs());
|
|
||||||
const secrets = this.options?.secrets || ({} as any);
|
|
||||||
|
|
||||||
for (const [key, module] of Object.entries(this.modules)) {
|
|
||||||
const config = moduleConfigs[key];
|
|
||||||
const schema = module.getSchema();
|
|
||||||
|
|
||||||
const extracted = [...schema.walk({ data: config })].filter(
|
|
||||||
(n) => n.schema instanceof SecretSchema,
|
|
||||||
);
|
|
||||||
|
|
||||||
//console.log("extracted", key, extracted, config);
|
|
||||||
for (const n of extracted) {
|
|
||||||
const path = [key, ...n.instancePath].join(".");
|
|
||||||
if (typeof n.data === "string" && n.data.length > 0) {
|
|
||||||
secrets[path] = n.data;
|
|
||||||
setPath(moduleConfigs, path, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
configs: moduleConfigs,
|
|
||||||
secrets,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
this.logger.context("save").log("saving version", this.version());
|
this.logger.context("save").log("saving version", this.version());
|
||||||
const { configs, secrets } = this.extractSecrets();
|
const { configs, secrets } = this.extractSecrets();
|
||||||
@@ -234,7 +206,7 @@ export class DbModuleManager extends ModuleManager {
|
|||||||
updates.push({
|
updates.push({
|
||||||
version: state.configs.version,
|
version: state.configs.version,
|
||||||
type: "secrets",
|
type: "secrets",
|
||||||
json: secrets,
|
json: secrets as any,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await this.mutator().insertMany(updates);
|
await this.mutator().insertMany(updates);
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { App, type AppConfig, type AppPlugin } from "bknd";
|
|||||||
export type SyncConfigOptions = {
|
export type SyncConfigOptions = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
includeSecrets?: boolean;
|
includeSecrets?: boolean;
|
||||||
|
includeFirstBoot?: boolean;
|
||||||
write: (config: AppConfig) => Promise<void>;
|
write: (config: AppConfig) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function syncConfig({
|
export function syncConfig({
|
||||||
enabled = true,
|
enabled = true,
|
||||||
includeSecrets = false,
|
includeSecrets = false,
|
||||||
|
includeFirstBoot = false,
|
||||||
write,
|
write,
|
||||||
}: SyncConfigOptions): AppPlugin {
|
}: SyncConfigOptions): AppPlugin {
|
||||||
let firstBoot = true;
|
let firstBoot = true;
|
||||||
@@ -26,7 +28,7 @@ export function syncConfig({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (firstBoot) {
|
if (firstBoot && includeFirstBoot) {
|
||||||
firstBoot = false;
|
firstBoot = false;
|
||||||
await write?.(app.toJSON(includeSecrets));
|
await write?.(app.toJSON(includeSecrets));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { type App, ModuleManagerEvents, type AppPlugin } from "bknd";
|
import { type App, ModuleManagerEvents, type AppPlugin } from "bknd";
|
||||||
import type { DbModuleManager } from "modules/db/DbModuleManager";
|
|
||||||
|
|
||||||
export type SyncSecretsOptions = {
|
export type SyncSecretsOptions = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
includeFirstBoot?: boolean;
|
||||||
write: (secrets: Record<string, any>) => Promise<void>;
|
write: (secrets: Record<string, any>) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function syncSecrets({ enabled = true, write }: SyncSecretsOptions): AppPlugin {
|
export function syncSecrets({
|
||||||
|
enabled = true,
|
||||||
|
includeFirstBoot = false,
|
||||||
|
write,
|
||||||
|
}: SyncSecretsOptions): AppPlugin {
|
||||||
let firstBoot = true;
|
let firstBoot = true;
|
||||||
return (app: App) => ({
|
return (app: App) => ({
|
||||||
name: "bknd-sync-secrets",
|
name: "bknd-sync-secrets",
|
||||||
onBuilt: async () => {
|
onBuilt: async () => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
const manager = app.modules as DbModuleManager;
|
const manager = app.modules;
|
||||||
|
|
||||||
if (!("extractSecrets" in manager)) {
|
|
||||||
throw new Error("ModuleManager does not support secrets");
|
|
||||||
}
|
|
||||||
|
|
||||||
app.emgr.onEvent(
|
app.emgr.onEvent(
|
||||||
ModuleManagerEvents.ModuleManagerSecretsExtractedEvent,
|
ModuleManagerEvents.ModuleManagerSecretsExtractedEvent,
|
||||||
@@ -28,7 +28,7 @@ export function syncSecrets({ enabled = true, write }: SyncSecretsOptions): AppP
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (firstBoot) {
|
if (firstBoot && includeFirstBoot) {
|
||||||
firstBoot = false;
|
firstBoot = false;
|
||||||
await write?.(manager.extractSecrets().secrets);
|
await write?.(manager.extractSecrets().secrets);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ import { App, type AppPlugin, EntityTypescript } from "bknd";
|
|||||||
|
|
||||||
export type SyncTypesOptions = {
|
export type SyncTypesOptions = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
includeFirstBoot?: boolean;
|
||||||
write: (et: EntityTypescript) => Promise<void>;
|
write: (et: EntityTypescript) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function syncTypes({ enabled = true, write }: SyncTypesOptions): AppPlugin {
|
export function syncTypes({
|
||||||
|
enabled = true,
|
||||||
|
includeFirstBoot = false,
|
||||||
|
write,
|
||||||
|
}: SyncTypesOptions): AppPlugin {
|
||||||
let firstBoot = true;
|
let firstBoot = true;
|
||||||
return (app: App) => ({
|
return (app: App) => ({
|
||||||
name: "bknd-sync-types",
|
name: "bknd-sync-types",
|
||||||
@@ -21,7 +26,7 @@ export function syncTypes({ enabled = true, write }: SyncTypesOptions): AppPlugi
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (firstBoot) {
|
if (firstBoot && includeFirstBoot) {
|
||||||
firstBoot = false;
|
firstBoot = false;
|
||||||
await write?.(new EntityTypescript(app.em));
|
await write?.(new EntityTypescript(app.em));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user