GetWebhookByID returns the webhook iff it's owned by userID. Cross- user reads (or missing rows) return ErrWebhookNotFound — same not-found-on-cross-user convention used elsewhere in the codebase (conversation reads, message reads). The returned Webhook has SigningSecret populated for the delivery
(ctx context.Context, webhookID, userID string)
| 155 | // worker's benefit; the public API layer scrubs this field before |
| 156 | // responding to GETs. |
| 157 | func (s *Store) GetWebhookByID(ctx context.Context, webhookID, userID string) (*Webhook, error) { |
| 158 | w := &Webhook{} |
| 159 | var filtersJSON []byte |
| 160 | err := s.pool.QueryRow(ctx, |
| 161 | `SELECT id, user_id, url, description, events, filters, |
| 162 | signing_secret, COALESCE(signing_secret_prev, ''), |
| 163 | signing_secret_prev_expires_at, |
| 164 | enabled, auto_disabled_at, created_at, last_delivered_at |
| 165 | FROM webhooks WHERE id = $1 AND user_id = $2`, |
| 166 | webhookID, userID, |
| 167 | ).Scan( |
| 168 | &w.ID, &w.UserID, &w.URL, &w.Description, &w.Events, &filtersJSON, |
| 169 | &w.SigningSecret, &w.SigningSecretPrev, |
| 170 | &w.SigningSecretPrevExpiresAt, |
| 171 | &w.Enabled, &w.AutoDisabledAt, &w.CreatedAt, &w.LastDeliveredAt, |
| 172 | ) |
| 173 | if err != nil { |
| 174 | if errors.Is(err, pgx.ErrNoRows) { |
| 175 | return nil, ErrWebhookNotFound |
| 176 | } |
| 177 | return nil, err |
| 178 | } |
| 179 | if err := json.Unmarshal(filtersJSON, &w.Filters); err != nil { |
| 180 | return nil, fmt.Errorf("unmarshal filters: %w", err) |
| 181 | } |
| 182 | return w, nil |
| 183 | } |
| 184 | |
| 185 | // GetWebhookByIDInternal returns the webhook by ID with no ownership |
| 186 | // check. INTERNAL USE ONLY — handler code MUST use GetWebhookByID |