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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/cli/commands/add-auth.d.ts +1 -0
  2. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  3. package/dist/cli/commands/add-auth.js +136 -1
  4. package/dist/cli/commands/add-auth.js.map +1 -1
  5. package/dist/cli/commands/dev.js +5 -2
  6. package/dist/cli/commands/dev.js.map +1 -1
  7. package/dist/cli/commands/init.d.ts +3 -0
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +68 -27
  10. package/dist/cli/commands/init.js.map +1 -1
  11. package/dist/cli/commands/scaffold.d.ts +9 -0
  12. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  13. package/dist/cli/commands/scaffold.js +56 -12
  14. package/dist/cli/commands/scaffold.js.map +1 -1
  15. package/dist/cli/index.d.ts.map +1 -1
  16. package/dist/cli/index.js +4 -0
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/core/config.d.ts +7 -2
  19. package/dist/core/config.d.ts.map +1 -1
  20. package/dist/core/config.js +81 -1
  21. package/dist/core/config.js.map +1 -1
  22. package/dist/core/mock/connector-mock-server.js +2 -2
  23. package/dist/core/mock/connector-mock-server.js.map +1 -1
  24. package/dist/core/project/manifest.js +2 -2
  25. package/dist/core/project/manifest.js.map +1 -1
  26. package/dist/core/project/validation.d.ts.map +1 -1
  27. package/dist/core/project/validation.js +17 -0
  28. package/dist/core/project/validation.js.map +1 -1
  29. package/dist/core/scaffold/auth-generator.d.ts +9 -2
  30. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  31. package/dist/core/scaffold/auth-generator.js +230 -13
  32. package/dist/core/scaffold/auth-generator.js.map +1 -1
  33. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
  34. package/dist/core/scaffold/connector-functions-generator.js +4 -2
  35. package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
  36. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  37. package/dist/core/scaffold/functions-generator.js +4 -2
  38. package/dist/core/scaffold/functions-generator.js.map +1 -1
  39. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  40. package/dist/core/scaffold/model-parser.js +8 -3
  41. package/dist/core/scaffold/model-parser.js.map +1 -1
  42. package/dist/core/scaffold/ui-generator.d.ts +3 -0
  43. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  44. package/dist/core/scaffold/ui-generator.js +12 -8
  45. package/dist/core/scaffold/ui-generator.js.map +1 -1
  46. package/dist/types/index.d.ts +34 -4
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/utils/package-manager.d.ts.map +1 -1
  49. package/dist/utils/package-manager.js +1 -0
  50. package/dist/utils/package-manager.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/__tests__/auth.test.ts +4 -0
  53. package/src/__tests__/config.test.ts +22 -1
  54. package/src/__tests__/dev.test.ts +8 -0
  55. package/src/__tests__/init.test.ts +8 -0
  56. package/src/__tests__/mcp.test.ts +2 -2
  57. package/src/__tests__/multi-auth.test.ts +133 -0
  58. package/src/cli/commands/add-auth.ts +112 -2
  59. package/src/cli/commands/dev.ts +6 -3
  60. package/src/cli/commands/init.ts +73 -28
  61. package/src/cli/commands/scaffold.ts +58 -14
  62. package/src/cli/index.ts +4 -0
  63. package/src/core/config.ts +69 -2
  64. package/src/core/mock/connector-mock-server.ts +2 -2
  65. package/src/core/project/manifest.ts +3 -3
  66. package/src/core/project/validation.ts +16 -0
  67. package/src/core/scaffold/auth-generator.ts +236 -14
  68. package/src/core/scaffold/connector-functions-generator.ts +4 -2
  69. package/src/core/scaffold/functions-generator.ts +4 -2
  70. package/src/core/scaffold/model-parser.ts +9 -3
  71. package/src/core/scaffold/ui-generator.ts +15 -8
  72. package/src/types/index.ts +35 -4
  73. package/src/utils/package-manager.ts +1 -0
@@ -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
- ? { authPolicy: uiAuthPolicy }
166
+ ? createUIAuthOptions(uiAuthPolicy, uiAuthConfig)
163
167
  : undefined;
164
168
  await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions);
165
- if (uiAuthConfig?.provider === 'external-token') {
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("@/lib/auth/authenticated-fetch")) {
215
+ if (!content.includes(importPath)) {
212
216
  const directive = content.match(/^'use client';\s*\r?\n/)?.[0] ?? '';
213
- content = `${directive}import { authenticatedFetch } from '@/lib/auth/authenticated-fetch';\n${content.slice(directive.length)}`;
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 hasAuth = authConfig && authConfig.provider !== 'none';
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 { generateBFFCallFunctionWithAuth, generateBFFCallFunctionWithSwaAuth } = await import("../../core/scaffold/auth-generator");
337
- helperCode = authConfig?.provider === "swa"
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
 
@@ -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 || policy.roles;
506
+ return Array.isArray(policy.write) ? policy.write : policy.roles;
507
507
  }
508
- return policy.read || policy.roles;
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
  }