minutework 0.1.39 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/EXTERNAL_ALPHA.md +17 -1
  2. package/README.md +21 -1
  3. package/assets/claude-local/skills/README.md +6 -0
  4. package/assets/claude-local/skills/ai-capability-defaults/SKILL.md +3 -0
  5. package/assets/claude-local/skills/app-pack-authoring/SKILL.md +15 -0
  6. package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +25 -9
  7. package/assets/claude-local/skills/integration-broker-and-connectors/SKILL.md +122 -0
  8. package/assets/claude-local/skills/layering-and-import-modes/SKILL.md +4 -0
  9. package/assets/claude-local/skills/project-overview-and-strategy/SKILL.md +6 -0
  10. package/assets/claude-local/skills/shell-architecture/SKILL.md +15 -0
  11. package/assets/claude-local/skills/vuilder-public-site-authoring/SKILL.md +10 -4
  12. package/assets/claude-local/skills/vuilder-workspace-architecture/SKILL.md +78 -0
  13. package/assets/templates/vuilder-public-site/.env.example +11 -0
  14. package/assets/templates/vuilder-public-site/README.md +15 -0
  15. package/assets/templates/vuilder-public-site/eslint.config.mjs +6 -0
  16. package/assets/templates/vuilder-public-site/next-env.d.ts +4 -0
  17. package/assets/templates/vuilder-public-site/next.config.mjs +28 -0
  18. package/assets/templates/vuilder-public-site/package.json +39 -0
  19. package/assets/templates/vuilder-public-site/postcss.config.mjs +5 -0
  20. package/assets/templates/vuilder-public-site/src/app/api/onboarding/start/route.ts +19 -0
  21. package/assets/templates/vuilder-public-site/src/app/blog/[slug]/page.tsx +25 -0
  22. package/assets/templates/vuilder-public-site/src/app/blog/page.tsx +26 -0
  23. package/assets/templates/vuilder-public-site/src/app/globals.css +103 -0
  24. package/assets/templates/vuilder-public-site/src/app/layout.tsx +18 -0
  25. package/assets/templates/vuilder-public-site/src/app/onboarding/[[...step]]/page.tsx +39 -0
  26. package/assets/templates/vuilder-public-site/src/app/page.tsx +43 -0
  27. package/assets/templates/vuilder-public-site/src/lib/env.server.ts +31 -0
  28. package/assets/templates/vuilder-public-site/src/lib/platform.server.ts +47 -0
  29. package/assets/templates/vuilder-public-site/src/lib/public-dj.server.ts +86 -0
  30. package/assets/templates/vuilder-public-site/src/lib/public-dj.test.ts +8 -0
  31. package/assets/templates/vuilder-public-site/src/lib/routes.test.ts +13 -0
  32. package/assets/templates/vuilder-public-site/src/lib/routes.ts +12 -0
  33. package/assets/templates/vuilder-public-site/template.json +21 -0
  34. package/assets/templates/vuilder-public-site/tools/template/validate-template.mjs +44 -0
  35. package/assets/templates/vuilder-public-site/tsconfig.json +23 -0
  36. package/assets/templates/vuilder-public-site/vitest.config.ts +13 -0
  37. package/assets/templates/vuilder-shell/.env.example +8 -0
  38. package/assets/templates/vuilder-shell/.storybook/main.ts +19 -0
  39. package/assets/templates/vuilder-shell/.storybook/preview.tsx +38 -0
  40. package/assets/templates/vuilder-shell/README.md +49 -0
  41. package/assets/templates/vuilder-shell/components.json +21 -0
  42. package/assets/templates/vuilder-shell/eslint.config.mjs +41 -0
  43. package/assets/templates/vuilder-shell/next-env.d.ts +6 -0
  44. package/assets/templates/vuilder-shell/next.config.mjs +33 -0
  45. package/assets/templates/vuilder-shell/package.json +61 -0
  46. package/assets/templates/vuilder-shell/postcss.config.mjs +8 -0
  47. package/assets/templates/vuilder-shell/public/.gitkeep +1 -0
  48. package/assets/templates/vuilder-shell/src/app/api/auth/accept-shell-session/route.test.ts +105 -0
  49. package/assets/templates/vuilder-shell/src/app/api/auth/accept-shell-session/route.ts +63 -0
  50. package/assets/templates/vuilder-shell/src/app/api/auth/logout/route.test.ts +63 -0
  51. package/assets/templates/vuilder-shell/src/app/api/auth/logout/route.ts +24 -0
  52. package/assets/templates/vuilder-shell/src/app/api/auth/session/route.test.ts +70 -0
  53. package/assets/templates/vuilder-shell/src/app/api/auth/session/route.ts +27 -0
  54. package/assets/templates/vuilder-shell/src/app/app/layout.tsx +17 -0
  55. package/assets/templates/vuilder-shell/src/app/app/page.tsx +30 -0
  56. package/assets/templates/vuilder-shell/src/app/blog/[slug]/page.tsx +15 -0
  57. package/assets/templates/vuilder-shell/src/app/blog/page.tsx +15 -0
  58. package/assets/templates/vuilder-shell/src/app/docs/[...slug]/page.tsx +15 -0
  59. package/assets/templates/vuilder-shell/src/app/docs/page.tsx +15 -0
  60. package/assets/templates/vuilder-shell/src/app/globals.css +70 -0
  61. package/assets/templates/vuilder-shell/src/app/layout.tsx +69 -0
  62. package/assets/templates/vuilder-shell/src/app/login/route.test.ts +33 -0
  63. package/assets/templates/vuilder-shell/src/app/login/route.ts +21 -0
  64. package/assets/templates/vuilder-shell/src/app/page.tsx +16 -0
  65. package/assets/templates/vuilder-shell/src/app/pricing/page.tsx +15 -0
  66. package/assets/templates/vuilder-shell/src/app/providers.tsx +25 -0
  67. package/assets/templates/vuilder-shell/src/app/robots.ts +21 -0
  68. package/assets/templates/vuilder-shell/src/app/sitemap.ts +33 -0
  69. package/assets/templates/vuilder-shell/src/app/w/[workspace_slug]/connect/page.tsx +31 -0
  70. package/assets/templates/vuilder-shell/src/app/w/[workspace_slug]/page.tsx +54 -0
  71. package/assets/templates/vuilder-shell/src/components/ui/button.tsx +59 -0
  72. package/assets/templates/vuilder-shell/src/components/ui/input.tsx +21 -0
  73. package/assets/templates/vuilder-shell/src/design-system/docs/governance.mdx +26 -0
  74. package/assets/templates/vuilder-shell/src/design-system/patterns/panel-frame.stories.tsx +48 -0
  75. package/assets/templates/vuilder-shell/src/design-system/patterns/panel-frame.tsx +26 -0
  76. package/assets/templates/vuilder-shell/src/design-system/patterns/status-badge.stories.tsx +26 -0
  77. package/assets/templates/vuilder-shell/src/design-system/patterns/status-badge.tsx +35 -0
  78. package/assets/templates/vuilder-shell/src/design-system/patterns/theme-mode-toggle.stories.tsx +21 -0
  79. package/assets/templates/vuilder-shell/src/design-system/patterns/theme-mode-toggle.tsx +75 -0
  80. package/assets/templates/vuilder-shell/src/design-system/primitives/button.stories.tsx +37 -0
  81. package/assets/templates/vuilder-shell/src/design-system/primitives/button.ts +1 -0
  82. package/assets/templates/vuilder-shell/src/design-system/primitives/input.stories.tsx +26 -0
  83. package/assets/templates/vuilder-shell/src/design-system/primitives/input.ts +1 -0
  84. package/assets/templates/vuilder-shell/src/design-system/recipes/chrome.ts +28 -0
  85. package/assets/templates/vuilder-shell/src/design-system/tokens/foundation.css +31 -0
  86. package/assets/templates/vuilder-shell/src/design-system/tokens/index.css +3 -0
  87. package/assets/templates/vuilder-shell/src/design-system/tokens/manifest.json +85 -0
  88. package/assets/templates/vuilder-shell/src/design-system/tokens/manifest.ts +87 -0
  89. package/assets/templates/vuilder-shell/src/design-system/tokens/semantic.css +105 -0
  90. package/assets/templates/vuilder-shell/src/design-system/tokens/theme.css +59 -0
  91. package/assets/templates/vuilder-shell/src/design-system/tokens/tokens.stories.tsx +71 -0
  92. package/assets/templates/vuilder-shell/src/features/dashboard/components/tenant-dashboard.tsx +134 -0
  93. package/assets/templates/vuilder-shell/src/features/public-shell/components/static-public-page.tsx +58 -0
  94. package/assets/templates/vuilder-shell/src/features/shell/components/authenticated-app-layout-shell.tsx +84 -0
  95. package/assets/templates/vuilder-shell/src/features/shell/components/private-app-shell.tsx +22 -0
  96. package/assets/templates/vuilder-shell/src/features/vuilder-shell/components/vuilder-connect-screen.tsx +89 -0
  97. package/assets/templates/vuilder-shell/src/features/vuilder-shell/components/vuilder-workspace-screen.tsx +49 -0
  98. package/assets/templates/vuilder-shell/src/lib/app-routes.test.ts +37 -0
  99. package/assets/templates/vuilder-shell/src/lib/app-routes.ts +86 -0
  100. package/assets/templates/vuilder-shell/src/lib/auth-routes.server.test.ts +26 -0
  101. package/assets/templates/vuilder-shell/src/lib/auth-routes.server.ts +53 -0
  102. package/assets/templates/vuilder-shell/src/lib/http/same-origin.test.ts +23 -0
  103. package/assets/templates/vuilder-shell/src/lib/http/same-origin.ts +18 -0
  104. package/assets/templates/vuilder-shell/src/lib/platform/client.server.test.ts +201 -0
  105. package/assets/templates/vuilder-shell/src/lib/platform/client.server.ts +540 -0
  106. package/assets/templates/vuilder-shell/src/lib/platform/contracts.ts +190 -0
  107. package/assets/templates/vuilder-shell/src/lib/platform/endpoints.server.ts +29 -0
  108. package/assets/templates/vuilder-shell/src/lib/platform/env.server.ts +82 -0
  109. package/assets/templates/vuilder-shell/src/lib/platform/route-response.ts +33 -0
  110. package/assets/templates/vuilder-shell/src/lib/platform/session.server.ts +145 -0
  111. package/assets/templates/vuilder-shell/src/lib/public-site.test.ts +20 -0
  112. package/assets/templates/vuilder-shell/src/lib/public-site.ts +48 -0
  113. package/assets/templates/vuilder-shell/src/lib/theme-config.ts +10 -0
  114. package/assets/templates/vuilder-shell/src/lib/theme.tsx +159 -0
  115. package/assets/templates/vuilder-shell/src/lib/utils.ts +6 -0
  116. package/assets/templates/vuilder-shell/template.json +28 -0
  117. package/assets/templates/vuilder-shell/template.schema.json +171 -0
  118. package/assets/templates/vuilder-shell/test/server-only-stub.ts +1 -0
  119. package/assets/templates/vuilder-shell/tools/design-system/build-token-manifest.mjs +3 -0
  120. package/assets/templates/vuilder-shell/tools/design-system/check-imports.mjs +9 -0
  121. package/assets/templates/vuilder-shell/tools/design-system/check-stories.mjs +9 -0
  122. package/assets/templates/vuilder-shell/tools/design-system/check-values.mjs +9 -0
  123. package/assets/templates/vuilder-shell/tools/design-system/checks.mjs +238 -0
  124. package/assets/templates/vuilder-shell/tools/design-system/eslint-plugin-design-system.mjs +184 -0
  125. package/assets/templates/vuilder-shell/tools/design-system/playwright.config.mjs +34 -0
  126. package/assets/templates/vuilder-shell/tools/design-system/run-checks.mjs +22 -0
  127. package/assets/templates/vuilder-shell/tools/design-system/shared.mjs +166 -0
  128. package/assets/templates/vuilder-shell/tools/design-system/visual.spec.ts +41 -0
  129. package/assets/templates/vuilder-shell/tools/template/validate-route-contract.mjs +373 -0
  130. package/assets/templates/vuilder-shell/tools/template/validate-template.mjs +45 -0
  131. package/assets/templates/vuilder-shell/tools/template/with-public-site-fixture.mjs +45 -0
  132. package/assets/templates/vuilder-shell/tsconfig.json +42 -0
  133. package/assets/templates/vuilder-shell/vitest.config.ts +23 -0
  134. package/dist/auth.js +66 -14
  135. package/dist/auth.js.map +1 -1
  136. package/dist/deploy-state.d.ts +1 -0
  137. package/dist/deploy-state.js.map +1 -1
  138. package/dist/deploy.js +18 -4
  139. package/dist/deploy.js.map +1 -1
  140. package/dist/developer-client.d.ts +1 -1
  141. package/dist/index.js +12 -2
  142. package/dist/index.js.map +1 -1
  143. package/dist/init-prompt.js +21 -13
  144. package/dist/init-prompt.js.map +1 -1
  145. package/dist/init.d.ts +3 -1
  146. package/dist/init.js +103 -12
  147. package/dist/init.js.map +1 -1
  148. package/dist/orchestrator-context.js +17 -5
  149. package/dist/orchestrator-context.js.map +1 -1
  150. package/dist/orchestrator-state.d.ts +2 -2
  151. package/dist/orchestrator-state.js.map +1 -1
  152. package/dist/publish.js +12 -2
  153. package/dist/publish.js.map +1 -1
  154. package/dist/state.d.ts +2 -0
  155. package/dist/state.js +9 -0
  156. package/dist/state.js.map +1 -1
  157. package/package.json +2 -2
  158. package/vendor/workspace-mcp/context.d.ts +3 -1
  159. package/vendor/workspace-mcp/context.js +134 -21
  160. package/vendor/workspace-mcp/context.js.map +1 -1
  161. package/vendor/workspace-mcp/types.d.ts +72 -7
  162. package/vendor/workspace-mcp/types.js +8 -4
  163. package/vendor/workspace-mcp/types.js.map +1 -1
  164. package/assets/templates/fastapi-sidecar/poetry.lock +0 -757
  165. package/assets/templates/next-tenant-app/package-lock.json +0 -9682
  166. package/assets/templates/next-tenant-app/pnpm-lock.yaml +0 -6062
