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.
- package/dist/cli/commands/add-auth.d.ts +1 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +136 -1
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/dev.js +5 -2
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +68 -27
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +9 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +56 -12
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +7 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +81 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.js +2 -2
- package/dist/core/mock/connector-mock-server.js.map +1 -1
- package/dist/core/project/manifest.js +2 -2
- package/dist/core/project/manifest.js.map +1 -1
- package/dist/core/project/validation.d.ts.map +1 -1
- package/dist/core/project/validation.js +17 -0
- package/dist/core/project/validation.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts +9 -2
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +230 -13
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/connector-functions-generator.js +4 -2
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +4 -2
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +8 -3
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +3 -0
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +12 -8
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/types/index.d.ts +34 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +1 -0
- package/dist/utils/package-manager.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/auth.test.ts +4 -0
- package/src/__tests__/config.test.ts +22 -1
- package/src/__tests__/dev.test.ts +8 -0
- package/src/__tests__/init.test.ts +8 -0
- package/src/__tests__/mcp.test.ts +2 -2
- package/src/__tests__/multi-auth.test.ts +133 -0
- package/src/cli/commands/add-auth.ts +112 -2
- package/src/cli/commands/dev.ts +6 -3
- package/src/cli/commands/init.ts +73 -28
- package/src/cli/commands/scaffold.ts +58 -14
- package/src/cli/index.ts +4 -0
- package/src/core/config.ts +69 -2
- package/src/core/mock/connector-mock-server.ts +2 -2
- package/src/core/project/manifest.ts +3 -3
- package/src/core/project/validation.ts +16 -0
- package/src/core/scaffold/auth-generator.ts +236 -14
- package/src/core/scaffold/connector-functions-generator.ts +4 -2
- package/src/core/scaffold/functions-generator.ts +4 -2
- package/src/core/scaffold/model-parser.ts +9 -3
- package/src/core/scaffold/ui-generator.ts +15 -8
- package/src/types/index.ts +35 -4
- 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,
|
|
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 =
|
|
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).
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -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
|
|
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
|
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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:
|
|
214
|
-
vnetOption:
|
|
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
|
|
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:
|
|
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
|
}
|