minutework 0.1.32 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/assets/claude-local/skills/README.md +2 -0
  2. package/assets/claude-local/skills/app-pack-authoring/SKILL.md +14 -1
  3. package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
  4. package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +10 -3
  5. package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +9 -6
  6. package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +5 -4
  7. package/assets/templates/next-tenant-app/README.md +26 -138
  8. package/assets/templates/next-tenant-app/package.json +1 -0
  9. package/assets/templates/next-tenant-app/src/app/app/demo/page.tsx +15 -0
  10. package/assets/templates/next-tenant-app/src/app/app/layout.tsx +1 -4
  11. package/assets/templates/next-tenant-app/src/app/app/page.tsx +2 -17
  12. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +9 -67
  13. package/assets/templates/next-tenant-app/src/app/blog/page.tsx +10 -46
  14. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +9 -65
  15. package/assets/templates/next-tenant-app/src/app/docs/page.tsx +10 -46
  16. package/assets/templates/next-tenant-app/src/app/layout.tsx +8 -10
  17. package/assets/templates/next-tenant-app/src/app/login/page.tsx +3 -23
  18. package/assets/templates/next-tenant-app/src/app/page.tsx +11 -44
  19. package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +10 -44
  20. package/assets/templates/next-tenant-app/src/app/providers.tsx +2 -1
  21. package/assets/templates/next-tenant-app/src/app/robots.ts +7 -18
  22. package/assets/templates/next-tenant-app/src/app/sitemap.ts +4 -39
  23. package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +97 -98
  24. package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +43 -78
  25. package/assets/templates/next-tenant-app/src/features/demo/components/manifest-demo.tsx +89 -0
  26. package/assets/templates/next-tenant-app/src/features/public-shell/components/static-public-page.tsx +58 -0
  27. package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +48 -552
  28. package/assets/templates/next-tenant-app/src/lib/app-routes.ts +2 -2
  29. package/assets/templates/next-tenant-app/src/lib/public-site.ts +5 -30
  30. package/assets/templates/next-tenant-app/src/mw/client.ts +18 -0
  31. package/assets/templates/next-tenant-app/src/mw/mock.test.ts +21 -0
  32. package/assets/templates/next-tenant-app/src/mw/mock.ts +35 -0
  33. package/assets/templates/next-tenant-app/src/mw/provider.tsx +17 -0
  34. package/assets/templates/next-tenant-app/template.json +3 -3
  35. package/assets/templates/next-tenant-app/template.schema.json +1 -0
  36. package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +4 -5
  37. package/package.json +2 -2
  38. package/vendor/workspace-mcp/types.d.ts +4 -0
  39. package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +0 -89
  40. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +0 -90
  41. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +0 -78
  42. package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +0 -31
  43. package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +0 -16
  44. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +0 -79
  45. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +0 -40
  46. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +0 -42
  47. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +0 -29
  48. package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +0 -26
  49. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +0 -40
  50. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +0 -47
  51. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +0 -43
  52. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +0 -45
  53. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +0 -83
  54. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +0 -30
  55. package/assets/templates/next-tenant-app/src/app/app/page.test.ts +0 -62
  56. package/assets/templates/next-tenant-app/src/app/app/private-content-source.test.ts +0 -88
  57. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +0 -70
  58. package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +0 -46
  59. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +0 -70
  60. package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +0 -46
  61. package/assets/templates/next-tenant-app/src/app/login/page.test.ts +0 -55
  62. package/assets/templates/next-tenant-app/src/app/page.test.ts +0 -90
  63. package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +0 -59
  64. package/assets/templates/next-tenant-app/src/app/robots.test.ts +0 -40
  65. package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +0 -63
  66. package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +0 -342
  67. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +0 -66
  68. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +0 -108
  69. package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +0 -111
  70. package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +0 -111
  71. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.test.ts +0 -38
  72. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx +0 -145
  73. package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +0 -189
  74. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +0 -444
  75. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +0 -383
  76. package/assets/templates/next-tenant-app/src/lib/content/contracts.test.ts +0 -138
  77. package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +0 -399
  78. package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +0 -5
  79. package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +0 -96
  80. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.test.ts +0 -93
  81. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.ts +0 -123
  82. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +0 -75
  83. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +0 -25
  84. package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +0 -170
  85. package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +0 -661
  86. package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +0 -131
  87. package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +0 -34
  88. package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +0 -211
  89. package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +0 -151
  90. package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +0 -33
  91. package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +0 -108
