swallowkit 1.0.0-beta.47 → 1.0.0-beta.49

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 (73) hide show
  1. package/dist/cli/commands/add-auth.d.ts +1 -0
  2. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  3. package/dist/cli/commands/add-auth.js +136 -1
  4. package/dist/cli/commands/add-auth.js.map +1 -1
  5. package/dist/cli/commands/dev.js +5 -2
  6. package/dist/cli/commands/dev.js.map +1 -1
  7. package/dist/cli/commands/init.d.ts +3 -0
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +68 -27
  10. package/dist/cli/commands/init.js.map +1 -1
  11. package/dist/cli/commands/scaffold.d.ts +9 -0
  12. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  13. package/dist/cli/commands/scaffold.js +56 -12
  14. package/dist/cli/commands/scaffold.js.map +1 -1
  15. package/dist/cli/index.d.ts.map +1 -1
  16. package/dist/cli/index.js +4 -0
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/core/config.d.ts +7 -2
  19. package/dist/core/config.d.ts.map +1 -1
  20. package/dist/core/config.js +81 -1
  21. package/dist/core/config.js.map +1 -1
  22. package/dist/core/mock/connector-mock-server.js +2 -2
  23. package/dist/core/mock/connector-mock-server.js.map +1 -1
  24. package/dist/core/project/manifest.js +2 -2
  25. package/dist/core/project/manifest.js.map +1 -1
  26. package/dist/core/project/validation.d.ts.map +1 -1
  27. package/dist/core/project/validation.js +17 -0
  28. package/dist/core/project/validation.js.map +1 -1
  29. package/dist/core/scaffold/auth-generator.d.ts +9 -2
  30. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  31. package/dist/core/scaffold/auth-generator.js +230 -13
  32. package/dist/core/scaffold/auth-generator.js.map +1 -1
  33. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
  34. package/dist/core/scaffold/connector-functions-generator.js +4 -2
  35. package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
  36. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  37. package/dist/core/scaffold/functions-generator.js +4 -2
  38. package/dist/core/scaffold/functions-generator.js.map +1 -1
  39. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  40. package/dist/core/scaffold/model-parser.js +8 -3
  41. package/dist/core/scaffold/model-parser.js.map +1 -1
  42. package/dist/core/scaffold/ui-generator.d.ts +3 -0
  43. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  44. package/dist/core/scaffold/ui-generator.js +12 -8
  45. package/dist/core/scaffold/ui-generator.js.map +1 -1
  46. package/dist/types/index.d.ts +34 -4
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/utils/package-manager.d.ts.map +1 -1
  49. package/dist/utils/package-manager.js +1 -0
  50. package/dist/utils/package-manager.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/__tests__/auth.test.ts +4 -0
  53. package/src/__tests__/config.test.ts +22 -1
  54. package/src/__tests__/dev.test.ts +8 -0
  55. package/src/__tests__/init.test.ts +8 -0
  56. package/src/__tests__/mcp.test.ts +2 -2
  57. package/src/__tests__/multi-auth.test.ts +133 -0
  58. package/src/cli/commands/add-auth.ts +112 -2
  59. package/src/cli/commands/dev.ts +6 -3
  60. package/src/cli/commands/init.ts +73 -28
  61. package/src/cli/commands/scaffold.ts +58 -14
  62. package/src/cli/index.ts +4 -0
  63. package/src/core/config.ts +69 -2
  64. package/src/core/mock/connector-mock-server.ts +2 -2
  65. package/src/core/project/manifest.ts +3 -3
  66. package/src/core/project/validation.ts +16 -0
  67. package/src/core/scaffold/auth-generator.ts +236 -14
  68. package/src/core/scaffold/connector-functions-generator.ts +4 -2
  69. package/src/core/scaffold/functions-generator.ts +4 -2
  70. package/src/core/scaffold/model-parser.ts +9 -3
  71. package/src/core/scaffold/ui-generator.ts +15 -8
  72. package/src/types/index.ts +35 -4
  73. package/src/utils/package-manager.ts +1 -0
