@suluk/platform 0.4.0 → 0.4.2
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/package.json +1 -1
- package/src/plan.ts +22 -12
- package/src/service.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suluk/platform",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "The platform generator (C051): write one `definePlatform` manifest → it plans the shadcn-registry adds, generates the wired Hono entry, and merges each module's provision fragment into a single provision.config. The manifest compiles to a shadcn-add list + a C047 provision.config; the generator runs the adds + `@suluk/provision`. Turns the Suluk backend registry into a one-command platform. CANDIDATE tooling.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
package/src/plan.ts
CHANGED
|
@@ -96,21 +96,21 @@ const secretsOf = (env: EnvVar[]): EnvVar[] => env.filter((e) => e.secret);
|
|
|
96
96
|
* Non-secret config is NOT here — it's in the manifest `vars` → wrangler `[vars]`. Safe to commit (no values). */
|
|
97
97
|
function buildEnvExample(env: EnvVar[]): string {
|
|
98
98
|
const line = (e: EnvVar) => `${e.name}=${e.hint ? ` # ${e.hint}` : ""}`;
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
// .env.example mirrors the COMMITTED .env AFTER provisioning: SULUK_PUBLIC_KEY (plaintext) + every secret EXCEPT the
|
|
100
|
+
// EPHEMERAL master (deleted after minting). Keepers + minted scoped tokens + runtime secrets — all encrypted at rest.
|
|
101
|
+
const localKeepers = secretsOf(env).filter((e) => !e.provisioning && (e.minted || e.surface === "local")); // account-id + minted tokens
|
|
101
102
|
const runtime = runtimeSecretsOf(env);
|
|
102
103
|
return [
|
|
103
|
-
"#
|
|
104
|
-
"#
|
|
105
|
-
"#
|
|
104
|
+
"# .env.example — the keys in the COMMITTED .env AFTER `bun run provision` (values ENCRYPTED with @suluk/env;",
|
|
105
|
+
"# SULUK_PUBLIC_KEY plaintext). The EPHEMERAL CF master token (CLOUDFLARE_API_TOKEN) is supplied in .env.temp and DELETED",
|
|
106
|
+
"# after minting — it is NOT here. Non-secret config lives in platform.config.ts `vars` (→ wrangler.toml [vars]).",
|
|
106
107
|
"",
|
|
107
|
-
"#
|
|
108
|
-
...provisioning.map(line),
|
|
108
|
+
"SULUK_PUBLIC_KEY= # @suluk/env public key (plaintext; can only encrypt)",
|
|
109
109
|
"",
|
|
110
|
-
"#
|
|
111
|
-
...
|
|
110
|
+
"# Provisioning keeper + minted scoped tokens (surface local — never shipped to the Worker; encrypted):",
|
|
111
|
+
...localKeepers.map(line),
|
|
112
112
|
"",
|
|
113
|
-
"#
|
|
113
|
+
"# Runtime secrets (encrypted; reach the Worker via loadEnv / sync-secrets):",
|
|
114
114
|
...runtime.map((e) => (e.required ? line(e) : `# ${line(e)}`)),
|
|
115
115
|
"",
|
|
116
116
|
].join("\n");
|
|
@@ -241,8 +241,9 @@ function buildEnvTemp(env: EnvVar[]): string {
|
|
|
241
241
|
"# Provisioning creds (used to create infra + mint scoped tokens; the master is DELETED, never committed):",
|
|
242
242
|
...provisioningOf(env).filter((e) => !e.minted).map(line),
|
|
243
243
|
"",
|
|
244
|
-
"# Runtime secrets (encrypted into .env + committed; shipped to the Worker)
|
|
245
|
-
|
|
244
|
+
"# Runtime secrets (encrypted into .env + committed; shipped to the Worker). AUTO-GENERATED ones are NOT here — `provision`",
|
|
245
|
+
"# creates them: " + (runtimeSecretsOf(env).filter((e) => e.generated).map((e) => e.name).join(", ") || "(none)") + ".",
|
|
246
|
+
...runtimeSecretsOf(env).filter((e) => !e.generated).map(line),
|
|
246
247
|
"",
|
|
247
248
|
].join("\n");
|
|
248
249
|
}
|
|
@@ -303,13 +304,16 @@ console.log("✓ scoped tokens ready (encrypted in .env).");
|
|
|
303
304
|
*/
|
|
304
305
|
function buildProvisionScript(env: EnvVar[]): string {
|
|
305
306
|
const ephemeral = ephemeralOf(env).map((e) => e.name);
|
|
307
|
+
const generated = env.filter((e) => e.generated).map((e) => e.name);
|
|
306
308
|
return `#!/usr/bin/env bun
|
|
307
309
|
// AUTO-GENERATED by @suluk/platform — stand up the infra + SEAL the secrets (@suluk/env encrypted-commit model). Run once
|
|
308
310
|
// after filling .env.temp (or with an existing encrypted .env). Idempotent.
|
|
309
311
|
import { existsSync, rmSync, readFileSync, writeFileSync } from "node:fs";
|
|
312
|
+
import { randomBytes } from "node:crypto";
|
|
310
313
|
import { loadEnvFile, setVar } from "@suluk/env/node";
|
|
311
314
|
|
|
312
315
|
const EPHEMERAL = ${JSON.stringify(ephemeral)}; // the CF master token(s): used to provision + mint, then DELETED (never committed)
|
|
316
|
+
const GENERATED = ${JSON.stringify(generated)}; // secrets the app creates itself (e.g. BETTER_AUTH_SECRET) — never operator-supplied
|
|
313
317
|
const sh = async (cmd: string, args: string[]) => { const p = Bun.spawn([cmd, ...args], { stdout: "inherit", stderr: "inherit" }); if ((await p.exited) !== 0) { console.error(\`✗ \${cmd} \${args.join(" ")}\`); process.exit(1); } };
|
|
314
318
|
|
|
315
319
|
// 1. keypair → the central ~/.suluk/settings.json (the private key never stays in the repo).
|
|
@@ -327,6 +331,12 @@ if (existsSync(".env.temp")) {
|
|
|
327
331
|
await loadEnvFile({ override: true }); // decrypt everything into process.env
|
|
328
332
|
if (!process.env.CLOUDFLARE_API_TOKEN || !process.env.CLOUDFLARE_ACCOUNT_ID) { console.error("✗ CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID missing — put them in .env.temp"); process.exit(1); }
|
|
329
333
|
|
|
334
|
+
// 2b. auto-generate any app-created secret (e.g. BETTER_AUTH_SECRET ← 32 random bytes) not already set — the operator never
|
|
335
|
+
// supplies these in .env.temp. Staged plaintext here, encrypted at step 5.
|
|
336
|
+
for (const name of GENERATED) {
|
|
337
|
+
if (!process.env[name]) { await setVar(name, randomBytes(32).toString("base64"), { plain: true }); console.log(\`✓ generated \${name}\`); }
|
|
338
|
+
}
|
|
339
|
+
|
|
330
340
|
// 3. provision the infra (D1/KV — the C047 provision.config), 4. mint the scoped least-privilege tokens from the master.
|
|
331
341
|
await sh("bunx", ["suluk-provision", "apply"]);
|
|
332
342
|
await sh("bun", ["run", "scripts/mint-tokens.ts"]);
|
package/src/service.ts
CHANGED
|
@@ -40,6 +40,9 @@ export interface EnvVar {
|
|
|
40
40
|
provisioning?: boolean;
|
|
41
41
|
/** a scoped least-privilege token MINTED during provisioning (from the master), then kept ENCRYPTED in `.env`. `surface: "local"`. */
|
|
42
42
|
minted?: boolean;
|
|
43
|
+
/** a random secret the provisioning flow AUTO-GENERATES (e.g. `BETTER_AUTH_SECRET` ← 32 random bytes) if not already set —
|
|
44
|
+
* so the operator never supplies it in `.env.temp`; it still lands ENCRYPTED in the committed `.env`. */
|
|
45
|
+
generated?: boolean;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
/** The old catalog record — now a DERIVED VIEW of a {@link Service} (see {@link toCatalogEntry}); kept so `planPlatform`
|
|
@@ -191,7 +194,7 @@ export const authService = defineService({
|
|
|
191
194
|
provision: { symbol: "authProvision", from: "./src/provision/auth" },
|
|
192
195
|
deps: ["better-auth", "@better-auth/api-key", "@better-auth/passkey", "@suluk/better-auth"],
|
|
193
196
|
env: [
|
|
194
|
-
{ name: "BETTER_AUTH_SECRET", required: true, secret: true, hint: "session-signing key — `
|
|
197
|
+
{ name: "BETTER_AUTH_SECRET", required: true, secret: true, generated: true, hint: "session-signing key — AUTO-GENERATED by `bun run provision` (32 random bytes); no need to supply it" },
|
|
195
198
|
{ name: "BETTER_AUTH_URL", hint: "your deployed origin, e.g. https://api.example.com" },
|
|
196
199
|
{ name: "GOOGLE_CLIENT_ID", secret: true, hint: "optional — enables Google sign-in" },
|
|
197
200
|
{ name: "GOOGLE_CLIENT_SECRET", secret: true, hint: "optional — pairs with GOOGLE_CLIENT_ID" },
|