Help us shape the future of Go! Take the 2026 Go Developer Survey

magiclink

package
v0.1.0-alpha.9 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package magiclink provides optional, existing-account email sign-in using public plugin, storage, mail and session contracts. PostgreSQL is required.

Index

Examples

Constants

View Source
const ID = "magic-link"
View Source
const Lifetime = 10 * time.Minute
View Source
const RedeemOperation plugin.Operation = "magic_link.redeem"

RedeemOperation names the audit event recorded when a proof is rejected. Successful redemption is recorded by core as plugin.SessionIssue.

Variables

This section is empty.

Functions

func Contract

func Contract() plugin.Definition

Contract returns the plugin's declarative API description for side-effect free export through sigil.Describe (for example by the sigil CLI). It is not a runnable registration: its handlers have no bound Auth and fail closed.

func PruneExpired

func PruneExpired(ctx context.Context, store storage.Store, before time.Time, limit int) (int64, error)

PruneExpired deletes at most limit (1..1000) challenges whose expiry, clamped to database time, is at or before cutoff, and returns the count. Consumed rows leave with their expiry; no removed row could still redeem. It needs only the store, because the plugin's persisted ledger ownership survives disablement: operators keep the table bounded while the plugin is disabled or before re-enabling it. A store where the plugin was never installed is ErrUnsupported. User deletion cascades independently through the foreign key.

Types

type Config

type Config struct {
	Store    storage.Store
	Delivery *sigil.MailDelivery
	Render   func(context.Context, string, sigil.Secret) (sigil.Mail, error)
}

Config resources must belong to the same deployment and Store as the Auth passed to Bind. Render must be local, cancellation-aware and concurrency-safe; it receives a secret to embed in a host-owned confirmation page's URL fragment.

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}
Example

This compiling host integration accepts provisioned host resources. Run the migration block in an explicit operator command before starting runtime hosts.

package main

import (
	"context"
	"database/sql"
	"net/http"

	sigil "github.com/brewgenixapp/sigil"
	"github.com/brewgenixapp/sigil/plugin"
	"github.com/brewgenixapp/sigil/plugin/magiclink"
	"github.com/brewgenixapp/sigil/storage/postgres"
)

func main() {
	build := func(ctx context.Context, db *sql.DB, secret sigil.Secret, protector sigil.Protector, sender sigil.MailSender) (http.Handler, *sigil.Auth, *sigil.MailDelivery, error) {
		store, err := postgres.New(db, "public")
		if err != nil {
			return nil, nil, nil, err
		}
		delivery, err := sigil.NewMailDelivery(store, protector, sender)
		if err != nil {
			return nil, nil, nil, err
		}
		links, err := magiclink.New(magiclink.Config{Store: store, Delivery: delivery, Render: func(_ context.Context, _ string, token sigil.Secret) (sigil.Mail, error) {
			// The host serves this page and requires an explicit Sign in button. No GET
			// or page-load script may redeem the proof. Never log or track these URLs.
			return sigil.Mail{Subject: sigil.NewSecret("Sign in to Example"), Text: sigil.NewSecret("Confirm your sign-in: https://app.example.com/magic-link#" + token.Reveal())}, nil
		}})
		if err != nil {
			return nil, nil, nil, err
		}
		definitions := []plugin.Definition{links.Definition()}
		registry, err := plugin.Compile(definitions, nil)
		if err != nil {
			return nil, nil, nil, err
		}
		core, err := postgres.NewMigrator(db, "public")
		if err != nil {
			return nil, nil, nil, err
		}
		if err := core.Apply(ctx); err != nil {
			return nil, nil, nil, err
		}
		migrations, err := postgres.NewPluginMigrator(db, "public", registry)
		if err != nil {
			return nil, nil, nil, err
		}
		if err := migrations.Apply(ctx); err != nil {
			return nil, nil, nil, err
		}
		// Runtime startup is read-only. Use a runtime database role here in production.
		a, err := sigil.New(ctx, sigil.Config{BaseURL: "https://auth.example.com", TrustedOrigins: []string{"https://app.example.com"}, Secret: secret, Store: store, Plugins: definitions})
		if err != nil {
			return nil, nil, nil, err
		}
		if err := links.Bind(a); err != nil {
			a.Close(ctx)
			return nil, nil, nil, err
		}
		// Host schedules delivery.DispatchOne(ctx), owns the server and closes Auth.
		return a.Handler(), a, delivery, nil
	}
	_ = build
}

func New

func New(c Config) (*Plugin, error)

func (*Plugin) Bind

func (p *Plugin) Bind(a *sigil.Auth) error

Bind attaches the startup-created Auth exactly once, before serving traffic. Register this instance's Definition and use the same Store in both configs.

func (*Plugin) Definition

func (p *Plugin) Definition() plugin.Definition

func (*Plugin) Redeem

func (p *Plugin) Redeem(ctx context.Context, ip netip.Addr, token sigil.Secret) (sigil.IssuedSession, error)

Redeem authenticates only after the hook consumes the proof in the SAME transaction as session issuance. Never retry an ambiguous commit automatically.

func (*Plugin) Request

func (p *Plugin) Request(ctx context.Context, ip netip.Addr, target string) error

Request gives the same success for missing, unverified and eligible accounts. Only an existing verified address receives mail. No accounts or links between identities are created, and requesting another link does not revoke old links.

Jump to

Keyboard shortcuts

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