@getstrata/starter 1.1.6 → 1.1.7

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/dist/cli.js CHANGED
@@ -1394,17 +1394,18 @@ async function completeBrowserSsoLogin(
1394
1394
  profile: { email: string; name: string },
1395
1395
  label: string,
1396
1396
  ): Promise<Response> {
1397
- let record = await starterAuthDirectory.findByEmail?.(profile.email);
1397
+ const email = profile.email.trim().toLowerCase();
1398
+ let record = await starterAuthDirectory.findByEmail?.(email);
1398
1399
  if (!record) {
1399
1400
  if ((process.env.FEATURE_REGISTRATION ?? "true") === "false") {
1400
1401
  return jsonResponse({ error: \`\${label} user is not provisioned.\` }, { status: 403 });
1401
1402
  }
1402
1403
  const hashed = await hashPassword(randomBytes(18).toString("hex"));
1403
1404
  try {
1404
- await runAuthWrite(profile.email, async () => {
1405
+ await runAuthWrite(email, async () => {
1405
1406
  await User.create({
1406
1407
  name: profile.name,
1407
- email: profile.email,
1408
+ email,
1408
1409
  password: hashed,
1409
1410
  is_admin: false,${samlTenantCreateField}
1410
1411
  });
@@ -1412,7 +1413,7 @@ async function completeBrowserSsoLogin(
1412
1413
  } catch {
1413
1414
  // Unique email: another request already provisioned this user.
1414
1415
  }
1415
- record = await starterAuthDirectory.findByEmail?.(profile.email);
1416
+ record = await starterAuthDirectory.findByEmail?.(email);
1416
1417
  }
1417
1418
  if (!record) {
1418
1419
  return jsonResponse({ error: \`Could not complete \${label} login.\` }, { status: 500 });
@@ -2385,7 +2386,7 @@ function renderLoginView(layers) {
2385
2386
  const githubLink = layers.extras.oauthGithub && htmlAuthKit(layers.auth) ? `
2386
2387
  <a href="/auth/github">Sign in with GitHub</a>` : "";
2387
2388
  const oidcLink = layers.extras.oidc && htmlAuthKit(layers.auth) ? `
2388
- <a href="/auth/oidc">Sign in with OIDC</a>` : "";
2389
+ <% if (it.oidcLogin) { %><a href="/auth/oidc">Sign in with OIDC</a><% } %>` : "";
2389
2390
  return renderFormView("Sign in", `${textField("email", "Email", "email", 'required autocomplete="username"')}
2390
2391
  ${textField("password", "Password", "password", 'required autocomplete="current-password"')}`, "Sign in", `<a href="/register">Create account</a>
2391
2392
  <a href="/forgot-password">Forgot password</a>${githubLink}${oidcLink}`).replace('action="<%= it.action || "" %>"', 'action="/login"');
@@ -3059,9 +3060,9 @@ function renderPackageJson(projectName, options = {}) {
3059
3060
  "@getstrata/cli": "workspace:*",
3060
3061
  "@getstrata/core": "workspace:*"
3061
3062
  } : {
3062
- "@getstrata/bootstrap": "^1.1.6",
3063
- "@getstrata/cli": "^1.1.6",
3064
- "@getstrata/core": "^1.1.6"
3063
+ "@getstrata/bootstrap": "^1.1.7",
3064
+ "@getstrata/cli": "^1.1.7",
3065
+ "@getstrata/core": "^1.1.7"
3065
3066
  };
3066
3067
  coreDeps.eta = "^4.6.0";
3067
3068
  if (options.layers?.database === "mysql") {
@@ -3273,7 +3274,9 @@ bunx strata openapi:generate
3273
3274
  bunx strata openapi:check
3274
3275
  \`\`\`
3275
3276
 
3276
- Put \`openapi:check\` in CI. A failure means \`docs/openapi.json\` does not match the live route map (billing, auth, webhooks, and anything else you added).
3277
+ Put \`openapi:check\` in CI. A failure means \`docs/openapi.json\` does not match the live JSON API route map (billing, auth, webhooks, and anything else you added).
3278
+
3279
+ \`openapi:generate\` lists API routes only. HTML admin and storefront paths are not in that file.
3277
3280
  `;
3278
3281
  }
3279
3282
  function renderReadmeIntro(projectName, options) {
@@ -3352,7 +3355,7 @@ HTML auth kit (restyle \`views/\` and \`public/assets/site.css\`):
3352
3355
  - Register: \`/register\`
3353
3356
  - Forgot password: \`/forgot-password\`
3354
3357
  - Reset password: signed \`/reset-password\` (mail log when \`MAIL_DRIVER=log\`)
3355
- ${layers.extras.emailVerification ? "- Verify email: `/email/verify`\n" : ""}${layers.extras.mfa ? "- MFA challenge: `/login/mfa` and setup: `/account/mfa`\n" : ""}${layers.extras.oauthGithub ? "- GitHub: `/auth/github` (needs `FEATURE_OAUTH=true`)\n" : ""}${layers.extras.oidc ? "- OIDC: `/auth/oidc` (needs `FEATURE_OAUTH=true`; PKCE cookie `oidc_pkce`)\n" : ""}
3358
+ ${layers.extras.emailVerification ? "- Verify email: `/email/verify`\n" : ""}${layers.extras.mfa ? "- MFA challenge: `/login/mfa` and setup: `/account/mfa`\n" : ""}${layers.extras.oauthGithub ? "- GitHub: `/auth/github` (needs `FEATURE_OAUTH=true`). Failures on the callback are JSON, not the login page. State is the signed query value from `createOAuthState()`, not `createOAuthStateCookie()`.\n" : ""}${layers.extras.oidc ? "- OIDC: `/auth/oidc` (needs `FEATURE_OAUTH=true` plus `OIDC_CLIENT_ID` and `OIDC_CLIENT_SECRET`; otherwise the login link stays hidden and the route is 404). PKCE cookie `oidc_pkce`. Callback failures are JSON, not the login page.\n" : ""}
3356
3359
  Cookie name is \`strata_session\`. Forms send CSRF as \`_token\`.
3357
3360
  ` : ""}${authUsesToken(layers.auth) ? `
3358
3361
  Opaque token login: \`POST /api/v1/auth/login\` with \`{ "email", "password" }\`. Register: \`POST /api/v1/auth/register\`. Forgot/reset: \`POST /api/v1/auth/forgot-password\` and signed \`POST /api/v1/auth/reset-password\`. Send \`Authorization: Bearer\` after login.
@@ -4866,10 +4869,13 @@ export async function renderPage(
4866
4869
  ): Promise<Response> {
4867
4870
  const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
4868
4871
  const flash = currentRequestMeta().flash ?? null;
4869
- ${userBlock}
4872
+ ${userBlock}${layers.extras.oidc ? `
4873
+ const oidcLogin =
4874
+ process.env.FEATURE_OAUTH === "true" &&
4875
+ Boolean(process.env.OIDC_CLIENT_ID?.trim() && process.env.OIDC_CLIENT_SECRET?.trim());` : ""}
4870
4876
  const html = await engine.render(
4871
4877
  template,
4872
- { ...data, csrfToken, flash, currentUser },
4878
+ { ...data, csrfToken, flash, currentUser${layers.extras.oidc ? ", oidcLogin" : ""} },
4873
4879
  { request },
4874
4880
  );
4875
4881
  return htmlResponse(html, { status });
@@ -4991,16 +4997,22 @@ function renderBillingMigration(layers) {
4991
4997
  ];
4992
4998
  return renderFileMigration(name, up, down);
4993
4999
  }
4994
- function renderDispatchOutboundWebhookJob() {
5000
+ function sqlParams(database, count) {
5001
+ return Array.from({ length: count }, (_, index) => database === "postgres" ? `$${index + 1}` : "?").join(", ");
5002
+ }
5003
+ function renderDispatchOutboundWebhookJob(layers) {
5004
+ const values = sqlParams(layers.database, 6);
4995
5005
  return `import { Job } from "@getstrata/core/queue";
4996
5006
  import { isProductionEnv } from "@getstrata/core/runtime/appEnv";
4997
5007
  import { webhookSignatureHeader } from "@getstrata/core/runtime/appKeyPrefix";
4998
5008
  import { safeFetch } from "@getstrata/core/security/safeFetch";
4999
5009
  import { signWebhookBody } from "@getstrata/core/security/webhookSignature";
5010
+ import { runWithMigrationBypass } from "@getstrata/core/tenant/databaseTenantContext";
5000
5011
  import { getSql } from "../bootstrap/database.ts";
5001
5012
 
5002
5013
  interface DispatchOutboundWebhookPayload {
5003
5014
  webhookId: number;
5015
+ event: string;
5004
5016
  url: string;
5005
5017
  secret: string;
5006
5018
  body: string;
@@ -5034,10 +5046,12 @@ class DispatchOutboundWebhookJob extends Job<DispatchOutboundWebhookPayload> {
5034
5046
  error = caught instanceof Error ? caught.message : "webhook dispatch failed";
5035
5047
  }
5036
5048
 
5037
- await getSql().unsafe(
5038
- "INSERT INTO webhook_deliveries (webhook_id, event, payload, response_status, attempt, error) VALUES (?, ?, ?, ?, ?, ?)",
5039
- [payload.webhookId, "notes.created", payload.body, responseStatus, 1, error],
5040
- );
5049
+ await runWithMigrationBypass(async () => {
5050
+ await getSql().unsafe(
5051
+ "INSERT INTO webhook_deliveries (webhook_id, event, payload, response_status, attempt, error) VALUES (${values})",
5052
+ [payload.webhookId, payload.event, payload.body, responseStatus, 1, error],
5053
+ );
5054
+ });
5041
5055
  }
5042
5056
  }
5043
5057
 
@@ -5060,13 +5074,15 @@ function registerNoteCreatedWebhookListener(): void {
5060
5074
  active: number | boolean;
5061
5075
  }>("SELECT id, url, secret, active FROM webhooks");
5062
5076
  const queue = resolveApplicationQueue();
5063
- const body = JSON.stringify({ event: "notes.created", data: payload });
5077
+ const event = "notes.created";
5078
+ const body = JSON.stringify({ event, data: payload });
5064
5079
  for (const hook of rows) {
5065
5080
  if (!hook.active) {
5066
5081
  continue;
5067
5082
  }
5068
5083
  await queue.dispatch(new DispatchOutboundWebhookJob(), {
5069
5084
  webhookId: hook.id,
5085
+ event,
5070
5086
  url: hook.url,
5071
5087
  secret: hook.secret,
5072
5088
  body,
@@ -5078,8 +5094,29 @@ function registerNoteCreatedWebhookListener(): void {
5078
5094
  export default registerNoteCreatedWebhookListener;
5079
5095
  `;
5080
5096
  }
5097
+ function renderApplyStripeWebhook() {
5098
+ return `interface StripeWebhookEvent {
5099
+ id?: string;
5100
+ type?: string;
5101
+ data?: { object?: Record<string, unknown> };
5102
+ }
5103
+
5104
+ /**
5105
+ * Called after the Stripe signature verifies. The default does nothing.
5106
+ * Plan and tenant updates belong in this function, not in framework core.
5107
+ * Stripe POSTs have no tenant ALS. See docs/INTEGRATIONS.md before writing tenant tables.
5108
+ */
5109
+ async function applyStripeWebhook(_event: StripeWebhookEvent): Promise<void> {
5110
+ return;
5111
+ }
5112
+
5113
+ export type { StripeWebhookEvent };
5114
+ export { applyStripeWebhook };
5115
+ `;
5116
+ }
5081
5117
  function renderBillingModule(layers) {
5082
5118
  const tenancyOn = usesTenantTable(layers.tenancy);
5119
+ const tenantPlaceholder = layers.database === "postgres" ? "$1" : "?";
5083
5120
  const tenantImport = tenancyOn ? `import { currentTenant } from "@getstrata/core/tenant/tenantContext";
5084
5121
  ` : "";
5085
5122
  const subscriptionQuery = tenancyOn ? ` const tenant = currentTenant();
@@ -5087,7 +5124,7 @@ function renderBillingModule(layers) {
5087
5124
  return jsonResponse({ error: "Tenant context is required." }, { status: 500 });
5088
5125
  }
5089
5126
  const rows = await getSql().unsafe<Record<string, unknown>>(
5090
- "SELECT plan, status, stripe_subscription_id, current_period_end FROM subscription WHERE tenant_id = ? LIMIT 1",
5127
+ "SELECT plan, status, stripe_subscription_id, current_period_end FROM subscription WHERE tenant_id = ${tenantPlaceholder} LIMIT 1",
5091
5128
  [tenant.id],
5092
5129
  );` : ` const rows = await getSql().unsafe<Record<string, unknown>>(
5093
5130
  "SELECT plan, status, stripe_subscription_id, current_period_end FROM subscription LIMIT 1",
@@ -5095,6 +5132,7 @@ function renderBillingModule(layers) {
5095
5132
  return `import type { AppModule } from "@getstrata/bootstrap/contracts";
5096
5133
  import { jsonResponse, withErrorHandling } from "@getstrata/core/http/response";
5097
5134
  import { verifyStripeWebhookSignature } from "@getstrata/core/security/stripeWebhook";
5135
+ import { applyStripeWebhook } from "../../billing/applyStripeWebhook.ts";
5098
5136
  ${tenantImport}import { getSql } from "../../bootstrap/database.ts";
5099
5137
 
5100
5138
  function billingEnabled(): boolean {
@@ -5125,10 +5163,7 @@ const billingModule: AppModule = {
5125
5163
  } catch {
5126
5164
  return jsonResponse({ error: "Invalid JSON." }, { status: 400 });
5127
5165
  }
5128
- // Persist event.id on stripe_webhook_event yourself. Stripe POSTs have no tenant ALS.
5129
- // Wrap those writes in runWithMigrationBypass when TENANCY_DRIVER=rls.
5130
- // See https://github.com/EyK-26/strata/blob/main/docs/INTEGRATIONS.md
5131
- void event.data;
5166
+ await applyStripeWebhook(event);
5132
5167
  return jsonResponse({ received: true, id: event.id ?? null, type: event.type ?? null });
5133
5168
  }),
5134
5169
  ),
@@ -5623,12 +5658,14 @@ function writeGeneratedFiles(options) {
5623
5658
  removeIfExists(join2(src, "modules/scim/index.ts"));
5624
5659
  }
5625
5660
  if (layers.extras.billing) {
5661
+ writeText(join2(src, "billing/applyStripeWebhook.ts"), renderApplyStripeWebhook());
5626
5662
  writeText(join2(src, "modules/billing/index.ts"), renderBillingModule(layers));
5627
5663
  } else {
5664
+ removeIfExists(join2(src, "billing/applyStripeWebhook.ts"));
5628
5665
  removeIfExists(join2(src, "modules/billing/index.ts"));
5629
5666
  }
5630
5667
  if (layers.extras.webhooks) {
5631
- writeText(join2(src, "jobs/dispatchOutboundWebhookJob.ts"), renderDispatchOutboundWebhookJob());
5668
+ writeText(join2(src, "jobs/dispatchOutboundWebhookJob.ts"), renderDispatchOutboundWebhookJob(layers));
5632
5669
  writeText(join2(src, "listeners/noteCreatedWebhookListener.ts"), renderNoteCreatedWebhookListener());
5633
5670
  } else {
5634
5671
  removeIfExists(join2(src, "jobs/dispatchOutboundWebhookJob.ts"));
@@ -32,10 +32,11 @@ export default function HomePage() {
32
32
  </table>
33
33
  </div>
34
34
 
35
- <h2>Add your first resource</h2>
35
+ <h2>Starter-only example</h2>
36
36
  <p>
37
- Create a module under <code>src/modules/</code> and return routes from it. The kernel picks
38
- it up on the next boot.
37
+ This empty app already serves <code>GET /api/v1/auth/me</code>. It does not serve{" "}
38
+ <code>GET /api/v1/notes</code>. The module below is an example you add yourself. Create it
39
+ under <code>src/modules/</code> and the kernel picks it up on the next boot.
39
40
  </p>
40
41
 
41
42
  <pre className="card">
@@ -12,9 +12,9 @@
12
12
  "check": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@getstrata/bootstrap": "^1.1.6",
16
- "@getstrata/cli": "^1.1.6",
17
- "@getstrata/core": "^1.1.6",
15
+ "@getstrata/bootstrap": "^1.1.7",
16
+ "@getstrata/cli": "^1.1.7",
17
+ "@getstrata/core": "^1.1.7",
18
18
  "eta": "^4.6.0"
19
19
  },
20
20
  "devDependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/starter",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "Interactive create-strata wizard. Choose each layer; one database engine.",
5
5
  "type": "module",
6
6
  "license": "MIT",