swallowkit 1.0.0-beta.46 → 1.0.0-beta.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/cli/commands/add-auth.d.ts +3 -0
  2. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  3. package/dist/cli/commands/add-auth.js +350 -17
  4. package/dist/cli/commands/add-auth.js.map +1 -1
  5. package/dist/cli/commands/dev.d.ts +4 -0
  6. package/dist/cli/commands/dev.d.ts.map +1 -1
  7. package/dist/cli/commands/dev.js +104 -13
  8. package/dist/cli/commands/dev.js.map +1 -1
  9. package/dist/cli/commands/init.d.ts +2 -0
  10. package/dist/cli/commands/init.d.ts.map +1 -1
  11. package/dist/cli/commands/init.js +56 -30
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/commands/scaffold.d.ts +9 -0
  14. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  15. package/dist/cli/commands/scaffold.js +84 -8
  16. package/dist/cli/commands/scaffold.js.map +1 -1
  17. package/dist/cli/index.d.ts.map +1 -1
  18. package/dist/cli/index.js +5 -1
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/core/config.d.ts +7 -2
  21. package/dist/core/config.d.ts.map +1 -1
  22. package/dist/core/config.js +81 -1
  23. package/dist/core/config.js.map +1 -1
  24. package/dist/core/mock/connector-mock-server.js +2 -2
  25. package/dist/core/mock/connector-mock-server.js.map +1 -1
  26. package/dist/core/project/manifest.js +2 -2
  27. package/dist/core/project/manifest.js.map +1 -1
  28. package/dist/core/project/validation.d.ts.map +1 -1
  29. package/dist/core/project/validation.js +17 -0
  30. package/dist/core/project/validation.js.map +1 -1
  31. package/dist/core/scaffold/auth-generator.d.ts +28 -3
  32. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  33. package/dist/core/scaffold/auth-generator.js +493 -23
  34. package/dist/core/scaffold/auth-generator.js.map +1 -1
  35. package/dist/core/scaffold/connector-functions-generator.d.ts +3 -3
  36. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
  37. package/dist/core/scaffold/connector-functions-generator.js +23 -21
  38. package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
  39. package/dist/core/scaffold/functions-generator.d.ts +2 -2
  40. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  41. package/dist/core/scaffold/functions-generator.js +20 -18
  42. package/dist/core/scaffold/functions-generator.js.map +1 -1
  43. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  44. package/dist/core/scaffold/model-parser.js +8 -3
  45. package/dist/core/scaffold/model-parser.js.map +1 -1
  46. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  47. package/dist/core/scaffold/nextjs-generator.js +13 -1
  48. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  49. package/dist/core/scaffold/ui-generator.d.ts +3 -0
  50. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  51. package/dist/core/scaffold/ui-generator.js +12 -8
  52. package/dist/core/scaffold/ui-generator.js.map +1 -1
  53. package/dist/types/index.d.ts +35 -5
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/dist/utils/package-manager.d.ts.map +1 -1
  56. package/dist/utils/package-manager.js +1 -0
  57. package/dist/utils/package-manager.js.map +1 -1
  58. package/package.json +1 -1
  59. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +20 -20
  60. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +13 -1
  61. package/src/__tests__/auth.test.ts +107 -0
  62. package/src/__tests__/config.test.ts +22 -1
  63. package/src/__tests__/dev.test.ts +27 -0
  64. package/src/__tests__/init.test.ts +19 -0
  65. package/src/__tests__/mcp.test.ts +2 -2
  66. package/src/__tests__/multi-auth.test.ts +133 -0
  67. package/src/__tests__/nextjs-generator.test.ts +8 -0
  68. package/src/cli/commands/add-auth.ts +351 -18
  69. package/src/cli/commands/dev.ts +116 -16
  70. package/src/cli/commands/init.ts +56 -30
  71. package/src/cli/commands/scaffold.ts +86 -10
  72. package/src/cli/index.ts +5 -1
  73. package/src/core/config.ts +69 -2
  74. package/src/core/mock/connector-mock-server.ts +2 -2
  75. package/src/core/project/manifest.ts +3 -3
  76. package/src/core/project/validation.ts +16 -0
  77. package/src/core/scaffold/auth-generator.ts +500 -24
  78. package/src/core/scaffold/connector-functions-generator.ts +26 -21
  79. package/src/core/scaffold/functions-generator.ts +21 -19
  80. package/src/core/scaffold/model-parser.ts +9 -3
  81. package/src/core/scaffold/nextjs-generator.ts +13 -1
  82. package/src/core/scaffold/ui-generator.ts +15 -8
  83. package/src/types/index.ts +36 -5
  84. package/src/utils/package-manager.ts +1 -0
