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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/cli/commands/add-auth.d.ts +1 -0
  2. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  3. package/dist/cli/commands/add-auth.js +136 -1
  4. package/dist/cli/commands/add-auth.js.map +1 -1
  5. package/dist/cli/commands/dev.js +5 -2
  6. package/dist/cli/commands/dev.js.map +1 -1
  7. package/dist/cli/commands/init.d.ts +3 -0
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +68 -27
  10. package/dist/cli/commands/init.js.map +1 -1
  11. package/dist/cli/commands/scaffold.d.ts +9 -0
  12. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  13. package/dist/cli/commands/scaffold.js +56 -12
  14. package/dist/cli/commands/scaffold.js.map +1 -1
  15. package/dist/cli/index.d.ts.map +1 -1
  16. package/dist/cli/index.js +4 -0
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/core/config.d.ts +7 -2
  19. package/dist/core/config.d.ts.map +1 -1
  20. package/dist/core/config.js +81 -1
  21. package/dist/core/config.js.map +1 -1
  22. package/dist/core/mock/connector-mock-server.js +2 -2
  23. package/dist/core/mock/connector-mock-server.js.map +1 -1
  24. package/dist/core/project/manifest.js +2 -2
  25. package/dist/core/project/manifest.js.map +1 -1
  26. package/dist/core/project/validation.d.ts.map +1 -1
  27. package/dist/core/project/validation.js +17 -0
  28. package/dist/core/project/validation.js.map +1 -1
  29. package/dist/core/scaffold/auth-generator.d.ts +9 -2
  30. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  31. package/dist/core/scaffold/auth-generator.js +230 -13
  32. package/dist/core/scaffold/auth-generator.js.map +1 -1
  33. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
  34. package/dist/core/scaffold/connector-functions-generator.js +4 -2
  35. package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
  36. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  37. package/dist/core/scaffold/functions-generator.js +4 -2
  38. package/dist/core/scaffold/functions-generator.js.map +1 -1
  39. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  40. package/dist/core/scaffold/model-parser.js +8 -3
  41. package/dist/core/scaffold/model-parser.js.map +1 -1
  42. package/dist/core/scaffold/ui-generator.d.ts +3 -0
  43. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  44. package/dist/core/scaffold/ui-generator.js +12 -8
  45. package/dist/core/scaffold/ui-generator.js.map +1 -1
  46. package/dist/types/index.d.ts +34 -4
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/utils/package-manager.d.ts.map +1 -1
  49. package/dist/utils/package-manager.js +1 -0
  50. package/dist/utils/package-manager.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/__tests__/auth.test.ts +4 -0
  53. package/src/__tests__/config.test.ts +22 -1
  54. package/src/__tests__/dev.test.ts +8 -0
  55. package/src/__tests__/init.test.ts +8 -0
  56. package/src/__tests__/mcp.test.ts +2 -2
  57. package/src/__tests__/multi-auth.test.ts +133 -0
  58. package/src/cli/commands/add-auth.ts +112 -2
  59. package/src/cli/commands/dev.ts +6 -3
  60. package/src/cli/commands/init.ts +73 -28
  61. package/src/cli/commands/scaffold.ts +58 -14
  62. package/src/cli/index.ts +4 -0
  63. package/src/core/config.ts +69 -2
  64. package/src/core/mock/connector-mock-server.ts +2 -2
  65. package/src/core/project/manifest.ts +3 -3
  66. package/src/core/project/validation.ts +16 -0
  67. package/src/core/scaffold/auth-generator.ts +236 -14
  68. package/src/core/scaffold/connector-functions-generator.ts +4 -2
  69. package/src/core/scaffold/functions-generator.ts +4 -2
  70. package/src/core/scaffold/model-parser.ts +9 -3
  71. package/src/core/scaffold/ui-generator.ts +15 -8
  72. package/src/types/index.ts +35 -4
  73. package/src/utils/package-manager.ts +1 -0
@@ -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
 
@@ -95,12 +95,25 @@ export interface SwaAuthConfig {
95
95
 
96
96
  // 認可設定
97
97
  export interface AuthorizationConfig {
98
- defaultPolicy?: "authenticated" | "anonymous";
98
+ defaultPolicy?: "authenticated" | "anonymous" | "public" | string;
99
+ policies?: Record<string, AuthorizationPolicy>;
100
+ }
101
+
102
+ export interface AuthorizationPolicy {
103
+ schemes: string[];
104
+ roles?: string[];
105
+ }
106
+
107
+ export interface AuthSchemeConfig {
108
+ provider: AuthProvider;
109
+ customJwt?: CustomJwtConfig;
110
+ swa?: SwaAuthConfig;
99
111
  }
100
112
 
101
113
  // 認証設定
102
114
  export interface AuthConfig {
103
- provider: AuthProvider;
115
+ provider?: AuthProvider;
116
+ schemes?: Record<string, AuthSchemeConfig>;
104
117
  customJwt?: CustomJwtConfig;
105
118
  swa?: SwaAuthConfig;
106
119
  authorization?: AuthorizationConfig;
@@ -109,8 +122,26 @@ export interface AuthConfig {
109
122
  // モデルに付与する認可ポリシー
110
123
  export interface ModelAuthPolicy {
111
124
  roles?: string[];
112
- read?: string[];
113
- write?: string[];
125
+ read?: string[] | string;
126
+ write?: string[] | string;
127
+ policy?: string;
128
+ }
129
+
130
+ export interface NormalizedAuthConfig extends Omit<AuthConfig, "schemes" | "authorization"> {
131
+ schemes: Record<string, AuthSchemeConfig>;
132
+ authorization: AuthorizationConfig & { defaultPolicy: string; policies: Record<string, AuthorizationPolicy> };
133
+ }
134
+
135
+ export interface AuthPrincipal {
136
+ subject: string;
137
+ scheme: string;
138
+ issuer: string;
139
+ roles: string[];
140
+ claims: Record<string, unknown>;
141
+ /** @deprecated Use subject. */
142
+ userId?: string;
143
+ /** @deprecated Read provider data from claims. */
144
+ userDetails?: string;
114
145
  }
115
146
 
116
147
  // CLI設定の型
@@ -178,6 +178,7 @@ export function getCiSetupStep(pm: PackageManager): string {
178
178
  }
179
179
 
180
180
  export function getCiInstallCommand(ciPm: PackageManager, projectPm: PackageManager = ciPm): string {
181
+ void projectPm; // Reserved for package-manager-specific CI bridging.
181
182
  if (ciPm === "pnpm") {
182
183
  return getCommands(ciPm).ci;
183
184
  }