swallowkit 1.0.0-beta.46 → 1.0.0-beta.48

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 (84) hide show
  1. package/dist/cli/commands/add-auth.d.ts +3 -0
  2. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  3. package/dist/cli/commands/add-auth.js +350 -17
  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 +104 -13
  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 +56 -30
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/commands/scaffold.d.ts +9 -0
  14. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  15. package/dist/cli/commands/scaffold.js +84 -8
  16. package/dist/cli/commands/scaffold.js.map +1 -1
  17. package/dist/cli/index.d.ts.map +1 -1
  18. package/dist/cli/index.js +5 -1
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/core/config.d.ts +7 -2
  21. package/dist/core/config.d.ts.map +1 -1
  22. package/dist/core/config.js +81 -1
  23. package/dist/core/config.js.map +1 -1
  24. package/dist/core/mock/connector-mock-server.js +2 -2
  25. package/dist/core/mock/connector-mock-server.js.map +1 -1
  26. package/dist/core/project/manifest.js +2 -2
  27. package/dist/core/project/manifest.js.map +1 -1
  28. package/dist/core/project/validation.d.ts.map +1 -1
  29. package/dist/core/project/validation.js +17 -0
  30. package/dist/core/project/validation.js.map +1 -1
  31. package/dist/core/scaffold/auth-generator.d.ts +28 -3
  32. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  33. package/dist/core/scaffold/auth-generator.js +493 -23
  34. package/dist/core/scaffold/auth-generator.js.map +1 -1
  35. package/dist/core/scaffold/connector-functions-generator.d.ts +3 -3
  36. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
  37. package/dist/core/scaffold/connector-functions-generator.js +23 -21
  38. package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
  39. package/dist/core/scaffold/functions-generator.d.ts +2 -2
  40. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  41. package/dist/core/scaffold/functions-generator.js +20 -18
  42. package/dist/core/scaffold/functions-generator.js.map +1 -1
  43. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  44. package/dist/core/scaffold/model-parser.js +8 -3
  45. package/dist/core/scaffold/model-parser.js.map +1 -1
  46. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  47. package/dist/core/scaffold/nextjs-generator.js +13 -1
  48. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  49. package/dist/core/scaffold/ui-generator.d.ts +3 -0
  50. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  51. package/dist/core/scaffold/ui-generator.js +12 -8
  52. package/dist/core/scaffold/ui-generator.js.map +1 -1
  53. package/dist/types/index.d.ts +35 -5
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/dist/utils/package-manager.d.ts.map +1 -1
  56. package/dist/utils/package-manager.js +1 -0
  57. package/dist/utils/package-manager.js.map +1 -1
  58. package/package.json +1 -1
  59. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +20 -20
  60. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +13 -1
  61. package/src/__tests__/auth.test.ts +107 -0
  62. package/src/__tests__/config.test.ts +22 -1
  63. package/src/__tests__/dev.test.ts +27 -0
  64. package/src/__tests__/init.test.ts +19 -0
  65. package/src/__tests__/mcp.test.ts +2 -2
  66. package/src/__tests__/multi-auth.test.ts +133 -0
  67. package/src/__tests__/nextjs-generator.test.ts +8 -0
  68. package/src/cli/commands/add-auth.ts +351 -18
  69. package/src/cli/commands/dev.ts +116 -16
  70. package/src/cli/commands/init.ts +56 -30
  71. package/src/cli/commands/scaffold.ts +86 -10
  72. package/src/cli/index.ts +5 -1
  73. package/src/core/config.ts +69 -2
  74. package/src/core/mock/connector-mock-server.ts +2 -2
  75. package/src/core/project/manifest.ts +3 -3
  76. package/src/core/project/validation.ts +16 -0
  77. package/src/core/scaffold/auth-generator.ts +500 -24
  78. package/src/core/scaffold/connector-functions-generator.ts +26 -21
  79. package/src/core/scaffold/functions-generator.ts +21 -19
  80. package/src/core/scaffold/model-parser.ts +9 -3
  81. package/src/core/scaffold/nextjs-generator.ts +13 -1
  82. package/src/core/scaffold/ui-generator.ts +15 -8
  83. package/src/types/index.ts +36 -5
  84. package/src/utils/package-manager.ts +1 -0
@@ -5,7 +5,7 @@
5
5
 
6
6
  import * as fs from "fs";
7
7
  import * as path from "path";
8
- import { ensureSwallowKitProject, getBackendLanguage, getConnectorDefinition, getFullConfig } from "../../core/config";
8
+ import { ensureSwallowKitProject, getBackendLanguage, getConnectorDefinition, getValidatedFullConfig } from "../../core/config";
9
9
  import { AuthProvider, BackendLanguage, CustomJwtConfig, RdbConnectorConfig } from "../../types";
