swallowkit 1.0.0-beta.47 → 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.
Files changed (66) 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/scaffold.d.ts +9 -0
  8. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  9. package/dist/cli/commands/scaffold.js +56 -12
  10. package/dist/cli/commands/scaffold.js.map +1 -1
  11. package/dist/cli/index.d.ts.map +1 -1
  12. package/dist/cli/index.js +2 -0
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/core/config.d.ts +7 -2
  15. package/dist/core/config.d.ts.map +1 -1
  16. package/dist/core/config.js +81 -1
  17. package/dist/core/config.js.map +1 -1
  18. package/dist/core/mock/connector-mock-server.js +2 -2
  19. package/dist/core/mock/connector-mock-server.js.map +1 -1
  20. package/dist/core/project/manifest.js +2 -2
  21. package/dist/core/project/manifest.js.map +1 -1
  22. package/dist/core/project/validation.d.ts.map +1 -1
  23. package/dist/core/project/validation.js +17 -0
  24. package/dist/core/project/validation.js.map +1 -1
  25. package/dist/core/scaffold/auth-generator.d.ts +9 -2
  26. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  27. package/dist/core/scaffold/auth-generator.js +230 -13
  28. package/dist/core/scaffold/auth-generator.js.map +1 -1
  29. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
  30. package/dist/core/scaffold/connector-functions-generator.js +4 -2
  31. package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
  32. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  33. package/dist/core/scaffold/functions-generator.js +4 -2
  34. package/dist/core/scaffold/functions-generator.js.map +1 -1
  35. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  36. package/dist/core/scaffold/model-parser.js +8 -3
  37. package/dist/core/scaffold/model-parser.js.map +1 -1
  38. package/dist/core/scaffold/ui-generator.d.ts +3 -0
  39. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  40. package/dist/core/scaffold/ui-generator.js +12 -8
  41. package/dist/core/scaffold/ui-generator.js.map +1 -1
  42. package/dist/types/index.d.ts +34 -4
  43. package/dist/types/index.d.ts.map +1 -1
  44. package/dist/utils/package-manager.d.ts.map +1 -1
  45. package/dist/utils/package-manager.js +1 -0
  46. package/dist/utils/package-manager.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/__tests__/auth.test.ts +4 -0
  49. package/src/__tests__/config.test.ts +22 -1
  50. package/src/__tests__/mcp.test.ts +2 -2
  51. package/src/__tests__/multi-auth.test.ts +133 -0
  52. package/src/cli/commands/add-auth.ts +112 -2
  53. package/src/cli/commands/dev.ts +6 -3
  54. package/src/cli/commands/scaffold.ts +58 -14
  55. package/src/cli/index.ts +2 -0
  56. package/src/core/config.ts +69 -2
  57. package/src/core/mock/connector-mock-server.ts +2 -2
  58. package/src/core/project/manifest.ts +3 -3
  59. package/src/core/project/validation.ts +16 -0
  60. package/src/core/scaffold/auth-generator.ts +236 -14
  61. package/src/core/scaffold/connector-functions-generator.ts +4 -2
  62. package/src/core/scaffold/functions-generator.ts +4 -2
  63. package/src/core/scaffold/model-parser.ts +9 -3
  64. package/src/core/scaffold/ui-generator.ts +15 -8
  65. package/src/types/index.ts +35 -4
  66. package/src/utils/package-manager.ts +1 -0
@@ -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
  }
@@ -3,10 +3,212 @@
3
3
  * add-auth コマンドおよび scaffold ロールガード挿入で使用するテンプレート群
4
4
  */
5
5
 
6
- import { AuthProvider, CustomJwtConfig, ModelAuthPolicy, RdbConnectorConfig } from "../../types";
6
+ import { AuthProvider, CustomJwtConfig, ModelAuthPolicy, NormalizedAuthConfig, RdbConnectorConfig } from "../../types";
7
7
 
8
8
  type RdbProvider = RdbConnectorConfig["provider"]; // "mysql" | "postgres" | "sqlserver"
9
9
 
