minutework 0.1.40 → 0.1.42
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/EXTERNAL_ALPHA.md +17 -1
- package/README.md +21 -1
- package/assets/claude-local/bundle.json +2 -1
- package/assets/claude-local/preambles/base.md +13 -0
- package/assets/claude-local/preambles/mobile.md +17 -0
- package/assets/claude-local/preambles/tenant.md +17 -0
- package/assets/claude-local/preambles/vuilder.md +29 -0
- package/assets/claude-local/skills/README.md +5 -0
- package/assets/claude-local/skills/app-pack-authoring/SKILL.md +15 -0
- package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +25 -9
- package/assets/claude-local/skills/shell-architecture/SKILL.md +15 -0
- package/assets/claude-local/skills/vuilder-public-site-authoring/SKILL.md +10 -4
- package/assets/claude-local/skills/vuilder-workspace-architecture/SKILL.md +78 -0
- package/assets/templates/vuilder-public-site/.env.example +13 -0
- package/assets/templates/vuilder-public-site/README.md +15 -0
- package/assets/templates/vuilder-public-site/eslint.config.mjs +6 -0
- package/assets/templates/vuilder-public-site/next-env.d.ts +4 -0
- package/assets/templates/vuilder-public-site/next.config.mjs +28 -0
- package/assets/templates/vuilder-public-site/package.json +40 -0
- package/assets/templates/vuilder-public-site/postcss.config.mjs +5 -0
- package/assets/templates/vuilder-public-site/src/app/api/onboarding/start/route.ts +19 -0
- package/assets/templates/vuilder-public-site/src/app/blog/[slug]/page.tsx +25 -0
- package/assets/templates/vuilder-public-site/src/app/blog/page.tsx +26 -0
- package/assets/templates/vuilder-public-site/src/app/globals.css +103 -0
- package/assets/templates/vuilder-public-site/src/app/layout.tsx +18 -0
- package/assets/templates/vuilder-public-site/src/app/onboarding/[[...step]]/page.tsx +39 -0
- package/assets/templates/vuilder-public-site/src/app/page.tsx +43 -0
- package/assets/templates/vuilder-public-site/src/lib/env.server.test.ts +47 -0
- package/assets/templates/vuilder-public-site/src/lib/env.server.ts +92 -0
- package/assets/templates/vuilder-public-site/src/lib/platform.server.ts +47 -0
- package/assets/templates/vuilder-public-site/src/lib/public-dj.server.ts +86 -0
- package/assets/templates/vuilder-public-site/src/lib/public-dj.test.ts +8 -0
- package/assets/templates/vuilder-public-site/src/lib/routes.test.ts +13 -0
- package/assets/templates/vuilder-public-site/src/lib/routes.ts +12 -0
- package/assets/templates/vuilder-public-site/template.json +21 -0
- package/assets/templates/vuilder-public-site/test/server-only-stub.ts +1 -0
- package/assets/templates/vuilder-public-site/tools/env/check-dev-env.mjs +109 -0
- package/assets/templates/vuilder-public-site/tools/env/check-dev-env.test.ts +49 -0
- package/assets/templates/vuilder-public-site/tools/template/validate-template.mjs +44 -0
- package/assets/templates/vuilder-public-site/tsconfig.json +23 -0
- package/assets/templates/vuilder-public-site/vitest.config.ts +15 -0
- package/assets/templates/vuilder-shell/.env.example +8 -0
- package/assets/templates/vuilder-shell/.storybook/main.ts +19 -0
- package/assets/templates/vuilder-shell/.storybook/preview.tsx +38 -0
- package/assets/templates/vuilder-shell/README.md +49 -0
- package/assets/templates/vuilder-shell/components.json +21 -0
- package/assets/templates/vuilder-shell/eslint.config.mjs +41 -0
- package/assets/templates/vuilder-shell/next-env.d.ts +6 -0
- package/assets/templates/vuilder-shell/next.config.mjs +33 -0
- package/assets/templates/vuilder-shell/package.json +61 -0
- package/assets/templates/vuilder-shell/postcss.config.mjs +8 -0
- package/assets/templates/vuilder-shell/public/.gitkeep +1 -0
- package/assets/templates/vuilder-shell/src/app/api/auth/accept-shell-session/route.test.ts +105 -0
- package/assets/templates/vuilder-shell/src/app/api/auth/accept-shell-session/route.ts +63 -0
- package/assets/templates/vuilder-shell/src/app/api/auth/logout/route.test.ts +63 -0
- package/assets/templates/vuilder-shell/src/app/api/auth/logout/route.ts +24 -0
- package/assets/templates/vuilder-shell/src/app/api/auth/session/route.test.ts +70 -0
- package/assets/templates/vuilder-shell/src/app/api/auth/session/route.ts +27 -0
- package/assets/templates/vuilder-shell/src/app/app/layout.tsx +17 -0
- package/assets/templates/vuilder-shell/src/app/app/page.tsx +30 -0
- package/assets/templates/vuilder-shell/src/app/blog/[slug]/page.tsx +15 -0
- package/assets/templates/vuilder-shell/src/app/blog/page.tsx +15 -0
- package/assets/templates/vuilder-shell/src/app/docs/[...slug]/page.tsx +15 -0
- package/assets/templates/vuilder-shell/src/app/docs/page.tsx +15 -0
- package/assets/templates/vuilder-shell/src/app/globals.css +70 -0
- package/assets/templates/vuilder-shell/src/app/layout.tsx +69 -0
- package/assets/templates/vuilder-shell/src/app/login/route.test.ts +33 -0
- package/assets/templates/vuilder-shell/src/app/login/route.ts +21 -0
- package/assets/templates/vuilder-shell/src/app/page.tsx +16 -0
- package/assets/templates/vuilder-shell/src/app/pricing/page.tsx +15 -0
- package/assets/templates/vuilder-shell/src/app/providers.tsx +25 -0
- package/assets/templates/vuilder-shell/src/app/robots.ts +21 -0
- package/assets/templates/vuilder-shell/src/app/sitemap.ts +33 -0
- package/assets/templates/vuilder-shell/src/app/w/[workspace_slug]/connect/page.tsx +31 -0
- package/assets/templates/vuilder-shell/src/app/w/[workspace_slug]/page.tsx +54 -0
- package/assets/templates/vuilder-shell/src/components/ui/button.tsx +59 -0
- package/assets/templates/vuilder-shell/src/components/ui/input.tsx +21 -0
- package/assets/templates/vuilder-shell/src/design-system/docs/governance.mdx +26 -0
- package/assets/templates/vuilder-shell/src/design-system/patterns/panel-frame.stories.tsx +48 -0
- package/assets/templates/vuilder-shell/src/design-system/patterns/panel-frame.tsx +26 -0
- package/assets/templates/vuilder-shell/src/design-system/patterns/status-badge.stories.tsx +26 -0
- package/assets/templates/vuilder-shell/src/design-system/patterns/status-badge.tsx +35 -0
- package/assets/templates/vuilder-shell/src/design-system/patterns/theme-mode-toggle.stories.tsx +21 -0
- package/assets/templates/vuilder-shell/src/design-system/patterns/theme-mode-toggle.tsx +75 -0
- package/assets/templates/vuilder-shell/src/design-system/primitives/button.stories.tsx +37 -0
- package/assets/templates/vuilder-shell/src/design-system/primitives/button.ts +1 -0
- package/assets/templates/vuilder-shell/src/design-system/primitives/input.stories.tsx +26 -0
- package/assets/templates/vuilder-shell/src/design-system/primitives/input.ts +1 -0
- package/assets/templates/vuilder-shell/src/design-system/recipes/chrome.ts +28 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/foundation.css +31 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/index.css +3 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/manifest.json +85 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/manifest.ts +87 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/semantic.css +105 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/theme.css +59 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/tokens.stories.tsx +71 -0
- package/assets/templates/vuilder-shell/src/features/dashboard/components/tenant-dashboard.tsx +134 -0
- package/assets/templates/vuilder-shell/src/features/public-shell/components/static-public-page.tsx +58 -0
- package/assets/templates/vuilder-shell/src/features/shell/components/authenticated-app-layout-shell.tsx +84 -0
- package/assets/templates/vuilder-shell/src/features/shell/components/private-app-shell.tsx +22 -0
- package/assets/templates/vuilder-shell/src/features/vuilder-shell/components/vuilder-connect-screen.tsx +89 -0
- package/assets/templates/vuilder-shell/src/features/vuilder-shell/components/vuilder-workspace-screen.tsx +49 -0
- package/assets/templates/vuilder-shell/src/lib/app-routes.test.ts +37 -0
- package/assets/templates/vuilder-shell/src/lib/app-routes.ts +86 -0
- package/assets/templates/vuilder-shell/src/lib/auth-routes.server.test.ts +26 -0
- package/assets/templates/vuilder-shell/src/lib/auth-routes.server.ts +53 -0
- package/assets/templates/vuilder-shell/src/lib/http/same-origin.test.ts +23 -0
- package/assets/templates/vuilder-shell/src/lib/http/same-origin.ts +18 -0
- package/assets/templates/vuilder-shell/src/lib/platform/client.server.test.ts +201 -0
- package/assets/templates/vuilder-shell/src/lib/platform/client.server.ts +540 -0
- package/assets/templates/vuilder-shell/src/lib/platform/contracts.ts +190 -0
- package/assets/templates/vuilder-shell/src/lib/platform/endpoints.server.ts +29 -0
- package/assets/templates/vuilder-shell/src/lib/platform/env.server.ts +82 -0
- package/assets/templates/vuilder-shell/src/lib/platform/route-response.ts +33 -0
- package/assets/templates/vuilder-shell/src/lib/platform/session.server.ts +145 -0
- package/assets/templates/vuilder-shell/src/lib/public-site.test.ts +20 -0
- package/assets/templates/vuilder-shell/src/lib/public-site.ts +48 -0
- package/assets/templates/vuilder-shell/src/lib/theme-config.ts +10 -0
- package/assets/templates/vuilder-shell/src/lib/theme.tsx +159 -0
- package/assets/templates/vuilder-shell/src/lib/utils.ts +6 -0
- package/assets/templates/vuilder-shell/template.json +28 -0
- package/assets/templates/vuilder-shell/template.schema.json +171 -0
- package/assets/templates/vuilder-shell/test/server-only-stub.ts +1 -0
- package/assets/templates/vuilder-shell/tools/design-system/build-token-manifest.mjs +3 -0
- package/assets/templates/vuilder-shell/tools/design-system/check-imports.mjs +9 -0
- package/assets/templates/vuilder-shell/tools/design-system/check-stories.mjs +9 -0
- package/assets/templates/vuilder-shell/tools/design-system/check-values.mjs +9 -0
- package/assets/templates/vuilder-shell/tools/design-system/checks.mjs +238 -0
- package/assets/templates/vuilder-shell/tools/design-system/eslint-plugin-design-system.mjs +184 -0
- package/assets/templates/vuilder-shell/tools/design-system/playwright.config.mjs +34 -0
- package/assets/templates/vuilder-shell/tools/design-system/run-checks.mjs +22 -0
- package/assets/templates/vuilder-shell/tools/design-system/shared.mjs +166 -0
- package/assets/templates/vuilder-shell/tools/design-system/visual.spec.ts +41 -0
- package/assets/templates/vuilder-shell/tools/template/validate-route-contract.mjs +373 -0
- package/assets/templates/vuilder-shell/tools/template/validate-template.mjs +45 -0
- package/assets/templates/vuilder-shell/tools/template/with-public-site-fixture.mjs +45 -0
- package/assets/templates/vuilder-shell/tsconfig.json +42 -0
- package/assets/templates/vuilder-shell/vitest.config.ts +23 -0
- package/dist/auth.js +97 -31
- package/dist/auth.js.map +1 -1
- package/dist/deploy-state.d.ts +1 -0
- package/dist/deploy-state.js.map +1 -1
- package/dist/deploy.js +18 -4
- package/dist/deploy.js.map +1 -1
- package/dist/developer-client.d.ts +2 -1
- package/dist/developer-client.js.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/init-prompt.js +21 -13
- package/dist/init-prompt.js.map +1 -1
- package/dist/init.d.ts +3 -1
- package/dist/init.js +105 -13
- package/dist/init.js.map +1 -1
- package/dist/orchestrator-context.js +17 -5
- package/dist/orchestrator-context.js.map +1 -1
- package/dist/orchestrator-state.d.ts +2 -2
- package/dist/orchestrator-state.js.map +1 -1
- package/dist/publish.js +12 -2
- package/dist/publish.js.map +1 -1
- package/dist/sandbox.js +11 -1
- package/dist/sandbox.js.map +1 -1
- package/dist/state.d.ts +2 -0
- package/dist/state.js +9 -0
- package/dist/state.js.map +1 -1
- package/dist/workspace-assets.d.ts +13 -0
- package/dist/workspace-assets.js +86 -5
- package/dist/workspace-assets.js.map +1 -1
- package/dist/workspace-bootstrap.d.ts +47 -2
- package/dist/workspace-bootstrap.js +45 -1
- package/dist/workspace-bootstrap.js.map +1 -1
- package/package.json +3 -3
- package/vendor/workspace-mcp/context.d.ts +3 -1
- package/vendor/workspace-mcp/context.js +134 -21
- package/vendor/workspace-mcp/context.js.map +1 -1
- package/vendor/workspace-mcp/types.d.ts +72 -7
- package/vendor/workspace-mcp/types.js +8 -4
- package/vendor/workspace-mcp/types.js.map +1 -1
- package/assets/templates/fastapi-sidecar/poetry.lock +0 -757
- package/assets/templates/next-tenant-app/package-lock.json +0 -9682
- package/assets/templates/next-tenant-app/pnpm-lock.yaml +0 -6062
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
const scriptPath = new URL("./check-dev-env.mjs", import.meta.url).pathname;
|
|
5
|
+
const baseEnv: NodeJS.ProcessEnv = { NODE_ENV: "test", PATH: process.env.PATH ?? "" };
|
|
6
|
+
|
|
7
|
+
describe("check-dev-env", () => {
|
|
8
|
+
it("fails fast with the sandbox-link hint when managed env is missing", () => {
|
|
9
|
+
try {
|
|
10
|
+
execFileSync(process.execPath, [scriptPath], {
|
|
11
|
+
encoding: "utf8",
|
|
12
|
+
env: baseEnv,
|
|
13
|
+
});
|
|
14
|
+
throw new Error("expected check-dev-env to fail");
|
|
15
|
+
} catch (error) {
|
|
16
|
+
const stderr =
|
|
17
|
+
typeof error === "object" && error !== null && "stderr" in error
|
|
18
|
+
? String((error as { stderr: string }).stderr)
|
|
19
|
+
: String(error);
|
|
20
|
+
expect(stderr).toContain("vuilder-app local dev is missing managed sandbox env.");
|
|
21
|
+
expect(stderr).toContain("MW_PUBLIC_CONTENT_REF");
|
|
22
|
+
expect(stderr).toContain("MW_VUILDER_ONBOARDING_INTENT_TOKEN");
|
|
23
|
+
expect(stderr).toContain("minutework sandbox create");
|
|
24
|
+
expect(stderr).toContain("minutework sandbox connect");
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("passes when managed env is present in the shell", () => {
|
|
29
|
+
expect(() =>
|
|
30
|
+
execFileSync(process.execPath, [scriptPath], {
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
env: {
|
|
33
|
+
...baseEnv,
|
|
34
|
+
MW_AUTH_BASE_URL: "https://auth.minutework.ai",
|
|
35
|
+
MW_PLATFORM_BASE_URL: "https://platform.minutework.ai",
|
|
36
|
+
MW_PUBLIC_BASE_URL: "http://127.0.0.1:3300",
|
|
37
|
+
MW_PUBLIC_CONTENT_DIGEST: "public-content-digest",
|
|
38
|
+
MW_PUBLIC_CONTENT_REF: "runtime:test-public:content",
|
|
39
|
+
MW_PUBLIC_DJ_BASE_URL: "https://public-dj.minutework.ai",
|
|
40
|
+
MW_PUBLIC_DJ_READ_TOKEN: "public-dj-read-token",
|
|
41
|
+
MW_PUBLIC_RELEASE_CLASS: "ssr_container",
|
|
42
|
+
MW_PUBLIC_SITE_ENV: "preview",
|
|
43
|
+
MW_PUBLIC_SITE_PROPERTY_KEY: "vuilder-app",
|
|
44
|
+
MW_VUILDER_ONBOARDING_INTENT_TOKEN: "onboarding-intent-token",
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
).not.toThrow();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const templateRoot = path.resolve(__dirname, "..", "..");
|
|
7
|
+
const manifestPath = path.join(templateRoot, "template.json");
|
|
8
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
9
|
+
|
|
10
|
+
const requiredFields = [
|
|
11
|
+
"template_id",
|
|
12
|
+
"template_kind",
|
|
13
|
+
"template_profile",
|
|
14
|
+
"template_version",
|
|
15
|
+
"materialize",
|
|
16
|
+
"builder_edit_mode",
|
|
17
|
+
"seed_source",
|
|
18
|
+
"required_bootstrap_steps",
|
|
19
|
+
"example_features",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
for (const field of requiredFields) {
|
|
23
|
+
if (!(field in manifest)) {
|
|
24
|
+
throw new Error(`template.json is missing required field "${field}"`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (manifest.template_id !== "vuilder-public-site") {
|
|
29
|
+
throw new Error("template_id must be vuilder-public-site");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (manifest.template_kind !== "public_site") {
|
|
33
|
+
throw new Error("template_kind must be public_site");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (manifest.template_profile !== "public_dj_cms") {
|
|
37
|
+
throw new Error("template_profile must be public_dj_cms");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (manifest.materialize?.destination !== "app") {
|
|
41
|
+
throw new Error("vuilder-public-site must materialize to app");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log("template.json is valid");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": false,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./src/*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { defineConfig } from "vitest/config";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
environment: "node",
|
|
7
|
+
globals: true,
|
|
8
|
+
},
|
|
9
|
+
resolve: {
|
|
10
|
+
alias: {
|
|
11
|
+
"@": new URL("./src", import.meta.url).pathname,
|
|
12
|
+
"server-only": path.resolve(__dirname, "test/server-only-stub.ts"),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
NEXT_PUBLIC_MW_APP_ID=vuilder.vertical
|
|
2
|
+
MW_TEMPLATE_APP_NAME=Vuilder Shell
|
|
3
|
+
MW_PUBLIC_BASE_URL=http://127.0.0.1:3301
|
|
4
|
+
MW_AUTH_BASE_URL=http://127.0.0.1:3400
|
|
5
|
+
MW_PUBLIC_SITE_PROPERTY_KEY=vuilder-shell
|
|
6
|
+
MW_PUBLIC_SITE_ENV=preview
|
|
7
|
+
MW_PLATFORM_BASE_URL=http://127.0.0.1:8000
|
|
8
|
+
MW_PLATFORM_FETCH_TIMEOUT_MS=15000
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineMain } from "@storybook/nextjs-vite/node";
|
|
2
|
+
|
|
3
|
+
export default defineMain({
|
|
4
|
+
stories: [
|
|
5
|
+
"../src/design-system/**/*.stories.@(ts|tsx|mdx)",
|
|
6
|
+
"../src/design-system/docs/**/*.mdx",
|
|
7
|
+
],
|
|
8
|
+
addons: ["@storybook/addon-docs", "@storybook/addon-a11y"],
|
|
9
|
+
framework: {
|
|
10
|
+
name: "@storybook/nextjs-vite",
|
|
11
|
+
options: {
|
|
12
|
+
nextConfigPath: "../next.config.mjs",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
staticDirs: ["../public"],
|
|
16
|
+
docs: {
|
|
17
|
+
defaultName: "Overview",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { definePreview } from "@storybook/nextjs-vite";
|
|
2
|
+
|
|
3
|
+
import "../src/app/globals.css";
|
|
4
|
+
import { ThemeProvider } from "../src/lib/theme";
|
|
5
|
+
|
|
6
|
+
export default definePreview({
|
|
7
|
+
decorators: [
|
|
8
|
+
(Story) => (
|
|
9
|
+
<ThemeProvider
|
|
10
|
+
attribute="class"
|
|
11
|
+
defaultTheme="light"
|
|
12
|
+
enableSystem
|
|
13
|
+
disableTransitionOnChange
|
|
14
|
+
>
|
|
15
|
+
<div className="min-h-screen bg-background p-6 text-foreground">
|
|
16
|
+
<Story />
|
|
17
|
+
</div>
|
|
18
|
+
</ThemeProvider>
|
|
19
|
+
),
|
|
20
|
+
],
|
|
21
|
+
parameters: {
|
|
22
|
+
layout: "centered",
|
|
23
|
+
nextjs: {
|
|
24
|
+
appDirectory: true,
|
|
25
|
+
},
|
|
26
|
+
controls: {
|
|
27
|
+
matchers: {
|
|
28
|
+
color: /(background|color)$/i,
|
|
29
|
+
date: /Date$/i,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
options: {
|
|
33
|
+
storySort: {
|
|
34
|
+
order: ["Design System"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Vuilder Shell Template
|
|
2
|
+
|
|
3
|
+
`vuilder-shell` is the branded customer tenant shell for Vuilder-owned verticals.
|
|
4
|
+
It is generated next to `vuilder-app` in a Vuilder workspace.
|
|
5
|
+
|
|
6
|
+
The shell opens after the customer has signed in through central SSO and the
|
|
7
|
+
platform has completed the `customer_vertical_signup` intent. The platform owns
|
|
8
|
+
identity, tenant runtime provisioning, and app-pack install authority. This
|
|
9
|
+
template owns customer-visible navigation, workspace status, and vertical
|
|
10
|
+
product UI.
|
|
11
|
+
|
|
12
|
+
## Route Shape
|
|
13
|
+
|
|
14
|
+
- public route at `/`
|
|
15
|
+
- platform member auth handoff at `/login`, which redirects to central SSO with
|
|
16
|
+
a shell return URL
|
|
17
|
+
- branded tenant setup/status route at `/w/[workspace_slug]/connect`
|
|
18
|
+
- branded tenant workspace route at `/w/[workspace_slug]`
|
|
19
|
+
- compatibility authenticated app route at `/app`
|
|
20
|
+
|
|
21
|
+
## Auth Profile
|
|
22
|
+
|
|
23
|
+
This template uses central platform SSO plus a tenant-bound shell session minted
|
|
24
|
+
by the platform after membership is established. `/login` sets a short-lived
|
|
25
|
+
host-only shell handoff state cookie before redirecting to SSO. SSO returns to
|
|
26
|
+
`/api/auth/accept-shell-session` with a short-lived one-use code and the echoed
|
|
27
|
+
state; the shell validates the state, exchanges the code server-side, sets a
|
|
28
|
+
host-only `mw_vuilder_shell_session` cookie, and redirects to the clean
|
|
29
|
+
workspace URL. Raw platform session and CSRF cookies stay on the SSO origin; the
|
|
30
|
+
shell never builds Django session or CSRF headers. The shell does not render a
|
|
31
|
+
password form and does not expose a local credential-login BFF; `/login` is only
|
|
32
|
+
an SSO redirect handoff. Local `/api/auth/session` and `/api/auth/logout` are
|
|
33
|
+
thin shell-token helpers; logout best-effort revokes the tenant-bound shell
|
|
34
|
+
token before clearing cookies. Browser React never stores platform credentials,
|
|
35
|
+
runtime keys, provisioning secrets, package refs, or install coordinates.
|
|
36
|
+
|
|
37
|
+
For production handoff from `auth.*` to a branded shell on a sibling subdomain,
|
|
38
|
+
configure `MW_AUTH_BASE_URL` and `MW_PUBLIC_BASE_URL`. Do not configure a shared
|
|
39
|
+
parent-domain shell cookie; the shell session cookie is host-only on the branded
|
|
40
|
+
shell origin.
|
|
41
|
+
|
|
42
|
+
`vuilder-shell` is not a `tenant-app` and must not use `@minutework/web-auth`
|
|
43
|
+
or same-origin `/_mw` customer routes.
|
|
44
|
+
|
|
45
|
+
## Vuilder Boundary
|
|
46
|
+
|
|
47
|
+
Vuilders may customize UI, route layout, visual design, tenant settings,
|
|
48
|
+
runtime-backed data views, agents, workflows, and branding. They must not move
|
|
49
|
+
provisioning decisions into React state or browser-submitted payloads.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/design-system",
|
|
15
|
+
"utils": "@/lib/utils",
|
|
16
|
+
"ui": "@/design-system/primitives",
|
|
17
|
+
"lib": "@/lib",
|
|
18
|
+
"hooks": "@/hooks"
|
|
19
|
+
},
|
|
20
|
+
"iconLibrary": "lucide"
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
|
2
|
+
import nextTypeScript from "eslint-config-next/typescript";
|
|
3
|
+
|
|
4
|
+
import designSystemPlugin from "./tools/design-system/eslint-plugin-design-system.mjs";
|
|
5
|
+
|
|
6
|
+
const eslintConfig = [
|
|
7
|
+
...nextCoreWebVitals,
|
|
8
|
+
...nextTypeScript,
|
|
9
|
+
{
|
|
10
|
+
ignores: [
|
|
11
|
+
".next/**",
|
|
12
|
+
"node_modules/**",
|
|
13
|
+
"next-env.d.ts",
|
|
14
|
+
"storybook-static/**",
|
|
15
|
+
"src/design-system/tokens/manifest.ts",
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
files: ["src/app/**/*.{ts,tsx}", "src/features/**/*.{ts,tsx}"],
|
|
20
|
+
plugins: {
|
|
21
|
+
"design-system": designSystemPlugin,
|
|
22
|
+
},
|
|
23
|
+
rules: {
|
|
24
|
+
"design-system/no-design-system-bypass-imports": "error",
|
|
25
|
+
"design-system/no-cva-outside-design-system": "error",
|
|
26
|
+
"design-system/no-raw-design-values": "error",
|
|
27
|
+
"design-system/no-bespoke-visual-stacks-in-features": "error",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
files: ["src/design-system/**/*.{ts,tsx}"],
|
|
32
|
+
plugins: {
|
|
33
|
+
"design-system": designSystemPlugin,
|
|
34
|
+
},
|
|
35
|
+
rules: {
|
|
36
|
+
"design-system/no-raw-design-values": "error",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export default eslintConfig;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
function resolveTurbopackRoot(startDirectory) {
|
|
8
|
+
let directory = startDirectory;
|
|
9
|
+
|
|
10
|
+
while (true) {
|
|
11
|
+
if (existsSync(path.join(directory, "pnpm-workspace.yaml"))) {
|
|
12
|
+
return directory;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parent = path.dirname(directory);
|
|
16
|
+
if (parent === directory) {
|
|
17
|
+
return startDirectory;
|
|
18
|
+
}
|
|
19
|
+
directory = parent;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nextConfig = {
|
|
24
|
+
reactStrictMode: true,
|
|
25
|
+
typedRoutes: true,
|
|
26
|
+
// Prefer the nearest workspace root so scaffolded tenant-app packages inside a
|
|
27
|
+
// monorepo and the in-repo template both resolve dependencies correctly.
|
|
28
|
+
turbopack: {
|
|
29
|
+
root: resolveTurbopackRoot(__dirname),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default nextConfig;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vuilder-shell",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"packageManager": "pnpm@9.0.0",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.9.0"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "next dev --hostname 127.0.0.1 --port 3301",
|
|
11
|
+
"build": "next build",
|
|
12
|
+
"build:validate": "node tools/template/with-public-site-fixture.mjs next build",
|
|
13
|
+
"start": "next start -p 3301",
|
|
14
|
+
"lint": "eslint .",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"typegen": "next typegen",
|
|
17
|
+
"template:validate": "node tools/template/validate-template.mjs",
|
|
18
|
+
"template:route-contract": "node tools/template/validate-route-contract.mjs",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"validate": "pnpm template:validate && pnpm template:route-contract && pnpm typegen && pnpm design-system:tokens && pnpm design-system:check && pnpm typecheck && pnpm lint && pnpm test && pnpm build:validate && pnpm build-storybook:validate",
|
|
21
|
+
"design-system:tokens": "node tools/design-system/build-token-manifest.mjs",
|
|
22
|
+
"design-system:check": "node tools/design-system/run-checks.mjs",
|
|
23
|
+
"design-system:check:staged": "node tools/design-system/run-checks.mjs --staged",
|
|
24
|
+
"storybook": "storybook dev -p 6006",
|
|
25
|
+
"build-storybook": "storybook build",
|
|
26
|
+
"build-storybook:validate": "node tools/template/with-public-site-fixture.mjs storybook build",
|
|
27
|
+
"design-system:visual": "playwright test -c tools/design-system/playwright.config.mjs"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
31
|
+
"class-variance-authority": "^0.7.1",
|
|
32
|
+
"clsx": "^2.1.1",
|
|
33
|
+
"lucide-react": "^0.564.0",
|
|
34
|
+
"next": "^16.2.0",
|
|
35
|
+
"react": "^19.2.0",
|
|
36
|
+
"react-dom": "^19.2.0",
|
|
37
|
+
"server-only": "^0.0.1",
|
|
38
|
+
"tailwind-merge": "^3.3.1",
|
|
39
|
+
"tw-animate-css": "^1.3.3",
|
|
40
|
+
"zod": "^3.24.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@playwright/test": "^1.58.2",
|
|
44
|
+
"@storybook/addon-a11y": "^10.2.19",
|
|
45
|
+
"@storybook/addon-docs": "^10.2.19",
|
|
46
|
+
"@storybook/nextjs-vite": "^10.2.19",
|
|
47
|
+
"@tailwindcss/postcss": "^4.2.0",
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"@types/react": "^19.0.0",
|
|
50
|
+
"@types/react-dom": "^19.0.0",
|
|
51
|
+
"ajv": "^8.17.1",
|
|
52
|
+
"eslint": "^9.39.1",
|
|
53
|
+
"eslint-config-next": "^16.2.0",
|
|
54
|
+
"http-server": "^14.1.1",
|
|
55
|
+
"postcss": "^8.5.6",
|
|
56
|
+
"storybook": "^10.2.19",
|
|
57
|
+
"tailwindcss": "^4.2.0",
|
|
58
|
+
"typescript": "^5.6.0",
|
|
59
|
+
"vitest": "^3.2.4"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
applyVuilderShellSessionToResponse: vi.fn(),
|
|
5
|
+
clearVuilderShellHandoffStateFromResponse: vi.fn(),
|
|
6
|
+
exchangeVuilderShellSessionHandoffCode: vi.fn(),
|
|
7
|
+
readVuilderShellHandoffState: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("@/lib/platform/client.server", () => ({
|
|
11
|
+
exchangeVuilderShellSessionHandoffCode:
|
|
12
|
+
mocks.exchangeVuilderShellSessionHandoffCode,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("@/lib/platform/session.server", () => ({
|
|
16
|
+
applyVuilderShellSessionToResponse: mocks.applyVuilderShellSessionToResponse,
|
|
17
|
+
clearVuilderShellHandoffStateFromResponse:
|
|
18
|
+
mocks.clearVuilderShellHandoffStateFromResponse,
|
|
19
|
+
readVuilderShellHandoffState: mocks.readVuilderShellHandoffState,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
import { GET } from "./route";
|
|
23
|
+
|
|
24
|
+
describe("GET /api/auth/accept-shell-session", () => {
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("exchanges the handoff code, sets the shell token, and redirects locally", async () => {
|
|
30
|
+
mocks.readVuilderShellHandoffState.mockResolvedValue("state-1");
|
|
31
|
+
mocks.exchangeVuilderShellSessionHandoffCode.mockResolvedValue({
|
|
32
|
+
data: {
|
|
33
|
+
expires_at: "2026-06-09T12:00:00Z",
|
|
34
|
+
shell_session_token: "shell-token-1",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const response = await GET(
|
|
39
|
+
new Request(
|
|
40
|
+
"https://shell.example.com/api/auth/accept-shell-session?code=code-1&state=state-1&return_to=%2Fw%2Ffleet-alpha%2Fconnect",
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(response.status).toBe(307);
|
|
45
|
+
expect(response.headers.get("location")).toBe(
|
|
46
|
+
"https://shell.example.com/w/fleet-alpha/connect",
|
|
47
|
+
);
|
|
48
|
+
expect(response.headers.get("cache-control")).toBe("no-store");
|
|
49
|
+
expect(response.headers.get("referrer-policy")).toBe("no-referrer");
|
|
50
|
+
expect(mocks.exchangeVuilderShellSessionHandoffCode).toHaveBeenCalledWith(
|
|
51
|
+
"code-1",
|
|
52
|
+
"state-1",
|
|
53
|
+
);
|
|
54
|
+
expect(mocks.applyVuilderShellSessionToResponse).toHaveBeenCalledWith(
|
|
55
|
+
expect.any(Response),
|
|
56
|
+
"shell-token-1",
|
|
57
|
+
"2026-06-09T12:00:00Z",
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("rejects external return paths", async () => {
|
|
62
|
+
mocks.readVuilderShellHandoffState.mockResolvedValue("state-1");
|
|
63
|
+
mocks.exchangeVuilderShellSessionHandoffCode.mockResolvedValue({
|
|
64
|
+
data: {
|
|
65
|
+
expires_at: "2026-06-09T12:00:00Z",
|
|
66
|
+
shell_session_token: "shell-token-1",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const response = await GET(
|
|
71
|
+
new Request(
|
|
72
|
+
"https://shell.example.com/api/auth/accept-shell-session?code=code-1&state=state-1&return_to=https%3A%2F%2Fevil.example.com%2F",
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(response.headers.get("location")).toBe("https://shell.example.com/app");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("fails closed when the shell state does not match", async () => {
|
|
80
|
+
mocks.readVuilderShellHandoffState.mockResolvedValue("state-1");
|
|
81
|
+
|
|
82
|
+
const response = await GET(
|
|
83
|
+
new Request(
|
|
84
|
+
"https://shell.example.com/api/auth/accept-shell-session?code=code-1&state=attacker-state&return_to=%2Fw%2Ffleet-alpha%2Fconnect",
|
|
85
|
+
{
|
|
86
|
+
headers: {
|
|
87
|
+
cookie: "mw_vuilder_shell_session=existing-shell-token",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(response.headers.get("location")).toBe(
|
|
94
|
+
"https://shell.example.com/w/fleet-alpha/connect",
|
|
95
|
+
);
|
|
96
|
+
expect(mocks.exchangeVuilderShellSessionHandoffCode).not.toHaveBeenCalled();
|
|
97
|
+
expect(mocks.applyVuilderShellSessionToResponse).not.toHaveBeenCalled();
|
|
98
|
+
expect(mocks.clearVuilderShellHandoffStateFromResponse).toHaveBeenCalledWith(
|
|
99
|
+
expect.any(Response),
|
|
100
|
+
);
|
|
101
|
+
expect(response.headers.get("set-cookie") ?? "").not.toContain(
|
|
102
|
+
"mw_vuilder_shell_session",
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
import { appRoutes } from "@/lib/app-routes";
|
|
4
|
+
import { exchangeVuilderShellSessionHandoffCode } from "@/lib/platform/client.server";
|
|
5
|
+
import {
|
|
6
|
+
applyVuilderShellSessionToResponse,
|
|
7
|
+
clearVuilderShellHandoffStateFromResponse,
|
|
8
|
+
readVuilderShellHandoffState,
|
|
9
|
+
} from "@/lib/platform/session.server";
|
|
10
|
+
|
|
11
|
+
function normalizeLocalReturnTo(value?: string | null) {
|
|
12
|
+
const candidate = String(value ?? "").trim();
|
|
13
|
+
if (!candidate || !candidate.startsWith("/") || candidate.startsWith("//")) {
|
|
14
|
+
return appRoutes.appHome;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const parsed = new URL(candidate, "http://localhost");
|
|
19
|
+
if (parsed.origin !== "http://localhost" || !parsed.pathname.startsWith("/")) {
|
|
20
|
+
return appRoutes.appHome;
|
|
21
|
+
}
|
|
22
|
+
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
23
|
+
} catch {
|
|
24
|
+
return appRoutes.appHome;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function GET(request: Request) {
|
|
29
|
+
const url = new URL(request.url);
|
|
30
|
+
const shellSessionHandoffCode = url.searchParams.get("code")?.trim() ?? "";
|
|
31
|
+
const shellHandoffState = url.searchParams.get("state")?.trim() ?? "";
|
|
32
|
+
const expectedShellHandoffState = await readVuilderShellHandoffState();
|
|
33
|
+
const returnTo = normalizeLocalReturnTo(url.searchParams.get("return_to"));
|
|
34
|
+
const response = NextResponse.redirect(new URL(returnTo, url.origin));
|
|
35
|
+
response.headers.set("Cache-Control", "no-store");
|
|
36
|
+
response.headers.set("Referrer-Policy", "no-referrer");
|
|
37
|
+
clearVuilderShellHandoffStateFromResponse(response);
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
!shellSessionHandoffCode ||
|
|
41
|
+
!shellHandoffState ||
|
|
42
|
+
!expectedShellHandoffState ||
|
|
43
|
+
shellHandoffState !== expectedShellHandoffState
|
|
44
|
+
) {
|
|
45
|
+
return response;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const result = await exchangeVuilderShellSessionHandoffCode(
|
|
50
|
+
shellSessionHandoffCode,
|
|
51
|
+
expectedShellHandoffState,
|
|
52
|
+
);
|
|
53
|
+
applyVuilderShellSessionToResponse(
|
|
54
|
+
response,
|
|
55
|
+
result.data.shell_session_token,
|
|
56
|
+
result.data.expires_at,
|
|
57
|
+
);
|
|
58
|
+
} catch {
|
|
59
|
+
return response;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return response;
|
|
63
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
clearPlatformSessionFromResponse: vi.fn(),
|
|
5
|
+
readPlatformAuthState: vi.fn(),
|
|
6
|
+
revokeVuilderShellSession: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("@/lib/platform/client.server", () => ({
|
|
10
|
+
revokeVuilderShellSession: mocks.revokeVuilderShellSession,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@/lib/platform/session.server", () => ({
|
|
14
|
+
clearPlatformSessionFromResponse: mocks.clearPlatformSessionFromResponse,
|
|
15
|
+
readPlatformAuthState: mocks.readPlatformAuthState,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { POST } from "./route";
|
|
19
|
+
|
|
20
|
+
describe("POST /api/auth/logout", () => {
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("rejects cross-origin submissions before clearing shared cookies", async () => {
|
|
26
|
+
const response = await POST(
|
|
27
|
+
new Request("https://shell.example.com/api/auth/logout", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { origin: "https://evil.example.com" },
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(response.status).toBe(403);
|
|
34
|
+
expect(mocks.readPlatformAuthState).not.toHaveBeenCalled();
|
|
35
|
+
expect(mocks.revokeVuilderShellSession).not.toHaveBeenCalled();
|
|
36
|
+
expect(mocks.clearPlatformSessionFromResponse).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("revokes and clears shell session cookies for same-origin submissions", async () => {
|
|
40
|
+
const authState = { shellSessionToken: "shell-token-1" };
|
|
41
|
+
mocks.readPlatformAuthState.mockResolvedValue(authState);
|
|
42
|
+
mocks.revokeVuilderShellSession.mockResolvedValue(undefined);
|
|
43
|
+
|
|
44
|
+
const response = await POST(
|
|
45
|
+
new Request("https://shell.example.com/api/auth/logout", {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { origin: "https://shell.example.com" },
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(response.status).toBe(204);
|
|
52
|
+
expect(response.headers.get("Cache-Control")).toBe("no-store");
|
|
53
|
+
expect(mocks.revokeVuilderShellSession).toHaveBeenCalledWith(authState);
|
|
54
|
+
expect(mocks.clearPlatformSessionFromResponse).toHaveBeenCalledWith(
|
|
55
|
+
expect.any(Response),
|
|
56
|
+
);
|
|
57
|
+
expect(
|
|
58
|
+
mocks.revokeVuilderShellSession.mock.invocationCallOrder[0],
|
|
59
|
+
).toBeLessThan(
|
|
60
|
+
mocks.clearPlatformSessionFromResponse.mock.invocationCallOrder[0],
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|