webhooks

package
v0.0.0-...-195c1da Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 16, 2026 License: AGPL-3.0 Imports: 43 Imported by: 0

Documentation

Overview

Package webhooks is the outgoing webhook (C-15): the request templates of the events mode with their Secrets, the Signing secret in the style of the Standard Webhooks specification, the version 1 body of an event with its Mentions as data, and the adapter that sends an event through internal/outbound under the outbound address policy and the Destination's proxy and maps the answer to a delivery outcome. Delivery queues and reconciles the events (ADR-0005); this package never decides when to send.

Index

Constants

View Source
const (
	ModeEvents   = "events"
	ModeTemplate = "template"
	ModeBoth     = "both"
)

The modes of an outgoing webhook Destination (C-15.FR-1).

View Source
const (
	WarningPreviousSigningSecret = "previous_signing_secret_active"
	WarningLiteralCredential     = "literal_credential" //nolint:gosec // G101: the name of a warning, not a credential
)

The kinds of DestinationWarning (C-15.FR-5, FR-10).

View Source
const (
	CodeRequired      = "required"
	CodeInvalidFormat = "invalid_format"
	CodeTooLong       = "too_long"
	CodeReserved      = "reserved"
)

The codes of FieldError.

View Source
const (
	ActionUpdated       = "destination.updated"
	ResourceDestination = "destination"
	HintDestination     = "destination"
)

The Audit log action, resource type and live hint of a change of the Secrets of a Destination.

View Source
const (
	HeaderID        = "webhook-id"
	HeaderTimestamp = "webhook-timestamp"
	HeaderSignature = "webhook-signature"
)

The headers of a signed request.

View Source
const (
	FieldSigningSecret         = "destinations.signing_secret"
	FieldPreviousSigningSecret = "destinations.previous_signing_secret"
	FieldSecret                = "destination_secrets.value"
	FieldProxyPassword         = "destinations.proxy_password"
)

The keyring fields of the secrets of an outgoing webhook (encrypted_values).

View Source
const (
	ActionSigningSecretGenerated = "destination.signing_secret_generated"
	ActionSigningSecretRetired   = "destination.signing_secret_retired"
)

The Audit log actions of the Signing secret.

View Source
const (
	RequestCreate        = "create"
	RequestUpdate        = "update"
	RequestOpenThread    = "open_thread"
	RequestReplyInThread = "reply_in_thread"
	RequestEvents        = "events"
)

The requests of the template mode (C-15.FR-3), and the request of the events mode as the logs name it.

View Source
const (
	StepEvent  = "event"
	StepCreate = RequestCreate
)

The steps of a test and the items of a preview.

View Source
const BodyVersion = 1

BodyVersion is the version of the body of events-mode requests (C-15.FR-2): new event names and new fields are added within it; removing a field or changing its meaning needs a new version.

View Source
const EventTest = "test"

EventTest is the event of a Destination test.

View Source
const (
	SigningSecretPrefix = "whsec_"
)

The Signing secret (C-15.FR-5) in the style of the Standard Webhooks specification: whsec_ and 32 random bytes in base64; the HMAC-SHA256 key is the decoded bytes. Each request carries webhook-id, webhook-timestamp (Unix seconds of the real clock) and webhook-signature, `v1,` and the base64 HMAC of "{id}.{timestamp}.{body}", one per secret that still signs, separated by a space.

View Source
const TypeWebhook = "webhook"

TypeWebhook is the Destination type of an outgoing webhook.

Variables

View Source
var (
	// ErrNotFound is a Destination that does not exist in the Organization or is deleted.
	ErrNotFound = errors.New("no such destination")
	// ErrNotWebhook is a Destination that is not an outgoing webhook (not_webhook_destination).
	ErrNotWebhook = errors.New("the destination is not an outgoing webhook")
	// ErrNoSecret is a Secret the Destination does not have.
	ErrNoSecret = errors.New("no such secret")
	// ErrVersionMismatch is an If-Match that names another version of the Destination's Secrets.
	ErrVersionMismatch = errors.New("the secrets changed since they were read")
)

Functions

func Mapping

func Mapping(status int, _ time.Duration, hasRetryAfter bool) outbound.Outcome

