Files
silo-server/internal/autoscan/types.go
T
d68e70bb47 feat(autoscan): Sonarr/Radarr webhook intake without arr API keys (#353)
* docs(autoscan): add arr webhook intake spec and implementation plan

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(autoscan): add webhook intake schema migration

Adds delivery_mode to autoscan_sources, the autoscan_webhook_endpoints
table, and delivery_mode/provider_event_type on autoscan_events.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(autoscan): add built-in arr-webhook source identity

Host-discovered scan-source entry so webhook-mode sources need no
plugin installation; composite lister appends it to plugin discovery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(autoscan): persist delivery mode, webhook endpoints, event metadata

Sources carry delivery_mode; autoscan_webhook_endpoints CRUD with
SHA-256 token lookup and AAD-bound encrypted redisplay; events record
delivery_mode/provider_event_type; CreateEvent gains SkipRunningCheck
so webhook deliveries are never dropped by the poll exclusion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(autoscan): share the consume path and add webhook IngestChanges

Extracts consumeSourceChanges from PollOnce (marker semantics
preserved, existing poll tests unchanged); PollOnce skips webhook
sources; IngestChanges feeds deliveries through the shared pipeline
without markers and without the running-event exclusion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(autoscan): add Sonarr/Radarr webhook payload parser

Host-side arrwebhook package: provider inference, import/rename/delete
path extraction with vanished-path-friendly previous paths, subtree
fallback, exact-path dedupe, and no-op unknown events. Fixture-backed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(autoscan): add public webhook delivery route and admin endpoint management

Public POST /api/v1/autoscan/webhooks/{token} with per-IP rate
limiting, 256KiB body cap, 202-for-noop semantics, and token/body kept
out of logs; admin create/rotate/delete endpoint routes; source
responses carry delivery mode + webhook status/URL; create/update
validate delivery mode against source identity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): add webhook delivery mode to Autoscan admin UI

Webhook sources get a generate/copy/rotate webhook URL section,
provider selector, delivery status, and a connection-free Add-source
flow; activity rows badge webhook deliveries with the arr event type.
Path rewrites stay editable in both modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(api): redact secret path params from request and activity logs

The request logger and activity-log middleware recorded raw URLs, so
bearer credentials in secret path segments (autoscan webhook {token},
webhook-sync {secret}) were persisted to app logs and activity_log.
Redact the secret segment via the chi route params in both sinks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(autoscan): make webhook delivery reliable

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:13:31 -04:00

215 lines
5.8 KiB
Go

