minutework 0.1.0

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 (203) hide show
  1. package/EXTERNAL_ALPHA.md +74 -0
  2. package/README.md +57 -0
  3. package/assets/claude-local/CLAUDE.md.template +45 -0
  4. package/assets/claude-local/bundle.json +22 -0
  5. package/assets/claude-local/skills/README.md +6 -0
  6. package/assets/claude-local/skills/app-pack-authoring.md +8 -0
  7. package/assets/claude-local/skills/event-bus.md +8 -0
  8. package/assets/claude-local/skills/ontology-mapping.md +8 -0
  9. package/assets/claude-local/skills/openclaw-skill-importer.md +7 -0
  10. package/assets/claude-local/skills/schema-engine.md +8 -0
  11. package/assets/claude-local/skills/secrets-runtime-bridge.md +9 -0
  12. package/assets/claude-local/skills/sidecar-generation.md +9 -0
  13. package/assets/templates/fastapi-sidecar/.env.example +8 -0
  14. package/assets/templates/fastapi-sidecar/README.md +77 -0
  15. package/assets/templates/fastapi-sidecar/poetry.lock +757 -0
  16. package/assets/templates/fastapi-sidecar/pyproject.toml +42 -0
  17. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/__init__.py +3 -0
  18. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/auth.py +70 -0
  19. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/bridge/__init__.py +3 -0
  20. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/bridge/client.py +71 -0
  21. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/logging_utils.py +25 -0
  22. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/main.py +85 -0
  23. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/receipts.py +24 -0
  24. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/settings.py +41 -0
  25. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/template_validation.py +26 -0
  26. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/worker.py +33 -0
  27. package/assets/templates/fastapi-sidecar/template.json +43 -0
  28. package/assets/templates/fastapi-sidecar/template.schema.json +160 -0
  29. package/assets/templates/fastapi-sidecar/tests/conftest.py +36 -0
  30. package/assets/templates/fastapi-sidecar/tests/test_app.py +39 -0
  31. package/assets/templates/fastapi-sidecar/tests/test_auth.py +32 -0
  32. package/assets/templates/fastapi-sidecar/tests/test_bridge_client.py +31 -0
  33. package/assets/templates/fastapi-sidecar/tests/test_materialization.py +55 -0
  34. package/assets/templates/fastapi-sidecar/tests/test_template_contract.py +49 -0
  35. package/assets/templates/fastapi-sidecar/tests/test_worker.py +7 -0
  36. package/assets/templates/fastapi-sidecar/tools/template/validate_template.py +20 -0
  37. package/assets/templates/next-tenant-app/.env.example +8 -0
  38. package/assets/templates/next-tenant-app/.storybook/main.ts +19 -0
  39. package/assets/templates/next-tenant-app/.storybook/preview.tsx +38 -0
  40. package/assets/templates/next-tenant-app/README.md +115 -0
  41. package/assets/templates/next-tenant-app/components.json +21 -0
  42. package/assets/templates/next-tenant-app/eslint.config.mjs +41 -0
  43. package/assets/templates/next-tenant-app/next-env.d.ts +6 -0
  44. package/assets/templates/next-tenant-app/next.config.ts +8 -0
  45. package/assets/templates/next-tenant-app/package-lock.json +9682 -0
  46. package/assets/templates/next-tenant-app/package.json +59 -0
  47. package/assets/templates/next-tenant-app/pnpm-lock.yaml +6062 -0
  48. package/assets/templates/next-tenant-app/postcss.config.mjs +8 -0
  49. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +90 -0
  50. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +78 -0
  51. package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +31 -0
  52. package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +16 -0
  53. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +79 -0
  54. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +40 -0
  55. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +42 -0
  56. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +29 -0
  57. package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +26 -0
  58. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +40 -0
  59. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +47 -0
  60. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +43 -0
  61. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +45 -0
  62. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +83 -0
  63. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +30 -0
  64. package/assets/templates/next-tenant-app/src/app/app/layout.tsx +20 -0
  65. package/assets/templates/next-tenant-app/src/app/app/page.test.ts +62 -0
  66. package/assets/templates/next-tenant-app/src/app/app/page.tsx +24 -0
  67. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +70 -0
  68. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +57 -0
  69. package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +42 -0
  70. package/assets/templates/next-tenant-app/src/app/blog/page.tsx +37 -0
  71. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +70 -0
  72. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +55 -0
  73. package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +42 -0
  74. package/assets/templates/next-tenant-app/src/app/docs/page.tsx +37 -0
  75. package/assets/templates/next-tenant-app/src/app/globals.css +70 -0
  76. package/assets/templates/next-tenant-app/src/app/layout.tsx +69 -0
  77. package/assets/templates/next-tenant-app/src/app/login/page.test.ts +55 -0
  78. package/assets/templates/next-tenant-app/src/app/login/page.tsx +33 -0
  79. package/assets/templates/next-tenant-app/src/app/page.test.ts +56 -0
  80. package/assets/templates/next-tenant-app/src/app/page.tsx +35 -0
  81. package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +55 -0
  82. package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +35 -0
  83. package/assets/templates/next-tenant-app/src/app/providers.tsx +25 -0
  84. package/assets/templates/next-tenant-app/src/app/robots.test.ts +20 -0
  85. package/assets/templates/next-tenant-app/src/app/robots.ts +18 -0
  86. package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +49 -0
  87. package/assets/templates/next-tenant-app/src/app/sitemap.ts +54 -0
  88. package/assets/templates/next-tenant-app/src/components/ui/button.tsx +59 -0
  89. package/assets/templates/next-tenant-app/src/components/ui/input.tsx +21 -0
  90. package/assets/templates/next-tenant-app/src/design-system/docs/governance.mdx +26 -0
  91. package/assets/templates/next-tenant-app/src/design-system/patterns/panel-frame.stories.tsx +48 -0
  92. package/assets/templates/next-tenant-app/src/design-system/patterns/panel-frame.tsx +26 -0
  93. package/assets/templates/next-tenant-app/src/design-system/patterns/status-badge.stories.tsx +26 -0
  94. package/assets/templates/next-tenant-app/src/design-system/patterns/status-badge.tsx +35 -0
  95. package/assets/templates/next-tenant-app/src/design-system/patterns/theme-mode-toggle.stories.tsx +21 -0
  96. package/assets/templates/next-tenant-app/src/design-system/patterns/theme-mode-toggle.tsx +75 -0
  97. package/assets/templates/next-tenant-app/src/design-system/primitives/button.stories.tsx +37 -0
  98. package/assets/templates/next-tenant-app/src/design-system/primitives/button.ts +1 -0
  99. package/assets/templates/next-tenant-app/src/design-system/primitives/input.stories.tsx +26 -0
  100. package/assets/templates/next-tenant-app/src/design-system/primitives/input.ts +1 -0
  101. package/assets/templates/next-tenant-app/src/design-system/recipes/chrome.ts +28 -0
  102. package/assets/templates/next-tenant-app/src/design-system/tokens/foundation.css +31 -0
  103. package/assets/templates/next-tenant-app/src/design-system/tokens/index.css +3 -0
  104. package/assets/templates/next-tenant-app/src/design-system/tokens/manifest.json +85 -0
  105. package/assets/templates/next-tenant-app/src/design-system/tokens/manifest.ts +87 -0
  106. package/assets/templates/next-tenant-app/src/design-system/tokens/semantic.css +105 -0
  107. package/assets/templates/next-tenant-app/src/design-system/tokens/theme.css +59 -0
  108. package/assets/templates/next-tenant-app/src/design-system/tokens/tokens.stories.tsx +71 -0
  109. package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +198 -0
  110. package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +153 -0
  111. package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +342 -0
  112. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +66 -0
  113. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +108 -0
  114. package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +111 -0
  115. package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +111 -0
  116. package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +624 -0
  117. package/assets/templates/next-tenant-app/src/lib/app-routes.test.ts +20 -0
  118. package/assets/templates/next-tenant-app/src/lib/app-routes.ts +59 -0
  119. package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +189 -0
  120. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +318 -0
  121. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +232 -0
  122. package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +339 -0
  123. package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +5 -0
  124. package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +96 -0
  125. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +75 -0
  126. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +25 -0
  127. package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +170 -0
  128. package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +661 -0
  129. package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +131 -0
  130. package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +34 -0
  131. package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +102 -0
  132. package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +87 -0
  133. package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +33 -0
  134. package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +108 -0
  135. package/assets/templates/next-tenant-app/src/lib/public-site.test.ts +20 -0
  136. package/assets/templates/next-tenant-app/src/lib/public-site.ts +49 -0
  137. package/assets/templates/next-tenant-app/src/lib/theme-config.ts +10 -0
  138. package/assets/templates/next-tenant-app/src/lib/theme.tsx +159 -0
  139. package/assets/templates/next-tenant-app/src/lib/utils.ts +6 -0
  140. package/assets/templates/next-tenant-app/template.json +27 -0
  141. package/assets/templates/next-tenant-app/template.schema.json +160 -0
  142. package/assets/templates/next-tenant-app/test/server-only-stub.ts +1 -0
  143. package/assets/templates/next-tenant-app/tools/design-system/build-token-manifest.mjs +3 -0
  144. package/assets/templates/next-tenant-app/tools/design-system/check-imports.mjs +9 -0
  145. package/assets/templates/next-tenant-app/tools/design-system/check-stories.mjs +9 -0
  146. package/assets/templates/next-tenant-app/tools/design-system/check-values.mjs +9 -0
  147. package/assets/templates/next-tenant-app/tools/design-system/checks.mjs +238 -0
  148. package/assets/templates/next-tenant-app/tools/design-system/eslint-plugin-design-system.mjs +184 -0
  149. package/assets/templates/next-tenant-app/tools/design-system/playwright.config.mjs +34 -0
  150. package/assets/templates/next-tenant-app/tools/design-system/run-checks.mjs +22 -0
  151. package/assets/templates/next-tenant-app/tools/design-system/shared.mjs +166 -0
  152. package/assets/templates/next-tenant-app/tools/design-system/visual.spec.ts +41 -0
  153. package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +39 -0
  154. package/assets/templates/next-tenant-app/tools/template/validate-template.mjs +45 -0
  155. package/assets/templates/next-tenant-app/tsconfig.json +42 -0
  156. package/assets/templates/next-tenant-app/vitest.config.ts +25 -0
  157. package/bin/minutework.js +40 -0
  158. package/dist/auth.d.ts +59 -0
  159. package/dist/auth.js +338 -0
  160. package/dist/auth.js.map +1 -0
  161. package/dist/browser.d.ts +1 -0
  162. package/dist/browser.js +26 -0
  163. package/dist/browser.js.map +1 -0
  164. package/dist/cli.d.ts +2 -0
  165. package/dist/cli.js +5 -0
  166. package/dist/cli.js.map +1 -0
  167. package/dist/compile.d.ts +20 -0
  168. package/dist/compile.js +121 -0
  169. package/dist/compile.js.map +1 -0
  170. package/dist/config.d.ts +25 -0
  171. package/dist/config.js +102 -0
  172. package/dist/config.js.map +1 -0
  173. package/dist/deploy-state.d.ts +35 -0
  174. package/dist/deploy-state.js +30 -0
  175. package/dist/deploy-state.js.map +1 -0
  176. package/dist/deploy.d.ts +22 -0
  177. package/dist/deploy.js +308 -0
  178. package/dist/deploy.js.map +1 -0
  179. package/dist/developer-client.d.ts +88 -0
  180. package/dist/developer-client.js +78 -0
  181. package/dist/developer-client.js.map +1 -0
  182. package/dist/index.d.ts +27 -0
  183. package/dist/index.js +290 -0
  184. package/dist/index.js.map +1 -0
  185. package/dist/init.d.ts +22 -0
  186. package/dist/init.js +421 -0
  187. package/dist/init.js.map +1 -0
  188. package/dist/launcher.d.ts +1 -0
  189. package/dist/launcher.js +50 -0
  190. package/dist/launcher.js.map +1 -0
  191. package/dist/paths.d.ts +12 -0
  192. package/dist/paths.js +33 -0
  193. package/dist/paths.js.map +1 -0
  194. package/dist/sandbox.d.ts +30 -0
  195. package/dist/sandbox.js +852 -0
  196. package/dist/sandbox.js.map +1 -0
  197. package/dist/state.d.ts +46 -0
  198. package/dist/state.js +82 -0
  199. package/dist/state.js.map +1 -0
  200. package/dist/tokens.d.ts +14 -0
  201. package/dist/tokens.js +293 -0
  202. package/dist/tokens.js.map +1 -0
  203. package/package.json +43 -0
