add server api key support
This commit is contained in:
parent
027f3b67ab
commit
a978b2352e
3 changed files with 41 additions and 2 deletions
|
|
@ -49,6 +49,7 @@ free-tier limits.
|
|||
| Variable | Purpose |
|
||||
|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `ZFIN_SERVER` | Optional URL of a remote [zfin-server](https://git.lerch.org/lobo/zfin-server) instance: a shared cache tier queried between the local cache and the providers. No-ops when unset. See [server sync](../../explanation/caching.md#server-sync-zfin_server). |
|
||||
| `ZFIN_SERVER_API_KEY` | Optional API key sent as the `X-API-Key` header on every `ZFIN_SERVER` request. Set it when the server requires auth for cache endpoints; leave unset for an open server. No-ops when `ZFIN_SERVER` is unset. |
|
||||
|
||||
## Output
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,13 @@ openfigi_key: ?[]const u8 = null,
|
|||
user_email: ?[]const u8 = null,
|
||||
/// URL of a zfin-server instance for lazy cache sync (e.g. "https://zfin.lerch.org")
|
||||
server_url: ?[]const u8 = null,
|
||||
/// Optional API key for the `server_url` instance above. Sent as the
|
||||
/// `X-API-Key` header on every server-sync request. Sourced from
|
||||
/// `ZFIN_SERVER_API_KEY` - the same variable the server reads to require
|
||||
/// the key, so a host running both just sets it once. Null when unset:
|
||||
/// requests then go out unauthenticated, which is correct against an open
|
||||
/// server or one still in its pre-enforcement soft cutover.
|
||||
server_api_key: ?[]const u8 = null,
|
||||
cache_dir: []const u8,
|
||||
cache_dir_owned: bool = false, // true when cache_dir was allocated via path.join
|
||||
zfin_home: ?[]const u8 = null,
|
||||
|
|
@ -97,6 +104,7 @@ pub fn fromEnv(io: std.Io, allocator: std.mem.Allocator, environ_map: *const std
|
|||
self.openfigi_key = self.resolve("OPENFIGI_API_KEY");
|
||||
self.user_email = self.resolve("ZFIN_USER_EMAIL");
|
||||
self.server_url = self.resolve("ZFIN_SERVER");
|
||||
self.server_api_key = self.resolve("ZFIN_SERVER_API_KEY");
|
||||
|
||||
const env_cache = self.resolve("ZFIN_CACHE_DIR");
|
||||
self.cache_dir = env_cache orelse blk: {
|
||||
|
|
|
|||
|
|
@ -2710,6 +2710,18 @@ pub const DataService = struct {
|
|||
}
|
||||
}
|
||||
|
||||
/// Build the auth header slice for a server-sync request. Writes the
|
||||
/// `X-API-Key` header into `buf` (caller-owned; must outlive the
|
||||
/// request) and returns a one-element slice of it, or an empty slice
|
||||
/// when no key is configured. The key points into `Config`
|
||||
/// (process-lifetime), so the value slice stays valid. Free function
|
||||
/// (not a method) so it's unit-testable without a `DataService`.
|
||||
fn serverAuthHeaders(server_api_key: ?[]const u8, buf: *[1]std.http.Header) []const std.http.Header {
|
||||
const key = server_api_key orelse return &.{};
|
||||
buf[0] = .{ .name = "X-API-Key", .value = key };
|
||||
return buf[0..1];
|
||||
}
|
||||
|
||||
/// L2 seam: fetch the whole CUSIP->ticker map from the server via
|
||||
/// `GET {server}/cusips`. Returns the raw SRF body (caller frees
|
||||
/// with `self.allocator`) or null on any failure. Best-effort: no
|
||||
|
|
@ -2723,7 +2735,9 @@ pub const DataService = struct {
|
|||
var client = http.Client.init(self.io, self.allocator);
|
||||
defer client.deinit();
|
||||
|
||||
var response = client.get(url) catch |err| {
|
||||
var hdr_buf: [1]std.http.Header = .{.{ .name = "", .value = "" }};
|
||||
const extra_headers = serverAuthHeaders(self.config.server_api_key, &hdr_buf);
|
||||
var response = client.request(.GET, url, null, extra_headers) catch |err| {
|
||||
log.debug("cusips server sync failed: {s}", .{@errorName(err)});
|
||||
return null;
|
||||
};
|
||||
|
|
@ -2878,7 +2892,9 @@ pub const DataService = struct {
|
|||
var client = http.Client.init(self.io, self.allocator);
|
||||
defer client.deinit();
|
||||
|
||||
var response = client.get(full_url) catch |err| {
|
||||
var hdr_buf: [1]std.http.Header = .{.{ .name = "", .value = "" }};
|
||||
const extra_headers = serverAuthHeaders(self.config.server_api_key, &hdr_buf);
|
||||
var response = client.request(.GET, full_url, null, extra_headers) catch |err| {
|
||||
const elapsed_ms = @divTrunc(std.Io.Timestamp.now(self.io, .awake).nanoseconds - t_start, std.time.ns_per_ms);
|
||||
// Operator-visible: surfaces meaningful failures
|
||||
// (`NoAddressReturned`, `ConnectionRefused`,
|
||||
|
|
@ -3034,6 +3050,20 @@ pub const DataService = struct {
|
|||
|
||||
// ── Tests ─────────────────────────────────────────────────────────
|
||||
|
||||
test "serverAuthHeaders: key present yields one X-API-Key header" {
|
||||
var buf: [1]std.http.Header = .{.{ .name = "", .value = "" }};
|
||||
const h = DataService.serverAuthHeaders("s3cret", &buf);
|
||||
try std.testing.expectEqual(@as(usize, 1), h.len);
|
||||
try std.testing.expectEqualStrings("X-API-Key", h[0].name);
|
||||
try std.testing.expectEqualStrings("s3cret", h[0].value);
|
||||
}
|
||||
|
||||
test "serverAuthHeaders: null key yields no headers" {
|
||||
var buf: [1]std.http.Header = .{.{ .name = "", .value = "" }};
|
||||
const h = DataService.serverAuthHeaders(null, &buf);
|
||||
try std.testing.expectEqual(@as(usize, 0), h.len);
|
||||
}
|
||||
|
||||
test "isPermanentProviderFailure: NotFound is permanent" {
|
||||
try std.testing.expect(isPermanentProviderFailure(error.NotFound));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue