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
@@ -730,6 +730,7 @@ export function buildGeneratedProjectDependencies(projectName: string): Record<s
730
730
  return {
731
731
  '@azure/cosmos': '^4.0.0',
732
732
  'applicationinsights': '^3.3.0',
733
+ 'zod': '^4.0.0',
733
734
  [`@${projectName}/shared`]: 'workspace:*',
734
735
  };
735
736
  }
@@ -892,6 +893,13 @@ function getFunctionsBaseUrl(): string {
892
893
  return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
893
894
  }
894
895
 
896
+ function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
897
+ const key = process.env.BACKEND_FUNCTIONS_KEY;
898
+ if (key) return { 'x-functions-key': key };
899
+ if (functionsBaseUrl.startsWith('http://localhost:') || functionsBaseUrl.startsWith('http://127.0.0.1:')) return {};
900
+ throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
901
+ }
902
+
895
903
  /**
896
904
  * Simple HTTP client for calling backend APIs
897
905
  * Use this to make requests to BFF API routes (which forward to Azure Functions)
@@ -903,6 +911,7 @@ async function request<T>(
903
911
  queryParams?: Record<string, string>
904
912
  ): Promise<T> {
905
913
  const functionsBaseUrl = getFunctionsBaseUrl();
914
+ const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
906
915
  let url = \`\${functionsBaseUrl}\${endpoint}\`;
907
916
  if (queryParams) {
908
917
  const params = new URLSearchParams(queryParams);
@@ -914,6 +923,7 @@ async function request<T>(
914
923
  method,
915
924
  headers: {
916
925
  'Content-Type': 'application/json',
926
+ ...keyHeaders,
917
927
  },
918
928
  body: body ? JSON.stringify(body) : undefined,
919
929
  });
@@ -997,6 +1007,7 @@ export const api = {
997
1007
  // 6. Create .env.example
998
1008
  const envExample = `# Azure Functions Backend URL
999
1009
  BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
1010
+ BACKEND_FUNCTIONS_KEY=
1000
1011
 
1001
1012
  # Azure Configuration
1002
1013
  AZURE_RESOURCE_GROUP=your-resource-group
@@ -1082,6 +1093,7 @@ export async function register() {
1082
1093
  const envLocalContent = [
1083
1094
  '# Azure Functions Backend URL (Local)',
1084
1095
  'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
1096
+ 'BACKEND_FUNCTIONS_KEY=',
1085
1097
  ''
1086
1098
  ].join('\n');
1087
1099
  fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
@@ -1378,7 +1390,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
1378
1390
 
1379
1391
  app.http('greet', {
1380
1392
  methods: ['GET', 'POST'],
1381
- authLevel: 'anonymous',
1393
+ authLevel: 'function',
1382
1394
  handler: greet
1383
1395
  });
1384
1396
  `);
@@ -1410,7 +1422,7 @@ public sealed class GreetFunction
1410
1422
  {
1411
1423
  [Function("greet")]
1412
1424
  public async Task<HttpResponseData> Run(
1413
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "greet")] HttpRequestData request)
1425
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
1414
1426
  {
1415
1427
  var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
1416
1428
  var name = "SwallowKit";
@@ -1478,7 +1490,7 @@ def greet(req: func.HttpRequest) -> func.HttpResponse:
1478
1490
 
1479
1491
  from blueprints.greet import bp as greet_bp
1480
1492
 
1481
- app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
1493
+ app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
1482
1494
 
1483
1495
  app.register_blueprint(greet_bp)
1484
1496
  # SwallowKit scaffold registrations
@@ -2109,7 +2121,7 @@ const containerName = 'Models'; // PascalCase + 's'
2109
2121
  app.http('{model}-get-all', {
2110
2122
  methods: ['GET'],
2111
2123
  route: '{model}',
2112
- authLevel: 'anonymous',
2124
+ authLevel: 'function',
2113
2125
  extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
2114
2126
  handler: async (request, context) => {
2115
2127
  const documents = context.extraInputs.get('cosmosInput');
@@ -2400,7 +2412,7 @@ import { ModelSchema } from '@${projectName}/shared';
2400
2412
  app.http('{model}-get-all', {
2401
2413
  methods: ['GET'],
2402
2414
  route: '{model}',
2403
- authLevel: 'anonymous',
2415
+ authLevel: 'function',
2404
2416
  extraInputs: [cosmosInput],
2405
2417
  handler: async (request, context) => {
2406
2418
  const documents = context.extraInputs.get(cosmosInput);
@@ -2431,6 +2443,43 @@ app.http('{model}-get-all', {
2431
2443
  console.log('');
2432
2444
  }
2433
2445
 
2446
+ export function buildFunctionsHostKeyBicepExpression(): string {
2447
+ return "listKeys('${functionsFlex.outputs.id}/host/default', '2023-12-01').functionKeys.default";
2448
+ }
2449
+
2450
+ export function buildStaticWebAppConfigBicepSource(): string {
2451
+ return `@description('Static Web App name')
2452
+ param staticWebAppName string
2453
+
2454
+ @description('Functions App default hostname for backend API calls')
2455
+ param functionsDefaultHostname string
2456
+
2457
+ @secure()
2458
+ @description('Functions host key used only by the server-side BFF')
2459
+ param functionsHostKey string
2460
+
2461
+ @description('Application Insights connection string for SWA')
2462
+ param appInsightsConnectionString string
2463
+
2464
+ resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
2465
+ name: staticWebAppName
2466
+ }
2467
+
2468
+ resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
2469
+ parent: staticWebApp
2470
+ name: 'appsettings'
2471
+ properties: {
2472
+ APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
2473
+ ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
2474
+ BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
2475
+ BACKEND_FUNCTIONS_KEY: functionsHostKey
2476
+ }
2477
+ }
2478
+
2479
+ output configName string = staticWebAppConfig.name
2480
+ `;
2481
+ }
2482
+
2434
2483
  async function createInfrastructure(
2435
2484
  projectDir: string,
2436
2485
  projectName: string,
@@ -2605,6 +2654,7 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
2605
2654
  params: {
2606
2655
  staticWebAppName: staticWebApp.outputs.name
2607
2656
  functionsDefaultHostname: functionsFlex.outputs.defaultHostname
2657
+ functionsHostKey: ${buildFunctionsHostKeyBicepExpression()}
2608
2658
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
2609
2659
  }
2610
2660
  dependsOn: [
@@ -2698,31 +2748,7 @@ output defaultHostname string = staticWebApp.properties.defaultHostname
2698
2748
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
2699
2749
 
2700
2750
  // modules/staticwebapp-config.bicep (for updating config after Functions deployment)
2701
- const staticWebAppConfigBicep = `@description('Static Web App name')
2702
- param staticWebAppName string
2703
-
2704
- @description('Functions App default hostname for backend API calls')
2705
- param functionsDefaultHostname string
2706
-
2707
- @description('Application Insights connection string for SWA')
2708
- param appInsightsConnectionString string
2709
-
2710
- resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
2711
- name: staticWebAppName
2712
- }
2713
-
2714
- resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
2715
- parent: staticWebApp
2716
- name: 'appsettings'
2717
- properties: {
2718
- APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
2719
- ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
2720
- BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
2721
- }
2722
- }
2723
-
2724
- output configName string = staticWebAppConfig.name
2725
- `;
2751
+ const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
2726
2752
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
2727
2753
 
2728
2754
  // modules/loganalytics.bicep (Shared Log Analytics Workspace)
@@ -6,7 +6,7 @@
6
6
  import * as fs from "fs";
7
7
  import * as path from "path";
8
8
  import { spawnSync } from "child_process";
9
- import { getBackendLanguage, getConnectorDefinition, getAuthConfig, ensureSwallowKitProject } from "../../core/config";
9
+ import { getBackendLanguage, getConnectorDefinition, getAuthConfig, getFullConfig, getNormalizedAuthConfig, ensureSwallowKitProject, normalizeAuthConfig, validateConfig } from "../../core/config";
10
10
  import { ModelInfo, parseModelFile, toKebabCase, toPascalCase, toCamelCase } from "../../core/scaffold/model-parser";
11
11
  import {
12
12
  generateCSharpAzureFunctionsCRUD,
@@ -42,6 +42,7 @@ import {
42
42
  AuthConfig,
43
43
  } from "../../types";
44
44
  import { syncProjectManifest } from "../../core/project/manifest";
45
+ import { generateBFFCallFunctionWithAuth, generateBFFCallFunctionWithMultipleAuth, generateBFFCallFunctionWithSwaAuth, generateNamedAuthRouterCSharp, generateNamedAuthRouterPython, generateNamedAuthRouterTS } from "../../core/scaffold/auth-generator";
45
46
 
46
47
  interface ScaffoldOptions {
47
48
  model: string; // モデルファイルのパス(例: "lib/models/todo.ts" or "todo")
@@ -74,6 +75,9 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
74
75
  // SwallowKit プロジェクトディレクトリかどうかを検証
75
76
  ensureSwallowKitProject("scaffold");
76
77
 
78
+ const configValidation = validateConfig(getFullConfig());
79
+ if (!configValidation.valid) throw new Error(`Invalid SwallowKit configuration:\n${configValidation.errors.map(error => `- ${error}`).join("\n")}`);
80
+
77
81
  console.log("🏗️ SwallowKit Scaffold: Generating CRUD operations...\n");
78
82
 
79
83
  try {
@@ -159,9 +163,12 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
159
163
  const uiAuthPolicy = resolveAuthPolicy(modelInfo, uiAuthConfig);
160
164
  const uiAuthOptions: UIAuthOptions | undefined =
161
165
  uiAuthPolicy && uiAuthConfig && uiAuthConfig.provider !== 'none'
162
- ? { authPolicy: uiAuthPolicy }
166
+ ? createUIAuthOptions(uiAuthPolicy, uiAuthConfig)
163
167
  : undefined;
164
168
  await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions);
169
+ if (uiAuthOptions?.authenticatedFetchImport) {
170
+ applyExternalTokenFetchToGeneratedUi(modelInfo, uiAuthOptions.authenticatedFetchImport);
171
+ }
165
172
  await updateNavigationMenu(modelInfo);
166
173
  }
167
174
 
@@ -190,15 +197,44 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
190
197
  }
191
198
  }
192
199
 
200
+ function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, importPath = '@/lib/auth/authenticated-fetch'): void {
201
+ const uiRoot = path.join(process.cwd(), 'app', toKebabCase(modelInfo.name));
202
+ if (!fs.existsSync(uiRoot)) return;
203
+ const visit = (dir: string): void => {
204
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
205
+ const target = path.join(dir, entry.name);
206
+ if (entry.isDirectory()) { visit(target); continue; }
207
+ if (!entry.name.endsWith('.tsx')) continue;
208
+ let content = fs.readFileSync(target, 'utf-8');
209
+ if (!content.includes('fetch(')) continue;
210
+ content = content.replace(/\bfetch\(/g, 'authenticatedFetch(');
211
+ content = content.replace(
212
+ /(if \(!res\.ok\) throw new Error\([^\n]+\);)/g,
213
+ `if (res.status === 401) throw new Error('Authentication required');\n $1`
214
+ );
215
+ if (!content.includes(importPath)) {
216
+ const directive = content.match(/^'use client';\s*\r?\n/)?.[0] ?? '';
217
+ content = `${directive}import { authenticatedFetch } from '${importPath}';\n${content.slice(directive.length)}`;
218
+ }
219
+ fs.writeFileSync(target, content, 'utf-8');
220
+ }
221
+ };
222
+ visit(uiRoot);
223
+ }
224
+
193
225
  /**
194
226
  * モデルの authPolicy と auth config から実効的な認可ポリシーを解決
195
227
  * - モデルに authPolicy があればそれを使用
196
228
  * - なければ auth.authorization.defaultPolicy に従う
197
229
  * - auth 設定がなければ undefined(ガードなし)
198
230
  */