@@ -1,131 +0,0 @@
1
- import { z } from "zod";
2
-
3
- export const commandTemplateKeys = [
4
- "python_version",
5
- "echo_hello",
6
- "system_info",
7
- "list_apps",
8
- ] as const;
9
-
10
- export const commandTemplateKeySchema = z.enum(commandTemplateKeys);
11
-
12
- export const loginRequestSchema = z.object({
13
- username: z.string().trim().min(1),
14
- password: z.string().min(1),
15
- });
16
-
17
- export const sessionMembershipSchema = z.object({
18
- tenant_id: z.string().min(1),
19
- tenant_slug: z.string().min(1),
20
- tenant_name: z.string().min(1),
21
- role: z.string().min(1),
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
- });
31
-
32
- export const upstreamSessionPayloadSchema = z
33
- .object({
34
- user: platformUserSchema,
35
- active_tenant_id: z.string().min(1),
36
- active_tenant: activeTenantSchema,
37
- memberships: z.array(sessionMembershipSchema),
38
- })
39
- .passthrough();
40
-
41
- export const anonymousPlatformSessionSchema = z.object({
42
- authenticated: z.literal(false),
43
- });
44
-
45
- export const authenticatedPlatformSessionSchema = z.object({
46
- authenticated: z.literal(true),
47
- user: platformUserSchema,
48
- active_tenant_id: z.string().min(1),
49
- active_tenant: activeTenantSchema,
50
- memberships: z.array(sessionMembershipSchema),
51
- });
52
-
53
- export const platformSessionSchema = z.union([
54
- anonymousPlatformSessionSchema,
55
- authenticatedPlatformSessionSchema,
56
- ]);
57
-
58
- export const platformErrorPayloadSchema = z
59
- .object({
60
- detail: z.string().min(1).optional(),
61
- error: z.string().min(1).optional(),
62
- code: z.string().min(1).optional(),
63
- })
64
- .passthrough();
65
-
66
- export const commandDispatchRequestSchema = z.object({
67
- tenant_id: z.string().min(1),
68
- template_key: commandTemplateKeySchema,
69
- });
70
-
71
- export const tenantSessionContextRequestSchema = z.object({
72
- tenant_id: z.string().min(1),
73
- });
74
-
75
- export const passwordStatusSchema = z.object({
76
- has_password: z.boolean(),
77
- });
78
-
79
- export const passwordChangeRequestSchema = z
80
- .object({
81
- current_password: z.string().optional().default(""),
82
- new_password: z.string().min(1),
83
- confirm_password: z.string().min(1),
84
- })
85
- .superRefine((value, context) => {
86
- if (value.new_password !== value.confirm_password) {
87
- context.addIssue({
88
- code: z.ZodIssueCode.custom,
89
- path: ["confirm_password"],
90
- message: "Passwords must match.",
91
- });
92
- }
93
- });
94
-
95
- export const upstreamPasswordChangeRequestSchema = z.object({
96
- current_password: z.string().optional(),
97
- new_password: z.string().min(1),
98
- });
99
-
100
- export const commandRunSchema = z.object({
101
- run_id: z.string().min(1),
102
- tenant_id: z.string().min(1),
103
- runtime_id: z.string().min(1),
104
- template_key: z.string().min(1),
105
- status: z.string().min(1),
106
- safe_summary: z.string(),
107
- exit_code: z.number().int().nullable(),
108
- stdout_tail: z.string(),
109
- stderr_tail: z.string(),
110
- source_ref: z.string(),
111
- dispatch_error: z.string(),
112
- });
113
-
114
- export type LoginRequest = z.infer<typeof loginRequestSchema>;
115
- export type SessionMembership = z.infer<typeof sessionMembershipSchema>;
116
- export type ActiveTenant = z.infer<typeof activeTenantSchema>;
117
- export type PlatformUser = z.infer<typeof platformUserSchema>;
118
- export type PlatformSession = z.infer<typeof platformSessionSchema>;
119
- export type AuthenticatedPlatformSession = z.infer<
120
- typeof authenticatedPlatformSessionSchema
121
- >;
122
- export type CommandTemplateKey = z.infer<typeof commandTemplateKeySchema>;
123
- export type CommandDispatchRequest = z.infer<
124
- typeof commandDispatchRequestSchema
125
- >;
126
- export type CommandRun = z.infer<typeof commandRunSchema>;
127
- export type TenantSessionContextRequest = z.infer<
128
- typeof tenantSessionContextRequestSchema
129
- >;
130
- export type PasswordStatus = z.infer<typeof passwordStatusSchema>;
131
- export type PasswordChangeRequest = z.infer<typeof passwordChangeRequestSchema>;
@@ -1,34 +0,0 @@
1
- import "server-only";
2
-
3
- import { env } from "@/lib/platform/env.server";
4
-
5
- const platformBaseUrl = new URL(
6
- env.MW_PLATFORM_BASE_URL.endsWith("/")
7
- ? env.MW_PLATFORM_BASE_URL
8
- : `${env.MW_PLATFORM_BASE_URL}/`,
9
- );
10
-
11
- function buildPlatformEndpoint(path: string) {
12
- return new URL(path.replace(/^\//, ""), platformBaseUrl).toString();
13
- }
14
-
15
- export const platformAuthEndpoints = {
16
- login: buildPlatformEndpoint("/api/v1/session/login/"),
17
- logout: buildPlatformEndpoint("/api/v1/session/logout/"),
18
- currentSession: buildPlatformEndpoint("/api/v1/session/me/"),
19
- sessionContext: buildPlatformEndpoint("/api/v1/session/context/"),
20
- passwordStatus: buildPlatformEndpoint("/api/v1/session/password-status/"),
21
- passwordChange: buildPlatformEndpoint("/api/v1/session/password-change/"),
22
- operatorConsole: buildPlatformEndpoint("/ops/login/"),
23
- };
24
-
25
- export const platformGatewayEndpoints = {
26
- tenantCommands: buildPlatformEndpoint("/api/v1/tenant/commands/"),
27
- tenantCommandRun(runId: string, tenantId: string) {
28
- const url = new URL(
29
- buildPlatformEndpoint(`/api/v1/tenant/commands/${encodeURIComponent(runId)}/`),
30
- );
31
- url.searchParams.set("tenant_id", tenantId);
32
- return url.toString();
33
- },
34
- };
@@ -1,211 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
-
3
- import { afterEach, describe, expect, it, vi } from "vitest";
4
-
5
- const originalEnv = { ...process.env };
6
-
7
- function restoreProcessEnv() {
8
- for (const key of Object.keys(process.env)) {
9
- if (!(key in originalEnv)) {
10
- delete process.env[key];
11
- }
12
- }
13
-
14
- Object.assign(process.env, originalEnv);
15
- }
16
-
17
- function applyServerEnv(overrides: Record<string, string | undefined>) {
18
- restoreProcessEnv();
19
- process.env.MW_PLATFORM_BASE_URL = "http://127.0.0.1:8000";
20
- process.env.MW_PUBLIC_CONTENT_SOURCE = "minutework_cms";
21
- process.env.MW_CONTENT_API_TOKEN = "test-content-token";
22
- process.env.MW_PUBLIC_BASE_URL = "http://127.0.0.1:3000";
23
- process.env.MW_PUBLIC_SITE_PROPERTY_KEY = "main-site";
24
- process.env.MW_ENABLE_RUNTIME_COMMAND_EXAMPLE = "false";
25
-
26
- for (const [key, value] of Object.entries(overrides)) {
27
- if (value === undefined) {
28
- delete process.env[key];
29
- } else {
30
- process.env[key] = value;
31
- }
32
- }
33
- }
34
-
35
- async function readEnvExample() {
36
- const contents = await readFile(".env.example", "utf8");
37
- return Object.fromEntries(
38
- contents
39
- .split("\n")
40
- .map((line) => line.trim())
41
- .filter((line) => line.length > 0 && !line.startsWith("#"))
42
- .map((line) => {
43
- const separatorIndex = line.indexOf("=");
44
- return [
45
- line.slice(0, separatorIndex),
46
- line.slice(separatorIndex + 1),
47
- ] as const;
48
- }),
49
- );
50
- }
51
-
52
- describe("server env", () => {
53
- afterEach(() => {
54
- restoreProcessEnv();
55
- vi.resetModules();
56
- });
57
-
58
- it("defaults MW_PUBLIC_SITE_ENV to preview", async () => {
59
- applyServerEnv({
60
- MW_PUBLIC_SITE_ENV: undefined,
61
- });
62
-
63
- const { env } = await import("./env.server");
64
-
65
- expect(env.MW_PUBLIC_SITE_ENV).toBe("preview");
66
- });
67
-
68
- it("defaults MW_PUBLIC_CONTENT_SOURCE to private-only mode", async () => {
69
- applyServerEnv({
70
- MW_PUBLIC_CONTENT_SOURCE: undefined,
71
- MW_CONTENT_API_TOKEN: undefined,
72
- MW_PUBLIC_BASE_URL: undefined,
73
- MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
74
- });
75
-
76
- const { env } = await import("./env.server");
77
-
78
- expect(env.MW_PUBLIC_CONTENT_SOURCE).toBe("none");
79
- expect(env.MW_CONTENT_API_TOKEN).toBeUndefined();
80
- expect(env.MW_PUBLIC_BASE_URL).toBeUndefined();
81
- expect(env.MW_PUBLIC_SITE_PROPERTY_KEY).toBeUndefined();
82
- });
83
-
84
- it("defaults MW_STATIC_PUBLIC_CONTENT_PATH to content/public-site.json", async () => {
85
- applyServerEnv({
86
- MW_STATIC_PUBLIC_CONTENT_PATH: undefined,
87
- });
88
-
89
- const { env } = await import("./env.server");
90
-
91
- expect(env.MW_STATIC_PUBLIC_CONTENT_PATH).toBe("content/public-site.json");
92
- });
93
-
94
- it("accepts MW_PUBLIC_SITE_ENV=live", async () => {
95
- applyServerEnv({
96
- MW_PUBLIC_SITE_ENV: "live",
97
- });
98
-
99
- const { env } = await import("./env.server");
100
-
101
- expect(env.MW_PUBLIC_SITE_ENV).toBe("live");
102
- });
103
-
104
- it("requires MW_PUBLIC_BASE_URL", async () => {
105
- applyServerEnv({
106
- MW_PUBLIC_BASE_URL: undefined,
107
- });
108
-
109
- await expect(import("./env.server")).rejects.toThrow("MW_PUBLIC_BASE_URL");
110
- });
111
-
112
- it("requires MW_CONTENT_API_TOKEN", async () => {
113
- applyServerEnv({
114
- MW_CONTENT_API_TOKEN: undefined,
115
- });
116
-
117
- await expect(import("./env.server")).rejects.toThrow("MW_CONTENT_API_TOKEN");
118
- });
119
-
120
- it("requires MW_PUBLIC_SITE_PROPERTY_KEY", async () => {
121
- applyServerEnv({
122
- MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
123
- });
124
-
125
- await expect(import("./env.server")).rejects.toThrow(
126
- "MW_PUBLIC_SITE_PROPERTY_KEY",
127
- );
128
- });
129
-
130
- it("accepts custom public content without MinuteWork CMS credentials", async () => {
131
- applyServerEnv({
132
- MW_PUBLIC_CONTENT_SOURCE: "custom",
133
- MW_CONTENT_API_TOKEN: undefined,
134
- MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
135
- });
136
-
137
- const { env } = await import("./env.server");
138
-
139
- expect(env.MW_PUBLIC_CONTENT_SOURCE).toBe("custom");
140
- expect(env.MW_CONTENT_API_TOKEN).toBeUndefined();
141
- expect(env.MW_PUBLIC_SITE_PROPERTY_KEY).toBeUndefined();
142
- });
143
-
144
- it("requires MW_PUBLIC_BASE_URL for custom public content", async () => {
145
- applyServerEnv({
146
- MW_PUBLIC_CONTENT_SOURCE: "custom",
147
- MW_CONTENT_API_TOKEN: undefined,
148
- MW_PUBLIC_BASE_URL: undefined,
149
- MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
150
- });
151
-
152
- await expect(import("./env.server")).rejects.toThrow("MW_PUBLIC_BASE_URL");
153
- });
154
-
155
- it("accepts static JSON public content without MinuteWork CMS credentials", async () => {
156
- applyServerEnv({
157
- MW_PUBLIC_CONTENT_SOURCE: "static_json",
158
- MW_CONTENT_API_TOKEN: undefined,
159
- MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
160
- MW_STATIC_PUBLIC_CONTENT_PATH: "content/site.json",
161
- });
162
-
163
- const { env } = await import("./env.server");
164
-
165
- expect(env.MW_PUBLIC_CONTENT_SOURCE).toBe("static_json");
166
- expect(env.MW_STATIC_PUBLIC_CONTENT_PATH).toBe("content/site.json");
167
- });
168
-
169
- it("accepts private-only mode without public or CMS environment", async () => {
170
- applyServerEnv({
171
- MW_PUBLIC_CONTENT_SOURCE: "none",
172
- MW_CONTENT_API_TOKEN: undefined,
173
- MW_PUBLIC_BASE_URL: undefined,
174
- MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
175
- });
176
-
177
- const { env } = await import("./env.server");
178
-
179
- expect(env.MW_PUBLIC_CONTENT_SOURCE).toBe("none");
180
- expect(env.MW_PUBLIC_BASE_URL).toBeUndefined();
181
- });
182
-
183
- it("ships .env.example as private-only so generated apps boot without CMS", async () => {
184
- await expect(readEnvExample()).resolves.toMatchObject({
185
- MW_PUBLIC_CONTENT_SOURCE: "none",
186
- MW_CONTENT_API_TOKEN: "",
187
- MW_PUBLIC_BASE_URL: "",
188
- MW_PUBLIC_SITE_PROPERTY_KEY: "",
189
- MW_STATIC_PUBLIC_CONTENT_PATH: "content/public-site.json",
190
- });
191
- });
192
-
193
- it("requires MW_PLATFORM_BASE_URL in production", async () => {
194
- applyServerEnv({
195
- MW_PLATFORM_BASE_URL: undefined,
196
- NODE_ENV: "production",
197
- });
198
-
199
- await expect(import("./env.server")).rejects.toThrow(
200
- "MW_PLATFORM_BASE_URL must be set in production.",
201
- );
202
- });
203
-
204
- it("rejects unsupported public-site environments", async () => {
205
- applyServerEnv({
206
- MW_PUBLIC_SITE_ENV: "draft",
207
- });
208
-
209
- await expect(import("./env.server")).rejects.toThrow("MW_PUBLIC_SITE_ENV");
210
- });
211
- });
@@ -1,151 +0,0 @@
1
- import "server-only";
2
-
3
- import { z } from "zod";
4
-
5
- const DEFAULT_LOCAL_PLATFORM_BASE_URL = "http://127.0.0.1:8000";
6
- const DEFAULT_STATIC_PUBLIC_CONTENT_PATH = "content/public-site.json";
7
-
8
- const exampleFlagSchema = z.preprocess(
9
- (value) => value ?? "false",
10
- z.enum(["true", "false"]).transform((value) => value === "true"),
11
- );
12
-
13
- const requiredStringEnvSchema = z.preprocess((value) => {
14
- if (typeof value !== "string") {
15
- return value;
16
- }
17
-
18
- const normalized = value.trim();
19
- return normalized.length > 0 ? normalized : undefined;
20
- }, z.string().min(1));
21
-
22
- const optionalStringEnvSchema = z.preprocess((value) => {
23
- if (typeof value !== "string") {
24
- return value;
25
- }
26
-
27
- const normalized = value.trim();
28
- return normalized.length > 0 ? normalized : undefined;
29
- }, z.string().min(1).optional());
30
-
31
- const optionalUrlEnvSchema = z.preprocess((value) => {
32
- if (typeof value !== "string") {
33
- return value;
34
- }
35
-
36
- const normalized = value.trim();
37
- return normalized.length > 0 ? normalized : undefined;
38
- }, z.string().url().optional());
39
-
40
- const publicContentSourceSchema = z.enum([
41
- "minutework_cms",
42
- "custom",
43
- "static_json",
44
- "none",
45
- ]);
46
- const publicSiteEnvironmentSchema = z.enum(["preview", "live"]);
47
-
48
- const envSchema = z
49
- .object({
50
- MW_PLATFORM_BASE_URL: z.string().url(),
51
- MW_PUBLIC_CONTENT_SOURCE: z.preprocess(
52
- (value) => value ?? "none",
53
- publicContentSourceSchema,
54
- ),
55
- MW_CONTENT_API_TOKEN: optionalStringEnvSchema,
56
- MW_TEMPLATE_APP_NAME: z.preprocess(
57
- (value) => value ?? "MinuteWork Combined Starter",
58
- z.string().trim().min(1),
59
- ),
60
- MW_PUBLIC_BASE_URL: optionalUrlEnvSchema,
61
- MW_PUBLIC_SITE_PROPERTY_KEY: optionalStringEnvSchema,
62
- MW_PUBLIC_SITE_ENV: z.preprocess(
63
- (value) => value ?? "preview",
64
- publicSiteEnvironmentSchema,
65
- ),
66
- MW_STATIC_PUBLIC_CONTENT_PATH: z.preprocess(
67
- (value) => value ?? DEFAULT_STATIC_PUBLIC_CONTENT_PATH,
68
- requiredStringEnvSchema,
69
- ),
70
- MW_ENABLE_RUNTIME_COMMAND_EXAMPLE: exampleFlagSchema,
71
- NODE_ENV: z
72
- .enum(["development", "test", "production"])
73
- .default("development"),
74
- })
75
- .superRefine((value, context) => {
76
- if (value.MW_PUBLIC_CONTENT_SOURCE === "minutework_cms") {
77
- if (!value.MW_CONTENT_API_TOKEN) {
78
- context.addIssue({
79
- code: z.ZodIssueCode.custom,
80
- path: ["MW_CONTENT_API_TOKEN"],
81
- message: "Required when MW_PUBLIC_CONTENT_SOURCE=minutework_cms",
82
- });
83
- }
84
- if (!value.MW_PUBLIC_BASE_URL) {
85
- context.addIssue({
86
- code: z.ZodIssueCode.custom,
87
- path: ["MW_PUBLIC_BASE_URL"],
88
- message: "Required when MW_PUBLIC_CONTENT_SOURCE=minutework_cms",
89
- });
90
- }
91
- if (!value.MW_PUBLIC_SITE_PROPERTY_KEY) {
92
- context.addIssue({
93
- code: z.ZodIssueCode.custom,
94
- path: ["MW_PUBLIC_SITE_PROPERTY_KEY"],
95
- message: "Required when MW_PUBLIC_CONTENT_SOURCE=minutework_cms",
96
- });
97
- }
98
- }
99
-
100
- if (
101
- (value.MW_PUBLIC_CONTENT_SOURCE === "custom" ||
102
- value.MW_PUBLIC_CONTENT_SOURCE === "static_json") &&
103
- !value.MW_PUBLIC_BASE_URL
104
- ) {
105
- context.addIssue({
106
- code: z.ZodIssueCode.custom,
107
- path: ["MW_PUBLIC_BASE_URL"],
108
- message: `Required when MW_PUBLIC_CONTENT_SOURCE=${value.MW_PUBLIC_CONTENT_SOURCE}`,
109
- });
110
- }
111
- });
112
-
113
- const defaultPlatformBaseUrl =
114
- process.env.MW_PLATFORM_BASE_URL ??
115
- (process.env.NODE_ENV === "production"
116
- ? undefined
117
- : DEFAULT_LOCAL_PLATFORM_BASE_URL);
118
-
119
- const parsedEnv = envSchema.safeParse({
120
- MW_PLATFORM_BASE_URL: defaultPlatformBaseUrl,
121
- MW_PUBLIC_CONTENT_SOURCE: process.env.MW_PUBLIC_CONTENT_SOURCE,
122
- MW_CONTENT_API_TOKEN: process.env.MW_CONTENT_API_TOKEN,
123
- MW_TEMPLATE_APP_NAME: process.env.MW_TEMPLATE_APP_NAME,
124
- MW_PUBLIC_BASE_URL: process.env.MW_PUBLIC_BASE_URL,
125
- MW_PUBLIC_SITE_PROPERTY_KEY: process.env.MW_PUBLIC_SITE_PROPERTY_KEY,
126
- MW_PUBLIC_SITE_ENV: process.env.MW_PUBLIC_SITE_ENV,
127
- MW_STATIC_PUBLIC_CONTENT_PATH: process.env.MW_STATIC_PUBLIC_CONTENT_PATH,
128
- MW_ENABLE_RUNTIME_COMMAND_EXAMPLE:
129
- process.env.MW_ENABLE_RUNTIME_COMMAND_EXAMPLE,
130
- NODE_ENV: process.env.NODE_ENV,
131
- });
132
-
133
- if (!parsedEnv.success) {
134
- if (
135
- process.env.NODE_ENV === "production" &&
136
- !process.env.MW_PLATFORM_BASE_URL
137
- ) {
138
- throw new Error("MW_PLATFORM_BASE_URL must be set in production.");
139
- }
140
-
141
- const issues = parsedEnv.error.issues
142
- .map((issue) => {
143
- const path = issue.path.join(".");
144
- return path.length > 0 ? `${path}: ${issue.message}` : issue.message;
145
- })
146
- .join("; ");
147
-
148
- throw new Error(`Invalid server environment for next-tenant-app: ${issues}`);
149
- }
150
-
151
- export const env = parsedEnv.data;
@@ -1,33 +0,0 @@
1
- import { NextResponse } from "next/server";
2
-
3
- import { PlatformClientError } from "@/lib/platform/client.server";
4
- import type { PlatformAuthState } from "@/lib/platform/session.server";
5
- import { syncPlatformAuthStateToResponse } from "@/lib/platform/session.server";
6
-
7
- export function toPlatformErrorResponse(
8
- error: unknown,
9
- fallbackDetail: string,
10
- previousAuthState?: PlatformAuthState,
11
- ) {
12
- if (error instanceof PlatformClientError) {
13
- const response = NextResponse.json(
14
- {
15
- detail: error.message,
16
- code: error.code,
17
- },
18
- { status: error.status },
19
- );
20
-
21
- if (previousAuthState && error.authState) {
22
- syncPlatformAuthStateToResponse(
23
- response,
24
- previousAuthState,
25
- error.authState,
26
- );
27
- }
28
-
29
- return response;
30
- }
31
-
32
- return NextResponse.json({ detail: fallbackDetail }, { status: 500 });
33
- }
@@ -1,108 +0,0 @@
1
- import "server-only";
2
-
3
- import { cookies } from "next/headers";
4
- import type { NextResponse } from "next/server";
5
-
6
- import { env } from "@/lib/platform/env.server";
7
-
8
- export const PLATFORM_SESSION_COOKIE = "mw_platform_session";
9
- export const PLATFORM_CSRF_COOKIE = "mw_platform_csrf";
10
-
11
- export type PlatformAuthState = {
12
- sessionId: string | null;
13
- csrfToken: string | null;
14
- };
15
-
16
- function platformCookie(name: string, value: string) {
17
- return {
18
- name,
19
- value,
20
- httpOnly: true,
21
- sameSite: "lax" as const,
22
- path: "/",
23
- secure: env.NODE_ENV === "production",
24
- };
25
- }
26
-
27
- function platformSessionCookie(value: string) {
28
- return platformCookie(PLATFORM_SESSION_COOKIE, value);
29
- }
30
-
31
- function platformCsrfCookie(value: string) {
32
- return platformCookie(PLATFORM_CSRF_COOKIE, value);
33
- }
34
-
35
- export async function readPlatformAuthState(): Promise<PlatformAuthState> {
36
- const cookieStore = await cookies();
37
-
38
- return {
39
- sessionId: cookieStore.get(PLATFORM_SESSION_COOKIE)?.value ?? null,
40
- csrfToken: cookieStore.get(PLATFORM_CSRF_COOKIE)?.value ?? null,
41
- };
42
- }
43
-
44
- export async function readPlatformSessionId() {
45
- return (await readPlatformAuthState()).sessionId;
46
- }
47
-
48
- export async function readPlatformCsrfToken() {
49
- return (await readPlatformAuthState()).csrfToken;
50
- }
51
-
52
- export function applyPlatformSessionToResponse(
53
- response: NextResponse,
54
- sessionId: string,
55
- ) {
56
- response.cookies.set(platformSessionCookie(sessionId));
57
- }
58
-
59
- export function applyPlatformCsrfToResponse(
60
- response: NextResponse,
61
- csrfToken: string,
62
- ) {
63
- response.cookies.set(platformCsrfCookie(csrfToken));
64
- }
65
-
66
- export function applyPlatformAuthStateToResponse(
67
- response: NextResponse,
68
- authState: PlatformAuthState,
69
- ) {
70
- if (authState.sessionId) {
71
- applyPlatformSessionToResponse(response, authState.sessionId);
72
- }
73
-
74
- if (authState.csrfToken) {
75
- applyPlatformCsrfToResponse(response, authState.csrfToken);
76
- }
77
- }
78
-
79
- export function syncPlatformAuthStateToResponse(
80
- response: NextResponse,
81
- previousAuthState: PlatformAuthState,
82
- nextAuthState: PlatformAuthState,
83
- ) {
84
- if (previousAuthState.sessionId !== nextAuthState.sessionId) {
85
- if (nextAuthState.sessionId) {
86
- applyPlatformSessionToResponse(response, nextAuthState.sessionId);
87
- } else {
88
- response.cookies.delete(PLATFORM_SESSION_COOKIE);
89
- }
90
- }
91
-
92
- if (previousAuthState.csrfToken !== nextAuthState.csrfToken) {
93
- if (nextAuthState.csrfToken) {
94
- applyPlatformCsrfToResponse(response, nextAuthState.csrfToken);
95
- } else {
96
- response.cookies.delete(PLATFORM_CSRF_COOKIE);
97
- }
98
- }
99
- }
100
-
101
- export function clearPlatformSessionFromResponse(response: NextResponse) {
102
- response.cookies.delete(PLATFORM_SESSION_COOKIE);
103
- response.cookies.delete(PLATFORM_CSRF_COOKIE);
104
- }
105
-
106
- export function clearPlatformCsrfFromResponse(response: NextResponse) {
107
- response.cookies.delete(PLATFORM_CSRF_COOKIE);
108
- }