swallowkit 1.0.0-beta.46 → 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 +56 -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 +19 -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 +56 -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
@@ -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,24 @@ 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 useSwaEmulator = authConfig?.provider === 'swa' && !options.noSwa;
652
+ const swaCommand = useSwaEmulator ? await resolveSwaCliCommand(process.cwd()) : null;
653
+
654
+ if (useSwaEmulator && !swaCommand) {
655
+ const installCommand = getSwaCliInstallCommand(pm);
656
+ console.error('❌ SWA authentication requires Azure Static Web Apps CLI for local development.');
657
+ console.error(' Install it in this project and retry:');
658
+ console.error(`\n ${installCommand}\n`);
659
+ process.exitCode = 1;
660
+ return;
661
+ }
626
662
 
627
663
  // プロセスを管理する配列
628
664
  const processes: ChildProcess[] = [];
@@ -925,7 +961,6 @@ async function startDevEnvironment(options: DevOptions) {
925
961
  const connectorModels = allModels.filter((m) => m.connectorConfig);
926
962
 
927
963
  // Resolve auth config — auth functions use RDB connectors, mocked alongside other models
928
- const authConfig = getAuthConfig();
929
964
  let mockAuthConfig: { jwtSecret: string; tokenExpiry?: string; customJwt?: { userTable: string; loginIdColumn: string; passwordHashColumn: string; rolesColumn: string }; defaultPolicy?: "authenticated" | "anonymous" } | undefined;
930
965
  if (authConfig?.provider === 'custom-jwt' && authConfig.customJwt) {
931
966
  // Read JWT_SECRET from functions/local.settings.json if available
@@ -985,18 +1020,6 @@ async function startDevEnvironment(options: DevOptions) {
985
1020
  // 5. Start Next.js development server
986
1021
  const nextArgs = buildNextDevArgs(pm, port);
987
1022
 
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
1023
  // Ensure .env.local points to bffTargetPort so Next.js reads the correct backend URL.
1001
1024
  // When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
1002
1025
  // Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
@@ -1063,10 +1086,52 @@ async function startDevEnvironment(options: DevOptions) {
1063
1086
  }
1064
1087
  }
1065
1088
 
1089
+ let swaReady = false;
1090
+ if (useSwaEmulator && swaCommand) {
1091
+ const nextUrl = `http://${options.host || 'localhost'}:${port}`;
1092
+ console.log(`⏳ Waiting for Next.js before starting the SWA emulator at ${nextUrl}...`);
1093
+ const nextReady = await waitForHttpServerReady(nextUrl);
1094
+ if (!nextReady) {
1095
+ throw new Error(`Next.js did not become ready at ${nextUrl}`);
1096
+ }
1097
+
1098
+ console.log(`🔐 Starting SWA authentication emulator (port: ${swaPort})...`);
1099
+ const swaProcess = spawn(swaCommand, buildSwaStartArgs(options.host, port, swaPort), {
1100
+ cwd: process.cwd(),
1101
+ shell: true,
1102
+ stdio: 'inherit',
1103
+ env: process.env,
1104
+ });
1105
+ processes.push(swaProcess);
1106
+ swaProcess.on('error', (error) => {
1107
+ console.error('❌ SWA CLI startup error:', error.message);
1108
+ });
1109
+
1110
+ const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
1111
+ swaReady = await waitForHttpServerReady(swaUrl);
1112
+ if (!swaReady) {
1113
+ throw new Error(`SWA authentication emulator did not become ready at ${swaUrl}`);
1114
+ }
1115
+ }
1116
+
1066
1117
  console.log('');
1067
1118
  console.log('✅ SwallowKit development environment is running!');
1068
1119
  console.log('');
1069
- console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
1120
+ if (useSwaEmulator) {
1121
+ const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
1122
+ console.log('🔐 SWA authenticated app:');
1123
+ console.log(` ${swaUrl}`);
1124
+ console.log('');
1125
+ console.log('🔑 Local sign-in:');
1126
+ console.log(` ${swaUrl}/.auth/login/aad`);
1127
+ console.log(' Enter any username and select Login.');
1128
+ console.log(' The "authenticated" role is added automatically.');
1129
+ console.log('');
1130
+ console.log('📱 Next.js direct access (authentication is not emulated):');
1131
+ console.log(` http://${options.host || 'localhost'}:${port}`);
1132
+ } else {
1133
+ console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
1134
+ }
1070
1135
  if (hasFunctions && !options.noFunctions) {
1071
1136
  console.log(`${functionsReady ? '⚡ Azure Functions' : '⏳ Azure Functions (starting)'}: ${functionsBaseUrl}`);
1072
1137
  }
@@ -1082,7 +1147,22 @@ async function startDevEnvironment(options: DevOptions) {
1082
1147
  if (mockServer) {
1083
1148
  console.log('💡 Connector models served from mock server (Zod-generated data)');
1084
1149
  }
1150
+ if (swaReady) {
1151
+ console.log('💡 Open the app through the SWA port when testing authenticated pages and CRUD APIs.');
1152
+ }
1085
1153
  console.log('');
1154
+
1155
+ if (options.open) {
1156
+ const url = useSwaEmulator
1157
+ ? `http://${options.host || 'localhost'}:${swaPort}`
1158
+ : `http://${options.host || 'localhost'}:${port}`;
1159
+ console.log(`🌐 Opening browser: ${url}`);
1160
+ const start = process.platform === 'darwin' ? 'open' :
1161
+ process.platform === 'win32' ? 'start' : 'xdg-open';
1162
+ spawn(start, [url], { shell: true });
1163
+ console.log('');
1164
+ }
1165
+
1086
1166
  console.log('🛑 Press Ctrl+C to stop');
1087
1167
  console.log('');
1088
1168
 
@@ -1097,6 +1177,23 @@ async function startDevEnvironment(options: DevOptions) {
1097
1177
  }
1098
1178
  }
1099
1179
 
1180
+ async function resolveSwaCliCommand(projectRoot: string): Promise<string | null> {
1181
+ const binaryName = process.platform === 'win32' ? 'swa.cmd' : 'swa';
1182
+ const localCommand = path.join(projectRoot, 'node_modules', '.bin', binaryName);
1183
+ if (fs.existsSync(localCommand) && await checkCommand(localCommand, ['--version'], {
1184
+ cwd: projectRoot,
1185
+ shell: process.platform === 'win32',
1186
+ })) {
1187
+ return localCommand;
1188
+ }
1189
+
1190
+ if (await checkCommand('swa', ['--version'], { cwd: projectRoot })) {
1191
+ return 'swa';
1192
+ }
1193
+
1194
+ return null;
1195
+ }
1196
+
1100
1197
  async function runCommand(
1101
1198
  command: string,
1102
1199
  args: string[],
@@ -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)
@@ -162,6 +162,9 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
162
162
  ? { authPolicy: uiAuthPolicy }
163
163
  : undefined;
164
164
  await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions);
165
+ if (uiAuthConfig?.provider === 'external-token') {
166
+ applyExternalTokenFetchToGeneratedUi(modelInfo);
167
+ }
165
168
  await updateNavigationMenu(modelInfo);
166
169
  }
