swallowkit 1.0.0-beta.46 → 1.0.0-beta.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/add-auth.d.ts +3 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +350 -17
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +4 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +104 -13
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +56 -30
- 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 +84 -8
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +5 -1
- 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 +28 -3
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +493 -23
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/connector-functions-generator.d.ts +3 -3
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/connector-functions-generator.js +23 -21
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
- package/dist/core/scaffold/functions-generator.d.ts +2 -2
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +20 -18
- 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/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +13 -1
- package/dist/core/scaffold/nextjs-generator.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 +35 -5
- 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__/__snapshots__/functions-generator.test.ts.snap +20 -20
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +13 -1
- package/src/__tests__/auth.test.ts +107 -0
- package/src/__tests__/config.test.ts +22 -1
- package/src/__tests__/dev.test.ts +27 -0
- package/src/__tests__/init.test.ts +19 -0
- package/src/__tests__/mcp.test.ts +2 -2
- package/src/__tests__/multi-auth.test.ts +133 -0
- package/src/__tests__/nextjs-generator.test.ts +8 -0
- package/src/cli/commands/add-auth.ts +351 -18
- package/src/cli/commands/dev.ts +116 -16
- package/src/cli/commands/init.ts +56 -30
- package/src/cli/commands/scaffold.ts +86 -10
- package/src/cli/index.ts +5 -1
- 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 +500 -24
- package/src/core/scaffold/connector-functions-generator.ts +26 -21
- package/src/core/scaffold/functions-generator.ts +21 -19
- package/src/core/scaffold/model-parser.ts +9 -3
- package/src/core/scaffold/nextjs-generator.ts +13 -1
- package/src/core/scaffold/ui-generator.ts +15 -8
- package/src/types/index.ts +36 -5
- package/src/utils/package-manager.ts +1 -0
package/src/cli/commands/init.ts
CHANGED
|
@@ -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: '
|
|
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.
|
|
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.
|
|
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: '
|
|
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: '
|
|
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 =
|
|
2702
|
-
param staticWebAppName string
|
|
2703
|
-
|
|
2704
|
-
@description('Functions App default hostname for backend API calls')
|
|
2705
|
-
param functionsDefaultHostname string
|
|
2706
|
-
|
|
2707
|
-
@description('Application Insights connection string for SWA')
|
|
2708
|
-
param appInsightsConnectionString string
|
|
2709
|
-
|
|
2710
|
-
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
2711
|
-
name: staticWebAppName
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
|
-
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
2715
|
-
parent: staticWebApp
|
|
2716
|
-
name: 'appsettings'
|
|
2717
|
-
properties: {
|
|
2718
|
-
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
2719
|
-
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
2720
|
-
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
2721
|
-
}
|
|
2722
|
-
}
|
|
2723
|
-
|
|
2724
|
-
output configName string = staticWebAppConfig.name
|
|
2725
|
-
`;
|
|
2751
|
+
const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
|
|
2726
2752
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
|
|
2727
2753
|
|
|
2728
2754
|
// modules/loganalytics.bicep (Shared Log Analytics Workspace)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { spawnSync } from "child_process";
|
|
9
|
-
import { getBackendLanguage, getConnectorDefinition, getAuthConfig, ensureSwallowKitProject } from "../../core/config";
|
|
9
|
+
import { getBackendLanguage, getConnectorDefinition, getAuthConfig, getFullConfig, getNormalizedAuthConfig, ensureSwallowKitProject, normalizeAuthConfig, validateConfig } from "../../core/config";
|
|
10
10
|
import { ModelInfo, parseModelFile, toKebabCase, toPascalCase, toCamelCase } from "../../core/scaffold/model-parser";
|
|
11
11
|
import {
|
|
12
12
|
generateCSharpAzureFunctionsCRUD,
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
AuthConfig,
|
|
43
43
|
} from "../../types";
|
|
44
44
|
import { syncProjectManifest } from "../../core/project/manifest";
|
|
45
|
+
import { generateBFFCallFunctionWithAuth, generateBFFCallFunctionWithMultipleAuth, generateBFFCallFunctionWithSwaAuth, generateNamedAuthRouterCSharp, generateNamedAuthRouterPython, generateNamedAuthRouterTS } from "../../core/scaffold/auth-generator";
|
|
45
46
|
|
|
46
47
|
interface ScaffoldOptions {
|
|
47
48
|
model: string; // モデルファイルのパス(例: "lib/models/todo.ts" or "todo")
|
|
@@ -74,6 +75,9 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
74
75
|
// SwallowKit プロジェクトディレクトリかどうかを検証
|
|
75
76
|
ensureSwallowKitProject("scaffold");
|
|
76
77
|
|
|
78
|
+
const configValidation = validateConfig(getFullConfig());
|
|
79
|
+
if (!configValidation.valid) throw new Error(`Invalid SwallowKit configuration:\n${configValidation.errors.map(error => `- ${error}`).join("\n")}`);
|
|
80
|
+
|
|
77
81
|
console.log("🏗️ SwallowKit Scaffold: Generating CRUD operations...\n");
|
|
78
82
|
|
|
79
83
|
try {
|
|
@@ -159,9 +163,12 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
159
163
|
const uiAuthPolicy = resolveAuthPolicy(modelInfo, uiAuthConfig);
|
|
160
164
|
const uiAuthOptions: UIAuthOptions | undefined =
|
|
161
165
|
uiAuthPolicy && uiAuthConfig && uiAuthConfig.provider !== 'none'
|
|
162
|
-
?
|
|
166
|
+
? createUIAuthOptions(uiAuthPolicy, uiAuthConfig)
|
|
163
167
|
: undefined;
|
|
164
168
|
await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions);
|
|
169
|
+
if (uiAuthOptions?.authenticatedFetchImport) {
|
|
170
|
+
applyExternalTokenFetchToGeneratedUi(modelInfo, uiAuthOptions.authenticatedFetchImport);
|
|
171
|
+
}
|
|
165
172
|
await updateNavigationMenu(modelInfo);
|
|
166
173
|
}
|
|
167
174
|
|
|
@@ -190,15 +197,44 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
190
197
|
}
|
|
191
198
|
}
|
|
192
199
|
|
|
200
|
+
function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, importPath = '@/lib/auth/authenticated-fetch'): void {
|
|
201
|
+
const uiRoot = path.join(process.cwd(), 'app', toKebabCase(modelInfo.name));
|
|
202
|
+
if (!fs.existsSync(uiRoot)) return;
|
|
203
|
+
const visit = (dir: string): void => {
|
|
204
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
205
|
+
const target = path.join(dir, entry.name);
|
|
206
|
+
if (entry.isDirectory()) { visit(target); continue; }
|
|
207
|
+
if (!entry.name.endsWith('.tsx')) continue;
|
|
208
|
+
let content = fs.readFileSync(target, 'utf-8');
|
|
209
|
+
if (!content.includes('fetch(')) continue;
|
|
210
|
+
content = content.replace(/\bfetch\(/g, 'authenticatedFetch(');
|
|
211
|
+
content = content.replace(
|
|
212
|
+
/(if \(!res\.ok\) throw new Error\([^\n]+\);)/g,
|
|
213
|
+
`if (res.status === 401) throw new Error('Authentication required');\n $1`
|
|
214
|
+
);
|
|
215
|
+
if (!content.includes(importPath)) {
|
|
216
|
+
const directive = content.match(/^'use client';\s*\r?\n/)?.[0] ?? '';
|
|
217
|
+
content = `${directive}import { authenticatedFetch } from '${importPath}';\n${content.slice(directive.length)}`;
|
|
218
|
+
}
|
|
219
|
+
fs.writeFileSync(target, content, 'utf-8');
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
visit(uiRoot);
|
|
223
|
+
}
|
|
224
|
+
|
|
193
225
|
/**
|
|
194
226
|
* モデルの authPolicy と auth config から実効的な認可ポリシーを解決
|
|
195
227
|
* - モデルに authPolicy があればそれを使用
|
|
196
228
|
* - なければ auth.authorization.defaultPolicy に従う
|
|
197
229
|
* - auth 設定がなければ undefined(ガードなし)
|
|
198
230
|
*/
|
|
199
|
-
function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
|
|
231
|
+
export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
|
|
200
232
|
// モデルに明示的な authPolicy がある場合はそれを使用
|
|
201
233
|
if (modelInfo.authPolicy) {
|
|
234
|
+
const normalized = normalizeAuthConfig(authConfig);
|
|
235
|
+
for (const policyName of [modelInfo.authPolicy.policy, typeof modelInfo.authPolicy.read === 'string' ? modelInfo.authPolicy.read : undefined, typeof modelInfo.authPolicy.write === 'string' ? modelInfo.authPolicy.write : undefined].filter((value): value is string => Boolean(value))) {
|
|
236
|
+
if (!normalized?.authorization.policies[policyName]) throw new Error(`Model '${modelInfo.name}' references undefined auth policy '${policyName}'. Define auth.authorization.policies.${policyName}.`);
|
|
237
|
+
}
|
|
202
238
|
return modelInfo.authPolicy;
|
|
203
239
|
}
|
|
204
240
|
|
|
@@ -208,11 +244,13 @@ function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefi
|
|
|
208
244
|
}
|
|
209
245
|
|
|
210
246
|
// defaultPolicy が 'authenticated' なら認証のみ(ロール指定なし)のポリシーを返す
|
|
211
|
-
const defaultPolicy = authConfig.authorization?.defaultPolicy ?? 'authenticated';
|
|
247
|
+
const defaultPolicy = authConfig.authorization?.defaultPolicy === 'public' ? 'anonymous' : authConfig.authorization?.defaultPolicy ?? 'authenticated';
|
|
212
248
|
if (defaultPolicy === 'authenticated') {
|
|
213
249
|
return {}; // 空のポリシー = 認証のみ、ロール制限なし
|
|
214
250
|
}
|
|
215
251
|
|
|
252
|
+
if (defaultPolicy !== 'anonymous') return { policy: defaultPolicy };
|
|
253
|
+
|
|
216
254
|
// defaultPolicy が 'anonymous' なら認証不要
|
|
217
255
|
return undefined;
|
|
218
256
|
}
|
|
@@ -302,11 +340,14 @@ async function generateCallFunctionHelper(): Promise<void> {
|
|
|
302
340
|
|
|
303
341
|
// auth 設定がある場合は Authorization ヘッダー転送版を生成
|
|
304
342
|
const authConfig = getAuthConfig();
|
|
305
|
-
const
|
|
343
|
+
const normalizedAuth = normalizeAuthConfig(authConfig);
|
|
344
|
+
const hasAuth = Boolean(normalizedAuth && Object.keys(normalizedAuth.schemes).length > 0);
|
|
306
345
|
let helperCode: string;
|
|
307
346
|
if (hasAuth) {
|
|
308
|
-
const
|
|
309
|
-
helperCode =
|
|
347
|
+
const providers = new Set(Object.values(normalizedAuth!.schemes).map(scheme => scheme.provider));
|
|
348
|
+
helperCode = normalizedAuth!.schemes.default && Object.keys(normalizedAuth!.schemes).length === 1
|
|
349
|
+
? authConfig?.provider === "swa" ? generateBFFCallFunctionWithSwaAuth() : generateBFFCallFunctionWithAuth()
|
|
350
|
+
: providers.has("swa") ? generateBFFCallFunctionWithMultipleAuth() : generateBFFCallFunctionWithAuth();
|
|
310
351
|
} else {
|
|
311
352
|
helperCode = generateBFFCallFunction();
|
|
312
353
|
}
|
|
@@ -335,6 +376,12 @@ async function generateFunctionsCode(
|
|
|
335
376
|
|
|
336
377
|
const modelKebab = toKebabCase(modelInfo.name);
|
|
337
378
|
if (backendLanguage === "typescript") {
|
|
379
|
+
const normalizedAuth = getNormalizedAuthConfig();
|
|
380
|
+
if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
|
|
381
|
+
const routerPath = path.join(process.cwd(), functionsDir, "src", "auth", "auth-router.ts");
|
|
382
|
+
fs.mkdirSync(path.dirname(routerPath), { recursive: true });
|
|
383
|
+
fs.writeFileSync(routerPath, generateNamedAuthRouterTS(normalizedAuth), "utf-8");
|
|
384
|
+
}
|
|
338
385
|
const functionFilePath = path.join(
|
|
339
386
|
process.cwd(),
|
|
340
387
|
functionsDir,
|
|
@@ -347,13 +394,19 @@ async function generateFunctionsCode(
|
|
|
347
394
|
fs.mkdirSync(functionDir, { recursive: true });
|
|
348
395
|
}
|
|
349
396
|
|
|
350
|
-
const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy);
|
|
397
|
+
const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy, authConfig?.provider);
|
|
351
398
|
fs.writeFileSync(functionFilePath, code, "utf-8");
|
|
352
399
|
console.log(`✅ Created: ${functionFilePath}`);
|
|
353
400
|
return;
|
|
354
401
|
}
|
|
355
402
|
|
|
356
403
|
if (backendLanguage === "csharp") {
|
|
404
|
+
const normalizedAuth = getNormalizedAuthConfig();
|
|
405
|
+
if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
|
|
406
|
+
const routerPath = path.join(process.cwd(), functionsDir, "Auth", "AuthRouter.cs");
|
|
407
|
+
fs.mkdirSync(path.dirname(routerPath), { recursive: true });
|
|
408
|
+
fs.writeFileSync(routerPath, generateNamedAuthRouterCSharp(normalizedAuth), "utf-8");
|
|
409
|
+
}
|
|
357
410
|
const crudDir = path.join(process.cwd(), functionsDir, "Crud");
|
|
358
411
|
const functionFilePath = path.join(crudDir, `${modelInfo.name}CrudFunctions.cs`);
|
|
359
412
|
fs.mkdirSync(crudDir, { recursive: true });
|
|
@@ -370,6 +423,13 @@ async function generateFunctionsCode(
|
|
|
370
423
|
return;
|
|
371
424
|
}
|
|
372
425
|
|
|
426
|
+
const normalizedAuth = getNormalizedAuthConfig();
|
|
427
|
+
if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
|
|
428
|
+
const routerPath = path.join(process.cwd(), functionsDir, "auth", "auth_router.py");
|
|
429
|
+
fs.mkdirSync(path.dirname(routerPath), { recursive: true });
|
|
430
|
+
fs.writeFileSync(routerPath, generateNamedAuthRouterPython(normalizedAuth), "utf-8");
|
|
431
|
+
}
|
|
432
|
+
|
|
373
433
|
const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
|
|
374
434
|
const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
|
|
375
435
|
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
@@ -380,6 +440,20 @@ async function generateFunctionsCode(
|
|
|
380
440
|
console.log(`✅ Created: ${blueprintPath}`);
|
|
381
441
|
}
|
|
382
442
|
|
|
443
|
+
function createUIAuthOptions(policy: ModelAuthPolicy, authConfig: AuthConfig): UIAuthOptions {
|
|
444
|
+
const normalized = normalizeAuthConfig(authConfig);
|
|
445
|
+
const readPolicyName = typeof policy.read === 'string' ? policy.read : policy.policy;
|
|
446
|
+
const writePolicyName = typeof policy.write === 'string' ? policy.write : policy.policy;
|
|
447
|
+
const readNamed = readPolicyName ? normalized?.authorization.policies[readPolicyName] : undefined;
|
|
448
|
+
const named = writePolicyName ? normalized?.authorization.policies[writePolicyName] : undefined;
|
|
449
|
+
const writeRoles = named?.roles ?? (Array.isArray(policy.write) ? policy.write : policy.roles) ?? [];
|
|
450
|
+
const externalScheme = [...(readNamed?.schemes ?? []), ...(named?.schemes ?? [])].find(name => normalized?.schemes[name]?.provider === 'external-token');
|
|
451
|
+
const authScheme = readNamed?.schemes[0] ?? named?.schemes[0];
|
|
452
|
+
const authenticatedFetchImport = externalScheme ? `@/lib/auth/schemes/${toKebabCase(externalScheme)}/authenticated-fetch` : authConfig.provider === 'external-token' ? '@/lib/auth/authenticated-fetch' : undefined;
|
|
453
|
+
const authContextImport = authScheme ? `@/lib/auth/schemes/${toKebabCase(authScheme)}/auth-context` : '@/lib/auth/auth-context';
|
|
454
|
+
return { authPolicy: policy, writeRoles, authenticatedFetchImport, authContextImport };
|
|
455
|
+
}
|
|
456
|
+
|
|
383
457
|
/**
|
|
384
458
|
* コネクタモデル用 Azure Functions コード生成
|
|
385
459
|
*/
|
|
@@ -425,14 +499,16 @@ async function generateConnectorFunctionsCode(
|
|
|
425
499
|
modelInfo, sharedPackageName,
|
|
426
500
|
connectorDef as RdbConnectorConfig,
|
|
427
501
|
connectorConfig as RdbModelConnectorConfig,
|
|
428
|
-
authPolicy
|
|
502
|
+
authPolicy,
|
|
503
|
+
authConfig?.provider
|
|
429
504
|
);
|
|
430
505
|
} else {
|
|
431
506
|
code = generateApiConnectorFunctionTS(
|
|
432
507
|
modelInfo, sharedPackageName,
|
|
433
508
|
connectorDef as ApiConnectorConfig,
|
|
434
509
|
connectorConfig as ApiModelConnectorConfig,
|
|
435
|
-
authPolicy
|
|
510
|
+
authPolicy,
|
|
511
|
+
authConfig?.provider
|
|
436
512
|
);
|
|
437
513
|
}
|
|
438
514
|
|
package/src/cli/index.ts
CHANGED
|
@@ -26,6 +26,8 @@ const DEV_OPTION_ARITY = new Map<string, 0 | 1>([
|
|
|
26
26
|
["--verbose", 0],
|
|
27
27
|
["--no-functions", 0],
|
|
28
28
|
["--mock-connectors", 0],
|
|
29
|
+
["--swa-port", 1],
|
|
30
|
+
["--no-swa", 0],
|
|
29
31
|
]);
|
|
30
32
|
|
|
31
33
|
function ensureUtf8ConsoleOnWindows(): void {
|
|
@@ -168,10 +170,12 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
168
170
|
program
|
|
169
171
|
.command("add-auth")
|
|
170
172
|
.description("Add authentication and authorization to the project")
|
|
171
|
-
.option("--provider <provider>", "Auth provider: custom-jwt | swa | swa-custom | none", "custom-jwt")
|
|
173
|
+
.option("--provider <provider>", "Auth provider: custom-jwt | swa | external-token | swa-custom | none", "custom-jwt")
|
|
174
|
+
.option("--scheme <name>", "Add a named authentication scheme")
|
|
172
175
|
.action((options) => {
|
|
173
176
|
addAuthCommand({
|
|
174
177
|
provider: options.provider,
|
|
178
|
+
scheme: options.scheme,
|
|
175
179
|
});
|
|
176
180
|
});
|
|
177
181
|
|
package/src/core/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import { createRequire } from "module";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import { AuthConfig, BackendLanguage, ConnectorDefinition, SwallowKitConfig } from "../types";
|
|
4
|
+
import { AuthConfig, AuthProvider, BackendLanguage, ConnectorDefinition, NormalizedAuthConfig, SwallowKitConfig } from "../types";
|
|
5
5
|
import { detectFromProject, getCommands } from "../utils/package-manager";
|
|
6
6
|
|
|
7
7
|
const requireFromHere = createRequire(__filename);
|
|
@@ -37,7 +37,7 @@ const DEFAULT_CONFIG: SwallowKitConfig = {
|
|
|
37
37
|
/**
|
|
38
38
|
* 設定ファイルを読み込み
|
|
39
39
|
*/
|
|
40
|
-
export function loadConfig(configPath?: string): SwallowKitConfig {
|
|
40
|
+
export function loadConfig(configPath?: string, throwOnError = false): SwallowKitConfig {
|
|
41
41
|
const defaultPaths = [
|
|
42
42
|
"swallowkit.config.json",
|
|
43
43
|
"swallowkit.config.js",
|
|
@@ -63,6 +63,7 @@ export function loadConfig(configPath?: string): SwallowKitConfig {
|
|
|
63
63
|
return mergeConfig(DEFAULT_CONFIG, unwrapConfigModule(loadedConfig));
|
|
64
64
|
}
|
|
65
65
|
} catch (error) {
|
|
66
|
+
if (throwOnError) throw error;
|
|
66
67
|
console.warn(`⚠️ 設定ファイルの読み込みに失敗: ${filePath}`, error);
|
|
67
68
|
}
|
|
68
69
|
}
|
|
@@ -200,6 +201,40 @@ export function getAuthConfig(configPath?: string): AuthConfig | undefined {
|
|
|
200
201
|
return config.auth;
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
/** Load configuration for mutating/generation commands and fail instead of silently falling back. */
|
|
205
|
+
export function getValidatedFullConfig(configPath?: string): SwallowKitConfig {
|
|
206
|
+
const config = mergeConfig(loadConfig(configPath, true), loadConfigFromEnv());
|
|
207
|
+
const validation = validateConfig(config);
|
|
208
|
+
if (!validation.valid) throw new Error(`Invalid SwallowKit configuration:\n${validation.errors.map(error => `- ${error}`).join("\n")}`);
|
|
209
|
+
return config;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const VALID_AUTH_PROVIDERS: AuthProvider[] = ["custom-jwt", "swa", "external-token", "swa-custom", "none"];
|
|
213
|
+
const AUTH_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
214
|
+
|
|
215
|
+
/** Convert the legacy single-provider shape to the canonical named-scheme shape. */
|
|
216
|
+
export function normalizeAuthConfig(auth?: AuthConfig): NormalizedAuthConfig | undefined {
|
|
217
|
+
if (!auth) return undefined;
|
|
218
|
+
const schemes = { ...(auth.schemes ?? {}) };
|
|
219
|
+
if (auth.provider && auth.provider !== "none" && !schemes.default) {
|
|
220
|
+
schemes.default = { provider: auth.provider, customJwt: auth.customJwt, swa: auth.swa };
|
|
221
|
+
}
|
|
222
|
+
const rawDefault = auth.authorization?.defaultPolicy ?? (Object.keys(schemes).length ? "authenticated" : "anonymous");
|
|
223
|
+
return {
|
|
224
|
+
...auth,
|
|
225
|
+
schemes,
|
|
226
|
+
authorization: {
|
|
227
|
+
...auth.authorization,
|
|
228
|
+
defaultPolicy: rawDefault === "public" ? "anonymous" : rawDefault,
|
|
229
|
+
policies: { ...(auth.authorization?.policies ?? {}) },
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function getNormalizedAuthConfig(configPath?: string): NormalizedAuthConfig | undefined {
|
|
235
|
+
return normalizeAuthConfig(getAuthConfig(configPath));
|
|
236
|
+
}
|
|
237
|
+
|
|
203
238
|
/**
|
|
204
239
|
* 設定の検証
|
|
205
240
|
*/
|
|
@@ -214,6 +249,38 @@ function hasConfiguredDatabase(config: SwallowKitConfig): boolean {
|
|
|
214
249
|
export function validateConfig(config: SwallowKitConfig): { valid: boolean; errors: string[] } {
|
|
215
250
|
const errors: string[] = [];
|
|
216
251
|
|
|
252
|
+
const auth = normalizeAuthConfig(config.auth);
|
|
253
|
+
if (auth) {
|
|
254
|
+
for (const [name, scheme] of Object.entries(auth.schemes)) {
|
|
255
|
+
const base = `auth.schemes.${name}`;
|
|
256
|
+
if (!AUTH_NAME.test(name)) errors.push(`${base}: scheme name must match ${AUTH_NAME}; rename the scheme`);
|
|
257
|
+
if (!VALID_AUTH_PROVIDERS.includes(scheme.provider)) errors.push(`${base}.provider: unsupported provider '${scheme.provider}'; use ${VALID_AUTH_PROVIDERS.join(", ")}`);
|
|
258
|
+
if (scheme.provider === "none" || scheme.provider === "swa-custom") errors.push(`${base}.provider: '${scheme.provider}' cannot be used as a named scheme; use swa, external-token, or custom-jwt`);
|
|
259
|
+
if (scheme.provider === "custom-jwt") {
|
|
260
|
+
const jwt = scheme.customJwt ?? (name === "default" ? auth.customJwt : undefined);
|
|
261
|
+
for (const key of ["userConnector", "userTable", "loginIdColumn", "passwordHashColumn", "rolesColumn"] as const) {
|
|
262
|
+
if (!jwt?.[key]) errors.push(`${base}.customJwt.${key}: required for custom-jwt; configure this value`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const [name, policy] of Object.entries(auth.authorization.policies)) {
|
|
267
|
+
const base = `auth.authorization.policies.${name}`;
|
|
268
|
+
if (!AUTH_NAME.test(name)) errors.push(`${base}: policy name must match ${AUTH_NAME}; rename the policy`);
|
|
269
|
+
if (!Array.isArray(policy.schemes) || policy.schemes.length === 0) errors.push(`${base}.schemes: provide at least one named scheme`);
|
|
270
|
+
for (const scheme of policy.schemes ?? []) if (!auth.schemes[scheme]) errors.push(`${base}.schemes: undefined scheme '${scheme}'; define auth.schemes.${scheme}`);
|
|
271
|
+
const sources = new Map<string, string>();
|
|
272
|
+
for (const schemeName of policy.schemes ?? []) {
|
|
273
|
+
const provider = auth.schemes[schemeName]?.provider;
|
|
274
|
+
const source = provider === "swa" ? "x-ms-client-principal" : provider === "custom-jwt" || provider === "external-token" ? "authorization-bearer" : provider;
|
|
275
|
+
if (source && sources.has(source)) errors.push(`${base}.schemes: '${sources.get(source)}' and '${schemeName}' both use ${source}; split the policy so credential selection is deterministic`);
|
|
276
|
+
else if (source) sources.set(source, schemeName);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const defaultPolicy = auth.authorization.defaultPolicy;
|
|
280
|
+
if (defaultPolicy === "authenticated" && !auth.schemes.default && Object.keys(auth.schemes).length !== 1) errors.push("auth.authorization.defaultPolicy: 'authenticated' requires auth.schemes.default (or exactly one scheme); set a default scheme or use a named policy");
|
|
281
|
+
else if (!["anonymous", "authenticated"].includes(defaultPolicy) && !auth.authorization.policies[defaultPolicy]) errors.push(`auth.authorization.defaultPolicy: undefined policy '${defaultPolicy}'; define auth.authorization.policies.${defaultPolicy}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
217
284
|
// データベース設定の検証
|
|
218
285
|
if (hasConfiguredDatabase(config) && !config.database?.connectionString) {
|
|
219
286
|
errors.push("Cosmos DB connection string is required");
|
|
@@ -503,9 +503,9 @@ export class ConnectorMockServer {
|
|
|
503
503
|
if (!policy) return undefined; // defaultPolicy: authenticated → 認証のみ、ロールチェック不要
|
|
504
504
|
|
|
505
505
|
if (isWrite) {
|
|
506
|
-
return policy.write
|
|
506
|
+
return Array.isArray(policy.write) ? policy.write : policy.roles;
|
|
507
507
|
}
|
|
508
|
-
return policy.read
|
|
508
|
+
return Array.isArray(policy.read) ? policy.read : policy.roles;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
private readBody(req: http.IncomingMessage, callback: (body: MockDocument) => void) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { getBackendLanguage, getFullConfig, validateConfig } from "../config";
|
|
3
|
+
import { getBackendLanguage, getFullConfig, normalizeAuthConfig, validateConfig } from "../config";
|
|
4
4
|
import { getAllModels, toCamelCase, toKebabCase } from "../scaffold/model-parser";
|
|
5
5
|
import { AuthConfig, BackendLanguage, ConnectorDefinition, ModelAuthPolicy, ModelConnectorConfig } from "../../types";
|
|
6
6
|
import { captureConsoleMessages, withWorkingDirectory } from "../operations/runtime";
|
|
@@ -340,7 +340,7 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
|
|
|
340
340
|
connectors: Object.entries(config.connectors || {})
|
|
341
341
|
.map(([name, definition]) => ({ name, definition }))
|
|
342
342
|
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
343
|
-
auth: config.auth || null,
|
|
343
|
+
auth: normalizeAuthConfig(config.auth) || null,
|
|
344
344
|
entities: entities.sort((left, right) => left.name.localeCompare(right.name)),
|
|
345
345
|
routes,
|
|
346
346
|
modules: buildModules(projectRoot),
|
|
@@ -356,7 +356,7 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
|
|
|
356
356
|
],
|
|
357
357
|
hasSharedWorkspace: fs.existsSync(path.join(projectRoot, "shared", "package.json")),
|
|
358
358
|
hasConnectors: Object.keys(config.connectors || {}).length > 0,
|
|
359
|
-
hasAuth: Boolean(config.auth && config.auth.provider !== "none"),
|
|
359
|
+
hasAuth: Boolean(config.auth && (config.auth.provider !== "none" || Object.keys(config.auth.schemes ?? {}).length > 0)),
|
|
360
360
|
},
|
|
361
361
|
};
|
|
362
362
|
|
|
@@ -209,6 +209,22 @@ export async function validateProject(projectRoot: string = process.cwd()): Prom
|
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
const namedPolicies = loaded.manifest.auth?.authorization?.policies ?? {};
|
|
213
|
+
for (const entity of loaded.manifest.entities) {
|
|
214
|
+
const policy = entity.authPolicy;
|
|
215
|
+
if (!policy) continue;
|
|
216
|
+
const references = [policy.policy, typeof policy.read === "string" ? policy.read : undefined, typeof policy.write === "string" ? policy.write : undefined].filter((value): value is string => Boolean(value));
|
|
217
|
+
for (const name of references) {
|
|
218
|
+
if (!namedPolicies[name]) pushViolation(violations, {
|
|
219
|
+
code: "undefined-auth-policy",
|
|
220
|
+
severity: "error",
|
|
221
|
+
message: `Model '${entity.name}' references undefined authorization policy '${name}'.`,
|
|
222
|
+
location: { path: entity.filePath, entity: entity.name },
|
|
223
|
+
suggestedFix: `Define auth.authorization.policies.${name} or update the model authPolicy reference.`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
212
228
|
for (const entity of loaded.manifest.entities) {
|
|
213
229
|
validateSchemaNaming(entity, violations);
|
|
214
230
|
}
|