swallowkit 1.0.0-beta.45 → 1.0.0-beta.47

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 (52) hide show
  1. package/dist/cli/commands/add-auth.d.ts +2 -0
  2. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  3. package/dist/cli/commands/add-auth.js +214 -16
  4. package/dist/cli/commands/add-auth.js.map +1 -1
  5. package/dist/cli/commands/dev.d.ts +4 -0
  6. package/dist/cli/commands/dev.d.ts.map +1 -1
  7. package/dist/cli/commands/dev.js +100 -12
  8. package/dist/cli/commands/dev.js.map +1 -1
  9. package/dist/cli/commands/init.d.ts +2 -0
  10. package/dist/cli/commands/init.d.ts.map +1 -1
  11. package/dist/cli/commands/init.js +74 -30
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  14. package/dist/cli/commands/scaffold.js +37 -5
  15. package/dist/cli/commands/scaffold.js.map +1 -1
  16. package/dist/cli/index.d.ts.map +1 -1
  17. package/dist/cli/index.js +3 -1
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/core/scaffold/auth-generator.d.ts +20 -2
  20. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  21. package/dist/core/scaffold/auth-generator.js +265 -12
  22. package/dist/core/scaffold/auth-generator.js.map +1 -1
  23. package/dist/core/scaffold/connector-functions-generator.d.ts +3 -3
  24. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
  25. package/dist/core/scaffold/connector-functions-generator.js +19 -19
  26. package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
  27. package/dist/core/scaffold/functions-generator.d.ts +2 -2
  28. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  29. package/dist/core/scaffold/functions-generator.js +16 -16
  30. package/dist/core/scaffold/functions-generator.js.map +1 -1
  31. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  32. package/dist/core/scaffold/nextjs-generator.js +13 -1
  33. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  34. package/dist/types/index.d.ts +1 -1
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +20 -20
  38. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +13 -1
  39. package/src/__tests__/auth.test.ts +103 -0
  40. package/src/__tests__/dev.test.ts +27 -0
  41. package/src/__tests__/init.test.ts +27 -0
  42. package/src/__tests__/nextjs-generator.test.ts +8 -0
  43. package/src/cli/commands/add-auth.ts +239 -16
  44. package/src/cli/commands/dev.ts +111 -14
  45. package/src/cli/commands/init.ts +76 -30
  46. package/src/cli/commands/scaffold.ts +37 -5
  47. package/src/cli/index.ts +3 -1
  48. package/src/core/scaffold/auth-generator.ts +267 -13
  49. package/src/core/scaffold/connector-functions-generator.ts +22 -19
  50. package/src/core/scaffold/functions-generator.ts +17 -17
  51. package/src/core/scaffold/nextjs-generator.ts +13 -1
  52. package/src/types/index.ts +1 -1