167
170
 
@@ -190,6 +193,31 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
190
193
  }
191
194
  }
192
195
 
196
+ function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo): void {
197
+ const uiRoot = path.join(process.cwd(), 'app', toKebabCase(modelInfo.name));
198
+ if (!fs.existsSync(uiRoot)) return;
199
+ const visit = (dir: string): void => {
200
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
201
+ const target = path.join(dir, entry.name);
202
+ if (entry.isDirectory()) { visit(target); continue; }
203
+ if (!entry.name.endsWith('.tsx')) continue;
204
+ let content = fs.readFileSync(target, 'utf-8');
205
+ if (!content.includes('fetch(')) continue;
206
+ content = content.replace(/\bfetch\(/g, 'authenticatedFetch(');
207
+ content = content.replace(
208
+ /(if \(!res\.ok\) throw new Error\([^\n]+\);)/g,
209
+ `if (res.status === 401) throw new Error('Authentication required');\n $1`
210
+ );
211
+ if (!content.includes("@/lib/auth/authenticated-fetch")) {
212
+ const directive = content.match(/^'use client';\s*\r?\n/)?.[0] ?? '';
213
+ content = `${directive}import { authenticatedFetch } from '@/lib/auth/authenticated-fetch';\n${content.slice(directive.length)}`;
214
+ }
215
+ fs.writeFileSync(target, content, 'utf-8');
216
+ }
217
+ };
218
+ visit(uiRoot);
219
+ }
220
+
193
221
  /**
194
222
  * モデルの authPolicy と auth config から実効的な認可ポリシーを解決
195
223
  * - モデルに authPolicy があればそれを使用
@@ -305,8 +333,10 @@ async function generateCallFunctionHelper(): Promise<void> {
305
333
  const hasAuth = authConfig && authConfig.provider !== 'none';
306
334
  let helperCode: string;
307
335
  if (hasAuth) {
308
- const { generateBFFCallFunctionWithAuth } = await import("../../core/scaffold/auth-generator");
309
- helperCode = generateBFFCallFunctionWithAuth();
336
+ const { generateBFFCallFunctionWithAuth, generateBFFCallFunctionWithSwaAuth } = await import("../../core/scaffold/auth-generator");
337
+ helperCode = authConfig?.provider === "swa"
338
+ ? generateBFFCallFunctionWithSwaAuth()
339
+ : generateBFFCallFunctionWithAuth();
310
340
  } else {
311
341
  helperCode = generateBFFCallFunction();
312
342
  }
@@ -347,7 +377,7 @@ async function generateFunctionsCode(
347
377
  fs.mkdirSync(functionDir, { recursive: true });
348
378
  }
349
379
 
350
- const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy);
380
+ const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy, authConfig?.provider);
351
381
  fs.writeFileSync(functionFilePath, code, "utf-8");
352
382
  console.log(`✅ Created: ${functionFilePath}`);
353
383
  return;
@@ -425,14 +455,16 @@ async function generateConnectorFunctionsCode(
425
455
  modelInfo, sharedPackageName,
426
456
  connectorDef as RdbConnectorConfig,
427
457
  connectorConfig as RdbModelConnectorConfig,
428
- authPolicy
458
+ authPolicy,
459
+ authConfig?.provider
429
460
  );
430
461
  } else {
431
462
  code = generateApiConnectorFunctionTS(
432
463
  modelInfo, sharedPackageName,
433
464
  connectorDef as ApiConnectorConfig,
434
465
  connectorConfig as ApiModelConnectorConfig,
435
- authPolicy
466
+ authPolicy,
467
+ authConfig?.provider
436
468
  );
437
469
  }
438
470
 
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,7 +170,7 @@ 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")
172
174
  .action((options) => {
173
175
  addAuthCommand({
174
176
  provider: options.provider,