~evanj/cms

4acbe351c1377a4398249f96ca7889f6f8737d1f — Evan M Jones 11 months ago d1032d2
Feat(invite): Adding invite feature.
M TODO => TODO +4 -5
@@ 1,19 1,18 @@
[high]
Testing: 100% happy path and 80% total
Documentation
Official Go API
Pay Goatounter
Pay logo: mybrandnewlogo.com
Doc pages: Contact, FAQ, Terms, Privacy, Tour
Invite a user (the user will have access to all the same spaces -- to your "org" basically)
Roll based access control
Depth option on APIs
When editing existing references don't blow away prev inputs
Restrict API requests for free users (limit users<->org)
Forgot password
Warn & delete excess users/spaces on downgrade.

[med]
Cache lists
Official Go API
Pay Goatounter
Pay logo: mybrandnewlogo.com

[low]
Optional memcached

M cms.go => cms.go +0 -142
@@ 4,51 4,7 @@ package main
import (
	"log"
	"net/http"
	"os"
	"strings"

	"git.sr.ht/~evanj/cms/internal/c"
	"git.sr.ht/~evanj/cms/internal/c/content"
	"git.sr.ht/~evanj/cms/internal/c/contenttype"
	"git.sr.ht/~evanj/cms/internal/c/doc"
	"git.sr.ht/~evanj/cms/internal/c/file"
	"git.sr.ht/~evanj/cms/internal/c/hook"
	"git.sr.ht/~evanj/cms/internal/c/redirect"
	"git.sr.ht/~evanj/cms/internal/c/space"
	"git.sr.ht/~evanj/cms/internal/c/stripe"
	"git.sr.ht/~evanj/cms/internal/c/user"
	"git.sr.ht/~evanj/cms/internal/s/cache"
	"git.sr.ht/~evanj/cms/internal/s/db"
	webhook "git.sr.ht/~evanj/cms/internal/s/hook"
	"git.sr.ht/~evanj/cms/internal/s/rl"
	libstripe "git.sr.ht/~evanj/cms/internal/s/stripe"
	"git.sr.ht/~evanj/cms/pkg/e3"
	"git.sr.ht/~evanj/security"

	_ "git.sr.ht/~evanj/embed" // For embedding static assets during build.
)

var (
	app   *App
	build string

	port             = os.Getenv("PORT")
	dbtype           = os.Getenv("DBTYPE")
	dbcreds          = os.Getenv("DB")
	url              = os.Getenv("URL")
	secret           = os.Getenv("SECRET")
	memcacheKey      = os.Getenv("MEMCACHE_KEY")
	memcacheServer   = os.Getenv("MEMCACHE_SERVER")
	e3user           = os.Getenv("E3_USER")
	e3pass           = os.Getenv("E3_PASS")
	e3url            = os.Getenv("E3_URL")
	signupEnabled    = os.Getenv("SIGNUP_ENABLE") == "true"
	staticDir        = os.Getenv("STATIC_DIR")
	analyticsEnabled = os.Getenv("ANALYTICS_ENABLE") == "true"
	stripeSuccessURL = os.Getenv("STRIPE_SUCCESS_URL")
	stripeErrorURL   = os.Getenv("STRIPE_ERROR_URL")
	stripePK         = os.Getenv("STRIPE_PK")
	stripeSK         = os.Getenv("STRIPE_SK")
)

type App struct {


@@ 79,101 35,3 @@ func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {

	h.ServeHTTP(w, r)
}

func init() {
	w := os.Stdout
	applogger := log.New(w, "[cms] ", 0)

	db, err := db.New(
		log.New(w, "[cms:db] ", 0),
		dbtype,
		dbcreds,
		security.Default(secret),
	)
	if err != nil {
		applogger.Fatal(err)
	}
	if err := db.EnsureSetup(); err != nil {
		applogger.Fatal(err)
	}

	cacher, err := cache.New(
		log.New(w, "[cms:cache] ", 0),
		db,
		memcacheKey,
		memcacheServer,
	)
	if err != nil {
		applogger.Fatal(err)
	}

	fs := e3.New(e3user, e3pass, e3url)
	rl := rl.New(log.New(w, "[cms:ratelimit] ", 0), cacher, fs)
	c := c.New(log.New(w, "[cms:content] ", 0), rl, analyticsEnabled, build)
	libs := libstripe.New(log.New(w, "[cms:stripe] ", 0), stripeSuccessURL, stripeErrorURL, stripePK, stripeSK, rl)

	app = &App{
		applogger,
		map[string]http.Handler{
			"content": content.New(
				c,
				log.New(w, "[cms:content] ", 0),
				rl,
				fs,
				webhook.New(log.New(w, "[cms:hook] ", 0), rl),
				url,
			),
			"contenttype": contenttype.New(
				c,
				log.New(w, "[cms:contenttype] ", 0),
				rl,
			),
			"space": space.New(
				c,
				log.New(w, "[cms:space] ", 0),
				rl,
			),
			"user": user.New(
				c,
				log.New(w, "[cms:user] ", 0),
				rl,
				signupEnabled,
				libs,
			),
			"hook": hook.New(
				c,
				log.New(w, "[cms:hook] ", 0),
				rl,
			),
			"file": file.New(
				c,
				log.New(w, "[cms:file] ", 0),
				rl,
				fs,
				url,
			),
			"static": http.StripPrefix("/static", http.FileServer(http.Dir(staticDir))),
			"redirect": redirect.New(
				c,
				log.New(w, "[cms:redirect] ", 0),
				rl,
			),
			"page": doc.New(
				c,
				log.New(w, "[cms:doc] ", 0),
				rl,
			),
			"stripe": http.StripPrefix("/stripe", stripe.New(
				c,
				log.New(w, "[cms:stripe] ", 0),
				rl,
				libs,
			)),
		},
	}
}

func main() {
	app.log.Println("listening on", url)
	app.log.Fatal(http.ListenAndServe(port, app))
}

M go.mod => go.mod +1 -1
@@ 12,6 12,6 @@ require (
	github.com/golang/mock v1.4.3
	github.com/google/uuid v1.1.1
	github.com/pkg/errors v0.9.1
	github.com/stripe/stripe-go/v71 v71.28.0
	github.com/stripe/stripe-go/v71 v71.39.0
	golang.org/x/sync v0.0.0-20190423024810-112230192c58
)

M go.sum => go.sum +2 -2
@@ 32,8 32,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stripe/stripe-go/v71 v71.28.0 h1:9nCL4gvsLuEA8ZA0HGKWWioJbKJaXpL4jA46GGTV03g=
github.com/stripe/stripe-go/v71 v71.28.0/go.mod h1:BXYwMQe+xjYomcy5/qaTGyoyVMTP3wDCHa7DVFvg8+Y=
github.com/stripe/stripe-go/v71 v71.39.0 h1:koL28dKJfP5aunjBU7EuCgI/TpMy/sgqrTjK4D3XO5M=
github.com/stripe/stripe-go/v71 v71.39.0/go.mod h1:BXYwMQe+xjYomcy5/qaTGyoyVMTP3wDCHa7DVFvg8+Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=

M internal/c/c.go => internal/c/c.go +1 -0
@@ 150,6 150,7 @@ func (c *Controller) HTML(w http.ResponseWriter, r *http.Request, tmpl *template
	data["Build"] = c.buildID

	if err := tmpl.Execute(&buf, data); err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to build html response")
		return
	}

A internal/c/invite/invite.go => internal/c/invite/invite.go +122 -0
@@ 0,0 1,122 @@
package invite

import (
	"errors"
	"fmt"
	"log"
	"net/http"
	"strings"

	"git.sr.ht/~evanj/cms/internal/c"
	"git.sr.ht/~evanj/cms/internal/m/invite"
	"git.sr.ht/~evanj/cms/internal/m/org"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/cms/internal/v"
)

var (
	inviteHTML = v.MustParse("html/invite.html")

	ErrLoggedIn = errors.New("you cannot use this invite")
	ErrNoInvite = errors.New("no invite found")
)

type Invite struct {
	*c.Controller
	log *log.Logger
	db  dber
}

type dber interface {
	InviteNew(o org.Org) (invite.Invite, error)
	InviteGetByToken(tok string) (invite.Invite, error)
	InviteAccept(i invite.Invite, u, p, v string) (user.User, invite.Invite, error)
	InviteList(o org.Org) (r []invite.Invite, err error)
}

func New(c *c.Controller, log *log.Logger, db dber) Invite {
	return Invite{c, log, db}
}

func (i Invite) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	switch i.Method(r) {
	case "GET":
		user, err := i.GetCookieUser(w, r)
		if err != nil {
			// This is the logged out user.
			tok := strings.Trim(r.URL.Path, "/")
			if tok == "" {
				i.Error2(w, r, http.StatusBadRequest, ErrNoInvite)
				return
			}
			i.HTML(w, r, inviteHTML, map[string]interface{}{"Invite": tok})
			return
		}

		invites, err := i.db.InviteList(user.Org())
		if err != nil {
			i.Error2(w, r, http.StatusInternalServerError, err)
			return
		}

		// Show the user that created the invite a list of current invites.
		i.HTML(w, r, inviteHTML, map[string]interface{}{
			"User":    user,
			"Invites": invites,
		})
		return

	case "POST":
		user, err := i.GetCookieUser(w, r)
		if err != nil {
			i.Error2(w, r, http.StatusBadRequest, c.ErrNoLogin)
			return
		}

		_, err = i.db.InviteNew(user.Org())
		if errors.Is(err, invite.ErrExpired) || errors.Is(err, invite.ErrUsed) {
			i.Error2(w, r, http.StatusBadRequest, err)
			return
		}
		if err != nil {
			i.Error2(w, r, http.StatusInternalServerError, fmt.Errorf("failed to create invite: %w", err))
			return
		}

		i.Redirect(w, r, "/invite")
		return

	case "PATCH":
		// Can't be logged in.
		_, err := i.GetCookieUser(w, r)
		if err == nil {
			i.Error2(w, r, http.StatusBadRequest, ErrLoggedIn)
			return
		}

		inv, err := i.db.InviteGetByToken(r.FormValue("invite"))
		if err == nil {
			i.Error2(w, r, http.StatusBadRequest, err)
			return
		}

		// Accept invite and create new user.
		user, _, err := i.db.InviteAccept(inv, r.FormValue("username"), r.FormValue("password"), r.FormValue("verify"))
		if errors.Is(err, invite.ErrExpired) || errors.Is(err, invite.ErrUsed) {
			i.Error2(w, r, http.StatusBadRequest, err)
			return
		}
		if err != nil {
			i.Error2(w, r, http.StatusInternalServerError, fmt.Errorf("failed to create invite: %w", err))
			return
		}

		i.SetCookieUser(w, r, user)
		i.Redirect(w, r, "/")
		return

	}

	http.NotFound(w, r)

}

A internal/c/invite/invite_test.go => internal/c/invite/invite_test.go +1 -0
@@ 0,0 1,1 @@
package invite_test

A internal/m/invite/invite.go => internal/m/invite/invite.go +19 -0
@@ 0,0 1,19 @@
package invite

import (
	"errors"

	"git.sr.ht/~evanj/cms/internal/m/org"
)

var (
	ErrExpired = errors.New("this invite has expired")
	ErrUsed    = errors.New("this invite has already been used")
)

type Invite interface {
	ID() string
	Token() string
	Validate() error
	Org() org.Org
}

A internal/m/invite/invite_test.go => internal/m/invite/invite_test.go +1 -0
@@ 0,0 1,1 @@
package invite_test

M internal/s/cache/cache.go => internal/s/cache/cache.go +4 -9
@@ 16,17 16,12 @@ type Cache struct {
	db      *db.DB
}

