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,29 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
|
|
3
|
+
import { getEnv } from "@/lib/platform/env.server";
|
|
4
|
+
|
|
5
|
+
function getPlatformBaseUrl() {
|
|
6
|
+
const { MW_PLATFORM_BASE_URL } = getEnv();
|
|
7
|
+
|
|
8
|
+
return new URL(
|
|
9
|
+
MW_PLATFORM_BASE_URL.endsWith("/")
|
|
10
|
+
? MW_PLATFORM_BASE_URL
|
|
11
|
+
: `${MW_PLATFORM_BASE_URL}/`,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildPlatformEndpoint(path: string) {
|
|
16
|
+
return new URL(path.replace(/^\//, ""), getPlatformBaseUrl()).toString();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const platformAuthEndpoints = {
|
|
20
|
+
get shellTokenExchange() {
|
|
21
|
+
return buildPlatformEndpoint("/api/v1/session/shell-token/exchange/");
|
|
22
|
+
},
|
|
23
|
+
get shellTokenContext() {
|
|
24
|
+
return buildPlatformEndpoint("/api/v1/session/shell-token/context/");
|
|
25
|
+
},
|
|
26
|
+
get shellTokenRevoke() {
|
|
27
|
+
return buildPlatformEndpoint("/api/v1/session/shell-token/revoke/");
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
const LOCAL_PLATFORM_BASE_URL = "http://127.0.0.1:8000";
|
|
6
|
+
const LOCAL_AUTH_BASE_URL = "http://127.0.0.1:3400";
|
|
7
|
+
const LOCAL_PUBLIC_BASE_URL = "http://127.0.0.1:3301";
|
|
8
|
+
|
|
9
|
+
function normalizeOptionalEnv(value: string | undefined): string | undefined {
|
|
10
|
+
const trimmedValue = value?.trim();
|
|
11
|
+
return trimmedValue ? trimmedValue : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const envSchema = z.object({
|
|
15
|
+
MW_AUTH_BASE_URL: z.string().url(),
|
|
16
|
+
MW_PUBLIC_BASE_URL: z.string().url(),
|
|
17
|
+
MW_PLATFORM_BASE_URL: z.string().url(),
|
|
18
|
+
MW_PLATFORM_FETCH_TIMEOUT_MS: z.coerce.number().int().positive().default(15000),
|
|
19
|
+
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type PlatformEnv = z.infer<typeof envSchema>;
|
|
23
|
+
|
|
24
|
+
let cachedEnv: PlatformEnv | null = null;
|
|
25
|
+
|
|
26
|
+
export function parsePlatformEnv(
|
|
27
|
+
input: Record<string, string | undefined>,
|
|
28
|
+
): PlatformEnv {
|
|
29
|
+
const nodeEnv = input.NODE_ENV ?? "development";
|
|
30
|
+
const isProduction = nodeEnv === "production";
|
|
31
|
+
const defaultPlatformBaseUrl =
|
|
32
|
+
normalizeOptionalEnv(input.MW_PLATFORM_BASE_URL) ??
|
|
33
|
+
(isProduction ? undefined : LOCAL_PLATFORM_BASE_URL);
|
|
34
|
+
const defaultAuthBaseUrl =
|
|
35
|
+
normalizeOptionalEnv(input.MW_AUTH_BASE_URL) ??
|
|
36
|
+
(isProduction ? undefined : LOCAL_AUTH_BASE_URL);
|
|
37
|
+
const defaultPublicBaseUrl =
|
|
38
|
+
normalizeOptionalEnv(input.MW_PUBLIC_BASE_URL) ??
|
|
39
|
+
(isProduction ? undefined : LOCAL_PUBLIC_BASE_URL);
|
|
40
|
+
|
|
41
|
+
const parsedEnv = envSchema.safeParse({
|
|
42
|
+
MW_AUTH_BASE_URL: defaultAuthBaseUrl,
|
|
43
|
+
MW_PUBLIC_BASE_URL: defaultPublicBaseUrl,
|
|
44
|
+
MW_PLATFORM_BASE_URL: defaultPlatformBaseUrl,
|
|
45
|
+
MW_PLATFORM_FETCH_TIMEOUT_MS: normalizeOptionalEnv(
|
|
46
|
+
input.MW_PLATFORM_FETCH_TIMEOUT_MS,
|
|
47
|
+
),
|
|
48
|
+
NODE_ENV: nodeEnv,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!parsedEnv.success) {
|
|
52
|
+
if (isProduction && !normalizeOptionalEnv(input.MW_AUTH_BASE_URL)) {
|
|
53
|
+
throw new Error("MW_AUTH_BASE_URL must be set in production.");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isProduction && !normalizeOptionalEnv(input.MW_PUBLIC_BASE_URL)) {
|
|
57
|
+
throw new Error("MW_PUBLIC_BASE_URL must be set in production.");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (isProduction && !normalizeOptionalEnv(input.MW_PLATFORM_BASE_URL)) {
|
|
61
|
+
throw new Error("MW_PLATFORM_BASE_URL must be set in production.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw new Error("Invalid server environment for vuilder-shell.");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return parsedEnv.data;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getEnv(): PlatformEnv {
|
|
71
|
+
if (!cachedEnv) {
|
|
72
|
+
cachedEnv = parsePlatformEnv(process.env);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return cachedEnv;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const env = new Proxy({} as PlatformEnv, {
|
|
79
|
+
get(_target, property) {
|
|
80
|
+
return getEnv()[property as keyof PlatformEnv];
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
import { PlatformClientError } from "@/lib/platform/client.server";
|
|
4
|
+
import type { PlatformAuthState } from "@/lib/platform/session.server";
|
|
5
|
+
import { syncPlatformAuthStateToResponse } from "@/lib/platform/session.server";
|
|
6
|
+
|
|
7
|
+
export function toPlatformErrorResponse(
|
|
8
|
+
error: unknown,
|
|
9
|
+
fallbackDetail: string,
|
|
10
|
+
previousAuthState?: PlatformAuthState,
|
|
11
|
+
) {
|
|
12
|
+
if (error instanceof PlatformClientError) {
|
|
13
|
+
const response = NextResponse.json(
|
|
14
|
+
{
|
|
15
|
+
detail: error.message,
|
|
16
|
+
code: error.code,
|
|
17
|
+
},
|
|
18
|
+
{ status: error.status },
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (previousAuthState && error.authState) {
|
|
22
|
+
syncPlatformAuthStateToResponse(
|
|
23
|
+
response,
|
|
24
|
+
previousAuthState,
|
|
25
|
+
error.authState,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return response;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return NextResponse.json({ detail: fallbackDetail }, { status: 500 });
|
|
33
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
import { cookies } from "next/headers";
|
|
6
|
+
import type { NextResponse } from "next/server";
|
|
7
|
+
|
|
8
|
+
import { env } from "@/lib/platform/env.server";
|
|
9
|
+
|
|
10
|
+
export const VUILDER_SHELL_SESSION_COOKIE = "mw_vuilder_shell_session";
|
|
11
|
+
export const VUILDER_SHELL_HANDOFF_STATE_COOKIE =
|
|
12
|
+
"mw_vuilder_shell_handoff_state";
|
|
13
|
+
const VUILDER_SHELL_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 12;
|
|
14
|
+
const VUILDER_SHELL_HANDOFF_STATE_MAX_AGE_SECONDS = 60 * 5;
|
|
15
|
+
const LEGACY_PLATFORM_SESSION_COOKIE = "mw_platform_session";
|
|
16
|
+
const LEGACY_PLATFORM_CSRF_COOKIE = "mw_platform_csrf";
|
|
17
|
+
|
|
18
|
+
export type PlatformAuthState = {
|
|
19
|
+
shellSessionToken: string | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ShellCookie = {
|
|
23
|
+
name: string;
|
|
24
|
+
value: string;
|
|
25
|
+
httpOnly: boolean;
|
|
26
|
+
maxAge: number;
|
|
27
|
+
sameSite: "lax";
|
|
28
|
+
path: string;
|
|
29
|
+
secure: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function boundedShellCookieMaxAge(expiresAt?: string | null) {
|
|
33
|
+
const parsedExpiresAt = Date.parse(String(expiresAt ?? ""));
|
|
34
|
+
if (!Number.isFinite(parsedExpiresAt)) {
|
|
35
|
+
return VUILDER_SHELL_COOKIE_MAX_AGE_SECONDS;
|
|
36
|
+
}
|
|
37
|
+
const secondsUntilExpiry = Math.floor((parsedExpiresAt - Date.now()) / 1000);
|
|
38
|
+
if (secondsUntilExpiry <= 0) {
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
return Math.min(secondsUntilExpiry, VUILDER_SHELL_COOKIE_MAX_AGE_SECONDS);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function shellSessionCookie(value: string, expiresAt?: string | null): ShellCookie {
|
|
45
|
+
return {
|
|
46
|
+
name: VUILDER_SHELL_SESSION_COOKIE,
|
|
47
|
+
value,
|
|
48
|
+
httpOnly: true,
|
|
49
|
+
maxAge: boundedShellCookieMaxAge(expiresAt),
|
|
50
|
+
sameSite: "lax",
|
|
51
|
+
path: "/",
|
|
52
|
+
secure: env.NODE_ENV === "production",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function shellHandoffStateCookie(value: string): ShellCookie {
|
|
57
|
+
return {
|
|
58
|
+
name: VUILDER_SHELL_HANDOFF_STATE_COOKIE,
|
|
59
|
+
value,
|
|
60
|
+
httpOnly: true,
|
|
61
|
+
maxAge: VUILDER_SHELL_HANDOFF_STATE_MAX_AGE_SECONDS,
|
|
62
|
+
sameSite: "lax",
|
|
63
|
+
path: "/",
|
|
64
|
+
secure: env.NODE_ENV === "production",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function expiredCookie(name: string): ShellCookie {
|
|
69
|
+
return {
|
|
70
|
+
name,
|
|
71
|
+
value: "",
|
|
72
|
+
httpOnly: true,
|
|
73
|
+
maxAge: 0,
|
|
74
|
+
sameSite: "lax",
|
|
75
|
+
path: "/",
|
|
76
|
+
secure: env.NODE_ENV === "production",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createVuilderShellHandoffState() {
|
|
81
|
+
return randomBytes(32).toString("base64url");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function readPlatformAuthState(): Promise<PlatformAuthState> {
|
|
85
|
+
const cookieStore = await cookies();
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
shellSessionToken:
|
|
89
|
+
cookieStore.get(VUILDER_SHELL_SESSION_COOKIE)?.value ?? null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function readVuilderShellHandoffState() {
|
|
94
|
+
const cookieStore = await cookies();
|
|
95
|
+
return cookieStore.get(VUILDER_SHELL_HANDOFF_STATE_COOKIE)?.value ?? null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function applyVuilderShellSessionToResponse(
|
|
99
|
+
response: NextResponse,
|
|
100
|
+
shellSessionToken: string,
|
|
101
|
+
expiresAt?: string | null,
|
|
102
|
+
) {
|
|
103
|
+
response.cookies.set(shellSessionCookie(shellSessionToken, expiresAt));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function applyVuilderShellHandoffStateToResponse(
|
|
107
|
+
response: NextResponse,
|
|
108
|
+
handoffState: string,
|
|
109
|
+
) {
|
|
110
|
+
response.cookies.set(shellHandoffStateCookie(handoffState));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function clearVuilderShellHandoffStateFromResponse(
|
|
114
|
+
response: NextResponse,
|
|
115
|
+
) {
|
|
116
|
+
response.cookies.set(expiredCookie(VUILDER_SHELL_HANDOFF_STATE_COOKIE));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function syncPlatformAuthStateToResponse(
|
|
120
|
+
response: NextResponse,
|
|
121
|
+
previousAuthState: PlatformAuthState,
|
|
122
|
+
nextAuthState: PlatformAuthState,
|
|
123
|
+
) {
|
|
124
|
+
if (previousAuthState.shellSessionToken !== nextAuthState.shellSessionToken) {
|
|
125
|
+
if (nextAuthState.shellSessionToken) {
|
|
126
|
+
applyVuilderShellSessionToResponse(
|
|
127
|
+
response,
|
|
128
|
+
nextAuthState.shellSessionToken,
|
|
129
|
+
);
|
|
130
|
+
} else {
|
|
131
|
+
clearPlatformSessionFromResponse(response);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function clearPlatformSessionFromResponse(response: NextResponse) {
|
|
137
|
+
for (const cookieName of [
|
|
138
|
+
VUILDER_SHELL_SESSION_COOKIE,
|
|
139
|
+
VUILDER_SHELL_HANDOFF_STATE_COOKIE,
|
|
140
|
+
LEGACY_PLATFORM_SESSION_COOKIE,
|
|
141
|
+
LEGACY_PLATFORM_CSRF_COOKIE,
|
|
142
|
+
]) {
|
|
143
|
+
response.cookies.set(expiredCookie(cookieName));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { buildPublicMetadata, resolvePublicSiteUrl } from "@/lib/public-site";
|
|
4
|
+
|
|
5
|
+
describe("public-site helpers", () => {
|
|
6
|
+
it("builds canonical urls from MW_PUBLIC_BASE_URL", () => {
|
|
7
|
+
expect(process.env.MW_PUBLIC_BASE_URL).toBe("http://127.0.0.1:3301");
|
|
8
|
+
|
|
9
|
+
const metadata = buildPublicMetadata({
|
|
10
|
+
title: "Docs",
|
|
11
|
+
description: "Starter documentation",
|
|
12
|
+
path: "/docs",
|
|
13
|
+
siteName: "MinuteWork Combined Starter",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(resolvePublicSiteUrl("/docs")?.toString()).toBe("http://127.0.0.1:3301/docs");
|
|
17
|
+
expect(metadata.alternates?.canonical).toBe("http://127.0.0.1:3301/docs");
|
|
18
|
+
expect(metadata.openGraph?.url).toBe("http://127.0.0.1:3301/docs");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
|
|
3
|
+
export function resolvePublicMetadataBase() {
|
|
4
|
+
const value = process.env.MW_PUBLIC_BASE_URL || "";
|
|
5
|
+
if (!value) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return new URL(value.endsWith("/") ? value : `${value}/`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolvePublicSiteUrl(pathname = "/") {
|
|
12
|
+
const metadataBase = resolvePublicMetadataBase();
|
|
13
|
+
return metadataBase
|
|
14
|
+
? new URL(pathname.replace(/^\/*/, "/"), metadataBase)
|
|
15
|
+
: null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildPublicMetadata(input: {
|
|
19
|
+
title: string;
|
|
20
|
+
description: string;
|
|
21
|
+
path: string;
|
|
22
|
+
siteName?: string;
|
|
23
|
+
}): Metadata {
|
|
24
|
+
const canonicalUrl = resolvePublicSiteUrl(input.path)?.toString();
|
|
25
|
+
return {
|
|
26
|
+
title: input.title,
|
|
27
|
+
description: input.description,
|
|
28
|
+
...(canonicalUrl
|
|
29
|
+
? {
|
|
30
|
+
alternates: {
|
|
31
|
+
canonical: canonicalUrl,
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
: {}),
|
|
35
|
+
openGraph: {
|
|
36
|
+
title: input.title,
|
|
37
|
+
description: input.description,
|
|
38
|
+
siteName: input.siteName ?? process.env.MW_TEMPLATE_APP_NAME ?? "Vuilder Shell",
|
|
39
|
+
type: "website",
|
|
40
|
+
...(canonicalUrl ? { url: canonicalUrl } : {}),
|
|
41
|
+
},
|
|
42
|
+
twitter: {
|
|
43
|
+
card: "summary_large_image",
|
|
44
|
+
title: input.title,
|
|
45
|
+
description: input.description,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ThemeMode = "light" | "dark" | "system";
|
|
2
|
+
export type ResolvedTheme = "light" | "dark";
|
|
3
|
+
|
|
4
|
+
export const THEME_STORAGE_KEY = "next-tenant-app-theme";
|
|
5
|
+
export const THEME_COOKIE_NAME = "next-tenant-app-theme";
|
|
6
|
+
export const SYSTEM_THEME_QUERY = "(prefers-color-scheme: dark)";
|
|
7
|
+
|
|
8
|
+
export function isThemeMode(value: string | null | undefined): value is ThemeMode {
|
|
9
|
+
return value === "light" || value === "dark" || value === "system";
|
|
10
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
isThemeMode,
|
|
14
|
+
SYSTEM_THEME_QUERY,
|
|
15
|
+
THEME_COOKIE_NAME,
|
|
16
|
+
THEME_STORAGE_KEY,
|
|
17
|
+
type ResolvedTheme,
|
|
18
|
+
type ThemeMode,
|
|
19
|
+
} from "@/lib/theme-config";
|
|
20
|
+
|
|
21
|
+
type ThemeContextValue = {
|
|
22
|
+
theme: ThemeMode;
|
|
23
|
+
resolvedTheme: ResolvedTheme;
|
|
24
|
+
setTheme: (theme: ThemeMode) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
28
|
+
|
|
29
|
+
function getSystemTheme(): ResolvedTheme {
|
|
30
|
+
return window.matchMedia(SYSTEM_THEME_QUERY).matches ? "dark" : "light";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveTheme(theme: ThemeMode, systemTheme: ResolvedTheme): ResolvedTheme {
|
|
34
|
+
return theme === "system" ? systemTheme : theme;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function applyThemeToDocument(resolvedTheme: ResolvedTheme, attribute: string) {
|
|
38
|
+
const root = document.documentElement;
|
|
39
|
+
|
|
40
|
+
if (attribute === "class") {
|
|
41
|
+
root.classList.remove("light", "dark");
|
|
42
|
+
root.classList.add(resolvedTheme);
|
|
43
|
+
} else {
|
|
44
|
+
root.setAttribute(attribute, resolvedTheme);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
root.style.colorScheme = resolvedTheme;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function disableTransitionsTemporarily() {
|
|
51
|
+
const style = document.createElement("style");
|
|
52
|
+
style.appendChild(
|
|
53
|
+
document.createTextNode(
|
|
54
|
+
"*{-webkit-transition:none !important;transition:none !important}",
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
document.head.appendChild(style);
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
window.getComputedStyle(document.body);
|
|
61
|
+
window.setTimeout(() => {
|
|
62
|
+
style.remove();
|
|
63
|
+
}, 0);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function ThemeProvider({
|
|
68
|
+
children,
|
|
69
|
+
attribute = "class",
|
|
70
|
+
defaultTheme = "system",
|
|
71
|
+
enableSystem = true,
|
|
72
|
+
disableTransitionOnChange = false,
|
|
73
|
+
}: {
|
|
74
|
+
children: ReactNode;
|
|
75
|
+
attribute?: string;
|
|
76
|
+
defaultTheme?: ThemeMode;
|
|
77
|
+
enableSystem?: boolean;
|
|
78
|
+
disableTransitionOnChange?: boolean;
|
|
79
|
+
}) {
|
|
80
|
+
const [theme, setThemeState] = useState<ThemeMode>(() => {
|
|
81
|
+
if (typeof window === "undefined") {
|
|
82
|
+
return defaultTheme;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY);
|
|
87
|
+
if (isThemeMode(storedTheme)) {
|
|
88
|
+
return storedTheme;
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
|
|
92
|
+
return defaultTheme;
|
|
93
|
+
});
|
|
94
|
+
const [systemTheme, setSystemTheme] = useState<ResolvedTheme>(() => {
|
|
95
|
+
if (typeof window === "undefined") {
|
|
96
|
+
return "light";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return getSystemTheme();
|
|
100
|
+
});
|
|
101
|
+
const resolvedTheme = resolveTheme(
|
|
102
|
+
theme,
|
|
103
|
+
enableSystem ? systemTheme : ("light" as ResolvedTheme),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const mediaQuery = window.matchMedia(SYSTEM_THEME_QUERY);
|
|
108
|
+
const syncSystemTheme = () => {
|
|
109
|
+
setSystemTheme(mediaQuery.matches ? "dark" : "light");
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
syncSystemTheme();
|
|
113
|
+
mediaQuery.addEventListener("change", syncSystemTheme);
|
|
114
|
+
|
|
115
|
+
return () => {
|
|
116
|
+
mediaQuery.removeEventListener("change", syncSystemTheme);
|
|
117
|
+
};
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const restoreTransitions = disableTransitionOnChange
|
|
122
|
+
? disableTransitionsTemporarily()
|
|
123
|
+
: null;
|
|
124
|
+
|
|
125
|
+
applyThemeToDocument(resolvedTheme, attribute);
|
|
126
|
+
|
|
127
|
+
if (restoreTransitions) {
|
|
128
|
+
restoreTransitions();
|
|
129
|
+
}
|
|
130
|
+
}, [attribute, disableTransitionOnChange, resolvedTheme]);
|
|
131
|
+
|
|
132
|
+
const value = useMemo<ThemeContextValue>(
|
|
133
|
+
() => ({
|
|
134
|
+
theme,
|
|
135
|
+
resolvedTheme,
|
|
136
|
+
setTheme: (nextTheme) => {
|
|
137
|
+
setThemeState(nextTheme);
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
window.localStorage.setItem(THEME_STORAGE_KEY, nextTheme);
|
|
141
|
+
document.cookie = `${THEME_COOKIE_NAME}=${nextTheme}; path=/; max-age=31536000; SameSite=Lax`;
|
|
142
|
+
} catch {}
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
[resolvedTheme, theme],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function useTheme() {
|
|
152
|
+
const context = useContext(ThemeContext);
|
|
153
|
+
|
|
154
|
+
if (!context) {
|
|
155
|
+
throw new Error("useTheme must be used within ThemeProvider");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return context;
|
|
159
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"template_id": "vuilder-shell",
|
|
3
|
+
"template_kind": "vuilder_shell",
|
|
4
|
+
"template_profile": "vuilder_shell_token",
|
|
5
|
+
"template_bundle_ref": "runtime/builder/templates/vuilder-shell",
|
|
6
|
+
"template_version": "0.1.0",
|
|
7
|
+
"materialize": {
|
|
8
|
+
"destination": "vuilder-shell"
|
|
9
|
+
},
|
|
10
|
+
"builder_edit_mode": "workspace_copy_only",
|
|
11
|
+
"seed_source": "apps/mw-next-client",
|
|
12
|
+
"required_bootstrap_steps": [
|
|
13
|
+
"next_typegen",
|
|
14
|
+
"design_system_tokens"
|
|
15
|
+
],
|
|
16
|
+
"runtime_contract_refs": [
|
|
17
|
+
"reference/mwv3-dj6-docs/auth_and_credential_contract.md",
|
|
18
|
+
"reference/mwv3-dj6-docs/gateway_shell_server_channel_thread_contract.md",
|
|
19
|
+
"reference/mwv3-dj6-docs/published_web_property_contract.md",
|
|
20
|
+
"reference/mwv3-dj6-docs/runtime_app_pack_contract.md",
|
|
21
|
+
"reference/mwv3-dj6-docs/runtime_compute_isolation_and_sandboxing_contract.md"
|
|
22
|
+
],
|
|
23
|
+
"example_features": {
|
|
24
|
+
"vuilder_shell_token_demo": {
|
|
25
|
+
"default_enabled": true
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|