Mapping is the response mapping of outgoing webhooks (C-15.FR-6): 2xx is delivered; 429 or 503 with Retry-After is a RetryAfter; 408 and every other 5xx are Transient; 401, 403, 404 and 410 are Fatal; 400, 413, 422 and every other answer are unknown.

func NewSigningSecret

func NewSigningSecret() logging.Secret

NewSigningSecret is a new random Signing secret.

func SecretRefs

func SecretRefs(src string) []string

SecretRefs are the names of the Secrets src reads as `.Secrets.<name>`, `$.Secrets.<name>` or `index .Secrets "<name>"`, sorted and without duplicates; a template that does not parse reads none.

func Sign

func Sign(secrets []logging.Secret, id string, at time.Time, body []byte) (string, error)

Sign is the value of webhook-signature for the request id sent at the Unix time at with body: a v1 signature with each secret, in order, separated by a space.

func ValidSecretName

func ValidSecretName(name string) bool

ValidSecretName reports whether name can name a Secret.

func Validate

func Validate(s *templates.Sandbox, base string, c *EventsConfig) error

Validate checks the request of the events mode on save (C-15.FR-1): the URL and every header parse in the sandbox and run on a dry run in which every Secret they read has a placeholder value, the URL then is an absolute http or https URL, and every header name is a valid name that Muster does not set itself. The URL and header values are trimmed of surrounding space; base is the JSON Pointer of the request in the body, such as /events.

func ValidateTemplate

func ValidateTemplate(s *templates.Sandbox, base string, c *TemplateConfig, now time.Time) error

ValidateTemplate checks the requests of the template mode on save (C-15.FR-1, FR-3): "create" and "update" are set, each request has a method of RequestTemplate, a URL, at most maxHeaders headers with valid names that Muster does not set itself, and extraction rules only on "create" and "open thread", each with a template identifier as its name, unique among them, and a valid JSONPath; every template parses and runs on a dry run with the example, its Secrets and the values of the extraction rules it may read filled with example-<name>, after which the URL is an absolute http or https URL. The fields are trimmed of surrounding space; base is the JSON Pointer of the requests in the body, such as /template, and now the business time the example's Alerts started ten minutes before.

func Verify

func Verify(secret logging.Secret, id, timestamp string, body []byte, header string) bool

Verify reports whether one of the signatures of header, the value of webhook-signature, is the v1 signature of the request id sent at timestamp with body under secret; the comparison takes constant time. A receiving endpoint also refuses a timestamp far from their clock, which this does not check.

Types

type Actor

type Actor struct {
	Kind      string  `json:"kind"`
	ID        *string `json:"id,omitempty"`
	Name      *string `json:"name,omitempty"`
	Login     *string `json:"login,omitempty"`
	TokenName *string `json:"token_name,omitempty"`
	Transport string  `json:"transport"`
	Reason    *string `json:"reason,omitempty"`
}

Actor is who made the change of an event (WebhookActor): a User or a Service account with the token and the Transport, or Muster itself (`system`) with the reason.

type Adapter

type Adapter struct {
	Service *Service
	Network Network
	Sandbox *templates.Sandbox
	// contains filtered or unexported fields
}

Adapter sends the requests of outgoing webhooks for delivery (C-15.FR-2, FR-3, FR-5, FR-6, FR-8): the events-mode requests, and in the template mode "create", "update", "open thread" and "reply in thread" as the adapter of the Destination type webhook, which delivery reconciles like a messenger's (ADR-0005). It renders the request templates with the Destination's Secrets in the sandbox, signs the body with every Signing secret that still signs, sends it through internal/outbound with the Destination's proxy in the client class of the call, maps the answer to a delivery outcome and extracts the values of the response. Every secret of the Destination is registered for redaction, so no error it returns carries one. It also renders the Desired state of the template mode and previews request templates.

func (*Adapter) Desired

func (a *Adapter) Desired(ctx context.Context, db deliverydb.DBTX, destinationID int64, st delivery.RequestState) (
	[]byte, error)

Desired renders, reading the Destination destinationID through db, the "update" request of its template mode for the Desired state st and returns its hash (delivery.Requests): the Secrets as [redacted] and the extracted values as example-<name>, so that neither a new Secret nor a new value changes it. A request that does not render hashes the state and the error, so that each new state makes the call that fails; one without request templates, the state.

