@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suluk/platform",
3
- "version": "0.4.0",
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
- const provisioning = provisioningOf(env).filter((e) => !e.minted); // the master + account id (the raw inputs)
100
- const minted = env.filter((e) => e.minted); // the scoped tokens the mint step creates
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
- "# Secret keys checklist (generated). SETUP: fill `.env.temp` (plaintext) `bun run provision`. It creates the keypair,",
104
- "# provisions infra, mints the scoped tokens, ENCRYPTS the keepers into the COMMITTED `.env` (@suluk/env ML-KEM-768), and",
105
- "# DELETES the ephemeral CF master token. Non-secret config lives in platform.config.ts `vars` (→ wrangler.toml [vars]).",
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
- "# PROVISIONING creds supply in .env.temp (plaintext). The master is EPHEMERAL (deleted after minting; never committed):",
108
- ...provisioning.map(line),
108
+ "SULUK_PUBLIC_KEY= # @suluk/env public key (plaintext; can only encrypt)",
109
109
  "",
110
- "# Scoped least-privilege tokens MINTED by `bun run provision`/`mint-tokens` (you don't supply these); kept encrypted:",
111
- ...minted.map((e) => `# ${line(e)}`),
110
+ "# Provisioning keeper + minted scoped tokens (surface local never shipped to the Worker; encrypted):",
111
+ ...localKeepers.map(line),
112
112
  "",
113
- "# RUNTIME secrets — supply in .env.temp; encrypted into .env + shipped to the Worker:",
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
- ...runtimeSecretsOf(env).map(line),
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 — `openssl rand -base64 32`" },
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" },