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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/cli/commands/add-auth.d.ts +2 -0
  2. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  3. package/dist/cli/commands/add-auth.js +214 -16
  4. package/dist/cli/commands/add-auth.js.map +1 -1
  5. package/dist/cli/commands/dev.d.ts +4 -0
  6. package/dist/cli/commands/dev.d.ts.map +1 -1
  7. package/dist/cli/commands/dev.js +100 -12
  8. package/dist/cli/commands/dev.js.map +1 -1
  9. package/dist/cli/commands/init.d.ts +2 -0
  10. package/dist/cli/commands/init.d.ts.map +1 -1
  11. package/dist/cli/commands/init.js +74 -30
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  14. package/dist/cli/commands/scaffold.js +37 -5
  15. package/dist/cli/commands/scaffold.js.map +1 -1
  16. package/dist/cli/index.d.ts.map +1 -1
  17. package/dist/cli/index.js +3 -1
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/core/scaffold/auth-generator.d.ts +20 -2
  20. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  21. package/dist/core/scaffold/auth-generator.js +265 -12
  22. package/dist/core/scaffold/auth-generator.js.map +1 -1
  23. package/dist/core/scaffold/connector-functions-generator.d.ts +3 -3
  24. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
  25. package/dist/core/scaffold/connector-functions-generator.js +19 -19
  26. package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
  27. package/dist/core/scaffold/functions-generator.d.ts +2 -2
  28. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  29. package/dist/core/scaffold/functions-generator.js +16 -16
  30. package/dist/core/scaffold/functions-generator.js.map +1 -1
  31. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  32. package/dist/core/scaffold/nextjs-generator.js +13 -1
  33. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  34. package/dist/types/index.d.ts +1 -1
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +20 -20
  38. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +13 -1
  39. package/src/__tests__/auth.test.ts +103 -0
  40. package/src/__tests__/dev.test.ts +27 -0
  41. package/src/__tests__/init.test.ts +27 -0
  42. package/src/__tests__/nextjs-generator.test.ts +8 -0
  43. package/src/cli/commands/add-auth.ts +239 -16
  44. package/src/cli/commands/dev.ts +111 -14
  45. package/src/cli/commands/init.ts +76 -30
  46. package/src/cli/commands/scaffold.ts +37 -5
  47. package/src/cli/index.ts +3 -1
  48. package/src/core/scaffold/auth-generator.ts +267 -13
  49. package/src/core/scaffold/connector-functions-generator.ts +22 -19
  50. package/src/core/scaffold/functions-generator.ts +17 -17
  51. package/src/core/scaffold/nextjs-generator.ts +13 -1
  52. package/src/types/index.ts +1 -1