func (*Adapter) LengthLimit

func (a *Adapter) LengthLimit() int

LengthLimit is the longest output of a template: the receiver's own limits are unknown.

func (*Adapter) PreviewRequest

func (a *Adapter) PreviewRequest(src string, sample templates.Data) (string, error)

PreviewRequest renders one request template src with the template data of a sample (previewTemplate of the kind webhook_request, C-12.FR-4): as a Loud message with a Mention of each kind, its Secrets as [redacted] and the extracted values it reads as example-<name>. A template that fails is the *templates.Error of its position.

func (*Adapter) Publish

Publish sends "create" of the template mode for a new Root message or Storm summary (C-15.FR-3): Loud with its Mention targets as delivery decided; the values its extraction rules find are returned, the first rule's as the message id. The Message is not used: the request templates render the Desired state of the call.

func (*Adapter) Reply

Reply sends "reply in thread" of the template mode for a lifecycle event whose messenger form is a Thread reply, with the latest Desired state; "open thread", when the Destination has one, runs first once per Root message and adds the values it extracts, which the reply reads. A failure of "open thread" is the outcome and sends no reply; once it ran, the outcome says so whatever the reply came to. Without "reply in thread" nothing is sent.

func (*Adapter) SendEvent

func (a *Adapter) SendEvent(ctx context.Context, c delivery.EventCall) delivery.Outcome

SendEvent sends one events-mode request (C-15.FR-2): POST with the body of the event as application/json, the headers of the Destination and webhook-id, webhook-timestamp and webhook-signature. A URL or header template that fails, or reads a Secret the Destination does not have, sends nothing and is a template error.

func (*Adapter) Update

Update sends "update" of the template mode with the latest Desired state, so that changes made while a request was pending collapse into it; the final edit carries `.Final`.

type Alert

type Alert struct {
	Fingerprint   string            `json:"fingerprint"`
	Status        string            `json:"status"`
	Labels        map[string]string `json:"labels"`
	Annotations   map[string]string `json:"annotations"`
	StartsAt      time.Time         `json:"starts_at"`
	ResolvedAt    *time.Time        `json:"resolved_at"`
	ResolveReason *string           `json:"resolve_reason"`
	GeneratorURL  *string           `json:"generator_url"`
}

Alert is one Alert of the Alert Group, by fingerprint (WebhookAlert).

type AlertGroup

type AlertGroup struct {
	Number        int64      `json:"number"`
	ID            string     `json:"id"`
	Title         string     `json:"title"`
	Summary       *string    `json:"summary"`
	Status        string     `json:"status"`
	Owner         *User      `json:"owner,omitempty"`
	Route         RouteRef   `json:"route"`
	Urgent        bool       `json:"urgent"`
	SeverityLevel string     `json:"severity_level"`
	StartedAt     time.Time  `json:"started_at"`
	ResolvedAt    *time.Time `json:"resolved_at"`
	SnoozeUntil   *time.Time `json:"snooze_until"`
	ReopenCount   int64      `json:"reopen_count"`
	URL           string     `json:"url"`
}

AlertGroup is the state of the Alert Group when the event was recorded (WebhookAlertGroup).

type Body

type Body struct {
	Version    int             `json:"version"`
	Event      string          `json:"event"`
	Notify     bool            `json:"notify"`
	Mentions   []MentionTarget `json:"mentions"`
	Sequence   int64           `json:"sequence"`
	OccurredAt time.Time       `json:"occurred_at"`
	Actor      Actor           `json:"actor"`
	AlertGroup AlertGroup      `json:"alert_group"`
	Alerts     []Alert         `json:"alerts"`
	// Test is set only on the test event of a Destination test (C-16.FR-1), which belongs to no Alert Group's order.
	Test bool `json:"test,omitempty"`
}

Body is the body of an events-mode request (OutgoingWebhookEvent), rendered when the event is queued, so that a retry hours later still carries the state the event described.

type Config

type Config struct {
	Store     Store
	Writer    Writer
	Keyring   *keyring.Keyring
	Audit     *audit.Writer
	Business  clock.Clock
	PublicURL string
}

