@stratum-hq/create 0.5.0 → 0.6.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/src/index.ts CHANGED
@@ -2,12 +2,23 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import crypto from "node:crypto";
4
4
  import { execSync } from "child_process";
5
- import { parsePresetString, isValidPreset } from "./matrix.js";
5
+ import { parsePresetString, isValidPreset, type StackPreset } from "./matrix.js";
6
6
  import { createPresetProject } from "./preset-project.js";
7
7
  import { STRATUM_RANGES } from "./stratum-versions.js";
8
- import { postgresAppRole, postgresAppRoleSql, POSTGRES_APP_PASSWORD } from "./generators/init-sql.js";
8
+ import {
9
+ postgresAppRole,
10
+ postgresAppRoleSql,
11
+ postgresStratumRole,
12
+ POSTGRES_APP_PASSWORD,
13
+ POSTGRES_STRATUM_PASSWORD,
14
+ } from "./generators/init-sql.js";
9
15
  import { generateTsconfig } from "./generators/tsconfig.js";
10
- import { nextjsTenantMiddleware } from "./generators/middleware.js";
16
+ import {
17
+ expressServer,
18
+ fastifyServer,
19
+ nextjsRootLayout,
20
+ nextjsTenantMiddleware,
21
+ } from "./generators/middleware.js";
11
22
 
12
23
  // ─── Types ────────────────────────────────────────────────────────────────────
13
24
 
