feat: add configuration, logging, metrics, scheduler and storage foundation
- config: defaults -> JSON file -> NOEMA_* env with validation and redaction - observability: slog with secret-redacting handler; Prometheus registry with the metric set from the plan (events, workspace, memory, LLM, tools, queues) - scheduler: supervised Group and bounded Pool so no goroutine is unowned - storage: pgx pool with statement timeout, embedded migration runner (advisory lock, checksums, up/down/status), per-test schema helper - migrations/0001: append-only guard trigger, users, sessions, api tokens, recovery codes, audit log, settings - pkg/ids: UUIDv7 with monotonic counter
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
DROP TABLE IF EXISTS settings;
|
||||
DROP TABLE IF EXISTS audit_log;
|
||||
DROP TABLE IF EXISTS api_tokens;
|
||||
DROP TABLE IF EXISTS sessions;
|
||||
DROP TABLE IF EXISTS recovery_codes;
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP FUNCTION IF EXISTS noema_forbid_change();
|
||||
@@ -0,0 +1,85 @@
|
||||
-- Foundation: append-only guard, users, sessions, API tokens, audit log, settings.
|
||||
|
||||
-- Raise on any UPDATE/DELETE. Attached to every history table so immutability
|
||||
-- is enforced by the database, not by application discipline (ADR-0002).
|
||||
CREATE OR REPLACE FUNCTION noema_forbid_change() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'table % is append-only', TG_TABLE_NAME USING ERRCODE = 'integrity_constraint_violation';
|
||||
END $$;
|
||||
|
||||
CREATE TABLE users (
|
||||
id uuid PRIMARY KEY,
|
||||
username text NOT NULL,
|
||||
password_hash text NOT NULL,
|
||||
role text NOT NULL CHECK (role IN ('administrator','operator','observer')),
|
||||
disabled boolean NOT NULL DEFAULT false,
|
||||
failed_attempts integer NOT NULL DEFAULT 0,
|
||||
locked_until timestamptz,
|
||||
totp_secret_enc text,
|
||||
totp_enabled boolean NOT NULL DEFAULT false,
|
||||
password_changed_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE UNIQUE INDEX users_username_lower_idx ON users (lower(username));
|
||||
|
||||
CREATE TABLE recovery_codes (
|
||||
id uuid PRIMARY KEY,
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
code_hash bytea NOT NULL,
|
||||
used_at timestamptz
|
||||
);
|
||||
CREATE INDEX recovery_codes_user_idx ON recovery_codes (user_id);
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id uuid PRIMARY KEY,
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash bytea NOT NULL UNIQUE,
|
||||
csrf_token text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
last_seen_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL,
|
||||
ip text NOT NULL DEFAULT '',
|
||||
user_agent text NOT NULL DEFAULT '',
|
||||
revoked_at timestamptz
|
||||
);
|
||||
CREATE INDEX sessions_user_idx ON sessions (user_id) WHERE revoked_at IS NULL;
|
||||
|
||||
CREATE TABLE api_tokens (
|
||||
id uuid PRIMARY KEY,
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
token_hash bytea NOT NULL UNIQUE,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
last_used_at timestamptz,
|
||||
expires_at timestamptz,
|
||||
revoked_at timestamptz
|
||||
);
|
||||
CREATE INDEX api_tokens_user_idx ON api_tokens (user_id);
|
||||
|
||||
-- Administrative actions. Append-only.
|
||||
CREATE TABLE audit_log (
|
||||
id uuid PRIMARY KEY,
|
||||
seq bigserial NOT NULL,
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
actor_kind text NOT NULL CHECK (actor_kind IN ('user','system','mind')),
|
||||
actor_id text NOT NULL DEFAULT '',
|
||||
actor_name text NOT NULL DEFAULT '',
|
||||
action text NOT NULL,
|
||||
target_kind text NOT NULL DEFAULT '',
|
||||
target_id text NOT NULL DEFAULT '',
|
||||
details jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
ip text NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE INDEX audit_log_seq_idx ON audit_log (seq);
|
||||
CREATE INDEX audit_log_target_idx ON audit_log (target_kind, target_id);
|
||||
CREATE TRIGGER audit_log_append_only BEFORE UPDATE OR DELETE ON audit_log
|
||||
FOR EACH ROW EXECUTE FUNCTION noema_forbid_change();
|
||||
|
||||
-- Runtime-editable settings (tunables only, never secrets).
|
||||
CREATE TABLE settings (
|
||||
key text PRIMARY KEY,
|
||||
value jsonb NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_by text NOT NULL DEFAULT ''
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
// Package migrations embeds the SQL schema migrations applied by
|
||||
// internal/storage. Files are named NNNN_name.up.sql / NNNN_name.down.sql and
|
||||
// are never edited once applied anywhere.
|
||||
package migrations
|
||||
|
||||
import "embed"
|
||||
|
||||
// FS contains every migration file.
|
||||
//
|
||||
//go:embed *.sql
|
||||
var FS embed.FS
|
||||
Reference in New Issue
Block a user