Config is what a Service needs: its queries, the Keyring that encrypts and opens the secrets, the Audit log, the business clock that dates the changes, and for the adapter and the bodies of events, MUSTER_PUBLIC_URL and the network of its outbound clients.

type Data

type Data struct {
	Secrets SecretValues
}

Data is what the request templates of the events mode read: the Destination's Secrets, by name.

type EventsConfig

type EventsConfig struct {
	URL     string   `json:"url"`
	Headers []Header `json:"headers"`
}

EventsConfig is the request of the events mode as stored in webhook_events_config (WebhookEventsConfig): its URL and its headers, Go templates rendered with `.Secrets`.

func ParseEventsConfig

func ParseEventsConfig(raw []byte) (EventsConfig, error)

ParseEventsConfig reads the stored request of the events mode.

func (EventsConfig) JSON

func (c EventsConfig) JSON() []byte

JSON is the request as stored.

type ExtractionRule

type ExtractionRule struct {
	Name string `json:"name"`
	Path string `json:"path"`
}

ExtractionRule picks a value from the JSON response of "create" or "open thread" (ExtractionRule): stored by Name per Alert Group and Destination and read by later requests as `.Response.<name>`; Path is a JSONPath (RFC 9535) such as `$.data.id`.

type FieldError

type FieldError struct {
	Pointer string
	Code    string
	Detail  string
	Line    int
	Column  int
}

FieldError is a field of the events request or of a Secret that is not valid, at a JSON pointer of the request body, with the 1-based line and column of a template error when known.

func (*FieldError) Error

func (e *FieldError) Error() string
type Header struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

Header is a header of a request: its name and its value, a Go template that may read `.Secrets`.

type Mention

type Mention struct {
	Type     string
	ID       string
	Name     string
	Login    string
	Everyone string
}

Mention is a resolved Mention as the request templates read it (`.Mentions`, C-15.FR-11): everyone with the choice of the Mention settings — all, channel or here — a group by its name, or a User by public_id, display name and login.

type MentionTarget

type MentionTarget struct {
	Type  string  `json:"type"`
	ID    *string `json:"id,omitempty"`
	Name  *string `json:"name,omitempty"`
	Login *string `json:"login,omitempty"`
}

MentionTarget is a resolved Mention as data (WebhookMentionTarget, C-15.FR-11): Muster does not know the markup of the receiving endpoint, so everyone has only its type, a group its name, and a User their public_id, display name and login.

func MentionTargets

func MentionTargets(ts []mentions.Target) []MentionTarget

MentionTargets are the targets of a Loud event as the body carries them, in the order Resolve gave them; none is the empty list, never null.

type Network

type Network struct {
	Policy   outbound.PolicySource
	Log      *logging.Logger
	Real     clock.Clock
	Resolver outbound.Resolver
}

Network is what the outbound clients of the adapter need: the outbound address policy, the logger, the real clock of the signatures and the outbound metrics, and a resolver (nil: the system resolver).

type RequestData

type RequestData struct {
	Status            string
	Alerts            templates.Alerts
	GroupLabels       templates.KV
	CommonLabels      templates.KV
	CommonAnnotations templates.KV
	ExternalURL       string
	AlertGroup        *templates.AlertGroup
	Storm             *delivery.StormState
	Response          map[string]string
	Event             string
	Notify            bool
	Mentions          []Mention
	Secrets           SecretValues `json:"-"`
	Final             string
}

RequestData is what the request templates of the template mode read (C-15.FR-3, FR-11): Alertmanager's template data and the Alert Group (the data of the Root message templates, alert data unescaped, since the receiver's format is unknown), or for a Storm summary the Storm and no Alert Group; the values extracted from earlier responses; the lifecycle event a Thread reply carries; whether the message is Loud; its Mention targets; the Destination's Secrets; and the text of the final edit, empty for another request.

type RequestTemplate

type RequestTemplate struct {
	Method  string           `json:"method"`
	URL     string           `json:"url"`
	Headers []Header         `json:"headers"`
	Body    *string          `json:"body"`
	Extract []ExtractionRule `json:"extract"`
}

RequestTemplate is one request of the template mode (RequestTemplate): its method, and its URL, headers and body, Go templates, with the rules that extract values from its response.

type Requester

