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,75 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const redirect = vi.fn((href: string) => {
4
+ throw new Error(`redirect:${href}`);
5
+ });
6
+ const resolveCurrentSession = vi.fn();
7
+ const readPlatformAuthState = vi.fn();
8
+
9
+ vi.mock("next/navigation", () => ({
10
+ redirect,
11
+ }));
12
+
13
+ vi.mock("@/lib/platform/client.server", () => ({
14
+ resolveCurrentSession,
15
+ }));
16
+
17
+ vi.mock("@/lib/platform/session.server", () => ({
18
+ readPlatformAuthState,
19
+ }));
20
+
21
+ describe("resolveAuthenticatedSession", () => {
22
+ beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ vi.resetModules();
25
+ });
26
+
27
+ it("redirects anonymous sessions to /login", async () => {
28
+ readPlatformAuthState.mockResolvedValue(null);
29
+ resolveCurrentSession.mockResolvedValue({ authenticated: false });
30
+
31
+ const auth = await import("./auth.server");
32
+
33
+ await expect(auth.resolveAuthenticatedSession()).rejects.toThrow(
34
+ "redirect:/login",
35
+ );
36
+ expect(redirect).toHaveBeenCalledWith("/login");
37
+ });
38
+
39
+ it("returns the authenticated platform session", async () => {
40
+ const session = {
41
+ authenticated: true as const,
42
+ user: {
43
+ id: "user-1",
44
+ username: "demo-user",
45
+ email: "demo@example.com",
46
+ },
47
+ active_tenant_id: "tenant-1",
48
+ active_tenant: {
49
+ tenant_id: "tenant-1",
50
+ tenant_slug: "alpha",
51
+ tenant_name: "Alpha",
52
+ role: "admin",
53
+ },
54
+ memberships: [
55
+ {
56
+ tenant_id: "tenant-1",
57
+ tenant_slug: "alpha",
58
+ tenant_name: "Alpha",
59
+ role: "admin",
60
+ },
61
+ ],
62
+ };
63
+
64
+ readPlatformAuthState.mockResolvedValue({
65
+ sessionId: "session-1",
66
+ csrfToken: "csrf-1",
67
+ });
68
+ resolveCurrentSession.mockResolvedValue(session);
69
+
70
+ const auth = await import("./auth.server");
71
+
72
+ await expect(auth.resolveAuthenticatedSession()).resolves.toEqual(session);
73
+ expect(redirect).not.toHaveBeenCalled();
74
+ });
75
+ });
@@ -0,0 +1,25 @@
1
+ import "server-only";
2
+
3
+ import { cache } from "react";
4
+ import { redirect } from "next/navigation";
5
+
6
+ import { appRoutes } from "@/lib/app-routes";
7
+ import { resolveCurrentSession } from "@/lib/platform/client.server";
8
+ import type { AuthenticatedPlatformSession } from "@/lib/platform/contracts";
9
+ import { readPlatformAuthState } from "@/lib/platform/session.server";
10
+
11
+ const loadResolvedPlatformSession = cache(async () =>
12
+ resolveCurrentSession(await readPlatformAuthState()),
13
+ );
14
+
15
+ export const resolvePlatformSession = loadResolvedPlatformSession;
16
+
17
+ export async function resolveAuthenticatedSession(): Promise<AuthenticatedPlatformSession> {
18
+ const session = await loadResolvedPlatformSession();
19
+
20
+ if (!session.authenticated) {
21
+ redirect(appRoutes.login);
22
+ }
23
+
24
+ return session;
25
+ }
@@ -0,0 +1,170 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import {
4
+ dispatchTenantCommand,
5
+ loadCurrentSession,
6
+ loginWithPassword,
7
+ } from "@/lib/platform/client.server";
8
+
9
+ const sessionPayload = {
10
+ user: {
11
+ id: "user-1",
12
+ username: "demo-user",
13
+ email: "demo@example.com",
14
+ },
15
+ active_tenant_id: "tenant-1",
16
+ active_tenant: {
17
+ tenant_id: "tenant-1",
18
+ tenant_slug: "alpha",
19
+ tenant_name: "Alpha",
20
+ role: "admin",
21
+ },
22
+ memberships: [
23
+ {
24
+ tenant_id: "tenant-1",
25
+ tenant_slug: "alpha",
26
+ tenant_name: "Alpha",
27
+ role: "admin",
28
+ },
29
+ ],
30
+ };
31
+
32
+ const commandRunPayload = {
33
+ run_id: "run-1",
34
+ tenant_id: "tenant-1",
35
+ runtime_id: "runtime-1",
36
+ template_key: "echo_hello",
37
+ status: "queued",
38
+ safe_summary: "Queued for dispatch.",
39
+ exit_code: null,
40
+ stdout_tail: "",
41
+ stderr_tail: "",
42
+ source_ref: "runtime:command:1",
43
+ dispatch_error: "",
44
+ };
45
+
46
+ function jsonResponse(
47
+ body: unknown,
48
+ init?: {
49
+ status?: number;
50
+ headers?: Record<string, string>;
51
+ },
52
+ ) {
53
+ return new Response(body === null ? null : JSON.stringify(body), {
54
+ status: init?.status ?? 200,
55
+ headers: {
56
+ "content-type": "application/json",
57
+ ...init?.headers,
58
+ },
59
+ });
60
+ }
61
+
62
+ describe("platform client", () => {
63
+ afterEach(() => {
64
+ vi.unstubAllGlobals();
65
+ });
66
+
67
+ it("bootstraps an anonymous csrf token while resolving the session", async () => {
68
+ const fetchMock = vi.fn().mockResolvedValue(
69
+ jsonResponse(
70
+ { detail: "Authentication required." },
71
+ {
72
+ status: 401,
73
+ headers: {
74
+ "set-cookie": "csrftoken=bootstrap-token; Path=/; SameSite=Lax",
75
+ },
76
+ },
77
+ ),
78
+ );
79
+
80
+ vi.stubGlobal("fetch", fetchMock);
81
+
82
+ const result = await loadCurrentSession(null);
83
+
84
+ expect(result.data).toEqual({ authenticated: false });
85
+ expect(result.authState).toEqual({
86
+ sessionId: null,
87
+ csrfToken: "bootstrap-token",
88
+ });
89
+ expect(fetchMock).toHaveBeenCalledTimes(1);
90
+ });
91
+
92
+ it("bootstraps csrf before login and persists the returned session cookies", async () => {
93
+ const fetchMock = vi
94
+ .fn()
95
+ .mockResolvedValueOnce(
96
+ jsonResponse(
97
+ { detail: "Authentication required." },
98
+ {
99
+ status: 401,
100
+ headers: {
101
+ "set-cookie": "csrftoken=bootstrap-token; Path=/; SameSite=Lax",
102
+ },
103
+ },
104
+ ),
105
+ )
106
+ .mockResolvedValueOnce(
107
+ jsonResponse(sessionPayload, {
108
+ status: 200,
109
+ headers: {
110
+ "set-cookie":
111
+ "sessionid=session-1; Path=/; HttpOnly, csrftoken=rotated-token; Path=/; SameSite=Lax",
112
+ },
113
+ }),
114
+ );
115
+
116
+ vi.stubGlobal("fetch", fetchMock);
117
+
118
+ const result = await loginWithPassword(null, {
119
+ username: "demo-user",
120
+ password: "demo-password",
121
+ });
122
+
123
+ const loginHeaders = new Headers(fetchMock.mock.calls[1]?.[1]?.headers);
124
+
125
+ expect(loginHeaders.get("x-csrftoken")).toBe("bootstrap-token");
126
+ expect(loginHeaders.get("cookie")).toBe("csrftoken=bootstrap-token");
127
+ expect(result.authState).toEqual({
128
+ sessionId: "session-1",
129
+ csrfToken: "rotated-token",
130
+ });
131
+ expect(result.data.authenticated).toBe(true);
132
+ expect(result.data.active_tenant_id).toBe("tenant-1");
133
+ });
134
+
135
+ it("forwards the stored session and csrf token on unsafe tenant mutations", async () => {
136
+ const fetchMock = vi
137
+ .fn()
138
+ .mockResolvedValueOnce(
139
+ jsonResponse(sessionPayload, {
140
+ status: 200,
141
+ headers: {
142
+ "set-cookie": "csrftoken=csrf-1; Path=/; SameSite=Lax",
143
+ },
144
+ }),
145
+ )
146
+ .mockResolvedValueOnce(jsonResponse(commandRunPayload));
147
+
148
+ vi.stubGlobal("fetch", fetchMock);
149
+
150
+ const result = await dispatchTenantCommand(
151
+ { sessionId: "session-1", csrfToken: null },
152
+ {
153
+ tenant_id: "tenant-1",
154
+ template_key: "echo_hello",
155
+ },
156
+ );
157
+
158
+ const commandHeaders = new Headers(fetchMock.mock.calls[1]?.[1]?.headers);
159
+
160
+ expect(commandHeaders.get("cookie")).toBe(
161
+ "sessionid=session-1; csrftoken=csrf-1",
162
+ );
163
+ expect(commandHeaders.get("x-csrftoken")).toBe("csrf-1");
164
+ expect(result.authState).toEqual({
165
+ sessionId: "session-1",
166
+ csrfToken: "csrf-1",
167
+ });
168
+ expect(result.data.run_id).toBe("run-1");
169
+ });
170
+ });