swallowkit 1.0.0-beta.45 → 1.0.0-beta.47

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 (52) hide show
  1. package/dist/cli/commands/add-auth.d.ts +2 -0
  2. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  3. package/dist/cli/commands/add-auth.js +214 -16
  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 +100 -12
  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 +74 -30
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  14. package/dist/cli/commands/scaffold.js +37 -5
  15. package/dist/cli/commands/scaffold.js.map +1 -1
  16. package/dist/cli/index.d.ts.map +1 -1
  17. package/dist/cli/index.js +3 -1
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/core/scaffold/auth-generator.d.ts +20 -2
  20. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  21. package/dist/core/scaffold/auth-generator.js +265 -12
  22. package/dist/core/scaffold/auth-generator.js.map +1 -1
  23. package/dist/core/scaffold/connector-functions-generator.d.ts +3 -3
  24. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
  25. package/dist/core/scaffold/connector-functions-generator.js +19 -19
  26. package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
  27. package/dist/core/scaffold/functions-generator.d.ts +2 -2
  28. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  29. package/dist/core/scaffold/functions-generator.js +16 -16
  30. package/dist/core/scaffold/functions-generator.js.map +1 -1
  31. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  32. package/dist/core/scaffold/nextjs-generator.js +13 -1
  33. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  34. package/dist/types/index.d.ts +1 -1
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +20 -20
  38. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +13 -1
  39. package/src/__tests__/auth.test.ts +103 -0
  40. package/src/__tests__/dev.test.ts +27 -0
  41. package/src/__tests__/init.test.ts +27 -0
  42. package/src/__tests__/nextjs-generator.test.ts +8 -0
  43. package/src/cli/commands/add-auth.ts +239 -16
  44. package/src/cli/commands/dev.ts +111 -14
  45. package/src/cli/commands/init.ts +76 -30
  46. package/src/cli/commands/scaffold.ts +37 -5
  47. package/src/cli/index.ts +3 -1
  48. package/src/core/scaffold/auth-generator.ts +267 -13
  49. package/src/core/scaffold/connector-functions-generator.ts +22 -19
  50. package/src/core/scaffold/functions-generator.ts +17 -17
  51. package/src/core/scaffold/nextjs-generator.ts +13 -1
  52. package/src/types/index.ts +1 -1
@@ -3,7 +3,7 @@
3
3
  * add-auth コマンドおよび scaffold ロールガード挿入で使用するテンプレート群
4
4
  */
5
5
 
6
- import { CustomJwtConfig, ModelAuthPolicy, RdbConnectorConfig } from "../../types";
6
+ import { AuthProvider, CustomJwtConfig, ModelAuthPolicy, RdbConnectorConfig } from "../../types";
7
7
 
8
8
  type RdbProvider = RdbConnectorConfig["provider"]; // "mysql" | "postgres" | "sqlserver"
9
9
 