type Requester struct {
	Actor     audit.Actor
	Transport audit.Transport
	Address   netip.Addr
}

Requester is who asks for a change and how: the actor and the Transport of the Audit log entry, and the client address.

type RouteRef

type RouteRef struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

RouteRef names the Route of an Alert Group (EntityRef).

type Secret

type Secret struct {
	Name      string
	UpdatedAt time.Time
}

Secret is the status of a named Secret: its name and the time of its last change; the value is never read back.

type SecretValues

type SecretValues map[string]string

SecretValues are the Destination's Secrets as the request templates read them, by name; printed or encoded whole, as {{ toJson . }} or {{ printf "%v" . }} would, they show as [redacted], so that only a Secret read by its name reaches a request.

func (SecretValues) MarshalJSON

func (SecretValues) MarshalJSON() ([]byte, error)

MarshalJSON hides the values.

func (SecretValues) String

func (SecretValues) String() string

type Secrets

type Secrets struct {
	Items   []Secret
	Version int64
}

Secrets are the Secrets of a Destination by name, with the version of the Destination, their ETag.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service holds the secrets of the outgoing webhooks of an Organization and reads what their events need.

func New

func New(orgID int64, cfg Config) *Service

New returns the Service of the Organization orgID.

func (*Service) DeleteSecret

func (s *Service) DeleteSecret(ctx context.Context, r Requester, publicID string, version *int64, name string) error

DeleteSecret deletes the Secret name of the outgoing webhook publicID; a non-nil version must be the current version of its Secrets. A Secret it does not have is ErrNoSecret. It is recorded as destination.updated.

func (*Service) EventBodies

func (s *Service) EventBodies(ctx context.Context, tx groups.DBTX, in delivery.EventSource) (delivery.BodyFunc,
	error)

EventBodies reads, through the transaction tx of a change, the Alert Group of in as its events carry it — its state after the change, its Owner, its Route and its Alerts — and the actor of the change, and returns the function that renders the body of each of its events (C-15.FR-2).

func (*Service) GenerateSigningSecret

func (s *Service) GenerateSigningSecret(ctx context.Context, r Requester, publicID string) (logging.Secret,
	SigningStatus, error)

GenerateSigningSecret makes a new Signing secret for the outgoing webhook publicID (C-15.FR-5): the current one becomes the previous one, which still signs until it is retired, and the new one is returned once. It is recorded as destination.signing_secret_generated.

func (*Service) ListSecrets

func (s *Service) ListSecrets(ctx context.Context, publicID string) (Secrets, error)

ListSecrets lists the Secrets of the outgoing webhook publicID (C-15.FR-10); another type is ErrNotWebhook.

func (*Service) RetirePreviousSigningSecret

func (s *Service) RetirePreviousSigningSecret(ctx context.Context, r Requester, publicID string) error

RetirePreviousSigningSecret wipes the previous Signing secret of the outgoing webhook publicID, which signs no more (C-15.FR-5); it is recorded as destination.signing_secret_retired. Without a previous one it changes nothing.

func (*Service) SetSecret

func (s *Service) SetSecret(ctx context.Context, r Requester, publicID string, version *int64, name string,
	value logging.Secret) (Secret, int64, error)

SetSecret creates or replaces the Secret name of the outgoing webhook publicID with value (C-15.FR-10); a non-nil version must be the current version of its Secrets. It is recorded as destination.updated with the Secret marked changed, never its value, and returns the Secret's status and the new version.

func (*Service) SigningStatus

func (s *Service) SigningStatus(ctx context.Context, publicID string) (SigningStatus, error)

SigningStatus reads the status of the Signing secrets of the outgoing webhook publicID; another type is ErrNotWebhook.

type SigningStatus

type SigningStatus struct {
	Set                 bool
	UpdatedAt           *time.Time
	PreviousActiveSince *time.Time
}

SigningStatus is the status of the Signing secrets of a Destination (SigningSecretStatus).

type Store

Store are the queries the package runs outside a change.

func NewStore

func NewStore(d dbgen.DBTX) Store

NewStore is the Store over d, the main pool.

type TemplateConfig

type TemplateConfig struct {
	Create        RequestTemplate  `json:"create"`
	Update        RequestTemplate  `json:"update"`
	OpenThread    *RequestTemplate `json:"open_thread"`
	ReplyInThread *RequestTemplate `json:"reply_in_thread"`
}

