minutework 0.1.32 → 0.1.33

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.
Files changed (91) hide show
  1. package/assets/claude-local/skills/README.md +2 -0
  2. package/assets/claude-local/skills/app-pack-authoring/SKILL.md +14 -1
  3. package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
  4. package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +10 -3
  5. package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +9 -6
  6. package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +5 -4
  7. package/assets/templates/next-tenant-app/README.md +26 -138
  8. package/assets/templates/next-tenant-app/package.json +1 -0
  9. package/assets/templates/next-tenant-app/src/app/app/demo/page.tsx +15 -0
  10. package/assets/templates/next-tenant-app/src/app/app/layout.tsx +1 -4
  11. package/assets/templates/next-tenant-app/src/app/app/page.tsx +2 -17
  12. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +9 -67
  13. package/assets/templates/next-tenant-app/src/app/blog/page.tsx +10 -46
  14. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +9 -65
  15. package/assets/templates/next-tenant-app/src/app/docs/page.tsx +10 -46
  16. package/assets/templates/next-tenant-app/src/app/layout.tsx +8 -10
  17. package/assets/templates/next-tenant-app/src/app/login/page.tsx +3 -23
  18. package/assets/templates/next-tenant-app/src/app/page.tsx +11 -44
  19. package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +10 -44
  20. package/assets/templates/next-tenant-app/src/app/providers.tsx +2 -1
  21. package/assets/templates/next-tenant-app/src/app/robots.ts +7 -18
  22. package/assets/templates/next-tenant-app/src/app/sitemap.ts +4 -39
  23. package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +97 -98
  24. package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +43 -78
  25. package/assets/templates/next-tenant-app/src/features/demo/components/manifest-demo.tsx +89 -0
  26. package/assets/templates/next-tenant-app/src/features/public-shell/components/static-public-page.tsx +58 -0
  27. package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +48 -552
  28. package/assets/templates/next-tenant-app/src/lib/app-routes.ts +2 -2
  29. package/assets/templates/next-tenant-app/src/lib/public-site.ts +5 -30
  30. package/assets/templates/next-tenant-app/src/mw/client.ts +18 -0
  31. package/assets/templates/next-tenant-app/src/mw/mock.test.ts +21 -0
  32. package/assets/templates/next-tenant-app/src/mw/mock.ts +35 -0
  33. package/assets/templates/next-tenant-app/src/mw/provider.tsx +17 -0
  34. package/assets/templates/next-tenant-app/template.json +3 -3
  35. package/assets/templates/next-tenant-app/template.schema.json +1 -0
  36. package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +4 -5
  37. package/package.json +2 -2
  38. package/vendor/workspace-mcp/types.d.ts +4 -0
  39. package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +0 -89
  40. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +0 -90
  41. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +0 -78
  42. package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +0 -31
  43. package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +0 -16
  44. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +0 -79
  45. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +0 -40
  46. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +0 -42
  47. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +0 -29
  48. package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +0 -26
  49. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +0 -40
  50. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +0 -47
  51. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +0 -43
  52. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +0 -45
  53. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +0 -83
  54. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +0 -30
  55. package/assets/templates/next-tenant-app/src/app/app/page.test.ts +0 -62
  56. package/assets/templates/next-tenant-app/src/app/app/private-content-source.test.ts +0 -88
  57. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +0 -70
  58. package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +0 -46
  59. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +0 -70
  60. package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +0 -46
  61. package/assets/templates/next-tenant-app/src/app/login/page.test.ts +0 -55
  62. package/assets/templates/next-tenant-app/src/app/page.test.ts +0 -90
  63. package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +0 -59
  64. package/assets/templates/next-tenant-app/src/app/robots.test.ts +0 -40
  65. package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +0 -63
  66. package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +0 -342
  67. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +0 -66
  68. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +0 -108
  69. package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +0 -111
  70. package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +0 -111
  71. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.test.ts +0 -38
  72. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx +0 -145
  73. package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +0 -189
  74. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +0 -444
  75. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +0 -383
  76. package/assets/templates/next-tenant-app/src/lib/content/contracts.test.ts +0 -138
  77. package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +0 -399
  78. package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +0 -5
  79. package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +0 -96
  80. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.test.ts +0 -93
  81. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.ts +0 -123
  82. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +0 -75
  83. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +0 -25
  84. package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +0 -170
  85. package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +0 -661
  86. package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +0 -131
  87. package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +0 -34
  88. package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +0 -211
  89. package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +0 -151
  90. package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +0 -33
  91. package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +0 -108