199
- function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
231
+ export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
200
232
  // モデルに明示的な authPolicy がある場合はそれを使用
201
233
  if (modelInfo.authPolicy) {
234
+ const normalized = normalizeAuthConfig(authConfig);
235
+ for (const policyName of [modelInfo.authPolicy.policy, typeof modelInfo.authPolicy.read === 'string' ? modelInfo.authPolicy.read : undefined, typeof modelInfo.authPolicy.write === 'string' ? modelInfo.authPolicy.write : undefined].filter((value): value is string => Boolean(value))) {
236
+ if (!normalized?.authorization.policies[policyName]) throw new Error(`Model '${modelInfo.name}' references undefined auth policy '${policyName}'. Define auth.authorization.policies.${policyName}.`);
237
+ }
202
238
  return modelInfo.authPolicy;
203
239
  }
204
240
 
@@ -208,11 +244,13 @@ function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefi
208
244
  }
209
245
 
210
246
  // defaultPolicy が 'authenticated' なら認証のみ(ロール指定なし)のポリシーを返す
211
- const defaultPolicy = authConfig.authorization?.defaultPolicy ?? 'authenticated';
247
+ const defaultPolicy = authConfig.authorization?.defaultPolicy === 'public' ? 'anonymous' : authConfig.authorization?.defaultPolicy ?? 'authenticated';
212
248
  if (defaultPolicy === 'authenticated') {
213
249
  return {}; // 空のポリシー = 認証のみ、ロール制限なし
214
250
  }