TemplateConfig is the requests of the template mode as stored in webhook_template_config (WebhookTemplateConfig): "create" and "update", and the optional "open thread" and "reply in thread".

func ParseTemplateConfig

func ParseTemplateConfig(raw []byte) (TemplateConfig, error)

ParseTemplateConfig reads the stored requests of the template mode.

func (TemplateConfig) JSON

func (c TemplateConfig) JSON() []byte

JSON is the requests as stored, with empty lists rather than null.

type TestData

type TestData interface {
	Data(src *messages.Source) templates.Data
	GroupURL(publicID string) string
}

TestData is what a test and a preview read of the renderer of messages, declared by their consumer; *messages.Renderer implements it: the template data of the source and the link to an Alert Group page.

type Tester

type Tester struct {
	Adapter *Adapter
	Path    interface {
		Do(ctx context.Context, s delivery.Subject, op delivery.Op) (delivery.Outcome, error)
	}
	Data   TestData
	DB     dbgen.DBTX
	Budget time.Duration
}

Tester is the Destination test and the preview of outgoing webhooks: Adapter sends, on the interactive path Path, each step within Budget (delivery.interactive_budget; zero is delivery.InteractiveBudget); Data renders the source; DB, the main pool, reads the name and login of the person who started a test, which the test event names.

func (*Tester) Preview

func (t *Tester) Preview(ctx context.Context, in delivery.TestInput) ([]delivery.PreviewItem, error)

Preview renders, without sending anything, the requests of the outgoing webhook of in for its source (C-16.FR-4, C-15.FR-10): in the events mode the item event, the body of the first event of an Alert Group; in the template mode the items create, update, open_thread and reply_in_thread of the requests it has, with the values extracted from responses as example-<name>. Every Secret shows as [redacted]; a request whose template fails shows its error as the item's text.

func (*Tester) Test

Test runs the test of the outgoing webhook of in (C-16.FR-1): the step event in the modes events and both, then the step create in the modes template and both.

type TxQueries

type TxQueries interface {
	LockWebhookDestination(ctx context.Context, arg dbgen.LockWebhookDestinationParams) (
		dbgen.LockWebhookDestinationRow, error)
	BumpDestinationVersion(ctx context.Context, arg dbgen.BumpDestinationVersionParams) (int64, error)
	UpsertSecret(ctx context.Context, arg dbgen.UpsertSecretParams) error
	DeleteSecret(ctx context.Context, arg dbgen.DeleteSecretParams) (int64, error)
	RotateSigningSecret(ctx context.Context, arg dbgen.RotateSigningSecretParams) (int64, error)
	RetirePreviousSigningSecret(ctx context.Context, arg dbgen.RetirePreviousSigningSecretParams) (int64, error)
	audit.Store
	Notify(ctx context.Context, h db.Hint) error
}

TxQueries are the queries of a change of the secrets of a Destination, with the insert of the Audit log and the live hint, in one transaction.

type User

type User struct {
	ID    string `json:"id"`
	Name  string `json:"name"`
	Login string `json:"login"`
}

User is a User as the body names them (WebhookUser).

type Warning

type Warning struct {
	Kind  string
	Field string
}

Warning is a DestinationWarning of an outgoing webhook that its request templates give: a literal credential at the JSON Pointer Field.

func TemplateWarnings

func TemplateWarnings(base string, c TemplateConfig) []Warning

TemplateWarnings are the literal_credential warnings of the requests of the template mode (C-15.FR-10), as Warnings gives them for the events mode; base is the JSON Pointer of the requests, such as /template.

func Warnings

func Warnings(base string, c EventsConfig) []Warning

Warnings are the literal_credential warnings of the request of the events mode (C-15.FR-10): a header named Authorization, or a URL whose user information or query holds a literal value, that reads no Secret. base is the JSON Pointer of the request, such as /events.

type Writer

type Writer interface {
	InTx(ctx context.Context, f func(TxQueries) error) error
}

Writer runs the queries of a change in one transaction.

func NewWriter

func NewWriter(pool *pgxpool.Pool) Writer

NewWriter is the Writer over the main pool.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL