minutework 0.1.40 → 0.1.41
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/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 +11 -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 +39 -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.ts +31 -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/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 +13 -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 +66 -14
- 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 +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 +103 -12
- 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/state.d.ts +2 -0
- package/dist/state.js +9 -0
- package/dist/state.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,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
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
import { hasTrustedBrowserOrigin } from "@/lib/http/same-origin";
|
|
4
|
+
import { revokeVuilderShellSession } from "@/lib/platform/client.server";
|
|
5
|
+
import {
|
|
6
|
+
clearPlatformSessionFromResponse,
|
|
7
|
+
readPlatformAuthState,
|
|
8
|
+
} from "@/lib/platform/session.server";
|
|
9
|
+
|
|
10
|
+
export async function POST(request: Request) {
|
|
11
|
+
if (!hasTrustedBrowserOrigin(request)) {
|
|
12
|
+
return NextResponse.json(
|
|
13
|
+
{ detail: "Same-origin browser submission required." },
|
|
14
|
+
{ status: 403 },
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const authState = await readPlatformAuthState();
|
|
19
|
+
await revokeVuilderShellSession(authState).catch(() => null);
|
|
20
|
+
const response = new NextResponse(null, { status: 204 });
|
|
21
|
+
response.headers.set("Cache-Control", "no-store");
|
|
22
|
+
clearPlatformSessionFromResponse(response);
|
|
23
|
+
return response;
|
|
24
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
loadCurrentSession: vi.fn(),
|
|
5
|
+
readPlatformAuthState: vi.fn(),
|
|
6
|
+
syncPlatformAuthStateToResponse: vi.fn(),
|
|
7
|
+
toPlatformErrorResponse: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("@/lib/platform/client.server", () => ({
|
|
11
|
+
loadCurrentSession: mocks.loadCurrentSession,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("@/lib/platform/route-response", () => ({
|
|
15
|
+
toPlatformErrorResponse: mocks.toPlatformErrorResponse,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("@/lib/platform/session.server", () => ({
|
|
19
|
+
readPlatformAuthState: mocks.readPlatformAuthState,
|
|
20
|
+
syncPlatformAuthStateToResponse: mocks.syncPlatformAuthStateToResponse,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import { GET } from "./route";
|
|
24
|
+
|
|
25
|
+
describe("GET /api/auth/session", () => {
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns the current session with no-store cache headers", async () => {
|
|
31
|
+
const authState = { shellSessionToken: "shell-token-1" };
|
|
32
|
+
const nextAuthState = { shellSessionToken: "shell-token-2" };
|
|
33
|
+
mocks.readPlatformAuthState.mockResolvedValue(authState);
|
|
34
|
+
mocks.loadCurrentSession.mockResolvedValue({
|
|
35
|
+
authState: nextAuthState,
|
|
36
|
+
data: { authenticated: true },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const response = await GET();
|
|
40
|
+
|
|
41
|
+
expect(response.status).toBe(200);
|
|
42
|
+
expect(response.headers.get("Cache-Control")).toBe("no-store");
|
|
43
|
+
await expect(response.json()).resolves.toEqual({ authenticated: true });
|
|
44
|
+
expect(mocks.syncPlatformAuthStateToResponse).toHaveBeenCalledWith(
|
|
45
|
+
expect.any(Response),
|
|
46
|
+
authState,
|
|
47
|
+
nextAuthState,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns platform errors with no-store cache headers", async () => {
|
|
52
|
+
const authState = { shellSessionToken: "shell-token-1" };
|
|
53
|
+
const error = new Error("platform failed");
|
|
54
|
+
mocks.readPlatformAuthState.mockResolvedValue(authState);
|
|
55
|
+
mocks.loadCurrentSession.mockRejectedValue(error);
|
|
56
|
+
mocks.toPlatformErrorResponse.mockReturnValue(
|
|
57
|
+
Response.json({ detail: "Platform error" }, { status: 503 }),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const response = await GET();
|
|
61
|
+
|
|
62
|
+
expect(response.status).toBe(503);
|
|
63
|
+
expect(response.headers.get("Cache-Control")).toBe("no-store");
|
|
64
|
+
expect(mocks.toPlatformErrorResponse).toHaveBeenCalledWith(
|
|
65
|
+
error,
|
|
66
|
+
"Unable to load the current platform session.",
|
|
67
|
+
authState,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
import { loadCurrentSession } from "@/lib/platform/client.server";
|
|
4
|
+
import { toPlatformErrorResponse } from "@/lib/platform/route-response";
|
|
5
|
+
import {
|
|
6
|
+
readPlatformAuthState,
|
|
7
|
+
syncPlatformAuthStateToResponse,
|
|
8
|
+
} from "@/lib/platform/session.server";
|
|
9
|
+
|
|
10
|
+
export async function GET() {
|
|
11
|
+
const authState = await readPlatformAuthState();
|
|
12
|
+
try {
|
|
13
|
+
const result = await loadCurrentSession(authState);
|
|
14
|
+
const response = NextResponse.json(result.data, { status: 200 });
|
|
15
|
+
response.headers.set("Cache-Control", "no-store");
|
|
16
|
+
syncPlatformAuthStateToResponse(response, authState, result.authState);
|
|
17
|
+
return response;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
const response = toPlatformErrorResponse(
|
|
20
|
+
error,
|
|
21
|
+
"Unable to load the current platform session.",
|
|
22
|
+
authState,
|
|
23
|
+
);
|
|
24
|
+
response.headers.set("Cache-Control", "no-store");
|
|
25
|
+
return response;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
robots: {
|
|
6
|
+
index: false,
|
|
7
|
+
follow: false,
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function AuthenticatedAppLayout({
|
|
12
|
+
children,
|
|
13
|
+
}: {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}) {
|
|
16
|
+
return children;
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { notFound, redirect } from "next/navigation";
|
|
2
|
+
|
|
3
|
+
import { appRoutes } from "@/lib/app-routes";
|
|
4
|
+
import { loadCurrentWorkspaceShellSession } from "@/lib/platform/client.server";
|
|
5
|
+
import { readPlatformAuthState } from "@/lib/platform/session.server";
|
|
6
|
+
|
|
7
|
+
export const metadata = {
|
|
8
|
+
title: "Workspace",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default async function AppHomePage() {
|
|
12
|
+
const authState = await readPlatformAuthState();
|
|
13
|
+
const shellSession = (await loadCurrentWorkspaceShellSession(authState)).data;
|
|
14
|
+
const { membership, session, workspaceCanOpen, workspaceCanView } =
|
|
15
|
+
shellSession;
|
|
16
|
+
|
|
17
|
+
if (!session.authenticated) {
|
|
18
|
+
redirect(appRoutes.login);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!membership || !workspaceCanView) {
|
|
22
|
+
notFound();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!workspaceCanOpen) {
|
|
26
|
+
redirect(appRoutes.workspaceConnect(membership.workspace_slug));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
redirect(appRoutes.workspaceShell(membership.workspace_slug));
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: "Blog",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function BlogPostPage() {
|
|
8
|
+
return (
|
|
9
|
+
<StaticPublicPage
|
|
10
|
+
eyebrow="Blog"
|
|
11
|
+
title="Update"
|
|
12
|
+
body="A customer-facing update."
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: "Blog",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function BlogIndexPage() {
|
|
8
|
+
return (
|
|
9
|
+
<StaticPublicPage
|
|
10
|
+
eyebrow="Blog"
|
|
11
|
+
title="Updates"
|
|
12
|
+
body="News, releases, and customer notes."
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: "Docs",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function DocsPage() {
|
|
8
|
+
return (
|
|
9
|
+
<StaticPublicPage
|
|
10
|
+
eyebrow="Docs"
|
|
11
|
+
title="Documentation page"
|
|
12
|
+
body="Helpful context for customer workflows."
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: "Docs",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function DocsIndexPage() {
|
|
8
|
+
return (
|
|
9
|
+
<StaticPublicPage
|
|
10
|
+
eyebrow="Docs"
|
|
11
|
+
title="Customer documentation"
|
|
12
|
+
body="Guides, onboarding notes, and product documentation."
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "../design-system/tokens/index.css";
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
html {
|
|
7
|
+
height: 100%;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
min-height: 100%;
|
|
12
|
+
margin: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
* {
|
|
16
|
+
@apply border-border outline-ring/50;
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
body {
|
|
21
|
+
@apply bg-background text-foreground antialiased;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
a {
|
|
25
|
+
color: inherit;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@layer components {
|
|
30
|
+
.public-site-background {
|
|
31
|
+
background:
|
|
32
|
+
linear-gradient(
|
|
33
|
+
180deg,
|
|
34
|
+
var(--color-background) 0%,
|
|
35
|
+
color-mix(in oklab, var(--color-surface-raised) 68%, white) 100%
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.public-site-orb {
|
|
40
|
+
background:
|
|
41
|
+
radial-gradient(
|
|
42
|
+
circle at top left,
|
|
43
|
+
color-mix(in oklab, var(--color-primary) 28%, white) 0%,
|
|
44
|
+
transparent 48%
|
|
45
|
+
),
|
|
46
|
+
radial-gradient(
|
|
47
|
+
circle at top right,
|
|
48
|
+
color-mix(in oklab, var(--color-chart-2) 18%, white) 0%,
|
|
49
|
+
transparent 42%
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.marketing-hero-surface {
|
|
54
|
+
background:
|
|
55
|
+
linear-gradient(
|
|
56
|
+
135deg,
|
|
57
|
+
color-mix(in oklab, var(--color-surface) 78%, white) 0%,
|
|
58
|
+
color-mix(in oklab, var(--color-primary) 8%, white) 100%
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.marketing-hero-orb {
|
|
63
|
+
background:
|
|
64
|
+
radial-gradient(
|
|
65
|
+
circle at center,
|
|
66
|
+
color-mix(in oklab, var(--color-chart-2) 14%, white) 0%,
|
|
67
|
+
transparent 70%
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import "./globals.css";
|
|
2
|
+
import type { Metadata } from "next";
|
|
3
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
4
|
+
import { cookies } from "next/headers";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
|
|
7
|
+
import { AppProviders } from "./providers";
|
|
8
|
+
import { resolvePublicMetadataBase } from "@/lib/public-site";
|
|
9
|
+
import {
|
|
10
|
+
isThemeMode,
|
|
11
|
+
THEME_COOKIE_NAME,
|
|
12
|
+
type ThemeMode,
|
|
13
|
+
} from "@/lib/theme-config";
|
|
14
|
+
|
|
15
|
+
const geistSans = Geist({
|
|
16
|
+
subsets: ["latin"],
|
|
17
|
+
variable: "--font-sans",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const geistMono = Geist_Mono({
|
|
21
|
+
subsets: ["latin"],
|
|
22
|
+
variable: "--font-mono",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export function generateMetadata(): Metadata {
|
|
26
|
+
const metadataBase = resolvePublicMetadataBase();
|
|
27
|
+
const appName = process.env.MW_TEMPLATE_APP_NAME || "Vuilder Shell";
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
...(metadataBase ? { metadataBase } : {}),
|
|
31
|
+
title: {
|
|
32
|
+
default: appName,
|
|
33
|
+
template: `%s | ${appName}`,
|
|
34
|
+
},
|
|
35
|
+
description: "Customer-facing branded tenant shell.",
|
|
36
|
+
openGraph: {
|
|
37
|
+
title: appName,
|
|
38
|
+
description: "Customer-facing branded tenant shell.",
|
|
39
|
+
siteName: appName,
|
|
40
|
+
type: "website",
|
|
41
|
+
...(process.env.MW_PUBLIC_BASE_URL ? { url: process.env.MW_PUBLIC_BASE_URL } : {}),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default async function RootLayout({ children }: { children: ReactNode }) {
|
|
47
|
+
const cookieStore = await cookies();
|
|
48
|
+
const themeCookie = cookieStore.get(THEME_COOKIE_NAME)?.value;
|
|
49
|
+
const initialTheme: ThemeMode = isThemeMode(themeCookie) ? themeCookie : "system";
|
|
50
|
+
const resolvedThemeClass =
|
|
51
|
+
initialTheme === "light" || initialTheme === "dark" ? initialTheme : undefined;
|
|
52
|
+
const colorScheme =
|
|
53
|
+
initialTheme === "light" || initialTheme === "dark" ? initialTheme : undefined;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<html
|
|
57
|
+
lang="en"
|
|
58
|
+
suppressHydrationWarning
|
|
59
|
+
className={resolvedThemeClass}
|
|
60
|
+
style={colorScheme ? { colorScheme } : undefined}
|
|
61
|
+
>
|
|
62
|
+
<body
|
|
63
|
+
className={`${geistSans.variable} ${geistMono.variable} bg-background font-sans text-foreground antialiased`}
|
|
64
|
+
>
|
|
65
|
+
<AppProviders initialTheme={initialTheme}>{children}</AppProviders>
|
|
66
|
+
</body>
|
|
67
|
+
</html>
|
|
68
|
+
);
|
|
69
|
+
}
|