@@ -3,10 +3,212 @@
3
3
  * add-auth コマンドおよび scaffold ロールガード挿入で使用するテンプレート群
4
4
  */
5
5
 
6
- import { 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
  // ============================================================
@@ -111,7 +313,7 @@ ${getConnection}
111
313
  app.http('auth-login', {
112
314
  methods: ['POST'],
113
315
  route: 'auth/login',
114
- authLevel: 'anonymous',
316
+ authLevel: 'function',
115
317
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
116
318
  try {
117
319
  const body = LoginRequest.parse(await request.json());
@@ -169,7 +371,7 @@ app.http('auth-login', {
169
371
  app.http('auth-me', {
170
372
  methods: ['GET'],
171
373
  route: 'auth/me',
172
- authLevel: 'anonymous',
374
+ authLevel: 'function',
173
375
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
174
376
  try {
175
377
  const user = requireAuth(request);
@@ -187,7 +389,7 @@ app.http('auth-me', {
187
389
  app.http('auth-logout', {
188
390
  methods: ['POST'],
189
391
  route: 'auth/logout',
190
- authLevel: 'anonymous',
392
+ authLevel: 'function',
191
393
  handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
192
394
  return { status: 200, jsonBody: { message: 'Logged out' } };
193
395
  },
@@ -312,7 +514,7 @@ namespace Functions.Auth
312
514
  {
313
515
  [Function("auth-login")]
314
516
  public async Task<HttpResponseData> Login(
315
- [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "auth/login")] HttpRequestData request)
517
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "auth/login")] HttpRequestData request)
316
518
  {
317
519
  var body = await JsonSerializer.DeserializeAsync<LoginRequest>(request.Body);
318
520
  if (body == null || string.IsNullOrEmpty(body.LoginId) || string.IsNullOrEmpty(body.Password))
@@ -371,7 +573,7 @@ namespace Functions.Auth
371
573
 
372
574
  [Function("auth-me")]
373
575
  public async Task<HttpResponseData> Me(
374
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth/me")] HttpRequestData request)
576
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "auth/me")] HttpRequestData request)
375
577
  {
376
578
  var (principal, errorResponse) = await JwtHelper.Authorize(request);
377
579
  if (errorResponse != null) return errorResponse;
@@ -390,7 +592,7 @@ namespace Functions.Auth
390
592
 
391
593
  [Function("auth-logout")]
392
594
  public async Task<HttpResponseData> Logout(
393
- [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "auth/logout")] HttpRequestData request)
595
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "auth/logout")] HttpRequestData request)
394
596
  {
395
597
  var response = request.CreateResponse(System.Net.HttpStatusCode.OK);
396
598
  await response.WriteAsJsonAsync(new { message = "Logged out" });
@@ -580,7 +782,7 @@ auth_bp = func.Blueprint()
580
782
  ${getConnection}
581
783
 
582
784
 
583
- @auth_bp.route(route="auth/login", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
785
+ @auth_bp.route(route="auth/login", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
584
786
  def auth_login(req: func.HttpRequest) -> func.HttpResponse:
585
787
  try:
586
788
  body = req.get_json()
@@ -652,7 +854,7 @@ def auth_login(req: func.HttpRequest) -> func.HttpResponse:
652
854
  )
653
855
 
654
856
 
655
- @auth_bp.route(route="auth/me", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
857
+ @auth_bp.route(route="auth/me", methods=["GET"], auth_level=func.AuthLevel.FUNCTION)
656
858
  def auth_me(req: func.HttpRequest) -> func.HttpResponse:
657
859
  from auth.jwt_helper import require_auth, handle_auth_error
658
860
 
@@ -671,7 +873,7 @@ def auth_me(req: func.HttpRequest) -> func.HttpResponse:
671
873
  )
672
874
 
673
875
 
674
- @auth_bp.route(route="auth/logout", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
876
+ @auth_bp.route(route="auth/logout", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
675
877
  def auth_logout(req: func.HttpRequest) -> func.HttpResponse:
676
878
  return func.HttpResponse(
677
879
  json.dumps({"message": "Logged out"}),
@@ -1122,6 +1324,17 @@ function getFunctionsBaseUrl(): string {
1122
1324
  return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
1123
1325
  }
1124
1326
 
1327
+ function isLocalFunctionsUrl(url: string): boolean {
1328
+ return url.startsWith('http://localhost:') || url.startsWith('http://127.0.0.1:');
1329
+ }
1330
+
1331
+ function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
1332
+ const key = process.env.BACKEND_FUNCTIONS_KEY;
1333
+ if (key) return { 'x-functions-key': key };
1334
+ if (isLocalFunctionsUrl(functionsBaseUrl)) return {};
1335
+ throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
1336
+ }
1337
+
1125
1338
  interface CallFunctionConfig<TInput = any, TOutput = any> {
1126
1339
  /** HTTP メソッド */
1127
1340
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
@@ -1160,11 +1373,13 @@ export async function callFunction<TInput = any, TOutput = any>(
1160
1373
  // Azure Functions を呼び出し
1161
1374
  const functionsBaseUrl = getFunctionsBaseUrl();
1162
1375
  const url = functionsBaseUrl + path;
1376
+ const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
1163
1377
  console.log(\`[BFF] \${method} \${url}\`);
1164
1378
 
1165
1379
  // Authorization ヘッダーの転送(Proxy が cookie → Authorization に変換済み)
1166
1380
  const fetchHeaders: Record<string, string> = {
1167
1381
  'Content-Type': 'application/json',
1382
+ ...keyHeaders,
1168
1383
  };
1169
1384
  try {
1170
1385
  const reqHeaders = await headers();
@@ -1224,6 +1439,258 @@ export async function callFunction<TInput = any, TOutput = any>(
1224
1439
  `;
1225
1440
  }
1226
1441
 
1442
+ /** SWA built-in auth variant: forward only a platform-derived client principal. */
1443
+ export function generateBFFCallFunctionWithSwaAuth(): string {
1444
+ return generateBFFCallFunctionWithAuth()
1445
+ .replace(
1446
+ ` // Authorization ヘッダーの転送(Proxy が cookie → Authorization に変換済み)`,
1447
+ ` // SWA client principal is accepted only from the platform request context.`
1448
+ )
1449
+ .replace(
1450
+ ` const authorization = reqHeaders.get('authorization');
1451
+ if (authorization) {
1452
+ fetchHeaders['Authorization'] = authorization;
1453
+ }`,
1454
+ ` // Never forward a browser-supplied principal header. Resolve it from the SWA session endpoint.
1455
+ let principal: string | null = null;
1456
+ {
1457
+ const cookie = reqHeaders.get('cookie');
1458
+ const host = reqHeaders.get('host');
1459
+ if (cookie && host) {
1460
+ const proto = reqHeaders.get('x-forwarded-proto') || 'https';
1461
+ const meResponse = await fetch(\`\${proto}://\${host}/.auth/me\`, {
1462
+ headers: { cookie },
1463
+ cache: 'no-store',
1464
+ });
1465
+ if (meResponse.ok) {
1466
+ const payload = await meResponse.json() as { clientPrincipal?: unknown };
1467
+ if (payload.clientPrincipal) {
1468
+ principal = Buffer.from(JSON.stringify(payload.clientPrincipal), 'utf8').toString('base64');
1469
+ }
1470
+ }
1471
+ }
1472
+ }
1473
+ if (principal) {
1474
+ const parsed = JSON.parse(Buffer.from(principal, 'base64').toString('utf8'));
1475
+ const normalized = {
1476
+ identityProvider: String(parsed.identityProvider || ''),
1477
+ userId: String(parsed.userId || ''),
1478
+ userDetails: String(parsed.userDetails || ''),
1479
+ userRoles: Array.isArray(parsed.userRoles) ? parsed.userRoles.filter((role: unknown) => typeof role === 'string') : [],
1480
+ };
1481
+ if (normalized.userId && normalized.userRoles.includes('authenticated')) {
1482
+ fetchHeaders['x-ms-client-principal'] = Buffer.from(JSON.stringify(normalized), 'utf8').toString('base64');
1483
+ }
1484
+ }`
1485
+ );
1486
+ }
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
+
1498
+ export function generateSwaAuthHelperTS(): string {
1499
+ return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
1500
+
1501
+ export interface AuthUser { id: string; loginId: string; roles: string[]; }
1502
+ export class AuthError extends Error { constructor(public status: number, message: string) { super(message); } }
1503
+
1504
+ export function requireAuth(request: HttpRequest): AuthUser {
1505
+ const encoded = request.headers.get('x-ms-client-principal');
1506
+ if (!encoded) throw new AuthError(401, 'Authentication required');
1507
+ try {
1508
+ const value = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
1509
+ const roles = Array.isArray(value.userRoles) ? value.userRoles.filter((r: unknown) => typeof r === 'string') : [];
1510
+ if (typeof value.userId !== 'string' || !value.userId || !roles.includes('authenticated')) throw new Error();
1511
+ return { id: value.userId, loginId: typeof value.userDetails === 'string' ? value.userDetails : '', roles };
1512
+ } catch { throw new AuthError(401, 'Invalid client principal'); }
1513
+ }
1514
+ export function requireRoles(user: AuthUser, roles: string[]): void {
1515
+ if (roles.length && !roles.some(role => user.roles.includes(role))) throw new AuthError(403, 'Insufficient permissions');
1516
+ }
1517
+ export function handleAuthError(error: unknown): HttpResponseInit | null {
1518
+ return error instanceof AuthError ? { status: error.status, jsonBody: { error: error.message } } : null;
1519
+ }
1520
+ `;
1521
+ }
1522
+
1523
+ // ============================================================
1524
+ // External token authentication
1525
+ // ============================================================
1526
+
1527
+ export function generateExternalTokenAdapter(): string {
1528
+ return `/** Replace these functions with your external IdP SDK integration. */
1529
+ export async function getToken(): Promise<string | null> { return null; }
1530
+ export async function login(): Promise<void> { throw new Error('External token login adapter is not configured'); }
1531
+ export async function logout(): Promise<void> { throw new Error('External token logout adapter is not configured'); }
1532
+ `;
1533
+ }
1534
+
1535
+ export function generateAuthenticatedFetch(): string {
1536
+ return `import { getToken } from './external-token-adapter';
1537
+
1538
+ export async function authenticatedFetch(input: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {
1539
+ const token = await getToken();
1540
+ if (!token) return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'content-type': 'application/json' } });
1541
+ const headers = new Headers(init.headers);
1542
+ headers.set('Authorization', \`Bearer \${token}\`);
1543
+ return fetch(input, { ...init, headers });
1544
+ }
1545
+ `;
1546
+ }
1547
+
1548
+ export function generateExternalTokenBFFMeRoute(): string {
1549
+ return `import { callFunction } from '@/lib/api/call-function';
1550
+ export async function GET() { return callFunction({ method: 'GET', path: '/api/auth/me' }); }
1551
+ `;
1552
+ }
1553
+
1554
+ export function generateExternalTokenAuthContext(): string {
1555
+ return `'use client';
1556
+ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
1557
+ import { authenticatedFetch } from './authenticated-fetch';
1558
+ import * as adapter from './external-token-adapter';
1559
+ export interface AuthUser { id: string; loginId: string; name: string; email: string; roles: string[]; }
1560
+ interface Value { user: AuthUser | null; loading: boolean; login: () => Promise<void>; logout: () => Promise<void>; hasRole: (role: string) => boolean; hasAnyRole: (roles: string[]) => boolean; }
1561
+ const Context = createContext<Value | null>(null);
1562
+ export function AuthProvider({ children }: { children: ReactNode }) {
1563
+ const [user, setUser] = useState<AuthUser | null>(null); const [loading, setLoading] = useState(true);
1564
+ useEffect(() => { authenticatedFetch('/api/auth/me').then(async r => {
1565
+ if (!r.ok || !(r.headers.get('content-type') || '').includes('application/json')) return null;
1566
+ return r.json() as Promise<AuthUser>;
1567
+ }).then(setUser).catch(() => setUser(null)).finally(() => setLoading(false)); }, []);
1568
+ const hasRole = (role: string) => user?.roles.includes(role) ?? false;
1569
+ const logout = async () => { await adapter.logout(); setUser(null); };
1570
+ return <Context.Provider value={{ user, loading, login: adapter.login, logout, hasRole, hasAnyRole: roles => roles.some(hasRole) }}>{children}</Context.Provider>;
1571
+ }
1572
+ export function useAuth() { const value = useContext(Context); if (!value) throw new Error('useAuth must be used within an AuthProvider'); return value; }
1573
+ `;
1574
+ }
1575
+
1576
+ export function generateExternalTokenVerifierTS(): string {
1577
+ return `export interface ExternalAuthPrincipal { userId: string; userDetails: string; roles: string[]; }
1578
+ export class ExternalTokenVerificationError extends Error { constructor(public status: 401 | 503, message: string) { super(message); } }
1579
+ /** Implement IdP verification here. Never decode and trust a token without verification. */
1580
+ export async function verifyExternalToken(_token: string): Promise<ExternalAuthPrincipal> {
1581
+ throw new Error('External token verifier is not configured');
1582
+ }
1583
+ `;
1584
+ }
1585
+
1586
+ export function generateExternalTokenHelperTS(): string {
1587
+ return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
1588
+ import { verifyExternalToken, ExternalTokenVerificationError, type ExternalAuthPrincipal } from './external-token-verifier';
1589
+ export class AuthError extends Error { constructor(public status: number, message: string) { super(message); } }
1590
+ export async function requireAuth(request: HttpRequest): Promise<ExternalAuthPrincipal> {
1591
+ const header = request.headers.get('authorization') || '';
1592
+ if (!header.startsWith('Bearer ') || !header.slice(7)) throw new AuthError(401, 'Missing or invalid Authorization header');
1593
+ try { const principal = await verifyExternalToken(header.slice(7));
1594
+ if (!principal?.userId || !Array.isArray(principal.roles)) throw new AuthError(401, 'Invalid external principal');
1595
+ return principal;
1596
+ } catch (error) { if (error instanceof AuthError) throw error; if (error instanceof ExternalTokenVerificationError) throw new AuthError(error.status, error.message); throw new AuthError(500, 'External token verifier is not configured'); }
1597
+ }
1598
+ export function requireRoles(user: ExternalAuthPrincipal, roles: string[]) { if (!roles.some(role => user.roles.includes(role))) throw new AuthError(403, 'Insufficient permissions'); }
1599
+ export function handleAuthError(error: unknown): HttpResponseInit | null { return error instanceof AuthError ? { status: error.status, jsonBody: { error: error.message } } : null; }
1600
+ `;
1601
+ }
1602
+
1603
+ export function generateExternalAuthMeTS(): string {
1604
+ return `import { app, HttpRequest, HttpResponseInit } from '@azure/functions';
1605
+ import { requireAuth, handleAuthError } from './auth/jwt-helper';
1606
+ app.http('external-auth-me', { methods: ['GET'], route: 'auth/me', authLevel: 'function', handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
1607
+ try { const p = await requireAuth(request); return { status: 200, jsonBody: { id: p.userId, loginId: p.userDetails, name: p.userDetails, email: '', roles: p.roles } }; }
1608
+ catch (error) { return handleAuthError(error) || { status: 500, jsonBody: { error: 'Authentication failed' } }; }
1609
+ } });
1610
+ `;
1611
+ }
1612
+
1613
+ export function generateExternalTokenVerifierCSharp(): string { return `namespace Functions.Auth;\npublic record ExternalAuthPrincipal(string UserId, string UserDetails, string[] Roles);\npublic static class ExternalTokenVerifier { public static Task<ExternalAuthPrincipal> VerifyExternalToken(string token) => throw new InvalidOperationException("External token verifier is not configured"); }\n`; }
1614
+ export function generateExternalTokenHelperCSharp(): string { return `using Microsoft.Azure.Functions.Worker.Http;\nnamespace Functions.Auth;\npublic static class JwtHelper { public static async Task<(ExternalAuthPrincipal? Principal, HttpResponseData? ErrorResponse)> Authorize(HttpRequestData request, params string[] roles) { if (!request.Headers.TryGetValues("Authorization", out var values) || !values.FirstOrDefault()!.StartsWith("Bearer ")) { var r=request.CreateResponse(System.Net.HttpStatusCode.Unauthorized); await r.WriteAsJsonAsync(new { error="Authentication required" }); return (null,r); } try { var p=await ExternalTokenVerifier.VerifyExternalToken(values.First()[7..]); if (roles.Length>0 && !roles.Any(p.Roles.Contains)) { var r=request.CreateResponse(System.Net.HttpStatusCode.Forbidden); await r.WriteAsJsonAsync(new { error="Insufficient permissions" }); return (null,r); } return (p,null); } catch (HttpRequestException) { var r=request.CreateResponse(System.Net.HttpStatusCode.ServiceUnavailable); await r.WriteAsJsonAsync(new { error="Identity provider unavailable" }); return (null,r); } catch { var r=request.CreateResponse(System.Net.HttpStatusCode.Unauthorized); await r.WriteAsJsonAsync(new { error="Authentication failed" }); return (null,r); } } }\n`; }
1615
+ export function generateExternalAuthMeCSharp(): string { return `using Microsoft.Azure.Functions.Worker;\nusing Microsoft.Azure.Functions.Worker.Http;\nnamespace Functions.Auth;\npublic class ExternalAuthMe { [Function("external-auth-me")] public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function,"get",Route="auth/me")] HttpRequestData req) { var (p,e)=await JwtHelper.Authorize(req); if(e!=null)return e; var r=req.CreateResponse(System.Net.HttpStatusCode.OK); await r.WriteAsJsonAsync(new { id=p!.UserId, loginId=p.UserDetails, name=p.UserDetails, email="", roles=p.Roles }); return r; } }\n`; }
1616
+ export function generateExternalTokenVerifierPython(): string { return `class ExternalTokenVerificationError(Exception):\n def __init__(self, status: int, message: str): self.status, self.message = status, message\n\ndef verify_external_token(token: str) -> dict:\n raise RuntimeError("External token verifier is not configured")\n`; }
1617
+ export function generateExternalTokenHelperPython(): string { return `import azure.functions as func\nfrom .external_token_verifier import verify_external_token, ExternalTokenVerificationError\nclass AuthError(Exception):\n def __init__(self,status,message): self.status,self.message=status,message\ndef require_auth(req: func.HttpRequest):\n header=req.headers.get("authorization","")\n if not header.startswith("Bearer "): raise AuthError(401,"Authentication required")\n try:\n p=verify_external_token(header[7:])\n if not p.get("userId") or not isinstance(p.get("roles"),list): raise AuthError(401,"Invalid external principal")\n return p\n except ExternalTokenVerificationError as e: raise AuthError(e.status,e.message)\n except AuthError: raise\n except RuntimeError: raise AuthError(500,"External token verifier is not configured")\ndef require_roles(user,roles):\n if not any(r in user.get("roles",[]) for r in roles): raise AuthError(403,"Insufficient permissions")\ndef handle_auth_error(error):\n return func.HttpResponse('{"error":"%s"}' % error.message,status_code=error.status,mimetype="application/json") if isinstance(error,AuthError) else None\n`; }
1618
+ export function generateExternalAuthMePython(): string { return `import json\nimport azure.functions as func\nfrom auth.jwt_helper import require_auth, handle_auth_error\nbp=func.Blueprint()\n@bp.route(route="auth/me",methods=["GET"],auth_level=func.AuthLevel.FUNCTION)\ndef auth_me(req: func.HttpRequest):\n try:\n p=require_auth(req); return func.HttpResponse(json.dumps({"id":p["userId"],"loginId":p.get("userDetails",""),"name":p.get("userDetails",""),"email":"","roles":p["roles"]}),mimetype="application/json")\n except Exception as e:\n return handle_auth_error(e) or func.HttpResponse('{"error":"Authentication failed"}',status_code=500,mimetype="application/json")\n`; }
1619
+ export function generateSwaAuthHelperPython(): string {
1620
+ return `import base64
1621
+ import json
1622
+ import azure.functions as func
1623
+
1624
+ class AuthError(Exception):
1625
+ def __init__(self, status, message): self.status, self.message = status, message
1626
+
1627
+ def require_auth(req):
1628
+ encoded = req.headers.get("x-ms-client-principal")
1629
+ if not encoded: raise AuthError(401, "Authentication required")
1630
+ try:
1631
+ value = json.loads(base64.b64decode(encoded).decode("utf-8"))
1632
+ roles = [r for r in value.get("userRoles", []) if isinstance(r, str)]
1633
+ if not value.get("userId") or "authenticated" not in roles: raise ValueError()
1634
+ return {"id": value["userId"], "loginId": value.get("userDetails", ""), "roles": roles}
1635
+ except Exception: raise AuthError(401, "Invalid client principal")
1636
+
1637
+ def require_roles(user, roles):
1638
+ if roles and not any(role in user["roles"] for role in roles): raise AuthError(403, "Insufficient permissions")
1639
+
1640
+ def handle_auth_error(error):
1641
+ return func.HttpResponse(json.dumps({"error": error.message}), status_code=error.status, mimetype="application/json") if isinstance(error, AuthError) else None
1642
+ `;
1643
+ }
1644
+
1645
+ export function generateSwaAuthHelperCSharp(): string {
1646
+ return `using System.Text;
1647
+ using System.Text.Json;
1648
+ using Microsoft.Azure.Functions.Worker.Http;
1649
+
1650
+ namespace Functions.Auth;
1651
+
1652
+ public class JwtPayload
1653
+ {
1654
+ public string Sub { get; set; } = "";
1655
+ public string LoginId { get; set; } = "";
1656
+ public string[] Roles { get; set; } = Array.Empty<string>();
1657
+ }
1658
+
1659
+ public static class JwtHelper
1660
+ {
1661
+ public static async Task<(JwtPayload?, HttpResponseData?)> Authorize(HttpRequestData request, params string[] requiredRoles)
1662
+ {
1663
+ try
1664
+ {
1665
+ var encoded = request.Headers.TryGetValues("x-ms-client-principal", out var values) ? values.FirstOrDefault() : null;
1666
+ if (string.IsNullOrWhiteSpace(encoded)) throw new InvalidOperationException();
1667
+ using var document = JsonDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(encoded)));
1668
+ var root = document.RootElement;
1669
+ var userId = root.GetProperty("userId").GetString() ?? "";
1670
+ var details = root.TryGetProperty("userDetails", out var detailValue) ? detailValue.GetString() ?? "" : "";
1671
+ var roles = root.TryGetProperty("userRoles", out var roleValue) && roleValue.ValueKind == JsonValueKind.Array
1672
+ ? roleValue.EnumerateArray().Where(v => v.ValueKind == JsonValueKind.String).Select(v => v.GetString()!).ToArray()
1673
+ : Array.Empty<string>();
1674
+ if (string.IsNullOrEmpty(userId) || !roles.Contains("authenticated")) throw new InvalidOperationException();
1675
+ if (requiredRoles.Length > 0 && !requiredRoles.Any(roles.Contains))
1676
+ {
1677
+ var forbidden = request.CreateResponse(System.Net.HttpStatusCode.Forbidden);
1678
+ await forbidden.WriteAsJsonAsync(new { error = "Forbidden" });
1679
+ return (null, forbidden);
1680
+ }
1681
+ return (new JwtPayload { Sub = userId, LoginId = details, Roles = roles }, null);
1682
+ }
1683
+ catch
1684
+ {
1685
+ var unauthorized = request.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
1686
+ await unauthorized.WriteAsJsonAsync(new { error = "Unauthorized" });
1687
+ return (null, unauthorized);
1688
+ }
1689
+ }
1690
+ }
1691
+ `;
1692
+ }
1693
+
1227
1694
  // ============================================================
1228
1695
  // 13. scaffold 用ロールガード挿入ヘルパー
1229
1696
  // ============================================================
@@ -1231,32 +1698,38 @@ export async function callFunction<TInput = any, TOutput = any>(
1231
1698
  /**
1232
1699
  * TypeScript Functions のハンドラーにロールガードを挿入するためのインポート文を生成
1233
1700
  */
1234
- export function generateAuthImportTS(): string {
1235
- 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"}';`;
1236
1703
  }
1237
1704
 
1238
1705
  /**
1239
1706
  * TypeScript Functions のハンドラー先頭に挿入する認証チェックコードを生成
1240
1707
  */
1241
- export function generateAuthGuardTS(policy: ModelAuthPolicy, operation: 'read' | 'write'): string {
1242
- const roles = operation === 'read'
1243
- ? (policy.read || policy.roles || [])
1244
- : (policy.write || policy.roles || []);
1708
+ export function generateAuthGuardTS(policy: ModelAuthPolicy, operation: 'read' | 'write', provider?: AuthProvider): string {
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 || [];
1245
1715
 
1246
1716
  if (roles.length > 0) {
1247
- return ` const authUser = requireAuth(request);
1717
+ return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);
1248
1718
  requireRoles(authUser, ${JSON.stringify(roles)});`;
1249
1719
  }
1250
- return ` const authUser = requireAuth(request);`;
1720
+ return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);`;
1251
1721
  }
1252
1722
 
1253
1723
  /**
1254
1724
  * C# Functions のハンドラーに挿入するロールガードコードを生成
1255
1725
  */
1256
1726
  export function generateAuthGuardCSharp(policy: ModelAuthPolicy, operation: 'read' | 'write'): string {
1257
- const roles = operation === 'read'
1258
- ? (policy.read || policy.roles || [])
1259
- : (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 || [];
1260
1733
 
1261
1734
  if (roles.length > 0) {
1262
1735
  const rolesStr = roles.map(r => `"${r}"`).join(', ');
@@ -1271,9 +1744,12 @@ export function generateAuthGuardCSharp(policy: ModelAuthPolicy, operation: 'rea
1271
1744
  * Python Functions のハンドラーに挿入するロールガードコードを生成
1272
1745
  */
1273
1746
  export function generateAuthGuardPython(policy: ModelAuthPolicy, operation: 'read' | 'write'): string {
1274
- const roles = operation === 'read'
1275
- ? (policy.read || policy.roles || [])
1276
- : (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 || [];
1277
1753
 
1278
1754
  if (roles.length > 0) {
1279
1755
  const rolesStr = roles.map(r => `"${r}"`).join(', ');