215
251
 
252
+ if (defaultPolicy !== 'anonymous') return { policy: defaultPolicy };
253
+
216
254
  // defaultPolicy が 'anonymous' なら認証不要
217
255
  return undefined;
218
256
  }
@@ -302,11 +340,14 @@ async function generateCallFunctionHelper(): Promise<void> {
302
340
 
303
341
  // auth 設定がある場合は Authorization ヘッダー転送版を生成
304
342
  const authConfig = getAuthConfig();
305
- const hasAuth = authConfig && authConfig.provider !== 'none';
343
+ const normalizedAuth = normalizeAuthConfig(authConfig);
344
+ const hasAuth = Boolean(normalizedAuth && Object.keys(normalizedAuth.schemes).length > 0);
306
345
  let helperCode: string;
307
346
  if (hasAuth) {
308
- const { generateBFFCallFunctionWithAuth } = await import("../../core/scaffold/auth-generator");
309
- helperCode = generateBFFCallFunctionWithAuth();
347
+ const providers = new Set(Object.values(normalizedAuth!.schemes).map(scheme => scheme.provider));
348
+ helperCode = normalizedAuth!.schemes.default && Object.keys(normalizedAuth!.schemes).length === 1
349
+ ? authConfig?.provider === "swa" ? generateBFFCallFunctionWithSwaAuth() : generateBFFCallFunctionWithAuth()
350
+ : providers.has("swa") ? generateBFFCallFunctionWithMultipleAuth() : generateBFFCallFunctionWithAuth();
310
351
  } else {
311
352
  helperCode = generateBFFCallFunction();
312
353
  }
@@ -335,6 +376,12 @@ async function generateFunctionsCode(
335
376
 
336
377
  const modelKebab = toKebabCase(modelInfo.name);
337
378
  if (backendLanguage === "typescript") {
379
+ const normalizedAuth = getNormalizedAuthConfig();
380
+ if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
381
+ const routerPath = path.join(process.cwd(), functionsDir, "src", "auth", "auth-router.ts");
382
+ fs.mkdirSync(path.dirname(routerPath), { recursive: true });
383
+ fs.writeFileSync(routerPath, generateNamedAuthRouterTS(normalizedAuth), "utf-8");
384
+ }
338
385
  const functionFilePath = path.join(
339
386
  process.cwd(),
340
387
  functionsDir,
@@ -347,13 +394,19 @@ async function generateFunctionsCode(
347
394
  fs.mkdirSync(functionDir, { recursive: true });
348
395
  }
349
396
 
350
- const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy);
397
+ const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy, authConfig?.provider);
351
398
  fs.writeFileSync(functionFilePath, code, "utf-8");
352
399
  console.log(`✅ Created: ${functionFilePath}`);
353
400
  return;
354
401
  }