@@ -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[],
@@ -446,6 +446,7 @@ export async function withNpmLockfileSafeManifests(projectDir: string, action: (
446
446
  const functionsPackageJsonPath = path.join(projectDir, 'functions', 'package.json');
447
447
  const sharedPackageJsonPath = path.join(projectDir, 'shared', 'package.json');
448
448
  const originalFiles = new Map<string, string>();
449
+ const hiddenNodeModules = new Map<string, string>();
449
450
 
450
451
  function rememberOriginal(filePath: string): void {
451
452
  if (!originalFiles.has(filePath)) {
@@ -493,11 +494,30 @@ export async function withNpmLockfileSafeManifests(projectDir: string, action: (
493
494
  writeJson(filePath, packageJson);
494
495
  }
495
496
 
497
+ function hideNodeModules(directory: string): void {
498
+ const nodeModulesPath = path.join(directory, 'node_modules');
499
+ if (!fs.existsSync(nodeModulesPath)) return;
500
+
501
+ const hiddenPath = path.join(directory, `.node_modules.swallowkit-npm-lockfile-${process.pid}`);
502
+ fs.renameSync(nodeModulesPath, hiddenPath);
503
+ hiddenNodeModules.set(nodeModulesPath, hiddenPath);
504
+ }
505
+
496
506
  try {
497
507
  normalizePackageJson(packageJsonPath, 'file:shared');
498
508
  normalizePackageJson(functionsPackageJsonPath, 'file:../shared');
509
+ hideNodeModules(projectDir);
510
+ hideNodeModules(path.join(projectDir, 'shared'));
511
+ hideNodeModules(path.join(projectDir, 'functions'));
499
512
  await action();
500
513
  } finally {
514
+ for (const [nodeModulesPath, hiddenPath] of hiddenNodeModules) {
515
+ if (fs.existsSync(nodeModulesPath)) {
516
+ fs.rmSync(nodeModulesPath, { recursive: true, force: true });
517
+ }
518
+ fs.renameSync(hiddenPath, nodeModulesPath);
519
+ }
520
+
501
521
  for (const [filePath, content] of originalFiles) {
502
522
  fs.writeFileSync(filePath, content);
503
523
  }
@@ -710,6 +730,7 @@ export function buildGeneratedProjectDependencies(projectName: string): Record<s
710
730
  return {
711
731
  '@azure/cosmos': '^4.0.0',
712
732
  'applicationinsights': '^3.3.0',
733
+ 'zod': '^4.0.0',
713
734
  [`@${projectName}/shared`]: 'workspace:*',
714
735
  };
715
736
  }
@@ -872,6 +893,13 @@ function getFunctionsBaseUrl(): string {
872
893
  return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
873
894
  }
874
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
+
875
903
  /**
876
904
  * Simple HTTP client for calling backend APIs
877
905
  * Use this to make requests to BFF API routes (which forward to Azure Functions)
@@ -883,6 +911,7 @@ async function request<T>(
883
911
  queryParams?: Record<string, string>
884
912
  ): Promise<T> {
885
913
  const functionsBaseUrl = getFunctionsBaseUrl();
914
+ const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
886
915
  let url = \`\${functionsBaseUrl}\${endpoint}\`;
887
916
  if (queryParams) {
888
917
  const params = new URLSearchParams(queryParams);
@@ -894,6 +923,7 @@ async function request<T>(
894
923
  method,
895
924
  headers: {
896
925
  'Content-Type': 'application/json',
926
+ ...keyHeaders,
897
927
  },
898
928
  body: body ? JSON.stringify(body) : undefined,
899
929
  });
@@ -977,6 +1007,7 @@ export const api = {
977
1007
  // 6. Create .env.example
978
1008
  const envExample = `# Azure Functions Backend URL
979
1009
  BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
1010
+ BACKEND_FUNCTIONS_KEY=
980
1011
 
981
1012
  # Azure Configuration
982
1013
  AZURE_RESOURCE_GROUP=your-resource-group
@@ -1062,6 +1093,7 @@ export async function register() {
1062
1093
  const envLocalContent = [
1063
1094
  '# Azure Functions Backend URL (Local)',
1064
1095
  'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
1096
+ 'BACKEND_FUNCTIONS_KEY=',
1065
1097
  ''
1066
1098
  ].join('\n');
1067
1099
  fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
@@ -1358,7 +1390,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
1358
1390
 
1359
1391
  app.http('greet', {
1360
1392
  methods: ['GET', 'POST'],
1361
- authLevel: 'anonymous',
1393
+ authLevel: 'function',
1362
1394
  handler: greet
1363
1395
  });
1364
1396
  `);
@@ -1390,7 +1422,7 @@ public sealed class GreetFunction
1390
1422
  {
1391
1423
  [Function("greet")]
1392
1424
  public async Task<HttpResponseData> Run(
1393
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "greet")] HttpRequestData request)
1425
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
1394
1426
  {
1395
1427
  var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
1396
1428
  var name = "SwallowKit";
@@ -1458,7 +1490,7 @@ def greet(req: func.HttpRequest) -> func.HttpResponse:
1458
1490
 
1459
1491
  from blueprints.greet import bp as greet_bp
1460
1492
 
1461
- app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
1493
+ app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
1462
1494
 
1463
1495
  app.register_blueprint(greet_bp)
1464
1496
  # SwallowKit scaffold registrations
@@ -2089,7 +2121,7 @@ const containerName = 'Models'; // PascalCase + 's'
2089
2121
  app.http('{model}-get-all', {
2090
2122
  methods: ['GET'],
2091
2123
  route: '{model}',
2092
- authLevel: 'anonymous',
2124
+ authLevel: 'function',
2093
2125
  extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
2094
2126
  handler: async (request, context) => {
2095
2127
  const documents = context.extraInputs.get('cosmosInput');
@@ -2380,7 +2412,7 @@ import { ModelSchema } from '@${projectName}/shared';
2380
2412
  app.http('{model}-get-all', {
2381
2413
  methods: ['GET'],
2382
2414
  route: '{model}',
2383
- authLevel: 'anonymous',
2415
+ authLevel: 'function',
2384
2416
  extraInputs: [cosmosInput],
2385
2417
  handler: async (request, context) => {
2386
2418
  const documents = context.extraInputs.get(cosmosInput);
@@ -2411,6 +2443,43 @@ app.http('{model}-get-all', {
2411
2443
  console.log('');
2412
2444
  }
2413
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
+
2414
2483
  async function createInfrastructure(
2415
2484
  projectDir: string,
2416
2485
  projectName: string,
@@ -2585,6 +2654,7 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
2585
2654
  params: {
2586
2655
  staticWebAppName: staticWebApp.outputs.name
2587
2656
  functionsDefaultHostname: functionsFlex.outputs.defaultHostname
2657
+ functionsHostKey: ${buildFunctionsHostKeyBicepExpression()}
2588
2658
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
2589
2659
  }
2590
2660
  dependsOn: [
@@ -2678,31 +2748,7 @@ output defaultHostname string = staticWebApp.properties.defaultHostname
2678
2748
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
2679
2749
 
2680
2750
  // modules/staticwebapp-config.bicep (for updating config after Functions deployment)
2681
- const staticWebAppConfigBicep = `@description('Static Web App name')
2682
- param staticWebAppName string
2683
-
2684
- @description('Functions App default hostname for backend API calls')
2685
- param functionsDefaultHostname string
2686
-
2687
- @description('Application Insights connection string for SWA')
2688
- param appInsightsConnectionString string
2689
-
2690
- resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
2691
- name: staticWebAppName
2692
- }
2693
-
2694
- resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
2695
- parent: staticWebApp
2696
- name: 'appsettings'
2697
- properties: {
2698
- APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
2699
- ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
2700
- BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
2701
- }
2702
- }
2703
-
2704
- output configName string = staticWebAppConfig.name
2705
- `;
2751
+ const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
2706
2752
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
2707
2753
 
2708
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,