diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/slowredirect/generator.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/slowredirect/generator.go
new file mode 100644
index 00000000..931e3a79
--- /dev/null
+++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/slowredirect/generator.go
@@ -0,0 +1,191 @@
+// ©AngelaMos | 2026
+// generator.go
+
+package slowredirect
+
+import (
+ "bytes"
+ "context"
+ _ "embed"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "html/template"
+ "net"
+ "net/http"
+ "strings"
+
+ "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/event"
+ "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token"
+ "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators"
+)
+
+const (
+ headerCFConnectingIP = "CF-Connecting-IP"
+ headerXForwardedFor = "X-Forwarded-For"
+ headerXRealIP = "X-Real-IP"
+ headerReferer = "Referer"
+ headerCSP = "Content-Security-Policy"
+ headerCacheControl = "Cache-Control"
+ headerPragma = "Pragma"
+
+ cspOverride = "default-src 'none'; script-src 'unsafe-inline'; connect-src 'self'"
+ cacheControlNoStore = "no-store, no-cache, must-revalidate, max-age=0"
+ pragmaNoCache = "no-cache"
+ contentTypeHTML = "text/html; charset=utf-8"
+
+ triggerPathPrefix = "/c/"
+ fingerprintPathSuffix = "/fingerprint"
+ metadataDestKey = "destination_url"
+
+ nilTokenBody = "Not Found"
+)
+
+var ErrMissingDestination = errors.New(
+ "slowredirect: destination_url missing from token metadata",
+)
+
+//go:embed template.html
+var templateHTML string
+
+var pageTemplate = template.Must(
+ template.New("slowredirect").Parse(templateHTML),
+)
+
+type pageData struct {
+ Destination string
+ FingerprintURL string
+}
+
+type Generator struct{}
+
+func New() *Generator { return &Generator{} }
+
+func (g *Generator) Type() token.Type { return token.TypeSlowRedirect }
+
+func (g *Generator) Generate(
+ _ context.Context,
+ t *token.Token,
+ baseURL string,
+) (generators.Artifact, error) {
+ dest, err := extractDestination(t.Metadata)
+ if err != nil {
+ return generators.Artifact{}, err
+ }
+ url := strings.TrimRight(baseURL, "/") + triggerPathPrefix + t.ID
+ return generators.Artifact{
+ Kind: generators.KindURL,
+ URL: url,
+ DestinationURL: dest,
+ }, nil
+}
+
+func (g *Generator) Trigger(
+ _ context.Context,
+ t *token.Token,
+ r *http.Request,
+) (*event.Event, *generators.TriggerResponse, error) {
+ if t == nil {
+ return nil, &generators.TriggerResponse{
+ StatusCode: http.StatusNotFound,
+ ContentType: contentTypeHTML,
+ Body: []byte(nilTokenBody),
+ ExtraHeaders: map[string]string{
+ headerCacheControl: cacheControlNoStore,
+ headerPragma: pragmaNoCache,
+ },
+ }, nil
+ }
+
+ dest, err := extractDestination(t.Metadata)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var body bytes.Buffer
+ data := pageData{
+ Destination: dest,
+ FingerprintURL: triggerPathPrefix + t.ID + fingerprintPathSuffix,
+ }
+ if err := pageTemplate.Execute(&body, data); err != nil {
+ return nil, nil, fmt.Errorf("render slowredirect template: %w", err)
+ }
+
+ resp := &generators.TriggerResponse{
+ StatusCode: http.StatusOK,
+ ContentType: contentTypeHTML,
+ Body: body.Bytes(),
+ ExtraHeaders: map[string]string{
+ headerCSP: cspOverride,
+ headerCacheControl: cacheControlNoStore,
+ headerPragma: pragmaNoCache,
+ },
+ }
+
+ evt := &event.Event{
+ TokenID: t.ID,
+ SourceIP: realIP(r),
+ UserAgent: optionalHeader(r.UserAgent()),
+ Referer: optionalHeader(r.Header.Get(headerReferer)),
+ }
+ return evt, resp, nil
+}
+
+func extractDestination(metadata json.RawMessage) (string, error) {
+ if len(metadata) == 0 {
+ return "", ErrMissingDestination
+ }
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(metadata, &m); err != nil {
+ return "", fmt.Errorf("parse token metadata: %w", err)
+ }
+ raw, ok := m[metadataDestKey]
+ if !ok {
+ return "", ErrMissingDestination
+ }
+ var dest string
+ if err := json.Unmarshal(raw, &dest); err != nil {
+ return "", fmt.Errorf("parse destination_url: %w", err)
+ }
+ if strings.TrimSpace(dest) == "" {
+ return "", ErrMissingDestination
+ }
+ return dest, nil
+}
+
+func optionalHeader(v string) *string {
+ v = strings.TrimSpace(v)
+ if v == "" {
+ return nil
+ }
+ return &v
+}
+
+func realIP(r *http.Request) string {
+ if v := strings.TrimSpace(r.Header.Get(headerCFConnectingIP)); v != "" {
+ return v
+ }
+ if v := lastNonEmptyXFF(r.Header.Get(headerXForwardedFor)); v != "" {
+ return v
+ }
+ if v := strings.TrimSpace(r.Header.Get(headerXRealIP)); v != "" {
+ return v
+ }
+ if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
+ return host
+ }
+ return r.RemoteAddr
+}
+
+func lastNonEmptyXFF(header string) string {
+ if header == "" {
+ return ""
+ }
+ parts := strings.Split(header, ",")
+ for i := len(parts) - 1; i >= 0; i-- {
+ if v := strings.TrimSpace(parts[i]); v != "" {
+ return v
+ }
+ }
+ return ""
+}
diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/slowredirect/generator_test.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/slowredirect/generator_test.go
new file mode 100644
index 00000000..78ceb0a3
--- /dev/null
+++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/slowredirect/generator_test.go
@@ -0,0 +1,333 @@
+// ©AngelaMos | 2026
+// generator_test.go
+
+package slowredirect_test
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token"
+ "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators"
+ "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators/slowredirect"
+)
+
+const (
+ cspOverride = "default-src 'none'; script-src 'unsafe-inline'; connect-src 'self'"
+ cacheControlNoStore = "no-store, no-cache, must-revalidate, max-age=0"
+ pragmaNoCache = "no-cache"
+ contentTypeHTML = "text/html; charset=utf-8"
+)
+
+func newSlowRedirectToken(t testing.TB, id, destination string) *token.Token {
+ t.Helper()
+ metadata := json.RawMessage(`{}`)
+ if destination != "" {
+ raw, err := json.Marshal(map[string]string{
+ "destination_url": destination,
+ })
+ require.NoError(t, err)
+ metadata = raw
+ }
+ return &token.Token{
+ ID: id,
+ ManageID: "manage-" + id,
+ Type: token.TypeSlowRedirect,
+ Memo: "unit test slowredirect",
+ AlertChannel: token.ChannelWebhook,
+ Enabled: true,
+ Metadata: metadata,
+ }
+}
+
+func TestGenerator_TypeIsSlowRedirect(t *testing.T) {
+ g := slowredirect.New()
+ require.Equal(t, token.TypeSlowRedirect, g.Type())
+}
+
+func TestGenerate_PersistsDestinationOnToken(t *testing.T) {
+ cases := []struct {
+ name string
+ baseURL string
+ id string
+ destination string
+ wantURL string
+ }{
+ {
+ name: "plain destination",
+ baseURL: "https://canary.example.com",
+ id: "abc123",
+ destination: "https://news.example.com/article/42",
+ wantURL: "https://canary.example.com/c/abc123",
+ },
+ {
+ name: "trailing slash trimmed on base URL",
+ baseURL: "https://canary.example.com/",
+ id: "xyz999",
+ destination: "https://news.example.com",
+ wantURL: "https://canary.example.com/c/xyz999",
+ },
+ {
+ name: "subpath base URL preserved",
+ baseURL: "https://example.com/canary",
+ id: "p7",
+ destination: "https://other.example.com",
+ wantURL: "https://example.com/canary/c/p7",
+ },
+ }
+
+ g := slowredirect.New()
+ for _, tc := range cases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ tok := newSlowRedirectToken(t, tc.id, tc.destination)
+ art, err := g.Generate(
+ context.Background(),
+ tok,
+ tc.baseURL,
+ )
+ require.NoError(t, err)
+ require.Equal(t, generators.KindURL, art.Kind)
+ require.Equal(t, tc.wantURL, art.URL)
+ require.Equal(
+ t,
+ tc.destination,
+ art.DestinationURL,
+ "artifact must surface destination_url that was persisted on the token metadata",
+ )
+ })
+ }
+}
+
+func TestGenerate_RejectsMissingDestination(t *testing.T) {
+ cases := []struct {
+ name string
+ metadata json.RawMessage
+ }{
+ {
+ name: "empty metadata",
+ metadata: json.RawMessage(``),
+ },
+ {
+ name: "empty object",
+ metadata: json.RawMessage(`{}`),
+ },
+ {
+ name: "empty destination string",
+ metadata: json.RawMessage(`{"destination_url":""}`),
+ },
+ {
+ name: "whitespace destination",
+ metadata: json.RawMessage(`{"destination_url":" "}`),
+ },
+ {
+ name: "wrong key",
+ metadata: json.RawMessage(`{"redirect":"https://example.com"}`),
+ },
+ }
+
+ g := slowredirect.New()
+ for _, tc := range cases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ tok := &token.Token{
+ ID: "missingdest",
+ Type: token.TypeSlowRedirect,
+ Metadata: tc.metadata,
+ }
+ _, err := g.Generate(
+ context.Background(),
+ tok,
+ "https://canary.example.com",
+ )
+ require.Error(t, err)
+ })
+ }
+}
+
+func TestTrigger_RendersHTMLWithEscapedDestination(t *testing.T) {
+ g := slowredirect.New()
+
+ t.Run("plain destination renders into JS and noscript", func(t *testing.T) {
+ dest := "https://news.example.com/article"
+ tok := newSlowRedirectToken(t, "plainok01", dest)
+ r := httptest.NewRequest(http.MethodGet, "/c/plainok01", nil)
+ r.Header.Set("CF-Connecting-IP", "203.0.113.7")
+
+ evt, resp, err := g.Trigger(context.Background(), tok, r)
+ require.NoError(t, err)
+ require.NotNil(t, evt)
+ require.NotNil(t, resp)
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ require.Equal(t, contentTypeHTML, resp.ContentType)
+
+ body := string(resp.Body)
+ require.Contains(
+ t,
+ body,
+ `url=https://news.example.com/article`,
+ "noscript meta refresh must point at the destination",
+ )
+ require.Contains(
+ t,
+ body,
+ "https://news.example.com/article",
+ "JS body must include the destination for window.location.replace",
+ )
+ require.Contains(
+ t,
+ body,
+ "/c/plainok01/fingerprint",
+ "JS body must reference the per-token fingerprint endpoint",
+ )
+ })
+
+ t.Run("XSS attempt in destination is neutralized", func(t *testing.T) {
+ hostile := `https://x.example.com/" />`
+ tok := newSlowRedirectToken(t, "xssa001", hostile)
+ r := httptest.NewRequest(http.MethodGet, "/c/xssa001", nil)
+
+ _, resp, err := g.Trigger(context.Background(), tok, r)
+ require.NoError(t, err)
+ body := string(resp.Body)
+
+ require.NotContains(
+ t,
+ body,
+ "",
+ "raw script injection must not appear unescaped in any context",
+ )
+ require.NotContains(
+ t,
+ body,
+ `/>"),
+ "only the trusted inline script closing tag may appear",
+ )
+ })
+
+ t.Run("script-closing injection is neutralized", func(t *testing.T) {
+ hostile := `https://x.example.com/`
+ tok := newSlowRedirectToken(t, "xssb001", hostile)
+ r := httptest.NewRequest(http.MethodGet, "/c/xssb001", nil)
+
+ _, resp, err := g.Trigger(context.Background(), tok, r)
+ require.NoError(t, err)
+ body := string(resp.Body)
+
+ require.NotContains(
+ t,
+ body,
+ "
+
+
+
+