355
402
 
356
403
  if (backendLanguage === "csharp") {
404
+ const normalizedAuth = getNormalizedAuthConfig();
405
+ if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
406
+ const routerPath = path.join(process.cwd(), functionsDir, "Auth", "AuthRouter.cs");
407
+ fs.mkdirSync(path.dirname(routerPath), { recursive: true });
408
+ fs.writeFileSync(routerPath, generateNamedAuthRouterCSharp(normalizedAuth), "utf-8");
409
+ }
357
410
  const crudDir = path.join(process.cwd(), functionsDir, "Crud");
358
411
  const functionFilePath = path.join(crudDir, `${modelInfo.name}CrudFunctions.cs`);
359
412
  fs.mkdirSync(crudDir, { recursive: true });
@@ -370,6 +423,13 @@ async function generateFunctionsCode(
370
423
  return;
371
424
  }
372
425
 
426
+ const normalizedAuth = getNormalizedAuthConfig();
427
+ if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
428
+ const routerPath = path.join(process.cwd(), functionsDir, "auth", "auth_router.py");
429
+ fs.mkdirSync(path.dirname(routerPath), { recursive: true });
430
+ fs.writeFileSync(routerPath, generateNamedAuthRouterPython(normalizedAuth), "utf-8");
431
+ }
432
+
373
433
  const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