@@ -13,6 +13,17 @@ import {
13
13
  generateLoginPage,
14
14
  generateAuthContext,
15
15
  generateBFFCallFunctionWithAuth,
16
+ generateBFFCallFunctionWithSwaAuth,
17
+ generateSwaAuthHelperTS,
18
+ generateSwaAuthHelperCSharp,
19
+ generateSwaAuthHelperPython,
20
+ generateExternalTokenAdapter,
21
+ generateAuthenticatedFetch,
22
+ generateExternalTokenAuthContext,
23
+ generateExternalTokenVerifierTS,
24
+ generateExternalTokenHelperTS,
25
+ generateExternalTokenVerifierCSharp,
26
+ generateExternalTokenVerifierPython,
16
27
  generateAuthImportTS,
17
28
  generateAuthGuardTS,
18
29
  generateAuthGuardCSharp,
@@ -22,6 +33,7 @@ import { generateCompactAzureFunctionsCRUD } from "../core/scaffold/functions-ge
22
33
  import { parseAuthPolicy } from "../core/scaffold/model-parser";
23
34
  import { createBasicModelInfo } from "./fixtures";
24
35
  import { ModelAuthPolicy, CustomJwtConfig } from "../types";
36
+ import { addAuthProviderToRootLayout, generateSwaAuthContext } from "../cli/commands/add-auth";
25
37
 
26
38
  const defaultJwtConfig: CustomJwtConfig = {
27
39
  userConnector: "mysql",
@@ -325,6 +337,97 @@ describe("generateBFFCallFunctionWithAuth", () => {
325
337
  const code = generateBFFCallFunctionWithAuth();
326
338
  expect(code).toContain("catch");
327
339
  });
340
+
341
+ it("also attaches the Functions host key", () => {
342
+ const code = generateBFFCallFunctionWithAuth();
343
+ expect(code).toContain("BACKEND_FUNCTIONS_KEY");
344
+ expect(code).toContain("'x-functions-key'");
345
+ });
346
+ });
347
+
348
+ describe("SWA built-in auth generators", () => {
349
+ it("wraps the root layout with AuthProvider exactly once", () => {
350
+ const layout = `import './globals.css';\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n return <html><body>{children}</body></html>;\n}\n`;
351
+ const once = addAuthProviderToRootLayout(layout);
352
+ const twice = addAuthProviderToRootLayout(once);
353
+
354
+ expect(twice).toBe(once);
355
+ expect(once.match(/import \{ AuthProvider \}/g)).toHaveLength(1);
356
+ expect(once.match(/<AuthProvider>/g)).toHaveLength(1);
357
+ expect(once).toContain("<AuthProvider>{children}</AuthProvider>");
358
+ });
359
+
360
+ it("preserves a React directive before the generated import", () => {
361
+ const layout = `'use client';\nimport { useAuth } from '@/lib/auth/auth-context';\nexport default function RootLayout({ children }: { children: React.ReactNode }) { return <body>{children}</body>; }`;
362
+ const updated = addAuthProviderToRootLayout(layout);
363
+
364
+ expect(updated).toMatch(/^'use client';\nimport \{ AuthProvider \}/);
365
+ expect(updated.match(/AuthProvider \} from/g)).toHaveLength(1);
366
+ });
367
+
368
+ it("treats unavailable or invalid /.auth/me responses as anonymous", () => {
369
+ const code = generateSwaAuthContext();
370
+ expect(code).toContain("if (!response.ok)");
371
+ expect(code).toContain("typeof body !== 'object'");
372
+ expect(code).toContain(".catch(() => setUser(null))");
373
+ expect(code).toContain(".finally(() => setLoading(false))");
374
+ });
375
+
376
+ it("normalizes a valid SWA principal and its string roles", () => {
377
+ const code = generateSwaAuthContext();
378
+ expect(code).toContain("typeof value.userId !== 'string'");
379
+ expect(code).toContain("value.userRoles.filter");
380
+ expect(code).toContain("satisfies AuthUser");
381
+ });
382
+
383
+ it("forwards a normalized platform principal with /.auth/me fallback", () => {
384
+ const code = generateBFFCallFunctionWithSwaAuth();
385
+ expect(code).toContain("x-ms-client-principal");
386
+ expect(code).toContain("/.auth/me");
387
+ expect(code).toContain("BACKEND_FUNCTIONS_KEY");
388
+ expect(code).not.toContain("fetchHeaders['Authorization']");
389
+ });
390
+
391
+ it.each([
392
+ ["typescript", generateSwaAuthHelperTS()],
393
+ ["csharp", generateSwaAuthHelperCSharp()],
394
+ ["python", generateSwaAuthHelperPython()],
395
+ ])("generates %s principal validation and role checks", (_language, code) => {
396
+ expect(code).toContain("x-ms-client-principal");
397
+ expect(code).toContain("authenticated");
398
+ expect(code.toLowerCase()).toContain("role");
399
+ expect(code).not.toContain("JWT_SECRET");
400
+ });
401
+ });
402
+
403
+ describe("external token auth generators", () => {
404
+ it("generates a fail-closed frontend adapter and authenticated fetch", () => {
405
+ expect(generateExternalTokenAdapter()).toContain("return null");
406
+ const fetchCode = generateAuthenticatedFetch();
407
+ expect(fetchCode).toContain("Authorization");
408
+ expect(fetchCode).toContain("Bearer");
409
+ expect(fetchCode).toContain("status: 401");
410
+ });
411
+
412
+ it("handles non-JSON auth/me responses without parsing them", () => {
413
+ const code = generateExternalTokenAuthContext();
414
+ expect(code).toContain("content-type");
415
+ expect(code).toContain(".catch(() => setUser(null))");
416
+ });
417
+
418
+ it.each([
419
+ ["typescript", generateExternalTokenVerifierTS()],
420
+ ["csharp", generateExternalTokenVerifierCSharp()],
421
+ ["python", generateExternalTokenVerifierPython()],
422
+ ])("generates a fail-closed %s verifier", (_language, code) => {
423
+ expect(code).toContain("External token verifier is not configured");
424
+ expect(code).not.toContain("JWT_SECRET");
425
+ });
426
+
427
+ it("awaits external token verification in TypeScript guards", () => {
428
+ expect(generateAuthGuardTS({}, "read", "external-token")).toContain("await requireAuth(request)");
429
+ expect(generateExternalTokenHelperTS()).toContain("await verifyExternalToken");
430
+ });
328
431
  });