10
10
  import {
11
11
  generateAuthModels,
@@ -22,12 +22,213 @@ import {
22
22
  generateLoginPage,
23
23
  generateAuthContext,
24
24
  generateBFFCallFunctionWithAuth,
25
+ generateBFFCallFunctionWithSwaAuth,
26
+ generateBFFCallFunctionWithMultipleAuth,
27
+ generateSwaAuthHelperTS,
28
+ generateSwaAuthHelperCSharp,
29
+ generateSwaAuthHelperPython,
30
+ generateExternalTokenAdapter,
31
+ generateAuthenticatedFetch,
32
+ generateExternalTokenBFFMeRoute,
33
+ generateExternalTokenAuthContext,
34
+ generateExternalTokenVerifierTS,
35
+ generateExternalTokenHelperTS,
36
+ generateExternalAuthMeTS,
37
+ generateExternalTokenVerifierCSharp,
38
+ generateExternalTokenHelperCSharp,
39
+ generateExternalAuthMeCSharp,
40
+ generateExternalTokenVerifierPython,
41
+ generateExternalTokenHelperPython,
42
+ generateExternalAuthMePython,
43
+ generateNamedExternalTokenVerifierCSharp,
44
+ generateNamedExternalTokenVerifierPython,
25
45
  } from "../../core/scaffold/auth-generator";
26
46
  import { syncProjectManifest } from "../../core/project/manifest";
27
47
  import { buildSharedTsConfig } from "./init";
28
48
 
29
49
  interface AddAuthOptions {
30
50
  provider?: string;
51
+ scheme?: string;
52
+ }
53
+
54
+ function writeIfMissing(filePath: string, content: string): void {
55
+ if (fs.existsSync(filePath)) return;
56
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
57
+ fs.writeFileSync(filePath, content, "utf-8");
58
+ }
59
+
60
+ function defaultCustomJwtConfig(): CustomJwtConfig {
61
+ return {
62
+ userConnector: "mysql",
63
+ userTable: "users",
64
+ loginIdColumn: "login_id",
65
+ passwordHashColumn: "password_hash",
66
+ rolesColumn: "roles",
67
+ jwtSecretEnv: "JWT_SECRET",
68
+ tokenExpiry: "24h",
69
+ };
70
+ }
71
+
72
+ function setupSwaAuth(cwd: string, backendLanguage: BackendLanguage): void {
73
+ const helperPath = backendLanguage === "typescript"
74
+ ? path.join(cwd, "functions", "src", "auth", "jwt-helper.ts")
75
+ : backendLanguage === "csharp"
76
+ ? path.join(cwd, "functions", "Auth", "JwtHelper.cs")
77
+ : path.join(cwd, "functions", "auth", "jwt_helper.py");
78
+ fs.mkdirSync(path.dirname(helperPath), { recursive: true });
79
+ const helper = backendLanguage === "typescript"
80
+ ? generateSwaAuthHelperTS()
81
+ : backendLanguage === "csharp"
82
+ ? generateSwaAuthHelperCSharp()
83
+ : generateSwaAuthHelperPython();
84
+ fs.writeFileSync(helperPath, helper, "utf-8");
85
+
86
+ const callFnPath = path.join(cwd, "lib", "api", "call-function.ts");
87
+ fs.mkdirSync(path.dirname(callFnPath), { recursive: true });
88
+ fs.writeFileSync(callFnPath, generateBFFCallFunctionWithSwaAuth(), "utf-8");
89
+
90
+ const authDir = path.join(cwd, "lib", "auth");
91
+ fs.mkdirSync(authDir, { recursive: true });
92
+ fs.writeFileSync(path.join(authDir, "auth-context.tsx"), generateSwaAuthContext(), "utf-8");
93
+ updateRootLayoutWithAuthProvider(cwd);
94
+ const loginDir = path.join(cwd, "app", "login");
95
+ fs.mkdirSync(loginDir, { recursive: true });
96
+ fs.writeFileSync(path.join(loginDir, "page.tsx"), generateSwaLoginPage(), "utf-8");
97
+ updateSwaRouteConfig(cwd);
98
+ }
99
+
100
+ function setupExternalTokenAuth(cwd: string, backendLanguage: BackendLanguage): void {
101
+ const authDir = path.join(cwd, "lib", "auth");
102
+ fs.mkdirSync(authDir, { recursive: true });
103
+ writeIfMissing(path.join(authDir, "external-token-adapter.ts"), generateExternalTokenAdapter());
104
+ fs.writeFileSync(path.join(authDir, "authenticated-fetch.ts"), generateAuthenticatedFetch(), "utf-8");
105
+ fs.writeFileSync(path.join(authDir, "auth-context.tsx"), generateExternalTokenAuthContext(), "utf-8");
106
+ fs.writeFileSync(path.join(cwd, "lib", "api", "call-function.ts"), generateBFFCallFunctionWithAuth(), "utf-8");
107
+ const meDir = path.join(cwd, "app", "api", "auth", "me");
108
+ fs.mkdirSync(meDir, { recursive: true });
109
+ fs.writeFileSync(path.join(meDir, "route.ts"), generateExternalTokenBFFMeRoute(), "utf-8");
110
+ updateRootLayoutWithAuthProvider(cwd);
111
+
112
+ if (backendLanguage === "typescript") {
113
+ const verifier = path.join(cwd, "functions", "src", "auth", "external-token-verifier.ts");
114
+ writeIfMissing(verifier, generateExternalTokenVerifierTS());
115
+ fs.writeFileSync(path.join(cwd, "functions", "src", "auth", "jwt-helper.ts"), generateExternalTokenHelperTS(), "utf-8");
116
+ fs.writeFileSync(path.join(cwd, "functions", "src", "auth-me.ts"), generateExternalAuthMeTS(), "utf-8");
117
+ } else if (backendLanguage === "csharp") {
118
+ const verifier = path.join(cwd, "functions", "Auth", "ExternalTokenVerifier.cs");
119
+ writeIfMissing(verifier, generateExternalTokenVerifierCSharp());
120
+ fs.writeFileSync(path.join(cwd, "functions", "Auth", "JwtHelper.cs"), generateExternalTokenHelperCSharp(), "utf-8");
121
+ fs.writeFileSync(path.join(cwd, "functions", "Auth", "ExternalAuthMe.cs"), generateExternalAuthMeCSharp(), "utf-8");
122
+ } else {
123
+ const verifier = path.join(cwd, "functions", "auth", "external_token_verifier.py");
124
+ writeIfMissing(verifier, generateExternalTokenVerifierPython());
125
+ fs.writeFileSync(path.join(cwd, "functions", "auth", "jwt_helper.py"), generateExternalTokenHelperPython(), "utf-8");
126
+ const authMePath = path.join(cwd, "functions", "blueprints", "auth_me.py");
127
+ fs.mkdirSync(path.dirname(authMePath), { recursive: true });
128
+ fs.writeFileSync(authMePath, generateExternalAuthMePython(), "utf-8");
129
+ updatePythonAuthMeRegistration(cwd);
130
+ }
131
+ }
132
+
133
+ function updatePythonAuthMeRegistration(cwd: string): void {
134
+ const appPath = path.join(cwd, "functions", "function_app.py");
135
+ if (!fs.existsSync(appPath)) return;
136
+ const importLine = "from blueprints.auth_me import bp as external_auth_me_bp";
137
+ const registerLine = "app.register_blueprint(external_auth_me_bp)";
138
+ let content = fs.readFileSync(appPath, "utf-8");
139
+ if (content.includes(importLine)) return;
140
+ const marker = "# SwallowKit scaffold registrations";
141
+ content = content.includes(marker)
142
+ ? content.replace(marker, `${importLine}\n${registerLine}\n${marker}`)
143
+ : `${content.trimEnd()}\n${importLine}\n${registerLine}\n`;
144
+ fs.writeFileSync(appPath, content, "utf-8");
145
+ }
146
+
147
+ function generateSwaLoginPage(): string {
148
+ return `'use client';
149
+ export default function LoginPage() {
150
+ return <main><h1>Sign in</h1><a href="/.auth/login/aad?post_login_redirect_uri=/">Sign in with Microsoft</a></main>;
151
+ }
152
+ `;
153
+ }
154
+
155
+ export function generateSwaAuthContext(): string {
156
+ return `'use client';
157
+ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
158
+
159
+ export interface AuthUser { id: string; loginId: string; name: string; email: string; roles: string[]; }
160
+ interface AuthContextValue { user: AuthUser | null; loading: boolean; login: () => void; logout: () => void; hasRole: (role: string) => boolean; hasAnyRole: (roles: string[]) => boolean; }
161
+ const AuthContext = createContext<AuthContextValue | null>(null);
162
+
163
+ export function AuthProvider({ children }: { children: ReactNode }) {
164
+ const [user, setUser] = useState<AuthUser | null>(null);
165
+ const [loading, setLoading] = useState(true);
166
+ useEffect(() => {
167
+ fetch('/.auth/me')
168
+ .then(async response => {
169
+ if (!response.ok) throw new Error('SWA authentication is unavailable');
170
+ const body: unknown = await response.json();
171
+ if (!body || typeof body !== 'object' || !('clientPrincipal' in body)) return null;
172
+ const principal = body.clientPrincipal;
173
+ if (!principal || typeof principal !== 'object') return null;
174
+ const value = principal as Record<string, unknown>;
175
+ if (typeof value.userId !== 'string') return null;
176
+ return {
177
+ id: value.userId,
178
+ loginId: typeof value.userDetails === 'string' ? value.userDetails : '',
179
+ name: typeof value.userDetails === 'string' ? value.userDetails : '',
180
+ email: typeof value.userDetails === 'string' ? value.userDetails : '',
181
+ roles: Array.isArray(value.userRoles) ? value.userRoles.filter((role): role is string => typeof role === 'string') : [],
182
+ } satisfies AuthUser;
183
+ })
184
+ .then(setUser)
185
+ .catch(() => setUser(null))
186
+ .finally(() => setLoading(false));
187
+ }, []);
188
+ const login = () => { window.location.href = '/.auth/login/aad?post_login_redirect_uri=/'; };
189
+ const logout = () => { window.location.href = '/.auth/logout?post_logout_redirect_uri=/'; };
190
+ const hasRole = (role: string) => user?.roles.includes(role) ?? false;
191
+ return <AuthContext.Provider value={{ user, loading, login, logout, hasRole, hasAnyRole: roles => roles.some(hasRole) }}>{children}</AuthContext.Provider>;
192
+ }
193
+ export function useAuth() { const value = useContext(AuthContext); if (!value) throw new Error('useAuth must be used within an AuthProvider'); return value; }
194
+ `;
195
+ }
196
+
197
+ export function addAuthProviderToRootLayout(layout: string): string {
198
+ let updated = layout;
199
+ if (!/import\s*\{[^}]*\bAuthProvider\b[^}]*\}\s*from\s*['"]@\/lib\/auth\/auth-context['"]/.test(updated)) {
200
+ const directive = updated.match(/^(?:\s*['"]use (?:client|server)['"];?\s*\r?\n)/)?.[0] ?? "";
201
+ updated = `${directive}import { AuthProvider } from '@/lib/auth/auth-context';\n${updated.slice(directive.length)}`;
202
+ }
203
+ if (!/<AuthProvider(?:\s|>)/.test(updated)) {
204
+ const childrenIndex = updated.lastIndexOf("{children}");
205
+ if (childrenIndex === -1) {
206
+ throw new Error("Could not find {children} in app/layout.tsx");
207
+ }
208
+ updated = `${updated.slice(0, childrenIndex)}<AuthProvider>{children}</AuthProvider>${updated.slice(childrenIndex + "{children}".length)}`;
209
+ }
210
+ return updated;
211
+ }
212
+
213
+ function updateRootLayoutWithAuthProvider(cwd: string): void {
214
+ const layoutPath = path.join(cwd, "app", "layout.tsx");
215
+ if (!fs.existsSync(layoutPath)) {
216
+ throw new Error("app/layout.tsx was not found; cannot install AuthProvider");
217
+ }
218
+ const layout = fs.readFileSync(layoutPath, "utf-8");
219
+ const updated = addAuthProviderToRootLayout(layout);
220
+ if (updated !== layout) fs.writeFileSync(layoutPath, updated, "utf-8");
221
+ }
222
+
223
+ function updateSwaRouteConfig(cwd: string): void {
224
+ const configPath = path.join(cwd, "staticwebapp.config.json");
225
+ const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf-8")) : {};
226
+ config.routes = Array.isArray(config.routes)
227
+ ? config.routes.filter((route: { route?: string }) => route.route !== "/api/*")
228
+ : [];
229
+ config.routes.unshift({ route: "/api/*", allowedRoles: ["authenticated"] });
230
+ config.responseOverrides = { ...(config.responseOverrides || {}), "401": { statusCode: 302, redirect: "/.auth/login/aad?post_login_redirect_uri=.referrer" } };
231
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
31
232
  }
32
233
 
33
234
  export async function addAuthCommand(options: AddAuthOptions) {
@@ -36,15 +237,49 @@ export async function addAuthCommand(options: AddAuthOptions) {
36
237
  console.log(" SwallowKit Add-Auth: Setting up authentication...\n");
37
238
 
38
239
  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`);
240
+ if (!["custom-jwt", "swa", "external-token", "swa-custom", "none"].includes(provider)) {
241
+ console.error(` Unknown provider: ${provider}. Use: custom-jwt | swa | external-token | swa-custom | none`);
41
242
  process.exit(1);
42
243
  }
43
244
 
44
245
  const backendLanguage = getBackendLanguage();
45
- const config = getFullConfig();
246
+ const config = getValidatedFullConfig();
46
247
  const cwd = process.cwd();
47
248
 
249
+ if (options.scheme) {
250
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(options.scheme)) throw new Error("--scheme must start with a letter and contain only letters, digits, '_' or '-'");
251
+ if (provider === "none" || provider === "swa-custom") throw new Error(`Provider '${provider}' cannot be added as a named scheme`);
252
+ addNamedScheme(cwd, options.scheme, provider, backendLanguage);
253
+ await syncProjectManifest();
254
+ console.log(`\n Named authentication scheme '${options.scheme}' added.`);
255
+ return;
256
+ }
257
+
258
+ if (provider === "swa-custom") {
259
+ throw new Error("The swa-custom provider is not implemented. Use --provider swa or --provider custom-jwt.");
260
+ }
261
+ if (provider === "none") {
262
+ updateConfigWithAuth(cwd, provider, config.auth?.customJwt || defaultCustomJwtConfig());
263
+ await syncProjectManifest();
264
+ console.log(" Authentication disabled in swallowkit.config.js");
265
+ return;
266
+ }
267
+ if (provider === "swa") {
268
+ setupSwaAuth(cwd, backendLanguage);
269
+ updateConfigWithAuth(cwd, provider, config.auth?.customJwt || defaultCustomJwtConfig());
270
+ await syncProjectManifest();
271
+ console.log("\n SWA built-in authentication setup complete!");
272
+ return;
273
+ }
274
+ if (provider === "external-token") {
275
+ setupExternalTokenAuth(cwd, backendLanguage);
276
+ updateConfigWithAuth(cwd, provider, config.auth?.customJwt || defaultCustomJwtConfig());
277
+ await syncProjectManifest();
278
+ console.log("\n External token authentication setup complete!");
279
+ console.log(" Implement the generated frontend adapter and backend verifier before use.");
280
+ return;
281
+ }
282
+
48
283
  // Read project name from package.json
49
284
  const pkgPath = path.join(cwd, "package.json");
50
285
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
@@ -59,15 +294,9 @@ export async function addAuthCommand(options: AddAuthOptions) {
59
294
  }
60
295
 
61
296
  // 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
- };
297
+ const customJwtConfig: CustomJwtConfig = config.auth?.customJwt || defaultCustomJwtConfig();
298
+
299
+ // custom-jwt setup continues below.
71
300
 
72
301
  // 1. Generate shared/models/auth.ts
73
302
  console.log(" Generating auth models...");
@@ -148,6 +377,103 @@ export async function addAuthCommand(options: AddAuthOptions) {
148
377
  console.log(` 5. Run scaffold to regenerate functions with auth guards`);
149
378
  }
150
379
 
380
+ function findObjectEnd(content: string, open: number): number {
381
+ let depth = 0, quote = "";
382
+ for (let i = open; i < content.length; i++) {
383
+ const ch = content[i];
384
+ if (quote) { if (ch === quote && content[i - 1] !== "\\") quote = ""; continue; }
385
+ if (ch === "'" || ch === '"' || ch === "`") { quote = ch; continue; }
386
+ if (ch === "{") depth++;
387
+ if (ch === "}" && --depth === 0) return i;
388
+ }
389
+ return -1;
390
+ }
391
+
392
+ function updateConfigWithNamedScheme(cwd: string, scheme: string, provider: AuthProvider): void {
393
+ const jsPath = path.join(cwd, "swallowkit.config.js");
394
+ const jsonPath = path.join(cwd, "swallowkit.config.json");
395
+ const configPath = fs.existsSync(jsPath) ? jsPath : jsonPath;
396
+ if (!fs.existsSync(configPath)) throw new Error("swallowkit.config.js or swallowkit.config.json is required");
397
+ if (configPath.endsWith(".json")) {
398
+ const value = JSON.parse(fs.readFileSync(configPath, "utf-8"));
399
+ value.auth ??= {};
400
+ value.auth.schemes ??= {};
401
+ if (value.auth.schemes[scheme]) throw new Error(`auth.schemes.${scheme} already exists; no files were changed`);
402
+ value.auth.schemes[scheme] = provider === "custom-jwt" ? { provider, customJwt: defaultCustomJwtConfig() } : { provider };
403
+ value.auth.authorization ??= { defaultPolicy: "anonymous", policies: {} };
404
+ fs.writeFileSync(configPath, JSON.stringify(value, null, 2) + "\n", "utf-8");
405
+ return;
406
+ }
407
+ let content = fs.readFileSync(configPath, "utf-8");
408
+ const authMatch = /\bauth\s*:\s*\{/.exec(content);
409
+ const customJwt = defaultCustomJwtConfig();
410
+ const entry = provider === "custom-jwt"
411
+ ? `\n ${scheme}: { provider: 'custom-jwt', customJwt: ${JSON.stringify(customJwt)} },`
412
+ : `\n ${scheme}: { provider: '${provider}' },`;
413
+ if (!authMatch) {
414
+ const rootEnd = content.lastIndexOf("}");
415
+ const beforeRootEnd = content.slice(0, rootEnd).trimEnd();
416
+ const separator = beforeRootEnd.endsWith(",") || beforeRootEnd.endsWith("{") ? "" : ",";
417
+ content = `${beforeRootEnd}${separator}\n auth: {\n schemes: {${entry}\n },\n authorization: { defaultPolicy: 'anonymous', policies: {} },\n },\n${content.slice(rootEnd)}`;
418
+ } else {
419
+ const authOpen = content.indexOf("{", authMatch.index);
420
+ const authEnd = findObjectEnd(content, authOpen);
421
+ const authBody = content.slice(authOpen + 1, authEnd);
422
+ const schemesMatch = /\bschemes\s*:\s*\{/.exec(authBody);
423
+ if (schemesMatch) {
424
+ const schemesOpen = authOpen + 1 + authBody.indexOf("{", schemesMatch.index);
425
+ const schemesEnd = findObjectEnd(content, schemesOpen);
426
+ const existing = content.slice(schemesOpen + 1, schemesEnd);
427
+ if (new RegExp(`(^|[,\\s])${scheme}\\s*:`).test(existing)) throw new Error(`auth.schemes.${scheme} already exists; no files were changed`);
428
+ content = content.slice(0, schemesEnd) + entry + "\n " + content.slice(schemesEnd);
429
+ } else {
430
+ content = content.slice(0, authOpen + 1) + `\n schemes: {${entry}\n },` + content.slice(authOpen + 1);
431
+ }
432
+ }
433
+ fs.writeFileSync(configPath, content, "utf-8");
434
+ }
435
+
436
+ function addNamedScheme(cwd: string, scheme: string, provider: AuthProvider, language: BackendLanguage): void {
437
+ // Validate/update config before creating files, so duplicate names stop safely.
438
+ updateConfigWithNamedScheme(cwd, scheme, provider);
439
+ const slug = scheme.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
440
+ if (language === "typescript") {
441
+ const dir = path.join(cwd, "functions", "src", "auth", "schemes", slug);
442
+ if (provider === "external-token") writeIfMissing(path.join(dir, "verifier.ts"), generateExternalTokenVerifierTS());
443
+ else if (provider === "swa") writeIfMissing(path.join(dir, "adapter.ts"), generateSwaAuthHelperTS());
444
+ else writeIfMissing(path.join(dir, "jwt-helper.ts"), generateJwtHelperTS());
445
+ } else if (language === "csharp") {
446
+ const dir = path.join(cwd, "functions", "Auth", "Schemes", slug);
447
+ if (provider === "external-token") writeIfMissing(path.join(dir, "ExternalTokenVerifier.cs"), generateNamedExternalTokenVerifierCSharp(scheme));
448
+ else if (provider === "swa") writeIfMissing(path.join(dir, "SwaAdapter.cs"), generateSwaAuthHelperCSharp());
449
+ else writeIfMissing(path.join(dir, "JwtHelper.cs"), generateJwtHelperCSharp());
450
+ } else {
451
+ const dir = path.join(cwd, "functions", "auth", "schemes", slug.replace(/-/g, "_"));
452
+ if (provider === "external-token") writeIfMissing(path.join(dir, "verifier.py"), generateNamedExternalTokenVerifierPython(scheme));
453
+ else if (provider === "swa") writeIfMissing(path.join(dir, "adapter.py"), generateSwaAuthHelperPython());
454
+ else writeIfMissing(path.join(dir, "jwt_helper.py"), generateJwtHelperPython());
455
+ }
456
+ const clientDir = path.join(cwd, "lib", "auth", "schemes", slug);
457
+ if (provider === "external-token") {
458
+ writeIfMissing(path.join(clientDir, "token-adapter.ts"), generateExternalTokenAdapter());
459
+ writeIfMissing(path.join(clientDir, "authenticated-fetch.ts"), generateAuthenticatedFetch().replace("'./external-token-adapter'", "'./token-adapter'"));
460
+ writeIfMissing(path.join(clientDir, "auth-context.tsx"), generateExternalTokenAuthContext().replace(/external-token-adapter/g, "token-adapter"));
461
+ } else if (provider === "swa") {
462
+ writeIfMissing(path.join(clientDir, "auth-context.tsx"), generateSwaAuthContext());
463
+ } else if (provider === "custom-jwt") {
464
+ writeIfMissing(path.join(clientDir, "auth-context.tsx"), generateAuthContext());
465
+ }
466
+ const callFunctionPath = path.join(cwd, "lib", "api", "call-function.ts");
467
+ if (!fs.existsSync(callFunctionPath)) writeIfMissing(callFunctionPath, generateBFFCallFunctionWithMultipleAuth());
468
+ else {
469
+ const current = fs.readFileSync(callFunctionPath, "utf-8");
470
+ if (!current.includes("x-ms-client-principal") || !current.includes("fetchHeaders['Authorization']")) {
471
+ if (current.includes("SwallowKit BFF Call Function Helper")) fs.writeFileSync(callFunctionPath, generateBFFCallFunctionWithMultipleAuth(), "utf-8");
472
+ else console.warn(" Existing customized lib/api/call-function.ts was preserved. Ensure it forwards Bearer and SWA credentials without logging them.");
473
+ }
474
+ }
475
+ }
476
+
151
477
  /**
152
478
  * Ensure the shared package has proper build infrastructure
153
479
  * (tsconfig.json, build script, typescript devDependency).
@@ -359,10 +685,7 @@ function updateConfigWithAuth(cwd: string, provider: AuthProvider, config: Custo
359
685
  const beforeClosing = content.substring(0, closingBraceIdx).trimEnd();
360
686
  const needsComma = !beforeClosing.endsWith(",") && !beforeClosing.endsWith("{");
361
687
 
362
- const authBlock = `${needsComma ? "," : ""}
363
- // 認証認可設定
364
- auth: {
365
- provider: '${provider}',
688
+ const providerSettings = provider === "custom-jwt" ? `
366
689
  customJwt: {
367
690
  userConnector: '${config.userConnector}',
368
691
  userTable: '${config.userTable}',
@@ -371,7 +694,17 @@ function updateConfigWithAuth(cwd: string, provider: AuthProvider, config: Custo
371
694
  rolesColumn: '${config.rolesColumn}',
372
695
  jwtSecretEnv: '${config.jwtSecretEnv || "JWT_SECRET"}',
373
696
  tokenExpiry: '${config.tokenExpiry || "24h"}',
374
- },
697
+ },` : provider === "swa" ? `
698
+ swa: {
699
+ allowedProviders: ['aad'],
700
+ roleSource: 'swa-roles',
701
+ },` : "";
702
+
703
+ const authBlock = `${needsComma ? "," : ""}
704
+ // 認証認可設定
705
+ auth: {
706
+ provider: '${provider}',
707
+ ${providerSettings}
375
708
  authorization: {
376
709
  defaultPolicy: 'authenticated',
377
710
  },
@@ -7,7 +7,7 @@ import * as fs from 'fs';
7
7
  import * as os from 'os';
8
8
  import * as net from 'net';
9
9
  import { CosmosClient, PartitionKeyKind } from '@azure/cosmos';
10
- import { ensureSwallowKitProject, getBackendLanguage, getAuthConfig } from '../../core/config';
10
+ import { ensureSwallowKitProject, getBackendLanguage, getAuthConfig, normalizeAuthConfig } from '../../core/config';
11
11
  import { ModelInfo } from '../../core/scaffold/model-parser';
12
12
  import {
13
13
  applyDevSeedEnvironment,
@@ -38,10 +38,13 @@ export interface DevOptions {
38
38
  noFunctions?: boolean;
39
39
  seedEnv?: string;
40
40
  mockConnectors?: boolean;
41
+ swaPort?: string;
42
+ noSwa?: boolean;
41
43
  }
42
44
 
43
45
  type ParsedDevActionOptions = DevOptions & {
44
46
  functions?: boolean;
47
+ swa?: boolean;
45
48
  };
46
49
 
47
50
  interface FunctionsCoreToolsCommand {
@@ -57,6 +60,7 @@ function normalizeParsedDevOptions(options: ParsedDevActionOptions): DevOptions
57
60
  return {
58
61
  ...options,
59
62
  noFunctions: options.noFunctions ?? options.functions === false,
63
+ noSwa: options.noSwa ?? options.swa === false,
60
64
  };
61
65
  }
62
66
 
@@ -121,6 +125,23 @@ export function buildFunctionsBaseUrl(host: string | undefined, functionsPort: s
121
125
  return `http://${host || 'localhost'}:${functionsPort}`;
122
126
  }
123
127
 
128
+ export function buildSwaStartArgs(host: string | undefined, nextPort: string, swaPort: string): string[] {
129
+ return [
130
+ 'start',
131
+ `http://${host || 'localhost'}:${nextPort}`,
132
+ '--swa-config-location',
133
+ '.',
134
+ '--port',
135
+ swaPort,
136
+ ];
137
+ }
138
+
139
+ export function getSwaCliInstallCommand(pm: string): string {
140
+ return pm === 'pnpm'
141
+ ? 'pnpm add -Dw @azure/static-web-apps-cli'
142
+ : 'npm install -D @azure/static-web-apps-cli';
143
+ }
144
+
124
145
  export function getFunctionsReadinessTimeoutMs(backendLanguage: BackendLanguage): number {
125
146
  return backendLanguage === 'csharp' ? 90_000 : 30_000;
126
147
  }
@@ -506,6 +527,8 @@ export function buildDevCommand(
506
527
  .option('--no-functions', 'Skip Azure Functions startup')
507
528
  .option('--seed-env <environment>', 'Replace Cosmos DB Emulator data from dev-seeds/<environment> before startup')
508
529
  .option('--mock-connectors', 'Start mock server for connector models (serves Zod-generated data)', false)
530
+ .option('--swa-port <port>', 'SWA authentication emulator port', '4280')
531
+ .option('--no-swa', 'Skip the SWA authentication emulator')
509
532
  .action(async (options: ParsedDevActionOptions) => {
510
533
  const normalizedOptions = normalizeParsedDevOptions(options);
511
534
 
@@ -618,11 +641,25 @@ async function initializeCosmosDB(databaseName: string): Promise<CosmosInitializ
618
641
  async function startDevEnvironment(options: DevOptions) {
619
642
  const port = options.port || '3000';
620
643
  const functionsPort = options.functionsPort || '7071';
644
+ const swaPort = options.swaPort || '4280';
621
645
 
622
646
  // Detect package manager from project lockfile
623
647
  const pm = detectFromProject();
624
648
  const pmCmd = getCommands(pm);
625
649
  const backendLanguage = getBackendLanguage();
650
+ const authConfig = getAuthConfig();
651
+ const normalizedAuth = normalizeAuthConfig(authConfig);
652
+ const useSwaEmulator = Boolean(normalizedAuth && Object.values(normalizedAuth.schemes).some(scheme => scheme.provider === 'swa')) && !options.noSwa;
653
+ const swaCommand = useSwaEmulator ? await resolveSwaCliCommand(process.cwd()) : null;
654
+
655
+ if (useSwaEmulator && !swaCommand) {
656
+ const installCommand = getSwaCliInstallCommand(pm);
657
+ console.error('❌ SWA authentication requires Azure Static Web Apps CLI for local development.');
658
+ console.error(' Install it in this project and retry:');
659
+ console.error(`\n ${installCommand}\n`);
660
+ process.exitCode = 1;
661
+ return;
662
+ }
626
663
 
627
664
  // プロセスを管理する配列
628
665
  const processes: ChildProcess[] = [];
@@ -925,7 +962,6 @@ async function startDevEnvironment(options: DevOptions) {
925
962
  const connectorModels = allModels.filter((m) => m.connectorConfig);
926
963
 
927
964
  // Resolve auth config — auth functions use RDB connectors, mocked alongside other models
928
- const authConfig = getAuthConfig();
929
965
  let mockAuthConfig: { jwtSecret: string; tokenExpiry?: string; customJwt?: { userTable: string; loginIdColumn: string; passwordHashColumn: string; rolesColumn: string }; defaultPolicy?: "authenticated" | "anonymous" } | undefined;
930
966
  if (authConfig?.provider === 'custom-jwt' && authConfig.customJwt) {
931
967
  // Read JWT_SECRET from functions/local.settings.json if available
@@ -946,7 +982,9 @@ async function startDevEnvironment(options: DevOptions) {
946
982
  passwordHashColumn: authConfig.customJwt.passwordHashColumn,
947
983
  rolesColumn: authConfig.customJwt.rolesColumn,
948
984
  },
949
- defaultPolicy: authConfig.authorization?.defaultPolicy,
985
+ defaultPolicy: authConfig.authorization?.defaultPolicy === "public" ? "anonymous" :
986
+ authConfig.authorization?.defaultPolicy === "authenticated" || authConfig.authorization?.defaultPolicy === "anonymous"
987
+ ? authConfig.authorization.defaultPolicy : undefined,
950
988
  };
951
989
  }
952
990
 
@@ -985,18 +1023,6 @@ async function startDevEnvironment(options: DevOptions) {
985
1023
  // 5. Start Next.js development server
986
1024
  const nextArgs = buildNextDevArgs(pm, port);
987
1025
 
988
- if (options.open) {
989
- // Next.js 14+ deprecated --open option, so we open browser manually
990
- setTimeout(() => {
991
- const url = `http://${options.host || 'localhost'}:${port}`;
992
- console.log(`\n🌐 Opening browser: ${url}`);
993
-
994
- const start = process.platform === 'darwin' ? 'open' :
995
- process.platform === 'win32' ? 'start' : 'xdg-open';
996
- spawn(start, [url], { shell: true });
997
- }, 3000);
998
- }
999
-
1000
1026
  // Ensure .env.local points to bffTargetPort so Next.js reads the correct backend URL.
1001
1027
  // When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
1002
1028
  // Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
@@ -1063,10 +1089,52 @@ async function startDevEnvironment(options: DevOptions) {
1063
1089
  }
1064
1090
  }
1065
1091
 
1092
+ let swaReady = false;
1093
+ if (useSwaEmulator && swaCommand) {
1094
+ const nextUrl = `http://${options.host || 'localhost'}:${port}`;
1095
+ console.log(`⏳ Waiting for Next.js before starting the SWA emulator at ${nextUrl}...`);
1096
+ const nextReady = await waitForHttpServerReady(nextUrl);
1097
+ if (!nextReady) {
1098
+ throw new Error(`Next.js did not become ready at ${nextUrl}`);
1099
+ }
1100
+
1101
+ console.log(`🔐 Starting SWA authentication emulator (port: ${swaPort})...`);
1102
+ const swaProcess = spawn(swaCommand, buildSwaStartArgs(options.host, port, swaPort), {
1103
+ cwd: process.cwd(),
1104
+ shell: true,
1105
+ stdio: 'inherit',
1106
+ env: process.env,
1107
+ });
1108
+ processes.push(swaProcess);
1109
+ swaProcess.on('error', (error) => {
1110
+ console.error('❌ SWA CLI startup error:', error.message);
1111
+ });
1112
+
1113
+ const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
1114
+ swaReady = await waitForHttpServerReady(swaUrl);
1115
+ if (!swaReady) {
1116
+ throw new Error(`SWA authentication emulator did not become ready at ${swaUrl}`);
1117
+ }
1118
+ }
1119
+
1066
1120
  console.log('');
1067
1121
  console.log('✅ SwallowKit development environment is running!');
1068
1122
  console.log('');
1069
- console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
1123
+ if (useSwaEmulator) {
1124
+ const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
1125
+ console.log('🔐 SWA authenticated app:');
1126
+ console.log(` ${swaUrl}`);
1127
+ console.log('');
1128
+ console.log('🔑 Local sign-in:');
1129
+ console.log(` ${swaUrl}/.auth/login/aad`);
1130
+ console.log(' Enter any username and select Login.');
1131
+ console.log(' The "authenticated" role is added automatically.');
1132
+ console.log('');
1133
+ console.log('📱 Next.js direct access (authentication is not emulated):');
1134
+ console.log(` http://${options.host || 'localhost'}:${port}`);
1135
+ } else {
1136
+ console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
1137
+ }
1070
1138
  if (hasFunctions && !options.noFunctions) {
1071
1139
  console.log(`${functionsReady ? '⚡ Azure Functions' : '⏳ Azure Functions (starting)'}: ${functionsBaseUrl}`);
1072
1140
  }
@@ -1082,7 +1150,22 @@ async function startDevEnvironment(options: DevOptions) {
1082
1150
  if (mockServer) {
1083
1151
  console.log('💡 Connector models served from mock server (Zod-generated data)');
1084
1152
  }
1153
+ if (swaReady) {
1154
+ console.log('💡 Open the app through the SWA port when testing authenticated pages and CRUD APIs.');
1155
+ }
1085
1156
  console.log('');
1157
+
1158
+ if (options.open) {
1159
+ const url = useSwaEmulator
1160
+ ? `http://${options.host || 'localhost'}:${swaPort}`
1161
+ : `http://${options.host || 'localhost'}:${port}`;
1162
+ console.log(`🌐 Opening browser: ${url}`);
1163
+ const start = process.platform === 'darwin' ? 'open' :
1164
+ process.platform === 'win32' ? 'start' : 'xdg-open';
1165
+ spawn(start, [url], { shell: true });
1166
+ console.log('');
1167
+ }
1168
+
1086
1169
  console.log('🛑 Press Ctrl+C to stop');
1087
1170
  console.log('');
1088
1171
 
@@ -1097,6 +1180,23 @@ async function startDevEnvironment(options: DevOptions) {
1097
1180
  }
1098
1181
  }
1099
1182
 
1183
+ async function resolveSwaCliCommand(projectRoot: string): Promise<string | null> {
1184
+ const binaryName = process.platform === 'win32' ? 'swa.cmd' : 'swa';
1185
+ const localCommand = path.join(projectRoot, 'node_modules', '.bin', binaryName);
1186
+ if (fs.existsSync(localCommand) && await checkCommand(localCommand, ['--version'], {
1187
+ cwd: projectRoot,
1188
+ shell: process.platform === 'win32',
1189
+ })) {
1190
+ return localCommand;
1191
+ }
1192
+
1193
+ if (await checkCommand('swa', ['--version'], { cwd: projectRoot })) {
1194
+ return 'swa';
1195
+ }
1196
+
1197
+ return null;
1198
+ }
1199
+
1100
1200
  async function runCommand(
1101
1201
  command: string,
1102
1202
  args: string[],