@@ -111,7 +111,7 @@ ${getConnection}
111
111
  app.http('auth-login', {
112
112
  methods: ['POST'],
113
113
  route: 'auth/login',
114
- authLevel: 'anonymous',
114
+ authLevel: 'function',
115
115
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
116
116
  try {
117
117
  const body = LoginRequest.parse(await request.json());
@@ -169,7 +169,7 @@ app.http('auth-login', {
169
169
  app.http('auth-me', {
170
170
  methods: ['GET'],
171
171
  route: 'auth/me',
172
- authLevel: 'anonymous',
172
+ authLevel: 'function',
173
173
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
174
174
  try {
175
175
  const user = requireAuth(request);
@@ -187,7 +187,7 @@ app.http('auth-me', {
187
187
  app.http('auth-logout', {
188
188
  methods: ['POST'],
189
189
  route: 'auth/logout',
190
- authLevel: 'anonymous',
190
+ authLevel: 'function',
191
191
  handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
192
192
  return { status: 200, jsonBody: { message: 'Logged out' } };
193
193
  },
@@ -312,7 +312,7 @@ namespace Functions.Auth
312
312
  {
313
313
  [Function("auth-login")]
314
314
  public async Task<HttpResponseData> Login(
315
- [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "auth/login")] HttpRequestData request)
315
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "auth/login")] HttpRequestData request)
316
316
  {
317
317
  var body = await JsonSerializer.DeserializeAsync<LoginRequest>(request.Body);
318
318
  if (body == null || string.IsNullOrEmpty(body.LoginId) || string.IsNullOrEmpty(body.Password))
@@ -371,7 +371,7 @@ namespace Functions.Auth
371
371
 
372
372
  [Function("auth-me")]
373
373
  public async Task<HttpResponseData> Me(
374
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth/me")] HttpRequestData request)
374
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "auth/me")] HttpRequestData request)
375
375
  {
376
376
  var (principal, errorResponse) = await JwtHelper.Authorize(request);
377
377
  if (errorResponse != null) return errorResponse;
@@ -390,7 +390,7 @@ namespace Functions.Auth
390
390
 
391
391
  [Function("auth-logout")]
392
392
  public async Task<HttpResponseData> Logout(
393
- [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "auth/logout")] HttpRequestData request)
393
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "auth/logout")] HttpRequestData request)
394
394
  {
395
395
  var response = request.CreateResponse(System.Net.HttpStatusCode.OK);
396
396
  await response.WriteAsJsonAsync(new { message = "Logged out" });
@@ -580,7 +580,7 @@ auth_bp = func.Blueprint()
580
580
  ${getConnection}
581
581
 
582
582
 
583
- @auth_bp.route(route="auth/login", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
583
+ @auth_bp.route(route="auth/login", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
584
584
  def auth_login(req: func.HttpRequest) -> func.HttpResponse:
585
585
  try:
586
586
  body = req.get_json()
@@ -652,7 +652,7 @@ def auth_login(req: func.HttpRequest) -> func.HttpResponse:
652
652
  )
653
653
 
654
654
 
655
- @auth_bp.route(route="auth/me", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
655
+ @auth_bp.route(route="auth/me", methods=["GET"], auth_level=func.AuthLevel.FUNCTION)
656
656
  def auth_me(req: func.HttpRequest) -> func.HttpResponse:
657
657
  from auth.jwt_helper import require_auth, handle_auth_error
658
658
 
@@ -671,7 +671,7 @@ def auth_me(req: func.HttpRequest) -> func.HttpResponse:
671
671
  )
672
672
 
673
673
 
674
- @auth_bp.route(route="auth/logout", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
674
+ @auth_bp.route(route="auth/logout", methods=["POST"], auth_level=func.AuthLevel.FUNCTION)
675
675
  def auth_logout(req: func.HttpRequest) -> func.HttpResponse:
676
676
  return func.HttpResponse(
677
677
  json.dumps({"message": "Logged out"}),
@@ -1122,6 +1122,17 @@ function getFunctionsBaseUrl(): string {
1122
1122
  return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
1123
1123
  }
1124
1124
 
1125
+ function isLocalFunctionsUrl(url: string): boolean {
1126
+ return url.startsWith('http://localhost:') || url.startsWith('http://127.0.0.1:');
1127
+ }
1128
+
1129
+ function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
1130
+ const key = process.env.BACKEND_FUNCTIONS_KEY;
1131
+ if (key) return { 'x-functions-key': key };
1132
+ if (isLocalFunctionsUrl(functionsBaseUrl)) return {};
1133
+ throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
1134
+ }
1135
+
1125
1136
  interface CallFunctionConfig<TInput = any, TOutput = any> {
1126
1137
  /** HTTP メソッド */
1127
1138
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
@@ -1160,11 +1171,13 @@ export async function callFunction<TInput = any, TOutput = any>(
1160
1171
  // Azure Functions を呼び出し
1161
1172
  const functionsBaseUrl = getFunctionsBaseUrl();
1162
1173
  const url = functionsBaseUrl + path;
1174
+ const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
1163
1175
  console.log(\`[BFF] \${method} \${url}\`);
1164
1176
 
1165
1177
  // Authorization ヘッダーの転送(Proxy が cookie → Authorization に変換済み)
1166
1178
  const fetchHeaders: Record<string, string> = {
1167
1179
  'Content-Type': 'application/json',
1180
+ ...keyHeaders,
1168
1181
  };
1169
1182
  try {
1170
1183
  const reqHeaders = await headers();
@@ -1224,6 +1237,247 @@ export async function callFunction<TInput = any, TOutput = any>(
1224
1237
  `;
1225
1238
  }
1226
1239
 
1240
+ /** SWA built-in auth variant: forward only a platform-derived client principal. */
1241
+ export function generateBFFCallFunctionWithSwaAuth(): string {
1242
+ return generateBFFCallFunctionWithAuth()
1243
+ .replace(
1244
+ ` // Authorization ヘッダーの転送(Proxy が cookie → Authorization に変換済み)`,
1245
+ ` // SWA client principal is accepted only from the platform request context.`
1246
+ )
1247
+ .replace(
1248
+ ` const authorization = reqHeaders.get('authorization');
1249
+ if (authorization) {
1250
+ fetchHeaders['Authorization'] = authorization;
1251
+ }`,
1252
+ ` let principal = reqHeaders.get('x-ms-client-principal');
1253
+ if (!principal) {
1254
+ const cookie = reqHeaders.get('cookie');
1255
+ const host = reqHeaders.get('host');
1256
+ if (cookie && host) {
1257
+ const proto = reqHeaders.get('x-forwarded-proto') || 'https';
1258
+ const meResponse = await fetch(\`\${proto}://\${host}/.auth/me\`, {
1259
+ headers: { cookie },
1260
+ cache: 'no-store',
1261
+ });
1262
+ if (meResponse.ok) {
1263
+ const payload = await meResponse.json() as { clientPrincipal?: unknown };
1264
+ if (payload.clientPrincipal) {
1265
+ principal = Buffer.from(JSON.stringify(payload.clientPrincipal), 'utf8').toString('base64');
1266
+ }
1267
+ }
1268
+ }
1269
+ }
1270
+ if (principal) {
1271
+ const parsed = JSON.parse(Buffer.from(principal, 'base64').toString('utf8'));
1272
+ const normalized = {
1273
+ identityProvider: String(parsed.identityProvider || ''),
1274
+ userId: String(parsed.userId || ''),
1275
+ userDetails: String(parsed.userDetails || ''),
1276
+ userRoles: Array.isArray(parsed.userRoles) ? parsed.userRoles.filter((role: unknown) => typeof role === 'string') : [],
1277
+ };
1278
+ if (normalized.userId && normalized.userRoles.includes('authenticated')) {
1279
+ fetchHeaders['x-ms-client-principal'] = Buffer.from(JSON.stringify(normalized), 'utf8').toString('base64');
1280
+ }
1281
+ }`
1282
+ );
1283
+ }
1284
+
1285
+ export function generateSwaAuthHelperTS(): string {
1286
+ return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
1287
+
1288
+ export interface AuthUser { id: string; loginId: string; roles: string[]; }
1289
+ export class AuthError extends Error { constructor(public status: number, message: string) { super(message); } }
1290
+
1291
+ export function requireAuth(request: HttpRequest): AuthUser {
1292
+ const encoded = request.headers.get('x-ms-client-principal');
1293
+ if (!encoded) throw new AuthError(401, 'Authentication required');
1294
+ try {
1295
+ const value = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
1296
+ const roles = Array.isArray(value.userRoles) ? value.userRoles.filter((r: unknown) => typeof r === 'string') : [];
1297
+ if (typeof value.userId !== 'string' || !value.userId || !roles.includes('authenticated')) throw new Error();
1298
+ return { id: value.userId, loginId: typeof value.userDetails === 'string' ? value.userDetails : '', roles };
1299
+ } catch { throw new AuthError(401, 'Invalid client principal'); }
1300
+ }
1301
+ export function requireRoles(user: AuthUser, roles: string[]): void {
1302
+ if (roles.length && !roles.some(role => user.roles.includes(role))) throw new AuthError(403, 'Insufficient permissions');
1303
+ }
1304
+ export function handleAuthError(error: unknown): HttpResponseInit | null {
1305
+ return error instanceof AuthError ? { status: error.status, jsonBody: { error: error.message } } : null;
1306
+ }
1307
+ `;
1308
+ }
1309
+
1310
+ // ============================================================
1311
+ // External token authentication
1312
+ // ============================================================
1313
+
1314
+ export function generateExternalTokenAdapter(): string {
1315
+ return `/** Replace these functions with your external IdP SDK integration. */
1316
+ export async function getToken(): Promise<string | null> { return null; }
1317
+ export async function login(): Promise<void> { throw new Error('External token login adapter is not configured'); }
1318
+ export async function logout(): Promise<void> { throw new Error('External token logout adapter is not configured'); }
1319
+ `;
1320
+ }
1321
+
1322
+ export function generateAuthenticatedFetch(): string {
1323
+ return `import { getToken } from './external-token-adapter';
1324
+
1325
+ export async function authenticatedFetch(input: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {
1326
+ const token = await getToken();
1327
+ if (!token) return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'content-type': 'application/json' } });
1328
+ const headers = new Headers(init.headers);
1329
+ headers.set('Authorization', \`Bearer \${token}\`);
1330
+ return fetch(input, { ...init, headers });
1331
+ }
1332
+ `;
1333
+ }
1334
+
1335
+ export function generateExternalTokenBFFMeRoute(): string {
1336
+ return `import { callFunction } from '@/lib/api/call-function';
1337
+ export async function GET() { return callFunction({ method: 'GET', path: '/api/auth/me' }); }
1338
+ `;
1339
+ }
1340
+
1341
+ export function generateExternalTokenAuthContext(): string {
1342
+ return `'use client';
1343
+ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
1344
+ import { authenticatedFetch } from './authenticated-fetch';
1345
+ import * as adapter from './external-token-adapter';
1346
+ export interface AuthUser { id: string; loginId: string; name: string; email: string; roles: string[]; }
1347
+ interface Value { user: AuthUser | null; loading: boolean; login: () => Promise<void>; logout: () => Promise<void>; hasRole: (role: string) => boolean; hasAnyRole: (roles: string[]) => boolean; }
1348
+ const Context = createContext<Value | null>(null);
1349
+ export function AuthProvider({ children }: { children: ReactNode }) {
1350
+ const [user, setUser] = useState<AuthUser | null>(null); const [loading, setLoading] = useState(true);
1351
+ useEffect(() => { authenticatedFetch('/api/auth/me').then(async r => {
1352
+ if (!r.ok || !(r.headers.get('content-type') || '').includes('application/json')) return null;
1353
+ return r.json() as Promise<AuthUser>;
1354
+ }).then(setUser).catch(() => setUser(null)).finally(() => setLoading(false)); }, []);
1355
+ const hasRole = (role: string) => user?.roles.includes(role) ?? false;
1356
+ const logout = async () => { await adapter.logout(); setUser(null); };
1357
+ return <Context.Provider value={{ user, loading, login: adapter.login, logout, hasRole, hasAnyRole: roles => roles.some(hasRole) }}>{children}</Context.Provider>;
1358
+ }
1359
+ export function useAuth() { const value = useContext(Context); if (!value) throw new Error('useAuth must be used within an AuthProvider'); return value; }
1360
+ `;
1361
+ }
1362
+
1363
+ export function generateExternalTokenVerifierTS(): string {
1364
+ return `export interface ExternalAuthPrincipal { userId: string; userDetails: string; roles: string[]; }
1365
+ export class ExternalTokenVerificationError extends Error { constructor(public status: 401 | 503, message: string) { super(message); } }
1366
+ /** Implement IdP verification here. Never decode and trust a token without verification. */
1367
+ export async function verifyExternalToken(_token: string): Promise<ExternalAuthPrincipal> {
1368
+ throw new Error('External token verifier is not configured');
1369
+ }
1370
+ `;
1371
+ }
1372
+
1373
+ export function generateExternalTokenHelperTS(): string {
1374
+ return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
1375
+ import { verifyExternalToken, ExternalTokenVerificationError, type ExternalAuthPrincipal } from './external-token-verifier';
1376
+ export class AuthError extends Error { constructor(public status: number, message: string) { super(message); } }
1377
+ export async function requireAuth(request: HttpRequest): Promise<ExternalAuthPrincipal> {
1378
+ const header = request.headers.get('authorization') || '';
1379
+ if (!header.startsWith('Bearer ') || !header.slice(7)) throw new AuthError(401, 'Missing or invalid Authorization header');
1380
+ try { const principal = await verifyExternalToken(header.slice(7));
1381
+ if (!principal?.userId || !Array.isArray(principal.roles)) throw new AuthError(401, 'Invalid external principal');
1382
+ return principal;
1383
+ } 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'); }
1384
+ }
1385
+ export function requireRoles(user: ExternalAuthPrincipal, roles: string[]) { if (!roles.some(role => user.roles.includes(role))) throw new AuthError(403, 'Insufficient permissions'); }
1386
+ export function handleAuthError(error: unknown): HttpResponseInit | null { return error instanceof AuthError ? { status: error.status, jsonBody: { error: error.message } } : null; }
1387
+ `;
1388
+ }
1389
+
1390
+ export function generateExternalAuthMeTS(): string {
1391
+ return `import { app, HttpRequest, HttpResponseInit } from '@azure/functions';
1392
+ import { requireAuth, handleAuthError } from './auth/jwt-helper';
1393
+ app.http('external-auth-me', { methods: ['GET'], route: 'auth/me', authLevel: 'function', handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
1394
+ try { const p = await requireAuth(request); return { status: 200, jsonBody: { id: p.userId, loginId: p.userDetails, name: p.userDetails, email: '', roles: p.roles } }; }
1395
+ catch (error) { return handleAuthError(error) || { status: 500, jsonBody: { error: 'Authentication failed' } }; }
1396
+ } });
1397
+ `;
1398
+ }
1399
+
1400
+ 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`; }
1401
+ 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`; }
1402
+ 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`; }
1403
+ 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`; }
1404
+ 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`; }
1405
+ 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`; }
1406
+ export function generateSwaAuthHelperPython(): string {
1407
+ return `import base64
1408
+ import json
1409
+ import azure.functions as func
1410
+
1411
+ class AuthError(Exception):
1412
+ def __init__(self, status, message): self.status, self.message = status, message
1413
+
1414
+ def require_auth(req):
1415
+ encoded = req.headers.get("x-ms-client-principal")
1416
+ if not encoded: raise AuthError(401, "Authentication required")
1417
+ try:
1418
+ value = json.loads(base64.b64decode(encoded).decode("utf-8"))
1419
+ roles = [r for r in value.get("userRoles", []) if isinstance(r, str)]
1420
+ if not value.get("userId") or "authenticated" not in roles: raise ValueError()
1421
+ return {"id": value["userId"], "loginId": value.get("userDetails", ""), "roles": roles}
1422
+ except Exception: raise AuthError(401, "Invalid client principal")
1423
+
1424
+ def require_roles(user, roles):
1425
+ if roles and not any(role in user["roles"] for role in roles): raise AuthError(403, "Insufficient permissions")
1426
+
1427
+ def handle_auth_error(error):
1428
+ return func.HttpResponse(json.dumps({"error": error.message}), status_code=error.status, mimetype="application/json") if isinstance(error, AuthError) else None
1429
+ `;
1430
+ }
1431
+
1432
+ export function generateSwaAuthHelperCSharp(): string {
1433
+ return `using System.Text;
1434
+ using System.Text.Json;
1435
+ using Microsoft.Azure.Functions.Worker.Http;
1436
+
1437
+ namespace Functions.Auth;
1438
+
1439
+ public class JwtPayload
1440
+ {
1441
+ public string Sub { get; set; } = "";
1442
+ public string LoginId { get; set; } = "";
1443
+ public string[] Roles { get; set; } = Array.Empty<string>();
1444
+ }
1445
+
1446
+ public static class JwtHelper
1447
+ {
1448
+ public static async Task<(JwtPayload?, HttpResponseData?)> Authorize(HttpRequestData request, params string[] requiredRoles)
1449
+ {
1450
+ try
1451
+ {
1452
+ var encoded = request.Headers.TryGetValues("x-ms-client-principal", out var values) ? values.FirstOrDefault() : null;
1453
+ if (string.IsNullOrWhiteSpace(encoded)) throw new InvalidOperationException();
1454
+ using var document = JsonDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(encoded)));
1455
+ var root = document.RootElement;
1456
+ var userId = root.GetProperty("userId").GetString() ?? "";
1457
+ var details = root.TryGetProperty("userDetails", out var detailValue) ? detailValue.GetString() ?? "" : "";
1458
+ var roles = root.TryGetProperty("userRoles", out var roleValue) && roleValue.ValueKind == JsonValueKind.Array
1459
+ ? roleValue.EnumerateArray().Where(v => v.ValueKind == JsonValueKind.String).Select(v => v.GetString()!).ToArray()
1460
+ : Array.Empty<string>();
1461
+ if (string.IsNullOrEmpty(userId) || !roles.Contains("authenticated")) throw new InvalidOperationException();
1462
+ if (requiredRoles.Length > 0 && !requiredRoles.Any(roles.Contains))
1463
+ {
1464
+ var forbidden = request.CreateResponse(System.Net.HttpStatusCode.Forbidden);
1465
+ await forbidden.WriteAsJsonAsync(new { error = "Forbidden" });
1466
+ return (null, forbidden);
1467
+ }
1468
+ return (new JwtPayload { Sub = userId, LoginId = details, Roles = roles }, null);
1469
+ }
1470
+ catch
1471
+ {
1472
+ var unauthorized = request.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
1473
+ await unauthorized.WriteAsJsonAsync(new { error = "Unauthorized" });
1474
+ return (null, unauthorized);
1475
+ }
1476
+ }
1477
+ }
1478
+ `;
1479
+ }
1480
+
1227
1481
  // ============================================================
1228
1482
  // 13. scaffold 用ロールガード挿入ヘルパー
1229
1483
  // ============================================================
@@ -1238,16 +1492,16 @@ export function generateAuthImportTS(): string {
1238
1492
  /**
1239
1493
  * TypeScript Functions のハンドラー先頭に挿入する認証チェックコードを生成
1240
1494
  */
1241
- export function generateAuthGuardTS(policy: ModelAuthPolicy, operation: 'read' | 'write'): string {
1495
+ export function generateAuthGuardTS(policy: ModelAuthPolicy, operation: 'read' | 'write', provider?: AuthProvider): string {
1242
1496
  const roles = operation === 'read'
1243
1497
  ? (policy.read || policy.roles || [])
1244
1498
  : (policy.write || policy.roles || []);
1245
1499
 
1246
1500
  if (roles.length > 0) {
1247
- return ` const authUser = requireAuth(request);
1501
+ return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);
1248
1502
  requireRoles(authUser, ${JSON.stringify(roles)});`;
1249
1503
  }
1250
- return ` const authUser = requireAuth(request);`;
1504
+ return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);`;
1251
1505
  }
1252
1506
 
1253
1507
  /**
@@ -11,6 +11,7 @@ import {
11
11
  ApiModelConnectorConfig,
12
12
  ConnectorOperation,
13
13
  ModelAuthPolicy,
14
+ AuthProvider,
14
15
  } from "../../types";
15
16
  import { generateAuthImportTS, generateAuthGuardTS } from "./auth-generator";
16
17
 
@@ -24,7 +25,8 @@ export function generateRdbConnectorFunctionTS(
24
25
  sharedPackageName: string,
25
26
  connectorDef: RdbConnectorConfig,
26
27
  modelConnector: RdbModelConnectorConfig,
27
- authPolicy?: ModelAuthPolicy
28
+ authPolicy?: ModelAuthPolicy,
29
+ authProvider?: AuthProvider
28
30
  ): string {
29
31
  const modelCamel = toCamelCase(model.name);
30
32
  const schemaName = model.schemaName;
@@ -123,7 +125,7 @@ export function generateRdbConnectorFunctionTS(
123
125
  // Auth guard setup
124
126
  const hasAuth = !!authPolicy;
125
127
  const authImport = hasAuth ? `\n${generateAuthImportTS()}\n` : '';
126
- const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read')}\n` : '';
128
+ const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read', authProvider)}\n` : '';
127
129
  const authCatchBlock = hasAuth
128
130
  ? ` const authErr = handleAuthError(error);
129
131
  if (authErr) return authErr;\n `
@@ -143,7 +145,7 @@ ${getConnection}
143
145
  app.http('${modelCamel}-get-all', {
144
146
  methods: ['GET'],
145
147
  route: '${modelCamel}',
146
- authLevel: 'anonymous',
148
+ authLevel: 'function',
147
149
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
148
150
  try {${readGuard}
149
151
  ${queryAll}
@@ -162,7 +164,7 @@ app.http('${modelCamel}-get-all', {
162
164
  app.http('${modelCamel}-get-by-id', {
163
165
  methods: ['GET'],
164
166
  route: '${modelCamel}/{id}',
165
- authLevel: 'anonymous',
167
+ authLevel: 'function',
166
168
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
167
169
  try {${readGuard}
168
170
  const id = request.params.id;
@@ -190,7 +192,8 @@ export function generateApiConnectorFunctionTS(
190
192
  sharedPackageName: string,
191
193
  connectorDef: ApiConnectorConfig,
192
194
  modelConnector: ApiModelConnectorConfig,
193
- authPolicy?: ModelAuthPolicy
195
+ authPolicy?: ModelAuthPolicy,
196
+ authProvider?: AuthProvider
194
197
  ): string {
195
198
  const modelCamel = toCamelCase(model.name);
196
199
  const schemaName = model.schemaName;
@@ -203,8 +206,8 @@ export function generateApiConnectorFunctionTS(
203
206
  // Auth guard setup (user access control, separate from connector auth to external APIs)
204
207
  const hasAuth = !!authPolicy;
205
208
  const authImportLine = hasAuth ? `\n${generateAuthImportTS()}\n` : '';
206
- const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read')}\n` : '';
207
- const writeGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'write')}\n` : '';
209
+ const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read', authProvider)}\n` : '';
210
+ const writeGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'write', authProvider)}\n` : '';
208
211
  const authCatchBlock = hasAuth
209
212
  ? ` const authErr = handleAuthError(error);
210
213
  if (authErr) return authErr;\n `
@@ -229,7 +232,7 @@ ${authSetup.helperCode}
229
232
  app.http('${modelCamel}-get-all', {
230
233
  methods: ['GET'],
231
234
  route: '${modelCamel}',
232
- authLevel: 'anonymous',
235
+ authLevel: 'function',
233
236
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
234
237
  try {${readGuard}
235
238
  const url = getBaseUrl() + '${epPath}';
@@ -263,7 +266,7 @@ app.http('${modelCamel}-get-all', {
263
266
  app.http('${modelCamel}-get-by-id', {
264
267
  methods: ['GET'],
265
268
  route: '${modelCamel}/{id}',
266
- authLevel: 'anonymous',
269
+ authLevel: 'function',
267
270
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
268
271
  try {${readGuard}
269
272
  const id = request.params.id;
@@ -303,7 +306,7 @@ app.http('${modelCamel}-get-by-id', {
303
306
  app.http('${modelCamel}-create', {
304
307
  methods: ['POST'],
305
308
  route: '${modelCamel}',
306
- authLevel: 'anonymous',
309
+ authLevel: 'function',
307
310
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
308
311
  try {${writeGuard}
309
312
  const body = await request.json();
@@ -341,7 +344,7 @@ app.http('${modelCamel}-create', {
341
344
  app.http('${modelCamel}-update', {
342
345
  methods: ['PUT'],
343
346
  route: '${modelCamel}/{id}',
344
- authLevel: 'anonymous',
347
+ authLevel: 'function',
345
348
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
346
349
  try {${writeGuard}
347
350
  const id = request.params.id;
@@ -381,7 +384,7 @@ app.http('${modelCamel}-update', {
381
384
  app.http('${modelCamel}-delete', {
382
385
  methods: ['DELETE'],
383
386
  route: '${modelCamel}/{id}',
384
- authLevel: 'anonymous',
387
+ authLevel: 'function',
385
388
  handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
386
389
  try {${writeGuard}
387
390
  const id = request.params.id;
@@ -496,7 +499,7 @@ export function generateRdbConnectorFunctionCSharp(
496
499
  methods += `
497
500
  [Function("${modelCamel}GetAll")]
498
501
  public async Task<HttpResponseData> GetAll(
499
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}")] HttpRequestData request)
502
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}")] HttpRequestData request)
500
503
  {
501
504
  try
502
505
  {
@@ -531,7 +534,7 @@ export function generateRdbConnectorFunctionCSharp(
531
534
  methods += `
532
535
  [Function("${modelCamel}GetById")]
533
536
  public async Task<HttpResponseData> GetById(
534
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
537
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
535
538
  string id)
536
539
  {
537
540
  try
@@ -618,7 +621,7 @@ export function generateApiConnectorFunctionCSharp(
618
621
  methods += `
619
622
  [Function("${modelCamel}GetAll")]
620
623
  public async Task<HttpResponseData> GetAll(
621
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}")] HttpRequestData request)
624
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}")] HttpRequestData request)
622
625
  {
623
626
  try
624
627
  {
@@ -651,7 +654,7 @@ export function generateApiConnectorFunctionCSharp(
651
654
  methods += `
652
655
  [Function("${modelCamel}GetById")]
653
656
  public async Task<HttpResponseData> GetById(
654
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
657
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "${modelCamel}/{id}")] HttpRequestData request,
655
658
  string id)
656
659
  {
657
660
  try
@@ -689,7 +692,7 @@ export function generateApiConnectorFunctionCSharp(
689
692
  methods += `
690
693
  [Function("${modelCamel}Create")]
691
694
  public async Task<HttpResponseData> Create(
692
- [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "${modelCamel}")] HttpRequestData request)
695
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "${modelCamel}")] HttpRequestData request)
693
696
  {
694
697
  try
695
698
  {
@@ -725,7 +728,7 @@ export function generateApiConnectorFunctionCSharp(
725
728
  methods += `
726
729
  [Function("${modelCamel}Update")]
727
730
  public async Task<HttpResponseData> Update(
728
- [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "${modelCamel}/{id}")] HttpRequestData request,
731
+ [HttpTrigger(AuthorizationLevel.Function, "put", Route = "${modelCamel}/{id}")] HttpRequestData request,
729
732
  string id)
730
733
  {
731
734
  try
@@ -759,7 +762,7 @@ export function generateApiConnectorFunctionCSharp(
759
762
  methods += `
760
763
  [Function("${modelCamel}Delete")]
761
764
  public async Task<HttpResponseData> Delete(
762
- [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "${modelCamel}/{id}")] HttpRequestData request,
765
+ [HttpTrigger(AuthorizationLevel.Function, "delete", Route = "${modelCamel}/{id}")] HttpRequestData request,
763
766
  string id)
764
767
  {
765
768
  try