10
+ export function generateNamedAuthRouterTS(config: NormalizedAuthConfig): string {
11
+ const external = Object.entries(config.schemes).filter(([, s]) => s.provider === "external-token");
12
+ const custom = Object.entries(config.schemes).filter(([, s]) => s.provider === "custom-jwt");
13
+ const imports = external.map(([name]) => {
14
+ const slug = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
15
+ const id = safeIdentifier(name);
16
+ return `import { verifyExternalToken as verify_${id}, ExternalTokenVerificationError as Error_${id} } from './schemes/${slug}/verifier';`;
17
+ }).concat(custom.map(([name]) => {
18
+ const slug = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
19
+ return `import { requireAuth as verify_${safeIdentifier(name)} } from './schemes/${slug}/jwt-helper';`;
20
+ })).join("\n");
21
+ const verifiers = external.map(([name]) => { const id = safeIdentifier(name); return `${JSON.stringify(name)}: async (token: string) => { try { return await verify_${id}(token); } catch (e) { if (e instanceof Error_${id}) throw new AuthError(e.status, e.message); throw e; } }`; }).join(",\n ");
22
+ return `import type { HttpRequest, HttpResponseInit } from '@azure/functions';
23
+ ${imports}
24
+ export type AuthPrincipal = { subject: string; scheme: string; issuer: string; roles: string[]; claims: Record<string, unknown>; /** @deprecated */ userId?: string; /** @deprecated */ userDetails?: string };
25
+ export class AuthError extends Error { constructor(public status: 401 | 403 | 503, message: string) { super(message); } }
26
+ const schemes = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n,s]) => [n,{provider:s.provider,allowedProviders:s.swa?.allowedProviders??[]}])))} as const;
27
+ const policies = ${JSON.stringify(config.authorization.policies)} as Record<string, { schemes: string[]; roles?: string[] }>;
28
+ const externalVerifiers: Record<string, (token: string) => Promise<any>> = { ${verifiers} };
29
+ const customVerifiers: Record<string, (request: HttpRequest) => any> = { ${custom.map(([name]) => `${JSON.stringify(name)}: verify_${safeIdentifier(name)}`).join(", ")} };
30
+ function swa(request: HttpRequest, scheme: string): AuthPrincipal {
31
+ const encoded = request.headers.get('x-ms-client-principal');
32
+ if (!encoded) throw new AuthError(401, 'Authentication required');
33
+ try { const value = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8')) as Record<string, unknown>;
34
+ const roles = Array.isArray(value.userRoles) ? value.userRoles.filter((r): r is string => typeof r === 'string') : [];
35
+ if (typeof value.userId !== 'string' || !value.userId || !roles.includes('authenticated')) throw new Error();
36
+ const allowed = (schemes as any)[scheme]?.allowedProviders as string[] | undefined;
37
+ if (allowed?.length && (typeof value.identityProvider !== 'string' || !allowed.includes(value.identityProvider))) throw new Error();
38
+ return { subject: value.userId, scheme, issuer: typeof value.identityProvider === 'string' ? 'swa:' + value.identityProvider : 'swa', roles, claims: value, userId: value.userId, userDetails: typeof value.userDetails === 'string' ? value.userDetails : undefined };
39
+ } catch (e) { if (e instanceof AuthError) throw e; throw new AuthError(401, 'Invalid SWA client principal'); }
40
+ }
41
+ export async function requireAuth(request: HttpRequest, policyName: string): Promise<AuthPrincipal> {
42
+ const policy = policies[policyName]; if (!policy) throw new AuthError(401, 'Undefined authorization policy');
43
+ const bearer = request.headers.get('authorization'); const hasSwa = Boolean(request.headers.get('x-ms-client-principal'));
44
+ const candidates = policy.schemes.filter(name => { const p=(schemes as any)[name]?.provider; return p === 'swa' ? hasSwa : (p === 'external-token' || p === 'custom-jwt') ? Boolean(bearer?.startsWith('Bearer ')) : false; });
45
+ if (candidates.length !== 1) throw new AuthError(401, candidates.length ? 'Ambiguous credentials' : 'Authentication required');
46
+ const scheme = candidates[0]; const provider = (schemes as any)[scheme]?.provider; let principal: AuthPrincipal;
47
+ if (provider === 'swa') principal = swa(request, scheme);
48
+ else if (provider === 'external-token') { const token=bearer!.slice(7); try { const p=await externalVerifiers[scheme](token); const subject=p?.subject ?? p?.userId; if (typeof subject !== 'string' || !subject || !Array.isArray(p.roles)) throw new AuthError(401,'Invalid external principal'); principal={subject,scheme,issuer:typeof p.issuer==='string'?p.issuer:scheme,roles:p.roles,claims:p.claims && typeof p.claims==='object'?p.claims:p,userId:subject,userDetails:p.userDetails}; } catch(e) { if(e instanceof AuthError) throw e; throw new AuthError(503,'Identity provider unavailable'); } }
49
+ else if (provider === 'custom-jwt') { try { const p=customVerifiers[scheme](request); const subject=p?.sub ?? p?.userId; if(typeof subject !== 'string' || !subject || !Array.isArray(p.roles)) throw new Error(); principal={subject,scheme,issuer:scheme,roles:p.roles,claims:p,userId:subject,userDetails:p.loginId}; } catch { throw new AuthError(401,'Invalid or expired token'); } }
50
+ else throw new AuthError(401, 'Authentication adapter is not configured');
51
+ requireRoles(principal, policy.roles || []); return principal;
52
+ }
53
+ export function requireRoles(principal: AuthPrincipal, roles: string[]): void { if (roles.length && !roles.some(r => principal.roles.includes(r))) throw new AuthError(403, 'Insufficient permissions'); }
54
+ export function identityKey(principal: AuthPrincipal): string { return principal.scheme + ':' + principal.subject; }
55
+ export function handleAuthError(error: unknown): HttpResponseInit | null { return error instanceof AuthError ? { status:error.status, jsonBody:{error:error.message} } : null; }
56
+ `;
57
+ }
58
+
59
+ function safeIdentifier(name: string): string {
60
+ return name.replace(/[^A-Za-z0-9_]/g, "_");
61
+ }
62
+
63
+ function schemeSlug(name: string): string {
64
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
65
+ }
66
+
67
+ export function generateNamedExternalTokenVerifierCSharp(scheme: string): string {
68
+ const id = safeIdentifier(scheme);
69
+ return `namespace Functions.Auth.Schemes;
70
+ public record ExternalPrincipal_${id}(string Subject, string Issuer, string[] Roles, IReadOnlyDictionary<string, object?> Claims);
71
+ public sealed class ExternalTokenVerificationException_${id} : Exception
72
+ {
73
+ public bool IsIdentityProviderUnavailable { get; }
74
+ public ExternalTokenVerificationException_${id}(string message, bool unavailable = false) : base(message) => IsIdentityProviderUnavailable = unavailable;
75
+ }
76
+ public static class ExternalTokenVerifier_${id}
77
+ {
78
+ // Validate signature, issuer, audience and expiry. This stub deliberately fails closed.
79
+ public static Task<ExternalPrincipal_${id}> VerifyAsync(string token) => throw new InvalidOperationException("External token verifier for ${scheme} is not configured");
80
+ }
81
+ `;
82
+ }
83
+
84
+ export function generateNamedExternalTokenVerifierPython(scheme: string): string {
85
+ return `class ExternalTokenVerificationError(Exception):
86
+ def __init__(self, message: str, identity_provider_unavailable: bool = False):
87
+ super().__init__(message)
88
+ self.identity_provider_unavailable = identity_provider_unavailable
89
+
90
+ def verify_external_token(token: str) -> dict:
91
+ # Validate signature, issuer, audience and expiry. This stub deliberately fails closed.
92
+ raise RuntimeError("External token verifier for ${scheme} is not configured")
93
+ `;
94
+ }
95
+
96
+ export function generateNamedAuthRouterCSharp(config: NormalizedAuthConfig): string {
97
+ const externalCases = Object.entries(config.schemes).filter(([,s]) => s.provider === "external-token").map(([name]) => {
98
+ const id = safeIdentifier(name);
99
+ return ` case "${name}":
100
+ try
101
+ {
102
+ var external = await ExternalTokenVerifier_${id}.VerifyAsync(bearer!);
103
+ principal = new AuthPrincipal(external.Subject, "${name}", external.Issuer, external.Roles, external.Claims);
104
+ }
105
+ catch (ExternalTokenVerificationException_${id} ex) when (ex.IsIdentityProviderUnavailable) { return (null, await Error(request, HttpStatusCode.ServiceUnavailable, "Identity provider unavailable")); }
106
+ catch (ExternalTokenVerificationException_${id}) { return (null, await Error(request, HttpStatusCode.Unauthorized, "Invalid token")); }
107
+ catch { return (null, await Error(request, HttpStatusCode.ServiceUnavailable, "External token verifier unavailable")); }
108
+ break;`;
109
+ }).join("\n");
110
+ return `using System.Net;
111
+ using System.Text;
112
+ using System.Text.Json;
113
+ using Microsoft.Azure.Functions.Worker.Http;
114
+ using Functions.Auth.Schemes;
115
+ namespace Functions.Auth;
116
+ public record AuthPrincipal(string Subject, string Scheme, string Issuer, string[] Roles, IReadOnlyDictionary<string, object?> Claims)
117
+ {
118
+ [Obsolete("Use Subject")] public string UserId => Subject;
119
+ public string IdentityKey => $"{Scheme}:{Subject}";
120
+ }
121
+ public static class AuthRouter
122
+ {
123
+ private static readonly Dictionary<string, (string[] Schemes, string[] Roles)> Policies = new()
124
+ {
125
+ ${Object.entries(config.authorization.policies).map(([n,p]) => ` ["${n}"] = (new[] { ${p.schemes.map(s=>`"${s}"`).join(", ")} }, new[] { ${(p.roles??[]).map(r=>`"${r}"`).join(", ")} }),`).join("\n")}
126
+ };
127
+ private static readonly Dictionary<string,string> Providers = new() { ${Object.entries(config.schemes).map(([n,s])=>`["${n}"]="${s.provider}"`).join(", ")} };
128
+ private static readonly Dictionary<string,string[]> AllowedProviders = new() { ${Object.entries(config.schemes).map(([n,s])=>`["${n}"]=new[] { ${(s.swa?.allowedProviders??[]).map(p=>`"${p}"`).join(", ")} }`).join(", ")} };
129
+ public static async Task<(AuthPrincipal? Principal, HttpResponseData? ErrorResponse)> Authorize(HttpRequestData request, string policyName)
130
+ {
131
+ if (!Policies.TryGetValue(policyName, out var policy)) return (null, await Error(request, HttpStatusCode.Unauthorized, "Undefined authorization policy"));
132
+ var bearerHeader = request.Headers.TryGetValues("Authorization", out var authValues) ? authValues.FirstOrDefault() : null;
133
+ var bearer = bearerHeader?.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) == true ? bearerHeader[7..] : null;
134
+ var swa = request.Headers.TryGetValues("x-ms-client-principal", out var swaValues) ? swaValues.FirstOrDefault() : null;
135
+ var candidates = policy.Schemes.Where(s => Providers[s] == "swa" ? swa != null : Providers[s] is "external-token" or "custom-jwt" ? bearer != null : false).ToArray();
136
+ if (candidates.Length != 1) return (null, await Error(request, HttpStatusCode.Unauthorized, candidates.Length == 0 ? "Authentication required" : "Ambiguous credentials"));
137
+ var scheme = candidates[0]; AuthPrincipal principal;
138
+ switch (scheme)
139
+ {
140
+ ${externalCases}
141
+ default:
142
+ if (Providers[scheme] != "swa") return (null, await Error(request, HttpStatusCode.Unauthorized, "Authentication adapter is not configured"));
143
+ try
144
+ {
145
+ using var doc = JsonDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(swa!)));
146
+ var root = doc.RootElement; var subject = root.GetProperty("userId").GetString();
147
+ var roles = root.TryGetProperty("userRoles", out var rv) ? rv.EnumerateArray().Where(v=>v.ValueKind==JsonValueKind.String).Select(v=>v.GetString()!).ToArray() : Array.Empty<string>();
148
+ if (string.IsNullOrEmpty(subject) || !roles.Contains("authenticated")) throw new InvalidOperationException();
149
+ var identityProvider = root.TryGetProperty("identityProvider", out var iv) ? iv.GetString() : null;
150
+ if (AllowedProviders[scheme].Length > 0 && (identityProvider == null || !AllowedProviders[scheme].Contains(identityProvider))) throw new InvalidOperationException();
151
+ var issuer = identityProvider == null ? "swa" : "swa:" + identityProvider;
152
+ principal = new AuthPrincipal(subject!, scheme, issuer, roles, new Dictionary<string,object?> { ["clientPrincipal"] = root.Clone() });
153
+ }
154
+ catch { return (null, await Error(request, HttpStatusCode.Unauthorized, "Invalid SWA client principal")); }
155
+ break;
156
+ }
157
+ if (policy.Roles.Length > 0 && !policy.Roles.Any(principal.Roles.Contains)) return (null, await Error(request, HttpStatusCode.Forbidden, "Insufficient permissions"));
158
+ return (principal, null);
159
+ }
160
+ private static async Task<HttpResponseData> Error(HttpRequestData request, HttpStatusCode status, string message) { var response=request.CreateResponse(status); await response.WriteAsJsonAsync(new { error=message }); return response; }
161
+ }
162
+ `;
163
+ }
164
+
165
+ export function generateNamedAuthRouterPython(config: NormalizedAuthConfig): string {
166
+ const external = Object.entries(config.schemes).filter(([,s])=>s.provider === "external-token");
167
+ const imports = external.map(([n])=>`from auth.schemes.${schemeSlug(n).replace(/-/g,"_")}.verifier import verify_external_token as verify_${safeIdentifier(n)}, ExternalTokenVerificationError as Error_${safeIdentifier(n)}`).join("\n");
168
+ const cases = external.map(([n], index)=>` ${index === 0 ? "if" : "elif"} scheme == ${JSON.stringify(n)}:
169
+ try:
170
+ value = verify_${safeIdentifier(n)}(bearer)
171
+ except Error_${safeIdentifier(n)} as exc:
172
+ raise AuthError(503 if exc.identity_provider_unavailable else 401, "Identity provider unavailable" if exc.identity_provider_unavailable else "Invalid token")
173
+ except Exception:
174
+ raise AuthError(503, "External token verifier unavailable")
175
+ if not value.get("subject") or not isinstance(value.get("roles"), list): raise AuthError(401, "Invalid external principal")
176
+ principal = {"subject": value["subject"], "scheme": scheme, "issuer": value.get("issuer", scheme), "roles": value["roles"], "claims": value.get("claims", {}), "userId": value["subject"]}`).join("\n");
177
+ return `import base64
178
+ import json
179
+ import azure.functions as func
180
+ ${imports}
181
+ POLICIES = ${JSON.stringify(config.authorization.policies)}
182
+ PROVIDERS = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n,s])=>[n,s.provider])))}
183
+ ALLOWED_PROVIDERS = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n,s])=>[n,s.swa?.allowedProviders??[]])))}
184
+ class AuthError(Exception):
185
+ def __init__(self, status, message): self.status, self.message = status, message
186
+ def require_auth(req: func.HttpRequest, policy_name: str):
187
+ policy = POLICIES.get(policy_name)
188
+ if not policy: raise AuthError(401, "Undefined authorization policy")
189
+ header = req.headers.get("authorization", "")
190
+ bearer = header[7:] if header.lower().startswith("bearer ") else None
191
+ swa = req.headers.get("x-ms-client-principal")
192
+ candidates = [s for s in policy["schemes"] if (PROVIDERS[s] == "swa" and swa) or (PROVIDERS[s] in ("external-token", "custom-jwt") and bearer)]
193
+ if len(candidates) != 1: raise AuthError(401, "Authentication required" if not candidates else "Ambiguous credentials")
194
+ scheme = candidates[0]
195
+ ${cases}
196
+ ${external.length ? "elif" : "if"} PROVIDERS[scheme] == "swa":
197
+ try:
198
+ value=json.loads(base64.b64decode(swa).decode("utf-8")); roles=[r for r in value.get("userRoles",[]) if isinstance(r,str)]
199
+ if not value.get("userId") or "authenticated" not in roles: raise ValueError()
200
+ if ALLOWED_PROVIDERS[scheme] and value.get("identityProvider") not in ALLOWED_PROVIDERS[scheme]: raise ValueError()
201
+ principal={"subject":value["userId"],"scheme":scheme,"issuer":"swa:"+value.get("identityProvider", "unknown"),"roles":roles,"claims":value,"userId":value["userId"]}
202
+ except Exception: raise AuthError(401,"Invalid SWA client principal")
203
+ else: raise AuthError(401,"Authentication adapter is not configured")
204
+ require_roles(principal, policy.get("roles", [])); return principal
205
+ def require_roles(principal, roles):
206
+ if roles and not any(role in principal.get("roles",[]) for role in roles): raise AuthError(403,"Insufficient permissions")
207
+ def identity_key(principal): return principal["scheme"] + ":" + principal["subject"]
208
+ def handle_auth_error(error): return func.HttpResponse(json.dumps({"error":error.message}),status_code=error.status,mimetype="application/json") if isinstance(error,AuthError) else None
209
+ `;
210
+ }
211
+
10
212
  // ============================================================
