diff options
Diffstat (limited to 'libpanto/src/auth.zig')
| -rw-r--r-- | libpanto/src/auth.zig | 42 |
1 files changed, 24 insertions, 18 deletions
diff --git a/libpanto/src/auth.zig b/libpanto/src/auth.zig index 9bf37b4..5b65bf4 100644 --- a/libpanto/src/auth.zig +++ b/libpanto/src/auth.zig @@ -43,10 +43,12 @@ pub const DeviceDialect = enum { token, codex }; /// Wire encoding for the `token` dialect's device-code / poll request bodies. pub const TokenRequestFormat = enum { form, json }; -/// Static API-key auth. `key` (a literal) wins over `key_env_var`. +/// Static API-key auth. `key` is the resolved credential — a literal, or the +/// result of `${env:VAR}`-style substitution performed by the embedder. An +/// absent/empty key means unresolved; providers using such a session are +/// omitted from the active config (export the key and they reappear). pub const ApiKeyAuth = struct { key: ?[]const u8 = null, - key_env_var: ?[]const u8 = null, }; /// A secondary token exchange run after the durable OAuth token is obtained @@ -64,8 +66,6 @@ pub const ExchangeConfig = struct { expires_at_json_path: ?[]const u8 = null, /// Dotted JSON path to a dynamic `base_url` in the exchange response. base_url_json_path: ?[]const u8 = null, - /// Headers sent with the exchange request (e.g. Copilot editor identity). - headers: []const Header = &.{}, pub const BearerSource = enum { oauth_access_token }; }; @@ -93,8 +93,6 @@ pub const OAuthDeviceAuth = struct { /// e.g. `https://api.openai.com/auth`. When set, the resolver extracts an /// account id and emits it as a header (see provider wiring). account_id_jwt_claim: ?[]const u8 = null, - /// Headers sent with the device-code / poll / token requests. - headers: []const Header = &.{}, /// Optional post-login token exchange (Copilot). exchange: ?ExchangeConfig = null, }; @@ -365,11 +363,14 @@ fn encodeBody( } } -/// Request a device/user code. Allocates the result into `arena`. +/// Request a device/user code. `headers` are the client-identity headers +/// (the provider's `extra_headers`) sent on every auth HTTP call. Allocates +/// the result into `arena`. fn requestDeviceAuth( arena: std.mem.Allocator, client: *std.http.Client, oauth: OAuthDeviceAuth, + headers: []const Header, ) !DeviceAuth { const enc = switch (oauth.dialect) { // GitHub: `client_id` (+ optional scope), in the configured encoding. @@ -383,7 +384,7 @@ fn requestDeviceAuth( }; const resp = try http.request(arena, client, .POST, oauth.device_code_url, .{ - .headers = oauth.headers, + .headers = headers, .body = enc.body, .content_type = enc.content_type, }); @@ -429,6 +430,7 @@ fn pollOnce( client: *std.http.Client, oauth: OAuthDeviceAuth, da: DeviceAuth, + headers: []const Header, ) !PollResult { switch (oauth.dialect) { .token => { @@ -438,7 +440,7 @@ fn pollOnce( .{ "grant_type", "urn:ietf:params:oauth:grant-type:device_code" }, }); const resp = try http.request(arena, client, .POST, oauth.token_url, .{ - .headers = oauth.headers, + .headers = headers, .body = enc.body, .content_type = enc.content_type, }); @@ -460,7 +462,7 @@ fn pollOnce( .{ "user_code", da.user_code }, }); const resp = try http.request(arena, client, .POST, poll_url, .{ - .headers = oauth.headers, + .headers = headers, .body = enc.body, .content_type = enc.content_type, }); @@ -472,7 +474,7 @@ fn pollOnce( const code = http.jsonStringAtPath(parsed.value, "authorization_code") orelse return .pending; const verifier = http.jsonStringAtPath(parsed.value, "code_verifier") orelse return AuthError.AuthFlowFailed; // Exchange the authorization code + server-generated PKCE verifier. - const tokens = try exchangeCode(arena, client, oauth, code, verifier); + const tokens = try exchangeCode(arena, client, oauth, code, verifier, headers); return .{ .done = tokens }; }, } @@ -485,6 +487,7 @@ fn exchangeCode( oauth: OAuthDeviceAuth, code: []const u8, verifier: []const u8, + headers: []const Header, ) !OAuthTokens { const redirect = oauth.redirect_uri orelse "https://auth.openai.com/deviceauth/callback"; const enc = try encodeBody(arena, .form, &.{ @@ -495,7 +498,7 @@ fn exchangeCode( .{ "redirect_uri", redirect }, }); const resp = try http.request(arena, client, .POST, oauth.token_url, .{ - .headers = oauth.headers, + .headers = headers, .body = enc.body, .content_type = enc.content_type, }); @@ -511,6 +514,7 @@ pub fn refreshTokens( client: *std.http.Client, oauth: OAuthDeviceAuth, refresh_token: []const u8, + headers: []const Header, ) !OAuthTokens { const enc = try encodeBody(arena, .form, &.{ .{ "grant_type", "refresh_token" }, @@ -518,7 +522,7 @@ pub fn refreshTokens( .{ "client_id", oauth.client_id }, }); const resp = try http.request(arena, client, .POST, oauth.token_url, .{ - .headers = oauth.headers, + .headers = headers, .body = enc.body, .content_type = enc.content_type, }); @@ -549,8 +553,9 @@ pub fn login( client: *std.http.Client, oauth: OAuthDeviceAuth, presenter: Presenter, + headers: []const Header, ) !OAuthTokens { - const da = try requestDeviceAuth(arena, client, oauth); + const da = try requestDeviceAuth(arena, client, oauth, headers); presenter.deviceCode(.{ .verification_uri = da.verification_uri, .user_code = da.user_code, @@ -562,7 +567,7 @@ pub fn login( const deadline_ns = start_ns + 15 * std.time.ns_per_min; while (true) { io.sleep(.fromSeconds(@intCast(da.interval_secs)), .real) catch {}; - const result = pollOnce(arena, client, oauth, da) catch |err| switch (err) { + const result = pollOnce(arena, client, oauth, da, headers) catch |err| switch (err) { // A transient transport hiccup mid-poll: keep waiting. error.AuthFlowFailed => return err, else => return err, @@ -587,11 +592,12 @@ pub fn runExchange( client: *std.http.Client, exchange: ExchangeConfig, bearer: []const u8, + headers: []const Header, ) !TokenSet.ExchangeToken { const auth_value = try std.fmt.allocPrint(arena, "Bearer {s}", .{bearer}); var hdrs: std.ArrayList(Header) = .empty; try hdrs.append(arena, .{ .name = "authorization", .value = auth_value }); - for (exchange.headers) |h| try hdrs.append(arena, h); + for (headers) |h| try hdrs.append(arena, h); const method: http.Method = if (std.ascii.eqlIgnoreCase(exchange.method, "POST")) .POST else .GET; const resp = try http.request(arena, client, method, exchange.url, .{ .headers = hdrs.items }); @@ -781,9 +787,9 @@ test "token storage: save, load, delete round-trip" { } test "AuthConfig: api_key variant" { - const a: AuthConfig = .{ .api_key = .{ .key_env_var = "OPENAI_API_KEY" } }; + const a: AuthConfig = .{ .api_key = .{ .key = "sk-resolved" } }; try t.expectEqual(AuthType.api_key, @as(AuthType, a)); - try t.expectEqualStrings("OPENAI_API_KEY", a.api_key.key_env_var.?); + try t.expectEqualStrings("sk-resolved", a.api_key.key.?); } test "AuthConfig: oauth_device defaults" { |
