@suluk/platform 0.3.0 → 0.3.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.3.0",
3
+ "version": "0.3.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/generate.ts CHANGED
@@ -58,6 +58,7 @@ export async function generatePlatform(input: PlatformManifest | Platform, opts:
58
58
  ["scripts/env-check.ts", plan.envCheck, true], // the encrypted-env preflight
59
59
  ["src/env.ts", plan.envTs, true], // the @suluk/env declare-once (derived from the manifest's secrets)
60
60
  ["scripts/sync-secrets.ts", plan.syncSecrets, true], // the deploy-time secret push (derived)
61
+ ["scripts/link-key.ts", plan.linkKey, true], // register the private key into ~/.suluk/settings.json (the central store)
61
62
  [".env", plan.envScaffold, false], // the COMMITTED encrypted-secrets file — SCAFFOLD IF ABSENT (never clobber secrets)
62
63
  ] as const) {
63
64
  if (always || (await read(file)) == null) {
package/src/plan.ts CHANGED
@@ -36,6 +36,9 @@ export interface PlatformPlan {
36
36
  /** the generated `scripts/sync-secrets.ts` — decrypt the cloudflare-surfaced secrets from the committed .env and push them
37
37
  * as `wrangler secret`s (the toolfactory-exact deploy path; the alternative is the entry's runtime `loadEnv`). */
38
38
  syncSecrets: string;
39
+ /** the generated `scripts/link-key.ts` — register the private key into the centralized `~/.suluk/settings.json` (the store
40
+ * `@suluk/env` reads by default for local dev/deploy/CI), the toolfactory model. */
41
+ linkKey: string;
39
42
  /** the generated `.env` SCAFFOLD (committed) — a header + the setup steps, NO values. `generate` writes it only if absent
40
43
  * (never clobbering the operator's encrypted secrets). Secret VALUES are added encrypted via `suluk-env set`. */
41
44
  envScaffold: string;
@@ -71,6 +74,7 @@ export function planPlatform(input: PlatformManifest | Platform): PlatformPlan {
71
74
  envCheck: buildEnvCheckScript(env),
72
75
  envTs: buildEnvTs(env),
73
76
  syncSecrets: buildSyncSecrets(),
77
+ linkKey: buildLinkKey(),
74
78
  envScaffold: buildEnvScaffold(env),
75
79
  };
76
80
  }
@@ -87,7 +91,8 @@ function buildEnvExample(env: EnvVar[]): string {
87
91
  const optional = secrets.filter((e) => !e.required);
88
92
  return [
89
93
  "# .env keys checklist (generated). The `.env` is COMMITTED with these values ENCRYPTED (@suluk/env, ML-KEM-768):",
90
- "# bunx suluk-env keygen # once — keypair (SULUK_PUBLIC_KEY → .env; private → .env.keys, gitignored)",
94
+ "# bunx suluk-env keygen # once — keypair (SULUK_PUBLIC_KEY → .env; private → .env.keys)",
95
+ "# bun run link-key # register the private key in ~/.suluk/settings.json (@suluk/env reads it by default)",
91
96
  "# bunx suluk-env set KEY=value # per secret — encrypts it into .env",
92
97
  "# Non-secret config lives in platform.config.ts `vars` (→ wrangler.toml [vars]), NOT here.",
93
98
  "",
@@ -151,6 +156,43 @@ console.log(\`✓ synced \${names.length} secret(s) to the Worker.\`);
151
156
  `;
152
157
  }
153
158
 
159
+ /**
160
+ * `scripts/link-key.ts` — register THIS project's @suluk/env private key into the centralized `~/.suluk/settings.json`
161
+ * (keyed by the repo's absolute path), the toolfactory model. `readPrivateKey()` then resolves it from there BY DEFAULT
162
+ * (precedence: `SULUK_PRIVATE_KEY` env > `~/.suluk/settings.json` by path > legacy `.env.keys`), so local dev, deploy, and a
163
+ * CI worktree (which checks out the encrypted .env but not `.env.keys`, via `SULUK_PROJECT_DIR`) all decrypt from one store.
164
+ */
165
+ function buildLinkKey(): string {
166
+ return `#!/usr/bin/env bun
167
+ // AUTO-GENERATED by @suluk/platform. Register this project's @suluk/env private key into ~/.suluk/settings.json (keyed by the
168
+ // repo path) — the central, out-of-git store @suluk/env reads by default. Run once after \`bunx suluk-env keygen\`; idempotent.
169
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
170
+ import { homedir } from "node:os";
171
+ import { join, dirname } from "node:path";
172
+ import { readPrivateKey } from "@suluk/env/node";
173
+
174
+ const repo = process.cwd();
175
+ const name = repo.split("/").filter(Boolean).pop() ?? "project";
176
+ const priv = readPrivateKey(); // SULUK_PRIVATE_KEY env > ~/.suluk/settings.json > .env.keys (legacy)
177
+ if (!priv) { console.error("✗ no private key — run \`bunx suluk-env keygen\` first"); process.exit(1); }
178
+
179
+ const settingsPath = process.env.SULUK_SETTINGS_PATH || join(homedir(), ".suluk", "settings.json");
180
+ type Proj = { name?: string; path?: string; env?: Array<{ key: string; value: string }> };
181
+ let data: { projects?: Proj[] } = {};
182
+ try { data = JSON.parse(readFileSync(settingsPath, "utf8")); } catch { /* fresh */ }
183
+ const projects: Proj[] = Array.isArray(data.projects) ? data.projects : [];
184
+ let entry = projects.find((p) => p.path === repo);
185
+ if (!entry) { entry = { name, path: repo, env: [] }; projects.push(entry); }
186
+ entry.name = name; entry.path = repo; entry.env = Array.isArray(entry.env) ? entry.env : [];
187
+ const existing = entry.env.find((e) => e.key === "SULUK_PRIVATE_KEY");
188
+ if (existing) existing.value = priv; else entry.env.push({ key: "SULUK_PRIVATE_KEY", value: priv });
189
+ data.projects = projects;
190
+ mkdirSync(dirname(settingsPath), { recursive: true });
191
+ writeFileSync(settingsPath, JSON.stringify(data, null, 2) + "\\n");
192
+ console.log(\`✓ linked \${name} → \${settingsPath}. You can now \\\`rm .env.keys\\\` — the key lives in the central store.\`);
193
+ `;
194
+ }
195
+
154
196
  /**
155
197
  * The `.env` SCAFFOLD — a header + setup steps, NO values (safe to commit). `generate` writes it ONLY IF ABSENT so it never
156
198
  * clobbers the operator's encrypted secrets. Its presence also lets `src/index.ts`'s `import "../.env"` resolve on a fresh app.
@@ -160,7 +202,9 @@ function buildEnvScaffold(env: EnvVar[]): string {
160
202
  return [
161
203
  "# This file is COMMITTED. Secret VALUES are stored ENCRYPTED (@suluk/env, post-quantum ML-KEM-768) — safe to push to git.",
162
204
  "# Setup:",
163
- "# bunx suluk-env keygen # keypair: SULUK_PUBLIC_KEY here (commit); private key → .env.keys (gitignored)",
205
+ "# bunx suluk-env keygen # keypair: SULUK_PUBLIC_KEY here (commit); private key → .env.keys",
206
+ "# bun run link-key # register the private key in ~/.suluk/settings.json (the central store @suluk/env",
207
+ "# # reads by DEFAULT for local dev/deploy/CI); then `rm .env.keys` if you like",
164
208
  "# bunx suluk-env set BETTER_AUTH_SECRET=... # encrypts + adds each secret" + (required.length ? ` (required: ${required.map((e) => e.name).join(", ")})` : ""),
165
209
  "# Get the secrets into the Worker EITHER way:",
166
210
  "# • runtime: `wrangler secret put SULUK_PRIVATE_KEY` → src/index.ts decrypts this file on the first request",
@@ -239,14 +283,19 @@ function buildGitignore(): string {
239
283
  }
240
284
 
241
285
  /** Merge the generated .gitignore into an existing one — APPEND any missing entries (never skip-if-present, so an app's
242
- * minimal .gitignore can't leave `.env`/`.env.temp` UNIGNORED and risk committing secrets). Dedup, preserve app entries. */
286
+ * minimal .gitignore can't leave `.env.keys`/`.env.temp` UNIGNORED and risk committing the private key). Dedup, preserve app
287
+ * entries. ENCRYPTED-ENV TRANSITION: if the new baseline ignores `.env.keys` (the private key) but NOT `.env`, a plaintext-era
288
+ * `.env` ignore is REMOVED — the .env is now COMMITTED with its values encrypted, so ignoring it is wrong (and safe to undo). */
243
289
  export function mergeGitignore(generated: string, existing: string | null): string {
244
290
  if (!existing) return generated;
245
291
  const norm = (s: string) => s.trim().replace(/\/$/, "");
246
- const have = new Set(existing.split("\n").map(norm).filter(Boolean));
292
+ const genLines = generated.split("\n").map(norm);
293
+ const encryptedModel = genLines.includes(".env.keys") && !genLines.includes(".env");
294
+ const existingClean = encryptedModel ? existing.split("\n").filter((l) => norm(l) !== ".env").join("\n") : existing;
295
+ const have = new Set(existingClean.split("\n").map(norm).filter(Boolean));
247
296
  const add = generated.split("\n").filter((l) => l.trim() && !have.has(norm(l)));
248
- if (!add.length) return existing.endsWith("\n") ? existing : existing + "\n";
249
- const base = existing.replace(/\n*$/, "");
297
+ if (!add.length) return existingClean.endsWith("\n") ? existingClean : existingClean + "\n";
298
+ const base = existingClean.replace(/\n*$/, "");
250
299
  return `${base}\n${add.join("\n")}\n`;
251
300
  }
252
301
 
@@ -309,6 +358,7 @@ export function buildPackageJson(name: string, services: string[], catalog: Reco
309
358
  dev: "wrangler dev",
310
359
  deploy: "wrangler deploy",
311
360
  "env:keygen": "suluk-env keygen", // create the @suluk/env keypair (SULUK_PUBLIC_KEY → .env; private → .env.keys)
361
+ "link-key": "bun run scripts/link-key.ts", // register the private key in ~/.suluk/settings.json (the central store)
312
362
  "env:set": "suluk-env set", // encrypt + add a secret: `bun run env:set BETTER_AUTH_SECRET=...`
313
363
  "sync-secrets": "bun run scripts/sync-secrets.ts", // decrypt cloudflare-surfaced secrets → `wrangler secret put` each
314
364
  typecheck: "tsc --noEmit -p .",