@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
|
-
|
|
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(
|
|
1405
|
+
await runAuthWrite(email, async () => {
|
|
1405
1406
|
await User.create({
|
|
1406
1407
|
name: profile.name,
|
|
1407
|
-
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?.(
|
|
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
|
-
|
|
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.
|
|
3063
|
-
"@getstrata/cli": "^1.1.
|
|
3064
|
-
"@getstrata/core": "^1.1.
|
|
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`)
|
|
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
|
|
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
|
|
5038
|
-
|
|
5039
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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>
|
|
35
|
+
<h2>Starter-only example</h2>
|
|
36
36
|
<p>
|
|
37
|
-
|
|
38
|
-
|
|
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.
|
|
16
|
-
"@getstrata/cli": "^1.1.
|
|
17
|
-
"@getstrata/core": "^1.1.
|
|
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": {
|