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
|
@@ -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,11 +163,11 @@ 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);
|
|
165
|
-
if (
|
|
166
|
-
applyExternalTokenFetchToGeneratedUi(modelInfo);
|
|
169
|
+
if (uiAuthOptions?.authenticatedFetchImport) {
|
|
170
|
+
applyExternalTokenFetchToGeneratedUi(modelInfo, uiAuthOptions.authenticatedFetchImport);
|
|
167
171
|
}
|
|
168
172
|
await updateNavigationMenu(modelInfo);
|
|
169
173
|
}
|
|
@@ -193,7 +197,7 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
193
197
|
}
|
|
194
198
|
}
|
|
195
199
|
|
|
196
|
-
function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo): void {
|
|
200
|
+
function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, importPath = '@/lib/auth/authenticated-fetch'): void {
|
|
197
201
|
const uiRoot = path.join(process.cwd(), 'app', toKebabCase(modelInfo.name));
|
|
198
202
|
if (!fs.existsSync(uiRoot)) return;
|
|
199
203
|
const visit = (dir: string): void => {
|
|
@@ -208,9 +212,9 @@ function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo): void {
|
|
|
208
212
|
/(if \(!res\.ok\) throw new Error\([^\n]+\);)/g,
|
|
209
213
|
`if (res.status === 401) throw new Error('Authentication required');\n $1`
|
|
210
214
|
);
|
|
211
|
-
if (!content.includes(
|
|
215
|
+
if (!content.includes(importPath)) {
|
|
212
216
|
const directive = content.match(/^'use client';\s*\r?\n/)?.[0] ?? '';
|
|
213
|
-
content = `${directive}import { authenticatedFetch } from '
|
|
217
|
+
content = `${directive}import { authenticatedFetch } from '${importPath}';\n${content.slice(directive.length)}`;
|
|
214
218
|
}
|
|
215
219
|
fs.writeFileSync(target, content, 'utf-8');
|
|
216
220
|
}
|
|
@@ -224,9 +228,13 @@ function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo): void {
|
|
|
224
228
|
* - なければ auth.authorization.defaultPolicy に従う
|
|
225
229
|
* - auth 設定がなければ undefined(ガードなし)
|
|
226
230
|
*/
|
|
227
|
-
function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
|
|
231
|
+
export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
|
|
228
232
|
// モデルに明示的な authPolicy がある場合はそれを使用
|
|
229
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
|
+
}
|
|
230
238
|
return modelInfo.authPolicy;
|
|
231
239
|
}
|
|
232
240
|
|
|
@@ -236,11 +244,13 @@ function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefi
|
|
|
236
244
|
}
|
|
237
245
|
|
|
238
246
|
// defaultPolicy が 'authenticated' なら認証のみ(ロール指定なし)のポリシーを返す
|
|
239
|
-
const defaultPolicy = authConfig.authorization?.defaultPolicy ?? 'authenticated';
|
|
247
|
+
const defaultPolicy = authConfig.authorization?.defaultPolicy === 'public' ? 'anonymous' : authConfig.authorization?.defaultPolicy ?? 'authenticated';
|
|
240
248
|
if (defaultPolicy === 'authenticated') {
|
|
241
249
|
return {}; // 空のポリシー = 認証のみ、ロール制限なし
|
|
242
250
|
}
|
|
243
251
|
|
|
252
|
+
if (defaultPolicy !== 'anonymous') return { policy: defaultPolicy };
|
|
253
|
+
|
|
244
254
|
// defaultPolicy が 'anonymous' なら認証不要
|
|
245
255
|
return undefined;
|
|
246
256
|
}
|
|
@@ -330,13 +340,14 @@ async function generateCallFunctionHelper(): Promise<void> {
|
|
|
330
340
|
|
|
331
341
|
// auth 設定がある場合は Authorization ヘッダー転送版を生成
|
|
332
342
|
const authConfig = getAuthConfig();
|
|
333
|
-
const
|
|
343
|
+
const normalizedAuth = normalizeAuthConfig(authConfig);
|
|
344
|
+
const hasAuth = Boolean(normalizedAuth && Object.keys(normalizedAuth.schemes).length > 0);
|
|
334
345
|
let helperCode: string;
|
|
335
346
|
if (hasAuth) {
|
|
336
|
-
const
|
|
337
|
-
helperCode =
|
|
338
|
-
? generateBFFCallFunctionWithSwaAuth()
|
|
339
|
-
: generateBFFCallFunctionWithAuth();
|
|
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();
|
|
340
351
|
} else {
|
|
341
352
|
helperCode = generateBFFCallFunction();
|
|
342
353
|
}
|
|
@@ -365,6 +376,12 @@ async function generateFunctionsCode(
|
|
|
365
376
|
|
|
366
377
|
const modelKebab = toKebabCase(modelInfo.name);
|
|
367
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
|
+
}
|
|
368
385
|
const functionFilePath = path.join(
|
|
369
386
|
process.cwd(),
|
|
370
387
|
functionsDir,
|
|
@@ -384,6 +401,12 @@ async function generateFunctionsCode(
|
|
|
384
401
|
}
|
|
385
402
|
|
|
386
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
|
+
}
|
|
387
410
|
const crudDir = path.join(process.cwd(), functionsDir, "Crud");
|
|
388
411
|
const functionFilePath = path.join(crudDir, `${modelInfo.name}CrudFunctions.cs`);
|
|
389
412
|
fs.mkdirSync(crudDir, { recursive: true });
|
|
@@ -400,6 +423,13 @@ async function generateFunctionsCode(
|
|
|
400
423
|
return;
|
|
401
424
|
}
|
|
402
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
|
+
|
|
403
433
|
const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
|
|
404
434
|
const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
|
|
405
435
|
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
@@ -410,6 +440,20 @@ async function generateFunctionsCode(
|
|
|
410
440
|
console.log(`✅ Created: ${blueprintPath}`);
|
|
411
441
|
}
|
|
412
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
|
+
|
|
413
457
|
/**
|
|
414
458
|
* コネクタモデル用 Azure Functions コード生成
|
|
415
459
|
*/
|
package/src/cli/index.ts
CHANGED
|
@@ -109,6 +109,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
109
109
|
.option("--backend-language <language>", "Azure Functions backend language: typescript | csharp | python")
|
|
110
110
|
.option("--cosmos-db-mode <mode>", "Cosmos DB mode: freetier | serverless")
|
|
111
111
|
.option("--vnet <option>", "Network security: outbound | none")
|
|
112
|
+
.option("--swa-plan <plan>", "Azure Static Web Apps plan: free | standard")
|
|
112
113
|
.action((projectName, options) => {
|
|
113
114
|
initCommand({
|
|
114
115
|
name: projectName || "swallowkit-app",
|
|
@@ -118,6 +119,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
118
119
|
backendLanguage: options.backendLanguage,
|
|
119
120
|
cosmosDbMode: options.cosmosDbMode,
|
|
120
121
|
vnet: options.vnet,
|
|
122
|
+
swaPlan: options.swaPlan,
|
|
121
123
|
});
|
|
122
124
|
});
|
|
123
125
|
|
|
@@ -171,9 +173,11 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
171
173
|
.command("add-auth")
|
|
172
174
|
.description("Add authentication and authorization to the project")
|
|
173
175
|
.option("--provider <provider>", "Auth provider: custom-jwt | swa | external-token | swa-custom | none", "custom-jwt")
|
|
176
|
+
.option("--scheme <name>", "Add a named authentication scheme")
|
|
174
177
|
.action((options) => {
|
|
175
178
|
addAuthCommand({
|
|
176
179
|
provider: options.provider,
|
|
180
|
+
scheme: options.scheme,
|
|
177
181
|
});
|
|
178
182
|
});
|
|
179
183
|
|
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
|
}
|