package autoscan
import "time"
// Settings is the global autoscan configuration (singleton row).
type Settings struct {
Enabled bool
DefaultPollIntervalSeconds int
DebounceSeconds int
}
// Connection is an arr server the host can reach: either own credentials, or a
// live reference to a Requests integration (RequestIntegrationID set).
type Connection struct {
ID string
Name string
Kind string
BaseURL string // own; empty when linked
APIKeyRef string // own; empty when linked
RequestIntegrationID *string
}
// PathRewrite is a per-source prefix translation from a raw arr/source-namespace
// path to a Silo-native path. The merged scan_source contract moved rewrite
// ownership from the plugin to the host, so these are applied host-side before a
// raw path is resolved/enqueued.
type PathRewrite struct {
From string `json:"from"`
To string `json:"to"`
}
// ChangeScope tells the host how to interpret a scan_source path.
type ChangeScope string
const (
ChangeScopeAuto ChangeScope = "auto"
ChangeScopeFile ChangeScope = "file"
ChangeScopeSubtree ChangeScope = "subtree"
)
// Change is one raw provider/source-namespace path returned by a scan_source
// plugin. The host applies PathRewrites before resolving the changed path.
type Change struct {
SourcePath string `json:"source_path"`
Scope ChangeScope `json:"scope"`
}
// DeliveryMode selects how a source's changes reach the host: the host polls
// the plugin (`poll`, the default), or the provider POSTs to a Silo webhook
// endpoint (`webhook`) and the plugin is never invoked.
const (
DeliveryModePoll = "poll"
DeliveryModeWebhook = "webhook"
)
// Source ties a scan_source plugin capability to a connection plus the
// host-owned scheduling/bookkeeping state.
type Source struct {
ID string
PluginID string
CapabilityID string
ConnectionID *string // nil until an operator binds a connection
Enabled bool
DeliveryMode string // DeliveryModePoll or DeliveryModeWebhook
PollIntervalSeconds *int // nil => use settings default
PathRewrites []PathRewrite // host-owned raw->Silo prefix rewrites
SourceConfig map[string]string
Label string // operator-set display label; "" = unset
Marker *string // opaque; nil on first run
LastRunAt *time.Time
LastError *string
}
// WebhookEndpoint is the admin-visible state of a source's webhook endpoint.
// The plaintext token and its lookup hash never live on this struct; creation
// and rotation return the token separately, and redisplay goes through
// RevealWebhookToken.
type WebhookEndpoint struct {
SourceID string
SecretSuffix string
CreatedAt time.Time
RotatedAt *time.Time
LastReceivedAt *time.Time
LastErrorAt *time.Time
LastErrorMessage string
}
// WebhookDelivery is one durably accepted ARR webhook delivery waiting to be
// consumed. LockedBy is an opaque lease owner; repository completion/failure
// updates are ownership-guarded so an expired worker cannot finalize a delivery
// that another node reclaimed.
type WebhookDelivery struct {
ID int64
SourceID string
ProviderEventType string
Changes []Change
ReceivedAt time.Time
AttemptCount int
LockedBy string
}
type EventStatus string
const (
EventStatusRunning EventStatus = "running"
EventStatusSuccess EventStatus = "success"
EventStatusError EventStatus = "error"
EventStatusUnresolved EventStatus = "unresolved"
)
type Event struct {
ID int64
SourceID *string
PluginID string
CapabilityID string
StartedAt time.Time
CompletedAt time.Time
DurationMS int64
Status EventStatus
DeliveryMode string
ProviderEventType string
ChangesReturned int
ChangesResolved int
TargetsClaimed int
ScansCreated int
ScansReused int
ScansSuppressed int
ErrorMessage string
MarkerBefore *string
MarkerAfter *string
}
type EventCreate struct {
SourceID string
PluginID string
CapabilityID string
StartedAt time.Time
MarkerBefore string
DeliveryMode string // "" defaults to DeliveryModePoll
ProviderEventType string
// SkipRunningCheck bypasses the running-event single-flight exclusion.
// Poll cycles may be skipped when an event is already running (their
// marker window is re-read next cycle); webhook deliveries carry no
// marker, so dropping one loses it forever — they always set this.
SkipRunningCheck bool
}
type EventFinish struct {
ID int64
CompletedAt time.Time
Status EventStatus
ChangesReturned int
ChangesResolved int
TargetsClaimed int
ScansCreated int
ScansReused int
ScansSuppressed int
ErrorMessage string
MarkerAfter string
}
type EnqueueResult struct {
Created int
Reused int
}
type EventListFilter struct {
SourceID string
Status EventStatus
Search string
Limit int
Offset int
}
type ScanListFilter struct {
Status string
Search string
Limit int
Offset int
}
type EventWithRuns struct {
Event Event
Runs []ScanRunSummary
}
type ScanRunSummary struct {
ID string
MediaFolderID int
Mode string
Path string
Trigger string
Status string
ErrorMessage string
RequestedAt *time.Time
StartedAt *time.Time
CompletedAt *time.Time
}
type ScanWithEvent struct {
ScanRunSummary
AutoscanEventID *int64
SourceID *string
PluginID string
CapabilityID string
EventStatus EventStatus
EventCompletedAt *time.Time
}
type QueueSummary struct {
Active int
Accepted int
Running int
}