374
434
  const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
375
435
  fs.mkdirSync(blueprintsDir, { recursive: true });
@@ -380,6 +440,20 @@ async function generateFunctionsCode(
380
440
  console.log(`✅ Created: ${blueprintPath}`);
381
441
  }
382
442
 
443
+ function createUIAuthOptions(policy: ModelAuthPolicy, authConfig: AuthConfig): UIAuthOptions {
444
+ const normalized = normalizeAuthConfig(authConfig);
445
+ const readPolicyName = typeof policy.read === 'string' ? policy.read : policy.policy;
446
+ const writePolicyName = typeof policy.write === 'string' ? policy.write : policy.policy;
447
+ const readNamed = readPolicyName ? normalized?.authorization.policies[readPolicyName] : undefined;
448
+ const named = writePolicyName ? normalized?.authorization.policies[writePolicyName] : undefined;
449
+ const writeRoles = named?.roles ?? (Array.isArray(policy.write) ? policy.write : policy.roles) ?? [];
450
+ const externalScheme = [...(readNamed?.schemes ?? []), ...(named?.schemes ?? [])].find(name => normalized?.schemes[name]?.provider === 'external-token');
451
+ const authScheme = readNamed?.schemes[0] ?? named?.schemes[0];
452
+ const authenticatedFetchImport = externalScheme ? `@/lib/auth/schemes/${toKebabCase(externalScheme)}/authenticated-fetch` : authConfig.provider === 'external-token' ? '@/lib/auth/authenticated-fetch' : undefined;
453
+ const authContextImport = authScheme ? `@/lib/auth/schemes/${toKebabCase(authScheme)}/auth-context` : '@/lib/auth/auth-context';
454
+ return { authPolicy: policy, writeRoles, authenticatedFetchImport, authContextImport };
455
+ }
456
+
383
457
  /**
384
458
  * コネクタモデル用 Azure Functions コード生成
385
459
  */
@@ -425,14 +499,16 @@ async function generateConnectorFunctionsCode(
425
499
  modelInfo, sharedPackageName,
426
500
  connectorDef as RdbConnectorConfig,
427
501
  connectorConfig as RdbModelConnectorConfig,
428
- authPolicy
502
+ authPolicy,
503
+ authConfig?.provider
429
504
  );