func New(log *log.Logger, db *db.DB, key string, memcacheServer string) (*Cache, error) {
	c := &Cache{
		db,
		log,
		key,
		memcache.New(memcacheServer),
		db,
	}
	return c, c.mc.Ping()
func New(log *log.Logger, db *db.DB, key string, memcacheServer string) *Cache {
	return &Cache{db, log, key, memcache.New(memcacheServer), db}
}

func (c *Cache) Setup() error { return c.mc.Ping() }

func (c *Cache) cache(breakCache bool, key string, v interface{}, getter func() (interface{}, error)) error {
	m, err := c.mc.Get(key)
	if err != nil || breakCache {

M internal/s/db/db.go => internal/s/db/db.go +17 -43
@@ 60,8 60,9 @@ func beformat(before int) int {

type DB struct {
	*sql.DB
	log *log.Logger
	sec securer
	log      *log.Logger
	sec      securer
	setupErr error
}

// securer provides us two things:


@@ 77,47 78,29 @@ type securer interface {
// New, does as one might expect, given a logger, type of database, database
// connection string, and securer interface, opens a pool'd connection to a
// mysql database and pings. If ping fails we return error and nil *DB.
func New(log *log.Logger, typ, creds string, sec securer) (*DB, error) {
	conn, err := sql.Open(typ, creds)
	if err != nil {
		return nil, err
	}
func New(log *log.Logger, typ, creds string, sec securer) *DB {
	db, err := sql.Open(typ, creds)
	return &DB{db, log, sec, err}
}

	if err := conn.Ping(); err != nil {
		return nil, err
func (db *DB) Setup() error {
	if db.setupErr != nil {
		return db.setupErr
	}

	// TODO: Best numbers?
	conn.SetMaxIdleConns(10)
	conn.SetMaxOpenConns(100)

	db := &DB{
		conn,
		log,
		sec,
	if err := db.Ping(); err != nil {
		return err
	}

	return db, nil
}

// NewWithConn creates a *DB type and pings the MySQL server. If ping fails we
// return error.
func NewWithConn(log *log.Logger, sec securer, conn *sql.DB) (*DB, error) {
	if err := conn.Ping(); err != nil {
		return nil, err
	if err := db.migrate(); err != nil {
		return err
	}

	// TODO: Best numbers?
	conn.SetMaxIdleConns(10)
	conn.SetMaxOpenConns(100)
	db.SetMaxIdleConns(10)
	db.SetMaxOpenConns(100)

	db := &DB{
		conn,
		log,
		sec,
	}

	return db, nil
	return nil
}

// migrate does our "migration" -migration in quotes as we just dummy


@@ 199,15 182,6 @@ func (db *DB) migrate() error {
	return nil
}

// Ensure we have the tables we require. If we receive an error other
// than "table already exists" and also "duplicate entry" for built in data
// types that error will be returned.
// Mainly, we are runnin CREATE TABLE and some INSERT INTO of predefined
// value types.
func (db *DB) EnsureSetup() error {
	return db.migrate()
}

// FileExists makes sure SOME space and content owns the file. I.E. deleted
// spaces can't server files.
func (db *DB) FileExists(URL string) (bool, error) {

A internal/s/db/invite.go => internal/s/db/invite.go +242 -0
@@ 0,0 1,242 @@
package db

import (
	"database/sql"
	"fmt"
	"strconv"
	"time"

	"git.sr.ht/~evanj/cms/internal/m/invite"
	"git.sr.ht/~evanj/cms/internal/m/org"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/security"
)

type Invite struct {
	// From DB.
	id, createdStr, expiresStr string
	used                       bool
	InviteOrg                  Org
	// Set on get.
	created, expires time.Time
	tok              string
}

var (
	queryCreate = `
		INSERT INTO cms_invite (EXPIRES, ORG_ID, USED) VALUES (?, ?, FALSE)
	`

	queryGet = `
		SELECT cms_invite.ID, CREATED, EXPIRES, USED, cms_org.ID, cms_billing.TIER_NAME, cms_billing.PAYMENT_CUSTOMER 
		FROM cms_invite 
		JOIN cms_org ON cms_org.ID=cms_invite.ORG_ID
		LEFT JOIN cms_billing ON cms_billing.ORG_ID=cms_org.ID
		WHERE cms_invite.ID=?
	`

	queryList = `
		SELECT ID FROM cms_invite WHERE ORG_ID=? AND EXPIRES < ? AND USED=FALSE ORDER BY ID DESC
	`

	queryUse = `
		UPDATE cms_invite SET USED=TRUE WHERE ID=?
	`

	layout = "2006-01-02 15:04:05"
)

func (db *DB) InviteNew(o org.Org) (invite.Invite, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	i, err := db.inviteNew(t, o)
	if err != nil {
		return nil, err
	}

	return i, t.Commit()
}

func (db *DB) inviteNew(t *sql.Tx, o org.Org) (invite.Invite, error) {
	res, err := t.Exec(queryCreate, time.Now().UTC().Add(1*time.Hour), o.ID())
	if err != nil {
		return nil, err
	}

	id, err := res.LastInsertId()
	if err != nil {
		return nil, err
	}

	return db.inviteGet(t, strconv.FormatInt(id, 10))
}

func (db *DB) InviteGet(id string) (invite.Invite, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	i, err := db.inviteGet(t, id)
	if err != nil {
		return nil, err
	}

	return i, t.Commit()
}

func (db *DB) inviteGet(t *sql.Tx, id string) (invite.Invite, error) {
	var i Invite
	if err := t.QueryRow(queryGet, id).Scan(
		&i.id, &i.createdStr, &i.expiresStr, &i.used,
		&i.InviteOrg.OrgID, &i.InviteOrg.OrgBillingTierName, &i.InviteOrg.OrgPaymentCustomer,
	); err != nil {
		return nil, err
	}

	created, err := time.Parse(layout, i.createdStr)
	if err != nil {
		return nil, err
	}

	expires, err := time.Parse(layout, i.expiresStr)
	if err != nil {
		return nil, err
	}

	tok, err := db.sec.TokenCreate(security.TokenMap{"ID": i.id})
	if err != nil {
		return nil, err
	}

	i.created = created
	i.expires = expires
	i.tok = tok

	return i, i.Validate()
}

func (db *DB) InviteAccept(i invite.Invite, u, p, v string) (user.User, invite.Invite, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, nil, err
	}
	defer t.Rollback()

	user, invite, err := db.inviteAccept(t, i, u, p, v)
	if err != nil {
		return nil, nil, err
	}

	return user, invite, t.Commit()
}

func (db *DB) inviteAccept(t *sql.Tx, i invite.Invite, u, p, v string) (user.User, invite.Invite, error) {
	if _, err := t.Exec(queryUse, i.ID()); err != nil {
		return nil, nil, err
	}

	// Create user.
	hash, err := db.userNewGeneratePassword(u, p, v)
	if err != nil {
		return nil, nil, err
	}

	if _, err := t.Exec(queryCreateNewUser, u, hash, i.Org().ID()); err != nil {
		return nil, nil, fmt.Errorf("user '%s' already exists", u)
	}

	user, err := db.userGet(t, u, p)
	if err != nil {
		return nil, nil, err
	}

	return user, i, nil
}

func (db *DB) InviteList(o org.Org) (r []invite.Invite, err error) {
	rows, err := db.Query(queryList, o.ID(), time.Now().UTC().Add(1*time.Hour).Format(layout))
	if err != nil {
		return nil, err
	}

	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}

		i, err := db.InviteGet(id)
		if err != nil {
			return nil, err
		}

		r = append(r, i)
	}

	return r, nil
}

func (db *DB) inviteGetByToken(t *sql.Tx, tok string) (invite.Invite, error) {
	tmap, err := db.sec.TokenFrom(tok)
	if err != nil {
		return nil, fmt.Errorf("failed to decode invite token")
	}

	id, ok := tmap["ID"]
	if !ok {
		return nil, fmt.Errorf("corrupted invite token")
	}

	str, ok := id.(string)
	if !ok {
		return nil, fmt.Errorf("corrupted invite token")
	}

	i, err := db.inviteGet(t, str)
	if err != nil {
		return nil, err
	}

	if err := i.Validate(); err != nil {
		return nil, err
	}

	return i, nil
}

func (db *DB) InviteGetByToken(tok string) (invite.Invite, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	i, err := db.inviteGetByToken(t, tok)
	if err != nil {
		return nil, err
	}

	return i, t.Commit()
}

func (i Invite) ID() string    { return i.id }
func (i Invite) Token() string { return i.tok }
func (i Invite) Org() org.Org  { return i.InviteOrg }

func (i Invite) Validate() error {
	if i.used {
		return invite.ErrUsed
	}

	if time.Now().UTC().After(i.expires) {
		return invite.ErrExpired
	}

	return nil
}

M internal/s/db/migrations_embed.go => internal/s/db/migrations_embed.go +2 -0
@@ 26,6 26,8 @@ func init() {

	migrations["sql/00006.sql"] = tostring("Q1JFQVRFIFRBQkxFIGNtc19lbWFpbCAoIAoJSUQgSU5URUdFUiBQUklNQVJZIEtFWSBBVVRPX0lOQ1JFTUVOVCwKCUVNQUlMIHZhcmNoYXIoMjU2KSBVTklRVUUgTk9UIE5VTEwsCiAgVVNFUl9JRCBJTlRFR0VSIE5PVCBOVUxMLAogIENPTlNUUkFJTlQgQ01TX0VNQUlMX1RPX1VTRVJfRksgRk9SRUlHTiBLRVkoVVNFUl9JRCkgUkVGRVJFTkNFUyBjbXNfdXNlcihJRCkKKTsK")

	migrations["sql/00007.sql"] = tostring("Q1JFQVRFIFRBQkxFIGNtc19pbnZpdGUgKCAKCUlEIElOVEVHRVIgUFJJTUFSWSBLRVkgQVVUT19JTkNSRU1FTlQsCiAgQ1JFQVRFRCBUSU1FU1RBTVAgTk9UIE5VTEwgREVGQVVMVCBDVVJSRU5UX1RJTUVTVEFNUCwKICBFWFBJUkVTIFRJTUVTVEFNUCBOT1QgTlVMTCwKICBVU0VEIEJPT0xFQU4sCiAgT1JHX0lEIElOVEVHRVIgTk9UIE5VTEwsCiAgQ09OU1RSQUlOVCBDTVNfSU5WSVRFX1RPX09SR19GSyBGT1JFSUdOIEtFWShPUkdfSUQpIFJFRkVSRU5DRVMgY21zX29yZyhJRCkKKTsK")

}

func Get(name string) (string, bool) {

M internal/s/db/org.go => internal/s/db/org.go +13 -0
@@ 105,3 105,16 @@ func (db *DB) OrgGetSpaceCount(o org.Org) (int, error) {

	return count, nil
}

func (db *DB) OrgGetUserCount(o org.Org) (int, error) {
	var (
		count int
		q     = "SELECT COUNT(*) FROM cms_user WHERE cms_user.ORG_ID=?"
	)

	if err := db.QueryRow(q, o.ID()).Scan(&count); err != nil {
		return 0, err
	}

	return count, nil
}

A internal/s/db/sql/00007.sql => internal/s/db/sql/00007.sql +8 -0
@@ 0,0 1,8 @@
CREATE TABLE cms_invite ( 
	ID INTEGER PRIMARY KEY AUTO_INCREMENT,
  CREATED TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  EXPIRES TIMESTAMP NOT NULL,
  USED BOOLEAN,
  ORG_ID INTEGER NOT NULL,
  CONSTRAINT CMS_INVITE_TO_ORG_FK FOREIGN KEY(ORG_ID) REFERENCES cms_org(ID)
);

M internal/s/db/user.go => internal/s/db/user.go +26 -22
@@ 59,55 59,59 @@ func (db *DB) UserNew(username, password, verifyPassword string) (user.User, err
	return user, t.Commit()
}

func (db *DB) userNew(t *sql.Tx, username, password, verifyPassword string) (user.User, error) {
func (db *DB) userNewGeneratePassword(username, password, verifyPassword string) (string, error) {
	if password == "" {
		return nil, fmt.Errorf("no password entered")
		return "", fmt.Errorf("no password entered")
	}

	if password != verifyPassword {
		return nil, fmt.Errorf("passwords do not match")
		return "", fmt.Errorf("passwords do not match")
	}

	hash, err := db.sec.HashCreate(username, password)
	if err != nil {
		return nil, fmt.Errorf("failed to create password hash")
		return "", fmt.Errorf("failed to create password hash")
	}

	org, err := db.orgNew(t)
	return hash, nil
}

func (db *DB) userNew(t *sql.Tx, username, password, verifyPassword string) (user.User, error) {
	hash, err := db.userNewGeneratePassword(username, password, verifyPassword)
	if err != nil {
		return nil, err
	}

	res, err := t.Exec(queryCreateNewUser, username, hash, org.ID())
	org, err := db.orgNew(t)
	if err != nil {
		return nil, fmt.Errorf("user '%s' already exists", username)
		return nil, err
	}

	id, err := res.LastInsertId()
	if err != nil {
		return nil, fmt.Errorf("failed to create user")
	if _, err := t.Exec(queryCreateNewUser, username, hash, org.ID()); err != nil {
		return nil, fmt.Errorf("user '%s' already exists", username)
	}

	var user User
	if err := t.QueryRow(queryFindUserByID, id).Scan(
		&user.UserID, &user.UserName, &user.userHash, &user.userEmail,
		&user.UserOrg.OrgID, &user.UserOrg.OrgBillingTierName, &user.UserOrg.OrgPaymentCustomer,
	); err != nil {
		return nil, fmt.Errorf("failed to find user created")
	return db.UserGet(username, password)
}

func (db *DB) UserGet(username, password string) (user.User, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	tok, err := db.sec.TokenCreate(security.TokenMap{"ID": user.UserID})
	user, err := db.userGet(t, username, password)
	if err != nil {
		return nil, fmt.Errorf("failed to create token for user")
		return nil, err
	}

	user.userToken = tok
	return &user, nil
	return user, t.Commit()
}

func (db *DB) UserGet(username, password string) (user.User, error) {
func (db *DB) userGet(t *sql.Tx, username, password string) (user.User, error) {
	var user User
	if err := db.QueryRow(queryFindUserByName, username).Scan(
	if err := t.QueryRow(queryFindUserByName, username).Scan(
		&user.UserID, &user.UserName, &user.userHash, &user.userEmail,
		&user.UserOrg.OrgID, &user.UserOrg.OrgBillingTierName, &user.UserOrg.OrgPaymentCustomer,
	); err != nil {

M internal/s/rl/rl.go => internal/s/rl/rl.go +47 -0
@@ 10,6 10,7 @@ import (
	"time"

	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/invite"
	"git.sr.ht/~evanj/cms/internal/m/org"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/tier"


@@ 33,6 34,12 @@ var (
		tier.Business.Name: 5,
	}

	userLimits = map[string]int{
		// If not in map, unlimited.
		tier.Free.Name:     1,
		tier.Business.Name: 15,
	}

	ErrHitLimit = errors.New("you have surpassed your usage limit: consider upgrading")
	ErrNoAccess = errors.New("you don't have access to that feature")
)


@@ 166,3 173,43 @@ func (rl RL) ContentTypeUpdate(space space.Space, contenttype contenttype.Conten
	}
	return rl.db.ContentTypeUpdate(space, contenttype, name, newParams, updateParams)
}

// Rate limit users to org.

func (rl RL) InviteNew(o org.Org) (invite.Invite, error) {
	limit, ok := userLimits[o.Tier().Name]
	if !ok {
		// If not in map, unlimited.
		return rl.db.InviteNew(o)
	}

	c, err := rl.db.OrgGetUserCount(o)
	if err != nil {
		return nil, err
	}

	if c >= limit {
		return nil, fmt.Errorf("can't invite new users: %w", ErrHitLimit)
	}

	return rl.db.InviteNew(o)
}

func (rl RL) InviteAccept(i invite.Invite, u, p, v string) (user.User, invite.Invite, error) {
	limit, ok := userLimits[i.Org().Tier().Name]
	if !ok {
		// If not in map, unlimited.
		return rl.db.InviteAccept(i, u, p, v)
	}

	c, err := rl.db.OrgGetUserCount(i.Org())
	if err != nil {
		return nil, nil, err
	}

	if c >= limit {
		return nil, nil, fmt.Errorf("cannot join this organization: too many users: %w", ErrHitLimit)
	}

	return rl.db.InviteAccept(i, u, p, v)
}

M internal/v/html/_footer.html => internal/v/html/_footer.html +32 -0
@@ 38,6 38,9 @@
                <input type=submit class="text-decoration-none m-0 p-0 btn btn-link text-muted border-0" value=Logout />
              </form>
            </li>
            {{if and .User (.User | paid)}}
              <li class='nav-item'><a data-toggle="modal" data-target="#inviteModal" class='text-muted' href='#'>Invite</a></li>
            {{end}}
            <li><a class='text-muted' href='/page/billing'>Billing</a></li>
          {{ else }}
            <li><a class='text-muted' href='/'>Home</a></li>


@@ 70,6 73,35 @@
    </div>
  </footer>
</div>

{{if and .User (.User | paid)}}
<form method=POST action='/invite' enctype='multipart/form-data'>
  <input type=hidden name=method value=POST />
  <input type=hidden name=method value="{{.User.Org.ID}}" />
  <div class="modal fade" id="inviteModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-dialog-scrollable">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title" id="exampleModalLabel">Invite someone to your space(s)</h5>
          <button type="button" class="close" data-dismiss="modal" aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
        <div class="modal-body">
          <p>We'll generate a special link for you. Send this to your friend, 
          coworker, or whoever!</p>
          <p>The invite will only be active for one hour.</p>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
          <button type="submit" class="btn btn-primary">Go</button>
        </div>
      </div>
    </div>
  </div>
</form>
{{end}}

{{if .A}}
<img style='position: fixed; bottom: 0; right: 0;' src="//skippercms.goatcounter.com/count?p={{.A.Path}}{{if .A.Referrer}}&r={{.A.Referrer}}{{end}}&rnd={{.A.RND}}">
{{end}}

M internal/v/html/_header.html => internal/v/html/_header.html +3 -0
@@ 43,6 43,9 @@
              <input type=submit class="btn btn-link nav-link border-0" value=Logout />
            </form>
          </li>
          {{if and .User (.User | paid)}}
            <li class='nav-item'><a data-toggle="modal" data-target="#inviteModal" class='nav-link' href='#'>Invite</a></li>
          {{end}}
          <li class='nav-item'><a class='nav-link' href='/page/billing'>Billing</a></li>
        {{ else }}
          <li class='nav-item'><a class='nav-link' href='/#signup'>Signup</a></li>

M internal/v/html/billing.html => internal/v/html/billing.html +5 -7
@@ 14,7 14,7 @@
    {{if .User}}
      <div class='container'>
        <div class='row'>
          <div class="col-12 col-md-6 mb-5">
          <div class="col-12 col-md-6 offset-md-3">
            <div class='text-center'>
              {{if .User.HasEmail}}
              <p>Update your email.</p>


@@ 22,15 22,15 @@
              <p>Set your email in case you get locked out of your account.</p>
              {{end}}
            </div>
            <form action='/user/update/email' method=POST>
            <form action='/user/update/email' method=POST class='mb-5'>
              <label for=email>Email</label>
              <input id=email name=email type=email class="mb-3 form-control" placeholder="email" required {{if .User.HasEmail}}value="{{.User.Email}}"{{end}}>
              <button type="submit" class="btn btn-primary">Go</button>
            </form>
            <div class='text-center mt-3'>
            <div class='text-center'>
              <p>Update your password.</p>
            </div>
            <form action='/user/update/password' method=POST>
            <form action='/user/update/password' method=POST class='mb-5'>
              <label for=current >Current Password</label>
              <input id=current name=current type=password class="mb-3 form-control" placeholder="current" required>
              <label for=password>New Password</label>


@@ 39,8 39,6 @@
              <input id=verify name=verify type=password class="mb-3 form-control" placeholder="verify" required>
              <button type="submit" class="btn btn-primary">Go</button>
            </form>
          </div>
          <div class="col-12 col-md-6 mb-5">
            {{if .User | paid}}
              <form action='/user/cancel/billing' method=POST class="text-center">
                <p class='mb-5'>Cancel your {{.User.Org.Tier.Name}} subscription.</p>


@@ 63,7 61,7 @@
              </form>
            {{else}}
              <p class='text-center mb-5'>Upgrade to a paid tier.<br>Get more access.</p>
              <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 mb-5 text-center">
              <div class="row row-cols-1 row-cols-md-2 row-cols-lg-2 mb-5 text-center">
                {{range .Tiers}}
                  {{if not (.|isFree)}}
                    <form action='/user/update/billing' method=POST class="col">

A internal/v/html/invite.html => internal/v/html/invite.html +70 -0
@@ 0,0 1,70 @@
<!DOCTYPE html>
<html lang=en>
<head>
  {{ template "html/_head.html" }}
  <title>Skipper CMS | Invite</title>
</head>
<body class='page bg-light'>
  <style>{{ template "css/main.css" }}</style>
  <main>
    {{ template "html/_header.html" $ }}
    {{if .User}}
    <div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
      <h1 class="display-4">Invites</h1>
    </div>
    <div class='container'>
      <div class='row'>
        <div class="col-12 col-md-6 offset-md-3 col-lg-4 offset-lg-4">
          {{if .Invites}}
            {{range $key, $val := .Invites}}
              <div class="card mb-4 shadow-sm flex-fill">
                <div class="card-body">
                  <div id='copy-{{$key}}' class='text-truncate mb-3'>
                    https://cms.evanjon.es/invite/{{$val.Token}}
                  </div>
                  <button data-clipboard-target="#copy-{{$key}}" class="btn btn-lg btn-primary btn-block">Copy</button>
                </div>
              </div>
            {{end}}
          {{else}}
          <p class='w-100 text-center'>No invites.</p>
          {{end}}
        </div>
      </div>
    </div>
    {{else}}
    <div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
      <h1 class="display-4">You've been invited</h1>
    </div>
    <div class='container'>
      <div class='row'>
        <div class="col-12 col-md-6 offset-md-3 col-lg-4 offset-lg-4 d-flex">
          <div class="card mb-4 shadow-sm flex-fill">
            <div class="card-header">
              <h4 class="my-0 font-weight-normal">Signup</h4>
            </div>
            <div class="card-body">
              <form id='signup' method=POST action='/invite' enctype='multipart/form-data'>
                <input type=hidden name=method value=PATCH />
                <input name=invite type=hidden value="{{.Invite}}" required>
                <label for="signupInputUsername" class="sr-only">Username</label>
                <input name=username type="text" id="signupInputUsername" class="mb-3 form-control" placeholder="Username" required>
                <label for="signupInputPassword" class="sr-only">Password</label>
                <input name=password type="password" id="signupInputPassword" class="mb-3 form-control" placeholder="Password" required>
                <label for="signupInputVerify" class="sr-only">Confirm Password</label>
                <input name=verify type="password" id="signupInputVerify" class="mb-3 form-control" placeholder="Confirm Password" required>
                <button class="btn btn-lg btn-primary btn-block" type="submit">Go</button>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
    {{end}}
    {{ template "html/_footer.html" $ }}
  </main>
  <script src='/static/js/clipboard.js'></script>
  <script>(function(){new ClipboardJS('button[data-clipboard-target]')})();</script>
  {{ template "html/_scripts.html" }}
</body>
</html>

M internal/v/tmpls_embed.go => internal/v/tmpls_embed.go +5 -3
@@ 20,15 20,15 @@ func init() {

	tmpls["css/mvp.css"] = tostring("OnJvb3QgewogICAgLS1ib3JkZXItcmFkaXVzOiA1cHg7CiAgICAtLWJveC1zaGFkb3c6IDJweCAycHggMTBweDsKICAgIC0tY29sb3I6ICMxMThiZWU7CiAgICAtLWNvbG9yLWFjY2VudDogIzExOGJlZTBiOwogICAgLS1jb2xvci1iZzogI2ZmZjsKICAgIC0tY29sb3ItYmctc2Vjb25kYXJ5OiAjZTllOWU5OwogICAgLS1jb2xvci1zZWNvbmRhcnk6ICM5MjBkZTk7CiAgICAtLWNvbG9yLXNlY29uZGFyeS1hY2NlbnQ6ICM5MjBkZTkwYjsKICAgIC0tY29sb3Itc2hhZG93OiAjZjRmNGY0OwogICAgLS1jb2xvci10ZXh0OiAjMDAwOwogICAgLS1jb2xvci10ZXh0LXNlY29uZGFyeTogIzk5OTsKICAgIC0taG92ZXItYnJpZ2h0bmVzczogMS4yOwogICAgLS1qdXN0aWZ5LWltcG9ydGFudDogY2VudGVyOwogICAgLS1qdXN0aWZ5LW5vcm1hbDogbGVmdDsKICAgIC0tbGluZS1oZWlnaHQ6IDE1MCU7CiAgICAtLXdpZHRoLWNhcmQ6IDI4NXB4OwogICAgLS13aWR0aC1jYXJkLW1lZGl1bTogNDYwcHg7CiAgICAtLXdpZHRoLWNhcmQtd2lkZTogODAwcHg7CiAgICAtLXdpZHRoLWNvbnRlbnQ6IDEwODBweDsKfQoKLyogTVZQLmNzcyB2MS4wIC0gYnkgQW5keSBCcmV3ZXIgKi8KCi8qIExheW91dCAqLwphcnRpY2xlIGFzaWRlIHsKICAgIGJhY2tncm91bmQ6IHZhcigtLWNvbG9yLXNlY29uZGFyeS1hY2NlbnQpOwogICAgYm9yZGVyLWxlZnQ6IDRweCBzb2xpZCB2YXIoLS1jb2xvci1zZWNvbmRhcnkpOwogICAgcGFkZGluZzogMC4wMXJlbSAwLjhyZW07Cn0KCmJvZHkgewogICAgYmFja2dyb3VuZDogdmFyKC0tY29sb3ItYmcpOwogICAgY29sb3I6IHZhcigtLWNvbG9yLXRleHQpOwogICAgZm9udC1mYW1pbHk6IC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgIlNlZ29lIFVJIiwgUm9ib3RvLCBPeHlnZW4tU2FucywgVWJ1bnR1LCBDYW50YXJlbGwsICJIZWx2ZXRpY2EgTmV1ZSIsIHNhbnMtc2VyaWY7CiAgICBsaW5lLWhlaWdodDogdmFyKC0tbGluZS1oZWlnaHQpOwogICAgbWFyZ2luOiAwOwogICAgb3ZlcmZsb3cteDogaGlkZGVuOwogICAgcGFkZGluZzogMXJlbSAwOwp9Cgpmb290ZXIsCmhlYWRlciwKbWFpbiB7CiAgICBtYXJnaW46IDAgYXV0bzsKICAgIG1heC13aWR0aDogdmFyKC0td2lkdGgtY29udGVudCk7CiAgICBwYWRkaW5nOiAycmVtIDFyZW07Cn0KCmhyIHsKICAgIGJhY2tncm91bmQtY29sb3I6IHZhcigtLWNvbG9yLWJnLXNlY29uZGFyeSk7CiAgICBib3JkZXI6IG5vbmU7CiAgICBoZWlnaHQ6IDFweDsKICAgIG1hcmdpbjogNHJlbSAwOwp9CgpzZWN0aW9uIHsKICAgIGRpc3BsYXk6IGZsZXg7CiAgICBmbGV4LXdyYXA6IHdyYXA7CiAgICBqdXN0aWZ5LWNvbnRlbnQ6IHZhcigtLWp1c3RpZnktaW1wb3J0YW50KTsKfQoKc2VjdGlvbiBhc2lkZSB7CiAgICBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1jb2xvci1iZy1zZWNvbmRhcnkpOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICBib3gtc2hhZG93OiB2YXIoLS1ib3gtc2hhZG93KSB2YXIoLS1jb2xvci1zaGFkb3cpOwogICAgbWFyZ2luOiAxcmVtOwogICAgcGFkZGluZzogMS4yNXJlbTsKICAgIHdpZHRoOiB2YXIoLS13aWR0aC1jYXJkKTsKfQoKc2VjdGlvbiBhc2lkZTpob3ZlciB7CiAgICBib3gtc2hhZG93OiB2YXIoLS1ib3gtc2hhZG93KSB2YXIoLS1jb2xvci1iZy1zZWNvbmRhcnkpOwp9CgpzZWN0aW9uIGFzaWRlIGltZyB7CiAgICBtYXgtd2lkdGg6IDEwMCU7Cn0KCi8qIEhlYWRlcnMgKi8KYXJ0aWNsZSBoZWFkZXIsCmRpdiBoZWFkZXIsCm1haW4gaGVhZGVyIHsKICAgIHBhZGRpbmctdG9wOiAwOwp9CgpoZWFkZXIgewogICAgdGV4dC1hbGlnbjogdmFyKC0tanVzdGlmeS1pbXBvcnRhbnQpOwp9CgpoZWFkZXIgYSBiLApoZWFkZXIgYSBlbSwKaGVhZGVyIGEgaSwKaGVhZGVyIGEgc3Ryb25nIHsKICAgIG1hcmdpbi1sZWZ0OiAxcmVtOwogICAgbWFyZ2luLXJpZ2h0OiAxcmVtOwp9CgpoZWFkZXIgbmF2IGltZyB7CiAgICBtYXJnaW46IDFyZW0gMDsKfQoKc2VjdGlvbiBoZWFkZXIgewogICAgcGFkZGluZy10b3A6IDA7CiAgICB3aWR0aDogMTAwJTsKfQoKLyogTmF2ICovCm5hdiB7CiAgICBhbGlnbi1pdGVtczogY2VudGVyOwogICAgZGlzcGxheTogZmxleDsKICAgIGZvbnQtd2VpZ2h0OiBib2xkOwogICAganVzdGlmeS1jb250ZW50OiBzcGFjZS1iZXR3ZWVuOwogICAgbWFyZ2luLWJvdHRvbTogN3JlbTsKfQoKbmF2IHVsIHsKICAgIGxpc3Qtc3R5bGU6IG5vbmU7CiAgICBwYWRkaW5nOiAwOwp9CgpuYXYgdWwgbGkgewogICAgZGlzcGxheTogaW5saW5lLWJsb2NrOwogICAgbWFyZ2luOiAwIDAuNXJlbTsKfQoKLyogVHlwb2dyYXBoeSAqLwpjb2RlIHsKICAgIGRpc3BsYXk6IGlubGluZS1ibG9jazsKICAgIG1hcmdpbjogMCAwLjFyZW07CiAgICBwYWRkaW5nOiAwcmVtIDAuNXJlbTsKfQoKY29kZSwKc2FtcCB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvci1hY2NlbnQpOwogICAgY29sb3I6IHZhcigtLWNvbG9yLXRleHQpOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICB0ZXh0LWFsaWduOiB2YXIoLS1qdXN0aWZ5LW5vcm1hbCk7Cn0KCmgxLApoMiwKaDMsCmg0LApoNSwKaDYgewogICAgbGluZS1oZWlnaHQ6IHZhcigtLWxpbmUtaGVpZ2h0KTsKfQoKbWFyayB7CiAgICBwYWRkaW5nOiAwLjFyZW07Cn0KCm9sIGxpLAp1bCBsaSB7CiAgICBwYWRkaW5nOiAwLjJyZW0gMDsKfQoKcCB7CiAgICBtYXJnaW46IDAuNzVyZW0gMDsKICAgIHBhZGRpbmc6IDA7Cn0KCnNhbXAgewogICAgZGlzcGxheTogYmxvY2s7CiAgICBtYXJnaW46IDFyZW0gMDsKICAgIG1heC13aWR0aDogdmFyKC0td2lkdGgtY2FyZC13aWRlKTsKICAgIHBhZGRpbmc6IDFyZW07Cn0KCnNtYWxsIHsKICAgIGNvbG9yOiB2YXIoLS1jb2xvci10ZXh0LXNlY29uZGFyeSk7Cn0KCnN1cCB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvci1zZWNvbmRhcnkpOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICBjb2xvcjogdmFyKC0tY29sb3ItYmcpOwogICAgZm9udC1zaXplOiB4eC1zbWFsbDsKICAgIGZvbnQtd2VpZ2h0OiBib2xkOwogICAgbWFyZ2luOiAwLjJyZW07CiAgICBwYWRkaW5nOiAwLjJyZW0gMC4zcmVtOwogICAgcG9zaXRpb246IHJlbGF0aXZlOwogICAgdG9wOiAtMnB4Owp9CgovKiBMaW5rcyAqLwphIHsKICAgIGNvbG9yOiB2YXIoLS1jb2xvci1zZWNvbmRhcnkpOwogICAgZm9udC13ZWlnaHQ6IGJvbGQ7CiAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7Cn0KCmE6aG92ZXIgewogICAgZmlsdGVyOiBicmlnaHRuZXNzKHZhcigtLWhvdmVyLWJyaWdodG5lc3MpKTsKICAgIHRleHQtZGVjb3JhdGlvbjogdW5kZXJsaW5lOwp9CgphIGIsCmEgZW0sCmEgaSwKYSBzdHJvbmcsCmJ1dHRvbiB7CiAgICBib3JkZXItcmFkaXVzOiB2YXIoLS1ib3JkZXItcmFkaXVzKTsKICAgIGRpc3BsYXk6IGlubGluZS1ibG9jazsKICAgIGZvbnQtc2l6ZTogbWVkaXVtOwogICAgZm9udC13ZWlnaHQ6IGJvbGQ7CiAgICBtYXJnaW46IDEuNXJlbSAwIDAuNXJlbSAwOwogICAgcGFkZGluZzogMXJlbSAycmVtOwp9CgppbnB1dFt0eXBlPXN1Ym1pdF06aG92ZXIsCmJ1dHRvbjpob3ZlciB7CiAgICBjdXJzb3I6IHBvaW50ZXI7CiAgICBmaWx0ZXI6IGJyaWdodG5lc3ModmFyKC0taG92ZXItYnJpZ2h0bmVzcykpOwp9CgphIGIsCmEgc3Ryb25nLAppbnB1dFt0eXBlPXN1Ym1pdF0sCmJ1dHRvbiB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvcik7CiAgICBib3JkZXI6IDJweCBzb2xpZCB2YXIoLS1jb2xvcik7CiAgICBjb2xvcjogdmFyKC0tY29sb3ItYmcpOwp9CgphIGVtLAphIGkgewogICAgYm9yZGVyOiAycHggc29saWQgdmFyKC0tY29sb3IpOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICBjb2xvcjogdmFyKC0tY29sb3IpOwogICAgZGlzcGxheTogaW5saW5lLWJsb2NrOwogICAgcGFkZGluZzogMXJlbSAycmVtOwp9CgovKiBJbWFnZXMgKi8KZmlndXJlIHsKICAgIG1hcmdpbjogMDsKICAgIHBhZGRpbmc6IDA7Cn0KCmZpZ3VyZSBmaWdjYXB0aW9uIHsKICAgIGNvbG9yOiB2YXIoLS1jb2xvci10ZXh0LXNlY29uZGFyeSk7Cn0KCi8qIEZvcm1zICovCmZvcm0gewogICAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tY29sb3ItYmctc2Vjb25kYXJ5KTsKICAgIGJvcmRlci1yYWRpdXM6IHZhcigtLWJvcmRlci1yYWRpdXMpOwogICAgYm94LXNoYWRvdzogdmFyKC0tYm94LXNoYWRvdykgdmFyKC0tY29sb3Itc2hhZG93KTsKICAgIGRpc3BsYXk6IGJsb2NrOwogICAgbWF4LXdpZHRoOiB2YXIoLS13aWR0aC1jYXJkLXdpZGUpOwogICAgbWluLXdpZHRoOiB2YXIoLS13aWR0aC1jYXJkKTsKICAgIHBhZGRpbmc6IDEuNXJlbTsKICAgIHRleHQtYWxpZ246IHZhcigtLWp1c3RpZnktbm9ybWFsKTsKfQoKZm9ybSBoZWFkZXIgewogICAgbWFyZ2luOiAxLjVyZW0gMDsKICAgIHBhZGRpbmc6IDEuNXJlbSAwOwp9CgppbnB1dCwKbGFiZWwsCnNlbGVjdCwKdGV4dGFyZWEgewogICAgZGlzcGxheTogYmxvY2s7CiAgICBmb250LXNpemU6IGluaGVyaXQ7CiAgICBtYXgtd2lkdGg6IHZhcigtLXdpZHRoLWNhcmQtd2lkZSk7Cn0KCmlucHV0LApzZWxlY3QsCnRleHRhcmVhIHsKICAgIG1hcmdpbi1ib3R0b206IDFyZW07Cn0KCmlucHV0LApzZWxlY3QsCnRleHRhcmVhIHsKICAgIGJvcmRlcjogMXB4IHNvbGlkIHZhcigtLWNvbG9yLWJnLXNlY29uZGFyeSk7CiAgICBib3JkZXItcmFkaXVzOiB2YXIoLS1ib3JkZXItcmFkaXVzKTsKICAgIHBhZGRpbmc6IDAuNHJlbSAwLjhyZW07Cn0KCmxhYmVsIHsKICAgIGZvbnQtd2VpZ2h0OiBib2xkOwogICAgbWFyZ2luLWJvdHRvbTogMC4ycmVtOwp9CgovKiBUYWJsZXMgKi8KdGFibGUgewogICAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tY29sb3ItYmctc2Vjb25kYXJ5KTsKICAgIGJvcmRlci1yYWRpdXM6IHZhcigtLWJvcmRlci1yYWRpdXMpOwogICAgYm9yZGVyLXNwYWNpbmc6IDA7CiAgICBtYXgtd2lkdGg6IDEwMCU7CiAgICBvdmVyZmxvdzogaGlkZGVuOwogICAgcGFkZGluZzogMDsKfQoKdGFibGUgdGQsCnRhYmxlIHRoLAp0YWJsZSB0ciB7CiAgICBwYWRkaW5nOiAwLjRyZW0gMC44cmVtOwogICAgdGV4dC1hbGlnbjogdmFyKC0tanVzdGlmeS1pbXBvcnRhbnQpOwp9Cgp0YWJsZSB0aGVhZCB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1jb2xvcik7CiAgICBib3JkZXItY29sbGFwc2U6IGNvbGxhcHNlOwogICAgYm9yZGVyLXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7CiAgICBjb2xvcjogdmFyKC0tY29sb3ItYmcpOwogICAgbWFyZ2luOiAwOwogICAgcGFkZGluZzogMDsKfQoKdGFibGUgdGhlYWQgdGg6Zmlyc3QtY2hpbGQgewogICAgYm9yZGVyLXRvcC1sZWZ0LXJhZGl1czogdmFyKC0tYm9yZGVyLXJhZGl1cyk7Cn0KCnRhYmxlIHRoZWFkIHRoOmxhc3QtY2hpbGQgewogICAgYm9yZGVyLXRvcC1yaWdodC1yYWRpdXM6IHZhcigtLWJvcmRlci1yYWRpdXMpOwp9Cgp0YWJsZSB0aGVhZCB0aDpmaXJzdC1jaGlsZCwKdGFibGUgdHIgdGQ6Zmlyc3QtY2hpbGQgewogICAgdGV4dC1hbGlnbjogdmFyKC0tanVzdGlmeS1ub3JtYWwpOwp9CgovKiBRdW90ZXMgKi8KYmxvY2txdW90ZSB7CiAgICBkaXNwbGF5OiBibG9jazsKICAgIGZvbnQtc2l6ZTogeC1sYXJnZTsKICAgIGxpbmUtaGVpZ2h0OiB2YXIoLS1saW5lLWhlaWdodCk7CiAgICBtYXJnaW46IDFyZW0gYXV0bzsKICAgIG1heC13aWR0aDogdmFyKC0td2lkdGgtY2FyZC1tZWRpdW0pOwogICAgcGFkZGluZzogMS41cmVtIDFyZW07CiAgICB0ZXh0LWFsaWduOiB2YXIoLS1qdXN0aWZ5LWltcG9ydGFudCk7Cn0KCmJsb2NrcXVvdGUgZm9vdGVyIHsKICAgIGNvbG9yOiB2YXIoLS1jb2xvci10ZXh0LXNlY29uZGFyeSk7CiAgICBkaXNwbGF5OiBibG9jazsKICAgIGZvbnQtc2l6ZTogc21hbGw7CiAgICBsaW5lLWhlaWdodDogdmFyKC0tbGluZS1oZWlnaHQpOwogICAgcGFkZGluZzogMS41cmVtIDA7Cn0KCi8qIEN1c3RvbSBzdHlsZXMgKi8K")

	tmpls["html/_footer.html"] = tostring("PGRpdiBjbGFzcz1jb250YWluZXI+CiAgPGZvb3RlciBjbGFzcz0icHQtNCBteS1tZC01IHB0LW1kLTUgYm9yZGVyLXRvcCI+CiAgICA8ZGl2IGNsYXNzPSJyb3ciPgogICAgICA8ZGl2IGNsYXNzPSJjb2wtNiBjb2wtbWQtMyBvZmZzZXQtbWQtMyB0ZXh0LW1kLXJpZ2h0Ij4KICAgICAgICA8aDU+TmF2aWdhdGlvbjwvaDU+CiAgICAgICAgPHVsIGNsYXNzPSJsaXN0LXVuc3R5bGVkIHRleHQtc21hbGwiPgogICAgICAgICAge3sgaWYgLlNwYWNlIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8nPkhvbWU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkNvbnRlbnRUeXBlIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIC5Ib29rIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIC5Db250ZW50IH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9jb250ZW50dHlwZS97eyAuU3BhY2UuSUR9fS97eyAuQ29udGVudFR5cGUuSUQgfX0nPnt7IC5Db250ZW50VHlwZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIGFuZCAuU3BhY2UgKG5vdCAuQ29udGVudFR5cGUpIChub3QgLkhvb2spIH19CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjY29weU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+Q29weTwvYT48L2xpPgogICAgICAgICAgICA8bGk+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+VXBkYXRlPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgYW5kIC5Db250ZW50VHlwZSAobm90IC5Db250ZW50KSB9fQogICAgICAgICAgICA8bGk+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+VXBkYXRlPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkNvbnRlbnQgfX0KICAgICAgICAgICAgPGxpPjxpbnB1dCB0eXBlPXN1Ym1pdCBjbGFzcz0idGV4dC1kZWNvcmF0aW9uLW5vbmUgbS0wIHAtMCBidG4gYnRuLWxpbmsgdGV4dC1tdXRlZCBib3JkZXItMCIgdmFsdWU9U2F2ZSAvPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkhvb2sgfX0KICAgICAgICAgICAgPGxpPjxhIGRhdGEtdG9nZ2xlPSJtb2RhbCIgZGF0YS10YXJnZXQ9IiNkZWxldGVNb2RhbCIgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9JyMnPkRlbGV0ZTwvYT48L2xpPgogICAgICAgICAge3sgZW5kIH19CiAgICAgICAgICB7eyBpZiAuVXNlciB9fQogICAgICAgICAgICA8bGk+CiAgICAgICAgICAgICAgPGZvcm0gbWV0aG9kPVBPU1QgYWN0aW9uPScvdXNlci9sb2dvdXQnIGVuY3R5cGU9J211bHRpcGFydC9mb3JtLWRhdGEnPgogICAgICAgICAgICAgICAgPGlucHV0IHR5cGU9c3VibWl0IGNsYXNzPSJ0ZXh0LWRlY29yYXRpb24tbm9uZSBtLTAgcC0wIGJ0biBidG4tbGluayB0ZXh0LW11dGVkIGJvcmRlci0wIiB2YWx1ZT1Mb2dvdXQgLz4KICAgICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgICAgIDwvbGk+CiAgICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nL3BhZ2UvYmlsbGluZyc+QmlsbGluZzwvYT48L2xpPgogICAgICAgICAge3sgZWxzZSB9fQogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8nPkhvbWU8L2E+PC9saT4KICAgICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvI3NpZ251cCc+U2lnbnVwPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nLyNsb2dpbic+TG9naW48L2E+PC9saT4KICAgICAgICAgIHt7IGVuZH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8vZ2l0LnNyLmh0L35ldmFuai9jbXMnPlNvdXJjZTwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvL2dpdC5zci5odC9+ZXZhbmovY21zL3RyZWUvbWFzdGVyL0xJQ0VOU0UnPkxpY2Vuc2U8L2E+PC9saT4KICAgICAgICA8L3VsPgogICAgICA8L2Rpdj4KICAgICAgPGRpdiBjbGFzcz0iY29sLTYgY29sLW1kLTMiPgogICAgICAgIDxoNT5SZXNvdXJjZXM8L2g1PgogICAgICAgIDx1bCBjbGFzcz0ibGlzdC11bnN0eWxlZCB0ZXh0LXNtYWxsIj4KICAgICAgICAgIHt7aWYgLlVzZXJ9fQogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9wYWdlL2JpbGxpbmcnPkJpbGxpbmc8L2E+PC9saT4KICAgICAgICAgIHt7ZWxzZX19CiAgICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nLyNwcmljaW5nJz5QcmljaW5nPC9hPjwvbGk+CiAgICAgICAgICB7e2VuZH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9wYWdlL3RvdXInPlRvdXI8L2E+PC9saT4KICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nL3BhZ2UvZG9jJz5Eb2NzPC9hPjwvbGk+CiAgICAgICAgICA8bGk+PGEgY2xhc3M9InRleHQtbXV0ZWQiIGhyZWY9Ii9wYWdlL2ZhcSI+RkFRPC9hPjwvbGk+CiAgICAgICAgICA8bGk+PGEgY2xhc3M9InRleHQtbXV0ZWQiIGhyZWY9Ii9wYWdlL3Rlcm1zIj5UZXJtczwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSJ0ZXh0LW11dGVkIiBocmVmPSIvcGFnZS9wcml2YWN5Ij5Qcml2YWN5PC9hPjwvbGk+CiAgICAgICAgICA8bGk+PGEgY2xhc3M9InRleHQtbXV0ZWQiIGhyZWY9Ii9wYWdlL2NvbnRhY3QiPkNvbnRhY3Q8L2E+PC9saT4KICAgICAgICA8L3VsPgogICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz1yb3c+CiAgICAgIDxwIGNsYXNzPSd0ZXh0LW11dGVkIHRleHQtY2VudGVyIG10LTUgdy0xMDAgdGV4dC10cnVuY2F0ZSBvdmVyZmxvdy1oaWRkZW4nPnYue3suQnVpbGR9fTwvcD4KICAgIDwvZGl2PgogIDwvZm9vdGVyPgo8L2Rpdj4Ke3tpZiAuQX19CjxpbWcgc3R5bGU9J3Bvc2l0aW9uOiBmaXhlZDsgYm90dG9tOiAwOyByaWdodDogMDsnIHNyYz0iLy9za2lwcGVyY21zLmdvYXRjb3VudGVyLmNvbS9jb3VudD9wPXt7LkEuUGF0aH19e3tpZiAuQS5SZWZlcnJlcn19JnI9e3suQS5SZWZlcnJlcn19e3tlbmR9fSZybmQ9e3suQS5STkR9fSI+Cnt7ZW5kfX0K")
	tmpls["html/_footer.html"] = tostring("PGRpdiBjbGFzcz1jb250YWluZXI+CiAgPGZvb3RlciBjbGFzcz0icHQtNCBteS1tZC01IHB0LW1kLTUgYm9yZGVyLXRvcCI+CiAgICA8ZGl2IGNsYXNzPSJyb3ciPgogICAgICA8ZGl2IGNsYXNzPSJjb2wtNiBjb2wtbWQtMyBvZmZzZXQtbWQtMyB0ZXh0LW1kLXJpZ2h0Ij4KICAgICAgICA8aDU+TmF2aWdhdGlvbjwvaDU+CiAgICAgICAgPHVsIGNsYXNzPSJsaXN0LXVuc3R5bGVkIHRleHQtc21hbGwiPgogICAgICAgICAge3sgaWYgLlNwYWNlIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8nPkhvbWU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkNvbnRlbnRUeXBlIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIC5Ib29rIH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIC5Db250ZW50IH19CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9jb250ZW50dHlwZS97eyAuU3BhY2UuSUR9fS97eyAuQ29udGVudFR5cGUuSUQgfX0nPnt7IC5Db250ZW50VHlwZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmQgfX0KICAgICAgICAgIHt7IGlmIGFuZCAuU3BhY2UgKG5vdCAuQ29udGVudFR5cGUpIChub3QgLkhvb2spIH19CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjY29weU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+Q29weTwvYT48L2xpPgogICAgICAgICAgICA8bGk+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+VXBkYXRlPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgYW5kIC5Db250ZW50VHlwZSAobm90IC5Db250ZW50KSB9fQogICAgICAgICAgICA8bGk+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nIyc+VXBkYXRlPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkNvbnRlbnQgfX0KICAgICAgICAgICAgPGxpPjxpbnB1dCB0eXBlPXN1Ym1pdCBjbGFzcz0idGV4dC1kZWNvcmF0aW9uLW5vbmUgbS0wIHAtMCBidG4gYnRuLWxpbmsgdGV4dC1tdXRlZCBib3JkZXItMCIgdmFsdWU9U2F2ZSAvPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAge3sgaWYgLkhvb2sgfX0KICAgICAgICAgICAgPGxpPjxhIGRhdGEtdG9nZ2xlPSJtb2RhbCIgZGF0YS10YXJnZXQ9IiNkZWxldGVNb2RhbCIgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9JyMnPkRlbGV0ZTwvYT48L2xpPgogICAgICAgICAge3sgZW5kIH19CiAgICAgICAgICB7eyBpZiAuVXNlciB9fQogICAgICAgICAgICA8bGk+CiAgICAgICAgICAgICAgPGZvcm0gbWV0aG9kPVBPU1QgYWN0aW9uPScvdXNlci9sb2dvdXQnIGVuY3R5cGU9J211bHRpcGFydC9mb3JtLWRhdGEnPgogICAgICAgICAgICAgICAgPGlucHV0IHR5cGU9c3VibWl0IGNsYXNzPSJ0ZXh0LWRlY29yYXRpb24tbm9uZSBtLTAgcC0wIGJ0biBidG4tbGluayB0ZXh0LW11dGVkIGJvcmRlci0wIiB2YWx1ZT1Mb2dvdXQgLz4KICAgICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgICAgIDwvbGk+CiAgICAgICAgICAgIHt7aWYgYW5kIC5Vc2VyICguVXNlciB8IHBhaWQpfX0KICAgICAgICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjaW52aXRlTW9kYWwiIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScjJz5JbnZpdGU8L2E+PC9saT4KICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9wYWdlL2JpbGxpbmcnPkJpbGxpbmc8L2E+PC9saT4KICAgICAgICAgIHt7IGVsc2UgfX0KICAgICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvJz5Ib21lPC9hPjwvbGk+CiAgICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nLyNzaWdudXAnPlNpZ251cDwvYT48L2xpPgogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8jbG9naW4nPkxvZ2luPC9hPjwvbGk+CiAgICAgICAgICB7eyBlbmR9fQogICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvL2dpdC5zci5odC9+ZXZhbmovY21zJz5Tb3VyY2U8L2E+PC9saT4KICAgICAgICAgIDxsaT48YSBjbGFzcz0ndGV4dC1tdXRlZCcgaHJlZj0nLy9naXQuc3IuaHQvfmV2YW5qL2Ntcy90cmVlL21hc3Rlci9MSUNFTlNFJz5MaWNlbnNlPC9hPjwvbGk+CiAgICAgICAgPC91bD4KICAgICAgPC9kaXY+CiAgICAgIDxkaXYgY2xhc3M9ImNvbC02IGNvbC1tZC0zIj4KICAgICAgICA8aDU+UmVzb3VyY2VzPC9oNT4KICAgICAgICA8dWwgY2xhc3M9Imxpc3QtdW5zdHlsZWQgdGV4dC1zbWFsbCI+CiAgICAgICAgICB7e2lmIC5Vc2VyfX0KICAgICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvcGFnZS9iaWxsaW5nJz5CaWxsaW5nPC9hPjwvbGk+CiAgICAgICAgICB7e2Vsc2V9fQogICAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy8jcHJpY2luZyc+UHJpY2luZzwvYT48L2xpPgogICAgICAgICAge3tlbmR9fQogICAgICAgICAgPGxpPjxhIGNsYXNzPSd0ZXh0LW11dGVkJyBocmVmPScvcGFnZS90b3VyJz5Ub3VyPC9hPjwvbGk+CiAgICAgICAgICA8bGk+PGEgY2xhc3M9J3RleHQtbXV0ZWQnIGhyZWY9Jy9wYWdlL2RvYyc+RG9jczwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSJ0ZXh0LW11dGVkIiBocmVmPSIvcGFnZS9mYXEiPkZBUTwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSJ0ZXh0LW11dGVkIiBocmVmPSIvcGFnZS90ZXJtcyI+VGVybXM8L2E+PC9saT4KICAgICAgICAgIDxsaT48YSBjbGFzcz0idGV4dC1tdXRlZCIgaHJlZj0iL3BhZ2UvcHJpdmFjeSI+UHJpdmFjeTwvYT48L2xpPgogICAgICAgICAgPGxpPjxhIGNsYXNzPSJ0ZXh0LW11dGVkIiBocmVmPSIvcGFnZS9jb250YWN0Ij5Db250YWN0PC9hPjwvbGk+CiAgICAgICAgPC91bD4KICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICAgIDxkaXYgY2xhc3M9cm93PgogICAgICA8cCBjbGFzcz0ndGV4dC1tdXRlZCB0ZXh0LWNlbnRlciBtdC01IHctMTAwIHRleHQtdHJ1bmNhdGUgb3ZlcmZsb3ctaGlkZGVuJz52Lnt7LkJ1aWxkfX08L3A+CiAgICA8L2Rpdj4KICA8L2Zvb3Rlcj4KPC9kaXY+Cgp7e2lmIGFuZCAuVXNlciAoLlVzZXIgfCBwYWlkKX19Cjxmb3JtIG1ldGhvZD1QT1NUIGFjdGlvbj0nL2ludml0ZScgZW5jdHlwZT0nbXVsdGlwYXJ0L2Zvcm0tZGF0YSc+CiAgPGlucHV0IHR5cGU9aGlkZGVuIG5hbWU9bWV0aG9kIHZhbHVlPVBPU1QgLz4KICA8aW5wdXQgdHlwZT1oaWRkZW4gbmFtZT1tZXRob2QgdmFsdWU9Int7LlVzZXIuT3JnLklEfX0iIC8+CiAgPGRpdiBjbGFzcz0ibW9kYWwgZmFkZSIgaWQ9Imludml0ZU1vZGFsIiB0YWJpbmRleD0iLTEiIHJvbGU9ImRpYWxvZyIgYXJpYS1sYWJlbGxlZGJ5PSJleGFtcGxlTW9kYWxMYWJlbCIgYXJpYS1oaWRkZW49InRydWUiPgogICAgPGRpdiBjbGFzcz0ibW9kYWwtZGlhbG9nIG1vZGFsLWRpYWxvZy1zY3JvbGxhYmxlIj4KICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtY29udGVudCI+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtaGVhZGVyIj4KICAgICAgICAgIDxoNSBjbGFzcz0ibW9kYWwtdGl0bGUiIGlkPSJleGFtcGxlTW9kYWxMYWJlbCI+SW52aXRlIHNvbWVvbmUgdG8geW91ciBzcGFjZShzKTwvaDU+CiAgICAgICAgICA8YnV0dG9uIHR5cGU9ImJ1dHRvbiIgY2xhc3M9ImNsb3NlIiBkYXRhLWRpc21pc3M9Im1vZGFsIiBhcmlhLWxhYmVsPSJDbG9zZSI+CiAgICAgICAgICAgIDxzcGFuIGFyaWEtaGlkZGVuPSJ0cnVlIj4mdGltZXM7PC9zcGFuPgogICAgICAgICAgPC9idXR0b24+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtYm9keSI+CiAgICAgICAgICA8cD5XZSdsbCBnZW5lcmF0ZSBhIHNwZWNpYWwgbGluayBmb3IgeW91LiBTZW5kIHRoaXMgdG8geW91ciBmcmllbmQsIAogICAgICAgICAgY293b3JrZXIsIG9yIHdob2V2ZXIhPC9wPgogICAgICAgICAgPHA+VGhlIGludml0ZSB3aWxsIG9ubHkgYmUgYWN0aXZlIGZvciBvbmUgaG91ci48L3A+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtZm9vdGVyIj4KICAgICAgICAgIDxidXR0b24gdHlwZT0iYnV0dG9uIiBjbGFzcz0iYnRuIGJ0bi1zZWNvbmRhcnkiIGRhdGEtZGlzbWlzcz0ibW9kYWwiPkNsb3NlPC9idXR0b24+CiAgICAgICAgICA8YnV0dG9uIHR5cGU9InN1Ym1pdCIgY2xhc3M9ImJ0biBidG4tcHJpbWFyeSI+R288L2J1dHRvbj4KICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICA8L2Rpdj4KPC9mb3JtPgp7e2VuZH19Cgp7e2lmIC5BfX0KPGltZyBzdHlsZT0ncG9zaXRpb246IGZpeGVkOyBib3R0b206IDA7IHJpZ2h0OiAwOycgc3JjPSIvL3NraXBwZXJjbXMuZ29hdGNvdW50ZXIuY29tL2NvdW50P3A9e3suQS5QYXRofX17e2lmIC5BLlJlZmVycmVyfX0mcj17ey5BLlJlZmVycmVyfX17e2VuZH19JnJuZD17ey5BLlJORH19Ij4Ke3tlbmR9fQo=")

	tmpls["html/_head.html"] = tostring("PG1ldGEgY2hhcnNldD0ndXRmLTgnPgo8bWV0YSBuYW1lPSd2aWV3cG9ydCcgY29udGVudD0nd2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEnPgo8bGluayByZWw9J2ljb24nIHR5cGU9J2ltYWdlL3gtaWNvbicgaHJlZj0naHR0cHM6Ly9mYXZpY29uLmV2YW5qb24uZXMvMC8xMDUvMjE3LzMyL2Zhdmljb24uaWNvJyAvPgo8bGluayByZWw9J3N0eWxlc2hlZXQnIGhyZWY9Jy9zdGF0aWMvY3NzL2Jvb3RzdHJhcC5taW4uY3NzJyAvPgo=")

	tmpls["html/_header.html"] = tostring("PGhlYWRlciBjbGFzcz0nYmctcHJpbWFyeSc+CiAgPG5hdiBjbGFzcz0nY29udGFpbmVyIG5hdmJhciBuYXZiYXItZXhwYW5kLWxnIG5hdmJhci1kYXJrJz4KICAgIDxhIGNsYXNzPSduYXZiYXItYnJhbmQnIGhyZWY9Jy8nPgogICAgICA8aW1nIHdpZHRoPTUwIGhlaWdodD01MCBzcmM9Jy9zdGF0aWMvaW1nL2xvZ28td2hpdGUuc3ZnJz4KICAgICAgPHNwYW4gY2xhc3M9J2Qtbm9uZSBkLWxnLWlubGluZSc+U2tpcHBlciBDTVM8L3NwYW4+CiAgICA8L2E+CiAgICA8YnV0dG9uIGNsYXNzPSduYXZiYXItdG9nZ2xlcicgdHlwZT0nYnV0dG9uJyBkYXRhLXRvZ2dsZT0nY29sbGFwc2UnIGRhdGEtdGFyZ2V0PScjbmF2YmFyU3VwcG9ydGVkQ29udGVudCcgYXJpYS1jb250cm9scz0nbmF2YmFyU3VwcG9ydGVkQ29udGVudCcgYXJpYS1leHBhbmRlZD0nZmFsc2UnIGFyaWEtbGFiZWw9J1RvZ2dsZSBuYXZpZ2F0aW9uJz4KICAgICAgPHNwYW4gY2xhc3M9J25hdmJhci10b2dnbGVyLWljb24nPjwvc3Bhbj4KICAgIDwvYnV0dG9uPgogICAgPGRpdiBjbGFzcz0nY29sbGFwc2UgbmF2YmFyLWNvbGxhcHNlJyBpZD0nbmF2YmFyU3VwcG9ydGVkQ29udGVudCc+CiAgICAgIDx1bCBjbGFzcz0nbmF2YmFyLW5hdiBtbC1hdXRvJz4KICAgICAgICB7eyBpZiAuU3BhY2UgfX0KICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy8nPkhvbWU8L2E+PC9saT4KICAgICAgICB7eyBlbmQgfX0KICAgICAgICB7eyBpZiAuQ29udGVudFR5cGUgfX0KICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAge3sgZW5kIH19CiAgICAgICAge3sgaWYgLkhvb2sgfX0KICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAge3sgZW5kIH19CiAgICAgICAge3sgaWYgLkNvbnRlbnQgfX0KICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy9jb250ZW50dHlwZS97eyAuU3BhY2UuSUR9fS97eyAuQ29udGVudFR5cGUuSUQgfX0nPnt7IC5Db250ZW50VHlwZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAge3sgZW5kIH19CiAgICAgICAge3sgaWYgYW5kIC5TcGFjZSAobm90IC5Db250ZW50VHlwZSkgKG5vdCAuSG9vaykgfX0KICAgICAgICAgIDxsaSBjbGFzcz0nbmF2LWl0ZW0nPjxhIGRhdGEtdG9nZ2xlPSJtb2RhbCIgZGF0YS10YXJnZXQ9IiNjb3B5TW9kYWwiIGNsYXNzPSduYXYtbGluaycgaHJlZj0nIyc+Q29weTwvYT48L2xpPgogICAgICAgICAgPGxpIGNsYXNzPSduYXYtaXRlbSc+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9JyMnPlVwZGF0ZTwvYT48L2xpPgogICAgICAgICAgPGxpIGNsYXNzPSduYXYtaXRlbSc+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI2RlbGV0ZU1vZGFsIiBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9JyMnPkRlbGV0ZTwvYT48L2xpPgogICAgICAgIHt7IGVuZCB9fQogICAgICAgIHt7IGlmIGFuZCAuQ29udGVudFR5cGUgKG5vdCAuQ29udGVudCkgfX0KICAgICAgICAgIDxsaSBjbGFzcz0nbmF2LWl0ZW0nPjxhIGRhdGEtdG9nZ2xlPSJtb2RhbCIgZGF0YS10YXJnZXQ9IiN1cGRhdGVNb2RhbCIgY2xhc3M9J25hdi1saW5rJyBocmVmPScjJz5VcGRhdGU8L2E+PC9saT4KICAgICAgICAgIDxsaSBjbGFzcz0nbmF2LWl0ZW0nPjxhIGRhdGEtdG9nZ2xlPSJtb2RhbCIgZGF0YS10YXJnZXQ9IiNkZWxldGVNb2RhbCIgY2xhc3M9J25hdi1saW5rJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICB7eyBlbmQgfX0KICAgICAgICB7eyBpZiAuQ29udGVudCB9fQogICAgICAgICAgPGxpIGNsYXNzPSduYXYtaXRlbSc+PGlucHV0IHR5cGU9c3VibWl0IGNsYXNzPSJidG4gYnRuLWxpbmsgbmF2LWxpbmsgYm9yZGVyLTAiIHZhbHVlPVNhdmUgLz48L2xpPgogICAgICAgICAgPGxpIGNsYXNzPSduYXYtaXRlbSc+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI2RlbGV0ZU1vZGFsIiBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9JyMnPkRlbGV0ZTwvYT48L2xpPgogICAgICAgIHt7IGVuZCB9fQogICAgICAgIHt7IGlmIC5Ib29rIH19CiAgICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSduYXYtbGluaycgaHJlZj0nIyc+RGVsZXRlPC9hPjwvbGk+CiAgICAgICAge3sgZW5kIH19CiAgICAgICAge3sgaWYgLlVzZXIgfX0KICAgICAgICAgIDxsaSBjbGFzcz0nbmF2LWl0ZW0nPgogICAgICAgICAgICA8Zm9ybSBtZXRob2Q9UE9TVCBhY3Rpb249Jy91c2VyL2xvZ291dCcgZW5jdHlwZT0nbXVsdGlwYXJ0L2Zvcm0tZGF0YSc+CiAgICAgICAgICAgICAgPGlucHV0IHR5cGU9c3VibWl0IGNsYXNzPSJidG4gYnRuLWxpbmsgbmF2LWxpbmsgYm9yZGVyLTAiIHZhbHVlPUxvZ291dCAvPgogICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgICA8L2xpPgogICAgICAgICAgPGxpIGNsYXNzPSduYXYtaXRlbSc+PGEgY2xhc3M9J25hdi1saW5rJyBocmVmPScvcGFnZS9iaWxsaW5nJz5CaWxsaW5nPC9hPjwvbGk+CiAgICAgICAge3sgZWxzZSB9fQogICAgICAgICAgPGxpIGNsYXNzPSduYXYtaXRlbSc+PGEgY2xhc3M9J25hdi1saW5rJyBocmVmPScvI3NpZ251cCc+U2lnbnVwPC9hPjwvbGk+CiAgICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy8jbG9naW4nPkxvZ2luPC9hPjwvbGk+CiAgICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy8jcHJpY2luZyc+UHJpY2luZzwvYT48L2xpPgogICAgICAgIHt7IGVuZH19CiAgICAgICAgPGxpIGNsYXNzPSduYXYtaXRlbSc+PGEgY2xhc3M9J25hdi1saW5rJyBocmVmPScvcGFnZS90b3VyJz5Ub3VyPC9hPjwvbGk+CiAgICAgICAgPGxpIGNsYXNzPSduYXYtaXRlbSc+PGEgY2xhc3M9J25hdi1saW5rJyBocmVmPScvcGFnZS9kb2MnPkRvY3M8L2E+PC9saT4KICAgICAgPC91bD4KICAgIDwvZGl2PgogIDwvbmF2Pgo8L2hlYWRlcj4K")
	tmpls["html/_header.html"] = tostring("PGhlYWRlciBjbGFzcz0nYmctcHJpbWFyeSc+CiAgPG5hdiBjbGFzcz0nY29udGFpbmVyIG5hdmJhciBuYXZiYXItZXhwYW5kLWxnIG5hdmJhci1kYXJrJz4KICAgIDxhIGNsYXNzPSduYXZiYXItYnJhbmQnIGhyZWY9Jy8nPgogICAgICA8aW1nIHdpZHRoPTUwIGhlaWdodD01MCBzcmM9Jy9zdGF0aWMvaW1nL2xvZ28td2hpdGUuc3ZnJz4KICAgICAgPHNwYW4gY2xhc3M9J2Qtbm9uZSBkLWxnLWlubGluZSc+U2tpcHBlciBDTVM8L3NwYW4+CiAgICA8L2E+CiAgICA8YnV0dG9uIGNsYXNzPSduYXZiYXItdG9nZ2xlcicgdHlwZT0nYnV0dG9uJyBkYXRhLXRvZ2dsZT0nY29sbGFwc2UnIGRhdGEtdGFyZ2V0PScjbmF2YmFyU3VwcG9ydGVkQ29udGVudCcgYXJpYS1jb250cm9scz0nbmF2YmFyU3VwcG9ydGVkQ29udGVudCcgYXJpYS1leHBhbmRlZD0nZmFsc2UnIGFyaWEtbGFiZWw9J1RvZ2dsZSBuYXZpZ2F0aW9uJz4KICAgICAgPHNwYW4gY2xhc3M9J25hdmJhci10b2dnbGVyLWljb24nPjwvc3Bhbj4KICAgIDwvYnV0dG9uPgogICAgPGRpdiBjbGFzcz0nY29sbGFwc2UgbmF2YmFyLWNvbGxhcHNlJyBpZD0nbmF2YmFyU3VwcG9ydGVkQ29udGVudCc+CiAgICAgIDx1bCBjbGFzcz0nbmF2YmFyLW5hdiBtbC1hdXRvJz4KICAgICAgICB7eyBpZiAuU3BhY2UgfX0KICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy8nPkhvbWU8L2E+PC9saT4KICAgICAgICB7eyBlbmQgfX0KICAgICAgICB7eyBpZiAuQ29udGVudFR5cGUgfX0KICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAge3sgZW5kIH19CiAgICAgICAge3sgaWYgLkhvb2sgfX0KICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy9zcGFjZS97eyAuU3BhY2UuSUQgfX0nPnt7IC5TcGFjZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAge3sgZW5kIH19CiAgICAgICAge3sgaWYgLkNvbnRlbnQgfX0KICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy9jb250ZW50dHlwZS97eyAuU3BhY2UuSUR9fS97eyAuQ29udGVudFR5cGUuSUQgfX0nPnt7IC5Db250ZW50VHlwZS5OYW1lIH19PC9hPjwvbGk+CiAgICAgICAge3sgZW5kIH19CiAgICAgICAge3sgaWYgYW5kIC5TcGFjZSAobm90IC5Db250ZW50VHlwZSkgKG5vdCAuSG9vaykgfX0KICAgICAgICAgIDxsaSBjbGFzcz0nbmF2LWl0ZW0nPjxhIGRhdGEtdG9nZ2xlPSJtb2RhbCIgZGF0YS10YXJnZXQ9IiNjb3B5TW9kYWwiIGNsYXNzPSduYXYtbGluaycgaHJlZj0nIyc+Q29weTwvYT48L2xpPgogICAgICAgICAgPGxpIGNsYXNzPSduYXYtaXRlbSc+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI3VwZGF0ZU1vZGFsIiBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9JyMnPlVwZGF0ZTwvYT48L2xpPgogICAgICAgICAgPGxpIGNsYXNzPSduYXYtaXRlbSc+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI2RlbGV0ZU1vZGFsIiBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9JyMnPkRlbGV0ZTwvYT48L2xpPgogICAgICAgIHt7IGVuZCB9fQogICAgICAgIHt7IGlmIGFuZCAuQ29udGVudFR5cGUgKG5vdCAuQ29udGVudCkgfX0KICAgICAgICAgIDxsaSBjbGFzcz0nbmF2LWl0ZW0nPjxhIGRhdGEtdG9nZ2xlPSJtb2RhbCIgZGF0YS10YXJnZXQ9IiN1cGRhdGVNb2RhbCIgY2xhc3M9J25hdi1saW5rJyBocmVmPScjJz5VcGRhdGU8L2E+PC9saT4KICAgICAgICAgIDxsaSBjbGFzcz0nbmF2LWl0ZW0nPjxhIGRhdGEtdG9nZ2xlPSJtb2RhbCIgZGF0YS10YXJnZXQ9IiNkZWxldGVNb2RhbCIgY2xhc3M9J25hdi1saW5rJyBocmVmPScjJz5EZWxldGU8L2E+PC9saT4KICAgICAgICB7eyBlbmQgfX0KICAgICAgICB7eyBpZiAuQ29udGVudCB9fQogICAgICAgICAgPGxpIGNsYXNzPSduYXYtaXRlbSc+PGlucHV0IHR5cGU9c3VibWl0IGNsYXNzPSJidG4gYnRuLWxpbmsgbmF2LWxpbmsgYm9yZGVyLTAiIHZhbHVlPVNhdmUgLz48L2xpPgogICAgICAgICAgPGxpIGNsYXNzPSduYXYtaXRlbSc+PGEgZGF0YS10b2dnbGU9Im1vZGFsIiBkYXRhLXRhcmdldD0iI2RlbGV0ZU1vZGFsIiBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9JyMnPkRlbGV0ZTwvYT48L2xpPgogICAgICAgIHt7IGVuZCB9fQogICAgICAgIHt7IGlmIC5Ib29rIH19CiAgICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZGVsZXRlTW9kYWwiIGNsYXNzPSduYXYtbGluaycgaHJlZj0nIyc+RGVsZXRlPC9hPjwvbGk+CiAgICAgICAge3sgZW5kIH19CiAgICAgICAge3sgaWYgLlVzZXIgfX0KICAgICAgICAgIDxsaSBjbGFzcz0nbmF2LWl0ZW0nPgogICAgICAgICAgICA8Zm9ybSBtZXRob2Q9UE9TVCBhY3Rpb249Jy91c2VyL2xvZ291dCcgZW5jdHlwZT0nbXVsdGlwYXJ0L2Zvcm0tZGF0YSc+CiAgICAgICAgICAgICAgPGlucHV0IHR5cGU9c3VibWl0IGNsYXNzPSJidG4gYnRuLWxpbmsgbmF2LWxpbmsgYm9yZGVyLTAiIHZhbHVlPUxvZ291dCAvPgogICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgICA8L2xpPgogICAgICAgICAge3tpZiBhbmQgLlVzZXIgKC5Vc2VyIHwgcGFpZCl9fQogICAgICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjaW52aXRlTW9kYWwiIGNsYXNzPSduYXYtbGluaycgaHJlZj0nIyc+SW52aXRlPC9hPjwvbGk+CiAgICAgICAgICB7e2VuZH19CiAgICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy9wYWdlL2JpbGxpbmcnPkJpbGxpbmc8L2E+PC9saT4KICAgICAgICB7eyBlbHNlIH19CiAgICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy8jc2lnbnVwJz5TaWdudXA8L2E+PC9saT4KICAgICAgICAgIDxsaSBjbGFzcz0nbmF2LWl0ZW0nPjxhIGNsYXNzPSduYXYtbGluaycgaHJlZj0nLyNsb2dpbic+TG9naW48L2E+PC9saT4KICAgICAgICAgIDxsaSBjbGFzcz0nbmF2LWl0ZW0nPjxhIGNsYXNzPSduYXYtbGluaycgaHJlZj0nLyNwcmljaW5nJz5QcmljaW5nPC9hPjwvbGk+CiAgICAgICAge3sgZW5kfX0KICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy9wYWdlL3RvdXInPlRvdXI8L2E+PC9saT4KICAgICAgICA8bGkgY2xhc3M9J25hdi1pdGVtJz48YSBjbGFzcz0nbmF2LWxpbmsnIGhyZWY9Jy9wYWdlL2RvYyc+RG9jczwvYT48L2xpPgogICAgICA8L3VsPgogICAgPC9kaXY+CiAgPC9uYXY+CjwvaGVhZGVyPgo=")

	tmpls["html/_scripts.html"] = tostring("PHNjcmlwdCBzcmM9Jy9zdGF0aWMvanMvcG9wcGVyLm1pbi5qcyc+PC9zY3JpcHQ+CjxzY3JpcHQgc3JjPScvc3RhdGljL2pzL2Jvb3RzdHJhcC5taW4uanMnPjwvc2NyaXB0Pgo=")

	tmpls["html/billing.html"] = tostring("PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ZW4+CjxoZWFkPgogIHt7IHRlbXBsYXRlICJodG1sL19oZWFkLmh0bWwiIH19CiAgPHRpdGxlPlNraXBwZXIgQ01TIHwgQmlsbGluZzwvdGl0bGU+CjwvaGVhZD4KPGJvZHkgY2xhc3M9J3BhZ2UgYmctbGlnaHQnPgogIDxzdHlsZT57eyB0ZW1wbGF0ZSAiY3NzL21haW4uY3NzIiB9fTwvc3R5bGU+CiAgPG1haW4+CiAgICB7eyB0ZW1wbGF0ZSAiaHRtbC9faGVhZGVyLmh0bWwiICQgfX0KICAgIDxkaXYgY2xhc3M9InByaWNpbmctaGVhZGVyIHB4LTMgcHktMyBwdC1tZC01IHBiLW1kLTQgbXgtYXV0byB0ZXh0LWNlbnRlciI+CiAgICAgIDxoMSBjbGFzcz0iZGlzcGxheS00Ij5CaWxsaW5nPC9oMT4KICAgIDwvZGl2PgogICAge3tpZiAuVXNlcn19CiAgICAgIDxkaXYgY2xhc3M9J2NvbnRhaW5lcic+CiAgICAgICAgPGRpdiBjbGFzcz0ncm93Jz4KICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbC0xMiBjb2wtbWQtNiBtYi01Ij4KICAgICAgICAgICAgPGRpdiBjbGFzcz0ndGV4dC1jZW50ZXInPgogICAgICAgICAgICAgIHt7aWYgLlVzZXIuSGFzRW1haWx9fQogICAgICAgICAgICAgIDxwPlVwZGF0ZSB5b3VyIGVtYWlsLjwvcD4KICAgICAgICAgICAgICB7e2Vsc2V9fQogICAgICAgICAgICAgIDxwPlNldCB5b3VyIGVtYWlsIGluIGNhc2UgeW91IGdldCBsb2NrZWQgb3V0IG9mIHlvdXIgYWNjb3VudC48L3A+CiAgICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPGZvcm0gYWN0aW9uPScvdXNlci91cGRhdGUvZW1haWwnIG1ldGhvZD1QT1NUPgogICAgICAgICAgICAgIDxsYWJlbCBmb3I9ZW1haWw+RW1haWw8L2xhYmVsPgogICAgICAgICAgICAgIDxpbnB1dCBpZD1lbWFpbCBuYW1lPWVtYWlsIHR5cGU9ZW1haWwgY2xhc3M9Im1iLTMgZm9ybS1jb250cm9sIiBwbGFjZWhvbGRlcj0iZW1haWwiIHJlcXVpcmVkIHt7aWYgLlVzZXIuSGFzRW1haWx9fXZhbHVlPSJ7ey5Vc2VyLkVtYWlsfX0ie3tlbmR9fT4KICAgICAgICAgICAgICA8YnV0dG9uIHR5cGU9InN1Ym1pdCIgY2xhc3M9ImJ0biBidG4tcHJpbWFyeSI+R288L2J1dHRvbj4KICAgICAgICAgICAgPC9mb3JtPgogICAgICAgICAgICA8ZGl2IGNsYXNzPSd0ZXh0LWNlbnRlciBtdC0zJz4KICAgICAgICAgICAgICA8cD5VcGRhdGUgeW91ciBwYXNzd29yZC48L3A+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8Zm9ybSBhY3Rpb249Jy91c2VyL3VwZGF0ZS9wYXNzd29yZCcgbWV0aG9kPVBPU1Q+CiAgICAgICAgICAgICAgPGxhYmVsIGZvcj1jdXJyZW50ID5DdXJyZW50IFBhc3N3b3JkPC9sYWJlbD4KICAgICAgICAgICAgICA8aW5wdXQgaWQ9Y3VycmVudCBuYW1lPWN1cnJlbnQgdHlwZT1wYXNzd29yZCBjbGFzcz0ibWItMyBmb3JtLWNvbnRyb2wiIHBsYWNlaG9sZGVyPSJjdXJyZW50IiByZXF1aXJlZD4KICAgICAgICAgICAgICA8bGFiZWwgZm9yPXBhc3N3b3JkPk5ldyBQYXNzd29yZDwvbGFiZWw+CiAgICAgICAgICAgICAgPGlucHV0IGlkPXBhc3N3b3JkIG5hbWU9cGFzc3dvcmQgdHlwZT1wYXNzd29yZCBjbGFzcz0ibWItMyBmb3JtLWNvbnRyb2wiIHBsYWNlaG9sZGVyPSJwYXNzd29yZCIgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgPGxhYmVsIGZvcj12ZXJpZnk+VmVyaWZ5PC9sYWJlbD4KICAgICAgICAgICAgICA8aW5wdXQgaWQ9dmVyaWZ5IG5hbWU9dmVyaWZ5IHR5cGU9cGFzc3dvcmQgY2xhc3M9Im1iLTMgZm9ybS1jb250cm9sIiBwbGFjZWhvbGRlcj0idmVyaWZ5IiByZXF1aXJlZD4KICAgICAgICAgICAgICA8YnV0dG9uIHR5cGU9InN1Ym1pdCIgY2xhc3M9ImJ0biBidG4tcHJpbWFyeSI+R288L2J1dHRvbj4KICAgICAgICAgICAgPC9mb3JtPgogICAgICAgICAgPC9kaXY+CiAgICAgICAgICA8ZGl2IGNsYXNzPSJjb2wtMTIgY29sLW1kLTYgbWItNSI+CiAgICAgICAgICAgIHt7aWYgLlVzZXIgfCBwYWlkfX0KICAgICAgICAgICAgICA8Zm9ybSBhY3Rpb249Jy91c2VyL2NhbmNlbC9iaWxsaW5nJyBtZXRob2Q9UE9TVCBjbGFzcz0idGV4dC1jZW50ZXIiPgogICAgICAgICAgICAgICAgPHAgY2xhc3M9J21iLTUnPkNhbmNlbCB5b3VyIHt7LlVzZXIuT3JnLlRpZXIuTmFtZX19IHN1YnNjcmlwdGlvbi48L3A+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJkLWlubGluZS1ibG9jayI+CiAgICAgICAgICAgICAgICAgIDxpbnB1dCB0eXBlPWhpZGRlbiBuYW1lPXRpZXIgdmFsdWU9Int7LlVzZXIuT3JnLlRpZXIuTmFtZX19IiAvPgogICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIG1iLTQgc2hhZG93LXNtIj4KICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZC1oZWFkZXIiPgogICAgICAgICAgICAgICAgICAgIDxoNCBjbGFzcz0ibXktMCBmb250LXdlaWdodC1ub3JtYWwiPnt7LlVzZXIuT3JnLlRpZXIuTmFtZX19PC9oND4KICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtYm9keSI+CiAgICAgICAgICAgICAgICAgICAgPGgxIGNsYXNzPSJjYXJkLXRpdGxlIHByaWNpbmctY2FyZC10aXRsZSI+e3suVXNlci5PcmcuVGllci5QcmljZX19IDxzbWFsbCBjbGFzcz0idGV4dC1tdXRlZCI+LyB7ey5Vc2VyLk9yZy5UaWVyLlRpbWVVbml0fX08L3NtYWxsPjwvaDE+CiAgICAgICAgICAgICAgICAgICAgPHVsIGNsYXNzPSJsaXN0LXVuc3R5bGVkIG10LTMgbWItNCI+CiAgICAgICAgICAgICAgICAgICAgICB7e3JhbmdlIC5Vc2VyLk9yZy5UaWVyLk9wdHN9fQogICAgICAgICAgICAgICAgICAgICAgICA8bGk+e3suVGV4dH19PC9saT4KICAgICAgICAgICAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgICAgICAgICAgICA8L3VsPgogICAgICAgICAgICAgICAgICAgIDxidXR0b24gdHlwZT0ic3VibWl0IiBjbGFzcz0iYnRuIGJ0bi1wcmltYXJ5IHctMTAwIj5DYW5jZWw8L2J1dHRvbj4KICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgICAgIHt7ZWxzZX19CiAgICAgICAgICAgICAgPHAgY2xhc3M9J3RleHQtY2VudGVyIG1iLTUnPlVwZ3JhZGUgdG8gYSBwYWlkIHRpZXIuPGJyPkdldCBtb3JlIGFjY2Vzcy48L3A+CiAgICAgICAgICAgICAgPGRpdiBjbGFzcz0icm93IHJvdy1jb2xzLTEgcm93LWNvbHMtbWQtMiByb3ctY29scy1sZy0zIG1iLTUgdGV4dC1jZW50ZXIiPgogICAgICAgICAgICAgICAge3tyYW5nZSAuVGllcnN9fQogICAgICAgICAgICAgICAgICB7e2lmIG5vdCAoLnxpc0ZyZWUpfX0KICAgICAgICAgICAgICAgICAgICA8Zm9ybSBhY3Rpb249Jy91c2VyL3VwZGF0ZS9iaWxsaW5nJyBtZXRob2Q9UE9TVCBjbGFzcz0iY29sIj4KICAgICAgICAgICAgICAgICAgICAgIDxpbnB1dCB0eXBlPWhpZGRlbiBuYW1lPXRpZXIgdmFsdWU9Int7Lk5hbWV9fSIgLz4KICAgICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQgbWItNCBzaGFkb3ctc20iPgogICAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZC1oZWFkZXIiPgogICAgICAgICAgICAgICAgICAgICAgICA8aDQgY2xhc3M9Im15LTAgZm9udC13ZWlnaHQtbm9ybWFsIj57ey5OYW1lfX08L2g0PgogICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8aDEgY2xhc3M9ImNhcmQtdGl0bGUgcHJpY2luZy1jYXJkLXRpdGxlIj57ey5QcmljZX19IDxzbWFsbCBjbGFzcz0idGV4dC1tdXRlZCI+LyB7ey5UaW1lVW5pdH19PC9zbWFsbD48L2gxPgogICAgICAgICAgICAgICAgICAgICAgICA8dWwgY2xhc3M9Imxpc3QtdW5zdHlsZWQgbXQtMyBtYi00Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICB7e3JhbmdlIC5PcHRzfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxsaT57ey5UZXh0fX08L2xpPgogICAgICAgICAgICAgICAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgICAgICAgICAgICAgICAgPC91bD4KICAgICAgICAgICAgICAgICAgICAgICAgPGJ1dHRvbiB0eXBlPSJzdWJtaXQiIGNsYXNzPSJidG4gYnRuLXByaW1hcnkgdy0xMDAiPkdvPC9idXR0b24+CiAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICAgIDwvZGl2PgogICAge3tlbHNlfX0KICAgICAgPGRpdiBjbGFzcz0nY29udGFpbmVyJz4KICAgICAgICA8ZGl2IGNsYXNzPSdyb3cnPgogICAgICAgICAgPGRpdiBjbGFzcz0iY29sLTEyIj4KICAgICAgICAgICAgPGgxPk9vcHM8L2gxPgogICAgICAgICAgICA8cD5Tb3JyeSwgb3VyIGRldmVsb3BlcnMgYXJlIGxhenkuIFRoaXMgc2hvdWxkIHJlYWxseSByZWRpcmVjdCB5b3UuPC9wPgogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICAgIDwvZGl2PgogICAge3tlbmR9fQogICAge3sgdGVtcGxhdGUgImh0bWwvX2Zvb3Rlci5odG1sIiAkIH19CiAgPC9tYWluPgogIHt7IHRlbXBsYXRlICJodG1sL19zY3JpcHRzLmh0bWwiIH19CjwvYm9keT4KPC9odG1sPgo=")
	tmpls["html/billing.html"] = tostring("PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ZW4+CjxoZWFkPgogIHt7IHRlbXBsYXRlICJodG1sL19oZWFkLmh0bWwiIH19CiAgPHRpdGxlPlNraXBwZXIgQ01TIHwgQmlsbGluZzwvdGl0bGU+CjwvaGVhZD4KPGJvZHkgY2xhc3M9J3BhZ2UgYmctbGlnaHQnPgogIDxzdHlsZT57eyB0ZW1wbGF0ZSAiY3NzL21haW4uY3NzIiB9fTwvc3R5bGU+CiAgPG1haW4+CiAgICB7eyB0ZW1wbGF0ZSAiaHRtbC9faGVhZGVyLmh0bWwiICQgfX0KICAgIDxkaXYgY2xhc3M9InByaWNpbmctaGVhZGVyIHB4LTMgcHktMyBwdC1tZC01IHBiLW1kLTQgbXgtYXV0byB0ZXh0LWNlbnRlciI+CiAgICAgIDxoMSBjbGFzcz0iZGlzcGxheS00Ij5CaWxsaW5nPC9oMT4KICAgIDwvZGl2PgogICAge3tpZiAuVXNlcn19CiAgICAgIDxkaXYgY2xhc3M9J2NvbnRhaW5lcic+CiAgICAgICAgPGRpdiBjbGFzcz0ncm93Jz4KICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbC0xMiBjb2wtbWQtNiBvZmZzZXQtbWQtMyI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9J3RleHQtY2VudGVyJz4KICAgICAgICAgICAgICB7e2lmIC5Vc2VyLkhhc0VtYWlsfX0KICAgICAgICAgICAgICA8cD5VcGRhdGUgeW91ciBlbWFpbC48L3A+CiAgICAgICAgICAgICAge3tlbHNlfX0KICAgICAgICAgICAgICA8cD5TZXQgeW91ciBlbWFpbCBpbiBjYXNlIHlvdSBnZXQgbG9ja2VkIG91dCBvZiB5b3VyIGFjY291bnQuPC9wPgogICAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgIDxmb3JtIGFjdGlvbj0nL3VzZXIvdXBkYXRlL2VtYWlsJyBtZXRob2Q9UE9TVCBjbGFzcz0nbWItNSc+CiAgICAgICAgICAgICAgPGxhYmVsIGZvcj1lbWFpbD5FbWFpbDwvbGFiZWw+CiAgICAgICAgICAgICAgPGlucHV0IGlkPWVtYWlsIG5hbWU9ZW1haWwgdHlwZT1lbWFpbCBjbGFzcz0ibWItMyBmb3JtLWNvbnRyb2wiIHBsYWNlaG9sZGVyPSJlbWFpbCIgcmVxdWlyZWQge3tpZiAuVXNlci5IYXNFbWFpbH19dmFsdWU9Int7LlVzZXIuRW1haWx9fSJ7e2VuZH19PgogICAgICAgICAgICAgIDxidXR0b24gdHlwZT0ic3VibWl0IiBjbGFzcz0iYnRuIGJ0bi1wcmltYXJ5Ij5HbzwvYnV0dG9uPgogICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9J3RleHQtY2VudGVyJz4KICAgICAgICAgICAgICA8cD5VcGRhdGUgeW91ciBwYXNzd29yZC48L3A+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8Zm9ybSBhY3Rpb249Jy91c2VyL3VwZGF0ZS9wYXNzd29yZCcgbWV0aG9kPVBPU1QgY2xhc3M9J21iLTUnPgogICAgICAgICAgICAgIDxsYWJlbCBmb3I9Y3VycmVudCA+Q3VycmVudCBQYXNzd29yZDwvbGFiZWw+CiAgICAgICAgICAgICAgPGlucHV0IGlkPWN1cnJlbnQgbmFtZT1jdXJyZW50IHR5cGU9cGFzc3dvcmQgY2xhc3M9Im1iLTMgZm9ybS1jb250cm9sIiBwbGFjZWhvbGRlcj0iY3VycmVudCIgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgPGxhYmVsIGZvcj1wYXNzd29yZD5OZXcgUGFzc3dvcmQ8L2xhYmVsPgogICAgICAgICAgICAgIDxpbnB1dCBpZD1wYXNzd29yZCBuYW1lPXBhc3N3b3JkIHR5cGU9cGFzc3dvcmQgY2xhc3M9Im1iLTMgZm9ybS1jb250cm9sIiBwbGFjZWhvbGRlcj0icGFzc3dvcmQiIHJlcXVpcmVkPgogICAgICAgICAgICAgIDxsYWJlbCBmb3I9dmVyaWZ5PlZlcmlmeTwvbGFiZWw+CiAgICAgICAgICAgICAgPGlucHV0IGlkPXZlcmlmeSBuYW1lPXZlcmlmeSB0eXBlPXBhc3N3b3JkIGNsYXNzPSJtYi0zIGZvcm0tY29udHJvbCIgcGxhY2Vob2xkZXI9InZlcmlmeSIgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgPGJ1dHRvbiB0eXBlPSJzdWJtaXQiIGNsYXNzPSJidG4gYnRuLXByaW1hcnkiPkdvPC9idXR0b24+CiAgICAgICAgICAgIDwvZm9ybT4KICAgICAgICAgICAge3tpZiAuVXNlciB8IHBhaWR9fQogICAgICAgICAgICAgIDxmb3JtIGFjdGlvbj0nL3VzZXIvY2FuY2VsL2JpbGxpbmcnIG1ldGhvZD1QT1NUIGNsYXNzPSJ0ZXh0LWNlbnRlciI+CiAgICAgICAgICAgICAgICA8cCBjbGFzcz0nbWItNSc+Q2FuY2VsIHlvdXIge3suVXNlci5PcmcuVGllci5OYW1lfX0gc3Vic2NyaXB0aW9uLjwvcD4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImQtaW5saW5lLWJsb2NrIj4KICAgICAgICAgICAgICAgICAgPGlucHV0IHR5cGU9aGlkZGVuIG5hbWU9dGllciB2YWx1ZT0ie3suVXNlci5PcmcuVGllci5OYW1lfX0iIC8+CiAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQgbWItNCBzaGFkb3ctc20iPgogICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgPGg0IGNsYXNzPSJteS0wIGZvbnQtd2VpZ2h0LW5vcm1hbCI+e3suVXNlci5PcmcuVGllci5OYW1lfX08L2g0PgogICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZC1ib2R5Ij4KICAgICAgICAgICAgICAgICAgICA8aDEgY2xhc3M9ImNhcmQtdGl0bGUgcHJpY2luZy1jYXJkLXRpdGxlIj57ey5Vc2VyLk9yZy5UaWVyLlByaWNlfX0gPHNtYWxsIGNsYXNzPSJ0ZXh0LW11dGVkIj4vIHt7LlVzZXIuT3JnLlRpZXIuVGltZVVuaXR9fTwvc21hbGw+PC9oMT4KICAgICAgICAgICAgICAgICAgICA8dWwgY2xhc3M9Imxpc3QtdW5zdHlsZWQgbXQtMyBtYi00Ij4KICAgICAgICAgICAgICAgICAgICAgIHt7cmFuZ2UgLlVzZXIuT3JnLlRpZXIuT3B0c319CiAgICAgICAgICAgICAgICAgICAgICAgIDxsaT57ey5UZXh0fX08L2xpPgogICAgICAgICAgICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICAgICAgICAgIDwvdWw+CiAgICAgICAgICAgICAgICAgICAgPGJ1dHRvbiB0eXBlPSJzdWJtaXQiIGNsYXNzPSJidG4gYnRuLXByaW1hcnkgdy0xMDAiPkNhbmNlbDwvYnV0dG9uPgogICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgIDwvZm9ybT4KICAgICAgICAgICAge3tlbHNlfX0KICAgICAgICAgICAgICA8cCBjbGFzcz0ndGV4dC1jZW50ZXIgbWItNSc+VXBncmFkZSB0byBhIHBhaWQgdGllci48YnI+R2V0IG1vcmUgYWNjZXNzLjwvcD4KICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJyb3cgcm93LWNvbHMtMSByb3ctY29scy1tZC0yIHJvdy1jb2xzLWxnLTIgbWItNSB0ZXh0LWNlbnRlciI+CiAgICAgICAgICAgICAgICB7e3JhbmdlIC5UaWVyc319CiAgICAgICAgICAgICAgICAgIHt7aWYgbm90ICgufGlzRnJlZSl9fQogICAgICAgICAgICAgICAgICAgIDxmb3JtIGFjdGlvbj0nL3VzZXIvdXBkYXRlL2JpbGxpbmcnIG1ldGhvZD1QT1NUIGNsYXNzPSJjb2wiPgogICAgICAgICAgICAgICAgICAgICAgPGlucHV0IHR5cGU9aGlkZGVuIG5hbWU9dGllciB2YWx1ZT0ie3suTmFtZX19IiAvPgogICAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZCBtYi00IHNoYWRvdy1zbSI+CiAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxoNCBjbGFzcz0ibXktMCBmb250LXdlaWdodC1ub3JtYWwiPnt7Lk5hbWV9fTwvaDQ+CiAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtYm9keSI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxoMSBjbGFzcz0iY2FyZC10aXRsZSBwcmljaW5nLWNhcmQtdGl0bGUiPnt7LlByaWNlfX0gPHNtYWxsIGNsYXNzPSJ0ZXh0LW11dGVkIj4vIHt7LlRpbWVVbml0fX08L3NtYWxsPjwvaDE+CiAgICAgICAgICAgICAgICAgICAgICAgIDx1bCBjbGFzcz0ibGlzdC11bnN0eWxlZCBtdC0zIG1iLTQiPgogICAgICAgICAgICAgICAgICAgICAgICAgIHt7cmFuZ2UgLk9wdHN9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgPGxpPnt7LlRleHR9fTwvbGk+CiAgICAgICAgICAgICAgICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICAgICAgICAgICAgICA8L3VsPgogICAgICAgICAgICAgICAgICAgICAgICA8YnV0dG9uIHR5cGU9InN1Ym1pdCIgY2xhc3M9ImJ0biBidG4tcHJpbWFyeSB3LTEwMCI+R288L2J1dHRvbj4KICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgIDwvZm9ybT4KICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICB7e2VuZH19CiAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CiAgICB7e2Vsc2V9fQogICAgICA8ZGl2IGNsYXNzPSdjb250YWluZXInPgogICAgICAgIDxkaXYgY2xhc3M9J3Jvdyc+CiAgICAgICAgICA8ZGl2IGNsYXNzPSJjb2wtMTIiPgogICAgICAgICAgICA8aDE+T29wczwvaDE+CiAgICAgICAgICAgIDxwPlNvcnJ5LCBvdXIgZGV2ZWxvcGVycyBhcmUgbGF6eS4gVGhpcyBzaG91bGQgcmVhbGx5IHJlZGlyZWN0IHlvdS48L3A+CiAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CiAgICB7e2VuZH19CiAgICB7eyB0ZW1wbGF0ZSAiaHRtbC9fZm9vdGVyLmh0bWwiICQgfX0KICA8L21haW4+CiAge3sgdGVtcGxhdGUgImh0bWwvX3NjcmlwdHMuaHRtbCIgfX0KPC9ib2R5Pgo8L2h0bWw+Cg==")

	tmpls["html/contact.html"] = tostring("PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ZW4+CjxoZWFkPgogIHt7IHRlbXBsYXRlICJodG1sL19oZWFkLmh0bWwiIH19CiAgPHRpdGxlPlNraXBwZXIgQ01TIHwgQ29udGFjdDwvdGl0bGU+CjwvaGVhZD4KPGJvZHkgY2xhc3M9J3BhZ2UgYmctbGlnaHQnPgogIDxzdHlsZT57eyB0ZW1wbGF0ZSAiY3NzL21haW4uY3NzIiB9fTwvc3R5bGU+CiAgPG1haW4+CiAgICB7eyB0ZW1wbGF0ZSAiaHRtbC9faGVhZGVyLmh0bWwiICQgfX0KICAgIDxkaXYgY2xhc3M9InByaWNpbmctaGVhZGVyIHB4LTMgcHktMyBwdC1tZC01IHBiLW1kLTQgbXgtYXV0byB0ZXh0LWNlbnRlciI+CiAgICAgIDxoMSBjbGFzcz0iZGlzcGxheS00Ij5Db250YWN0PC9oMT4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0nY29udGFpbmVyJz4KICAgICAgPGRpdiBjbGFzcz0ncm93Jz4KICAgICAgICA8ZGl2IGNsYXNzPSJjb2wtMTIgb2Zmc2V0LTAgY29sLWxnLTggb2Zmc2V0LWxnLTIiPgogICAgICAgICAgSGkuIE15IG5hbWUgaXMgRXZhbi4gSSdtIHRoZSBvbmx5IHNvdWwgY3VycmVudGx5IHdvcmtpbmcgb24gU2tpcHBlcgogICAgICAgICAgQ01TLiBJZiB5b3UgbmVlZCBzb21lb25lIHRvIGNvbnRhY3QgSSdtIHRoYXQgc29tZW9uZS4gWW91IGNhbiByZWFjaCBtZQogICAgICAgICAgYXQgbWUgQVQgZXZhbmpvbiBET1QgZXMgb3IgdmlhIFR3aXR0ZXIsIAogICAgICAgICAgPGEgaHJlZj0naHR0cHM6Ly90d2l0dGVyLmNvbS9taW5pZWdnczQwJz5AbWluaWVnZ3M0MDwvYT4uIENhb2khCiAgICAgICAgPC9kaXY+CiAgICAgIDwvZGl2PgogICAgPC9kaXY+CiAgICB7eyB0ZW1wbGF0ZSAiaHRtbC9fZm9vdGVyLmh0bWwiICQgfX0KICA8L21haW4+CiAge3sgdGVtcGxhdGUgImh0bWwvX3NjcmlwdHMuaHRtbCIgfX0KPC9ib2R5Pgo8L2h0bWw+Cg==")



@@ 44,6 44,8 @@ func init() {

	tmpls["html/index.html"] = tostring("PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ZW4+CjxoZWFkPgogIHt7IHRlbXBsYXRlICJodG1sL19oZWFkLmh0bWwiIH19CiAgPHRpdGxlPlNraXBwZXIgQ01TPC90aXRsZT4KPC9oZWFkPgo8Ym9keSBjbGFzcz0naW5kZXggYmctbGlnaHQnPgogIDxzdHlsZT57eyB0ZW1wbGF0ZSAiY3NzL21haW4uY3NzIiB9fTwvc3R5bGU+CiAgPG1haW4+CiAgICB7eyB0ZW1wbGF0ZSAiaHRtbC9faGVhZGVyLmh0bWwiICQgfX0KICAgIHt7aWYgbm90IC5Vc2VyfX0KICAgIDxkaXYgY2xhc3M9J2NvbnRhaW5lciBteS01Jz4KICAgICAgPGRpdiBjbGFzcz0ncm93Jz4KICAgICAgICA8ZGl2IGNsYXNzPSJjb2wtMTIgdGV4dC1jZW50ZXIiPgogICAgICAgICAgPGRpdiBjbGFzcz0iZC1pbmxpbmUtYmxvY2sgYWxlcnQgYWxlcnQtd2FybmluZyIgcm9sZT0iYWxlcnQiPgogICAgICAgICAgICA8c3Ryb25nPldBUk5JTkc6PC9zdHJvbmc+IFRoaXMgc2l0ZSBpcyBpbiA8c3Ryb25nPkFMUEhBPC9zdHJvbmc+IAogICAgICAgICAgICBhbmQgaXMgbm90IHlldCBnZW5lcmFsbHkgYXZhaWxhYmxlLgogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICAgIDwvZGl2PgogICAgPC9kaXY+CiAgICB7e2VuZH19CiAgICA8ZGl2IGNsYXNzPSJwcmljaW5nLWhlYWRlciBweC0zIHB5LTUgcHQtbWQtNSBwYi1tZC00IG14LWF1dG8gdGV4dC1jZW50ZXIiPgogICAgICA8aW1nIHdpZHRoPTIwMCBoZWlnaHQ9MjAwIHNyYz0nL3N0YXRpYy9pbWcvbG9nby1ibGFjay5zdmcnIC8+CiAgICAgIDxoMSBjbGFzcz0iZGlzcGxheS0xIj5Ta2lwcGVyIENNUzwvaDE+CiAgICAgIDxwIGNsYXNzPSJsZWFkIj5BbiBvbGQtc2Nob29sIGNvbnRlbnQgbWFuYWdlbWVudCBzeXN0ZW0gZm9yIG1vc3QuPC9wPgogICAgPC9kaXY+CiAgICB7e2lmIG5vdCAuVXNlcn19CiAgICA8ZGl2IGNsYXNzPSdjb250YWluZXIgbXktNSc+CiAgICAgIDxkaXYgY2xhc3M9J3Jvdyc+CiAgICAgICAgPGRpdiBjbGFzcz0nY29sIGNvbC0xMic+CiAgICAgICAgICA8aDIgY2xhc3M9J2Rpc3BsYXktNCB0ZXh0LWNlbnRlcic+Q29udGVudCBNYW5hZ2VtZW50IFN5c3RlbSAoQ01TKTwvaDI+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0nY29sIG9mZnNldC1sZy0yIGNvbC1sZy04Jz4KICAgICAgICAgIDx0YWJsZT4KICAgICAgICAgICAgPHRyIHZhbGlnbj10b3A+CiAgICAgICAgICAgICAgPHRkPgogICAgICAgICAgICAgICAgPHAgY2xhc3M9J21yLTMnPjxzdHJvbmc+Tm91bi48L3N0cm9uZz48L3A+CiAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICA8dGQ+CiAgICAgICAgICAgICAgICA8cD5BIGNvbXB1dGVyIHNvZnR3YXJlIHN5c3RlbSBmb3Igb3JnYW5pemluZyBhbmQgZmFjaWxpdGF0aW5nIGNvbGxhYm9yYXRpdmUgY3JlYXRpb24gb2YgZG9jdW1lbnRzIGFuZCBvdGhlciBjb250ZW50LCBlc3BlY2lhbGx5IGZvciBkaXNwbGF5aW5nIGNvbnRlbnQgdG8gYSB3ZWJzaXRlIG9yIG1vYmlsZSBhcHBsaWNhdGlvbi48L3A+CiAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgPC90cj4KICAgICAgICAgIDwvdGFibGU+CiAgICAgICAgPC9kaXY+CiAgICAgIDwvZGl2PgogICAgPC9kaXY+CiAgICA8ZGl2IGNsYXNzPSdjb250YWluZXIgbXktNSc+CiAgICAgIDxkaXYgY2xhc3M9InJvdyI+CiAgICAgICAgPGRpdiBjbGFzcz0iY29sIj4KICAgICAgICAgIDxoMSBjbGFzcz0iZGlzcGxheS00IHRleHQtY2VudGVyIG1iLTUiPkZlYXR1cmVzPC9oMT4KICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CiAgICAgIDxkaXYgY2xhc3M9cm93PgoKICAgICAgICA8ZGl2IGNsYXNzPSdjb2wgY29sLTEyIGNvbC1tZC02Jz4KICAgICAgICAgIDxkaXYgY2xhc3M9J2NhcmQgbWItMyc+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9J2NhcmQtYm9keSc+CiAgICAgICAgICAgICAgPGRpdiBjbGFzcz0nY2FyZC10ZXh0Jz4KICAgICAgICAgICAgICAgIDxhIGhyZWY9J2h0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL1dlYl9BUEknPkFQSTwvYT4KICAgICAgICAgICAgICAgIGZpcnN0IENNUy4gU2tpcHBlciBpcyBhIGZ1bGx5LWZsZWRnZWQgY29udGVudCAKICAgICAgICAgICAgICAgIG1hbmFnZW1lbnQgPG1hcms+aW5mcmFzdHJ1Y3R1cmU8L21hcms+IGFzIG11Y2ggYXMgaXQgaXMgYQogICAgICAgICAgICAgICAgY29udGVudCBtYW5hZ2VtZW50IHN5c3RlbS4gCiAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CgogICAgICAgIDxkaXYgY2xhc3M9J2NvbCBjb2wtMTIgY29sLW1kLTYnPgogICAgICAgICAgPGRpdiBjbGFzcz0nY2FyZCBtYi0zJz4KICAgICAgICAgICAgPGRpdiBjbGFzcz0nY2FyZC1ib2R5Jz4KICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSdjYXJkLXRleHQnPgogICAgICAgICAgICAgICAgWW91ciBjb250ZW50IG1vZGVsIHlvdXIgd2F5LiBZb3UncmUgPG1hcms+bmV2ZXIKICAgICAgICAgICAgICAgIHJlc3RyaWN0ZWQ8L21hcms+IHRvIGEKICAgICAgICAgICAgICAgIGJsZXNzZWQgY29udGVudCBtb2RlbCwgYSBsYSBjYXRlZ29yeS90YWdzIGZvciBwYWdlcy9wb3N0cy4gCiAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CgogICAgICAgIDxkaXYgY2xhc3M9J2NvbCBjb2wtMTIgY29sLW1kLTYnPgogICAgICAgICAgPGRpdiBjbGFzcz0nY2FyZCBtYi0zJz4KICAgICAgICAgICAgPGRpdiBjbGFzcz0nY2FyZC1ib2R5Jz4KICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSdjYXJkLXRleHQnPgogICAgICAgICAgICAgICAgPG1hcms+TGlnaHR3ZWlnaHQ8L21hcms+IGFuZCA8bWFyaz5mYXN0PC9tYXJrPjsgQVBJIGNhbGxzIHRvCiAgICAgICAgICAgICAgICBTa2lwcGVyIENNUyB3aWxsIG5vdCBiZSB5b3VyIGJvdHRsZW5lY2suIFNraXBwZXIgQ01TIG1ha2VzIGhlYXZ5CiAgICAgICAgICAgICAgICB1c2Ugb2YgY2FjaGluZy4KICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KCiAgICAgICAgPGRpdiBjbGFzcz0nY29sIGNvbC0xMiBjb2wtbWQtNic+CiAgICAgICAgICA8ZGl2IGNsYXNzPSdjYXJkIG1iLTMnPgogICAgICAgICAgICA8ZGl2IGNsYXNzPSdjYXJkLWJvZHknPgogICAgICAgICAgICAgIDxkaXYgY2xhc3M9J2NhcmQtdGV4dCc+CiAgICAgICAgICAgICAgICAxMDAlIGNvbW1pdHRlZCB0byA8bWFyaz5vcGVuIHNvdXJjZTwvbWFyaz47IHlvdSBjYW4gc2VlIGV4YWN0bHkgd2hhdCB0aGUgY29kZQogICAgICAgICAgICAgICAgZG9lcyBhbmQgbWFrZSBpbXByb3ZlbWVudHMuIFNlZSAKICAgICAgICAgICAgICAgIDxhIGhyZWY9J2h0dHBzOi8vZ2l0LnNyLmh0L35ldmFuai9jbXMnPnNvdXJjZWh1dDwvYT4uCiAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CgogICAgICAgIDxkaXYgY2xhc3M9J2NvbCBjb2wtMTIgY29sLW1kLTYnPgogICAgICAgICAgPGRpdiBjbGFzcz0nY2FyZCBtYi0zJz4KICAgICAgICAgICAgPGRpdiBjbGFzcz0nY2FyZC1ib2R5Jz4KICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSdjYXJkLXRleHQnPgogICAgICAgICAgICAgICAgPG1hcms+RWFzeTwvbWFyaz4gdG8gdXNlIGZvciA8bWFyaz5hbGw8L21hcms+LiBXZSBrZWVwIGEgYmlnCiAgICAgICAgICAgICAgICB0ZW50LiBObyBtYXR0ZXIgeW91ciBiYWNrZ3JvdW5kLCBTa2lwcGVyIENNUyBpcyBjb21taXR0ZWQgdG8KICAgICAgICAgICAgICAgIGFzc2lzdGluZyB5b3Ugd2VsbC4KICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KCiAgICAgICAgPGRpdiBjbGFzcz0nY29sIGNvbC0xMiBjb2wtbWQtNic+CiAgICAgICAgICA8ZGl2IGNsYXNzPSdjYXJkIG1iLTMnPgogICAgICAgICAgICA8ZGl2IGNsYXNzPSdjYXJkLWJvZHknPgogICAgICAgICAgICAgIDxkaXYgY2xhc3M9J2NhcmQtdGV4dCc+CiAgICAgICAgICAgICAgICA8bWFyaz5RdWlja2x5PC9tYXJrPiBzZXR1cCBzdGFnaW5nIGFuZCB0ZXN0IGVudmlyb25tZW50cyBmb3IKICAgICAgICAgICAgICAgIHlvdXIgZGF0YS4gVGFpbG9yZWQgdXNlIGZvciA8bWFyaz55b3VyIGVudmlyb25tZW50czwvbWFyaz4uCiAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CgogICAgICAgIDxkaXYgY2xhc3M9J2NvbCBjb2wtMTIgY29sLW1kLTYnPgogICAgICAgICAgPGRpdiBjbGFzcz0nY2FyZCBtYi0zJz4KICAgICAgICAgICAgPGRpdiBjbGFzcz0nY2FyZC1ib2R5Jz4KICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSdjYXJkLXRleHQnPgogICAgICAgICAgICAgICAgPG1hcms+QXBwcm9hY2hhYmxlPC9tYXJrPiAKICAgICAgICAgICAgICAgIGRvY3VtZW50aW9uLiBFeGFtcGxlcyBwcm92aWRlZCBpbiAKICAgICAgICAgICAgICAgIDxhIGhyZWY9J2h0dHBzOi8vbGludXguZGllLm5ldC9tYW4vMS9jdXJsJz5jVVJMPC9hPi4KICAgICAgICAgICAgICAgIFVzZSBmcm9tIHlvdXIgcHJvZ3JhbW1pbmcgbGFuZ3VhZ2Ugb3IgcnVudGltZSBvZiBjaG9pY2UuCiAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CgogICAgICAgIDxkaXYgY2xhc3M9J2NvbCBjb2wtMTIgY29sLW1kLTYnPgogICAgICAgICAgPGRpdiBjbGFzcz0nY2FyZCBtYi0zJz4KICAgICAgICAgICAgPGRpdiBjbGFzcz0nY2FyZC1ib2R5Jz4KICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSdjYXJkLXRleHQnPgogICAgICAgICAgICAgICAgPG1hcms+RnJlZWRvbTwvbWFyaz4gcmVzcGVjdGluZyBsaWNlbnNlLiBUaGUgCiAgICAgICAgICAgICAgICA8YSBocmVmPSdodHRwOi8vd3d3LmdudS5vcmcvcGhpbG9zb3BoeS9mcmVlLXN3Lmh0bWwjY29udGVudCc+Zm91ciBlc3NlbnRpYWwgZnJlZWRvbXM8L2E+IAogICAgICAgICAgICAgICAgYXJlIHVwaGVsZCB1bmRlciB0aGUgRVVQTCB2MS4yIGxpY2Vuc2UuIENvbXBhdGlibGUgd2l0aCBBR1BMIHYzLgogICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvZGl2PgoKICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICAgIDxkaXYgY2xhc3M9J2NvbnRhaW5lciBteS01Jz4KICAgICAgPGRpdiBpZD0ncHJpY2luZycgY2xhc3M9InJvdyI+CiAgICAgICAgPGRpdiBjbGFzcz0iY29sIj4KICAgICAgICAgIDxoMSBjbGFzcz0iZGlzcGxheS00IHRleHQtY2VudGVyIG1iLTUiPlByaWNpbmc8L2gxPgogICAgICAgIDwvZGl2PgogICAgICA8L2Rpdj4KICAgICAgPGRpdiBjbGFzcz0icm93IHJvdy1jb2xzLTEgcm93LWNvbHMtbWQtMiByb3ctY29scy1sZy0zIG1iLTUgdGV4dC1jZW50ZXIiPgogICAgICAgIHt7cmFuZ2UgLlRpZXJzfX0KICAgICAgICA8ZGl2IGNsYXNzPSJjb2wiPgogICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZCBtYi00IHNoYWRvdy1zbSI+CiAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgIDxoNCBjbGFzcz0ibXktMCBmb250LXdlaWdodC1ub3JtYWwiPnt7Lk5hbWV9fTwvaDQ+CiAgICAgICAgICA8L2Rpdj4KICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtYm9keSI+CiAgICAgICAgICAgIDxoMSBjbGFzcz0iY2FyZC10aXRsZSBwcmljaW5nLWNhcmQtdGl0bGUiPnt7LlByaWNlfX0gPHNtYWxsIGNsYXNzPSJ0ZXh0LW11dGVkIj4vIHt7LlRpbWVVbml0fX08L3NtYWxsPjwvaDE+CiAgICAgICAgICAgIDx1bCBjbGFzcz0ibGlzdC11bnN0eWxlZCBtdC0zIG1iLTQiPgogICAgICAgICAgICAgIHt7cmFuZ2UgLk9wdHN9fQogICAgICAgICAgICAgICAgPGxpPnt7LlRleHR9fTwvbGk+CiAgICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICA8L3VsPgogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICAgICAge3tlbmR9fQogICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAge3tlbmR9fQogICAgPGFydGljbGU+CiAgICAgIHt7IGlmIC5Vc2VyIH19CiAgICAgICAgPGZvcm0gbWV0aG9kPVBPU1QgYWN0aW9uPScvc3BhY2UnIGVuY3R5cGU9J211bHRpcGFydC9mb3JtLWRhdGEnPgogICAgICAgICAgPGlucHV0IHR5cGU9aGlkZGVuIG5hbWU9bWV0aG9kIHZhbHVlPVBPU1QgLz4KICAgICAgICAgIDxkaXYgY2xhc3M9Im1vZGFsIGZhZGUiIGlkPSJleGFtcGxlTW9kYWwiIHRhYmluZGV4PSItMSIgcm9sZT0iZGlhbG9nIiBhcmlhLWxhYmVsbGVkYnk9ImV4YW1wbGVNb2RhbExhYmVsIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9Im1vZGFsLWRpYWxvZyBtb2RhbC1kaWFsb2ctc2Nyb2xsYWJsZSI+CiAgICAgICAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtY29udGVudCI+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJtb2RhbC1oZWFkZXIiPgogICAgICAgICAgICAgICAgICA8aDUgY2xhc3M9Im1vZGFsLXRpdGxlIiBpZD0iZXhhbXBsZU1vZGFsTGFiZWwiPkNyZWF0ZSBhIG5ldyBzcGFjZTwvaDU+CiAgICAgICAgICAgICAgICAgIDxidXR0b24gdHlwZT0iYnV0dG9uIiBjbGFzcz0iY2xvc2UiIGRhdGEtZGlzbWlzcz0ibW9kYWwiIGFyaWEtbGFiZWw9IkNsb3NlIj4KICAgICAgICAgICAgICAgICAgICA8c3BhbiBhcmlhLWhpZGRlbj0idHJ1ZSI+JnRpbWVzOzwvc3Bhbj4KICAgICAgICAgICAgICAgICAgPC9idXR0b24+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9Im1vZGFsLWJvZHkiPgogICAgICAgICAgICAgICAgICA8bGFiZWwgZm9yPSJzcGFjZU5hbWUiPk5hbWU8L2xhYmVsPgogICAgICAgICAgICAgICAgICA8aW5wdXQgbmFtZT1uYW1lIHR5cGU9dGV4dCBpZD0ic3BhY2VOYW1lIiBjbGFzcz0ibWItMyBmb3JtLWNvbnRyb2wiIHBsYWNlaG9sZGVyPSJOYW1lIiByZXF1aXJlZD4KICAgICAgICAgICAgICAgICAgPGxhYmVsIGZvcj0ic3BhY2VEZXNjIj5EZXNjcmlwdGlvbjwvbGFiZWw+CiAgICAgICAgICAgICAgICAgIDxpbnB1dCBuYW1lPWRlc2MgdHlwZT10ZXh0IGlkPSJzcGFjZURlc2MiIGNsYXNzPSJtYi0zIGZvcm0tY29udHJvbCIgcGxhY2Vob2xkZXI9IkRlc2NyaXB0aW9uIiByZXF1aXJlZD4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0ibW9kYWwtZm9vdGVyIj4KICAgICAgICAgICAgICAgICAgPGJ1dHRvbiB0eXBlPSJidXR0b24iIGNsYXNzPSJidG4gYnRuLXNlY29uZGFyeSIgZGF0YS1kaXNtaXNzPSJtb2RhbCI+Q2xvc2U8L2J1dHRvbj4KICAgICAgICAgICAgICAgICAgPGJ1dHRvbiB0eXBlPSJzdWJtaXQiIGNsYXNzPSJidG4gYnRuLXByaW1hcnkiPkdvPC9idXR0b24+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Zvcm0+CiAgICAgICAgPGRpdiBjbGFzcz0iY29udGFpbmVyIj4KICAgICAgICAgIDxkaXYgY2xhc3M9J3Jvdyc+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9J29mZnNldC1sZy0zIGNvbC1sZy02Jz4KICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJteS0zIHAtMyBiZy13aGl0ZSByb3VuZGVkIHNoYWRvdy1zbSI+CiAgICAgICAgICAgICAgICAgIDxzbWFsbCBjbGFzcz0iZC1ibG9jayB0ZXh0LXJpZ2h0IGZsb2F0LXJpZ2h0IiBkYXRhLXRvZ2dsZT0ibW9kYWwiIGRhdGEtdGFyZ2V0PSIjZXhhbXBsZU1vZGFsIj4KICAgICAgICAgICAgICAgICAgICA8YSBocmVmPSIjIj5DcmVhdGUgYSBuZXcgc3BhY2U8L2E+CiAgICAgICAgICAgICAgICAgIDwvc21hbGw+CiAgICAgICAgICAgICAgICA8aDYgY2xhc3M9ImJvcmRlci1ib3R0b20gYm9yZGVyLWdyYXkgcGItMiBtYi0wIj5Zb3VyIHNwYWNlczwvaDY+CiAgICAgICAgICAgICAgICB7eyBpZiAuU3BhY2VzLkxpc3QgfX0KICAgICAgICAgICAgICAgICAge3sgcmFuZ2UgLlNwYWNlcy5MaXN0IH19CiAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9Im1lZGlhIHRleHQtbXV0ZWQgcHQtMyI+CiAgICAgICAgICAgICAgICAgICAgPGEgaHJlZj0nL3NwYWNlL3t7IC5JRCB9fScgIGNsYXNzPSJkLWJsb2NrIG1lZGlhLWJvZHkgcGItMyBtYi0wIHNtYWxsIGxoLTEyNSBib3JkZXItYm90dG9tIGJvcmRlci1ncmF5Ij4KICAgICAgICAgICAgICAgICAgICAgIDxzdHJvbmcgY2xhc3M9ImQtYmxvY2sgdGV4dC1ncmF5LWRhcmsiPnt7IC5OYW1lIH19PC9zdHJvbmc+CiAgICAgICAgICAgICAgICAgICAgICB7eyAuRGVzYyB9fQogICAgICAgICAgICAgICAgICAgIDwvYT4KICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAgICAgICAgICB7eyBpZiAuU3BhY2VzLk1vcmUgfX0KICAgICAgICAgICAgICAgICAgPHNtYWxsIGNsYXNzPSJkLWJsb2NrIHRleHQtcmlnaHQgbXQtMyI+CiAgICAgICAgICAgICAgICAgICAgPGEgaHJlZj0iLz9iZWZvcmU9e3sgLlNwYWNlcy5CZWZvcmUgfX0iPkxvYWQgbW9yZTwvYT4KICAgICAgICAgICAgICAgICAgPC9zbWFsbD4KICAgICAgICAgICAgICAgICAge3sgZW5kIH19CiAgICAgICAgICAgICAgICB7eyBlbHNlIH19CiAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9Im10LTMgYWxlcnQgYWxlcnQtcHJpbWFyeSIgcm9sZT0iYWxlcnQiPgogICAgICAgICAgICAgICAgICAgIFlvdSBoYXZlbid0IGNyZWF0ZWQgYW55IHNwYWNlcyB5ZXQuIAogICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIHt7IGVuZCB9fQogICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvZGl2PgogICAgICB7eyBlbHNlIH19CiAgICAgICAgPGRpdiBjbGFzcz0iY29udGFpbmVyIG15LTUiPgogICAgICAgICAgPGRpdiBjbGFzcz0icm93Ij4KICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sIj4KICAgICAgICAgICAgICA8aDEgY2xhc3M9ImRpc3BsYXktNCB0ZXh0LWNlbnRlciBtYi01Ij5MZXQncyBnbzwvaDE+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgPC9kaXY+CiAgICAgICAgICA8ZGl2IGNsYXNzPSdyb3cganVzdGlmeS1jb250ZW50LWNlbnRlcic+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbC0xMiBjb2wtbWQtNiBjb2wtbGctNCBvZmZzZXQtY29sLWxnLTIgY29sLXhsLTMgb2Zmc2V0LWNvbC14bC0zIGQtZmxleCI+CiAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZCBtYi00IHNoYWRvdy1zbSBmbGV4LWZpbGwiPgogICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZC1oZWFkZXIiPgogICAgICAgICAgICAgICAgICA8aDQgY2xhc3M9Im15LTAgZm9udC13ZWlnaHQtbm9ybWFsIj5TaWdudXA8L2g0PgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWJvZHkiPgogICAgICAgICAgICAgICAgICA8Zm9ybSBpZD0nc2lnbnVwJyBtZXRob2Q9UE9TVCBhY3Rpb249Jy91c2VyL3NpZ251cCcgZW5jdHlwZT0nbXVsdGlwYXJ0L2Zvcm0tZGF0YSc+CiAgICAgICAgICAgICAgICAgICAgPGxhYmVsIGZvcj0ic2lnbnVwSW5wdXRVc2VybmFtZSIgY2xhc3M9InNyLW9ubHkiPlVzZXJuYW1lPC9sYWJlbD4KICAgICAgICAgICAgICAgICAgICA8aW5wdXQgbmFtZT11c2VybmFtZSB0eXBlPSJ0ZXh0IiBpZD0ic2lnbnVwSW5wdXRVc2VybmFtZSIgY2xhc3M9Im1iLTMgZm9ybS1jb250cm9sIiBwbGFjZWhvbGRlcj0iVXNlcm5hbWUiIHJlcXVpcmVkPgogICAgICAgICAgICAgICAgICAgIDxsYWJlbCBmb3I9InNpZ251cElucHV0UGFzc3dvcmQiIGNsYXNzPSJzci1vbmx5Ij5QYXNzd29yZDwvbGFiZWw+CiAgICAgICAgICAgICAgICAgICAgPGlucHV0IG5hbWU9cGFzc3dvcmQgdHlwZT0icGFzc3dvcmQiIGlkPSJzaWdudXBJbnB1dFBhc3N3b3JkIiBjbGFzcz0ibWItMyBmb3JtLWNvbnRyb2wiIHBsYWNlaG9sZGVyPSJQYXNzd29yZCIgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgICAgICAgPGxhYmVsIGZvcj0ic2lnbnVwSW5wdXRWZXJpZnkiIGNsYXNzPSJzci1vbmx5Ij5Db25maXJtIFBhc3N3b3JkPC9sYWJlbD4KICAgICAgICAgICAgICAgICAgICA8aW5wdXQgbmFtZT12ZXJpZnkgdHlwZT0icGFzc3dvcmQiIGlkPSJzaWdudXBJbnB1dFZlcmlmeSIgY2xhc3M9Im1iLTMgZm9ybS1jb250cm9sIiBwbGFjZWhvbGRlcj0iQ29uZmlybSBQYXNzd29yZCIgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgICAgICAgPGxhYmVsIGZvcj0ic2lnbnVwSW5wdXRQbGFuIiBjbGFzcz0ic3Itb25seSI+U2VsZWN0IHRpZXI8L2xhYmVsPgogICAgICAgICAgICAgICAgICAgIDxzZWxlY3QgbmFtZT10aWVyIGlkPSJzaWdudXBJbnB1dFBsYW4iIGNsYXNzPSJ3LTEwMCBmb3JtLWNvbnRyb2wgbWItMyIgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgICAgICAgICA8b3B0aW9uIGRpc2FibGVkIHNlbGVjdGVkIHZhbHVlPlBheW1lbnQgdGllcjwvb3B0aW9uPgogICAgICAgICAgICAgICAgICAgICAge3tyYW5nZSAuVGllcnN9fQogICAgICAgICAgICAgICAgICAgICAgICA8b3B0aW9uIHZhbHVlPSJ7ey5OYW1lfX0iPnt7Lk5hbWV9fTwvb3B0aW9uPgogICAgICAgICAgICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICAgICAgICAgIDwvc2VsZWN0PgogICAgICAgICAgICAgICAgICAgIDxidXR0b24gY2xhc3M9ImJ0biBidG4tbGcgYnRuLXByaW1hcnkgYnRuLWJsb2NrIiB0eXBlPSJzdWJtaXQiPkdvPC9idXR0b24+CiAgICAgICAgICAgICAgICAgIDwvZm9ybT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sLTEyIGNvbC1tZC02IGNvbC1sZy00IGNvbC14bC0zIGQtZmxleCI+CiAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZCBtYi00IHNoYWRvdy1zbSBmbGV4LWZpbGwiPgogICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZC1oZWFkZXIiPgogICAgICAgICAgICAgICAgICA8aDQgY2xhc3M9Im15LTAgZm9udC13ZWlnaHQtbm9ybWFsIj5Mb2dpbjwvaDQ+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtYm9keSBkLWZsZXgiPgogICAgICAgICAgICAgICAgICA8Zm9ybSBpZD0nbG9naW4nIGNsYXNzPSdkLWZsZXggZmxleC1ncm93LTEgZmxleC1jb2x1bW4nIG1ldGhvZD1QT1NUIGFjdGlvbj0nL3VzZXIvbG9naW4nIGVuY3R5cGU9J211bHRpcGFydC9mb3JtLWRhdGEnPgogICAgICAgICAgICAgICAgICAgIDxsYWJlbCBmb3I9ImxvZ2luSW5wdXRVc2VybmFtZSIgY2xhc3M9InNyLW9ubHkiPlVzZXJuYW1lPC9sYWJlbD4KICAgICAgICAgICAgICAgICAgICA8aW5wdXQgbmFtZT11c2VybmFtZSB0eXBlPSJ0ZXh0IiBpZD0ibG9naW5JbnB1dFVzZXJuYW1lIiBjbGFzcz0ibWItMyBmb3JtLWNvbnRyb2wiIHBsYWNlaG9sZGVyPSJVc2VybmFtZSIgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgICAgICAgPGxhYmVsIGZvcj0ibG9naW5JbnB1dFBhc3N3b3JkIiBjbGFzcz0ic3Itb25seSI+UGFzc3dvcmQ8L2xhYmVsPgogICAgICAgICAgICAgICAgICAgIDxpbnB1dCBuYW1lPXBhc3N3b3JkIHR5cGU9InBhc3N3b3JkIiBpZD0ibG9naW5JbnB1dFBhc3N3b3JkIiBjbGFzcz0ibWItMyBmb3JtLWNvbnRyb2wiIHBsYWNlaG9sZGVyPSJQYXNzd29yZCIgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgICAgICAgPGJ1dHRvbiBjbGFzcz0ibXQtYXV0byBidG4gYnRuLWxnIGJ0bi1wcmltYXJ5IGJ0bi1ibG9jayIgdHlwZT0ic3VibWl0Ij5HbzwvYnV0dG9uPgogICAgICAgICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgICAge3sgZW5kIH19CiAgICA8L2FydGljbGU+CiAgICB7eyB0ZW1wbGF0ZSAiaHRtbC9fZm9vdGVyLmh0bWwiICQgfX0KICA8L21haW4+CiAge3sgdGVtcGxhdGUgImh0bWwvX3NjcmlwdHMuaHRtbCIgfX0KICB7eyBpZiAuVXNlciB9fQogICAgPHNjcmlwdD57eyB0ZW1wbGF0ZSAianMvbWFpbi5qcyIgJCB9fTwvc2NyaXB0PgogIHt7IGVsc2UgfX0KICAgIDxzY3JpcHQ+CiAgICAgIChmdW5jdGlvbigpIHsgCiAgICAgICAgd2luZG93LmFkZEV2ZW50TGlzdGVuZXIoJ0RPTUNvbnRlbnRMb2FkZWQnLCBoYW5kbGVIYXNoQ2xpY2spOwogICAgICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdoYXNoY2hhbmdlJywgaGFuZGxlSGFzaENsaWNrKTsKICAgICAgICBmdW5jdGlvbiBoYW5kbGVIYXNoQ2xpY2soKSB7IAogICAgICAgICAgdmFyIGVsID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQobG9jYXRpb24uaGFzaC5zdWJzdHIoMSkpOwogICAgICAgICAgaWYgKGVsKSBlbC5xdWVyeVNlbGVjdG9yKCdpbnB1dCcpLmZvY3VzKCk7CiAgICAgICAgfTsKICAgICAgfSkoKTsKICAgIDwvc2NyaXB0PgogIHt7IGVuZCB9fQo8L2JvZHk+CjwvaHRtbD4K")

	tmpls["html/invite.html"] = tostring("PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ZW4+CjxoZWFkPgogIHt7IHRlbXBsYXRlICJodG1sL19oZWFkLmh0bWwiIH19CiAgPHRpdGxlPlNraXBwZXIgQ01TIHwgSW52aXRlPC90aXRsZT4KPC9oZWFkPgo8Ym9keSBjbGFzcz0ncGFnZSBiZy1saWdodCc+CiAgPHN0eWxlPnt7IHRlbXBsYXRlICJjc3MvbWFpbi5jc3MiIH19PC9zdHlsZT4KICA8bWFpbj4KICAgIHt7IHRlbXBsYXRlICJodG1sL19oZWFkZXIuaHRtbCIgJCB9fQogICAge3tpZiAuVXNlcn19CiAgICA8ZGl2IGNsYXNzPSJwcmljaW5nLWhlYWRlciBweC0zIHB5LTMgcHQtbWQtNSBwYi1tZC00IG14LWF1dG8gdGV4dC1jZW50ZXIiPgogICAgICA8aDEgY2xhc3M9ImRpc3BsYXktNCI+SW52aXRlczwvaDE+CiAgICA8L2Rpdj4KICAgIDxkaXYgY2xhc3M9J2NvbnRhaW5lcic+CiAgICAgIDxkaXYgY2xhc3M9J3Jvdyc+CiAgICAgICAgPGRpdiBjbGFzcz0iY29sLTEyIGNvbC1tZC02IG9mZnNldC1tZC0zIGNvbC1sZy00IG9mZnNldC1sZy00Ij4KICAgICAgICAgIHt7aWYgLkludml0ZXN9fQogICAgICAgICAgICB7e3JhbmdlICRrZXksICR2YWwgOj0gLkludml0ZXN9fQogICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQgbWItNCBzaGFkb3ctc20gZmxleC1maWxsIj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtYm9keSI+CiAgICAgICAgICAgICAgICAgIDxkaXYgaWQ9J2NvcHkte3ska2V5fX0nIGNsYXNzPSd0ZXh0LXRydW5jYXRlIG1iLTMnPgogICAgICAgICAgICAgICAgICAgIGh0dHBzOi8vY21zLmV2YW5qb24uZXMvaW52aXRlL3t7JHZhbC5Ub2tlbn19CiAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICA8YnV0dG9uIGRhdGEtY2xpcGJvYXJkLXRhcmdldD0iI2NvcHkte3ska2V5fX0iIGNsYXNzPSJidG4gYnRuLWxnIGJ0bi1wcmltYXJ5IGJ0bi1ibG9jayI+Q29weTwvYnV0dG9uPgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgIHt7ZWxzZX19CiAgICAgICAgICA8cCBjbGFzcz0ndy0xMDAgdGV4dC1jZW50ZXInPk5vIGludml0ZXMuPC9wPgogICAgICAgICAge3tlbmR9fQogICAgICAgIDwvZGl2PgogICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAge3tlbHNlfX0KICAgIDxkaXYgY2xhc3M9InByaWNpbmctaGVhZGVyIHB4LTMgcHktMyBwdC1tZC01IHBiLW1kLTQgbXgtYXV0byB0ZXh0LWNlbnRlciI+CiAgICAgIDxoMSBjbGFzcz0iZGlzcGxheS00Ij5Zb3UndmUgYmVlbiBpbnZpdGVkPC9oMT4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0nY29udGFpbmVyJz4KICAgICAgPGRpdiBjbGFzcz0ncm93Jz4KICAgICAgICA8ZGl2IGNsYXNzPSJjb2wtMTIgY29sLW1kLTYgb2Zmc2V0LW1kLTMgY29sLWxnLTQgb2Zmc2V0LWxnLTQgZC1mbGV4Ij4KICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQgbWItNCBzaGFkb3ctc20gZmxleC1maWxsIj4KICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZC1oZWFkZXIiPgogICAgICAgICAgICAgIDxoNCBjbGFzcz0ibXktMCBmb250LXdlaWdodC1ub3JtYWwiPlNpZ251cDwvaDQ+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWJvZHkiPgogICAgICAgICAgICAgIDxmb3JtIGlkPSdzaWdudXAnIG1ldGhvZD1QT1NUIGFjdGlvbj0nL2ludml0ZScgZW5jdHlwZT0nbXVsdGlwYXJ0L2Zvcm0tZGF0YSc+CiAgICAgICAgICAgICAgICA8aW5wdXQgdHlwZT1oaWRkZW4gbmFtZT1tZXRob2QgdmFsdWU9UEFUQ0ggLz4KICAgICAgICAgICAgICAgIDxpbnB1dCBuYW1lPWludml0ZSB0eXBlPWhpZGRlbiB2YWx1ZT0ie3suSW52aXRlfX0iIHJlcXVpcmVkPgogICAgICAgICAgICAgICAgPGxhYmVsIGZvcj0ic2lnbnVwSW5wdXRVc2VybmFtZSIgY2xhc3M9InNyLW9ubHkiPlVzZXJuYW1lPC9sYWJlbD4KICAgICAgICAgICAgICAgIDxpbnB1dCBuYW1lPXVzZXJuYW1lIHR5cGU9InRleHQiIGlkPSJzaWdudXBJbnB1dFVzZXJuYW1lIiBjbGFzcz0ibWItMyBmb3JtLWNvbnRyb2wiIHBsYWNlaG9sZGVyPSJVc2VybmFtZSIgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgICA8bGFiZWwgZm9yPSJzaWdudXBJbnB1dFBhc3N3b3JkIiBjbGFzcz0ic3Itb25seSI+UGFzc3dvcmQ8L2xhYmVsPgogICAgICAgICAgICAgICAgPGlucHV0IG5hbWU9cGFzc3dvcmQgdHlwZT0icGFzc3dvcmQiIGlkPSJzaWdudXBJbnB1dFBhc3N3b3JkIiBjbGFzcz0ibWItMyBmb3JtLWNvbnRyb2wiIHBsYWNlaG9sZGVyPSJQYXNzd29yZCIgcmVxdWlyZWQ+CiAgICAgICAgICAgICAgICA8bGFiZWwgZm9yPSJzaWdudXBJbnB1dFZlcmlmeSIgY2xhc3M9InNyLW9ubHkiPkNvbmZpcm0gUGFzc3dvcmQ8L2xhYmVsPgogICAgICAgICAgICAgICAgPGlucHV0IG5hbWU9dmVyaWZ5IHR5cGU9InBhc3N3b3JkIiBpZD0ic2lnbnVwSW5wdXRWZXJpZnkiIGNsYXNzPSJtYi0zIGZvcm0tY29udHJvbCIgcGxhY2Vob2xkZXI9IkNvbmZpcm0gUGFzc3dvcmQiIHJlcXVpcmVkPgogICAgICAgICAgICAgICAgPGJ1dHRvbiBjbGFzcz0iYnRuIGJ0bi1sZyBidG4tcHJpbWFyeSBidG4tYmxvY2siIHR5cGU9InN1Ym1pdCI+R288L2J1dHRvbj4KICAgICAgICAgICAgICA8L2Zvcm0+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICAgIDwvZGl2PgogICAgPC9kaXY+CiAgICB7e2VuZH19CiAgICB7eyB0ZW1wbGF0ZSAiaHRtbC9fZm9vdGVyLmh0bWwiICQgfX0KICA8L21haW4+CiAgPHNjcmlwdCBzcmM9Jy9zdGF0aWMvanMvY2xpcGJvYXJkLmpzJz48L3NjcmlwdD4KICA8c2NyaXB0PihmdW5jdGlvbigpe25ldyBDbGlwYm9hcmRKUygnYnV0dG9uW2RhdGEtY2xpcGJvYXJkLXRhcmdldF0nKX0pKCk7PC9zY3JpcHQ+CiAge3sgdGVtcGxhdGUgImh0bWwvX3NjcmlwdHMuaHRtbCIgfX0KPC9ib2R5Pgo8L2h0bWw+Cg==")

	tmpls["html/privacy.html"] = tostring("PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ZW4+CjxoZWFkPgogIHt7IHRlbXBsYXRlICJodG1sL19oZWFkLmh0bWwiIH19CiAgPHRpdGxlPlNraXBwZXIgQ01TIHwgUHJpdmFjeTwvdGl0bGU+CjwvaGVhZD4KPGJvZHkgY2xhc3M9J3BhZ2UgYmctbGlnaHQnPgogIDxzdHlsZT57eyB0ZW1wbGF0ZSAiY3NzL21haW4uY3NzIiB9fTwvc3R5bGU+CiAgPG1haW4+CiAgICB7eyB0ZW1wbGF0ZSAiaHRtbC9faGVhZGVyLmh0bWwiICQgfX0KICAgIDxkaXYgY2xhc3M9InByaWNpbmctaGVhZGVyIHB4LTMgcHktMyBwdC1tZC01IHBiLW1kLTQgbXgtYXV0byB0ZXh0LWNlbnRlciI+CiAgICAgIDxoMSBjbGFzcz0iZGlzcGxheS00Ij5Qcml2YWN5PC9oMT4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0nY29udGFpbmVyJz4KICAgICAgPGRpdiBjbGFzcz0ncm93Jz4KICAgICAgICA8ZGl2IGNsYXNzPSJjb2wtMTIgb2Zmc2V0LTAgY29sLWxnLTggb2Zmc2V0LWxnLTIiPgogICAgICAgICAgVE9ETwogICAgICAgIDwvZGl2PgogICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAge3sgdGVtcGxhdGUgImh0bWwvX2Zvb3Rlci5odG1sIiAkIH19CiAgPC9tYWluPgogIHt7IHRlbXBsYXRlICJodG1sL19zY3JpcHRzLmh0bWwiIH19CjwvYm9keT4KPC9odG1sPgo=")

	tmpls["html/redirect.html"] = tostring("PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ZW4+CjxoZWFkPgogIHt7IHRlbXBsYXRlICJodG1sL19oZWFkLmh0bWwiIH19CiAgPHRpdGxlPlNraXBwZXIgQ01TPC90aXRsZT4KPC9oZWFkPgo8Ym9keSBjbGFzcz0naW5kZXggYmctbGlnaHQnPgogIDxzdHlsZT57eyB0ZW1wbGF0ZSAiY3NzL21haW4uY3NzIiB9fTwvc3R5bGU+CiAgPG1haW4+CiAgICB7eyB0ZW1wbGF0ZSAiaHRtbC9faGVhZGVyLmh0bWwiICQgfX0KICAgIDxkaXYgY2xhc3M9InByaWNpbmctaGVhZGVyIHB4LTMgcHktMyBwdC1tZC01IHBiLW1kLTQgbXgtYXV0byB0ZXh0LWNlbnRlciI+CiAgICAgIDxoMSBjbGFzcz0iZGlzcGxheS00Ij5SZWRpcmVjdGluZy4uLjwvaDE+CiAgICA8L2Rpdj4KICAgIHt7IHRlbXBsYXRlICJodG1sL19mb290ZXIuaHRtbCIgJCB9fQogIDwvbWFpbj4KICB7eyB0ZW1wbGF0ZSAiaHRtbC9fc2NyaXB0cy5odG1sIiB9fQogIDxzY3JpcHQ+KGZ1bmN0aW9uKCl7c2V0VGltZW91dChmdW5jdGlvbigpe2xvY2F0aW9uLmhyZWY9Int7LlVSTH19Ijt9LDUwMCk7fSkoKTs8L3NjcmlwdD4KPC9ib2R5Pgo8L2h0bWw+Cg==")

M internal/v/v.go => internal/v/v.go +8 -3
@@ 16,9 16,14 @@ func MustParse(name string) *template.Template {
	if all == nil {

		fns := template.FuncMap{
			"inc":    func(i int) int { return i + 1 },
			"title":  func(str string) string { return strings.Title(str) },
			"paid":   func(u user.User) bool { return u.Org().Tier().Is(tier.Business) || u.Org().Tier().Is(tier.Enterprise) },
			"inc":   func(i int) int { return i + 1 },
			"title": func(str string) string { return strings.Title(str) },
			"paid": func(u user.User) bool {
				if u == nil {
					return false
				}
				return u.Org().Tier().Is(tier.Business) || u.Org().Tier().Is(tier.Enterprise)
			},
			"isFree": func(t tier.Tier) bool { return t.Is(tier.Free) },
		}


A main.go => main.go +152 -0
@@ 0,0 1,152 @@
// cms is a free and open source content management system.
package main

import (
	"log"
	"net/http"
	"os"

	"git.sr.ht/~evanj/cms/internal/c"
	"git.sr.ht/~evanj/cms/internal/c/content"
	"git.sr.ht/~evanj/cms/internal/c/contenttype"
	"git.sr.ht/~evanj/cms/internal/c/doc"
	"git.sr.ht/~evanj/cms/internal/c/file"
	"git.sr.ht/~evanj/cms/internal/c/hook"
	"git.sr.ht/~evanj/cms/internal/c/invite"
	"git.sr.ht/~evanj/cms/internal/c/redirect"
	"git.sr.ht/~evanj/cms/internal/c/space"
	"git.sr.ht/~evanj/cms/internal/c/stripe"
	"git.sr.ht/~evanj/cms/internal/c/user"
	"git.sr.ht/~evanj/cms/internal/s/cache"
	"git.sr.ht/~evanj/cms/internal/s/db"
	webhook "git.sr.ht/~evanj/cms/internal/s/hook"
	"git.sr.ht/~evanj/cms/internal/s/rl"
	libstripe "git.sr.ht/~evanj/cms/internal/s/stripe"
	"git.sr.ht/~evanj/cms/pkg/e3"
	"git.sr.ht/~evanj/security"

	_ "git.sr.ht/~evanj/embed" // For embedding static assets during build.
)

var (
	build string

	port             = os.Getenv("PORT")
	dbtype           = os.Getenv("DBTYPE")
	dbcreds          = os.Getenv("DB")
	url              = os.Getenv("URL")
	secret           = os.Getenv("SECRET")
	memcacheKey      = os.Getenv("MEMCACHE_KEY")
	memcacheServer   = os.Getenv("MEMCACHE_SERVER")
	e3user           = os.Getenv("E3_USER")
	e3pass           = os.Getenv("E3_PASS")
	e3url            = os.Getenv("E3_URL")
	signupEnabled    = os.Getenv("SIGNUP_ENABLE") == "true"
	staticDir        = os.Getenv("STATIC_DIR")
	analyticsEnabled = os.Getenv("ANALYTICS_ENABLE") == "true"
	stripeSuccessURL = os.Getenv("STRIPE_SUCCESS_URL")
	stripeErrorURL   = os.Getenv("STRIPE_ERROR_URL")
	stripePK         = os.Getenv("STRIPE_PK")
	stripeSK         = os.Getenv("STRIPE_SK")
)

func main() {
	var (
		w         = os.Stdout
		applogger = log.New(w, "[cms] ", 0)

		db = db.New(
			log.New(w, "[cms:db] ", 0),
			dbtype,
			dbcreds,
			security.Default(secret),
		)

		cacher = cache.New(
			log.New(w, "[cms:cache] ", 0),
			db,
			memcacheKey,
			memcacheServer,
		)

		fs   = e3.New(e3user, e3pass, e3url)
		rl   = rl.New(log.New(w, "[cms:ratelimit] ", 0), cacher, fs)
		c    = c.New(log.New(w, "[cms:content] ", 0), rl, analyticsEnabled, build)
		libs = libstripe.New(log.New(w, "[cms:stripe] ", 0), stripeSuccessURL, stripeErrorURL, stripePK, stripeSK, rl)

		app = &App{
			applogger,
			map[string]http.Handler{
				"content": content.New(
					c,
					log.New(w, "[cms:content] ", 0),
					rl,
					fs,
					webhook.New(log.New(w, "[cms:hook] ", 0), rl),
					url,
				),
				"contenttype": contenttype.New(
					c,
					log.New(w, "[cms:contenttype] ", 0),
					rl,
				),
				"space": space.New(
					c,
					log.New(w, "[cms:space] ", 0),
					rl,
				),
				"user": user.New(
					c,
					log.New(w, "[cms:user] ", 0),
					rl,
					signupEnabled,
					libs,
				),
				"hook": hook.New(
					c,
					log.New(w, "[cms:hook] ", 0),
					rl,
				),
				"file": file.New(
					c,
					log.New(w, "[cms:file] ", 0),
					rl,
					fs,
					url,
				),
				"static": http.StripPrefix("/static", http.FileServer(http.Dir(staticDir))),
				"redirect": redirect.New(
					c,
					log.New(w, "[cms:redirect] ", 0),
					rl,
				),
				"page": doc.New(
					c,
					log.New(w, "[cms:doc] ", 0),
					rl,
				),
				"stripe": http.StripPrefix("/stripe", stripe.New(
					c,
					log.New(w, "[cms:stripe] ", 0),
					rl,
					libs,
				)),
				"invite": http.StripPrefix("/invite", invite.New(
					c,
					log.New(w, "[cms:doc] ", 0),
					rl,
				)),
			},
		}
	)

	if err := db.Setup(); err != nil {
		app.log.Fatal(err)
	}
	if err := cacher.Setup(); err != nil {
		app.log.Fatal(err)
	}

	app.log.Println("listening on", url)
	app.log.Fatal(http.ListenAndServe(port, app))
}

A static/js/clipboard.js => static/js/clipboard.js +7 -0
@@ 0,0 1,7 @@
/*!
 * clipboard.js v2.0.6
 * https://clipboardjs.com/
 * 
 * Licensed MIT © Zeno Rocha
 */
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return o={},r.m=n=[function(t,e){t.exports=function(t){var e;if("SELECT"===t.nodeName)t.focus(),e=t.value;else if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName){var n=t.hasAttribute("readonly");n||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute("readonly"),e=t.value}else{t.hasAttribute("contenteditable")&&t.focus();var o=window.getSelection(),r=document.createRange();r.selectNodeContents(t),o.removeAllRanges(),o.addRange(r),e=o.toString()}return e}},function(t,e){function n(){}n.prototype={on:function(t,e,n){var o=this.e||(this.e={});return(o[t]||(o[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){var o=this;function r(){o.off(t,r),e.apply(n,arguments)}return r._=e,this.on(t,r,n)},emit:function(t){for(var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),o=0,r=n.length;o<r;o++)n[o].fn.apply(n[o].ctx,e);return this},off:function(t,e){var n=this.e||(this.e={}),o=n[t],r=[];if(o&&e)for(var i=0,a=o.length;i<a;i++)o[i].fn!==e&&o[i].fn._!==e&&r.push(o[i]);return r.length?n[t]=r:delete n[t],this}},t.exports=n,t.exports.TinyEmitter=n},function(t,e,n){var d=n(3),h=n(4);t.exports=function(t,e,n){if(!t&&!e&&!n)throw new Error("Missing required arguments");if(!d.string(e))throw new TypeError("Second argument must be a String");if(!d.fn(n))throw new TypeError("Third argument must be a Function");if(d.node(t))return s=e,f=n,(u=t).addEventListener(s,f),{destroy:function(){u.removeEventListener(s,f)}};if(d.nodeList(t))return a=t,c=e,l=n,Array.prototype.forEach.call(a,function(t){t.addEventListener(c,l)}),{destroy:function(){Array.prototype.forEach.call(a,function(t){t.removeEventListener(c,l)})}};if(d.string(t))return o=t,r=e,i=n,h(document.body,o,r,i);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList");var o,r,i,a,c,l,u,s,f}},function(t,n){n.node=function(t){return void 0!==t&&t instanceof HTMLElement&&1===t.nodeType},n.nodeList=function(t){var e=Object.prototype.toString.call(t);return void 0!==t&&("[object NodeList]"===e||"[object HTMLCollection]"===e)&&"length"in t&&(0===t.length||n.node(t[0]))},n.string=function(t){return"string"==typeof t||t instanceof String},n.fn=function(t){return"[object Function]"===Object.prototype.toString.call(t)}},function(t,e,n){var a=n(5);function i(t,e,n,o,r){var i=function(e,n,t,o){return function(t){t.delegateTarget=a(t.target,n),t.delegateTarget&&o.call(e,t)}}.apply(this,arguments);return t.addEventListener(n,i,r),{destroy:function(){t.removeEventListener(n,i,r)}}}t.exports=function(t,e,n,o,r){return"function"==typeof t.addEventListener?i.apply(null,arguments):"function"==typeof n?i.bind(null,document).apply(null,arguments):("string"==typeof t&&(t=document.querySelectorAll(t)),Array.prototype.map.call(t,function(t){return i(t,e,n,o,r)}))}},function(t,e){if("undefined"!=typeof Element&&!Element.prototype.matches){var n=Element.prototype;n.matches=n.matchesSelector||n.mozMatchesSelector||n.msMatchesSelector||n.oMatchesSelector||n.webkitMatchesSelector}t.exports=function(t,e){for(;t&&9!==t.nodeType;){if("function"==typeof t.matches&&t.matches(e))return t;t=t.parentNode}}},function(t,e,n){"use strict";n.r(e);var o=n(0),r=n.n(o),i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};function a(t,e){for(var n=0;n<e.length;n++){var o=e[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}function c(t){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,c),this.resolveOptions(t),this.initSelection()}var l=(function(t,e,n){return e&&a(t.prototype,e),n&&a(t,n),t}(c,[{key:"resolveOptions",value:function(t){var e=0<arguments.length&&void 0!==t?t:{};this.action=e.action,this.container=e.container,this.emitter=e.emitter,this.target=e.target,this.text=e.text,this.trigger=e.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=r()(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=r()(this.target),this.copyText()}},{key:"copyText",value:function(){var e=void 0;try{e=document.execCommand(this.action)}catch(t){e=!1}this.handleResult(e)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),document.activeElement.blur(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(t){var e=0<arguments.length&&void 0!==t?t:"copy";if(this._action=e,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":i(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),c),u=n(1),s=n.n(u),f=n(2),d=n.n(f),h="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},p=function(t,e,n){return e&&y(t.prototype,e),n&&y(t,n),t};function y(t,e){for(var n=0;n<e.length;n++){var o=e[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}var m=(function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(v,s.a),p(v,[{key:"resolveOptions",value:function(t){var e=0<arguments.length&&void 0!==t?t:{};this.action="function"==typeof e.action?e.action:this.defaultAction,this.target="function"==typeof e.target?e.target:this.defaultTarget,this.text="function"==typeof e.text?e.text:this.defaultText,this.container="object"===h(e.container)?e.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=d()(t,"click",function(t){return e.onClick(t)})}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new l({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return b("action",t)}},{key:"defaultTarget",value:function(t){var e=b("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return b("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(t){var e=0<arguments.length&&void 0!==t?t:["copy","cut"],n="string"==typeof e?[e]:e,o=!!document.queryCommandSupported;return n.forEach(function(t){o=o&&!!document.queryCommandSupported(t)}),o}}]),v);function v(t,e){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,v);var n=function(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,(v.__proto__||Object.getPrototypeOf(v)).call(this));return n.resolveOptions(e),n.listenClick(t),n}function b(t,e){var n="data-clipboard-"+t;if(e.hasAttribute(n))return e.getAttribute(n)}e.default=m}],r.c=o,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=6).default;function r(t){if(o[t])return o[t].exports;var e=o[t]={i:t,l:!1,exports:{}};return n[t].call(e.exports,e,e.exports,r),e.l=!0,e.exports}var n,o});
\ No newline at end of file

M vendor/github.com/stripe/stripe-go/v71/.travis.yml => vendor/github.com/stripe/stripe-go/v71/.travis.yml +25 -4
@@ 2,8 2,20 @@ before_install:
  # Install various build dependencies. We use `travis_retry` because `go get`
  # will occasionally fail intermittently.

  # The testify require framework is used for assertions in the test suite
  - travis_retry go get -u github.com/stretchr/testify/require
  # Testify is used for assertions in the test suite.
  #
  # Unfortunately, they broke versions of Go < 1.13. so we conditionally fetch
  # and test only on newer versions of Go. We can back this conditional out
  # once we've dropped < 1.13.
  #
  # Comparing versions lexically like this is technically not safe, but I'm
  # cheating a bit by taking advantage of the fact that we only support > 1.9,
  # so comparing Go versions lexically will be safe for quite some time -- well
  # beyond the point where we back this out. `tip` also sorts after any version.
  - |
    if [[ "$TRAVIS_GO_VERSION" > "1.13" ]]; then
      travis_retry go get -u github.com/stretchr/testify/require
    fi

  # Install lint / code coverage / coveralls tooling
  - travis_retry go get -u golang.org/x/net/http2


@@ 50,8 62,17 @@ matrix:
  fast_finish: true

script:
  - make
  - make coverage
  - |
    # See note above, but Testify broke versions < 1.13, so we only build (and
    # not test) on older versions. Drop this after we've dropped support for <
    # 1.13.
    if [[ "$TRAVIS_GO_VERSION" > "1.13" ]]; then
      make
      make coverage
    else
      make build
    fi


after_script:
  # Send code coverage report to coveralls.io

M vendor/github.com/stripe/stripe-go/v71/CHANGELOG.md => vendor/github.com/stripe/stripe-go/v71/CHANGELOG.md +34 -0
@@ 1,5 1,39 @@
# Changelog

## 71.39.0 - 2020-07-27
* [#1142](https://github.com/stripe/stripe-go/pull/1142) Bug fix: Copy the JSON data of ephemeral keys to own buffer

## 71.38.0 - 2020-07-27
* [#1145](https://github.com/stripe/stripe-go/pull/1145) Fix `ApplicationFeePercent` on `SubscriptionSchedule` to support floats

## 71.37.0 - 2020-07-25
* [#1144](https://github.com/stripe/stripe-go/pull/1144) Add support for `FPXPayments` as a property on `AccountCapabilities`

## 71.36.0 - 2020-07-24
* [#1143](https://github.com/stripe/stripe-go/pull/1143) Add support for `FPXPayments` as a `Capability` on `Account` create and update

## 71.35.0 - 2020-07-22
* [#1140](https://github.com/stripe/stripe-go/pull/1140) Add support for `CartesBancairesPayments` as a `Capability`

## 71.34.0 - 2020-07-20
* [#1138](https://github.com/stripe/stripe-go/pull/1138) Add support for `Capabilities` on `Account` create and update

## 71.33.0 - 2020-07-19
* [#1137](https://github.com/stripe/stripe-go/pull/1137) Add support for `Title` on Sigma `ScheduledQueryRun`

## 71.32.0 - 2020-07-17
* [#1135](https://github.com/stripe/stripe-go/pull/1135) Add support for `PoliticalExposure` on `Person`

## 71.31.0 - 2020-07-16
* [#1133](https://github.com/stripe/stripe-go/pull/1133) Add support for `Deleted` on `LineItem`
* [#1134](https://github.com/stripe/stripe-go/pull/1134) Add support for new constants for `AccountLinkType`

## 71.30.0 - 2020-07-15
* [#1132](https://github.com/stripe/stripe-go/pull/1132) Add support for `AmountTotal`, `AmountSubtotal`, `Currency` and `TotalDetails` on Checkout `Session`

## 71.29.0 - 2020-07-13
* [#1131](https://github.com/stripe/stripe-go/pull/1131) Add `billing_cycle_anchor` to `default_settings` and `phases` for `SubscriptionSchedules`

## 71.28.0 - 2020-06-23
* [#1127](https://github.com/stripe/stripe-go/pull/1127) Add `FilePurposeDocumentProviderIdentityDocument` on `File`
* [#1126](https://github.com/stripe/stripe-go/pull/1126) Add support for `Discounts` on `LineItem`

M vendor/github.com/stripe/stripe-go/v71/VERSION => vendor/github.com/stripe/stripe-go/v71/VERSION +1 -1
@@ 1,1 1,1 @@
71.28.0
71.39.0

M vendor/github.com/stripe/stripe-go/v71/account.go => vendor/github.com/stripe/stripe-go/v71/account.go +109 -32
@@ 21,15 21,17 @@ type AccountCapability string

// List of values that AccountCapability can take.
const (
	AccountCapabilityAUBECSDebitPayments    AccountCapability = "au_becs_debit_payments"
	AccountCapabilityBACSDebitPayments      AccountCapability = "bacs_debit_payments"
	AccountCapabilityCardIssuing            AccountCapability = "card_issuing"
	AccountCapabilityCardPayments           AccountCapability = "card_payments"
	AccountCapabilityJCBPayments            AccountCapability = "jcb_payments"
	AccountCapabilityLegacyPayments         AccountCapability = "legacy_payments"
	AccountCapabilityTaxReportingUS1099K    AccountCapability = "tax_reporting_us_1099_k"
	AccountCapabilityTaxReportingUS1099MISC AccountCapability = "tax_reporting_us_1099_misc"
	AccountCapabilityTransfers              AccountCapability = "transfers"
	AccountCapabilityAUBECSDebitPayments     AccountCapability = "au_becs_debit_payments"
	AccountCapabilityBACSDebitPayments       AccountCapability = "bacs_debit_payments"
	AccountCapabilityCardIssuing             AccountCapability = "card_issuing"
	AccountCapabilityCardPayments            AccountCapability = "card_payments"
	AccountCapabilityCartesBancairesPayments AccountCapability = "cartes_bancaires_payments"
	AccountCapabilityFPXPayments             AccountCapability = "fpx_payments"
	AccountCapabilityJCBPayments             AccountCapability = "jcb_payments"
	AccountCapabilityLegacyPayments          AccountCapability = "legacy_payments"
	AccountCapabilityTaxReportingUS1099K     AccountCapability = "tax_reporting_us_1099_k"
	AccountCapabilityTaxReportingUS1099MISC  AccountCapability = "tax_reporting_us_1099_misc"
	AccountCapabilityTransfers               AccountCapability = "transfers"
)

// AccountCapabilityStatus is the status a given capability can have


@@ 150,6 152,76 @@ type AccountBusinessProfileParams struct {
	URL                *string        `form:"url"`
}

// AccountCapabilitiesAUBECSDebitPaymentsParams represent allowed parameters to configure the AU BECS Debit capability on an account.
type AccountCapabilitiesAUBECSDebitPaymentsParams struct {
	Requested *bool `form:"requested"`
}

// AccountCapabilitiesBACSDebitPaymentsParams represent allowed parameters to configure the BACS Debit capability on an account.
type AccountCapabilitiesBACSDebitPaymentsParams struct {
	Requested *bool `form:"requested"`
}

// AccountCapabilitiesCardIssuingParams represent allowed parameters to configure the Issuing capability on an account.
type AccountCapabilitiesCardIssuingParams struct {
	Requested *bool `form:"requested"`
}

// AccountCapabilitiesCardPaymentsParams represent allowed parameters to configure the card payments capability on an account.
type AccountCapabilitiesCardPaymentsParams struct {
	Requested *bool `form:"requested"`
}

// AccountCapabilitiesCartesBancairesPaymentsParams represent allowed parameters to configure the Cartes Bancaires payments capability on an account.
type AccountCapabilitiesCartesBancairesPaymentsParams struct {
	Requested *bool `form:"requested"`
}

// AccountCapabilitiesFPXPaymentsParams represent allowed parameters to configure the FPX payments capability on an account.
type AccountCapabilitiesFPXPaymentsParams struct {
	Requested *bool `form:"requested"`
}

// AccountCapabilitiesJCBPaymentsParams represent allowed parameters to configure the JCB payments capability on an account.
type AccountCapabilitiesJCBPaymentsParams struct {
	Requested *bool `form:"requested"`
}

// AccountCapabilitiesLegacyPaymentsParams represent allowed parameters to configure the legacy payments capability on an account.
type AccountCapabilitiesLegacyPaymentsParams struct {
	Requested *bool `form:"requested"`
}

// AccountCapabilitiesTaxReportingUS1099KParams represent allowed parameters to configure the 1099-K capability on an account.
type AccountCapabilitiesTaxReportingUS1099KParams struct {
	Requested *bool `form:"requested"`
}

// AccountCapabilitiesTaxReportingUS1099MISCParams represent allowed parameters to configure the 1099-Misc capability on an account.
type AccountCapabilitiesTaxReportingUS1099MISCParams struct {
	Requested *bool `form:"requested"`
}

// AccountCapabilitiesTransfersParams represent allowed parameters to configure the transfers capability on an account.
type AccountCapabilitiesTransfersParams struct {
	Requested *bool `form:"requested"`
}

// AccountCapabilitiesParams represent allowed parameters to configure capabilities on an account.
type AccountCapabilitiesParams struct {
	AUBECSDebitPayments     *AccountCapabilitiesAUBECSDebitPaymentsParams     `form:"au_becs_debit_payments"`
	BACSDebitPayments       *AccountCapabilitiesBACSDebitPaymentsParams       `form:"bacs_debit_payments"`
	CardIssuing             *AccountCapabilitiesCardIssuingParams             `form:"card_issuing"`
	CardPayments            *AccountCapabilitiesCardPaymentsParams            `form:"card_payments"`
	CartesBancairesPayments *AccountCapabilitiesCartesBancairesPaymentsParams `form:"cartes_bancaires_payments"`
	FPXPayments             *AccountCapabilitiesFPXPaymentsParams             `form:"fpx_payments"`
	JCBPayments             *AccountCapabilitiesJCBPaymentsParams             `form:"jcb_payments"`
	LegacyPayments          *AccountCapabilitiesLegacyPaymentsParams          `form:"legacy_payments"`
	TaxReportingUS1099K     *AccountCapabilitiesTaxReportingUS1099KParams     `form:"tax_reporting_us_1099_k"`
	TaxReportingUS1099MISC  *AccountCapabilitiesTaxReportingUS1099MISCParams  `form:"tax_reporting_us_1099_misc"`
	Transfers               *AccountCapabilitiesTransfersParams               `form:"transfers"`
}

// AccountCompanyVerificationDocumentParams are the parameters allowed to pass for a document
// verifying a company.
type AccountCompanyVerificationDocumentParams struct {


@@ 262,20 334,23 @@ func (p *PayoutScheduleParams) AppendTo(body *form.Values, keyParts []string) {

// AccountParams are the parameters allowed during account creation/updates.
type AccountParams struct {
	Params                `form:"*"`
	AccountToken          *string                       `form:"account_token"`
	BusinessProfile       *AccountBusinessProfileParams `form:"business_profile"`
	BusinessType          *string                       `form:"business_type"`
	Company               *AccountCompanyParams         `form:"company"`
	Country               *string                       `form:"country"`
	DefaultCurrency       *string                       `form:"default_currency"`
	Email                 *string                       `form:"email"`
	ExternalAccount       *AccountExternalAccountParams `form:"external_account"`
	Individual            *PersonParams                 `form:"individual"`
	RequestedCapabilities []*string                     `form:"requested_capabilities"`
	Settings              *AccountSettingsParams        `form:"settings"`
	TOSAcceptance         *AccountTOSAcceptanceParams   `form:"tos_acceptance"`
	Type                  *string                       `form:"type"`
	Params          `form:"*"`
	AccountToken    *string                       `form:"account_token"`
	BusinessProfile *AccountBusinessProfileParams `form:"business_profile"`
	BusinessType    *string                       `form:"business_type"`
	Capabilities    *AccountCapabilitiesParams    `form:"capabilities"`
	Company         *AccountCompanyParams         `form:"company"`
	Country         *string                       `form:"country"`
	DefaultCurrency *string                       `form:"default_currency"`
	Email           *string                       `form:"email"`
	ExternalAccount *AccountExternalAccountParams `form:"external_account"`
	Individual      *PersonParams                 `form:"individual"`
	Settings        *AccountSettingsParams        `form:"settings"`
	TOSAcceptance   *AccountTOSAcceptanceParams   `form:"tos_acceptance"`
	Type            *string                       `form:"type"`

	// This parameter is deprecated. Prefer using Capabilities instead.
	RequestedCapabilities []*string `form:"requested_capabilities"`
}

// AccountAddressParams represents an address during account creation/updates.


@@ 349,15 424,17 @@ type AccountBusinessProfile struct {

// AccountCapabilities is the resource representing the capabilities enabled on that account.
type AccountCapabilities struct {
	AUBECSDebitPayments    AccountCapabilityStatus `json:"au_becs_debit_payments"`
	BACSDebitPayments      AccountCapabilityStatus `json:"bacs_debit_payments"`
	CardIssuing            AccountCapabilityStatus `json:"card_issuing"`
	CardPayments           AccountCapabilityStatus `json:"card_payments"`
	JCBPayments            AccountCapabilityStatus `json:"jcb_payments"`
	LegacyPayments         AccountCapabilityStatus `json:"legacy_payments"`
	TaxReportingUS1099K    AccountCapabilityStatus `json:"tax_reporting_us_1099_k"`
	TaxReportingUS1099MISC AccountCapabilityStatus `json:"tax_reporting_us_1099_misc"`
	Transfers              AccountCapabilityStatus `json:"transfers"`
	AUBECSDebitPayments     AccountCapabilityStatus `json:"au_becs_debit_payments"`
	BACSDebitPayments       AccountCapabilityStatus `json:"bacs_debit_payments"`
	CardIssuing             AccountCapabilityStatus `json:"card_issuing"`
	CardPayments            AccountCapabilityStatus `json:"card_payments"`
	CartesBancairesPayments AccountCapabilityStatus `json:"cartes_bancaires_payments"`
	FPXPayments             AccountCapabilityStatus `json:"fpx_payments"`
	JCBPayments             AccountCapabilityStatus `json:"jcb_payments"`
	LegacyPayments          AccountCapabilityStatus `json:"legacy_payments"`
	TaxReportingUS1099K     AccountCapabilityStatus `json:"tax_reporting_us_1099_k"`
	TaxReportingUS1099MISC  AccountCapabilityStatus `json:"tax_reporting_us_1099_misc"`
	Transfers               AccountCapabilityStatus `json:"transfers"`
}

// AccountCompanyVerificationDocument represents details about a company's verification state.

M vendor/github.com/stripe/stripe-go/v71/accountlink.go => vendor/github.com/stripe/stripe-go/v71/accountlink.go +4 -0
@@ 5,6 5,10 @@ type AccountLinkType string

// List of values that AccountLinkType can take.
const (
	AccountLinkTypeAccountOnboarding AccountLinkType = "account_onboarding"
	AccountLinkTypeAccountUpdate     AccountLinkType = "account_update"

	// TODO: The following constants are deprecated
	AccountLinkTypeCustomAccountUpdate       AccountLinkType = "custom_account_update"
	AccountLinkTypeCustomAccountVerification AccountLinkType = "custom_account_verification"
)

M vendor/github.com/stripe/stripe-go/v71/checkout_session.go => vendor/github.com/stripe/stripe-go/v71/checkout_session.go +29 -0
@@ 203,12 203,40 @@ type CheckoutSessionShippingAddressCollection struct {
	AllowedCountries []string `json:"allowed_countries"`
}

// CheckoutSessionTotalDetailsBreakdownDiscount represent the details of one discount applied to a session.
type CheckoutSessionTotalDetailsBreakdownDiscount struct {
	Amount   int64     `json:"amount"`
	Discount *Discount `json:"discount"`
}

// CheckoutSessionTotalDetailsBreakdownTax represent the details of tax rate applied to a session.
type CheckoutSessionTotalDetailsBreakdownTax struct {
	Amount  int64    `json:"amount"`
	TaxRate *TaxRate `json:"tax_rate"`
}

// CheckoutSessionTotalDetailsBreakdown is the set of properties detailing a breakdown of taxes and discounts applied to a session if any.
type CheckoutSessionTotalDetailsBreakdown struct {
	Discounts []*CheckoutSessionTotalDetailsBreakdownDiscount `json:"discounts"`
	Taxes     []*CheckoutSessionTotalDetailsBreakdownTax      `json:"taxes"`
}

// CheckoutSessionTotalDetails is the set of properties detailing how the amounts were calculated.
type CheckoutSessionTotalDetails struct {
	AmountDiscount int64                                 `json:"amount_discount"`
	AmountTax      int64                                 `json:"amount_tax"`
	Breakdown      *CheckoutSessionTotalDetailsBreakdown `json:"breakdown"`
}

// CheckoutSession is the resource representing a Stripe checkout session.
// For more details see https://stripe.com/docs/api/checkout/sessions/object
type CheckoutSession struct {
	APIResource
	CancelURL                 string                                    `json:"cancel_url"`
	AmountSubtotal            int64                                     `json:"amount_subtotal"`
	AmountTotal               int64                                     `json:"amount_total"`
	ClientReferenceID         string                                    `json:"client_reference_id"`
	Currency                  Currency                                  `json:"currency"`
	Customer                  *Customer                                 `json:"customer"`
	CustomerEmail             string                                    `json:"customer_email"`
	Deleted                   bool                                      `json:"deleted"`


@@ 228,6 256,7 @@ type CheckoutSession struct {
	Subscription              *Subscription                             `json:"subscription"`
	SubmitType                CheckoutSessionSubmitType                 `json:"submit_type"`
	SuccessURL                string                                    `json:"success_url"`
	TotalDetails              *CheckoutSessionTotalDetails              `json:"total_details"`
}

// CheckoutSessionList is a list of sessions as retrieved from a list endpoint.

M vendor/github.com/stripe/stripe-go/v71/ephemeralkey.go => vendor/github.com/stripe/stripe-go/v71/ephemeralkey.go +4 -1
@@ 46,7 46,10 @@ func (e *EphemeralKey) UnmarshalJSON(data []byte) error {
		*e = EphemeralKey(ee)
	}

	e.RawJSON = data
	// Go does guarantee the longevity of `data`, so copy when assigning `RawJSON`
	// See https://golang.org/pkg/encoding/json/#Unmarshaler
	// and https://github.com/stripe/stripe-go/pull/1142
	e.RawJSON = append(e.RawJSON[:0], data...)

	return nil
}

M vendor/github.com/stripe/stripe-go/v71/lineitem.go => vendor/github.com/stripe/stripe-go/v71/lineitem.go +1 -0
@@ 24,6 24,7 @@ type LineItem struct {
	Currency       Currency            `json:"currency"`
	Description    string              `json:"description"`
	Discounts      []*LineItemDiscount `json:"discounts"`
	Deleted        bool                `json:"deleted"`
	ID             string              `json:"id"`
	Object         string              `json:"object"`
	Price          *Price              `json:"price"`

M vendor/github.com/stripe/stripe-go/v71/person.go => vendor/github.com/stripe/stripe-go/v71/person.go +56 -45
@@ 24,6 24,15 @@ const (
	VerificationDocumentDetailsCodeDocumentTooLarge              VerificationDocumentDetailsCode = "document_too_large"
)

// PersonPoliticalExposure describes the political exposure of a given person.
type PersonPoliticalExposure string

// List of values that IdentityVerificationStatus can take.
const (
	PersonPoliticalExposureExisting PersonPoliticalExposure = "existing"
	PersonPoliticalExposureNone     PersonPoliticalExposure = "none"
)

// PersonVerificationDetailsCode is a machine-readable code specifying the verification state of a
// person.
type PersonVerificationDetailsCode string


@@ 79,27 88,28 @@ type PersonVerificationParams struct {
// PersonParams is the set of parameters that can be used when creating or updating a person.
// For more details see https://stripe.com/docs/api#create_person.
type PersonParams struct {
	Params         `form:"*"`
	Account        *string                   `form:"-"` // Included in URL
	Address        *AccountAddressParams     `form:"address"`
	AddressKana    *AccountAddressParams     `form:"address_kana"`
	AddressKanji   *AccountAddressParams     `form:"address_kanji"`
	DOB            *DOBParams                `form:"dob"`
	Email          *string                   `form:"email"`
	FirstName      *string                   `form:"first_name"`
	FirstNameKana  *string                   `form:"first_name_kana"`
	FirstNameKanji *string                   `form:"first_name_kanji"`
	Gender         *string                   `form:"gender"`
	IDNumber       *string                   `form:"id_number"`
	LastName       *string                   `form:"last_name"`
	LastNameKana   *string                   `form:"last_name_kana"`
	LastNameKanji  *string                   `form:"last_name_kanji"`
	MaidenName     *string                   `form:"maiden_name"`
	PersonToken    *string                   `form:"person_token"`
	Phone          *string                   `form:"phone"`
	Relationship   *RelationshipParams       `form:"relationship"`
	SSNLast4       *string                   `form:"ssn_last_4"`
	Verification   *PersonVerificationParams `form:"verification"`
	Params            `form:"*"`
	Account           *string                   `form:"-"` // Included in URL
	Address           *AccountAddressParams     `form:"address"`
	AddressKana       *AccountAddressParams     `form:"address_kana"`
	AddressKanji      *AccountAddressParams     `form:"address_kanji"`
	DOB               *DOBParams                `form:"dob"`
	Email             *string                   `form:"email"`
	FirstName         *string                   `form:"first_name"`
	FirstNameKana     *string                   `form:"first_name_kana"`
	FirstNameKanji    *string                   `form:"first_name_kanji"`
	Gender            *string                   `form:"gender"`
	IDNumber          *string                   `form:"id_number"`
	LastName          *string                   `form:"last_name"`
	LastNameKana      *string                   `form:"last_name_kana"`
	LastNameKanji     *string                   `form:"last_name_kanji"`
	MaidenName        *string                   `form:"maiden_name"`
	PersonToken       *string                   `form:"person_token"`
	Phone             *string                   `form:"phone"`
	PoliticalExposure *string                   `form:"political_exposure"`
	Relationship      *RelationshipParams       `form:"relationship"`
	SSNLast4          *string                   `form:"ssn_last_4"`
	Verification      *PersonVerificationParams `form:"verification"`
}

// RelationshipListParams is used to filter persons by the relationship


@@ 165,30 175,31 @@ type PersonVerification struct {
// For more details see https://stripe.com/docs/api#persons.
type Person struct {
	APIResource
	Account          string              `json:"account"`
	Address          *AccountAddress     `json:"address"`
	AddressKana      *AccountAddress     `json:"address_kana"`
	AddressKanji     *AccountAddress     `json:"address_kanji"`
	Deleted          bool                `json:"deleted"`
	DOB              *DOB                `json:"dob"`
	Email            string              `json:"email"`
	FirstName        string              `json:"first_name"`
	FirstNameKana    string              `json:"first_name_kana"`
	FirstNameKanji   string              `json:"first_name_kanji"`
	Gender           string              `json:"gender"`
	ID               string              `json:"id"`
	IDNumberProvided bool                `json:"id_number_provided"`
	LastName         string              `json:"last_name"`
	LastNameKana     string              `json:"last_name_kana"`
	LastNameKanji    string              `json:"last_name_kanji"`
	MaidenName       string              `json:"maiden_name"`
	Metadata         map[string]string   `json:"metadata"`
	Object           string              `json:"object"`
	Phone            string              `json:"phone"`
	Relationship     *Relationship       `json:"relationship"`
	Requirements     *Requirements       `json:"requirements"`
	SSNLast4Provided bool                `json:"ssn_last_4_provided"`
	Verification     *PersonVerification `json:"verification"`
	Account           string                  `json:"account"`
	Address           *AccountAddress         `json:"address"`
	AddressKana       *AccountAddress         `json:"address_kana"`
	AddressKanji      *AccountAddress         `json:"address_kanji"`
	Deleted           bool                    `json:"deleted"`
	DOB               *DOB                    `json:"dob"`
	Email             string                  `json:"email"`
	FirstName         string                  `json:"first_name"`
	FirstNameKana     string                  `json:"first_name_kana"`
	FirstNameKanji    string                  `json:"first_name_kanji"`
	Gender            string                  `json:"gender"`
	ID                string                  `json:"id"`
	IDNumberProvided  bool                    `json:"id_number_provided"`
	LastName          string                  `json:"last_name"`
	LastNameKana      string                  `json:"last_name_kana"`
	LastNameKanji     string                  `json:"last_name_kanji"`
	MaidenName        string                  `json:"maiden_name"`
	Metadata          map[string]string       `json:"metadata"`
	Object            string                  `json:"object"`
	Phone             string                  `json:"phone"`
	PoliticalExposure PersonPoliticalExposure `json:"political_exposure"`
	Relationship      *Relationship           `json:"relationship"`
	Requirements      *Requirements           `json:"requirements"`
	SSNLast4Provided  bool                    `json:"ssn_last_4_provided"`
	Verification      *PersonVerification     `json:"verification"`
}

// PersonList is a list of persons as retrieved from a list endpoint.

M vendor/github.com/stripe/stripe-go/v71/sigma_scheduledqueryrun.go => vendor/github.com/stripe/stripe-go/v71/sigma_scheduledqueryrun.go +1 -0
@@ 37,6 37,7 @@ type SigmaScheduledQueryRun struct {
	SQL                  string                       `json:"sql"`
	Status               SigmaScheduledQueryRunStatus `json:"status"`
	Query                string                       `json:"query"`
	Title                string                       `json:"title"`
}

// SigmaScheduledQueryRunList is a list of scheduled query runs as retrieved from a list endpoint.

M vendor/github.com/stripe/stripe-go/v71/stripe.go => vendor/github.com/stripe/stripe-go/v71/stripe.go +1 -1
@@ 994,7 994,7 @@ func StringSlice(v []string) []*string {
const apiURL = "https://api.stripe.com"

// clientversion is the binding version
const clientversion = "71.28.0"
const clientversion = "71.39.0"

// defaultHTTPTimeout is the default timeout on the http.Client used by the library.
// This is chosen to be consistent with the other Stripe language libraries and

M vendor/github.com/stripe/stripe-go/v71/subschedule.go => vendor/github.com/stripe/stripe-go/v71/subschedule.go +50 -34
@@ 27,6 27,16 @@ const (
	SubscriptionScheduleStatusTrialing  SubscriptionScheduleStatus = "released"
)

// SubscriptionSchedulePhaseBillingCycleAnchor is the list of allowed values for the
// schedule's billing_cycle_anchor.
type SubscriptionSchedulePhaseBillingCycleAnchor string

// List of values for SubscriptionSchedulePhaseBillingCycleAnchor
const (
	SubscriptionSchedulePhaseBillingCycleAnchorAutomatic  SubscriptionSchedulePhaseBillingCycleAnchor = "automatic"
	SubscriptionSchedulePhaseBillingCycleAnchorPhaseStart SubscriptionSchedulePhaseBillingCycleAnchor = "phase_start"
)

// SubscriptionScheduleInvoiceSettingsParams is a structure representing the parameters allowed to
// control invoice settings on invoices associated with a subscription schedule.
type SubscriptionScheduleInvoiceSettingsParams struct {


@@ 37,6 47,7 @@ type SubscriptionScheduleInvoiceSettingsParams struct {
// representing the subscription schedule’s default settings.
type SubscriptionScheduleDefaultSettingsParams struct {
	Params               `form:"*"`
	BillingCycleAnchor   *string                                    `form:"billing_cycle_anchor"`
	BillingThresholds    *SubscriptionBillingThresholdsParams       `form:"billing_thresholds"`
	CollectionMethod     *string                                    `form:"collection_method"`
	DefaultPaymentMethod *string                                    `form:"default_payment_method"`


@@ 86,22 97,25 @@ type SubscriptionSchedulePhaseItemParams struct {
// SubscriptionSchedulePhaseParams is a structure representing the parameters allowed to control
// a phase on a subscription schedule.
type SubscriptionSchedulePhaseParams struct {
	AddInvoiceItems       []*SubscriptionSchedulePhaseAddInvoiceItemParams `form:"add_invoice_items"`
	ApplicationFeePercent *int64                                           `form:"application_fee_percent"`
	BillingThresholds     *SubscriptionBillingThresholdsParams             `form:"billing_thresholds"`
	CollectionMethod      *string                                          `form:"collection_method"`
	Coupon                *string                                          `form:"coupon"`
	DefaultPaymentMethod  *string                                          `form:"default_payment_method"`
	DefaultTaxRates       []*string                                        `form:"default_tax_rates"`
	EndDate               *int64                                           `form:"end_date"`
	InvoiceSettings       *SubscriptionScheduleInvoiceSettingsParams       `form:"invoice_settings"`
	Iterations            *int64                                           `form:"iterations"`
	Plans                 []*SubscriptionSchedulePhaseItemParams           `form:"plans"`
	ProrationBehavior     *string                                          `form:"proration_behavior"`
	StartDate             *int64                                           `form:"start_date"`
	TransferData          *SubscriptionTransferDataParams                  `form:"transfer_data"`
	Trial                 *bool                                            `form:"trial"`
	TrialEnd              *int64                                           `form:"trial_end"`
	AddInvoiceItems []*SubscriptionSchedulePhaseAddInvoiceItemParams `form:"add_invoice_items"`
	// This parameter expects a *float64 but was defined as *int64 so we're adding support for both
	// TODO: Remove in the next major
	ApplicationFeePercent interface{}                                `form:"application_fee_percent"`
	BillingCycleAnchor    *string                                    `form:"billing_cycle_anchor"`
	BillingThresholds     *SubscriptionBillingThresholdsParams       `form:"billing_thresholds"`
	CollectionMethod      *string                                    `form:"collection_method"`
	Coupon                *string                                    `form:"coupon"`
	DefaultPaymentMethod  *string                                    `form:"default_payment_method"`
	DefaultTaxRates       []*string                                  `form:"default_tax_rates"`
	EndDate               *int64                                     `form:"end_date"`
	InvoiceSettings       *SubscriptionScheduleInvoiceSettingsParams `form:"invoice_settings"`
	Iterations            *int64                                     `form:"iterations"`
	Plans                 []*SubscriptionSchedulePhaseItemParams     `form:"plans"`
	ProrationBehavior     *string                                    `form:"proration_behavior"`
	StartDate             *int64                                     `form:"start_date"`
	TransferData          *SubscriptionTransferDataParams            `form:"transfer_data"`
	Trial                 *bool                                      `form:"trial"`
	TrialEnd              *int64                                     `form:"trial_end"`

	// This parameter is deprecated and we recommend that you use TaxRates instead.
	TaxPercent *float64 `form:"tax_percent"`


@@ 179,11 193,12 @@ type SubscriptionScheduleInvoiceSettings struct {
// SubscriptionScheduleDefaultSettings is a structure representing the
// subscription schedule’s default settings.
type SubscriptionScheduleDefaultSettings struct {
	BillingThresholds    *SubscriptionBillingThresholds       `json:"billing_thresholds"`
	CollectionMethod     SubscriptionCollectionMethod         `json:"collection_method"`
	DefaultPaymentMethod *PaymentMethod                       `json:"default_payment_method"`
	InvoiceSettings      *SubscriptionScheduleInvoiceSettings `json:"invoice_settings"`
	TransferData         *SubscriptionTransferData            `json:"transfer_data"`
	BillingCycleAnchor   SubscriptionSchedulePhaseBillingCycleAnchor `json:"billing_cycle_anchor"`
	BillingThresholds    *SubscriptionBillingThresholds              `json:"billing_thresholds"`
	CollectionMethod     SubscriptionCollectionMethod                `json:"collection_method"`
	DefaultPaymentMethod *PaymentMethod                              `json:"default_payment_method"`
	InvoiceSettings      *SubscriptionScheduleInvoiceSettings        `json:"invoice_settings"`
	TransferData         *SubscriptionTransferData                   `json:"transfer_data"`
}

// SubscriptionSchedulePhaseAddInvoiceItem represents the invoice items to add when the phase starts.


@@ 203,19 218,20 @@ type SubscriptionSchedulePhaseItem struct {

// SubscriptionSchedulePhase is a structure a phase of a subscription schedule.
type SubscriptionSchedulePhase struct {
	AddInvoiceItems       []*SubscriptionSchedulePhaseAddInvoiceItem `json:"add_invoice_items"`
	ApplicationFeePercent float64                                    `json:"application_fee_percent"`
	BillingThresholds     *SubscriptionBillingThresholds             `json:"billing_thresholds"`
	CollectionMethod      SubscriptionCollectionMethod               `json:"collection_method"`
	Coupon                *Coupon                                    `json:"coupon"`
	DefaultPaymentMethod  *PaymentMethod                             `json:"default_payment_method"`
	DefaultTaxRates       []*TaxRate                                 `json:"default_tax_rates"`
	EndDate               int64                                      `json:"end_date"`
	InvoiceSettings       *SubscriptionScheduleInvoiceSettings       `json:"invoice_settings"`
	Plans                 []*SubscriptionSchedulePhaseItem           `json:"plans"`
	StartDate             int64                                      `json:"start_date"`
	TransferData          *SubscriptionTransferData                  `json:"transfer_data"`
	TrialEnd              int64                                      `json:"trial_end"`
	AddInvoiceItems       []*SubscriptionSchedulePhaseAddInvoiceItem  `json:"add_invoice_items"`
	ApplicationFeePercent float64                                     `json:"application_fee_percent"`
	BillingCycleAnchor    SubscriptionSchedulePhaseBillingCycleAnchor `json:"billing_cycle_anchor"`
	BillingThresholds     *SubscriptionBillingThresholds              `json:"billing_thresholds"`
	CollectionMethod      SubscriptionCollectionMethod                `json:"collection_method"`
	Coupon                *Coupon                                     `json:"coupon"`
	DefaultPaymentMethod  *PaymentMethod                              `json:"default_payment_method"`
	DefaultTaxRates       []*TaxRate                                  `json:"default_tax_rates"`
	EndDate               int64                                       `json:"end_date"`
	InvoiceSettings       *SubscriptionScheduleInvoiceSettings        `json:"invoice_settings"`
	Plans                 []*SubscriptionSchedulePhaseItem            `json:"plans"`
	StartDate             int64                                       `json:"start_date"`
	TransferData          *SubscriptionTransferData                   `json:"transfer_data"`
	TrialEnd              int64                                       `json:"trial_end"`

	// This field is deprecated and we recommend that you use TaxRates instead.
	TaxPercent float64 `json:"tax_percent"`

M vendor/modules.txt => vendor/modules.txt +1 -1
@@ 31,7 31,7 @@ github.com/kr/text
# github.com/pkg/errors v0.9.1
## explicit
github.com/pkg/errors
# github.com/stripe/stripe-go/v71 v71.28.0
# github.com/stripe/stripe-go/v71 v71.39.0
## explicit
github.com/stripe/stripe-go/v71
github.com/stripe/stripe-go/v71/checkout/session