329
432
 
330
433
  // ============================================================
@@ -6,6 +6,7 @@ import {
6
6
  buildFunctionsStartArgs,
7
7
  buildFunctionsCoreToolsCommand,
8
8
  buildNextDevArgs,
9
+ buildSwaStartArgs,
9
10
  buildDevCommand,
10
11
  buildPythonFunctionsEnv,
11
12
  compareVersionNumbers,
@@ -13,6 +14,7 @@ import {
13
14
  getFunctionsReadinessTimeoutMs,
14
15
  getCSharpFunctionsBuildArtifactPaths,
15
16
  getPythonVirtualEnvPaths,
17
+ getSwaCliInstallCommand,
16
18
  parseCoreToolsVersion,
17
19
  waitForHttpServerReady,
18
20
  } from "../cli/commands/dev";
@@ -70,6 +72,23 @@ describe("dev command helpers", () => {
70
72
  expect(buildFunctionsBaseUrl("127.0.0.1", "7072")).toBe("http://127.0.0.1:7072");
71
73
  });
72
74
 
75
+ it("builds SWA CLI arguments without bypassing the Next.js BFF", () => {
76
+ expect(buildSwaStartArgs(undefined, "3000", "4280")).toEqual([
77
+ "start",
78
+ "http://localhost:3000",
79
+ "--swa-config-location",
80
+ ".",
81
+ "--port",
82
+ "4280",
83
+ ]);
84
+ expect(buildSwaStartArgs(undefined, "3000", "4280")).not.toContain("--api-devserver-url");
85
+ });
86
+
87
+ it("provides a project-local SWA CLI install command", () => {
88
+ expect(getSwaCliInstallCommand("pnpm")).toBe("pnpm add -Dw @azure/static-web-apps-cli");
89
+ expect(getSwaCliInstallCommand("npm")).toBe("npm install -D @azure/static-web-apps-cli");
90
+ });
91
+
73
92
  it("gives C# more startup time before reporting readiness timeout", () => {
74
93
  expect(getFunctionsReadinessTimeoutMs("csharp")).toBeGreaterThan(getFunctionsReadinessTimeoutMs("typescript"));
75
94
  expect(getFunctionsReadinessTimeoutMs("csharp")).toBeGreaterThan(getFunctionsReadinessTimeoutMs("python"));
@@ -197,6 +216,8 @@ describe("dev CLI parser", () => {
197
216
  noFunctions: true,
198
217
  seedEnv: "local",
199
218
  mockConnectors: true,
219
+ swaPort: "4290",
220
+ noSwa: true,
200
221
  };
201
222
 
202
223
  const optionsAfterCommand = await parseDevOptions([
@@ -215,6 +236,9 @@ describe("dev CLI parser", () => {
215
236
  "--seed-env",
216
237
  "local",
217
238
  "--mock-connectors",
239
+ "--swa-port",
240
+ "4290",
241
+ "--no-swa",
218
242
  ]);
219
243
 
220
244
  const optionsBeforeCommand = await parseDevOptions([
@@ -226,6 +250,9 @@ describe("dev CLI parser", () => {
226
250
  "--seed-env",
227
251
  "local",
228
252
  "--mock-connectors",
253
+ "--swa-port",
254
+ "4290",
255
+ "--no-swa",
229
256
  "--no-functions",
230
257
  "--port",
231
258
  "3001",
@@ -17,6 +17,8 @@ import {
17
17
  injectSwallowKitNextConfig,
18
18
  parseIgnoredBuilds,
19
19
  buildCosmosDbFreeTierBicepSource,
20
+ buildFunctionsHostKeyBicepExpression,
21
+ buildStaticWebAppConfigBicepSource,
20
22
  withNpmLockfileSafeManifests,
21
23
  } from "../cli/commands/init";
22
24
 
@@ -100,6 +102,7 @@ module.exports = nextConfig;
100
102
  expect(dependencies).toEqual({
101
103
  "@azure/cosmos": "^4.0.0",
102
104
  applicationinsights: "^3.3.0",
105
+ zod: "^4.0.0",
103
106
  "@sample-app/shared": "workspace:*",
104
107
  });
105
108
  expect(dependencies).not.toHaveProperty("swallowkit");
@@ -141,6 +144,10 @@ module.exports = nextConfig;
141
144
  const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-lockfile-"));
142
145
  fs.mkdirSync(path.join(projectDir, "shared"));
143
146
  fs.mkdirSync(path.join(projectDir, "functions"));
147
+ fs.mkdirSync(path.join(projectDir, "node_modules"));
148
+ fs.mkdirSync(path.join(projectDir, "shared", "node_modules"));
149
+ fs.writeFileSync(path.join(projectDir, "node_modules", ".pnpm-marker"), "root");
150
+ fs.writeFileSync(path.join(projectDir, "shared", "node_modules", ".pnpm-marker"), "shared");
144
151
 
145
152
  const rootPackageJson = {
146
153
  name: "sample-app",
@@ -168,10 +175,14 @@ module.exports = nextConfig;
168
175
 
169
176
  expect(normalizedRoot.dependencies["@sample-app/shared"]).toBe("file:shared");
170
177
  expect(normalizedFunctions.dependencies["@sample-app/shared"]).toBe("file:../shared");
178
+ expect(fs.existsSync(path.join(projectDir, "node_modules"))).toBe(false);
179
+ expect(fs.existsSync(path.join(projectDir, "shared", "node_modules"))).toBe(false);
171
180
  });
172
181
 
173
182
  expect(JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf-8"))).toEqual(rootPackageJson);
174
183
  expect(JSON.parse(fs.readFileSync(path.join(projectDir, "functions", "package.json"), "utf-8"))).toEqual(functionsPackageJson);
184
+ expect(fs.readFileSync(path.join(projectDir, "node_modules", ".pnpm-marker"), "utf-8")).toBe("root");
185
+ expect(fs.readFileSync(path.join(projectDir, "shared", "node_modules", ".pnpm-marker"), "utf-8")).toBe("shared");
175
186
  });
176
187
  });
177
188
 
@@ -204,6 +215,22 @@ describe("buildCosmosDbFreeTierBicepSource", () => {
204
215
  });
205
216
  });
206
217
 
218
+ describe("Functions host key infrastructure", () => {
219
+ it("reads the generated default host key without exposing a literal value", () => {
220
+ const expression = buildFunctionsHostKeyBicepExpression();
221
+ expect(expression).toContain("listKeys(");
222
+ expect(expression).toContain("functionKeys.default");
223
+ });
224
+
225
+ it("passes the key to SWA through a secure module parameter", () => {
226
+ const source = buildStaticWebAppConfigBicepSource();
227
+ expect(source).toContain("@secure()");
228
+ expect(source).toContain("param functionsHostKey string");
229
+ expect(source).toContain("BACKEND_FUNCTIONS_KEY: functionsHostKey");
230
+ expect(source).not.toContain("output functionsHostKey");
231
+ });
232
+ });
233
+
207
234
  describe("GitHub Actions workflow generation", () => {
208
235
  it("uses npm ci for the SWA workflow when the project was initialized with pnpm", () => {
209
236
  const workflow = buildGitHubSwaWorkflow("npm", "pnpm");
@@ -21,6 +21,14 @@ describe("generateBFFCallFunction", () => {
21
21
  expect(code).toContain("export async function callFunction");
22
22
  });
23
23
 
24
+ it("uses a server-side Functions key and fails closed outside localhost", () => {
25
+ const code = generateBFFCallFunction();
26
+ expect(code).toContain("BACKEND_FUNCTIONS_KEY");
27
+ expect(code).toContain("'x-functions-key'");
28
+ expect(code).toContain("required for non-local Functions calls");
29
+ expect(code).not.toContain("?code=");
30
+ });
31
+
24
32
  it("includes z.ZodSchema typings", () => {
25
33
  const code = generateBFFCallFunction();
26
34
  expect(code).toContain("z.ZodSchema");
@@ -22,6 +22,23 @@ import {
22
22
  generateLoginPage,
23
23
  generateAuthContext,
24
24
  generateBFFCallFunctionWithAuth,
25
+ generateBFFCallFunctionWithSwaAuth,
26
+ generateSwaAuthHelperTS,
27
+ generateSwaAuthHelperCSharp,
28
+ generateSwaAuthHelperPython,
29
+ generateExternalTokenAdapter,
30
+ generateAuthenticatedFetch,
31
+ generateExternalTokenBFFMeRoute,
32
+ generateExternalTokenAuthContext,
33
+ generateExternalTokenVerifierTS,
34
+ generateExternalTokenHelperTS,
35
+ generateExternalAuthMeTS,
36
+ generateExternalTokenVerifierCSharp,
37
+ generateExternalTokenHelperCSharp,
38
+ generateExternalAuthMeCSharp,
39
+ generateExternalTokenVerifierPython,
40
+ generateExternalTokenHelperPython,
41
+ generateExternalAuthMePython,
25
42
  } from "../../core/scaffold/auth-generator";
26
43
  import { syncProjectManifest } from "../../core/project/manifest";
27
44
  import { buildSharedTsConfig } from "./init";
@@ -30,14 +47,194 @@ interface AddAuthOptions {
30
47
  provider?: string;
31
48
  }
32
49
 
50
+ function writeIfMissing(filePath: string, content: string): void {
51
+ if (fs.existsSync(filePath)) return;
52
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
53
+ fs.writeFileSync(filePath, content, "utf-8");
54
+ }
55
+
56
+ function defaultCustomJwtConfig(): CustomJwtConfig {
57
+ return {
58
+ userConnector: "mysql",
59
+ userTable: "users",
60
+ loginIdColumn: "login_id",
61
+ passwordHashColumn: "password_hash",
62
+ rolesColumn: "roles",
63
+ jwtSecretEnv: "JWT_SECRET",
64
+ tokenExpiry: "24h",
65
+ };
66
+ }
67
+
68
+ function setupSwaAuth(cwd: string, backendLanguage: BackendLanguage): void {
69
+ const helperPath = backendLanguage === "typescript"
70
+ ? path.join(cwd, "functions", "src", "auth", "jwt-helper.ts")
71
+ : backendLanguage === "csharp"
72
+ ? path.join(cwd, "functions", "Auth", "JwtHelper.cs")
73
+ : path.join(cwd, "functions", "auth", "jwt_helper.py");
74
+ fs.mkdirSync(path.dirname(helperPath), { recursive: true });
75
+ const helper = backendLanguage === "typescript"
76
+ ? generateSwaAuthHelperTS()
77
+ : backendLanguage === "csharp"
78
+ ? generateSwaAuthHelperCSharp()
79
+ : generateSwaAuthHelperPython();
80
+ fs.writeFileSync(helperPath, helper, "utf-8");
81
+
82
+ const callFnPath = path.join(cwd, "lib", "api", "call-function.ts");
83
+ fs.mkdirSync(path.dirname(callFnPath), { recursive: true });
84
+ fs.writeFileSync(callFnPath, generateBFFCallFunctionWithSwaAuth(), "utf-8");
85
+
86
+ const authDir = path.join(cwd, "lib", "auth");
87
+ fs.mkdirSync(authDir, { recursive: true });
88
+ fs.writeFileSync(path.join(authDir, "auth-context.tsx"), generateSwaAuthContext(), "utf-8");
89
+ updateRootLayoutWithAuthProvider(cwd);
90
+ const loginDir = path.join(cwd, "app", "login");
91
+ fs.mkdirSync(loginDir, { recursive: true });
92
+ fs.writeFileSync(path.join(loginDir, "page.tsx"), generateSwaLoginPage(), "utf-8");
93
+ updateSwaRouteConfig(cwd);
94
+ }
95
+
96
+ function setupExternalTokenAuth(cwd: string, backendLanguage: BackendLanguage): void {
97
+ const authDir = path.join(cwd, "lib", "auth");
98
+ fs.mkdirSync(authDir, { recursive: true });
99
+ writeIfMissing(path.join(authDir, "external-token-adapter.ts"), generateExternalTokenAdapter());
100
+ fs.writeFileSync(path.join(authDir, "authenticated-fetch.ts"), generateAuthenticatedFetch(), "utf-8");
101
+ fs.writeFileSync(path.join(authDir, "auth-context.tsx"), generateExternalTokenAuthContext(), "utf-8");
102
+ fs.writeFileSync(path.join(cwd, "lib", "api", "call-function.ts"), generateBFFCallFunctionWithAuth(), "utf-8");
103
+ const meDir = path.join(cwd, "app", "api", "auth", "me");
104
+ fs.mkdirSync(meDir, { recursive: true });
105
+ fs.writeFileSync(path.join(meDir, "route.ts"), generateExternalTokenBFFMeRoute(), "utf-8");
106
+ updateRootLayoutWithAuthProvider(cwd);
107
+
108
+ if (backendLanguage === "typescript") {
109
+ const verifier = path.join(cwd, "functions", "src", "auth", "external-token-verifier.ts");
110
+ writeIfMissing(verifier, generateExternalTokenVerifierTS());
111
+ fs.writeFileSync(path.join(cwd, "functions", "src", "auth", "jwt-helper.ts"), generateExternalTokenHelperTS(), "utf-8");
112
+ fs.writeFileSync(path.join(cwd, "functions", "src", "auth-me.ts"), generateExternalAuthMeTS(), "utf-8");
113
+ } else if (backendLanguage === "csharp") {
114
+ const verifier = path.join(cwd, "functions", "Auth", "ExternalTokenVerifier.cs");
115
+ writeIfMissing(verifier, generateExternalTokenVerifierCSharp());
116
+ fs.writeFileSync(path.join(cwd, "functions", "Auth", "JwtHelper.cs"), generateExternalTokenHelperCSharp(), "utf-8");
117
+ fs.writeFileSync(path.join(cwd, "functions", "Auth", "ExternalAuthMe.cs"), generateExternalAuthMeCSharp(), "utf-8");
118
+ } else {
119
+ const verifier = path.join(cwd, "functions", "auth", "external_token_verifier.py");
120
+ writeIfMissing(verifier, generateExternalTokenVerifierPython());
121
+ fs.writeFileSync(path.join(cwd, "functions", "auth", "jwt_helper.py"), generateExternalTokenHelperPython(), "utf-8");
122
+ const authMePath = path.join(cwd, "functions", "blueprints", "auth_me.py");
123
+ fs.mkdirSync(path.dirname(authMePath), { recursive: true });
124
+ fs.writeFileSync(authMePath, generateExternalAuthMePython(), "utf-8");
125
+ updatePythonAuthMeRegistration(cwd);
126
+ }
127
+ }
128
+
129
+ function updatePythonAuthMeRegistration(cwd: string): void {
130
+ const appPath = path.join(cwd, "functions", "function_app.py");
131
+ if (!fs.existsSync(appPath)) return;
132
+ const importLine = "from blueprints.auth_me import bp as external_auth_me_bp";
133
+ const registerLine = "app.register_blueprint(external_auth_me_bp)";
134
+ let content = fs.readFileSync(appPath, "utf-8");
135
+ if (content.includes(importLine)) return;
136
+ const marker = "# SwallowKit scaffold registrations";
137
+ content = content.includes(marker)
138
+ ? content.replace(marker, `${importLine}\n${registerLine}\n${marker}`)
139
+ : `${content.trimEnd()}\n${importLine}\n${registerLine}\n`;
140
+ fs.writeFileSync(appPath, content, "utf-8");
141
+ }
142
+
143
+ function generateSwaLoginPage(): string {
144
+ return `'use client';
145
+ export default function LoginPage() {
146
+ return <main><h1>Sign in</h1><a href="/.auth/login/aad?post_login_redirect_uri=/">Sign in with Microsoft</a></main>;
147
+ }
148
+ `;
149
+ }
150
+
151
+ export function generateSwaAuthContext(): string {
152
+ return `'use client';
153
+ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
154
+
155
+ export interface AuthUser { id: string; loginId: string; name: string; email: string; roles: string[]; }
156
+ interface AuthContextValue { user: AuthUser | null; loading: boolean; login: () => void; logout: () => void; hasRole: (role: string) => boolean; hasAnyRole: (roles: string[]) => boolean; }
157
+ const AuthContext = createContext<AuthContextValue | null>(null);
158
+
159
+ export function AuthProvider({ children }: { children: ReactNode }) {
160
+ const [user, setUser] = useState<AuthUser | null>(null);
161
+ const [loading, setLoading] = useState(true);
162
+ useEffect(() => {
163
+ fetch('/.auth/me')
164
+ .then(async response => {
165
+ if (!response.ok) throw new Error('SWA authentication is unavailable');
166
+ const body: unknown = await response.json();
167
+ if (!body || typeof body !== 'object' || !('clientPrincipal' in body)) return null;
168
+ const principal = body.clientPrincipal;
169
+ if (!principal || typeof principal !== 'object') return null;
170
+ const value = principal as Record<string, unknown>;
171
+ if (typeof value.userId !== 'string') return null;
172
+ return {
173
+ id: value.userId,
174
+ loginId: typeof value.userDetails === 'string' ? value.userDetails : '',
175
+ name: typeof value.userDetails === 'string' ? value.userDetails : '',
176
+ email: typeof value.userDetails === 'string' ? value.userDetails : '',
177
+ roles: Array.isArray(value.userRoles) ? value.userRoles.filter((role): role is string => typeof role === 'string') : [],
178
+ } satisfies AuthUser;
179
+ })
180
+ .then(setUser)
181
+ .catch(() => setUser(null))
182
+ .finally(() => setLoading(false));
183
+ }, []);
184
+ const login = () => { window.location.href = '/.auth/login/aad?post_login_redirect_uri=/'; };
185
+ const logout = () => { window.location.href = '/.auth/logout?post_logout_redirect_uri=/'; };
186
+ const hasRole = (role: string) => user?.roles.includes(role) ?? false;
187
+ return <AuthContext.Provider value={{ user, loading, login, logout, hasRole, hasAnyRole: roles => roles.some(hasRole) }}>{children}</AuthContext.Provider>;
188
+ }
189
+ export function useAuth() { const value = useContext(AuthContext); if (!value) throw new Error('useAuth must be used within an AuthProvider'); return value; }
190
+ `;
191
+ }
192
+
193
+ export function addAuthProviderToRootLayout(layout: string): string {
194
+ let updated = layout;
195
+ if (!/import\s*\{[^}]*\bAuthProvider\b[^}]*\}\s*from\s*['"]@\/lib\/auth\/auth-context['"]/.test(updated)) {
196
+ const directive = updated.match(/^(?:\s*['"]use (?:client|server)['"];?\s*\r?\n)/)?.[0] ?? "";
197
+ updated = `${directive}import { AuthProvider } from '@/lib/auth/auth-context';\n${updated.slice(directive.length)}`;
198
+ }
199
+ if (!/<AuthProvider(?:\s|>)/.test(updated)) {
200
+ const childrenIndex = updated.lastIndexOf("{children}");
201
+ if (childrenIndex === -1) {
202
+ throw new Error("Could not find {children} in app/layout.tsx");
203
+ }
204
+ updated = `${updated.slice(0, childrenIndex)}<AuthProvider>{children}</AuthProvider>${updated.slice(childrenIndex + "{children}".length)}`;
205
+ }
206
+ return updated;
207
+ }
208
+
209
+ function updateRootLayoutWithAuthProvider(cwd: string): void {
210
+ const layoutPath = path.join(cwd, "app", "layout.tsx");
211
+ if (!fs.existsSync(layoutPath)) {
212
+ throw new Error("app/layout.tsx was not found; cannot install AuthProvider");
213
+ }
214
+ const layout = fs.readFileSync(layoutPath, "utf-8");
215
+ const updated = addAuthProviderToRootLayout(layout);
216
+ if (updated !== layout) fs.writeFileSync(layoutPath, updated, "utf-8");
217
+ }
218
+
219
+ function updateSwaRouteConfig(cwd: string): void {
220
+ const configPath = path.join(cwd, "staticwebapp.config.json");
221
+ const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf-8")) : {};
222
+ config.routes = Array.isArray(config.routes)
223
+ ? config.routes.filter((route: { route?: string }) => route.route !== "/api/*")
224
+ : [];
225
+ config.routes.unshift({ route: "/api/*", allowedRoles: ["authenticated"] });
226
+ config.responseOverrides = { ...(config.responseOverrides || {}), "401": { statusCode: 302, redirect: "/.auth/login/aad?post_login_redirect_uri=.referrer" } };
227
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
228
+ }
229
+
33
230
  export async function addAuthCommand(options: AddAuthOptions) {
34
231
  ensureSwallowKitProject("add-auth");
35
232
 
36
233
  console.log(" SwallowKit Add-Auth: Setting up authentication...\n");
37
234
 
38
235
  const provider = (options.provider || "custom-jwt") as AuthProvider;
39
- if (!["custom-jwt", "swa", "swa-custom", "none"].includes(provider)) {
40
- console.error(` Unknown provider: ${provider}. Use: custom-jwt | swa | swa-custom | none`);
236
+ if (!["custom-jwt", "swa", "external-token", "swa-custom", "none"].includes(provider)) {
237
+ console.error(` Unknown provider: ${provider}. Use: custom-jwt | swa | external-token | swa-custom | none`);
41
238
  process.exit(1);
42
239
  }
43
240
 
@@ -45,6 +242,31 @@ export async function addAuthCommand(options: AddAuthOptions) {
45
242
  const config = getFullConfig();
46
243
  const cwd = process.cwd();
47
244
 
245
+ if (provider === "swa-custom") {
246
+ throw new Error("The swa-custom provider is not implemented. Use --provider swa or --provider custom-jwt.");
247
+ }
248
+ if (provider === "none") {
249
+ updateConfigWithAuth(cwd, provider, config.auth?.customJwt || defaultCustomJwtConfig());
250
+ await syncProjectManifest();
251
+ console.log(" Authentication disabled in swallowkit.config.js");
252
+ return;
253
+ }
254
+ if (provider === "swa") {
255
+ setupSwaAuth(cwd, backendLanguage);
256
+ updateConfigWithAuth(cwd, provider, config.auth?.customJwt || defaultCustomJwtConfig());
257
+ await syncProjectManifest();
258
+ console.log("\n SWA built-in authentication setup complete!");
259
+ return;
260
+ }
261
+ if (provider === "external-token") {
262
+ setupExternalTokenAuth(cwd, backendLanguage);
263
+ updateConfigWithAuth(cwd, provider, config.auth?.customJwt || defaultCustomJwtConfig());
264
+ await syncProjectManifest();
265
+ console.log("\n External token authentication setup complete!");
266
+ console.log(" Implement the generated frontend adapter and backend verifier before use.");
267
+ return;
268
+ }
269
+
48
270
  // Read project name from package.json
49
271
  const pkgPath = path.join(cwd, "package.json");
50
272
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
@@ -59,15 +281,9 @@ export async function addAuthCommand(options: AddAuthOptions) {
59
281
  }
60
282
 
61
283
  // Default custom-jwt config
62
- const customJwtConfig: CustomJwtConfig = config.auth?.customJwt || {
63
- userConnector: "mysql",
64
- userTable: "users",
65
- loginIdColumn: "login_id",
66
- passwordHashColumn: "password_hash",
67
- rolesColumn: "roles",
68
- jwtSecretEnv: "JWT_SECRET",
69
- tokenExpiry: "24h",
70
- };
284
+ const customJwtConfig: CustomJwtConfig = config.auth?.customJwt || defaultCustomJwtConfig();
285
+
286
+ // custom-jwt setup continues below.
71
287
 
72
288
  // 1. Generate shared/models/auth.ts
73
289
  console.log(" Generating auth models...");
@@ -359,10 +575,7 @@ function updateConfigWithAuth(cwd: string, provider: AuthProvider, config: Custo
359
575
  const beforeClosing = content.substring(0, closingBraceIdx).trimEnd();
360
576
  const needsComma = !beforeClosing.endsWith(",") && !beforeClosing.endsWith("{");
361
577
 
362
- const authBlock = `${needsComma ? "," : ""}
363
- // 認証認可設定
364
- auth: {
365
- provider: '${provider}',
578
+ const providerSettings = provider === "custom-jwt" ? `
366
579
  customJwt: {
367
580
  userConnector: '${config.userConnector}',
368
581
  userTable: '${config.userTable}',
@@ -371,7 +584,17 @@ function updateConfigWithAuth(cwd: string, provider: AuthProvider, config: Custo
371
584
  rolesColumn: '${config.rolesColumn}',
372
585
  jwtSecretEnv: '${config.jwtSecretEnv || "JWT_SECRET"}',
373
586
  tokenExpiry: '${config.tokenExpiry || "24h"}',
374
- },
587
+ },` : provider === "swa" ? `
588
+ swa: {
589
+ allowedProviders: ['aad'],
590
+ roleSource: 'swa-roles',
591
+ },` : "";
592
+
593
+ const authBlock = `${needsComma ? "," : ""}
594
+ // 認証認可設定
595
+ auth: {
596
+ provider: '${provider}',
597
+ ${providerSettings}
375
598
  authorization: {
376
599
  defaultPolicy: 'authenticated',
377
600
  },