feat(cmd,graph): wire /query onto core-go's server for webhooks (Phase 5a) The faithful runtime wiring. /query moves from spec's anon-router handler onto core-go's authenticated router, so the webhook engine gets the auth/database/server context it requires. - cmd: build the executable schema via graph.NewSchema and hand it to both webhooks.NewQueue and server.WithSchema (one schema, both). The server is coreserver.New().WithDefaultMiddleware() (core-go auth + database + server context + the delivery worker via WithQueues). web, MCP and REST stay on the anon router with spec's own authn — only /query changes. The owner "user" row is seeded at startup so core-go's LookupUser stays local. - ownerOnly middleware on /query: 403s any non-owner (core-go auth admits any meta user; spec is single-owner) and remaps the owner to AUTH_INTERNAL so the webhook engine's NewAuthConfig/FilterWebhooks (which refuse AUTH_COOKIE) accept them. - graph: NewSchema exposes the raw executable schema; the webhook resolver ACL now requires AUTH_INTERNAL (only the owner gets it, via ownerOnly) instead of spec's authn, which is no longer in the /query chain. Accepted trade-off: agents lose GraphQL /query reads (they keep MCP + REST). New deploy requirement: WithDefaultMiddleware needs [mail] smtp-from (core-go's notification queue). Verified against a live daemon on Postgres: owner cookie creates and lists webhooks (row stored INTERNAL/user_id 1); non-owner 403; unauth 401; web UI 200 on the anon router.
feat(graph): GraphQL-native webhook surface (Phase 5a) The webhook types, mutations, and resolvers, adapted from the pages.sr.ht core-go template for spec's single-owner model. - SDL: WebhookEvent (PROPOSAL_OPENED/MERGED/REJECTED), WebhookSubscription interface + UserWebhookSubscription, WebhookDelivery, WebhookPayload interface + ProposalEvent (carries a Proposal), cursor wrappers, `webhook` payload root field, and a `type Mutation` with createUserWebhook / deleteUserWebhook. No OAuth `client` field and no @access/@private directives — spec has no OAuth clients or scopes, so the owner gate is the entire ACL. - Models: hand-written database.Model impls (UserWebhookSubscription, WebhookDelivery) so gqlgen autobinds rather than generates them; events via pq.Array; cursor keyset pagination. - Resolvers: all owner-gated via authn (spec's ACL), using core-go's webhook engine — Validate, NewAuthConfig (INTERNAL, via the coreauth bridge), FilterWebhooks, WebhookContext.Exec for the sample, and the `webhook`→Payload(ctx) root. Proposal writes deliberately stay off this surface (only webhook mutations; the schema test now asserts exactly that). - gqlgen.yml binds Cursor to core-go's model.Cursor; generated code regenerated with the pinned gqlgen v0.17.36 (reproducible). Compiles and vets clean; existing graph read tests still pass. Runtime context wiring and event firing are the next slices.
feat(graph): wire the proposals read to service.ListProposals (Phase 3) The Proposals port declared a proposal listing and left Options.Proposals nil, so the `proposals` query failed loudly with "service/ exposes no proposal listing yet". Phase 3 supplies it: an adapter maps service.Proposal onto graph.Proposal at the edge — the two structs are identical, but service/ must not import graph/, so the rename lives here beside web.NewReader's equivalent — and cmd wires graph.NewProposals(svc) into the schema. The `proposals` field now answers from service/.
feat(graph): the read-only GraphQL schema at /query Eight query fields over the service layer, no Mutation and no Subscription — the design defers mutations until the proposal state machine settles, and TestSchemaHasNoMutations stands guard on that. Access is fail-closed and gated before parse, matching web's ACL exactly, so introspection is treated as content too. A federating api.sr.ht must therefore present a token or skip us, which costs one log line. A malformed rev is reported as a GraphQL error rather than folded into null. service/ deliberately hides malformed-versus-absent from probing, but the caller here is already authenticated as the owner or its own agent, and a bare null for rev=proposals/42 is indistinguishable from an absent document — it reads as a silently dropped argument. Proposal listing declares its port but is unwired: service/ exposes no proposal read yet, and returning an empty list would tell a reviewer their queue is clear when it is merely unread.