@@ -0,0 +1,8 @@
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ "@tailwindcss/postcss": {},
5
+ },
6
+ };
7
+
8
+ export default config;
@@ -0,0 +1,90 @@
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
+ });
@@ -0,0 +1,78 @@
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
+ }
@@ -0,0 +1,31 @@
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
+ }
@@ -0,0 +1,16 @@
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
+ }
@@ -0,0 +1,79 @@
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
+ });
@@ -0,0 +1,40 @@
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
+ }
@@ -0,0 +1,42 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const fetchPasswordStatus = vi.fn();
4
+ const readPlatformAuthState = vi.fn();
5
+ const syncPlatformAuthStateToResponse = vi.fn();
6
+
7
+ vi.mock("@/lib/platform/client.server", () => ({
8
+ fetchPasswordStatus,
9
+ }));
10
+
11
+ vi.mock("@/lib/platform/session.server", () => ({
12
+ readPlatformAuthState,
13
+ syncPlatformAuthStateToResponse,
14
+ }));
15
+
16
+ describe("password status route", () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ it("returns the proxied password status for authenticated sessions", async () => {
22
+ const authState = { sessionId: "session-1", csrfToken: "csrf-1" };
23
+
24
+ readPlatformAuthState.mockResolvedValue(authState);
25
+ fetchPasswordStatus.mockResolvedValue({
26
+ data: { has_password: true },
27
+ authState,
28
+ });
29
+
30
+ const route = await import("./route");
31
+ const response = await route.GET();
32
+
33
+ expect(fetchPasswordStatus).toHaveBeenCalledWith(authState);
34
+ expect(syncPlatformAuthStateToResponse).toHaveBeenCalledWith(
35
+ response,
36
+ authState,
37
+ authState,
38
+ );
39
+ expect(response.status).toBe(200);
40
+ await expect(response.json()).resolves.toEqual({ has_password: true });
41
+ });
42
+ });
@@ -0,0 +1,29 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ import { fetchPasswordStatus } 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
+
13
+ if (!authState.sessionId) {
14
+ return NextResponse.json({ detail: "Authentication required." }, { status: 401 });
15
+ }
16
+
17
+ try {
18
+ const result = await fetchPasswordStatus(authState);
19
+ const response = NextResponse.json(result.data, { status: 200 });
20
+ syncPlatformAuthStateToResponse(response, authState, result.authState);
21
+ return response;
22
+ } catch (error) {
23
+ return toPlatformErrorResponse(
24
+ error,
25
+ "Unable to load password status.",
26
+ authState,
27
+ );
28
+ }
29
+ }
@@ -0,0 +1,26 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ import {
4
+ loadCurrentSession,
5
+ } from "@/lib/platform/client.server";
6
+ import { toPlatformErrorResponse } from "@/lib/platform/route-response";
7
+ import {
8
+ readPlatformAuthState,
9
+ syncPlatformAuthStateToResponse,
10
+ } from "@/lib/platform/session.server";
11
+
12
+ export async function GET() {
13
+ const authState = await readPlatformAuthState();
14
+ try {
15
+ const result = await loadCurrentSession(authState);
16
+ const response = NextResponse.json(result.data, { status: 200 });
17
+ syncPlatformAuthStateToResponse(response, authState, result.authState);
18
+ return response;
19
+ } catch (error) {
20
+ return toPlatformErrorResponse(
21
+ error,
22
+ "Unable to load the current platform session.",
23
+ authState,
24
+ );
25
+ }
26
+ }
@@ -0,0 +1,40 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const fetchTenantCommandRun = vi.fn();
4
+ const readPlatformAuthState = vi.fn();
5
+ const syncPlatformAuthStateToResponse = vi.fn();
6
+
7
+ vi.mock("@/lib/platform/client.server", () => ({
8
+ fetchTenantCommandRun,
9
+ }));
10
+
11
+ vi.mock("@/lib/platform/env.server", () => ({
12
+ env: {
13
+ MW_ENABLE_RUNTIME_COMMAND_EXAMPLE: false,
14
+ },
15
+ }));
16
+
17
+ vi.mock("@/lib/platform/session.server", () => ({
18
+ readPlatformAuthState,
19
+ syncPlatformAuthStateToResponse,
20
+ }));
21
+
22
+ describe("runtime command run route", () => {
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ vi.resetModules();
26
+ });
27
+
28
+ it("returns 404 when the example feature is disabled", async () => {
29
+ const route = await import("./route");
30
+ const response = await route.GET(
31
+ new Request("http://localhost/api/gateway/commands/run-1?tenant_id=tenant-1"),
32
+ { params: Promise.resolve({ runId: "run-1" }) },
33
+ );
34
+
35
+ expect(response.status).toBe(404);
36
+ expect(readPlatformAuthState).not.toHaveBeenCalled();
37
+ expect(fetchTenantCommandRun).not.toHaveBeenCalled();
38
+ expect(syncPlatformAuthStateToResponse).not.toHaveBeenCalled();
39
+ });
40
+ });
@@ -0,0 +1,47 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ import { fetchTenantCommandRun } from "@/lib/platform/client.server";
4
+ import { env } from "@/lib/platform/env.server";
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 GET(
12
+ request: Request,
13
+ { params }: { params: Promise<{ runId: string }> },
14
+ ) {
15
+ if (!env.MW_ENABLE_RUNTIME_COMMAND_EXAMPLE) {
16
+ return new NextResponse(null, { status: 404 });
17
+ }
18
+
19
+ const authState = await readPlatformAuthState();
20
+ if (!authState.sessionId) {
21
+ return NextResponse.json({ detail: "Authentication required." }, { status: 401 });
22
+ }
23
+
24
+ const { runId } = await params;
25
+ const url = new URL(request.url);
26
+ const tenantId = url.searchParams.get("tenant_id") || "";
27
+
28
+ if (!tenantId) {
29
+ return NextResponse.json(
30
+ { detail: "Missing tenant_id query parameter." },
31
+ { status: 400 },
32
+ );
33
+ }
34
+
35
+ try {
36
+ const result = await fetchTenantCommandRun(authState, runId, tenantId);
37
+ const response = NextResponse.json(result.data, { status: 200 });
38
+ syncPlatformAuthStateToResponse(response, authState, result.authState);
39
+ return response;
40
+ } catch (error) {
41
+ return toPlatformErrorResponse(
42
+ error,
43
+ "Unable to load command status.",
44
+ authState,
45
+ );
46
+ }
47
+ }
@@ -0,0 +1,43 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const dispatchTenantCommand = vi.fn();
4
+ const readPlatformAuthState = vi.fn();
5
+ const syncPlatformAuthStateToResponse = vi.fn();
6
+
7
+ vi.mock("@/lib/platform/client.server", () => ({
8
+ dispatchTenantCommand,
9
+ }));
10
+
11
+ vi.mock("@/lib/platform/env.server", () => ({
12
+ env: {
13
+ MW_ENABLE_RUNTIME_COMMAND_EXAMPLE: false,
14
+ },
15
+ }));
16
+
17
+ vi.mock("@/lib/platform/session.server", () => ({
18
+ readPlatformAuthState,
19
+ syncPlatformAuthStateToResponse,
20
+ }));
21
+
22
+ describe("runtime command route", () => {
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ vi.resetModules();
26
+ });
27
+
28
+ it("returns 404 when the example feature is disabled", async () => {
29
+ const route = await import("./route");
30
+ const response = await route.POST(
31
+ new Request("http://localhost/api/gateway/commands", {
32
+ method: "POST",
33
+ headers: { "content-type": "application/json" },
34
+ body: JSON.stringify({ command: "deploy" }),
35
+ }),
36
+ );
37
+
38
+ expect(response.status).toBe(404);
39
+ expect(readPlatformAuthState).not.toHaveBeenCalled();
40
+ expect(dispatchTenantCommand).not.toHaveBeenCalled();
41
+ expect(syncPlatformAuthStateToResponse).not.toHaveBeenCalled();
42
+ });
43
+ });
@@ -0,0 +1,45 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ import { dispatchTenantCommand } from "@/lib/platform/client.server";
4
+ import { commandDispatchRequestSchema } from "@/lib/platform/contracts";
5
+ import { env } from "@/lib/platform/env.server";
6
+ import { toPlatformErrorResponse } from "@/lib/platform/route-response";
7
+ import {
8
+ readPlatformAuthState,
9
+ syncPlatformAuthStateToResponse,
10
+ } from "@/lib/platform/session.server";
11
+
12
+ export async function POST(request: Request) {
13
+ if (!env.MW_ENABLE_RUNTIME_COMMAND_EXAMPLE) {
14
+ return new NextResponse(null, { status: 404 });
15
+ }
16
+
17
+ const authState = await readPlatformAuthState();
18
+
19
+ if (!authState.sessionId) {
20
+ return NextResponse.json({ detail: "Authentication required." }, { status: 401 });
21
+ }
22
+
23
+ const body = await request.json().catch(() => null);
24
+ const parsed = commandDispatchRequestSchema.safeParse(body);
25
+
26
+ if (!parsed.success) {
27
+ return NextResponse.json(
28
+ { detail: "Invalid command payload." },
29
+ { status: 400 },
30
+ );
31
+ }
32
+
33
+ try {
34
+ const result = await dispatchTenantCommand(authState, parsed.data);
35
+ const response = NextResponse.json(result.data, { status: 200 });
36
+ syncPlatformAuthStateToResponse(response, authState, result.authState);
37
+ return response;
38
+ } catch (error) {
39
+ return toPlatformErrorResponse(
40
+ error,
41
+ "Unable to dispatch command.",
42
+ authState,
43
+ );
44
+ }
45
+ }