@@ -111,11 +122,11 @@ function writeFile(filePath: string, content: string): void {
111
122
  function generatePackageJson(projectName: string, template: Template): string {
112
123
  const frameworkDeps: Record<Template, Record<string, string>> = {
113
124
  express: {
114
- express: "^4.18.0",
125
+ express: "^4.22.3",
115
126
  "@types/express": "^4.17.21",
116
127
  },
117
128
  fastify: {
118
- fastify: "^4.26.0",
129
+ fastify: "^5.12.5",
119
130
  },
120
131
  nextjs: {
121
132
  next: "^15.5.16",
@@ -123,14 +134,14 @@ function generatePackageJson(projectName: string, template: Template): string {
123
134
  "react-dom": "^19.0.0",
124
135
  "@types/react": "^19.0.0",
125
136
  "@types/react-dom": "^19.0.0",
126
- // The middleware verifies the tenant JWT with jose.
127
- jose: "^6.2.12",
128
137
  },
129
138
  };
130
139
 
131
140
  const deps = {
132
141
  "@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"],
133
142
  pg: "^8.11.0",
143
+ // Every template verifies the tenant JWT with jose.
144
+ jose: "^6.2.12",
134
145
  ...frameworkDeps[template],
135
146
  };
136
147
 
@@ -149,7 +160,7 @@ function generatePackageJson(projectName: string, template: Template): string {
149
160
  devDependencies: {
150
161
  typescript: "^5.3.0",
151
162
  "@types/node": "^20.11.0",
152
- ...(template === "nextjs" ? {} : { tsx: "^4.7.0", "@types/pg": "^8.11.0" }),
163
+ ...(template === "nextjs" ? {} : { tsx: "^4.19.3", "@types/pg": "^8.11.0" }),
153
164
  },
154
165
  engines: {
155
166
  node: ">=20.0.0",
@@ -213,7 +224,10 @@ function generateEnv(projectName: string): string {
213
224
  DATABASE_URL=postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}
214
225
 
215
226
  # Superuser: bootstrap and migrations only. It bypasses row-level security.
216
- DATABASE_ADMIN_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
227
+ DATABASE_SUPERUSER_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
228
+
229
+ # Stratum's own login: the library's adminPool, which runs the Stratum migrations (see init.sql).
230
+ STRATUM_ADMIN_DATABASE_URL=postgres://${postgresStratumRole(dbName)}:${POSTGRES_STRATUM_PASSWORD}@localhost:5432/${dbName}
217
231
 
218
232
  # Authentication
219
233
  JWT_SECRET=${jwtSecret}
@@ -226,77 +240,8 @@ NODE_ENV=development
226
240
  `;
227
241
  }
228
242
 
229
- function generateExpressServer(projectName: string): string {
230
- return `import express from "express";
231
- import { Pool } from "pg";
232
-
233
- const app = express();
234
- const port = process.env.PORT || 3000;
235
-
236
- const pool = new Pool({
237
- connectionString: process.env.DATABASE_URL,
238
- });
239
-
240
- app.use(express.json());
241
-
242
- app.get("/health", (_req, res) => {
243
- res.json({ status: "ok", project: "${projectName}" });
244
- });
245
-
246
- app.get("/tenants", async (_req, res) => {
247
- try {
248
- // Example: query your tenant table
249
- // const { rows } = await pool.query("SELECT * FROM tenants LIMIT 20");
250
- // res.json(rows);
251
- res.json({ message: "Replace this with your tenant queries", pool: !!pool });
252
- } catch (err) {
253
- res.status(500).json({ error: String(err) });
254
- }
255
- });
256
-
257
- app.listen(port, () => {
258
- console.log(\`${projectName} server running on http://localhost:\${port}\`);
259
- });
260
- `;
261
- }
262
-
263
- function generateFastifyServer(projectName: string): string {
264
- return `import Fastify from "fastify";
265
- import { Pool } from "pg";
266
-
267
- const fastify = Fastify({ logger: true });
268
- const port = Number(process.env.PORT) || 3000;
269
-
270
- const pool = new Pool({
271
- connectionString: process.env.DATABASE_URL,
272
- });
273
-
274
- fastify.get("/health", async () => {
275
- return { status: "ok", project: "${projectName}" };
276
- });
277
-
278
- fastify.get("/tenants", async (_request, reply) => {
279
- try {
280
- // Example: query your tenant table
281
- // const { rows } = await pool.query("SELECT * FROM tenants LIMIT 20");
282
- // return rows;
283
- return { message: "Replace this with your tenant queries", pool: !!pool };
284
- } catch (err) {
285
- return reply.status(500).send({ error: String(err) });
286
- }
287
- });
288
-
289
- fastify.listen({ port, host: "0.0.0.0" }, (err) => {
290
- if (err) {
291
- fastify.log.error(err);
292
- process.exit(1);
293
- }
294
- });
295
- `;
296
- }
297
-
298
243
  function generateNextjsPage(projectName: string): string {
299
- return `// app/page.tsx — ${projectName} root page
244
+ return `// app/page.tsx: ${projectName} root page
300
245
  export default function Home() {
301
246
  return (
302
247
  <main style={{ padding: "2rem", fontFamily: "sans-serif" }}>
@@ -304,7 +249,7 @@ export default function Home() {
304
249
  <p>Multi-tenant app powered by Stratum.</p>
305
250
  <ul>
306
251
  <li>Configure tenants via the Stratum control plane</li>
307
- <li>The tenant comes from a verified JWT in <code>middleware.ts</code></li>
252
+ <li>The tenant comes from a verified JWT in <code>src/middleware.ts</code></li>
308
253
  <li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
309
254
  </ul>
310
255
  </main>
@@ -330,7 +275,7 @@ docker compose up -d
330
275
 
331
276
  \`\`\`bash
332
277
  cp .env.example .env
333
- # Edit .env — update DATABASE_URL, JWT_SECRET, and STRATUM_API_KEY
278
+ # Edit .env: update DATABASE_URL, JWT_SECRET, and STRATUM_API_KEY
334
279
  \`\`\`
335
280
 
336
281
  ### 3. Install dependencies
@@ -360,9 +305,13 @@ ${template === "nextjs" ? "" : "├── tsconfig.json\n"}└── package.jso
360
305
 
361
306
  This project uses Stratum for hierarchical multi-tenancy:
362
307
 
363
- - **Tenant resolution** — from the \`tenant_id\` claim of a bearer token verified with \`JWT_SECRET\` (see \`middleware.ts\`); the subdomain is only a display slug
364
- - **Config inheritance** — settings flow down the tenant tree with override support
365
- - **Permission ABAC** — role-based permissions with tenant-scoped enforcement
308
+ ${
309
+ template === "nextjs"
310
+ ? "- **Tenant resolution**: from the `tenant_id` claim of a bearer token verified with `JWT_SECRET` (see `src/middleware.ts`); the subdomain is only a display slug"
311
+ : "- **Tenant resolution**: the tenant middleware in `src/index.ts` takes the tenant from the `tenant_id` claim of a bearer token verified with `JWT_SECRET` (HS256, using `jose`). A token that does not verify is rejected with 401, and `GET /tenants` answers 401 without a tenant. The hostname and headers such as `x-tenant-id` are never used"
312
+ }
313
+ - **Config inheritance**: settings flow down the tenant tree with override support
314
+ - **Permission ABAC**: role-based permissions with tenant-scoped enforcement
366
315
 
367
316
  See the [Stratum docs](https://github.com/stratum-hq/Stratum) for full reference.
368
317
  `;
@@ -395,14 +344,15 @@ export function createProject(
395
344
 
396
345
  // Server starter file
397
346
  if (template === "express") {
398
- writeFile(path.join(targetDir, "src", "index.ts"), generateExpressServer(projectName));
347
+ writeFile(path.join(targetDir, "src", "index.ts"), expressServer(projectName));
399
348
  writeFile(path.join(targetDir, "tsconfig.json"), generateTsconfig(template));
400
349
  } else if (template === "fastify") {
401
- writeFile(path.join(targetDir, "src", "index.ts"), generateFastifyServer(projectName));
350
+ writeFile(path.join(targetDir, "src", "index.ts"), fastifyServer(projectName));
402
351
  writeFile(path.join(targetDir, "tsconfig.json"), generateTsconfig(template));
403
352
  } else if (template === "nextjs") {
353
+ writeFile(path.join(targetDir, "src", "app", "layout.tsx"), nextjsRootLayout(projectName));
404
354
  writeFile(path.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
405
- writeFile(path.join(targetDir, "middleware.ts"), nextjsTenantMiddleware());
355
+ writeFile(path.join(targetDir, "src", "middleware.ts"), nextjsTenantMiddleware());
406
356
  }
407
357
 
408
358
  // README
@@ -453,6 +403,25 @@ export function main(argv: string[]): void {
453
403
  process.exit(1);
454
404
  }
455
405
 
406
+ // Check the preset before touching the file system, so an invalid preset
407
+ // neither leaves an empty directory nor removes one under --force.
408
+ let parsedPreset: StackPreset | null = null;
409
+ if (preset) {
410
+ parsedPreset = parsePresetString(preset);
411
+ if (!parsedPreset) {
412
+ console.error(`Error: Invalid preset string "${preset}".`);
413
+ console.error("Format: {database}-{strategy}-{orm}-{framework}");
414
+ console.error("Example: postgres-rls-prisma-express, mongodb-database-mongoose-hono");
415
+ process.exit(1);
416
+ }
417
+ if (!isValidPreset(parsedPreset)) {
418
+ console.error(`Error: Invalid preset combination "${preset}".`);
419
+ console.error(`${parsedPreset.database} does not support ${parsedPreset.strategy}/${parsedPreset.orm} together.`);
420
+ console.error("Run with --help to see valid combinations.");
421
+ process.exit(1);
422
+ }
423
+ }
424
+
456
425
  const targetDir = path.resolve(process.cwd(), projectName);
457
426
 
458
427
  if (fs.existsSync(targetDir) && !force) {
@@ -468,21 +437,8 @@ export function main(argv: string[]): void {
468
437
  fs.mkdirSync(targetDir, { recursive: true });
469
438
 
470
439
  // Preset path: full stack generation
471
- if (preset) {
472
- const parsed = parsePresetString(preset);
473
- if (!parsed) {
474
- console.error(`Error: Invalid preset string "${preset}".`);
475
- console.error("Format: {database}-{strategy}-{orm}-{framework}");
476
- console.error("Example: postgres-rls-prisma-express, mongodb-database-mongoose-hono");
477
- process.exit(1);
478
- }
479
- if (!isValidPreset(parsed)) {
480
- console.error(`Error: Invalid preset combination "${preset}".`);
481
- console.error(`${parsed.database} does not support ${parsed.strategy}/${parsed.orm} together.`);
482
- console.error("Run with --help to see valid combinations.");
483
- process.exit(1);
484
- }
485
- createPresetProject(projectName, parsed, targetDir, skipInstall);
440
+ if (parsedPreset) {
441
+ createPresetProject(projectName, parsedPreset, targetDir, skipInstall);
486
442
  } else {
487
443
  // Template path: existing behavior, untouched
488
444
  createProject(projectName, template, targetDir, skipInstall);
package/src/matrix.ts CHANGED
@@ -75,7 +75,7 @@ export function parsePresetString(s: string): StackPreset | null {
75
75
 
76
76
  const parts = s.toLowerCase().split("-");
77
77
 
78
- // Handle "table-prefix" which contains a hyphen -- it will split into
78
+ // Handle "table-prefix" which contains a hyphen; it will split into
79
79
  // 5 parts: [db, strategy1, "table", "prefix", orm, framework] or similar.
80
80
  // We need to reconstruct multi-word tokens.
81
81
  // Format: {database}-{strategy}-{orm}-{framework}
@@ -4,7 +4,13 @@ import crypto from "node:crypto";
4
4
  import { execSync } from "child_process";
5
5
  import type { StackPreset } from "./matrix.js";
6
6
  import { generatePresetDockerCompose } from "./generators/docker-compose.js";
7
- import { generatePresetInitSql, postgresAppRole, POSTGRES_APP_PASSWORD } from "./generators/init-sql.js";
7
+ import {
8
+ generatePresetInitSql,
9
+ postgresAppRole,
10
+ postgresStratumRole,
11
+ POSTGRES_APP_PASSWORD,
12
+ POSTGRES_STRATUM_PASSWORD,
13
+ } from "./generators/init-sql.js";
8
14
  import { generateDbSetup } from "./generators/db-setup.js";
9
15
  import { generateMiddleware } from "./generators/middleware.js";
10
16
  import { generatePresetPackageJson } from "./generators/package-json.js";
@@ -32,7 +38,9 @@ function generatePresetEnv(projectName: string, preset: StackPreset): string {
32
38
  // row-level security applies to it. The superuser URL is for bootstrap
33
39
  // and migrations only.
34
40
  dbUrl = `postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}`;
35
- adminUrlLine = `\n# Superuser: bootstrap and migrations only. It bypasses row-level security.\nDATABASE_ADMIN_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}\n`;
41
+ adminUrlLine = `\n# Superuser: bootstrap and migrations only. It bypasses row-level security.\nDATABASE_SUPERUSER_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}\n` +
42
+ `\n# Stratum's own login: the library's adminPool, which runs the Stratum migrations (see init.sql).\n` +
43
+ `STRATUM_ADMIN_DATABASE_URL=postgres://${postgresStratumRole(dbName)}:${POSTGRES_STRATUM_PASSWORD}@localhost:5432/${dbName}\n`;
36
44
  break;
37
45
  case "mongodb":
38
46
  dbUrl = `mongodb://${dbName}:dev_password@localhost:27017/${dbName}?authSource=admin`;