Add empty operator consumer pointers and verification history schema
3 files changed,
+57
−2
internal/home/directorydata/operator_migrations/migrations/0001_operator_bindings.sql+34 −0internal/home/directorydata/operator_schema.go+21 −0internal/home/directorydata/roster.go+2 −2
CI passed — run details
diff --git a/internal/home/directorydata/operator_migrations/migrations/0001_operator_bindings.sql b/internal/home/directorydata/operator_migrations/migrations/0001_operator_bindings.sql| new file mode 100644 |
| index 0000000..5cec2d7 |
| --- /dev/null |
| +++ b/internal/home/directorydata/operator_migrations/migrations/0001_operator_bindings.sql |
| @@ -0,0 +1,34 @@ |
| +CREATE TABLE directory_operator_consumer_records ( |
| + consumer_id TEXT PRIMARY KEY REFERENCES roster_consumers(consumer_id), |
| + operation_id TEXT NOT NULL UNIQUE REFERENCES roster_operator_receipts(operation_id), |
| + sequence INTEGER NOT NULL CHECK(typeof(sequence)='integer' AND sequence>0), |
| + generation TEXT NOT NULL CHECK(length(generation)=43), |
| + record_digest BLOB NOT NULL CHECK(typeof(record_digest)='blob' AND length(record_digest)=32) |
| +); |
| +CREATE TABLE verification_attempts ( |
| + operation_id TEXT PRIMARY KEY CHECK(length(operation_id)=43), |
| + consumer_id TEXT NOT NULL REFERENCES roster_consumers(consumer_id), |
| + team_id INTEGER NOT NULL REFERENCES teams(id), |
| + app TEXT NOT NULL, |
| + origin TEXT NOT NULL, |
| + generation TEXT NOT NULL CHECK(length(generation)=43), |
| + record_sequence INTEGER NOT NULL CHECK(typeof(record_sequence)='integer' AND record_sequence>0), |
| + record_digest BLOB NOT NULL CHECK(typeof(record_digest)='blob' AND length(record_digest)=32), |
| + expected_epoch INTEGER NOT NULL CHECK(typeof(expected_epoch)='integer' AND expected_epoch>0), |
| + owner_ref TEXT NOT NULL CHECK(length(owner_ref)=29), |
| + issued_at INTEGER NOT NULL CHECK(typeof(issued_at)='integer' AND issued_at>0), |
| + expires_at INTEGER NOT NULL CHECK(typeof(expires_at)='integer' AND expires_at>issued_at AND expires_at-issued_at<=600), |
| + authorization_digest BLOB NOT NULL CHECK(typeof(authorization_digest)='blob' AND length(authorization_digest)=32) |
| +); |
| +CREATE TABLE verification_active ( |
| + consumer_id TEXT PRIMARY KEY REFERENCES roster_consumers(consumer_id), |
| + operation_id TEXT NOT NULL UNIQUE REFERENCES verification_attempts(operation_id) |
| +); |
| +CREATE TABLE verification_outcomes ( |
| + id INTEGER PRIMARY KEY AUTOINCREMENT, |
| + attempt_id TEXT NOT NULL REFERENCES verification_attempts(operation_id), |
| + operator_operation_id TEXT NOT NULL CHECK(length(operator_operation_id)=43), |
| + kind TEXT NOT NULL CHECK(kind IN ('opened','expired','activated')), |
| + completed_at INTEGER NOT NULL CHECK(typeof(completed_at)='integer' AND completed_at>0), |
| + UNIQUE(attempt_id,operator_operation_id,kind) |
| +); |
diff --git a/internal/home/directorydata/operator_schema.go b/internal/home/directorydata/operator_schema.go| new file mode 100644 |
| index 0000000..357ecae |
| --- /dev/null |
| +++ b/internal/home/directorydata/operator_schema.go |
| @@ -0,0 +1,21 @@ |
| +package directorydata |
| + |
| +import ( |
| + "embed" |
| + "io/fs" |
| + |
| + "github.com/carlosframework/rastrillo/migrate" |
| +) |
| + |
| +//go:embed operator_migrations |
| +var operatorMigrations embed.FS |
| + |
| +// OperatorSchema adds empty provisioning pointers and verification history. |
| +// No migration creates authority or opens a pending consumer window. |
| +var OperatorSchema = func() *migrate.Set { |
| + f, e := fs.Sub(operatorMigrations, "operator_migrations") |
| + if e != nil { |
| + panic(e) |
| + } |
| + return migrate.MustFromFS(f, "oficina_directory_operator_bindings") |
| +}() |
diff --git a/internal/home/directorydata/roster.go b/internal/home/directorydata/roster.go| index 317cd2c..2023d7f 100644 |
| --- a/internal/home/directorydata/roster.go |
| +++ b/internal/home/directorydata/roster.go |
| @@ -42,7 +42,7 @@ type RosterSource struct { |
| // NewRosterSource creates a read-only source. The caller authenticates operator |
| // provisioning separately; this constructor never initializes authority. |
| func NewRosterSource(db *sql.DB, sealer Sealer, cfg Config, directory string) (*RosterSource, error) { |
| - if !validOrigin(directory) { |
| + if !ValidOrigin(directory) { |
| return nil, ErrInvalid |
| } |
| s, err := New(db, sealer, cfg) |
| @@ -107,7 +107,7 @@ func (s *RosterSource) Read(ctx context.Context, req RosterRequest, now time.Tim |
| if req.Team != route.TeamSQID { |
| return fail(404, ErrRosterNotFound) |
| } |
| - if state != "live" || !s.store.apps[route.App] || !validOrigin(route.Origin) || !validConsumer(route.ConsumerID) || !validLabel(route.Label) || !s.store.validateRoute(route) { |
| + if state != "live" || !s.store.apps[route.App] || !ValidOrigin(route.Origin) || !validConsumer(route.ConsumerID) || !validLabel(route.Label) || !s.store.validateRoute(route) { |
| return fail(503, ErrUnavailable) |
| } |
| p := roster.Page{Version: 1, Type: "page", Directory: s.directory, Consumer: route.ConsumerID, Team: route.TeamSQID, Audience: route.Origin, Fetch: req.Fetch, RequestCursorHash: roster.CursorHash(req.Cursor)} |