// inbound.go holds what RFC 0014 Phase 1 needs the forge to remember
// about replies that arrive through notify: the secret notify signs
// its deliveries with, and which replies have already been applied.
//
// Both are forge-local state, not truth: a lost secret is re-minted
// and re-registered, a lost dedup row can at worst let one retried
// delivery post twice.
package oltp
import (
"database/sql"
"time"
)
const inboundSchema = `
CREATE TABLE IF NOT EXISTS notify_hook (
id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
secret TEXT NOT NULL,
endpoint_id TEXT NOT NULL DEFAULT '',
url TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS inbound_reply (
message_id TEXT NOT NULL PRIMARY KEY, -- the mail's Message-ID, notify's delivery key
txn_id TEXT NOT NULL,
account TEXT NOT NULL,
outcome TEXT NOT NULL, -- what the reply became, or why it did not
at TEXT NOT NULL
);`
// NotifyHookSecret is the key notify signs reply deliveries with,
// minted on first use and stable after. quarry chooses it and hands it
// to notify when it registers the endpoint, so that a fresh forge is
// signed from its first delivery rather than after an operator pastes
// something.
func (d *DB) NotifyHookSecret() (string, error) {
var secret string
err := d.db.QueryRow(`SELECT secret FROM notify_hook WHERE id = 1`).Scan(&secret)
if err == nil {
return secret, nil
}
if err != sql.ErrNoRows {
return "", err
}
secret, err = newSecret()
if err != nil {
return "", err
}
// Two nodes minting at once: the first insert wins and the second
// reads it back, so a fleet still shares one secret.
if _, err := d.db.Exec(`INSERT OR IGNORE INTO notify_hook (id, secret) VALUES (1, ?)`, secret); err != nil {
return "", err
}
err = d.db.QueryRow(`SELECT secret FROM notify_hook WHERE id = 1`).Scan(&secret)
return secret, err
}
// SetNotifyHookEndpoint records which notify endpoint carries the
// forge's replies, for the fleet page and for a later re-registration.
func (d *DB) SetNotifyHookEndpoint(id, url string) error {
_, err := d.db.Exec(`UPDATE notify_hook SET endpoint_id = ?, url = ? WHERE id = 1`, id, url)
return err
}
// InboundReplySeen answers whether a delivery was already applied and
// what it became. notify redelivers on any non-2xx and on transport
// error, so the same mail can arrive more than once; one mail is one
// post.
func (d *DB) InboundReplySeen(messageID string) (outcome string, seen bool, err error) {
err = d.db.QueryRow(`SELECT outcome FROM inbound_reply WHERE message_id = ?`, messageID).Scan(&outcome)
if err == sql.ErrNoRows {
return "", false, nil
}
if err != nil {
return "", false, err
}
return outcome, true, nil
}
// RecordInboundReply remembers what a delivery became.
func (d *DB) RecordInboundReply(messageID, txnID, account, outcome string) error {
_, err := d.db.Exec(`INSERT OR REPLACE INTO inbound_reply (message_id, txn_id, account, outcome, at)
VALUES (?, ?, ?, ?, ?)`, messageID, txnID, account, outcome, time.Now().UTC().Format(time.RFC3339))
return err
}