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,661 @@
1
+ import "server-only";
2
+
3
+ import { z, type ZodSchema } from "zod";
4
+
5
+ import type {
6
+ AuthenticatedPlatformSession,
7
+ CommandDispatchRequest,
8
+ CommandRun,
9
+ LoginRequest,
10
+ PasswordChangeRequest,
11
+ PasswordStatus,
12
+ PlatformSession,
13
+ TenantSessionContextRequest,
14
+ } from "@/lib/platform/contracts";
15
+ import {
16
+ commandDispatchRequestSchema,
17
+ commandRunSchema,
18
+ loginRequestSchema,
19
+ passwordChangeRequestSchema,
20
+ passwordStatusSchema,
21
+ platformErrorPayloadSchema,
22
+ tenantSessionContextRequestSchema,
23
+ upstreamPasswordChangeRequestSchema,
24
+ upstreamSessionPayloadSchema,
25
+ } from "@/lib/platform/contracts";
26
+ import {
27
+ platformAuthEndpoints,
28
+ platformGatewayEndpoints,
29
+ } from "@/lib/platform/endpoints.server";
30
+ import type { PlatformAuthState } from "@/lib/platform/session.server";
31
+
32
+ export type PlatformResult<T> = {
33
+ data: T;
34
+ authState: PlatformAuthState;
35
+ };
36
+
37
+ export class PlatformClientError extends Error {
38
+ status: number;
39
+ code: string | null;
40
+ authState: PlatformAuthState | null;
41
+
42
+ constructor(
43
+ message: string,
44
+ status: number,
45
+ code: string | null = null,
46
+ authState: PlatformAuthState | null = null,
47
+ ) {
48
+ super(message);
49
+ this.name = "PlatformClientError";
50
+ this.status = status;
51
+ this.code = code;
52
+ this.authState = authState;
53
+ }
54
+ }
55
+
56
+ function normalizeAuthState(
57
+ input?: Partial<PlatformAuthState> | string | null,
58
+ ): PlatformAuthState {
59
+ if (typeof input === "string" || input == null) {
60
+ return {
61
+ sessionId: input ?? null,
62
+ csrfToken: null,
63
+ };
64
+ }
65
+
66
+ return {
67
+ sessionId: input.sessionId ?? null,
68
+ csrfToken: input.csrfToken ?? null,
69
+ };
70
+ }
71
+
72
+ function mergeAuthState(
73
+ currentAuthState: PlatformAuthState,
74
+ partialAuthState?: Partial<PlatformAuthState>,
75
+ ): PlatformAuthState {
76
+ if (!partialAuthState) {
77
+ return currentAuthState;
78
+ }
79
+
80
+ return {
81
+ sessionId:
82
+ partialAuthState.sessionId === undefined
83
+ ? currentAuthState.sessionId
84
+ : partialAuthState.sessionId,
85
+ csrfToken:
86
+ partialAuthState.csrfToken === undefined
87
+ ? currentAuthState.csrfToken
88
+ : partialAuthState.csrfToken,
89
+ };
90
+ }
91
+
92
+ function getRequestTarget(input: string) {
93
+ try {
94
+ return new URL(input).origin;
95
+ } catch {
96
+ return input;
97
+ }
98
+ }
99
+
100
+ function toPlatformUnavailableError(
101
+ input: string,
102
+ cause: unknown,
103
+ authState: PlatformAuthState,
104
+ ) {
105
+ const error = new PlatformClientError(
106
+ `Unable to reach the platform backend at ${getRequestTarget(input)}.`,
107
+ 503,
108
+ "platform_unavailable",
109
+ authState,
110
+ ) as PlatformClientError & { cause?: unknown };
111
+ error.cause = cause;
112
+ return error;
113
+ }
114
+
115
+ function splitCombinedSetCookieHeader(value: string) {
116
+ const cookies: string[] = [];
117
+ let start = 0;
118
+ let inExpiresAttribute = false;
119
+
120
+ for (let index = 0; index < value.length; index += 1) {
121
+ const character = value[index];
122
+
123
+ if (value.slice(index, index + 8).toLowerCase() === "expires=") {
124
+ inExpiresAttribute = true;
125
+ continue;
126
+ }
127
+
128
+ if (inExpiresAttribute && character === ";") {
129
+ inExpiresAttribute = false;
130
+ continue;
131
+ }
132
+
133
+ if (character === "," && !inExpiresAttribute) {
134
+ cookies.push(value.slice(start, index).trim());
135
+ start = index + 1;
136
+ }
137
+ }
138
+
139
+ const finalCookie = value.slice(start).trim();
140
+ if (finalCookie) {
141
+ cookies.push(finalCookie);
142
+ }
143
+
144
+ return cookies.filter(Boolean);
145
+ }
146
+
147
+ function getSetCookieHeaders(headers: Headers) {
148
+ const typedHeaders = headers as Headers & {
149
+ getSetCookie?: () => string[];
150
+ };
151
+
152
+ if (typeof typedHeaders.getSetCookie === "function") {
153
+ return typedHeaders.getSetCookie();
154
+ }
155
+
156
+ const combinedHeader = headers.get("set-cookie");
157
+ return combinedHeader ? splitCombinedSetCookieHeader(combinedHeader) : [];
158
+ }
159
+
160
+ function extractCookieValue(name: string, cookieHeader: string) {
161
+ const match = cookieHeader.match(new RegExp(`${name}=([^;]*)`));
162
+
163
+ if (!match) {
164
+ return undefined;
165
+ }
166
+
167
+ const value = match[1]?.trim() ?? "";
168
+ return value || null;
169
+ }
170
+
171
+ function extractAuthStateFromHeaders(headers: Headers): Partial<PlatformAuthState> {
172
+ const setCookieHeaders = getSetCookieHeaders(headers);
173
+ const nextAuthState: Partial<PlatformAuthState> = {};
174
+
175
+ for (const cookieHeader of setCookieHeaders) {
176
+ const sessionId = extractCookieValue("sessionid", cookieHeader);
177
+ const csrfToken = extractCookieValue("csrftoken", cookieHeader);
178
+
179
+ if (sessionId !== undefined) {
180
+ nextAuthState.sessionId = sessionId;
181
+ }
182
+
183
+ if (csrfToken !== undefined) {
184
+ nextAuthState.csrfToken = csrfToken;
185
+ }
186
+ }
187
+
188
+ return nextAuthState;
189
+ }
190
+
191
+ async function parseJson(response: Response): Promise<unknown> {
192
+ const text = await response.text();
193
+
194
+ if (!text) {
195
+ return null;
196
+ }
197
+
198
+ const trimmedText = text.trim();
199
+ const contentType = response.headers.get("content-type") ?? "";
200
+ const looksLikeJson =
201
+ contentType.includes("application/json") ||
202
+ trimmedText.startsWith("{") ||
203
+ trimmedText.startsWith("[");
204
+
205
+ if (!looksLikeJson) {
206
+ return trimmedText;
207
+ }
208
+
209
+ try {
210
+ return JSON.parse(trimmedText) as unknown;
211
+ } catch {
212
+ return trimmedText;
213
+ }
214
+ }
215
+
216
+ function extractErrorDetails(data: unknown) {
217
+ if (typeof data === "string" && data.trim()) {
218
+ const snippet = data.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
219
+
220
+ return {
221
+ message: snippet.slice(0, 240) || "Platform request failed",
222
+ code: null,
223
+ };
224
+ }
225
+
226
+ const parsed = platformErrorPayloadSchema.safeParse(data);
227
+
228
+ if (!parsed.success) {
229
+ return {
230
+ message: "Platform request failed",
231
+ code: null,
232
+ };
233
+ }
234
+
235
+ return {
236
+ message: parsed.data.detail ?? parsed.data.error ?? "Platform request failed",
237
+ code: parsed.data.code ?? null,
238
+ };
239
+ }
240
+
241
+ function toAuthenticatedPlatformSession(
242
+ payload: z.infer<typeof upstreamSessionPayloadSchema>,
243
+ ): AuthenticatedPlatformSession {
244
+ return {
245
+ authenticated: true,
246
+ user: payload.user,
247
+ active_tenant_id: payload.active_tenant_id,
248
+ active_tenant: payload.active_tenant,
249
+ memberships: payload.memberships,
250
+ };
251
+ }
252
+
253
+ function platformCookieHeader(authState: PlatformAuthState) {
254
+ const cookieParts = [
255
+ authState.sessionId ? `sessionid=${authState.sessionId}` : null,
256
+ authState.csrfToken ? `csrftoken=${authState.csrfToken}` : null,
257
+ ].filter(Boolean);
258
+
259
+ return cookieParts.length > 0 ? cookieParts.join("; ") : null;
260
+ }
261
+
262
+ function platformHeaders(
263
+ input: string,
264
+ authState: PlatformAuthState,
265
+ headers?: HeadersInit,
266
+ includeCsrf = false,
267
+ ) {
268
+ const resolvedHeaders = new Headers(headers);
269
+ const cookieHeader = platformCookieHeader(authState);
270
+
271
+ resolvedHeaders.set("Accept", "application/json");
272
+
273
+ if (cookieHeader) {
274
+ resolvedHeaders.set("Cookie", cookieHeader);
275
+ }
276
+
277
+ if (includeCsrf && authState.csrfToken) {
278
+ resolvedHeaders.set("X-CSRFToken", authState.csrfToken);
279
+ try {
280
+ const target = new URL(input);
281
+ resolvedHeaders.set("Origin", target.origin);
282
+ resolvedHeaders.set("Referer", input);
283
+ } catch {
284
+ // Platform endpoints are expected to be absolute URLs, but keep the
285
+ // header helper tolerant if a caller ever passes a relative path.
286
+ }
287
+ }
288
+
289
+ return resolvedHeaders;
290
+ }
291
+
292
+ async function performPlatformFetch(
293
+ input: string,
294
+ init: RequestInit,
295
+ authState: PlatformAuthState,
296
+ includeCsrf = false,
297
+ ) {
298
+ let response: Response;
299
+
300
+ try {
301
+ response = await fetch(input, {
302
+ ...init,
303
+ cache: init.cache ?? "no-store",
304
+ headers: platformHeaders(input, authState, init.headers, includeCsrf),
305
+ });
306
+ } catch (error) {
307
+ throw toPlatformUnavailableError(input, error, authState);
308
+ }
309
+
310
+ return {
311
+ response,
312
+ authState: mergeAuthState(authState, extractAuthStateFromHeaders(response.headers)),
313
+ };
314
+ }
315
+
316
+ async function ensureCsrfToken(authState: PlatformAuthState) {
317
+ if (authState.csrfToken) {
318
+ return authState;
319
+ }
320
+
321
+ const { response, authState: nextAuthState } = await performPlatformFetch(
322
+ platformAuthEndpoints.currentSession,
323
+ { method: "GET" },
324
+ authState,
325
+ );
326
+ const data = await parseJson(response);
327
+
328
+ if (response.ok || response.status === 401) {
329
+ if (nextAuthState.csrfToken) {
330
+ return nextAuthState;
331
+ }
332
+
333
+ throw new PlatformClientError(
334
+ "Platform did not return a CSRF cookie.",
335
+ 502,
336
+ "missing_csrf_cookie",
337
+ nextAuthState,
338
+ );
339
+ }
340
+
341
+ const { message, code } = extractErrorDetails(data);
342
+ throw new PlatformClientError(message, response.status, code, nextAuthState);
343
+ }
344
+
345
+ async function requestJson<T>(
346
+ input: string,
347
+ schema: ZodSchema<T>,
348
+ init: RequestInit,
349
+ options?: {
350
+ authState?: Partial<PlatformAuthState> | string | null;
351
+ requireCsrf?: boolean;
352
+ },
353
+ ): Promise<PlatformResult<T>> {
354
+ let authState = normalizeAuthState(options?.authState);
355
+
356
+ if (options?.requireCsrf) {
357
+ authState = await ensureCsrfToken(authState);
358
+ }
359
+
360
+ const { response, authState: nextAuthState } = await performPlatformFetch(
361
+ input,
362
+ init,
363
+ authState,
364
+ options?.requireCsrf ?? false,
365
+ );
366
+ const data = await parseJson(response);
367
+
368
+ if (!response.ok) {
369
+ const { message, code } = extractErrorDetails(data);
370
+ throw new PlatformClientError(message, response.status, code, nextAuthState);
371
+ }
372
+
373
+ return {
374
+ data: schema.parse(data),
375
+ authState: nextAuthState,
376
+ };
377
+ }
378
+
379
+ export async function fetchCurrentSession(
380
+ authInput: Partial<PlatformAuthState> | string | null,
381
+ ): Promise<PlatformResult<AuthenticatedPlatformSession>> {
382
+ const result = await requestJson(
383
+ platformAuthEndpoints.currentSession,
384
+ upstreamSessionPayloadSchema,
385
+ {
386
+ method: "GET",
387
+ },
388
+ {
389
+ authState: authInput,
390
+ },
391
+ );
392
+
393
+ return {
394
+ data: toAuthenticatedPlatformSession(result.data),
395
+ authState: result.authState,
396
+ };
397
+ }
398
+
399
+ export async function loadCurrentSession(
400
+ authInput: Partial<PlatformAuthState> | string | null,
401
+ ): Promise<PlatformResult<PlatformSession>> {
402
+ const authState = normalizeAuthState(authInput);
403
+
404
+ if (!authState.sessionId) {
405
+ return {
406
+ data: {
407
+ authenticated: false,
408
+ },
409
+ authState: authState.csrfToken ? authState : await ensureCsrfToken(authState),
410
+ };
411
+ }
412
+
413
+ try {
414
+ return await fetchCurrentSession(authState);
415
+ } catch (error) {
416
+ if (error instanceof PlatformClientError) {
417
+ if (error.status === 401) {
418
+ return {
419
+ data: { authenticated: false },
420
+ authState: {
421
+ ...normalizeAuthState(error.authState),
422
+ sessionId: null,
423
+ },
424
+ };
425
+ }
426
+
427
+ if (error.status >= 500) {
428
+ return {
429
+ data: { authenticated: false },
430
+ authState: normalizeAuthState(error.authState ?? authState),
431
+ };
432
+ }
433
+ }
434
+
435
+ throw error;
436
+ }
437
+ }
438
+
439
+ export async function resolveCurrentSession(
440
+ authInput: Partial<PlatformAuthState> | string | null,
441
+ ): Promise<PlatformSession> {
442
+ return (await loadCurrentSession(authInput)).data;
443
+ }
444
+
445
+ export async function loginWithPassword(
446
+ authInput: Partial<PlatformAuthState> | string | null,
447
+ credentials: LoginRequest,
448
+ ): Promise<PlatformResult<AuthenticatedPlatformSession>> {
449
+ const payload = loginRequestSchema.parse(credentials);
450
+ const result = await requestJson(
451
+ platformAuthEndpoints.login,
452
+ upstreamSessionPayloadSchema,
453
+ {
454
+ method: "POST",
455
+ headers: {
456
+ "Content-Type": "application/json",
457
+ },
458
+ body: JSON.stringify(payload),
459
+ },
460
+ {
461
+ authState: authInput,
462
+ requireCsrf: true,
463
+ },
464
+ );
465
+
466
+ if (!result.authState.sessionId) {
467
+ throw new PlatformClientError(
468
+ "Platform login did not return a session cookie.",
469
+ 502,
470
+ "missing_session_cookie",
471
+ result.authState,
472
+ );
473
+ }
474
+
475
+ return {
476
+ data: toAuthenticatedPlatformSession(result.data),
477
+ authState: result.authState,
478
+ };
479
+ }
480
+
481
+ export async function logoutPlatformSession(
482
+ authInput: Partial<PlatformAuthState> | string | null,
483
+ ): Promise<PlatformResult<null>> {
484
+ const authState = normalizeAuthState(authInput);
485
+
486
+ if (!authState.sessionId) {
487
+ return {
488
+ data: null,
489
+ authState: {
490
+ ...authState,
491
+ sessionId: null,
492
+ },
493
+ };
494
+ }
495
+
496
+ const result = await requestJson(
497
+ platformAuthEndpoints.logout,
498
+ z.null(),
499
+ {
500
+ method: "POST",
501
+ },
502
+ {
503
+ authState,
504
+ requireCsrf: true,
505
+ },
506
+ );
507
+
508
+ return {
509
+ data: null,
510
+ authState: {
511
+ ...result.authState,
512
+ sessionId: null,
513
+ },
514
+ };
515
+ }
516
+
517
+ export async function fetchSessionContext(
518
+ authInput: Partial<PlatformAuthState> | string | null,
519
+ ): Promise<PlatformResult<AuthenticatedPlatformSession>> {
520
+ const result = await requestJson(
521
+ platformAuthEndpoints.sessionContext,
522
+ upstreamSessionPayloadSchema,
523
+ {
524
+ method: "GET",
525
+ },
526
+ {
527
+ authState: authInput,
528
+ },
529
+ );
530
+
531
+ return {
532
+ data: toAuthenticatedPlatformSession(result.data),
533
+ authState: result.authState,
534
+ };
535
+ }
536
+
537
+ export async function updateSessionContext(
538
+ authInput: Partial<PlatformAuthState> | string | null,
539
+ request: TenantSessionContextRequest,
540
+ ): Promise<PlatformResult<AuthenticatedPlatformSession>> {
541
+ const payload = tenantSessionContextRequestSchema.parse(request);
542
+ const result = await requestJson(
543
+ platformAuthEndpoints.sessionContext,
544
+ upstreamSessionPayloadSchema,
545
+ {
546
+ method: "PUT",
547
+ headers: {
548
+ "Content-Type": "application/json",
549
+ },
550
+ body: JSON.stringify(payload),
551
+ },
552
+ {
553
+ authState: authInput,
554
+ requireCsrf: true,
555
+ },
556
+ );
557
+
558
+ return {
559
+ data: toAuthenticatedPlatformSession(result.data),
560
+ authState: result.authState,
561
+ };
562
+ }
563
+
564
+ export async function resetSessionContext(
565
+ authInput: Partial<PlatformAuthState> | string | null,
566
+ ): Promise<PlatformResult<AuthenticatedPlatformSession>> {
567
+ const result = await requestJson(
568
+ platformAuthEndpoints.sessionContext,
569
+ upstreamSessionPayloadSchema,
570
+ {
571
+ method: "DELETE",
572
+ },
573
+ {
574
+ authState: authInput,
575
+ requireCsrf: true,
576
+ },
577
+ );
578
+
579
+ return {
580
+ data: toAuthenticatedPlatformSession(result.data),
581
+ authState: result.authState,
582
+ };
583
+ }
584
+
585
+ export async function fetchPasswordStatus(
586
+ authInput: Partial<PlatformAuthState> | string | null,
587
+ ): Promise<PlatformResult<PasswordStatus>> {
588
+ return requestJson(
589
+ platformAuthEndpoints.passwordStatus,
590
+ passwordStatusSchema,
591
+ {
592
+ method: "GET",
593
+ },
594
+ {
595
+ authState: authInput,
596
+ },
597
+ );
598
+ }
599
+
600
+ export async function changePassword(
601
+ authInput: Partial<PlatformAuthState> | string | null,
602
+ request: PasswordChangeRequest,
603
+ ): Promise<PlatformResult<PasswordStatus>> {
604
+ const payload = passwordChangeRequestSchema.parse(request);
605
+
606
+ return requestJson(
607
+ platformAuthEndpoints.passwordChange,
608
+ passwordStatusSchema,
609
+ {
610
+ method: "POST",
611
+ headers: {
612
+ "Content-Type": "application/json",
613
+ },
614
+ body: JSON.stringify(
615
+ upstreamPasswordChangeRequestSchema.parse({
616
+ current_password: payload.current_password,
617
+ new_password: payload.new_password,
618
+ }),
619
+ ),
620
+ },
621
+ {
622
+ authState: authInput,
623
+ requireCsrf: true,
624
+ },
625
+ );
626
+ }
627
+
628
+ export async function dispatchTenantCommand(
629
+ authInput: Partial<PlatformAuthState> | string | null,
630
+ request: CommandDispatchRequest,
631
+ ): Promise<PlatformResult<CommandRun>> {
632
+ const payload = commandDispatchRequestSchema.parse(request);
633
+
634
+ return requestJson(platformGatewayEndpoints.tenantCommands, commandRunSchema, {
635
+ method: "POST",
636
+ headers: {
637
+ "Content-Type": "application/json",
638
+ },
639
+ body: JSON.stringify(payload),
640
+ }, {
641
+ authState: authInput,
642
+ requireCsrf: true,
643
+ });
644
+ }
645
+
646
+ export async function fetchTenantCommandRun(
647
+ authInput: Partial<PlatformAuthState> | string | null,
648
+ runId: string,
649
+ tenantId: string,
650
+ ): Promise<PlatformResult<CommandRun>> {
651
+ return requestJson(
652
+ platformGatewayEndpoints.tenantCommandRun(runId, tenantId),
653
+ commandRunSchema,
654
+ {
655
+ method: "GET",
656
+ },
657
+ {
658
+ authState: authInput,
659
+ },
660
+ );
661
+ }