P4.3: token security at rest + dist token CLI

Subplan 02 Slice 5: Token domain entity (scopes, app/channel scoping,
expiry, revocation, last-used) with boundary validation; secrets are
dist_<64 hex> drawn from arc4random_buf and only their SHA-256 is
persisted. check_token gates revocation > expiry > scope > app/channel;
mark_token_used stamps usage for the P4.4 server auth.

CLI: dist token create (raw secret shown exactly once; works on a fresh
store so CI tokens can predate the first publish), list (lifecycle
status, never the secret), revoke (unknown id and double-revoke are
distinct errors). Every mutation appends an audit event; tokens joins
db.json's persisted arrays, with an absent member loading as empty so
older db.json files stay readable.

make test 16/16 (new: token_check.sx unit suite, token_ops.sx pinned
CLI acceptance).
This commit is contained in:
agra
2026-06-12 10:52:08 +03:00
parent 6c19f1073f
commit d8b7a7bfb3
8 changed files with 1217 additions and 2 deletions

View File

@@ -20,6 +20,7 @@
#import "../domain/release.sx";
#import "../domain/artifact.sx";
#import "../domain/channel.sx";
#import "../domain/token.sx";
#import "../domain/audit.sx";
#import "../domain/validate.sx";
@@ -44,6 +45,7 @@ Repo :: struct {
releases: List(Release);
artifacts: List(Artifact);
channels: List(Channel);
tokens: List(Token);
audit_events: List(AuditEvent);
// Capture the owning allocator. The List fields default to empty
@@ -170,6 +172,39 @@ Repo :: struct {
return true;
}
// ── Tokens ───────────────────────────────────────────────────────
create_token :: (self: *Repo, t: Token) {
self.tokens.append(t, self.own_allocator);
}
get_token :: (self: *Repo, id: string) -> ?Token {
i := 0;
while i < self.tokens.len {
if self.tokens.items[i].id == id { return self.tokens.items[i]; }
i += 1;
}
return null;
}
// The auth lookup: a presented secret is hashed and matched here.
find_token_by_hash :: (self: *Repo, token_hash: string) -> ?Token {
i := 0;
while i < self.tokens.len {
if self.tokens.items[i].token_hash == token_hash { return self.tokens.items[i]; }
i += 1;
}
return null;
}
list_tokens :: (self: *Repo) -> []Token {
return .{ ptr = self.tokens.items, len = self.tokens.len };
}
update_token :: (self: *Repo, t: Token) -> bool {
i := 0;
while i < self.tokens.len {
if self.tokens.items[i].id == t.id { self.tokens.items[i] = t; return true; }
i += 1;
}
return false;
}
// ── Audit events ─────────────────────────────────────────────────
create_audit_event :: (self: *Repo, e: AuditEvent) {
self.audit_events.append(e, self.own_allocator);