@@ -0,0 +1,540 @@
1
+ import "server-only";
2
+
3
+ import { z } from "zod";
4
+
5
+ import { platformAuthEndpoints } from "@/lib/platform/endpoints.server";
6
+ import { getEnv } from "@/lib/platform/env.server";
7
+ import type {
8
+ AuthenticatedPlatformSession,
9
+ PlatformSession,
10
+ PlatformUser,
11
+ SessionMembership,
12
+ ShellContext,
13
+ ShellTokenResponse,
14
+ } from "@/lib/platform/contracts";
15
+ import {
16
+ platformErrorPayloadSchema,
17
+ shellContextCanOpenWorkspace,
18
+ shellContextCanViewWorkspace,
19
+ shellContextMatchesWorkspace,
20
+ shellContextSchema,
21
+ shellTokenResponseSchema,
22
+ } from "@/lib/platform/contracts";
23
+ import type { PlatformAuthState } from "@/lib/platform/session.server";
24
+
25
+ export type PlatformResult<T> = {
26
+ data: T;
27
+ authState: PlatformAuthState;
28
+ };
29
+
30
+ export type WorkspaceShellSession = {
31
+ session: PlatformSession;
32
+ shellContext: ShellContext | null;
33
+ membership: SessionMembership | null;
34
+ workspaceCanView: boolean;
35
+ workspaceCanOpen: boolean;
36
+ };
37
+
38
+ export class PlatformClientError extends Error {
39
+ status: number;
40
+ code: string | null;
41
+ authState: PlatformAuthState | null;
42
+
43
+ constructor(
44
+ message: string,
45
+ status: number,
46
+ code: string | null = null,
47
+ authState: PlatformAuthState | null = null,
48
+ ) {
49
+ super(message);
50
+ this.name = "PlatformClientError";
51
+ this.status = status;
52
+ this.code = code;
53
+ this.authState = authState;
54
+ }
55
+ }
56
+
57
+ function normalizeAuthState(
58
+ input?: Partial<PlatformAuthState> | string | null,
59
+ ): PlatformAuthState {
60
+ if (typeof input === "string" || input == null) {
61
+ return {
62
+ shellSessionToken: input ?? null,
63
+ };
64
+ }
65
+
66
+ return {
67
+ shellSessionToken: input.shellSessionToken ?? null,
68
+ };
69
+ }
70
+
71
+ function getRequestTarget(input: string) {
72
+ try {
73
+ return new URL(input).origin;
74
+ } catch {
75
+ return input;
76
+ }
77
+ }
78
+
79
+ function toPlatformUnavailableError(
80
+ input: string,
81
+ cause: unknown,
82
+ authState: PlatformAuthState,
83
+ ) {
84
+ const error = new PlatformClientError(
85
+ `Unable to reach the platform backend at ${getRequestTarget(input)}.`,
86
+ 503,
87
+ "platform_unavailable",
88
+ authState,
89
+ ) as PlatformClientError & { cause?: unknown };
90
+ error.cause = cause;
91
+ return error;
92
+ }
93
+
94
+ function toPlatformTimeoutError(
95
+ input: string,
96
+ timeoutMs: number,
97
+ cause: unknown,
98
+ authState: PlatformAuthState,
99
+ ) {
100
+ const error = new PlatformClientError(
101
+ `Timed out waiting for the platform backend at ${getRequestTarget(
102
+ input,
103
+ )} after ${timeoutMs}ms.`,
104
+ 504,
105
+ "platform_timeout",
106
+ authState,
107
+ ) as PlatformClientError & { cause?: unknown };
108
+ error.cause = cause;
109
+ return error;
110
+ }
111
+
112
+ async function parseJson(response: Response): Promise<unknown> {
113
+ const text = await response.text();
114
+
115
+ if (!text) {
116
+ return null;
117
+ }
118
+
119
+ const trimmedText = text.trim();
120
+ const contentType = response.headers.get("content-type") ?? "";
121
+ const looksLikeJson =
122
+ contentType.includes("application/json") ||
123
+ trimmedText.startsWith("{") ||
124
+ trimmedText.startsWith("[");
125
+
126
+ if (!looksLikeJson) {
127
+ return trimmedText;
128
+ }
129
+
130
+ try {
131
+ return JSON.parse(trimmedText) as unknown;
132
+ } catch {
133
+ return trimmedText;
134
+ }
135
+ }
136
+
137
+ function extractErrorDetails(data: unknown) {
138
+ if (typeof data === "string" && data.trim()) {
139
+ const snippet = data.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
140
+
141
+ return {
142
+ message: snippet.slice(0, 240) || "Platform request failed",
143
+ code: null,
144
+ };
145
+ }
146
+
147
+ const parsed = platformErrorPayloadSchema.safeParse(data);
148
+
149
+ if (!parsed.success) {
150
+ return {
151
+ message: "Platform request failed",
152
+ code: null,
153
+ };
154
+ }
155
+
156
+ const nestedError =
157
+ typeof parsed.data.error === "object" && parsed.data.error !== null
158
+ ? parsed.data.error
159
+ : null;
160
+
161
+ return {
162
+ message:
163
+ parsed.data.detail ??
164
+ (typeof parsed.data.error === "string" ? parsed.data.error : undefined) ??
165
+ nestedError?.message ??
166
+ "Platform request failed",
167
+ code: parsed.data.code ?? nestedError?.code ?? null,
168
+ };
169
+ }
170
+
171
+ function platformHeaders(
172
+ authState: PlatformAuthState,
173
+ headers?: HeadersInit,
174
+ ) {
175
+ const resolvedHeaders = new Headers(headers);
176
+ resolvedHeaders.set("Accept", "application/json");
177
+
178
+ if (authState.shellSessionToken) {
179
+ resolvedHeaders.set("X-Vuilder-Shell-Session", authState.shellSessionToken);
180
+ }
181
+
182
+ return resolvedHeaders;
183
+ }
184
+
185
+ async function performPlatformFetch(
186
+ input: string,
187
+ init: RequestInit,
188
+ authState: PlatformAuthState,
189
+ ) {
190
+ let response: Response;
191
+ const timeoutMs = getEnv().MW_PLATFORM_FETCH_TIMEOUT_MS;
192
+ const controller = new AbortController();
193
+ let didTimeout = false;
194
+ const timeoutId = globalThis.setTimeout(() => {
195
+ didTimeout = true;
196
+ controller.abort();
197
+ }, timeoutMs);
198
+
199
+ try {
200
+ response = await fetch(input, {
201
+ ...init,
202
+ cache: init.cache ?? "no-store",
203
+ headers: platformHeaders(authState, init.headers),
204
+ signal: controller.signal,
205
+ });
206
+ } catch (error) {
207
+ if (didTimeout) {
208
+ throw toPlatformTimeoutError(input, timeoutMs, error, authState);
209
+ }
210
+ throw toPlatformUnavailableError(input, error, authState);
211
+ } finally {
212
+ globalThis.clearTimeout(timeoutId);
213
+ }
214
+
215
+ return {
216
+ response,
217
+ authState,
218
+ };
219
+ }
220
+
221
+ async function requestJson<TSchema extends z.ZodTypeAny>(
222
+ input: string,
223
+ schema: TSchema,
224
+ init: RequestInit,
225
+ options?: {
226
+ authState?: Partial<PlatformAuthState> | string | null;
227
+ },
228
+ ): Promise<PlatformResult<z.output<TSchema>>> {
229
+ const authState = normalizeAuthState(options?.authState);
230
+ const { response, authState: nextAuthState } = await performPlatformFetch(
231
+ input,
232
+ init,
233
+ authState,
234
+ );
235
+ const data = await parseJson(response);
236
+
237
+ if (!response.ok) {
238
+ const { message, code } = extractErrorDetails(data);
239
+ throw new PlatformClientError(message, response.status, code, nextAuthState);
240
+ }
241
+
242
+ return {
243
+ data: schema.parse(data),
244
+ authState: nextAuthState,
245
+ };
246
+ }
247
+
248
+ function fallbackUser(context: ShellContext): PlatformUser {
249
+ return {
250
+ id: `tenant-member:${context.tenant_id}`,
251
+ username: "member",
252
+ email: "member@localhost.invalid",
253
+ display_name: context.workspace_name,
254
+ avatar_url: "",
255
+ };
256
+ }
257
+
258
+ export function shellContextToMembership(
259
+ context: ShellContext,
260
+ ): SessionMembership {
261
+ const roleSlug = context.role_slug || context.role_slugs[0] || "member";
262
+
263
+ return {
264
+ tenant_id: context.tenant_id,
265
+ tenant_slug: context.workspace_slug,
266
+ tenant_name: context.workspace_name,
267
+ workspace_slug: context.workspace_slug,
268
+ workspace_name: context.workspace_name,
269
+ role: roleSlug,
270
+ tenant_icon_url: "",
271
+ runtime_status:
272
+ context.affordances.workspace_shell_open === true ? "ready" : "pending",
273
+ roles: context.role_slugs.map((slug, index) => ({
274
+ slug,
275
+ name: slug,
276
+ position: index,
277
+ })),
278
+ };
279
+ }
280
+
281
+ function unauthenticatedWorkspaceShellSession(): WorkspaceShellSession {
282
+ return {
283
+ session: {
284
+ authenticated: false,
285
+ },
286
+ shellContext: null,
287
+ membership: null,
288
+ workspaceCanView: false,
289
+ workspaceCanOpen: false,
290
+ };
291
+ }
292
+
293
+ function toAuthenticatedPlatformSession(
294
+ context: ShellContext,
295
+ ): AuthenticatedPlatformSession {
296
+ const membership = shellContextToMembership(context);
297
+
298
+ return {
299
+ authenticated: true,
300
+ user: context.user ?? fallbackUser(context),
301
+ active_tenant_id: context.tenant_id,
302
+ active_tenant: membership,
303
+ memberships: [membership],
304
+ onboarding_completed_for_active_workspace:
305
+ context.affordances.workspace_shell_open === true,
306
+ };
307
+ }
308
+
309
+ export async function fetchTenantMemberShellContext(
310
+ authInput: Partial<PlatformAuthState> | string | null,
311
+ ): Promise<PlatformResult<ShellContext>> {
312
+ return await requestJson(
313
+ platformAuthEndpoints.shellTokenContext,
314
+ shellContextSchema,
315
+ {
316
+ method: "GET",
317
+ },
318
+ {
319
+ authState: authInput,
320
+ },
321
+ );
322
+ }
323
+
324
+ export async function exchangeVuilderShellSessionHandoffCode(
325
+ shellSessionHandoffCode: string,
326
+ shellState: string,
327
+ ): Promise<PlatformResult<ShellTokenResponse>> {
328
+ const normalizedCode = String(shellSessionHandoffCode || "").trim();
329
+ if (!normalizedCode) {
330
+ throw new PlatformClientError(
331
+ "Shell session handoff code is required.",
332
+ 400,
333
+ "missing_shell_session_handoff_code",
334
+ { shellSessionToken: null },
335
+ );
336
+ }
337
+ const normalizedShellState = String(shellState || "").trim();
338
+ if (!normalizedShellState) {
339
+ throw new PlatformClientError(
340
+ "Shell handoff state is required.",
341
+ 400,
342
+ "missing_shell_state",
343
+ { shellSessionToken: null },
344
+ );
345
+ }
346
+
347
+ return await requestJson(
348
+ platformAuthEndpoints.shellTokenExchange,
349
+ shellTokenResponseSchema,
350
+ {
351
+ method: "POST",
352
+ headers: {
353
+ "Content-Type": "application/json",
354
+ },
355
+ body: JSON.stringify({
356
+ shell_session_handoff_code: normalizedCode,
357
+ shell_state: normalizedShellState,
358
+ }),
359
+ },
360
+ {
361
+ authState: null,
362
+ },
363
+ );
364
+ }
365
+
366
+ export async function revokeVuilderShellSession(
367
+ authInput: Partial<PlatformAuthState> | string | null,
368
+ ): Promise<void> {
369
+ const authState = normalizeAuthState(authInput);
370
+ if (!authState.shellSessionToken) {
371
+ return;
372
+ }
373
+ await requestJson(
374
+ platformAuthEndpoints.shellTokenRevoke,
375
+ z.null(),
376
+ {
377
+ method: "POST",
378
+ },
379
+ {
380
+ authState,
381
+ },
382
+ );
383
+ }
384
+
385
+ export async function loadCurrentSession(
386
+ authInput: Partial<PlatformAuthState> | string | null,
387
+ ): Promise<PlatformResult<PlatformSession>> {
388
+ const authState = normalizeAuthState(authInput);
389
+
390
+ if (!authState.shellSessionToken) {
391
+ return {
392
+ data: {
393
+ authenticated: false,
394
+ },
395
+ authState,
396
+ };
397
+ }
398
+
399
+ try {
400
+ const result = await fetchTenantMemberShellContext(authState);
401
+ return {
402
+ data: toAuthenticatedPlatformSession(result.data),
403
+ authState: result.authState,
404
+ };
405
+ } catch (error) {
406
+ if (error instanceof PlatformClientError) {
407
+ if (error.status === 401 || error.status === 403) {
408
+ return {
409
+ data: { authenticated: false },
410
+ authState: {
411
+ shellSessionToken: null,
412
+ },
413
+ };
414
+ }
415
+
416
+ if (error.status >= 500) {
417
+ return {
418
+ data: { authenticated: false },
419
+ authState: error.authState ?? authState,
420
+ };
421
+ }
422
+ }
423
+
424
+ throw error;
425
+ }
426
+ }
427
+
428
+ function workspaceShellSessionFromContext(
429
+ context: ShellContext,
430
+ workspaceSlug: string,
431
+ ): WorkspaceShellSession {
432
+ const matches = shellContextMatchesWorkspace(context, workspaceSlug);
433
+ const membership = matches ? shellContextToMembership(context) : null;
434
+ const workspaceCanView = matches && shellContextCanViewWorkspace(context);
435
+ const workspaceCanOpen = matches && shellContextCanOpenWorkspace(context);
436
+
437
+ return {
438
+ session: toAuthenticatedPlatformSession(context),
439
+ shellContext: context,
440
+ membership,
441
+ workspaceCanView,
442
+ workspaceCanOpen,
443
+ };
444
+ }
445
+
446
+ export async function loadCurrentWorkspaceShellSession(
447
+ authInput: Partial<PlatformAuthState> | string | null,
448
+ ): Promise<PlatformResult<WorkspaceShellSession>> {
449
+ const authState = normalizeAuthState(authInput);
450
+
451
+ if (!authState.shellSessionToken) {
452
+ return {
453
+ data: unauthenticatedWorkspaceShellSession(),
454
+ authState,
455
+ };
456
+ }
457
+
458
+ let contextResult: PlatformResult<ShellContext>;
459
+ try {
460
+ contextResult = await fetchTenantMemberShellContext(authState);
461
+ } catch (error) {
462
+ if (error instanceof PlatformClientError) {
463
+ if (error.status === 401 || error.status === 403) {
464
+ return {
465
+ data: unauthenticatedWorkspaceShellSession(),
466
+ authState: {
467
+ shellSessionToken: null,
468
+ },
469
+ };
470
+ }
471
+
472
+ if (error.status >= 500) {
473
+ return {
474
+ data: unauthenticatedWorkspaceShellSession(),
475
+ authState: error.authState ?? authState,
476
+ };
477
+ }
478
+ }
479
+
480
+ throw error;
481
+ }
482
+
483
+ return {
484
+ data: workspaceShellSessionFromContext(
485
+ contextResult.data,
486
+ contextResult.data.workspace_slug,
487
+ ),
488
+ authState: contextResult.authState,
489
+ };
490
+ }
491
+
492
+ export async function loadWorkspaceShellSession(
493
+ authInput: Partial<PlatformAuthState> | string | null,
494
+ workspaceSlug: string,
495
+ ): Promise<PlatformResult<WorkspaceShellSession>> {
496
+ const authState = normalizeAuthState(authInput);
497
+
498
+ if (!authState.shellSessionToken) {
499
+ return {
500
+ data: unauthenticatedWorkspaceShellSession(),
501
+ authState,
502
+ };
503
+ }
504
+
505
+ let contextResult: PlatformResult<ShellContext>;
506
+ try {
507
+ contextResult = await fetchTenantMemberShellContext(authState);
508
+ } catch (error) {
509
+ if (error instanceof PlatformClientError) {
510
+ if (error.status === 401 || error.status === 403) {
511
+ return {
512
+ data: unauthenticatedWorkspaceShellSession(),
513
+ authState: {
514
+ shellSessionToken: null,
515
+ },
516
+ };
517
+ }
518
+
519
+ if (error.status >= 500) {
520
+ return {
521
+ data: unauthenticatedWorkspaceShellSession(),
522
+ authState: error.authState ?? authState,
523
+ };
524
+ }
525
+ }
526
+
527
+ throw error;
528
+ }
529
+
530
+ return {
531
+ data: workspaceShellSessionFromContext(contextResult.data, workspaceSlug),
532
+ authState: contextResult.authState,
533
+ };
534
+ }
535
+
536
+ export async function resolveCurrentSession(
537
+ authInput: Partial<PlatformAuthState> | string | null,
538
+ ): Promise<PlatformSession> {
539
+ return (await loadCurrentSession(authInput)).data;
540
+ }
@@ -0,0 +1,190 @@
1
+ import { z } from "zod";
2
+
3
+ export const sessionMembershipSchema = z.object({
4
+ tenant_id: z.string().min(1),
5
+ tenant_slug: z.string().min(1),
6
+ tenant_name: z.string().min(1),
7
+ workspace_slug: z.string().min(1),
8
+ workspace_name: z.string().min(1),
9
+ role: z.string().min(1),
10
+ tenant_icon_url: z.string().optional().default(""),
11
+ runtime_status: z.string().optional().default(""),
12
+ roles: z
13
+ .array(
14
+ z.object({
15
+ slug: z.string().min(1),
16
+ name: z.string().min(1),
17
+ position: z.number().int(),
18
+ }),
19
+ )
20
+ .optional()
21
+ .default([]),
22
+ });
23
+
24
+ export const activeTenantSchema = sessionMembershipSchema;
25
+
26
+ export const platformUserSchema = z.object({
27
+ id: z.string().min(1),
28
+ username: z.string().min(1),
29
+ email: z.string().min(1),
30
+ display_name: z.string().optional().default(""),
31
+ avatar_url: z.string().optional().default(""),
32
+ });
33
+
34
+ export const upstreamSessionPayloadSchema = z
35
+ .object({
36
+ user: platformUserSchema,
37
+ active_tenant_id: z.string().min(1).nullable(),
38
+ active_tenant: activeTenantSchema.nullable(),
39
+ memberships: z.array(sessionMembershipSchema),
40
+ onboarding_completed_for_active_workspace: z
41
+ .boolean()
42
+ .nullable()
43
+ .optional()
44
+ .default(null),
45
+ })
46
+ .passthrough();
47
+
48
+ export const anonymousPlatformSessionSchema = z.object({
49
+ authenticated: z.literal(false),
50
+ });
51
+
52
+ export const authenticatedPlatformSessionSchema = z.object({
53
+ authenticated: z.literal(true),
54
+ user: platformUserSchema,
55
+ active_tenant_id: z.string().min(1).nullable(),
56
+ active_tenant: activeTenantSchema.nullable(),
57
+ memberships: z.array(sessionMembershipSchema),
58
+ onboarding_completed_for_active_workspace: z
59
+ .boolean()
60
+ .nullable()
61
+ .default(null),
62
+ });
63
+
64
+ export const platformSessionSchema = z.union([
65
+ anonymousPlatformSessionSchema,
66
+ authenticatedPlatformSessionSchema,
67
+ ]);
68
+
69
+ export const platformErrorPayloadSchema = z
70
+ .object({
71
+ detail: z.string().min(1).optional(),
72
+ error: z
73
+ .union([
74
+ z.string().min(1),
75
+ z.object({
76
+ code: z.string().min(1).optional(),
77
+ message: z.string().min(1).optional(),
78
+ }),
79
+ ])
80
+ .optional(),
81
+ code: z.string().min(1).optional(),
82
+ })
83
+ .passthrough();
84
+
85
+ export const shellContextSchema = z
86
+ .object({
87
+ principal_kind: z.literal("tenant_member"),
88
+ tenant_id: z.string().min(1),
89
+ workspace_slug: z.string().min(1),
90
+ workspace_name: z.string().min(1),
91
+ role_slug: z.string().optional().default(""),
92
+ role_slugs: z.array(z.string().min(1)).optional().default([]),
93
+ stored_permissions: z.array(z.string().min(1)).optional().default([]),
94
+ affordances: z.record(z.unknown()).optional().default({}),
95
+ capabilities: z.record(z.unknown()).optional().default({}),
96
+ user: platformUserSchema.optional(),
97
+ expires_at: z.string().optional().default(""),
98
+ })
99
+ .passthrough();
100
+
101
+ export const shellTokenResponseSchema = z
102
+ .object({
103
+ shell_session_token: z.string().min(1),
104
+ expires_at: z.string().min(1),
105
+ shell_context: shellContextSchema,
106
+ })
107
+ .strict();
108
+
109
+ export type SessionMembership = z.infer<typeof sessionMembershipSchema>;
110
+ export type PlatformUser = z.infer<typeof platformUserSchema>;
111
+ export type PlatformSession = z.infer<typeof platformSessionSchema>;
112
+ export type AuthenticatedPlatformSession = z.infer<
113
+ typeof authenticatedPlatformSessionSchema
114
+ >;
115
+ export type ShellContext = z.infer<typeof shellContextSchema>;
116
+ export type ShellTokenResponse = z.infer<typeof shellTokenResponseSchema>;
117
+
118
+ export function isAuthenticatedPlatformSession(
119
+ session: PlatformSession,
120
+ ): session is AuthenticatedPlatformSession {
121
+ return session.authenticated;
122
+ }
123
+
124
+ export function preferredWorkspace(
125
+ session: AuthenticatedPlatformSession,
126
+ ): SessionMembership | null {
127
+ return session.active_tenant ?? session.memberships[0] ?? null;
128
+ }
129
+
130
+ export function workspaceMembershipForSession(
131
+ session: PlatformSession,
132
+ workspaceSlug?: string | null,
133
+ ): SessionMembership | null {
134
+ if (!session.authenticated) {
135
+ return null;
136
+ }
137
+
138
+ const normalizedSlug = String(workspaceSlug ?? "").trim();
139
+ if (!normalizedSlug) {
140
+ return preferredWorkspace(session);
141
+ }
142
+
143
+ if (session.active_tenant?.workspace_slug === normalizedSlug) {
144
+ return session.active_tenant;
145
+ }
146
+
147
+ return (
148
+ session.memberships.find(
149
+ (membership) => membership.workspace_slug === normalizedSlug,
150
+ ) ?? null
151
+ );
152
+ }
153
+
154
+ function nestedBoolean(
155
+ value: Record<string, unknown>,
156
+ group: string,
157
+ key: string,
158
+ ) {
159
+ const groupValue = value[group];
160
+ if (
161
+ typeof groupValue !== "object" ||
162
+ groupValue === null ||
163
+ Array.isArray(groupValue)
164
+ ) {
165
+ return false;
166
+ }
167
+
168
+ return (groupValue as Record<string, unknown>)[key] === true;
169
+ }
170
+
171
+ export function shellContextMatchesWorkspace(
172
+ context: ShellContext,
173
+ workspaceSlug?: string | null,
174
+ ) {
175
+ return context.workspace_slug === String(workspaceSlug ?? "").trim();
176
+ }
177
+
178
+ export function shellContextCanViewWorkspace(context: ShellContext) {
179
+ return (
180
+ nestedBoolean(context.capabilities, "channels", "view") &&
181
+ nestedBoolean(context.capabilities, "threads", "view")
182
+ );
183
+ }
184
+
185
+ export function shellContextCanOpenWorkspace(context: ShellContext) {
186
+ return (
187
+ shellContextCanViewWorkspace(context) &&
188
+ context.affordances.workspace_shell_open === true
189
+ );
190
+ }