Skip to content

Commit a74c401

Browse files
committed
Merge remote-tracking branch 'origin/dev' into custom-dashboards-and-unified-ai-no-playground
2 parents d06ee3f + f016cd8 commit a74c401

49 files changed

Lines changed: 1158 additions & 70 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/npm-publish.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@ jobs:
4343

4444
- name: Publish packages
4545
# pnpm publish skips versions that already exist on npm by default
46-
run: pnpm publish -r --no-git-checks
46+
run: pnpm publish -r --no-git-checks --access public
4747
env:
4848
NPM_CONFIG_PROVENANCE: true

README.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,7 @@ To install Stack Auth in your Next.js project (for React, JavaScript, or other f
7575

7676
1. Run Stack Auth's installation wizard with the following command:
7777
```bash
78-
npx @stackframe/init-stack@latest
79-
```
80-
81-
If you prefer not to open a browser during setup (useful for CI/CD environments or restricted environments):
82-
```bash
83-
npx @stackframe/init-stack@latest --no-browser
78+
npx @stackframe/stack-cli@latest init
8479
```
8580

8681
2. Then, create an account on the [Stack Auth dashboard](https://app.stack-auth.com/projects), create a new project with an API key, and copy its environment variables into the .env.local file of your Next.js project:

apps/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackframe/backend",
3-
"version": "2.8.73",
3+
"version": "2.8.74",
44
"repository": "https://github.com/stack-auth/stack-auth",
55
"private": true,
66
"type": "module",
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { handleApiRequest } from "@/route-handlers/smart-route-handler";
2+
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
3+
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
4+
import { NextRequest } from "next/server";
5+
6+
const OPENROUTER_BASE_URL = "https://openrouter.ai/api";
7+
const OPENROUTER_MODEL = "anthropic/claude-sonnet-4.6";
8+
9+
function sanitizeBody(raw: ArrayBuffer): Uint8Array {
10+
const text = new TextDecoder().decode(raw);
11+
let parsed;
12+
try {
13+
parsed = JSON.parse(text);
14+
} catch {
15+
throw new StatusError(400, "Request body must be valid JSON");
16+
}
17+
18+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
19+
throw new StatusError(400, "Request body must be a JSON object");
20+
}
21+
22+
parsed.model = OPENROUTER_MODEL;
23+
24+
// OpenRouter limits metadata.user_id to 128 characters
25+
if (parsed.metadata?.user_id && parsed.metadata.user_id.length > 128) {
26+
parsed.metadata.user_id = parsed.metadata.user_id.slice(0, 128);
27+
}
28+
29+
return new TextEncoder().encode(JSON.stringify(parsed));
30+
}
31+
32+
async function proxyToOpenRouter(req: NextRequest, options: { params: Promise<{ path?: string[] }> }) {
33+
const apiKey = getEnvVariable("STACK_OPENROUTER_API_KEY");
34+
const params = await options.params;
35+
const subpath = params.path?.join("/") ?? "";
36+
const targetUrl = `${OPENROUTER_BASE_URL}/${subpath}${req.nextUrl.search}`;
37+
38+
const headers: Record<string, string> = {
39+
"Authorization": `Bearer ${apiKey}`,
40+
"anthropic-version": "2023-06-01",
41+
};
42+
43+
const contentType = req.headers.get("Content-Type");
44+
if (contentType) {
45+
headers["Content-Type"] = contentType;
46+
}
47+
48+
const body = req.method !== "GET" && req.method !== "HEAD"
49+
? Buffer.from(sanitizeBody(await req.arrayBuffer()))
50+
: undefined;
51+
52+
const response = await fetch(targetUrl, {
53+
method: req.method,
54+
headers,
55+
body,
56+
});
57+
58+
return new Response(response.body, {
59+
status: response.status,
60+
headers: {
61+
"Content-Type": response.headers.get("Content-Type") ?? "application/json",
62+
"Cache-Control": "no-cache",
63+
},
64+
});
65+
}
66+
67+
export const GET = handleApiRequest(proxyToOpenRouter);
68+
export const POST = handleApiRequest(proxyToOpenRouter);

apps/backend/src/app/api/latest/internal/config/override/[level]/route.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { globalPrismaClient, rawQuery } from "@/prisma-client";
1818
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
1919
import { branchConfigSchema, environmentConfigSchema, getConfigOverrideErrors, migrateConfigOverride, projectConfigSchema } from "@stackframe/stack-shared/dist/config/schema";
2020
import { adaptSchema, adminAuthTypeSchema, branchConfigSourceSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
21-
import { StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors";
21+
import { StackAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors";
2222
import * as yup from "yup";
2323
type BranchConfigSourceApi = yup.InferType<typeof branchConfigSourceSchema>;
2424

@@ -194,7 +194,7 @@ async function warnOnValidationFailure(
194194
captureError("config-override-validation-warning", `Config override validation warning for project ${options.projectId} (this may not be a logic error, but rather a client/implementation issue — e.g. dot notation into non-existent record entries): ${validationResult.error}`);
195195
}
196196
} catch (e) {
197-
captureError("config-override-validation-check-failed", e);
197+
captureError("config-override-validation-check-failed", new StackAssertionError("Config override validation check failed. This may be really bad! Make sure to check the error and the config.", { cause: e, options, levelConfig }));
198198
}
199199
}
200200

apps/dashboard/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackframe/dashboard",
3-
"version": "2.8.73",
3+
"version": "2.8.74",
44
"repository": "https://github.com/stack-auth/stack-auth",
55
"private": true,
66
"scripts": {

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ export default function SetupPage(props: { toMetrics: () => void }) {
6363
</Typography>
6464
<CodeBlock
6565
language="bash"
66-
content={`npx @stackframe/init-stack@latest`}
66+
content={`npx @stackframe/stack-cli@latest init`}
6767
customRender={
6868
<div className="p-4 font-mono text-sm">
69-
<span className={commandClasses}>pnpx</span> <span className={nameClasses}>@stackframe/init-stack@latest</span>
69+
<span className={commandClasses}>pnpx</span> <span className={nameClasses}>@stackframe/stack-cli@latest</span> init
7070
</div>
7171
}
7272
title="Terminal"

apps/dev-launchpad/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackframe/dev-launchpad",
3-
"version": "2.8.73",
3+
"version": "2.8.74",
44
"repository": "https://github.com/stack-auth/stack-auth",
55
"private": true,
66
"scripts": {

apps/e2e/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackframe/e2e-tests",
3-
"version": "2.8.73",
3+
"version": "2.8.74",
44
"repository": "https://github.com/stack-auth/stack-auth",
55
"private": true,
66
"type": "module",

apps/e2e/tests/general/cli.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,4 +345,119 @@ describe("Stack CLI", () => {
345345
expect(exitCode).toBe(1);
346346
expect(stderr).toContain("plain `config` object");
347347
});
348+
349+
// --- init command tests ---
350+
351+
it("init create writes stack.config.ts with selected apps", async ({ expect }) => {
352+
const initDir = path.join(tmpDir, "init-create");
353+
fs.mkdirSync(initDir, { recursive: true });
354+
355+
const { stdout, exitCode } = await runCli([
356+
"init", "--mode", "create", "--apps", "authentication,teams", "--output-dir", initDir,
357+
]);
358+
expect(exitCode).toBe(0);
359+
expect(stdout).toContain("Config file written to");
360+
361+
const content = fs.readFileSync(path.join(initDir, "stack.config.ts"), "utf-8");
362+
expect(content).toContain("export const config");
363+
const configMatch = content.match(/export const config = (.+);/s);
364+
expect(configMatch).toBeTruthy();
365+
const parsed = JSON.parse(configMatch![1]);
366+
expect(parsed.apps.installed.authentication).toEqual({ enabled: true });
367+
expect(parsed.apps.installed.teams).toEqual({ enabled: true });
368+
});
369+
370+
it("init create with single app", async ({ expect }) => {
371+
const initDir = path.join(tmpDir, "init-create-single");
372+
fs.mkdirSync(initDir, { recursive: true });
373+
374+
const { stdout, exitCode } = await runCli([
375+
"init", "--mode", "create", "--apps", "authentication", "--output-dir", initDir,
376+
]);
377+
expect(exitCode).toBe(0);
378+
expect(stdout).toContain("Config file written to");
379+
380+
const content = fs.readFileSync(path.join(initDir, "stack.config.ts"), "utf-8");
381+
const configMatch = content.match(/export const config = (.+);/s);
382+
const parsed = JSON.parse(configMatch![1]);
383+
expect(Object.keys(parsed.apps.installed)).toEqual(["authentication"]);
384+
});
385+
386+
it("init link-config with valid path", async ({ expect }) => {
387+
// Create a dummy config file to link to
388+
const dummyConfig = path.join(tmpDir, "dummy-stack.config.ts");
389+
fs.writeFileSync(dummyConfig, "export const config = {};\n");
390+
391+
const { stdout, exitCode } = await runCli([
392+
"init", "--mode", "link-config", "--config-file", dummyConfig,
393+
]);
394+
expect(exitCode).toBe(0);
395+
expect(stdout).toContain("Linked to config file");
396+
expect(stdout).toContain(dummyConfig);
397+
});
398+
399+
it("init link-config with invalid path fails", async ({ expect }) => {
400+
const { stderr, exitCode } = await runCli([
401+
"init", "--mode", "link-config", "--config-file", "/nonexistent/stack.config.ts",
402+
]);
403+
expect(exitCode).toBe(1);
404+
expect(stderr).toContain("File not found");
405+
});
406+
407+
it("init link-cloud creates .env with API keys", async ({ expect }) => {
408+
expect(createdProjectId).toBeDefined();
409+
410+
const initDir = path.join(tmpDir, "init-cloud");
411+
fs.mkdirSync(initDir, { recursive: true });
412+
413+
const { stdout, exitCode } = await runCli([
414+
"init", "--mode", "link-cloud", "--select-project-id", createdProjectId, "--output-dir", initDir,
415+
]);
416+
expect(exitCode).toBe(0);
417+
expect(stdout).toContain("Created .env with Stack Auth keys");
418+
419+
const envContent = fs.readFileSync(path.join(initDir, ".env"), "utf-8");
420+
expect(envContent).toContain("# Stack Auth");
421+
expect(envContent).toContain(`NEXT_PUBLIC_STACK_PROJECT_ID=${createdProjectId}`);
422+
expect(envContent).toContain("NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=");
423+
expect(envContent).toContain("STACK_SECRET_SERVER_KEY=");
424+
});
425+
426+
it("init link-cloud appends to existing .env", async ({ expect }) => {
427+
expect(createdProjectId).toBeDefined();
428+
429+
const initDir = path.join(tmpDir, "init-cloud-append");
430+
fs.mkdirSync(initDir, { recursive: true });
431+
fs.writeFileSync(path.join(initDir, ".env"), "EXISTING_VAR=hello\n");
432+
433+
const { stdout, exitCode } = await runCli([
434+
"init", "--mode", "link-cloud", "--select-project-id", createdProjectId, "--output-dir", initDir,
435+
]);
436+
expect(exitCode).toBe(0);
437+
expect(stdout).toContain("Appended Stack Auth keys to .env");
438+
439+
const envContent = fs.readFileSync(path.join(initDir, ".env"), "utf-8");
440+
expect(envContent).toContain("EXISTING_VAR=hello");
441+
expect(envContent).toContain("# Stack Auth");
442+
expect(envContent).toContain(`NEXT_PUBLIC_STACK_PROJECT_ID=${createdProjectId}`);
443+
});
444+
445+
it("init link-cloud fails with invalid project ID", async ({ expect }) => {
446+
const { stderr, exitCode } = await runCli([
447+
"init", "--mode", "link-cloud", "--select-project-id", "nonexistent-project-id",
448+
]);
449+
expect(exitCode).toBe(1);
450+
expect(stderr).toContain("not found");
451+
});
452+
453+
it("init outputs setup instructions", async ({ expect }) => {
454+
const initDir = path.join(tmpDir, "init-instructions");
455+
fs.mkdirSync(initDir, { recursive: true });
456+
457+
const { stdout, exitCode } = await runCli([
458+
"init", "--mode", "create", "--apps", "authentication", "--output-dir", initDir,
459+
]);
460+
expect(exitCode).toBe(0);
461+
expect(stdout).toContain("STACK AUTH SETUP INSTRUCTIONS");
462+
});
348463
});

0 commit comments

Comments
 (0)