@@ -0,0 +1,133 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { addAuthCommand } from "../cli/commands/add-auth";
5
+ import {
6
+ generateBFFCallFunctionWithMultipleAuth,
7
+ generateNamedAuthRouterCSharp,
8
+ generateNamedAuthRouterPython,
9
+ generateNamedAuthRouterTS,
10
+ } from "../core/scaffold/auth-generator";
11
+ import { normalizeAuthConfig } from "../core/config";
12
+ import { parseAuthPolicy } from "../core/scaffold/model-parser";
13
+ import { createBasicModelInfo } from "./fixtures";
14
+ import { generateCSharpAzureFunctionsCRUD, generateCompactAzureFunctionsCRUD, generatePythonAzureFunctionsCRUD } from "../core/scaffold/functions-generator";
15
+
16
+ const auth = normalizeAuthConfig({
17
+ schemes: {
18
+ admin: { provider: "swa" },
19
+ lineUser: { provider: "external-token" },
20
+ },
21
+ authorization: {
22
+ defaultPolicy: "anonymous",
23
+ policies: {
24
+ adminOnly: { schemes: ["admin"], roles: ["authenticated"] },
25
+ lineUserOnly: { schemes: ["lineUser"], roles: ["authenticated"] },
26
+ },
27
+ },
28
+ })!;
29
+
30
+ describe("named authentication routers", () => {
31
+ it("generates deterministic TypeScript policy routing and normalized principals", () => {
32
+ const code = generateNamedAuthRouterTS(auth);
33
+ expect(code).toContain("policy.schemes.filter");
34
+ expect(code).toContain("candidates.length !== 1");
35
+ expect(code).toContain("subject: value.userId");
36
+ expect(code).toContain("scheme + ':' + principal.subject");
37
+ expect(code).toContain("new AuthError(403");
38
+ expect(code).toContain("new AuthError(503");
39
+ expect(code).not.toContain("JWT_SECRET");
40
+ });
41
+
42
+ it("generates equivalent C# and Python named guards", () => {
43
+ const csharp = generateNamedAuthRouterCSharp(auth);
44
+ const python = generateNamedAuthRouterPython(auth);
45
+ expect(csharp).toContain("record AuthPrincipal(string Subject, string Scheme, string Issuer");
46
+ expect(csharp).toContain("candidates.Length != 1");
47
+ expect(csharp).toContain("HttpStatusCode.Forbidden");
48
+ expect(python).toContain('principal["scheme"] + ":" + principal["subject"]');
49
+ expect(python).toContain("len(candidates) != 1");
50
+ expect(python).toContain("AuthError(503");
51
+ });
52
+
53
+ it("generates valid branch structure for an SWA-only Python router", () => {
54
+ const swaOnly = normalizeAuthConfig({ schemes: { admin: { provider: "swa", swa: { allowedProviders: ["github"] } } }, authorization: { defaultPolicy: "anonymous", policies: { adminOnly: { schemes: ["admin"] } } } })!;
55
+ const python = generateNamedAuthRouterPython(swaOnly);
56
+ expect(python).toContain(' if PROVIDERS[scheme] == "swa":');
57
+ expect(python).not.toContain(' elif PROVIDERS[scheme] == "swa":');
58
+ expect(python).toContain("ALLOWED_PROVIDERS");
59
+ expect(generateNamedAuthRouterCSharp(swaOnly)).toContain("AllowedProviders[scheme]");
60
+ });
61
+
62
+ it("scaffolds named read/write guards for all Functions languages", () => {
63
+ const policy = { read: "adminOnly", write: "lineUserOnly" };
64
+ const model = createBasicModelInfo();
65
+ const ts = generateCompactAzureFunctionsCRUD(model, "@fixture/shared", policy);
66
+ const cs = generateCSharpAzureFunctionsCRUD(model, policy);
67
+ const py = generatePythonAzureFunctionsCRUD(model, policy).blueprint;
68
+ expect(ts).toContain("./auth/auth-router");
69
+ expect(ts).toContain('requireAuth(request, "adminOnly")');
70
+ expect(ts).toContain('requireAuth(request, "lineUserOnly")');
71
+ expect(cs).toContain('AuthRouter.Authorize(request, "adminOnly")');
72
+ expect(cs).toContain('AuthRouter.Authorize(request, "lineUserOnly")');
73
+ expect(py).toContain("from auth.auth_router import");
74
+ expect(py).toContain('require_auth(req, "adminOnly")');
75
+ expect(py).toContain('require_auth(req, "lineUserOnly")');
76
+ });
77
+
78
+ it("forwards both credential sources and the Functions host key", () => {
79
+ const code = generateBFFCallFunctionWithMultipleAuth();
80
+ expect(code).toContain("fetchHeaders['Authorization']");
81
+ expect(code).toContain("fetchHeaders['x-ms-client-principal']");
82
+ expect(code).toContain("'x-functions-key'");
83
+ expect(code).not.toMatch(/console\.log\([^\n]*(authorization|principal|cookie)/i);
84
+ });
85
+
86
+ it("parses model-wide and operation-specific named policies", () => {
87
+ expect(parseAuthPolicy("export const authPolicy = { policy: 'adminOnly' };"))
88
+ .toEqual({ policy: "adminOnly" });
89
+ expect(parseAuthPolicy("export const authPolicy = { read: 'adminOnly', write: 'lineUserOnly' };"))
90
+ .toEqual({ read: "adminOnly", write: "lineUserOnly" });
91
+ });
92
+ });
93
+
94
+ describe("add-auth named scheme CLI", () => {
95
+ const originalCwd = process.cwd();
96
+ let root: string;
97
+
98
+ beforeEach(() => {
99
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-multi-auth-"));
100
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "fixture" }));
101
+ fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language: "typescript" }, auth: { schemes: {}, authorization: { defaultPolicy: "anonymous", policies: {} } } }, null, 2));
102
+ process.chdir(root);
103
+ });
104
+
105
+ afterEach(() => {
106
+ process.chdir(originalCwd);
107
+ fs.rmSync(root, { recursive: true, force: true });
108
+ });
109
+
110
+ it("adds two schemes without overwriting the first verifier", async () => {
111
+ await addAuthCommand({ scheme: "admin", provider: "swa" });
112
+ await addAuthCommand({ scheme: "lineUser", provider: "external-token" });
113
+ const verifier = path.join(root, "functions", "src", "auth", "schemes", "line-user", "verifier.ts");
114
+ fs.writeFileSync(verifier, "// user verifier\n", "utf-8");
115
+ const config = JSON.parse(fs.readFileSync(path.join(root, "swallowkit.config.json"), "utf-8"));
116
+ expect(config.auth.schemes.admin.provider).toBe("swa");
117
+ expect(config.auth.schemes.lineUser.provider).toBe("external-token");
118
+ await expect(addAuthCommand({ scheme: "lineUser", provider: "external-token" })).rejects.toThrow("already exists");
119
+ expect(fs.readFileSync(verifier, "utf-8")).toBe("// user verifier\n");
120
+ expect(fs.readFileSync(path.join(root, "lib", "api", "call-function.ts"), "utf-8")).toContain("x-functions-key");
121
+ });
122
+
123
+ it("updates a JavaScript config that already has a trailing comma", async () => {
124
+ fs.rmSync(path.join(root, "swallowkit.config.json"));
125
+ fs.writeFileSync(path.join(root, "swallowkit.config.js"), "module.exports = {\n backend: { language: 'typescript' },\n api: { endpoint: '/api/test' },\n};\n");
126
+ await addAuthCommand({ scheme: "admin", provider: "swa" });
127
+ const content = fs.readFileSync(path.join(root, "swallowkit.config.js"), "utf-8");
128
+ expect(content).not.toContain(",,");
129
+ const configModule = { exports: {} as Record<string, unknown> };
130
+ new Function("module", content)(configModule);
131
+ expect((configModule.exports as { auth: { schemes: { admin: { provider: string } } } }).auth.schemes.admin.provider).toBe("swa");
132
+ });
133
+ });
@@ -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,
@@ -23,6 +23,7 @@ import {
23
23
  generateAuthContext,
24
24
  generateBFFCallFunctionWithAuth,
25
25
  generateBFFCallFunctionWithSwaAuth,
26
+ generateBFFCallFunctionWithMultipleAuth,
26
27
  generateSwaAuthHelperTS,
27
28
  generateSwaAuthHelperCSharp,
28
29
  generateSwaAuthHelperPython,
@@ -39,12 +40,15 @@ import {
39
40
  generateExternalTokenVerifierPython,
40
41
  generateExternalTokenHelperPython,
41
42
  generateExternalAuthMePython,
43
+ generateNamedExternalTokenVerifierCSharp,
44
+ generateNamedExternalTokenVerifierPython,
42
45
  } from "../../core/scaffold/auth-generator";
43
46
  import { syncProjectManifest } from "../../core/project/manifest";
44
47
  import { buildSharedTsConfig } from "./init";
45
48
 
46
49
  interface AddAuthOptions {
47
50
  provider?: string;
51
+ scheme?: string;
48
52
  }
49
53
 
50
54
  function writeIfMissing(filePath: string, content: string): void {
@@ -239,9 +243,18 @@ export async function addAuthCommand(options: AddAuthOptions) {
239
243
  }
240
244
 
241
245
  const backendLanguage = getBackendLanguage();
242
- const config = getFullConfig();
246
+ const config = getValidatedFullConfig();
243
247
  const cwd = process.cwd();
244
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
+
245
258
  if (provider === "swa-custom") {
246
259
  throw new Error("The swa-custom provider is not implemented. Use --provider swa or --provider custom-jwt.");
247
260
  }
@@ -364,6 +377,103 @@ export async function addAuthCommand(options: AddAuthOptions) {
364
377
  console.log(` 5. Run scaffold to regenerate functions with auth guards`);
365
378
  }
366
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
+
367
477
  /**
368
478
  * Ensure the shared package has proper build infrastructure
369
479
  * (tsconfig.json, build script, typescript devDependency).
@@ -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,
@@ -648,7 +648,8 @@ async function startDevEnvironment(options: DevOptions) {
648
648
  const pmCmd = getCommands(pm);
649
649
  const backendLanguage = getBackendLanguage();
650
650
  const authConfig = getAuthConfig();
651
- const useSwaEmulator = authConfig?.provider === 'swa' && !options.noSwa;
651
+ const normalizedAuth = normalizeAuthConfig(authConfig);
652
+ const useSwaEmulator = Boolean(normalizedAuth && Object.values(normalizedAuth.schemes).some(scheme => scheme.provider === 'swa')) && !options.noSwa;
652
653
  const swaCommand = useSwaEmulator ? await resolveSwaCliCommand(process.cwd()) : null;
653
654
 
654
655
  if (useSwaEmulator && !swaCommand) {
@@ -981,7 +982,9 @@ async function startDevEnvironment(options: DevOptions) {
981
982
  passwordHashColumn: authConfig.customJwt.passwordHashColumn,
982
983
  rolesColumn: authConfig.customJwt.rolesColumn,
983
984
  },
984
- 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,
985
988
  };
986
989
  }
987
990
 
@@ -31,15 +31,22 @@ interface InitOptions {
31
31
  backendLanguage?: BackendLanguage;
32
32
  cosmosDbMode?: CosmosDbMode;
33
33
  vnet?: VNetOption;
34
+ swaPlan?: StaticWebAppPlan;
34
35
  }
35
36
 
36
37
  type CiCdProvider = 'github' | 'azure' | 'skip';
37
38
  type CosmosDbMode = 'freetier' | 'serverless';
38
39
  type VNetOption = 'none' | 'outbound';
40
+ type StaticWebAppPlan = 'free' | 'standard';
41
+
42
+ export function getStaticWebAppSku(plan: StaticWebAppPlan): 'Free' | 'Standard' {
43
+ return plan === 'free' ? 'Free' : 'Standard';
44
+ }
39
45
 
40
46
  interface AzureConfig {
41
47
  cosmosDbMode: CosmosDbMode;
42
48
  vnetOption: VNetOption;
49
+ swaPlan: StaticWebAppPlan;
43
50
  }
44
51
 
45
52
  const BACKEND_LANGUAGE_CHOICES: Array<{ title: string; value: BackendLanguage }> = [
@@ -186,32 +193,56 @@ async function promptCiCd(): Promise<CiCdProvider> {
186
193
  return response.cicd || 'skip';
187
194
  }
188
195
 
189
- async function promptAzureConfig(): Promise<AzureConfig> {
190
- const cosmosResponse = await prompts({
191
- type: 'select',
192
- name: 'mode',
193
- message: 'Cosmos DB mode (affects cost):',
194
- choices: [
195
- { title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
196
- { title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
197
- ],
198
- initial: 0
199
- });
200
-
201
- const vnetResponse = await prompts({
202
- type: 'select',
203
- name: 'vnet',
204
- message: 'Network security:',
205
- choices: [
206
- { title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
207
- { title: 'None - Public endpoints, simplest but least secure', value: 'none' }
208
- ],
209
- initial: 0
210
- });
196
+ async function promptAzureConfig(options: InitOptions): Promise<AzureConfig> {
197
+ let swaPlan = options.swaPlan;
198
+ if (!swaPlan) {
199
+ const response = await prompts({
200
+ type: 'select',
201
+ name: 'plan',
202
+ message: 'Azure Static Web Apps plan:',
203
+ choices: [
204
+ { title: 'Standard (production, SLA and advanced auth/networking features)', value: 'standard' },
205
+ { title: 'Free (personal projects, no SLA and lower quotas)', value: 'free' }
206
+ ],
207
+ initial: 0
208
+ });
209
+ swaPlan = response.plan || 'standard';
210
+ }
211
+
212
+ let cosmosDbMode = options.cosmosDbMode;
213
+ if (!cosmosDbMode) {
214
+ const response = await prompts({
215
+ type: 'select',
216
+ name: 'mode',
217
+ message: 'Cosmos DB mode (affects cost):',
218
+ choices: [
219
+ { title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
220
+ { title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
221
+ ],
222
+ initial: 0
223
+ });
224
+ cosmosDbMode = response.mode || 'freetier';
225
+ }
226
+
227
+ let vnetOption = options.vnet;
228
+ if (!vnetOption) {
229
+ const response = await prompts({
230
+ type: 'select',
231
+ name: 'vnet',
232
+ message: 'Network security:',
233
+ choices: [
234
+ { title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
235
+ { title: 'None - Public endpoints, simplest but least secure', value: 'none' }
236
+ ],
237
+ initial: 0
238
+ });
239
+ vnetOption = response.vnet || 'outbound';
240
+ }
211
241
 
212
242
  return {
213
- cosmosDbMode: cosmosResponse.mode || 'freetier',
214
- vnetOption: vnetResponse.vnet || 'outbound'
243
+ cosmosDbMode: cosmosDbMode || 'freetier',
244
+ vnetOption: vnetOption || 'outbound',
245
+ swaPlan: swaPlan || 'standard'
215
246
  };
216
247
  }
217
248
 
@@ -219,6 +250,7 @@ const VALID_CICD: CiCdProvider[] = ['github', 'azure', 'skip'];
219
250
  const VALID_BACKEND_LANGUAGE: BackendLanguage[] = ['typescript', 'csharp', 'python'];
220
251
  const VALID_COSMOS_DB_MODE: CosmosDbMode[] = ['freetier', 'serverless'];
221
252
  const VALID_VNET: VNetOption[] = ['none', 'outbound'];
253
+ const VALID_SWA_PLAN: StaticWebAppPlan[] = ['free', 'standard'];
222
254
 
223
255
  function validateInitFlags(options: InitOptions): void {
224
256
  if (options.cicd && !VALID_CICD.includes(options.cicd)) {
@@ -237,6 +269,10 @@ function validateInitFlags(options: InitOptions): void {
237
269
  console.error(`❌ Invalid --vnet value: "${options.vnet}". Must be: ${VALID_VNET.join(', ')}`);
238
270
  process.exit(1);
239
271
  }
272
+ if (options.swaPlan && !VALID_SWA_PLAN.includes(options.swaPlan)) {
273
+ console.error(`❌ Invalid --swa-plan value: "${options.swaPlan}". Must be: ${VALID_SWA_PLAN.join(', ')}`);
274
+ process.exit(1);
275
+ }
240
276
  }
241
277
 
242
278
  export async function initCommand(options: InitOptions) {
@@ -264,9 +300,7 @@ export async function initCommand(options: InitOptions) {
264
300
  const cicdProvider: CiCdProvider = options.cicd || await promptCiCd();
265
301
  const backendLanguage: BackendLanguage = options.backendLanguage || await promptBackendLanguage();
266
302
 
267
- const azureConfig: AzureConfig = (options.cosmosDbMode && options.vnet)
268
- ? { cosmosDbMode: options.cosmosDbMode, vnetOption: options.vnet }
269
- : await promptAzureConfig();
303
+ const azureConfig = await promptAzureConfig(options);
270
304
 
271
305
  // Create Next.js project with create-next-app
272
306
  await createNextJsProject(options.name, pm);
@@ -2494,6 +2528,7 @@ async function createInfrastructure(
2494
2528
 
2495
2529
  const enableVNet = azureConfig.vnetOption !== 'none';
2496
2530
  const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
2531
+ const staticWebAppSku = getStaticWebAppSku(azureConfig.swaPlan);
2497
2532
 
2498
2533
  // main.bicep
2499
2534
  const mainBicep = `targetScope = 'resourceGroup'
@@ -2507,6 +2542,13 @@ param location string = resourceGroup().location
2507
2542
  @description('Location for Static Web App (must be explicitly provided)')
2508
2543
  param swaLocation string
2509
2544
 
2545
+ @description('Static Web Apps hosting plan')
2546
+ @allowed([
2547
+ 'Free'
2548
+ 'Standard'
2549
+ ])
2550
+ param staticWebAppSku string = '${staticWebAppSku}'
2551
+
2510
2552
  @description('Cosmos DB mode')
2511
2553
  @allowed(['freetier', 'serverless'])
2512
2554
  param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
@@ -2549,7 +2591,7 @@ module staticWebApp 'modules/staticwebapp.bicep' = {
2549
2591
  params: {
2550
2592
  name: 'swa-\${projectName}'
2551
2593
  location: swaLocation
2552
- sku: 'Standard'
2594
+ sku: staticWebAppSku
2553
2595
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
2554
2596
  }
2555
2597
  }
@@ -2692,6 +2734,9 @@ output vnetName string = enableVNet ? vnet.outputs.name : ''
2692
2734
  "cosmosDbMode": {
2693
2735
  "value": "${azureConfig.cosmosDbMode}"
2694
2736
  },
2737
+ "staticWebAppSku": {
2738
+ "value": "${staticWebAppSku}"
2739
+ },
2695
2740
  "enableVNet": {
2696
2741
  "value": ${enableVNet}
2697
2742
  }