11
213
  // 1. shared/models/auth.ts(Zod スキーマ)
12
214
  // ============================================================
@@ -1249,8 +1451,9 @@ export function generateBFFCallFunctionWithSwaAuth(): string {
1249
1451
  if (authorization) {
1250
1452
  fetchHeaders['Authorization'] = authorization;
1251
1453
  }`,
1252
- ` let principal = reqHeaders.get('x-ms-client-principal');
1253
- if (!principal) {
1454
+ ` // Never forward a browser-supplied principal header. Resolve it from the SWA session endpoint.
1455
+ let principal: string | null = null;
1456
+ {
1254
1457
  const cookie = reqHeaders.get('cookie');
1255
1458
  const host = reqHeaders.get('host');
1256
1459
  if (cookie && host) {
@@ -1282,6 +1485,16 @@ export function generateBFFCallFunctionWithSwaAuth(): string {
1282
1485
  );
1283
1486
  }
1284
1487
 
1488
+ /** Combined transport for named schemes. Functions still performs all verification. */
1489
+ export function generateBFFCallFunctionWithMultipleAuth(): string {
1490
+ return generateBFFCallFunctionWithSwaAuth().replace(
1491
+ ` // Never forward a browser-supplied principal header. Resolve it from the SWA session endpoint.`,
1492
+ ` const authorization = reqHeaders.get('authorization');
1493
+ if (authorization?.startsWith('Bearer ')) fetchHeaders['Authorization'] = authorization;
1494
+ // Never forward a browser-supplied principal header. Resolve it from the SWA session endpoint.`
1495
+ );
1496
+ }
1497
+
1285
1498
  export function generateSwaAuthHelperTS(): string {
1286
1499
  return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
1287
1500
 
@@ -1485,17 +1698,20 @@ public static class JwtHelper
1485
1698
  /**
1486
1699
  * TypeScript Functions のハンドラーにロールガードを挿入するためのインポート文を生成
1487
1700
  */
1488
- export function generateAuthImportTS(): string {
1489
- return `import { requireAuth, requireRoles, handleAuthError } from './auth/jwt-helper';`;
1701
+ export function generateAuthImportTS(namedPolicy = false): string {
1702
+ return `import { requireAuth, requireRoles, handleAuthError } from './auth/${namedPolicy ? "auth-router" : "jwt-helper"}';`;
1490
1703
  }
1491
1704
 
1492
1705
  /**
1493
1706
  * TypeScript Functions のハンドラー先頭に挿入する認証チェックコードを生成
1494
1707
  */
1495
1708
  export function generateAuthGuardTS(policy: ModelAuthPolicy, operation: 'read' | 'write', provider?: AuthProvider): string {
1496
- const roles = operation === 'read'
1497
- ? (policy.read || policy.roles || [])
1498
- : (policy.write || policy.roles || []);
1709
+ const selected = operation === 'read' ? policy.read : policy.write;
1710
+ if (typeof selected === 'string' || policy.policy) {
1711
+ const name = typeof selected === 'string' ? selected : policy.policy!;
1712
+ return ` const authUser = await requireAuth(request, ${JSON.stringify(name)});`;
1713
+ }
1714
+ const roles = selected || policy.roles || [];
1499
1715
 
1500
1716
  if (roles.length > 0) {
1501
1717
  return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);
@@ -1508,9 +1724,12 @@ export function generateAuthGuardTS(policy: ModelAuthPolicy, operation: 'read' |
1508
1724
  * C# Functions のハンドラーに挿入するロールガードコードを生成
1509
1725
  */
1510
1726
  export function generateAuthGuardCSharp(policy: ModelAuthPolicy, operation: 'read' | 'write'): string {
1511
- const roles = operation === 'read'
1512
- ? (policy.read || policy.roles || [])
1513
- : (policy.write || policy.roles || []);
1727
+ const selected = operation === 'read' ? policy.read : policy.write;
1728
+ if (typeof selected === 'string' || policy.policy) {
1729
+ const name = typeof selected === 'string' ? selected : policy.policy!;
1730
+ return ` var (principal, errorResponse) = await AuthRouter.Authorize(request, ${JSON.stringify(name)});\n if (errorResponse != null) return errorResponse;`;
1731
+ }
1732
+ const roles = selected || policy.roles || [];
1514
1733
 
1515
1734
  if (roles.length > 0) {
1516
1735
  const rolesStr = roles.map(r => `"${r}"`).join(', ');
@@ -1525,9 +1744,12 @@ export function generateAuthGuardCSharp(policy: ModelAuthPolicy, operation: 'rea
1525
1744
  * Python Functions のハンドラーに挿入するロールガードコードを生成
1526
1745
  */
1527
1746
  export function generateAuthGuardPython(policy: ModelAuthPolicy, operation: 'read' | 'write'): string {
1528
- const roles = operation === 'read'
1529
- ? (policy.read || policy.roles || [])
1530
- : (policy.write || policy.roles || []);
1747
+ const selected = operation === 'read' ? policy.read : policy.write;
1748
+ if (typeof selected === 'string' || policy.policy) {
1749
+ const name = typeof selected === 'string' ? selected : policy.policy!;
1750
+ return ` user = require_auth(req, ${JSON.stringify(name)})`;
1751
+ }
1752
+ const roles = selected || policy.roles || [];
1531
1753
 
1532
1754
  if (roles.length > 0) {
1533
1755
  const rolesStr = roles.map(r => `"${r}"`).join(', ');
@@ -124,7 +124,8 @@ export function generateRdbConnectorFunctionTS(
124
124
 
125
125
  // Auth guard setup
126
126
  const hasAuth = !!authPolicy;
127
- const authImport = hasAuth ? `\n${generateAuthImportTS()}\n` : '';
127
+ const namedAuth = Boolean(authPolicy && (authPolicy.policy || typeof authPolicy.read === 'string' || typeof authPolicy.write === 'string'));
128
+ const authImport = hasAuth ? `\n${generateAuthImportTS(namedAuth)}\n` : '';
128
129
  const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read', authProvider)}\n` : '';
129
130
  const authCatchBlock = hasAuth
130
131
  ? ` const authErr = handleAuthError(error);
@@ -205,7 +206,8 @@ export function generateApiConnectorFunctionTS(
205
206
 
206
207
  // Auth guard setup (user access control, separate from connector auth to external APIs)
207
208
  const hasAuth = !!authPolicy;
208
- const authImportLine = hasAuth ? `\n${generateAuthImportTS()}\n` : '';
209
+ const namedAuth = Boolean(authPolicy && (authPolicy.policy || typeof authPolicy.read === 'string' || typeof authPolicy.write === 'string'));
210
+ const authImportLine = hasAuth ? `\n${generateAuthImportTS(namedAuth)}\n` : '';
209
211
  const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read', authProvider)}\n` : '';
210
212
  const writeGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'write', authProvider)}\n` : '';
211
213
  const authCatchBlock = hasAuth
@@ -22,7 +22,8 @@ export function generateCompactAzureFunctionsCRUD(model: ModelInfo, sharedPackag
22
22
  const isIdPartition = partitionKeyField === 'id';
23
23
 
24
24
  const hasAuth = !!authPolicy;
25
- const authImport = hasAuth ? `\n${generateAuthImportTS()}\n` : '';
25
+ const namedAuth = Boolean(authPolicy && (authPolicy.policy || typeof authPolicy.read === 'string' || typeof authPolicy.write === 'string'));
26
+ const authImport = hasAuth ? `\n${generateAuthImportTS(namedAuth)}\n` : '';
26
27
  const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read', authProvider)}\n` : '';
27
28
  const writeGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'write', authProvider)}\n` : '';
28
29
  const authCatchBlock = hasAuth
@@ -779,7 +780,8 @@ export function generatePythonAzureFunctionsCRUD(model: ModelInfo, authPolicy?:
779
780
  const isIdPartition = partitionKeyField === 'id';
780
781
 
781
782
  const hasAuth = !!authPolicy;
782
- const authImport = hasAuth ? '\nfrom auth.jwt_helper import require_auth, require_roles, handle_auth_error\n' : '';
783
+ const namedAuth = Boolean(authPolicy && (authPolicy.policy || typeof authPolicy.read === 'string' || typeof authPolicy.write === 'string'));
784
+ const authImport = hasAuth ? `\nfrom auth.${namedAuth ? 'auth_router' : 'jwt_helper'} import require_auth, require_roles, handle_auth_error\n` : '';
783
785
  // generateAuthGuardPython outputs at 4-space indent; inside try: we need 8-space
784
786
  const readGuardRaw = hasAuth ? generateAuthGuardPython(authPolicy!, 'read') : '';
785
787
  const writeGuardRaw = hasAuth ? generateAuthGuardPython(authPolicy!, 'write') : '';
@@ -779,14 +779,20 @@ export function parseAuthPolicy(content: string): ModelAuthPolicy | undefined {
779
779
  const read = extractRoles('read');
780
780
  const write = extractRoles('write');
781
781
 
782
- if (!roles && !read && !write) {
782
+ const extractName = (key: string): string | undefined => objectStr.match(new RegExp(`${key}\\s*:\\s*['"]([^'"]+)['"]`))?.[1];
783
+ const namedPolicy = extractName('policy');
784
+ const namedRead = extractName('read');
785
+ const namedWrite = extractName('write');
786
+
787
+ if (!roles && !read && !write && !namedPolicy && !namedRead && !namedWrite) {
783
788
  return undefined;
784
789
  }
785
790
 
786
791
  return {
787
792
  ...(roles ? { roles } : {}),
788
- ...(read ? { read } : {}),
789
- ...(write ? { write } : {}),
793
+ ...(namedRead ? { read: namedRead } : read ? { read } : {}),
794
+ ...(namedWrite ? { write: namedWrite } : write ? { write } : {}),
795
+ ...(namedPolicy ? { policy: namedPolicy } : {}),
790
796
  };
791
797
  }
792
798
 
@@ -10,6 +10,9 @@ import { ModelAuthPolicy } from "../../types";
10
10
  export interface UIAuthOptions {
11
11
  /** モデル固有のロールポリシー */
12
12
  authPolicy: ModelAuthPolicy;
13
+ writeRoles?: string[];
14
+ authenticatedFetchImport?: string;
15
+ authContextImport?: string;
13
16
  }
14
17
 
15
18
  /**
@@ -21,7 +24,8 @@ export function generateListPage(model: ModelInfo, sharedPackageName: string, au
21
24
  const modelKebab = toKebabCase(modelName);
22
25
 
23
26
  const hasAuth = !!authOptions;
24
- const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
27
+ const authContextImport = authOptions?.authContextImport ?? '@/lib/auth/auth-context';
28
+ const writeRoles = authOptions?.writeRoles ?? (Array.isArray(authOptions?.authPolicy?.write) ? authOptions.authPolicy.write : authOptions?.authPolicy?.roles);
25
29
  const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
26
30
 
27
31
  // フィールドから表示するカラムを抽出(id以外の最初の3つ)
@@ -71,7 +75,7 @@ import { useEffect, useState } from 'react';
71
75
  import Link from 'next/link';
72
76
  import { z } from 'zod/v4';
73
77
  ${schemaImportLine}
74
- ${hasAuth ? `import { useAuth } from '@/lib/auth/auth-context';` : ''}
78
+ ${hasAuth ? `import { useAuth } from '${authContextImport}';` : ''}
75
79
 
76
80
  type ${modelName} = z.infer<typeof ${localSchemaName}>;
77
81
 
@@ -266,7 +270,8 @@ export function generateDetailPage(model: ModelInfo, sharedPackageName: string,
266
270
  const modelKebab = toKebabCase(modelName);
267
271
 
268
272
  const hasAuth = !!authOptions;
269
- const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
273
+ const authContextImport = authOptions?.authContextImport ?? '@/lib/auth/auth-context';
274
+ const writeRoles = authOptions?.writeRoles ?? (Array.isArray(authOptions?.authPolicy?.write) ? authOptions.authPolicy.write : authOptions?.authPolicy?.roles);
270
275
  const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
271
276
 
272
277
  // 外部キーフィールドを検出
@@ -311,7 +316,7 @@ import { useParams, useRouter } from 'next/navigation';
311
316
  import Link from 'next/link';
312
317
  import { z } from 'zod/v4';
313
318
  ${schemaImportLine}
314
- ${hasAuth ? `import { useAuth } from '@/lib/auth/auth-context';` : ''}
319
+ ${hasAuth ? `import { useAuth } from '${authContextImport}';` : ''}
315
320
 
316
321
  type ${modelName} = z.infer<typeof ${localSchemaName}>;
317
322
 
@@ -910,7 +915,8 @@ export function generateNewPage(model: ModelInfo, authOptions?: UIAuthOptions):
910
915
  const modelKebab = toKebabCase(modelName);
911
916
 
912
917
  const hasAuth = !!authOptions;
913
- const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
918
+ const authContextImport = authOptions?.authContextImport ?? '@/lib/auth/auth-context';
919
+ const writeRoles = authOptions?.writeRoles ?? (Array.isArray(authOptions?.authPolicy?.write) ? authOptions.authPolicy.write : authOptions?.authPolicy?.roles);
914
920
  const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
915
921
 
916
922
  if (!hasAuth) {
@@ -936,7 +942,7 @@ export default function New${modelName}Page() {
936
942
  import { useEffect } from 'react';
937
943
  import { useRouter } from 'next/navigation';
938
944
  import ${modelName}Form from '../_components/${modelName}Form';
939
- import { useAuth } from '@/lib/auth/auth-context';
945
+ import { useAuth } from '${authContextImport}';
940
946
 
941
947
  export default function New${modelName}Page() {
942
948
  const router = useRouter();
@@ -979,7 +985,8 @@ export function generateEditPage(model: ModelInfo, sharedPackageName: string, au
979
985
  const modelKebab = toKebabCase(modelName);
980
986
 
981
987
  const hasAuth = !!authOptions;
982
- const writeRoles = authOptions?.authPolicy?.write || authOptions?.authPolicy?.roles;
988
+ const authContextImport = authOptions?.authContextImport ?? '@/lib/auth/auth-context';
989
+ const writeRoles = authOptions?.writeRoles ?? (Array.isArray(authOptions?.authPolicy?.write) ? authOptions.authPolicy.write : authOptions?.authPolicy?.roles);
983
990
  const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
984
991
 
985
992
  const schemaName = model.schemaName;
@@ -997,7 +1004,7 @@ import { useParams, useRouter } from 'next/navigation';
997
1004
  import ${modelName}Form from '../../_components/${modelName}Form';
998
1005
  import { z } from 'zod/v4';
999
1006
  ${schemaImportLine}
1000
- ${hasAuth ? `import { useAuth } from '@/lib/auth/auth-context';` : ''}
1007
+ ${hasAuth ? `import { useAuth } from '${authContextImport}';` : ''}
1001
1008
 
1002
1009
  type ${modelName} = z.infer<typeof ${localSchemaName}>;
1003
1010