auth: write auth store and model cache atomically

This commit is contained in:
Adrià Arrufat
2026-07-29 09:26:56 +02:00
parent 41e365b3f4
commit 6070119ca8
2 changed files with 20 additions and 17 deletions

View File

@@ -133,13 +133,15 @@ fn storeSaveAt(arena: std.mem.Allocator, dir: []const u8, id: []const u8, tokens
.account_id = tokens.account_id,
});
// Secrets file: owner read/write only, from the moment it exists.
const file = try std.Io.Dir.cwd().createFile(lp.io, path, .{ .permissions = .fromMode(0o600) });
defer file.close(lp.io);
// Secrets file: owner read/write only, from the moment it exists. Written
// to a temp file and renamed so a failed write can't corrupt the store.
var af = try std.Io.Dir.cwd().createFileAtomic(lp.io, path, .{ .permissions = .fromMode(0o600), .replace = true });
defer af.deinit(lp.io);
var buf: [1024]u8 = undefined;
var w = file.writer(lp.io, &buf);
var w = af.file.writer(lp.io, &buf);
try std.json.Stringify.value(map, .{}, &w.interface);
try w.end();
try af.replace(lp.io);
}
fn storeDeleteAt(arena: std.mem.Allocator, dir: []const u8, id: []const u8) void {
@@ -147,12 +149,13 @@ fn storeDeleteAt(arena: std.mem.Allocator, dir: []const u8, id: []const u8) void
const data = std.Io.Dir.cwd().readFileAlloc(lp.io, path, arena, .limited(64 * 1024)) catch return;
var parsed = std.json.parseFromSliceLeaky(std.json.ArrayHashMap(StoredToken), arena, data, .{ .ignore_unknown_fields = true }) catch return;
_ = parsed.map.swapRemove(id);
const file = std.Io.Dir.cwd().createFile(lp.io, path, .{ .permissions = .fromMode(0o600) }) catch return;
defer file.close(lp.io);
var af = std.Io.Dir.cwd().createFileAtomic(lp.io, path, .{ .permissions = .fromMode(0o600), .replace = true }) catch return;
defer af.deinit(lp.io);
var buf: [1024]u8 = undefined;
var w = file.writer(lp.io, &buf);
var w = af.file.writer(lp.io, &buf);
std.json.Stringify.value(parsed, .{}, &w.interface) catch return;
w.end() catch return;
af.replace(lp.io) catch return;
}
/// Load the stored token for `id`, or null when absent/unreadable/no data dir.

View File

@@ -40,7 +40,7 @@ pub fn modelIds(arena: std.mem.Allocator, provider_id: []const u8, app_dir: ?[]c
}
const catalog = fetch(arena) catch return &.{};
const ids = parseProviderModels(arena, catalog, provider_id) catch return &.{};
if (app_dir) |dir| writeCache(dir, provider_id, ids) catch {};
if (app_dir) |dir| writeCache(arena, dir, provider_id, ids) catch {};
return ids;
}
@@ -63,15 +63,15 @@ fn readCache(arena: std.mem.Allocator, app_dir: []const u8, provider_id: []const
return parsed.ids;
}
fn writeCache(app_dir: []const u8, provider_id: []const u8, ids: []const []const u8) !void {
var arena: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
defer arena.deinit();
const a = arena.allocator();
const path = try cachePath(a, app_dir, provider_id);
var buf: std.Io.Writer.Allocating = .init(a);
try std.json.Stringify.value(Cache{ .fetched_ms = auth.nowMs(), .ids = ids }, .{}, &buf.writer);
try std.Io.Dir.cwd().writeFile(lp.io, .{ .sub_path = path, .data = buf.written() });
fn writeCache(arena: std.mem.Allocator, app_dir: []const u8, provider_id: []const u8, ids: []const []const u8) !void {
const path = try cachePath(arena, app_dir, provider_id);
var af = try std.Io.Dir.cwd().createFileAtomic(lp.io, path, .{ .replace = true });
defer af.deinit(lp.io);
var buf: [1024]u8 = undefined;
var w = af.file.writer(lp.io, &buf);
try std.json.Stringify.value(Cache{ .fetched_ms = auth.nowMs(), .ids = ids }, .{}, &w.interface);
try w.end();
try af.replace(lp.io);
}
/// Model-id keys of `catalog[provider_id].models`, filtered to tool-call-capable