@@ -1,32 +1,11 @@
1
- import "server-only";
2
-
3
1
  import type { Metadata } from "next";
4
2
 
5
- import { env } from "@/lib/platform/env.server";
6
-
7
- export function isPublicContentDisabled() {
8
- return env.MW_PUBLIC_CONTENT_SOURCE === "none";
9
- }
10
-
11
- export function buildDisabledPublicMetadata(): Metadata {
12
- return {
13
- robots: {
14
- index: false,
15
- follow: false,
16
- },
17
- };
18
- }
19
-
20
3
  export function resolvePublicMetadataBase() {
21
- if (!env.MW_PUBLIC_BASE_URL) {
4
+ const value = process.env.MW_PUBLIC_BASE_URL || "";
5
+ if (!value) {
22
6
  return null;
23
7
  }
24
-
25
- return new URL(
26
- env.MW_PUBLIC_BASE_URL.endsWith("/")
27
- ? env.MW_PUBLIC_BASE_URL
28
- : `${env.MW_PUBLIC_BASE_URL}/`,
29
- );
8
+ return new URL(value.endsWith("/") ? value : `${value}/`);
30
9
  }
31
10
 
32
11
  export function resolvePublicSiteUrl(pathname = "/") {
@@ -41,11 +20,8 @@ export function buildPublicMetadata(input: {
41
20
  description: string;
42
21
  path: string;
43
22
  siteName?: string;
44
- type?: "website" | "article";
45
- publishedTime?: string | null;
46
23
  }): Metadata {
47
24
  const canonicalUrl = resolvePublicSiteUrl(input.path)?.toString();
48
-
49
25
  return {
50
26
  title: input.title,
51
27
  description: input.description,
@@ -59,10 +35,9 @@ export function buildPublicMetadata(input: {
59
35
  openGraph: {
60
36
  title: input.title,
61
37
  description: input.description,
62
- siteName: input.siteName ?? env.MW_TEMPLATE_APP_NAME,
63
- type: input.type ?? "website",
38
+ siteName: input.siteName ?? process.env.MW_TEMPLATE_APP_NAME ?? "Tenant App",
39
+ type: "website",
64
40
  ...(canonicalUrl ? { url: canonicalUrl } : {}),
65
- ...(input.publishedTime ? { publishedTime: input.publishedTime } : {}),
66
41
  },
67
42
  twitter: {
68
43
  card: "summary_large_image",
@@ -0,0 +1,18 @@
1
+ "use client";
2
+
3
+ // MinuteWork substrate. Thin layer - do not put product UI/logic here.
4
+
5
+ import {
6
+ createIdempotencyKey,
7
+ createMinuteWorkClient,
8
+ type IdempotencyKey,
9
+ } from "@minutework/web-auth";
10
+
11
+ export const mwAppId =
12
+ process.env.NEXT_PUBLIC_MW_APP_ID?.trim() || "tenant.app";
13
+
14
+ export const mw = createMinuteWorkClient({
15
+ appId: mwAppId,
16
+ });
17
+
18
+ export { createIdempotencyKey, type IdempotencyKey };
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { createIdempotencyKey } from "@/mw/client";
4
+ import { createTemplateMockMinuteWorkClient } from "@/mw/mock";
5
+
6
+ describe("MinuteWork SDK mock substrate", () => {
7
+ it("supports the template manifest query and action without platform BFF routes", async () => {
8
+ const client = createTemplateMockMinuteWorkClient();
9
+ await client.auth.verifyEmail({ token: "mock-verification-token" });
10
+
11
+ const queryResult = await client.query<{ count: number }>("demo.list", {});
12
+ const actionResult = await client.action<{ created: boolean }>(
13
+ "demo.create",
14
+ { title: "Created" },
15
+ { idempotencyKey: createIdempotencyKey("test") },
16
+ );
17
+
18
+ expect(queryResult.count).toBe(1);
19
+ expect(actionResult.created).toBe(true);
20
+ });
21
+ });
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ // MinuteWork substrate. Thin layer - do not put product UI/logic here.
4
+
5
+ import { createMockMinuteWorkClient } from "@minutework/web-auth/mock";
6
+
7
+ export function createTemplateMockMinuteWorkClient() {
8
+ return createMockMinuteWorkClient({
9
+ appId: "tenant.app",
10
+ verified: true,
11
+ queryHandlers: {
12
+ "demo.list": () => ({
13
+ data: [
14
+ {
15
+ id: "demo-record-1",
16
+ display_label: "Demo record",
17
+ data: { title: "Demo record", status: "ready" },
18
+ },
19
+ ],
20
+ count: 1,
21
+ }),
22
+ },
23
+ actionHandlers: {
24
+ "demo.create": (payload, options) => ({
25
+ data: {
26
+ id: "demo-created",
27
+ display_label: String(payload.title || "Created record"),
28
+ data: payload,
29
+ },
30
+ created: true,
31
+ idempotency_key: options.idempotencyKey,
32
+ }),
33
+ },
34
+ });
35
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ // MinuteWork substrate. Thin layer - do not put product UI/logic here.
4
+
5
+ import type { ReactNode } from "react";
6
+
7
+ import { MinuteWorkProvider } from "@minutework/web-auth/react";
8
+
9
+ import { mw } from "@/mw/client";
10
+
11
+ export function MinuteWorkSdkProvider({ children }: { children: ReactNode }) {
12
+ return (
13
+ <MinuteWorkProvider client={mw} loadSessionOnMount>
14
+ {children}
15
+ </MinuteWorkProvider>
16
+ );
17
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "template_id": "next-tenant-app",
3
3
  "template_kind": "combined_web",
4
- "template_profile": "platform_session_bff",
4
+ "template_profile": "tenant_web_auth_sdk",
5
5
  "template_bundle_ref": "runtime/builder/templates/next-tenant-app",
6
6
  "template_version": "0.3.0",
7
7
  "materialize": {
@@ -20,8 +20,8 @@
20
20
  "reference/mwv3-dj6-docs/runtime_compute_isolation_and_sandboxing_contract.md"
21
21
  ],
22
22
  "example_features": {
23
- "runtime_command_demo": {
24
- "default_enabled": false
23
+ "manifest_sdk_demo": {
24
+ "default_enabled": true
25
25
  }
26
26
  }
27
27
  }
@@ -33,6 +33,7 @@
33
33
  "type": "string",
34
34
  "enum": [
35
35
  "platform_session_bff",
36
+ "tenant_web_auth_sdk",
36
37
  "bridge_internal",
37
38
  "public_dj_cms"
38
39
  ]
@@ -15,13 +15,12 @@ const requiredPaths = [
15
15
  "src/app/login/page.tsx",
16
16
  "src/app/app/layout.tsx",
17
17
  "src/app/app/page.tsx",
18
- "src/app/app/examples/runtime-commands/page.tsx",
18
+ "src/app/app/demo/page.tsx",
19
19
  "src/app/robots.ts",
20
20
  "src/app/sitemap.ts",
21
- "src/lib/content/contracts.ts",
22
- "src/lib/content/custom-adapter.ts",
23
- "src/lib/content/empty-state.ts",
24
- "src/lib/content/adapter.server.ts",
21
+ "src/mw/client.ts",
22
+ "src/mw/provider.tsx",
23
+ "src/mw/mock.ts",
25
24
  ];
26
25
 
27
26
  const missingPaths = requiredPaths.filter(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minutework",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "MinuteWork CLI for workspace scaffolding, local preview workflows, and hosted preview deploys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "jiti": "^2.6.1",
26
26
  "zod": "^4.3.6",
27
27
  "@minutework/platform-config": "0.1.2",
28
- "@minutework/schema-compiler": "0.1.4"
28
+ "@minutework/schema-compiler": "0.1.5"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^24.9.1",
@@ -356,8 +356,10 @@ export declare const SchemaStatusSchema: z.ZodObject<{
356
356
  input_schema_key: z.ZodOptional<z.ZodNullable<z.ZodString>>;
357
357
  output_schema_key: z.ZodOptional<z.ZodNullable<z.ZodString>>;
358
358
  primitive: z.ZodOptional<z.ZodString>;
359
+ required_roles: z.ZodOptional<z.ZodArray<z.ZodString>>;
359
360
  schema_key: z.ZodString;
360
361
  version: z.ZodLiteral<"ActionManifestV1">;
362
+ web_customer_exposed: z.ZodOptional<z.ZodBoolean>;
361
363
  }, z.core.$strict>>;
362
364
  appManifests: z.ZodArray<z.ZodObject<{
363
365
  actions: z.ZodArray<z.ZodString>;
@@ -416,8 +418,10 @@ export declare const SchemaStatusSchema: z.ZodObject<{
416
418
  detail: "detail";
417
419
  list: "list";
418
420
  }>, z.ZodLiteral<"connector">]>;
421
+ required_roles: z.ZodOptional<z.ZodArray<z.ZodString>>;
419
422
  schema_key: z.ZodString;
420
423
  version: z.ZodLiteral<"QueryManifestV1">;
424
+ web_customer_exposed: z.ZodOptional<z.ZodBoolean>;
421
425
  }, z.core.$strict>>;
422
426
  releaseMetadata: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
423
427
  routeManifests: z.ZodArray<z.ZodObject<{
@@ -1,89 +0,0 @@
1
- import type { Metadata } from "next";
2
- import { notFound } from "next/navigation";
3
-
4
- import { ContentStructureRenderer } from "@/features/public-shell/components/section-renderer";
5
- import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
6
- import { getPageByPath, getSiteConfig, listPages } from "@/lib/content/adapter.server";
7
- import {
8
- buildDisabledPublicMetadata,
9
- buildPublicMetadata,
10
- isPublicContentDisabled,
11
- } from "@/lib/public-site";
12
-
13
- type PageParams = { path: string[] };
14
-
15
- export async function generateStaticParams(): Promise<PageParams[]> {
16
- if (isPublicContentDisabled()) {
17
- return [];
18
- }
19
-
20
- const pages = await listPages("published");
21
- return pages
22
- .filter((p) => p.path !== "/")
23
- .map((p) => ({
24
- path: p.path.replace(/^\//, "").split("/").filter(Boolean),
25
- }));
26
- }
27
-
28
- export async function generateMetadata({
29
- params,
30
- }: {
31
- params: Promise<PageParams>;
32
- }): Promise<Metadata> {
33
- if (isPublicContentDisabled()) {
34
- return buildDisabledPublicMetadata();
35
- }
36
-
37
- const { path: pathSegments } = await params;
38
- const pagePath = `/${pathSegments.join("/")}`;
39
- const page = await getPageByPath(pagePath);
40
-
41
- if (!page) return {};
42
-
43
- const seo = page.seo_metadata;
44
- return buildPublicMetadata({
45
- title: seo?.title ?? page.title,
46
- description: seo?.description ?? page.title,
47
- path: pagePath,
48
- });
49
- }
50
-
51
- export default async function CmsPage({
52
- params,
53
- }: {
54
- params: Promise<PageParams>;
55
- }) {
56
- if (isPublicContentDisabled()) {
57
- notFound();
58
- }
59
-
60
- const { path: pathSegments } = await params;
61
- const pagePath = `/${pathSegments.join("/")}`;
62
- const [siteConfig, page] = await Promise.all([
63
- getSiteConfig(),
64
- getPageByPath(pagePath),
65
- ]);
66
-
67
- if (!page || page.status === "draft") {
68
- notFound();
69
- }
70
-
71
- const sections = page.content_structure?.sections ?? [];
72
-
73
- return (
74
- <PublicSiteShell siteConfig={siteConfig} activeHref={pagePath}>
75
- <div className="space-y-8">
76
- <div className="space-y-3">
77
- <h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">
78
- {page.title}
79
- </h1>
80
- </div>
81
- {sections.length > 0 ? (
82
- <ContentStructureRenderer sections={sections} />
83
- ) : (
84
- <p className="text-muted-foreground">This page has no content yet.</p>
85
- )}
86
- </div>
87
- </PublicSiteShell>
88
- );
89
- }
@@ -1,90 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- const fetchSessionContext = vi.fn();
4
- const resetSessionContext = vi.fn();
5
- const updateSessionContext = vi.fn();
6
- const readPlatformAuthState = vi.fn();
7
- const syncPlatformAuthStateToResponse = vi.fn();
8
-
9
- vi.mock("@/lib/platform/client.server", () => ({
10
- fetchSessionContext,
11
- resetSessionContext,
12
- updateSessionContext,
13
- }));
14
-
15
- vi.mock("@/lib/platform/session.server", () => ({
16
- readPlatformAuthState,
17
- syncPlatformAuthStateToResponse,
18
- }));
19
-
20
- describe("auth context route", () => {
21
- beforeEach(() => {
22
- vi.clearAllMocks();
23
- });
24
-
25
- it("proxies valid tenant context updates through the BFF", async () => {
26
- const authState = { sessionId: "session-1", csrfToken: "csrf-1" };
27
- const payload = {
28
- authenticated: true as const,
29
- user: {
30
- id: "user-1",
31
- username: "demo-user",
32
- email: "demo@example.com",
33
- },
34
- active_tenant_id: "tenant-2",
35
- active_tenant: {
36
- tenant_id: "tenant-2",
37
- tenant_slug: "beta",
38
- tenant_name: "Beta",
39
- role: "member",
40
- },
41
- memberships: [
42
- {
43
- tenant_id: "tenant-2",
44
- tenant_slug: "beta",
45
- tenant_name: "Beta",
46
- role: "member",
47
- },
48
- ],
49
- };
50
-
51
- readPlatformAuthState.mockResolvedValue(authState);
52
- updateSessionContext.mockResolvedValue({
53
- data: payload,
54
- authState: authState,
55
- });
56
-
57
- const route = await import("./route");
58
- const response = await route.PUT(
59
- new Request("http://localhost/api/auth/context", {
60
- method: "PUT",
61
- headers: { "content-type": "application/json" },
62
- body: JSON.stringify({ tenant_id: "tenant-2" }),
63
- }),
64
- );
65
-
66
- expect(updateSessionContext).toHaveBeenCalledWith(authState, {
67
- tenant_id: "tenant-2",
68
- });
69
- expect(syncPlatformAuthStateToResponse).toHaveBeenCalledWith(
70
- response,
71
- authState,
72
- authState,
73
- );
74
- expect(response.status).toBe(200);
75
- await expect(response.json()).resolves.toEqual(payload);
76
- });
77
-
78
- it("returns 401 when the browser has no stored platform session", async () => {
79
- readPlatformAuthState.mockResolvedValue({
80
- sessionId: null,
81
- csrfToken: "csrf-1",
82
- });
83
-
84
- const route = await import("./route");
85
- const response = await route.DELETE();
86
-
87
- expect(resetSessionContext).not.toHaveBeenCalled();
88
- expect(response.status).toBe(401);
89
- });
90
- });
@@ -1,78 +0,0 @@
1
- import { NextResponse } from "next/server";
2
-
3
- import {
4
- fetchSessionContext,
5
- resetSessionContext,
6
- updateSessionContext,
7
- } from "@/lib/platform/client.server";
8
- import { tenantSessionContextRequestSchema } from "@/lib/platform/contracts";
9
- import { toPlatformErrorResponse } from "@/lib/platform/route-response";
10
- import {
11
- readPlatformAuthState,
12
- syncPlatformAuthStateToResponse,
13
- } from "@/lib/platform/session.server";
14
-
15
- function unauthorizedResponse() {
16
- return NextResponse.json({ detail: "Authentication required." }, { status: 401 });
17
- }
18
-
19
- export async function GET() {
20
- const authState = await readPlatformAuthState();
21
-
22
- if (!authState.sessionId) {
23
- return unauthorizedResponse();
24
- }
25
-
26
- try {
27
- const result = await fetchSessionContext(authState);
28
- const response = NextResponse.json(result.data, { status: 200 });
29
- syncPlatformAuthStateToResponse(response, authState, result.authState);
30
- return response;
31
- } catch (error) {
32
- return toPlatformErrorResponse(error, "Unable to load tenant context.", authState);
33
- }
34
- }
35
-
36
- export async function PUT(request: Request) {
37
- const authState = await readPlatformAuthState();
38
-
39
- if (!authState.sessionId) {
40
- return unauthorizedResponse();
41
- }
42
-
43
- const body = await request.json().catch(() => null);
44
- const parsed = tenantSessionContextRequestSchema.safeParse(body);
45
-
46
- if (!parsed.success) {
47
- return NextResponse.json(
48
- { detail: "Invalid tenant context payload." },
49
- { status: 400 },
50
- );
51
- }
52
-
53
- try {
54
- const result = await updateSessionContext(authState, parsed.data);
55
- const response = NextResponse.json(result.data, { status: 200 });
56
- syncPlatformAuthStateToResponse(response, authState, result.authState);
57
- return response;
58
- } catch (error) {
59
- return toPlatformErrorResponse(error, "Unable to update tenant context.", authState);
60
- }
61
- }
62
-
63
- export async function DELETE() {
64
- const authState = await readPlatformAuthState();
65
-
66
- if (!authState.sessionId) {
67
- return unauthorizedResponse();
68
- }
69
-
70
- try {
71
- const result = await resetSessionContext(authState);
72
- const response = NextResponse.json(result.data, { status: 200 });
73
- syncPlatformAuthStateToResponse(response, authState, result.authState);
74
- return response;
75
- } catch (error) {
76
- return toPlatformErrorResponse(error, "Unable to reset tenant context.", authState);
77
- }
78
- }
@@ -1,31 +0,0 @@
1
- import { NextResponse } from "next/server";
2
-
3
- import { loginWithPassword } from "@/lib/platform/client.server";
4
- import { loginRequestSchema } from "@/lib/platform/contracts";
5
- import { toPlatformErrorResponse } from "@/lib/platform/route-response";
6
- import {
7
- readPlatformAuthState,
8
- syncPlatformAuthStateToResponse,
9
- } from "@/lib/platform/session.server";
10
-
11
- export async function POST(request: Request) {
12
- const authState = await readPlatformAuthState();
13
- const body = await request.json().catch(() => null);
14
- const parsed = loginRequestSchema.safeParse(body);
15
-
16
- if (!parsed.success) {
17
- return NextResponse.json(
18
- { detail: "Invalid login payload." },
19
- { status: 400 },
20
- );
21
- }
22
-
23
- try {
24
- const result = await loginWithPassword(authState, parsed.data);
25
- const response = NextResponse.json({ authenticated: true }, { status: 200 });
26
- syncPlatformAuthStateToResponse(response, authState, result.authState);
27
- return response;
28
- } catch (error) {
29
- return toPlatformErrorResponse(error, "Unable to sign in.", authState);
30
- }
31
- }
@@ -1,16 +0,0 @@
1
- import { NextResponse } from "next/server";
2
-
3
- import { logoutPlatformSession } from "@/lib/platform/client.server";
4
- import {
5
- clearPlatformSessionFromResponse,
6
- readPlatformAuthState,
7
- } from "@/lib/platform/session.server";
8
-
9
- export async function POST() {
10
- const authState = await readPlatformAuthState();
11
- await logoutPlatformSession(authState).catch(() => null);
12
-
13
- const response = new NextResponse(null, { status: 204 });
14
- clearPlatformSessionFromResponse(response);
15
- return response;
16
- }
@@ -1,79 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- const changePassword = vi.fn();
4
- const readPlatformAuthState = vi.fn();
5
- const syncPlatformAuthStateToResponse = vi.fn();
6
-
7
- vi.mock("@/lib/platform/client.server", () => ({
8
- changePassword,
9
- }));
10
-
11
- vi.mock("@/lib/platform/session.server", () => ({
12
- readPlatformAuthState,
13
- syncPlatformAuthStateToResponse,
14
- }));
15
-
16
- describe("password change route", () => {
17
- beforeEach(() => {
18
- vi.clearAllMocks();
19
- });
20
-
21
- it("rejects invalid password payloads before proxying upstream", async () => {
22
- readPlatformAuthState.mockResolvedValue({
23
- sessionId: "session-1",
24
- csrfToken: "csrf-1",
25
- });
26
-
27
- const route = await import("./route");
28
- const response = await route.POST(
29
- new Request("http://localhost/api/auth/password-change", {
30
- method: "POST",
31
- headers: { "content-type": "application/json" },
32
- body: JSON.stringify({
33
- current_password: "old",
34
- new_password: "new-password",
35
- confirm_password: "different-password",
36
- }),
37
- }),
38
- );
39
-
40
- expect(changePassword).not.toHaveBeenCalled();
41
- expect(response.status).toBe(400);
42
- });
43
-
44
- it("proxies valid password changes through the BFF", async () => {
45
- const authState = { sessionId: "session-1", csrfToken: "csrf-1" };
46
-
47
- readPlatformAuthState.mockResolvedValue(authState);
48
- changePassword.mockResolvedValue({
49
- data: { has_password: true },
50
- authState,
51
- });
52
-
53
- const route = await import("./route");
54
- const response = await route.POST(
55
- new Request("http://localhost/api/auth/password-change", {
56
- method: "POST",
57
- headers: { "content-type": "application/json" },
58
- body: JSON.stringify({
59
- current_password: "old-password",
60
- new_password: "new-password",
61
- confirm_password: "new-password",
62
- }),
63
- }),
64
- );
65
-
66
- expect(changePassword).toHaveBeenCalledWith(authState, {
67
- current_password: "old-password",
68
- new_password: "new-password",
69
- confirm_password: "new-password",
70
- });
71
- expect(syncPlatformAuthStateToResponse).toHaveBeenCalledWith(
72
- response,
73
- authState,
74
- authState,
75
- );
76
- expect(response.status).toBe(200);
77
- await expect(response.json()).resolves.toEqual({ has_password: true });
78
- });
79
- });
@@ -1,40 +0,0 @@
1
- import { NextResponse } from "next/server";
2
-
3
- import { changePassword } from "@/lib/platform/client.server";
4
- import { passwordChangeRequestSchema } from "@/lib/platform/contracts";
5
- import { toPlatformErrorResponse } from "@/lib/platform/route-response";
6
- import {
7
- readPlatformAuthState,
8
- syncPlatformAuthStateToResponse,
9
- } from "@/lib/platform/session.server";
10
-
11
- export async function POST(request: Request) {
12
- const authState = await readPlatformAuthState();
13
-
14
- if (!authState.sessionId) {
15
- return NextResponse.json({ detail: "Authentication required." }, { status: 401 });
16
- }
17
-
18
- const body = await request.json().catch(() => null);
19
- const parsed = passwordChangeRequestSchema.safeParse(body);
20
-
21
- if (!parsed.success) {
22
- return NextResponse.json(
23
- { detail: "Invalid password change payload." },
24
- { status: 400 },
25
- );
26
- }
27
-
28
- try {
29
- const result = await changePassword(authState, parsed.data);
30
- const response = NextResponse.json(result.data, { status: 200 });
31
- syncPlatformAuthStateToResponse(response, authState, result.authState);
32
- return response;
33
- } catch (error) {
34
- return toPlatformErrorResponse(
35
- error,
36
- "Unable to change password.",
37
- authState,
38
- );
39
- }
40
- }