430
505
  } else {
431
506
  code = generateApiConnectorFunctionTS(
432
507
  modelInfo, sharedPackageName,
433
508
  connectorDef as ApiConnectorConfig,
434
509
  connectorConfig as ApiModelConnectorConfig,
435
- authPolicy
510
+ authPolicy,
511
+ authConfig?.provider
436
512
  );
437
513
  }
438
514
 
package/src/cli/index.ts CHANGED
@@ -26,6 +26,8 @@ const DEV_OPTION_ARITY = new Map<string, 0 | 1>([
26
26
  ["--verbose", 0],
27
27
  ["--no-functions", 0],
28
28
  ["--mock-connectors", 0],
29
+ ["--swa-port", 1],
30
+ ["--no-swa", 0],
29
31
  ]);
30
32
 
31
33
  function ensureUtf8ConsoleOnWindows(): void {
@@ -168,10 +170,12 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
168
170
  program
169
171
  .command("add-auth")
170
172
  .description("Add authentication and authorization to the project")
171
- .option("--provider <provider>", "Auth provider: custom-jwt | swa | swa-custom | none", "custom-jwt")
173
+ .option("--provider <provider>", "Auth provider: custom-jwt | swa | external-token | swa-custom | none", "custom-jwt")
174
+ .option("--scheme <name>", "Add a named authentication scheme")
172
175
  .action((options) => {
173
176
  addAuthCommand({
174
177
  provider: options.provider,
178
+ scheme: options.scheme,
175
179
  });
176
180
  });
177
181
 
@@ -1,7 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import { createRequire } from "module";
3
3
  import * as path from "path";
4
- import { AuthConfig, BackendLanguage, ConnectorDefinition, SwallowKitConfig } from "../types";
4
+ import { AuthConfig, AuthProvider, BackendLanguage, ConnectorDefinition, NormalizedAuthConfig, SwallowKitConfig } from "../types";
5
5
  import { detectFromProject, getCommands } from "../utils/package-manager";
6
6
 
7
7
  const requireFromHere = createRequire(__filename);
@@ -37,7 +37,7 @@ const DEFAULT_CONFIG: SwallowKitConfig = {
37
37
  /**
38
38
  * 設定ファイルを読み込み
39
39
  */
40
- export function loadConfig(configPath?: string): SwallowKitConfig {
40
+ export function loadConfig(configPath?: string, throwOnError = false): SwallowKitConfig {
41
41
  const defaultPaths = [
42
42
  "swallowkit.config.json",
43
43
  "swallowkit.config.js",
@@ -63,6 +63,7 @@ export function loadConfig(configPath?: string): SwallowKitConfig {
63
63
  return mergeConfig(DEFAULT_CONFIG, unwrapConfigModule(loadedConfig));
64
64
  }
65
65
  } catch (error) {
66
+ if (throwOnError) throw error;
66
67
  console.warn(`⚠️ 設定ファイルの読み込みに失敗: ${filePath}`, error);
67
68
  }
68
69
  }
@@ -200,6 +201,40 @@ export function getAuthConfig(configPath?: string): AuthConfig | undefined {
200
201
  return config.auth;
201
202
  }
202
203
 
204
+ /** Load configuration for mutating/generation commands and fail instead of silently falling back. */
205
+ export function getValidatedFullConfig(configPath?: string): SwallowKitConfig {
206
+ const config = mergeConfig(loadConfig(configPath, true), loadConfigFromEnv());
207
+ const validation = validateConfig(config);
208
+ if (!validation.valid) throw new Error(`Invalid SwallowKit configuration:\n${validation.errors.map(error => `- ${error}`).join("\n")}`);
209
+ return config;
210
+ }
211
+
212
+ const VALID_AUTH_PROVIDERS: AuthProvider[] = ["custom-jwt", "swa", "external-token", "swa-custom", "none"];
213
+ const AUTH_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
214
+
215
+ /** Convert the legacy single-provider shape to the canonical named-scheme shape. */
216
+ export function normalizeAuthConfig(auth?: AuthConfig): NormalizedAuthConfig | undefined {
217
+ if (!auth) return undefined;
218
+ const schemes = { ...(auth.schemes ?? {}) };
219
+ if (auth.provider && auth.provider !== "none" && !schemes.default) {
220
+ schemes.default = { provider: auth.provider, customJwt: auth.customJwt, swa: auth.swa };
221
+ }
222
+ const rawDefault = auth.authorization?.defaultPolicy ?? (Object.keys(schemes).length ? "authenticated" : "anonymous");
223
+ return {
224
+ ...auth,
225
+ schemes,
226
+ authorization: {
227
+ ...auth.authorization,
228
+ defaultPolicy: rawDefault === "public" ? "anonymous" : rawDefault,
229
+ policies: { ...(auth.authorization?.policies ?? {}) },
230
+ },
231
+ };
232
+ }
233
+
234
+ export function getNormalizedAuthConfig(configPath?: string): NormalizedAuthConfig | undefined {
235
+ return normalizeAuthConfig(getAuthConfig(configPath));
236
+ }
237
+
203
238
  /**
204
239
  * 設定の検証
205
240
  */
@@ -214,6 +249,38 @@ function hasConfiguredDatabase(config: SwallowKitConfig): boolean {
214
249
  export function validateConfig(config: SwallowKitConfig): { valid: boolean; errors: string[] } {
215
250
  const errors: string[] = [];
216
251
 
252
+ const auth = normalizeAuthConfig(config.auth);
253
+ if (auth) {
254
+ for (const [name, scheme] of Object.entries(auth.schemes)) {
255
+ const base = `auth.schemes.${name}`;
256
+ if (!AUTH_NAME.test(name)) errors.push(`${base}: scheme name must match ${AUTH_NAME}; rename the scheme`);
257
+ if (!VALID_AUTH_PROVIDERS.includes(scheme.provider)) errors.push(`${base}.provider: unsupported provider '${scheme.provider}'; use ${VALID_AUTH_PROVIDERS.join(", ")}`);
258
+ if (scheme.provider === "none" || scheme.provider === "swa-custom") errors.push(`${base}.provider: '${scheme.provider}' cannot be used as a named scheme; use swa, external-token, or custom-jwt`);
259
+ if (scheme.provider === "custom-jwt") {
260
+ const jwt = scheme.customJwt ?? (name === "default" ? auth.customJwt : undefined);
261
+ for (const key of ["userConnector", "userTable", "loginIdColumn", "passwordHashColumn", "rolesColumn"] as const) {
262
+ if (!jwt?.[key]) errors.push(`${base}.customJwt.${key}: required for custom-jwt; configure this value`);
263
+ }
264
+ }
265
+ }
266
+ for (const [name, policy] of Object.entries(auth.authorization.policies)) {
267
+ const base = `auth.authorization.policies.${name}`;
268
+ if (!AUTH_NAME.test(name)) errors.push(`${base}: policy name must match ${AUTH_NAME}; rename the policy`);
269
+ if (!Array.isArray(policy.schemes) || policy.schemes.length === 0) errors.push(`${base}.schemes: provide at least one named scheme`);
270
+ for (const scheme of policy.schemes ?? []) if (!auth.schemes[scheme]) errors.push(`${base}.schemes: undefined scheme '${scheme}'; define auth.schemes.${scheme}`);
271
+ const sources = new Map<string, string>();
272
+ for (const schemeName of policy.schemes ?? []) {
273
+ const provider = auth.schemes[schemeName]?.provider;
274
+ const source = provider === "swa" ? "x-ms-client-principal" : provider === "custom-jwt" || provider === "external-token" ? "authorization-bearer" : provider;
275
+ if (source && sources.has(source)) errors.push(`${base}.schemes: '${sources.get(source)}' and '${schemeName}' both use ${source}; split the policy so credential selection is deterministic`);
276
+ else if (source) sources.set(source, schemeName);
277
+ }
278
+ }
279
+ const defaultPolicy = auth.authorization.defaultPolicy;
280
+ if (defaultPolicy === "authenticated" && !auth.schemes.default && Object.keys(auth.schemes).length !== 1) errors.push("auth.authorization.defaultPolicy: 'authenticated' requires auth.schemes.default (or exactly one scheme); set a default scheme or use a named policy");
281
+ else if (!["anonymous", "authenticated"].includes(defaultPolicy) && !auth.authorization.policies[defaultPolicy]) errors.push(`auth.authorization.defaultPolicy: undefined policy '${defaultPolicy}'; define auth.authorization.policies.${defaultPolicy}`);
282
+ }
283
+
217
284
  // データベース設定の検証
218
285
  if (hasConfiguredDatabase(config) && !config.database?.connectionString) {
219
286
  errors.push("Cosmos DB connection string is required");
@@ -503,9 +503,9 @@ export class ConnectorMockServer {
503
503
  if (!policy) return undefined; // defaultPolicy: authenticated → 認証のみ、ロールチェック不要
504
504
 
505
505
  if (isWrite) {
506
- return policy.write || policy.roles;
506
+ return Array.isArray(policy.write) ? policy.write : policy.roles;
507
507
  }
508
- return policy.read || policy.roles;
508
+ return Array.isArray(policy.read) ? policy.read : policy.roles;
509
509
  }
510
510
 
511
511
  private readBody(req: http.IncomingMessage, callback: (body: MockDocument) => void) {
@@ -1,6 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { getBackendLanguage, getFullConfig, validateConfig } from "../config";
3
+ import { getBackendLanguage, getFullConfig, normalizeAuthConfig, validateConfig } from "../config";
4
4
  import { getAllModels, toCamelCase, toKebabCase } from "../scaffold/model-parser";
5
5
  import { AuthConfig, BackendLanguage, ConnectorDefinition, ModelAuthPolicy, ModelConnectorConfig } from "../../types";
6
6
  import { captureConsoleMessages, withWorkingDirectory } from "../operations/runtime";
@@ -340,7 +340,7 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
340
340
  connectors: Object.entries(config.connectors || {})
341
341
  .map(([name, definition]) => ({ name, definition }))
342
342
  .sort((left, right) => left.name.localeCompare(right.name)),
343
- auth: config.auth || null,
343
+ auth: normalizeAuthConfig(config.auth) || null,
344
344
  entities: entities.sort((left, right) => left.name.localeCompare(right.name)),
345
345
  routes,
346
346
  modules: buildModules(projectRoot),
@@ -356,7 +356,7 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
356
356
  ],
357
357
  hasSharedWorkspace: fs.existsSync(path.join(projectRoot, "shared", "package.json")),
358
358
  hasConnectors: Object.keys(config.connectors || {}).length > 0,
359
- hasAuth: Boolean(config.auth && config.auth.provider !== "none"),
359
+ hasAuth: Boolean(config.auth && (config.auth.provider !== "none" || Object.keys(config.auth.schemes ?? {}).length > 0)),
360
360
  },
361
361
  };
362
362
 
@@ -209,6 +209,22 @@ export async function validateProject(projectRoot: string = process.cwd()): Prom
209
209
  });
210
210
  }
211
211
 
212
+ const namedPolicies = loaded.manifest.auth?.authorization?.policies ?? {};
213
+ for (const entity of loaded.manifest.entities) {
214
+ const policy = entity.authPolicy;
215
+ if (!policy) continue;
216
+ const references = [policy.policy, typeof policy.read === "string" ? policy.read : undefined, typeof policy.write === "string" ? policy.write : undefined].filter((value): value is string => Boolean(value));
217
+ for (const name of references) {
218
+ if (!namedPolicies[name]) pushViolation(violations, {
219
+ code: "undefined-auth-policy",
220
+ severity: "error",
221
+ message: `Model '${entity.name}' references undefined authorization policy '${name}'.`,
222
+ location: { path: entity.filePath, entity: entity.name },
223
+ suggestedFix: `Define auth.authorization.policies.${name} or update the model authPolicy reference.`,
224
+ });
225
+ }
226
+ }
227
+
212
228
  for (const entity of loaded.manifest.entities) {
213
229
  validateSchemaNaming(entity, violations);
214
230
  }