diff --git a/.changeset/breezy-pans-move.md b/.changeset/breezy-pans-move.md new file mode 100644 index 00000000..53cf2e32 --- /dev/null +++ b/.changeset/breezy-pans-move.md @@ -0,0 +1,5 @@ +--- +'pockethost': patch +--- + +Add db migration support for multiple custom domains per instance diff --git a/packages/pockethost/src/mothership-app/pb_hooks/mothership.js b/packages/pockethost/src/mothership-app/pb_hooks/mothership.js index 993f96e8..645c70ae 100644 --- a/packages/pockethost/src/mothership-app/pb_hooks/mothership.js +++ b/packages/pockethost/src/mothership-app/pb_hooks/mothership.js @@ -1,3255 +1,3083 @@ -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); -// src/lib/index.ts -var lib_exports = {}; -__export(lib_exports, { - HandleInstanceBeforeUpdate: () => HandleInstanceBeforeUpdate, - HandleInstanceCreate: () => HandleInstanceCreate, - HandleInstanceDelete: () => HandleInstanceDelete, - HandleInstanceResolve: () => HandleInstanceResolve, - HandleInstanceUpdate: () => HandleInstanceUpdate, - HandleInstanceVersionValidation: () => HandleInstanceVersionValidation, - HandleInstancesResetIdle: () => HandleInstancesResetIdle, - HandleLemonSqueezySale: () => HandleLemonSqueezySale, - HandleMailSend: () => HandleMailSend, - HandleMetaUpdateAtBoot: () => HandleMetaUpdateAtBoot, - HandleMigrateInstanceVersions: () => HandleMigrateInstanceVersions, - HandleMigrateRegions: () => HandleMigrateRegions, - HandleMirrorData: () => HandleMirrorData, - HandleNotifyDiscordAfterCreate: () => HandleNotifyDiscordAfterCreate, - HandleProcessNotification: () => HandleProcessNotification, - HandleProcessSingleNotification: () => HandleProcessSingleNotification, - HandleSesError: () => HandleSesError, - HandleSignupCheck: () => HandleSignupCheck, - HandleSignupConfirm: () => HandleSignupConfirm, - HandleStatsRequest: () => HandleStatsRequest, - HandleUserTokenRequest: () => HandleUserTokenRequest, - HandleUserWelcomeMessage: () => HandleUserWelcomeMessage, - HandleVersionsRequest: () => HandleVersionsRequest -}); -module.exports = __toCommonJS(lib_exports); - -// src/lib/util/Logger.ts -var mkLog = (namespace) => (...s) => console.log( - `[${namespace}]`, - ...s.map((p) => { - if (typeof p === "object") return JSON.stringify(p, null, 2); - return p; - }) -); -var dbg = (...args) => console.log(args); -var interpolateString = (template, dict) => { - return template.replace(/\{\$(\w+)\}/g, (match, key) => { - dbg({ match, key }); - const lowerKey = key.toLowerCase(); - return dict.hasOwnProperty(lowerKey) ? dict[lowerKey] || "" : match; - }); +//#region src/lib/util/Logger.ts +const mkLog = (namespace) => (...s) => console.log(`[${namespace}]`, ...s.map((p) => { + if (typeof p === "object") return JSON.stringify(p, null, 2); + return p; +})); +const dbg = (...args) => console.log(args); +const interpolateString = (template, dict) => { + return template.replace(/\{\$(\w+)\}/g, (match, key) => { + dbg({ + match, + key + }); + const lowerKey = key.toLowerCase(); + return dict.hasOwnProperty(lowerKey) ? dict[lowerKey] || "" : match; + }); }; -// src/lib/util/versions.ts -var versions = require(`${__hooks}/versions.cjs`); +//#endregion +//#region src/lib/util/versions.ts +const versions = require(`${__hooks}/versions.cjs`); -// src/lib/handlers/instance/api/HandleInstanceCreate.ts -var HandleInstanceCreate = (c) => { - const dao = $app.dao(); - const log = mkLog(`POST:instance`); - const authRecord = c.get("authRecord"); - log(`***authRecord`, JSON.stringify(authRecord)); - if (!authRecord) { - throw new Error(`Expected authRecord here`); - } - log(`***TOP OF POST`); - let data = new DynamicModel({ - subdomain: "", - version: versions[0], - region: "sfo-2" - }); - log(`***before bind`); - c.bind(data); - log(`***after bind`); - data = JSON.parse(JSON.stringify(data)); - const { subdomain, version, region } = data; - log(`***vars`, JSON.stringify({ subdomain, region })); - if (!subdomain) { - throw new BadRequestError( - `Subdomain is required when creating an instance.` - ); - } - const collection = dao.findCollectionByNameOrId("instances"); - const record = new Record(collection); - record.set("uid", authRecord.getId()); - record.set("region", region || `sfo-1`); - record.set("subdomain", subdomain); - record.set("power", true); - record.set("status", "idle"); - record.set("version", version); - record.set("dev", true); - record.set("syncAdmin", true); - const form = new RecordUpsertForm($app, record); - form.submit(); - return c.json(200, { instance: record }); +//#endregion +//#region src/lib/handlers/instance/api/HandleInstanceCreate.ts +const HandleInstanceCreate = (c) => { + const dao = $app.dao(); + const log = mkLog(`POST:instance`); + const authRecord = c.get("authRecord"); + log(`***authRecord`, JSON.stringify(authRecord)); + if (!authRecord) throw new Error(`Expected authRecord here`); + log(`***TOP OF POST`); + let data = new DynamicModel({ + subdomain: "", + version: versions[0], + region: "sfo-2" + }); + log(`***before bind`); + c.bind(data); + log(`***after bind`); + data = JSON.parse(JSON.stringify(data)); + const { subdomain, version, region } = data; + log(`***vars`, JSON.stringify({ + subdomain, + region + })); + if (!subdomain) throw new BadRequestError(`Subdomain is required when creating an instance.`); + const collection = dao.findCollectionByNameOrId("instances"); + const record = new Record(collection); + record.set("uid", authRecord.getId()); + record.set("region", region || `sfo-1`); + record.set("subdomain", subdomain); + record.set("power", true); + record.set("status", "idle"); + record.set("version", version); + record.set("dev", true); + record.set("syncAdmin", true); + const form = new RecordUpsertForm($app, record); + form.submit(); + return c.json(200, { instance: record }); }; -// src/lib/handlers/instance/api/HandleInstanceDelete.ts -var HandleInstanceDelete = (c) => { - const dao = $app.dao(); - const log = mkLog(`DELETE:instance`); - log(`TOP OF DELETE`); - let data = new DynamicModel({ - id: "" - }); - c.bind(data); - log(`After bind`); - data = JSON.parse(JSON.stringify(data)); - const id = c.pathParam("id"); - log( - `vars`, - JSON.stringify({ - id - }) - ); - const authRecord = c.get("authRecord"); - log(`authRecord`, JSON.stringify(authRecord)); - if (!authRecord) { - throw new BadRequestError(`Expected authRecord here`); - } - const record = dao.findRecordById("instances", id); - if (!record) { - throw new BadRequestError(`Instance ${id} not found.`); - } - if (record.get("uid") !== authRecord.id) { - throw new BadRequestError(`Not authorized`); - } - if (record.getString("status").toLowerCase() !== "idle") { - throw new BadRequestError(`Instance must be shut down first.`); - } - const path = [$os.getenv("DATA_ROOT"), id].join("/"); - log(`path ${path}`); - const res = $os.removeAll(path); - log(`res`, res); - dao.deleteRecord(record); - return c.json(200, { status: "ok" }); +//#endregion +//#region src/lib/handlers/instance/api/HandleInstanceDelete.ts +const HandleInstanceDelete = (c) => { + const dao = $app.dao(); + const log = mkLog(`DELETE:instance`); + log(`TOP OF DELETE`); + let data = new DynamicModel({ id: "" }); + c.bind(data); + log(`After bind`); + data = JSON.parse(JSON.stringify(data)); + const id = c.pathParam("id"); + log(`vars`, JSON.stringify({ id })); + const authRecord = c.get("authRecord"); + log(`authRecord`, JSON.stringify(authRecord)); + if (!authRecord) throw new BadRequestError(`Expected authRecord here`); + const record = dao.findRecordById("instances", id); + if (!record) throw new BadRequestError(`Instance ${id} not found.`); + if (record.get("uid") !== authRecord.id) throw new BadRequestError(`Not authorized`); + if (record.getString("status").toLowerCase() !== "idle") throw new BadRequestError(`Instance must be shut down first.`); + const path = [$os.getenv("DATA_ROOT"), id].join("/"); + log(`path ${path}`); + const res = $os.removeAll(path); + log(`res`, res); + dao.deleteRecord(record); + return c.json(200, { status: "ok" }); }; -// src/lib/handlers/instance/api/HandleInstanceResolve.ts -var HandleInstanceResolve = (c) => { - const dao = $app.dao(); - const log = mkLog(`GET:instance/resolve`); - log(`***TOP OF GET`); - const host = c.queryParam("host"); - if (!host) { - throw new BadRequestError(`Host is required when resolving an instance.`); - } - const instance = (() => { - try { - log(`Checking for cname ${host}`); - const record = $app.dao().findFirstRecordByData("instances", "cname", host); - return record; - } catch (e) { - log(`${host} is not a cname`); - } - const [subdomain, ...junk] = host.split("."); - if (!subdomain) { - throw new BadRequestError( - `Subdomain or instance ID is required when resolving an instance without a cname.` - ); - } - try { - log(`Checking for instance ID ${subdomain}`); - const record = $app.dao().findRecordById("instances", subdomain); - return record; - } catch (e) { - log(`${subdomain} is not an instance ID`); - } - try { - log(`Checking for subdomain ${subdomain}`); - const record = $app.dao().findFirstRecordByData("instances", `subdomain`, subdomain); - return record; - } catch (e) { - log(`${subdomain} is not a subdomain`); - } - throw new BadRequestError(`Instance not found.`); - })(); - log(`Checking for instance suspension`); - if (instance.get("suspension")) { - throw new BadRequestError(instance.get("suspension")); - } - const APP_URL = (...path) => [$os.getenv("APP_URL"), ...path].join("/"); - const DOC_URL = (...path) => APP_URL("docs", ...path); - log(`Checking for power`); - if (!instance.getBool("power")) { - throw new BadRequestError( - `This instance is powered off. See ${DOC_URL( - `power` - )} for more information.` - ); - } - const user = (() => { - const userId = instance.get("uid"); - if (!userId) { - throw new BadRequestError(`Instance has no user.`); - } - try { - log(`Checking for user ${userId}`); - const record = $app.dao().findRecordById("users", userId); - return record; - } catch (e) { - log(`User ${userId} not found`); - } - throw new BadRequestError(`User not found.`); - })(); - log(`Checking for user suspension`); - if (user.get("suspension")) { - throw new BadRequestError(user.get("suspension")); - } - log(`Checking for active instances`); - if (user.getInt("subscription_quantity") === 0) { - throw new BadRequestError( - `Instances will not run until you upgrade.` - ); - } - log(`Checking for verified account`); - if (!user.getBool("verified")) { - throw new BadRequestError(`Log in at ${APP_URL()} to verify your account.`); - } - const tokenKey = user.getString("tokenKey"); - const passwordHash = user.getString("passwordHash"); - const email = user.getString("email"); - const userJSON = JSON.parse(JSON.stringify(user)); - const ret = { - instance, - user: { - ...userJSON, - tokenKey, - passwordHash, - email - } - }; - log(`Returning instance and user`, ret); - return c.json(200, ret); +//#endregion +//#region src/lib/handlers/instance/api/HandleInstanceResolve.ts +const HandleInstanceResolve = (c) => { + const dao = $app.dao(); + const log = mkLog(`GET:instance/resolve`); + log(`***TOP OF GET`); + const host = c.queryParam("host"); + if (!host) throw new BadRequestError(`Host is required when resolving an instance.`); + const instance = (() => { + try { + log(`Checking for cname ${host}`); + const record = $app.dao().findFirstRecordByData("instances", "cname", host); + return record; + } catch (e) { + log(`${host} is not a cname`); + } + const [subdomain, ...junk] = host.split("."); + if (!subdomain) throw new BadRequestError(`Subdomain or instance ID is required when resolving an instance without a cname.`); + try { + log(`Checking for instance ID ${subdomain}`); + const record = $app.dao().findRecordById("instances", subdomain); + return record; + } catch (e) { + log(`${subdomain} is not an instance ID`); + } + try { + log(`Checking for subdomain ${subdomain}`); + const record = $app.dao().findFirstRecordByData("instances", `subdomain`, subdomain); + return record; + } catch (e) { + log(`${subdomain} is not a subdomain`); + } + throw new BadRequestError(`Instance not found.`); + })(); + log(`Checking for instance suspension`); + if (instance.get("suspension")) throw new BadRequestError(instance.get("suspension")); + const APP_URL = (...path) => [$os.getenv("APP_URL"), ...path].join("/"); + const DOC_URL = (...path) => APP_URL("docs", ...path); + log(`Checking for power`); + if (!instance.getBool("power")) throw new BadRequestError(`This instance is powered off. See ${DOC_URL(`power`)} for more information.`); + const user = (() => { + const userId = instance.get("uid"); + if (!userId) throw new BadRequestError(`Instance has no user.`); + try { + log(`Checking for user ${userId}`); + const record = $app.dao().findRecordById("users", userId); + return record; + } catch (e) { + log(`User ${userId} not found`); + } + throw new BadRequestError(`User not found.`); + })(); + log(`Checking for user suspension`); + if (user.get("suspension")) throw new BadRequestError(user.get("suspension")); + log(`Checking for active instances`); + if (user.getInt("subscription_quantity") === 0) throw new BadRequestError(`Instances will not run until you upgrade.`); + log(`Checking for verified account`); + if (!user.getBool("verified")) throw new BadRequestError(`Log in at ${APP_URL()} to verify your account.`); + const tokenKey = user.getString("tokenKey"); + const passwordHash = user.getString("passwordHash"); + const email = user.getString("email"); + const userJSON = JSON.parse(JSON.stringify(user)); + const ret = { + instance, + user: { + ...userJSON, + tokenKey, + passwordHash, + email + } + }; + log(`Returning instance and user`, ret); + return c.json(200, ret); }; -// ../../../../node_modules/.pnpm/@s-libs+micro-dash@18.0.0/node_modules/@s-libs/micro-dash/fesm2022/micro-dash.mjs +//#endregion +//#region ../../../../node_modules/.pnpm/@s-libs+micro-dash@18.0.0/node_modules/@s-libs/micro-dash/fesm2022/micro-dash.mjs function keysOfNonArray(object) { - return object ? Object.getOwnPropertyNames(object) : []; + return object ? Object.getOwnPropertyNames(object) : []; } function forOwnOfNonArray(object, iteratee) { - forEachOfArray(keysOfNonArray(object), (key) => iteratee(object[key], key)); - return object; + forEachOfArray(keysOfNonArray(object), (key) => iteratee(object[key], key)); + return object; } function forEach(collection, iteratee) { - if (Array.isArray(collection)) { - forEachOfArray(collection, iteratee); - } else { - forOwnOfNonArray(collection, iteratee); - } - return collection; + if (Array.isArray(collection)) forEachOfArray(collection, iteratee); + else forOwnOfNonArray(collection, iteratee); + return collection; } function forEachOfArray(array, iteratee) { - for (let i = 0, len = array.length; i < len; ++i) { - if (iteratee(array[i], i) === false) { - break; - } - } + for (let i = 0, len = array.length; i < len; ++i) if (iteratee(array[i], i) === false) break; } function doReduce(iterationFn, collection, iteratee, accumulator, initAccum) { - iterationFn(collection, (value, indexOrKey) => { - if (initAccum) { - accumulator = value; - initAccum = false; - } else { - accumulator = iteratee(accumulator, value, indexOrKey); - } - }); - return accumulator; + iterationFn(collection, (value, indexOrKey) => { + if (initAccum) { + accumulator = value; + initAccum = false; + } else accumulator = iteratee(accumulator, value, indexOrKey); + }); + return accumulator; } function reduce(collection, iteratee, accumulator) { - return doReduce(forEach, collection, iteratee, accumulator, arguments.length < 3); + return doReduce(forEach, collection, iteratee, accumulator, arguments.length < 3); } -// src/lib/util/removeEmptyKeys.ts -var removeEmptyKeys = (obj) => { - const sanitized = reduce( - obj, - (acc, value, key) => { - if (value !== null && value !== void 0) { - acc[key] = value; - } - return acc; - }, - {} - ); - return sanitized; +//#endregion +//#region src/lib/util/removeEmptyKeys.ts +const removeEmptyKeys = (obj) => { + const sanitized = reduce(obj, (acc, value, key) => { + if (value !== null && value !== void 0) acc[key] = value; + return acc; + }, {}); + return sanitized; }; -// src/lib/handlers/instance/api/HandleInstanceUpdate.ts -var HandleInstanceUpdate = (c) => { - const dao = $app.dao(); - const log = mkLog(`PUT:instance`); - log(`TOP OF PUT`); - let data = new DynamicModel({ - id: "", - fields: { - subdomain: null, - power: null, - version: null, - secrets: null, - syncAdmin: null, - dev: null, - cname: null - } - }); - c.bind(data); - log(`After bind`); - data = JSON.parse(JSON.stringify(data)); - const id = c.pathParam("id"); - const { - fields: { subdomain, power, version, secrets, syncAdmin, dev, cname } - } = data; - log( - `vars`, - JSON.stringify({ - id, - subdomain, - power, - version, - secrets, - syncAdmin, - dev, - cname - }) - ); - const record = dao.findRecordById("instances", id); - const authRecord = c.get("authRecord"); - log(`authRecord`, JSON.stringify(authRecord)); - if (!authRecord) { - throw new Error(`Expected authRecord here`); - } - if (record.get("uid") !== authRecord.id) { - throw new BadRequestError(`Not authorized`); - } - const sanitized = removeEmptyKeys({ - subdomain, - version, - power, - secrets, - syncAdmin, - dev, - cname - }); - const form = new RecordUpsertForm($app, record); - form.loadData(sanitized); - form.submit(); - return c.json(200, { status: "ok" }); +//#endregion +//#region src/lib/handlers/instance/api/HandleInstanceUpdate.ts +const HandleInstanceUpdate = (c) => { + const dao = $app.dao(); + const log = mkLog(`PUT:instance`); + log(`TOP OF PUT`); + let data = new DynamicModel({ + id: "", + fields: { + subdomain: null, + power: null, + version: null, + secrets: null, + syncAdmin: null, + dev: null, + cname: null + } + }); + c.bind(data); + log(`After bind`); + data = JSON.parse(JSON.stringify(data)); + const id = c.pathParam("id"); + const { fields: { subdomain, power, version, secrets, syncAdmin, dev, cname } } = data; + log(`vars`, JSON.stringify({ + id, + subdomain, + power, + version, + secrets, + syncAdmin, + dev, + cname + })); + const record = dao.findRecordById("instances", id); + const authRecord = c.get("authRecord"); + log(`authRecord`, JSON.stringify(authRecord)); + if (!authRecord) throw new Error(`Expected authRecord here`); + if (record.get("uid") !== authRecord.id) throw new BadRequestError(`Not authorized`); + const sanitized = removeEmptyKeys({ + subdomain, + version, + power, + secrets, + syncAdmin, + dev, + cname + }); + const form = new RecordUpsertForm($app, record); + form.loadData(sanitized); + form.submit(); + return c.json(200, { status: "ok" }); }; -// src/lib/handlers/instance/bootstrap/HandleInstancesResetIdle.ts -var HandleInstancesResetIdle = (e) => { - const dao = $app.dao(); - dao.db().newQuery(`update instances set status='idle'`).execute(); +//#endregion +//#region src/lib/handlers/instance/bootstrap/HandleInstancesResetIdle.ts +const HandleInstancesResetIdle = (e) => { + const dao = $app.dao(); + dao.db().newQuery(`update instances set status='idle'`).execute(); }; -// src/lib/handlers/instance/bootstrap/HandleMigrateInstanceVersions.ts -var HandleMigrateInstanceVersions = (e) => { - const dao = $app.dao(); - const log = mkLog(`bootstrap`); - const records = dao.findRecordsByFilter(`instances`, "1=1").filter((r) => !!r); - const unrecognized = []; - records.forEach((record) => { - const v = record.getString("version").trim(); - if (versions.includes(v)) return; - const newVersion = (() => { - if (v.startsWith(`~`)) { - const [major, minor] = v.slice(1).split("."); - const newVersion2 = [major, minor, "*"].join("."); - return newVersion2; - } else { - if (v === `^0` || v === `0` || v === "1") { - return versions[0]; - } - } - return v; - })(); - if (versions.includes(newVersion)) { - record.set(`version`, newVersion); - dao.saveRecord(record); - } else { - unrecognized.push(v); - } - }); - log({ unrecognized }); +//#endregion +//#region src/lib/handlers/instance/bootstrap/HandleMigrateCnamesToDomains.ts +const HandleMigrateCnamesToDomains = (e) => { + const dao = $app.dao(); + const log = mkLog(`bootstrap:migrate-cnames`); + log(`Starting cname to domains migration`); + try { + const domainsCollection = dao.findCollectionByNameOrId("domains"); + if (!domainsCollection) { + log(`Domains collection not found, skipping migration`); + return; + } + log(`Checking for instances with cnames`); + const instancesWithCnames = dao.findRecordsByFilter("instances", "cname != NULL && cname != ''"); + if (instancesWithCnames.length === 0) { + log(`No cnames to migrate`); + return; + } + log(`Found ${instancesWithCnames.length} instances with cnames`); + const unmigrated = instancesWithCnames.filter((instance) => { + if (!instance) return false; + try { + const existingDomain = dao.findFirstRecordByFilter("domains", `instance = "${instance.getId()}"`); + return !existingDomain; + } catch (e$1) { + return true; + } + }); + if (unmigrated.length === 0) { + log(`All cnames already migrated`); + return; + } + log(`Found ${unmigrated.length} cnames to migrate`); + let migrated = 0; + unmigrated.forEach((instance) => { + if (!instance) return; + try { + const domainsCollection$1 = dao.findCollectionByNameOrId("domains"); + const domainRecord = new Record(domainsCollection$1); + domainRecord.set("instance", instance.getId()); + domainRecord.set("domain", instance.getString("cname")); + domainRecord.set("active", instance.getBool("cname_active")); + dao.saveRecord(domainRecord); + migrated++; + } catch (error$1) { + log(`Failed to migrate cname for instance ${instance.getId()}:`, error$1); + } + }); + log(`Successfully migrated ${migrated} cnames to domains table`); + } catch (error$1) { + log(`Error migrating cnames: ${error$1}`); + } }; -// src/lib/handlers/instance/bootstrap/HandleMigrateRegions.ts -var HandleMigrateRegions = (e) => { - const dao = $app.dao(); - console.log(`***Migrating regions`); - dao.db().newQuery(`update instances set region='sfo-1' where region=''`).execute(); +//#endregion +//#region src/lib/handlers/instance/bootstrap/HandleMigrateInstanceVersions.ts +const HandleMigrateInstanceVersions = (e) => { + const dao = $app.dao(); + const log = mkLog(`bootstrap`); + const records = dao.findRecordsByFilter(`instances`, "1=1").filter((r) => !!r); + const unrecognized = []; + records.forEach((record) => { + const v = record.getString("version").trim(); + if (versions.includes(v)) return; + const newVersion = (() => { + if (v.startsWith(`~`)) { + const [major, minor] = v.slice(1).split("."); + const newVersion$1 = [ + major, + minor, + "*" + ].join("."); + return newVersion$1; + } else if (v === `^0` || v === `0` || v === "1") return versions[0]; + return v; + })(); + if (versions.includes(newVersion)) { + record.set(`version`, newVersion); + dao.saveRecord(record); + } else unrecognized.push(v); + }); + log({ unrecognized }); }; -// src/lib/handlers/instance/model/HandleInstanceBeforeUpdate.ts -var HandleInstanceBeforeUpdate = (e) => { - const dao = e.dao || $app.dao(); - const log = mkLog(`instances-validate-before-update`); - const id = e.model.getId(); - const version = e.model.get("version"); - if (!versions.includes(version)) { - const msg = `[ERROR] Invalid version '${version}' for [${id}]. Version must be one of: ${versions.join( - ", " - )}`; - log(`${msg}`); - throw new BadRequestError(msg); - } - const cname = e.model.get("cname"); - if (cname.length > 0) { - const result = new DynamicModel({ - id: "" - }); - const inUse = (() => { - try { - dao.db().newQuery( - `select id from instances where cname='${cname}' and id <> '${id}'` - ).one(result); - } catch (e2) { - return false; - } - return true; - })(); - if (inUse) { - const msg = `[ERROR] [${id}] Custom domain ${cname} already in use.`; - log(`${msg}`); - throw new BadRequestError(msg); - } - } +//#endregion +//#region src/lib/handlers/instance/bootstrap/HandleMigrateRegions.ts +/** Migrate version numbers */ +const HandleMigrateRegions = (e) => { + const dao = $app.dao(); + console.log(`***Migrating regions`); + dao.db().newQuery(`update instances set region='sfo-1' where region=''`).execute(); }; -// src/lib/handlers/instance/model/HandleInstanceVersionValidation.ts -var HandleInstanceVersionValidation = (e) => { - const dao = e.dao || $app.dao(); - const version = e.model.get("version"); - if (!versions.includes(version)) { - throw new BadRequestError( - `Invalid version ${version}. Version must be one of: ${versions.join( - ", " - )}` - ); - } +//#endregion +//#region src/lib/handlers/instance/model/HandleInstanceBeforeUpdate.ts +const HandleInstanceBeforeUpdate = (e) => { + const dao = e.dao || $app.dao(); + const log = mkLog(`instances-validate-before-update`); + const id = e.model.getId(); + const version = e.model.get("version"); + if (!versions.includes(version)) { + const msg = `[ERROR] Invalid version '${version}' for [${id}]. Version must be one of: ${versions.join(", ")}`; + log(`${msg}`); + throw new BadRequestError(msg); + } + const cname = e.model.get("cname"); + if (cname.length > 0) { + const result = new DynamicModel({ id: "" }); + const inUse = (() => { + try { + dao.db().newQuery(`select id from instances where cname='${cname}' and id <> '${id}'`).one(result); + } catch (e$1) { + return false; + } + return true; + })(); + if (inUse) { + const msg = `[ERROR] [${id}] Custom domain ${cname} already in use.`; + log(`${msg}`); + throw new BadRequestError(msg); + } + } }; -// src/lib/util/mkAudit.ts -var mkAudit = (log, dao) => { - return (event, note, context) => { - log(`top of audit`); - log(`AUDIT:${event}: ${note}`, JSON.stringify({ context }, null, 2)); - dao.saveRecord( - new Record(dao.findCollectionByNameOrId("audit"), { - event, - note, - context - }) - ); - }; +//#endregion +//#region src/lib/handlers/instance/model/HandleInstanceVersionValidation.ts +const HandleInstanceVersionValidation = (e) => { + const dao = e.dao || $app.dao(); + const version = e.model.get("version"); + if (!versions.includes(version)) throw new BadRequestError(`Invalid version ${version}. Version must be one of: ${versions.join(", ")}`); }; -// src/lib/handlers/instance/model/HandleNotifyDiscordAfterCreate.ts -var HandleNotifyDiscordAfterCreate = (e) => { - const dao = e.dao || $app.dao(); - const log = mkLog(`instances:create:discord:notify`); - const audit = mkAudit(log, dao); - const webhookUrl = process.env.DISCORD_STREAM_CHANNEL_URL; - if (!webhookUrl) { - return; - } - const version = e.model.get("version"); - try { - const res = $http.send({ - url: webhookUrl, - method: "POST", - body: JSON.stringify({ - content: `Someone just created an app running PocketBase v${version}` - }), - headers: { "content-type": "application/json" }, - timeout: 5 - // in seconds - }); - } catch (e2) { - audit(`ERROR`, `Instance creation discord notify failed with ${e2}`); - } +//#endregion +//#region src/lib/util/mkAudit.ts +const mkAudit = (log, dao) => { + return (event, note, context) => { + log(`top of audit`); + log(`AUDIT:${event}: ${note}`, JSON.stringify({ context }, null, 2)); + dao.saveRecord(new Record(dao.findCollectionByNameOrId("audit"), { + event, + note, + context + })); + }; }; -// src/lib/util/mkNotifier.ts -var mkNotifier = (log, dao) => (channel, template, user_id, context = {}) => { - log({ channel, template, user_id }); - const emailTemplate = dao.findFirstRecordByData( - "message_templates", - `slug`, - template - ); - log(`got email template`, emailTemplate); - if (!emailTemplate) throw new Error(`Template ${template} not found`); - const emailNotification = new Record( - dao.findCollectionByNameOrId("notifications"), - { - user: user_id, - channel, - message_template: emailTemplate.getId(), - message_template_vars: context - } - ); - log(`built notification record`, emailNotification); - dao.saveRecord(emailNotification); +//#endregion +//#region src/lib/handlers/instance/model/HandleNotifyDiscordAfterCreate.ts +const HandleNotifyDiscordAfterCreate = (e) => { + const dao = e.dao || $app.dao(); + const log = mkLog(`instances:create:discord:notify`); + const audit = mkAudit(log, dao); + const webhookUrl = process.env.DISCORD_STREAM_CHANNEL_URL; + if (!webhookUrl) return; + const version = e.model.get("version"); + try { + const res = $http.send({ + url: webhookUrl, + method: "POST", + body: JSON.stringify({ content: `Someone just created an app running PocketBase v${version}` }), + headers: { "content-type": "application/json" }, + timeout: 5 + }); + } catch (e$1) { + audit(`ERROR`, `Instance creation discord notify failed with ${e$1}`); + } }; -// src/lib/handlers/lemon/api/HandleLemonSqueezySale.ts -var HandleLemonSqueezySale = (c) => { - const dao = $app.dao(); - const log = mkLog(`ls`); - const audit = mkAudit(log, dao); - const context = {}; - log(`Top of ls`); - try { - context.secret = process.env.LS_WEBHOOK_SECRET; - if (!context.secret) { - throw new Error(`No secret`); - } - log(`Secret`, context.secret); - context.raw = readerToString(c.request().body); - context.body_hash = $security.hs256(context.raw, context.secret); - log(`Body hash`, context.body_hash); - context.xsignature_header = c.request().header.get("X-Signature"); - log(`Signature`, context.xsignature_header); - if (context.xsignature_header == void 0 || !$security.equal(context.body_hash, context.xsignature_header)) { - throw new BadRequestError(`Invalid signature`); - } - log(`Signature verified`); - context.data = JSON.parse(context.raw); - log(`payload`, JSON.stringify(context.data, null, 2)); - context.type = context.data?.data?.type; - if (!context.type) { - throw new Error(`No type`); - } else { - log(`type ok`, context.type); - } - context.event_name = context.data?.meta?.event_name; - if (!context.event_name) { - throw new Error(`No event name`); - } else { - log(`event name ok`, context.event_name); - } - context.user_id = context.data?.meta?.custom_data?.user_id; - if (!context.user_id) { - throw new Error(`No user ID`); - } else { - log(`user ID ok`, context.user_id); - } - context.product_id = context.data?.data?.attributes?.first_order_item?.product_id || context.data?.data?.attributes?.product_id || 0; - if (!context.product_id) { - throw new Error(`No product ID`); - } else { - log(`product ID ok`, context.product_id); - } - context.product_name = context.data?.data?.attributes?.first_order_item?.product_name || context.data?.data?.attributes?.product_name || ""; - log(`product name ok`, context.product_name); - context.variant_id = context.data?.data?.attributes?.first_order_item?.variant_id || context.data?.data?.attributes?.variant_id || 0; - if (!context.variant_id) { - throw new Error(`No variant ID`); - } else { - log(`variant ID ok`, context.variant_id); - } - context.variant_name = context.data?.data?.attributes?.first_order_item?.variant_name || context.data?.data?.attributes?.variant_name || ""; - log(`variant name ok`, context.variant_name); - context.quantity = context.data?.data?.attributes?.first_order_item?.quantity || 0; - log(`quantity ok`, context.quantity); - const FLOUNDER_ANNUAL_PV_ID = `367781-200790`; - const FLOUNDER_LIFETIME_PV_ID = `306534-441845`; - const PRO_MONTHLY_PV_ID = `159790-200788`; - const PRO_ANNUAL_PV_ID = `159791-200789`; - const FOUNDER_ANNUAL_PV_ID = `159792-200790`; - const PAYWALL_INSTANCE_MONTHLY_PV_ID = `424532-651625`; - const PAYWALL_PRO_MONTHLY_PV_ID = `424532-651629`; - const PAYWALL_PRO_ANNUAL_PV_ID = `424532-651634`; - const PAYWALL_FLOUNDER_PV_ID = `424532-651627`; - const pv_id = `${context.product_id}-${context.variant_id}`; - if (![ - FLOUNDER_ANNUAL_PV_ID, - FLOUNDER_LIFETIME_PV_ID, - PRO_MONTHLY_PV_ID, - PRO_ANNUAL_PV_ID, - FOUNDER_ANNUAL_PV_ID, - PAYWALL_INSTANCE_MONTHLY_PV_ID, - PAYWALL_PRO_MONTHLY_PV_ID, - PAYWALL_PRO_ANNUAL_PV_ID, - PAYWALL_FLOUNDER_PV_ID - ].includes(pv_id)) { - throw new Error(`Product and variant not found: ${pv_id}`); - } - const userRec = (() => { - try { - return dao.findFirstRecordByData("users", "id", context.user_id); - } catch (e) { - throw new Error(`User ${context.user_id} not found`); - } - })(); - log(`user record ok`, userRec); - const event_name_map = { - order_created: () => { - signup_finalizer(); - }, - order_refunded: () => { - signup_canceller(); - }, - subscription_expired: () => { - signup_canceller(); - }, - subscription_payment_refunded: () => { - signup_canceller(); - } - }; - const event_handler = event_name_map[context.event_name]; - if (!event_handler) { - throw new Error(`Unsupported event: ${context.event_name}`); - } else { - log(`event handler ok`, event_handler); - } - const product_handler_map = { - // Founder's annual - [FOUNDER_ANNUAL_PV_ID]: () => { - userRec.set(`subscription`, `founder`); - userRec.set(`subscription_interval`, `year`); - userRec.set(`subscription_quantity`, 2147483647); - }, - // Pro annual - [PRO_ANNUAL_PV_ID]: () => { - userRec.set(`subscription`, `premium`); - userRec.set(`subscription_interval`, `year`); - userRec.set(`subscription_quantity`, 250); - }, - // Pro monthly - [PRO_MONTHLY_PV_ID]: () => { - userRec.set(`subscription`, `premium`); - userRec.set(`subscription_interval`, `month`); - userRec.set(`subscription_quantity`, 250); - }, - // Flounder lifetime - [FLOUNDER_LIFETIME_PV_ID]: () => { - userRec.set(`subscription`, `flounder`); - userRec.set(`subscription_interval`, `life`); - userRec.set(`subscription_quantity`, 250); - }, - // Flounder annual - [FLOUNDER_ANNUAL_PV_ID]: () => { - userRec.set(`subscription`, `flounder`); - userRec.set(`subscription_interval`, `year`); - userRec.set(`subscription_quantity`, 250); - }, - // Paywall instance - [PAYWALL_INSTANCE_MONTHLY_PV_ID]: () => { - userRec.set(`subscription`, `premium`); - userRec.set(`subscription_interval`, `month`); - userRec.set(`subscription_quantity`, context.quantity); - }, - // Paywall pro monthly - [PAYWALL_PRO_MONTHLY_PV_ID]: () => { - userRec.set(`subscription`, `premium`); - userRec.set(`subscription_interval`, `month`); - userRec.set(`subscription_quantity`, 250); - }, - // Paywall pro annual - [PAYWALL_PRO_ANNUAL_PV_ID]: () => { - userRec.set(`subscription`, `premium`); - userRec.set(`subscription_interval`, `year`); - userRec.set(`subscription_quantity`, 250); - }, - // Paywall flounder - [PAYWALL_FLOUNDER_PV_ID]: () => { - userRec.set(`subscription`, `flounder`); - userRec.set(`subscription_interval`, `life`); - userRec.set(`subscription_quantity`, 250); - } - }; - const product_handler = product_handler_map[pv_id]; - if (!product_handler) { - throw new Error(`No product handler for ${pv_id}`); - } else { - log(`product handler ok`, pv_id); - } - const signup_finalizer = () => { - product_handler(); - dao.saveRecord(userRec); - log(`saved user`); - const notify = mkNotifier(log, dao); - const { user_id } = context; - if (!user_id) { - throw new Error(`User ID expected here`); - } - notify(`lemonbot`, `lemon_order_discord`, user_id, context); - log(`saved discord notice`); - audit(`LS`, `Signup processed.`, context); - }; - const signup_canceller = () => { - userRec.set(`subscription`, `free`); - userRec.set(`subscription_quantity`, 0); - userRec.set(`subscription_interval`, ``); - dao.saveRecord(userRec); - log(`saved user`); - audit(`LS`, `Signup cancelled.`, context); - }; - event_handler(); - return c.json(200, { status: "ok" }); - } catch (e) { - audit(`LS_ERR`, `${e}`, context); - return c.json(500, { status: `error`, error: e.message }); - } +//#endregion +//#region src/lib/util/mkNotifier.ts +const mkNotifier = (log, dao) => (channel, template, user_id, context = {}) => { + log({ + channel, + template, + user_id + }); + const emailTemplate = dao.findFirstRecordByData("message_templates", `slug`, template); + log(`got email template`, emailTemplate); + if (!emailTemplate) throw new Error(`Template ${template} not found`); + const emailNotification = new Record(dao.findCollectionByNameOrId("notifications"), { + user: user_id, + channel, + message_template: emailTemplate.getId(), + message_template_vars: context + }); + log(`built notification record`, emailNotification); + dao.saveRecord(emailNotification); }; -// src/lib/handlers/mail/api/HandleMailSend.ts -var HandleMailSend = (c) => { - const log = mkLog(`mail`); - let data = new DynamicModel({ - to: "", - subject: "", - body: "" - }); - log(`before bind`); - c.bind(data); - log(`after bind`); - data = JSON.parse(JSON.stringify(data)); - log(`bind parsed`, JSON.stringify(data)); - const { to, subject, body } = data; - const email = new MailerMessage({ - from: { - address: $app.settings().meta.senderAddress, - name: $app.settings().meta.senderName - }, - to: [{ address: to }], - bcc: [process.env.TEST_EMAIL].filter((e) => !!e).map((e) => ({ address: e })), - subject, - html: body - }); - $app.newMailClient().send(email); - const msg = `Sent to ${to}`; - log(msg); - return c.json(200, { status: "ok" }); +//#endregion +//#region src/lib/handlers/lemon/api/HandleLemonSqueezySale.ts +const HandleLemonSqueezySale = (c) => { + const dao = $app.dao(); + const log = mkLog(`ls`); + const audit = mkAudit(log, dao); + const context = {}; + log(`Top of ls`); + try { + context.secret = process.env.LS_WEBHOOK_SECRET; + if (!context.secret) throw new Error(`No secret`); + log(`Secret`, context.secret); + context.raw = readerToString(c.request().body); + context.body_hash = $security.hs256(context.raw, context.secret); + log(`Body hash`, context.body_hash); + context.xsignature_header = c.request().header.get("X-Signature"); + log(`Signature`, context.xsignature_header); + if (context.xsignature_header == void 0 || !$security.equal(context.body_hash, context.xsignature_header)) throw new BadRequestError(`Invalid signature`); + log(`Signature verified`); + context.data = JSON.parse(context.raw); + log(`payload`, JSON.stringify(context.data, null, 2)); + context.type = context.data?.data?.type; + if (!context.type) throw new Error(`No type`); + else log(`type ok`, context.type); + context.event_name = context.data?.meta?.event_name; + if (!context.event_name) throw new Error(`No event name`); + else log(`event name ok`, context.event_name); + context.user_id = context.data?.meta?.custom_data?.user_id; + if (!context.user_id) throw new Error(`No user ID`); + else log(`user ID ok`, context.user_id); + context.product_id = context.data?.data?.attributes?.first_order_item?.product_id || context.data?.data?.attributes?.product_id || 0; + if (!context.product_id) throw new Error(`No product ID`); + else log(`product ID ok`, context.product_id); + context.product_name = context.data?.data?.attributes?.first_order_item?.product_name || context.data?.data?.attributes?.product_name || ""; + log(`product name ok`, context.product_name); + context.variant_id = context.data?.data?.attributes?.first_order_item?.variant_id || context.data?.data?.attributes?.variant_id || 0; + if (!context.variant_id) throw new Error(`No variant ID`); + else log(`variant ID ok`, context.variant_id); + context.variant_name = context.data?.data?.attributes?.first_order_item?.variant_name || context.data?.data?.attributes?.variant_name || ""; + log(`variant name ok`, context.variant_name); + context.quantity = context.data?.data?.attributes?.first_order_item?.quantity || 0; + log(`quantity ok`, context.quantity); + const FLOUNDER_ANNUAL_PV_ID = `367781-200790`; + const FLOUNDER_LIFETIME_PV_ID = `306534-441845`; + const PRO_MONTHLY_PV_ID = `159790-200788`; + const PRO_ANNUAL_PV_ID = `159791-200789`; + const FOUNDER_ANNUAL_PV_ID = `159792-200790`; + const PAYWALL_INSTANCE_MONTHLY_PV_ID = `424532-651625`; + const PAYWALL_PRO_MONTHLY_PV_ID = `424532-651629`; + const PAYWALL_PRO_ANNUAL_PV_ID = `424532-651634`; + const PAYWALL_FLOUNDER_PV_ID = `424532-651627`; + const pv_id = `${context.product_id}-${context.variant_id}`; + if (![ + FLOUNDER_ANNUAL_PV_ID, + FLOUNDER_LIFETIME_PV_ID, + PRO_MONTHLY_PV_ID, + PRO_ANNUAL_PV_ID, + FOUNDER_ANNUAL_PV_ID, + PAYWALL_INSTANCE_MONTHLY_PV_ID, + PAYWALL_PRO_MONTHLY_PV_ID, + PAYWALL_PRO_ANNUAL_PV_ID, + PAYWALL_FLOUNDER_PV_ID + ].includes(pv_id)) throw new Error(`Product and variant not found: ${pv_id}`); + const userRec = (() => { + try { + return dao.findFirstRecordByData("users", "id", context.user_id); + } catch (e) { + throw new Error(`User ${context.user_id} not found`); + } + })(); + log(`user record ok`, userRec); + const event_name_map = { + order_created: () => { + signup_finalizer(); + }, + order_refunded: () => { + signup_canceller(); + }, + subscription_expired: () => { + signup_canceller(); + }, + subscription_payment_refunded: () => { + signup_canceller(); + } + }; + const event_handler = event_name_map[context.event_name]; + if (!event_handler) throw new Error(`Unsupported event: ${context.event_name}`); + else log(`event handler ok`, event_handler); + const product_handler_map = { + [FOUNDER_ANNUAL_PV_ID]: () => { + userRec.set(`subscription`, `founder`); + userRec.set(`subscription_interval`, `year`); + userRec.set(`subscription_quantity`, 2147483647); + }, + [PRO_ANNUAL_PV_ID]: () => { + userRec.set(`subscription`, `premium`); + userRec.set(`subscription_interval`, `year`); + userRec.set(`subscription_quantity`, 250); + }, + [PRO_MONTHLY_PV_ID]: () => { + userRec.set(`subscription`, `premium`); + userRec.set(`subscription_interval`, `month`); + userRec.set(`subscription_quantity`, 250); + }, + [FLOUNDER_LIFETIME_PV_ID]: () => { + userRec.set(`subscription`, `flounder`); + userRec.set(`subscription_interval`, `life`); + userRec.set(`subscription_quantity`, 250); + }, + [FLOUNDER_ANNUAL_PV_ID]: () => { + userRec.set(`subscription`, `flounder`); + userRec.set(`subscription_interval`, `year`); + userRec.set(`subscription_quantity`, 250); + }, + [PAYWALL_INSTANCE_MONTHLY_PV_ID]: () => { + userRec.set(`subscription`, `premium`); + userRec.set(`subscription_interval`, `month`); + userRec.set(`subscription_quantity`, context.quantity); + }, + [PAYWALL_PRO_MONTHLY_PV_ID]: () => { + userRec.set(`subscription`, `premium`); + userRec.set(`subscription_interval`, `month`); + userRec.set(`subscription_quantity`, 250); + }, + [PAYWALL_PRO_ANNUAL_PV_ID]: () => { + userRec.set(`subscription`, `premium`); + userRec.set(`subscription_interval`, `year`); + userRec.set(`subscription_quantity`, 250); + }, + [PAYWALL_FLOUNDER_PV_ID]: () => { + userRec.set(`subscription`, `flounder`); + userRec.set(`subscription_interval`, `life`); + userRec.set(`subscription_quantity`, 250); + } + }; + const product_handler = product_handler_map[pv_id]; + if (!product_handler) throw new Error(`No product handler for ${pv_id}`); + else log(`product handler ok`, pv_id); + const signup_finalizer = () => { + product_handler(); + dao.saveRecord(userRec); + log(`saved user`); + const notify = mkNotifier(log, dao); + const { user_id } = context; + if (!user_id) throw new Error(`User ID expected here`); + notify(`lemonbot`, `lemon_order_discord`, user_id, context); + log(`saved discord notice`); + audit(`LS`, `Signup processed.`, context); + }; + const signup_canceller = () => { + userRec.set(`subscription`, `free`); + userRec.set(`subscription_quantity`, 0); + userRec.set(`subscription_interval`, ``); + dao.saveRecord(userRec); + log(`saved user`); + audit(`LS`, `Signup cancelled.`, context); + }; + event_handler(); + return c.json(200, { status: "ok" }); + } catch (e) { + audit(`LS_ERR`, `${e}`, context); + return c.json(500, { + status: `error`, + error: e.message + }); + } }; -// src/lib/handlers/meta/boot/HandleMetaUpdateAtBoot.ts -var HandleMetaUpdateAtBoot = (c) => { - const log = mkLog("HandleMetaUpdateAtBoot"); - log(`At top of HandleMetaUpdateAtBoot`); - log(`***app URL`, process.env.APP_URL); - const form = new SettingsUpsertForm($app); - form.meta = { - ...$app.settings().meta, - appUrl: process.env.APP_URL || $app.settings().meta.appUrl, - verificationTemplate: { - ...$app.settings().meta.verificationTemplate, - actionUrl: `{APP_URL}/login/confirm-account/{TOKEN}` - }, - resetPasswordTemplate: { - ...$app.settings().meta.resetPasswordTemplate, - actionUrl: `{APP_URL}/login/password-reset/confirm/{TOKEN}` - }, - confirmEmailChangeTemplate: { - ...$app.settings().meta.confirmEmailChangeTemplate, - actionUrl: `{APP_URL}/login/confirm-email-change/{TOKEN}` - } - }; - log(`Saving form`); - form.submit(); - log(`Saved form`); +//#endregion +//#region src/lib/handlers/mail/api/HandleMailSend.ts +const HandleMailSend = (c) => { + const log = mkLog(`mail`); + let data = new DynamicModel({ + to: "", + subject: "", + body: "" + }); + log(`before bind`); + c.bind(data); + log(`after bind`); + data = JSON.parse(JSON.stringify(data)); + log(`bind parsed`, JSON.stringify(data)); + const { to, subject, body } = data; + const email = new MailerMessage({ + from: { + address: $app.settings().meta.senderAddress, + name: $app.settings().meta.senderName + }, + to: [{ address: to }], + bcc: [process.env.TEST_EMAIL].filter((e) => !!e).map((e) => ({ address: e })), + subject, + html: body + }); + $app.newMailClient().send(email); + const msg = `Sent to ${to}`; + log(msg); + return c.json(200, { status: "ok" }); }; -// src/lib/handlers/mirror/api/HandleMirrorData.ts -var HandleMirrorData = (c) => { - const users = $app.dao().findRecordsByExpr("verified_users", $dbx.exp("1=1")); - const instances = $app.dao().findRecordsByExpr( - "instances", - $dbx.exp("instances.uid in (select id from verified_users)") - ); - return c.json(200, { users, instances }); +//#endregion +//#region src/lib/handlers/meta/boot/HandleMetaUpdateAtBoot.ts +const HandleMetaUpdateAtBoot = (c) => { + const log = mkLog("HandleMetaUpdateAtBoot"); + log(`At top of HandleMetaUpdateAtBoot`); + log(`***app URL`, process.env.APP_URL); + const form = new SettingsUpsertForm($app); + form.meta = { + ...$app.settings().meta, + appUrl: process.env.APP_URL || $app.settings().meta.appUrl, + verificationTemplate: { + ...$app.settings().meta.verificationTemplate, + actionUrl: `{APP_URL}/login/confirm-account/{TOKEN}` + }, + resetPasswordTemplate: { + ...$app.settings().meta.resetPasswordTemplate, + actionUrl: `{APP_URL}/login/password-reset/confirm/{TOKEN}` + }, + confirmEmailChangeTemplate: { + ...$app.settings().meta.confirmEmailChangeTemplate, + actionUrl: `{APP_URL}/login/confirm-email-change/{TOKEN}` + } + }; + log(`Saving form`); + form.submit(); + log(`Saved form`); }; -// src/lib/util/mkNotificationProcessor.ts -var mkNotificationProcessor = (log, dao, test = false) => (notificationRec) => { - log({ notificationRec }); - const channel = notificationRec.getString(`channel`); - dao.expandRecord(notificationRec, ["message_template", "user"]); - const messageTemplateRec = notificationRec.expandedOne("message_template"); - if (!messageTemplateRec) { - throw new Error(`Missing message template`); - } - const userRec = notificationRec.expandedOne("user"); - if (!userRec) { - throw new Error(`Missing user record`); - } - const vars = JSON.parse(notificationRec.getString(`message_template_vars`)); - const to = userRec.email(); - const subject = interpolateString( - messageTemplateRec.getString(`subject`), - vars - ); - const html = interpolateString( - messageTemplateRec.getString(`body_html`), - vars - ); - log({ channel, messageTemplateRec, userRec, vars, to, subject, html }); - switch (channel) { - case `email`: - const msgArgs = { - from: { - address: $app.settings().meta.senderAddress, - name: $app.settings().meta.senderName - }, - to: [{ address: to }], - bcc: [{ address: `pockethost+notifications@benallfree.com` }], - subject, - html - }; - if (test) { - msgArgs.to = [{ address: `ben@benallfree.com` }]; - msgArgs.subject = `***TEST ${to} *** ${msgArgs.subject}`; - } - log({ msgArgs }); - const msg = new MailerMessage(msgArgs); - $app.newMailClient().send(msg); - log(`email sent`); - break; - case `lemonbot`: - const url = test ? process.env.DISCORD_TEST_CHANNEL_URL : process.env.DISCORD_STREAM_CHANNEL_URL; - if (url) { - const params = { - url, - method: "POST", - body: JSON.stringify({ - content: subject - }), - headers: { "content-type": "application/json" }, - timeout: 5 - // in seconds - }; - log(`sending discord message`, params); - const res = $http.send(params); - log(`discord sent`, res); - } - break; - default: - throw new Error(`Unsupported channel: ${channel}`); - } - if (!test) { - notificationRec.set(`delivered`, new DateTime()); - dao.saveRecord(notificationRec); - } +//#endregion +//#region src/lib/handlers/mirror/api/HandleMirrorData.ts +const HandleMirrorData = (c) => { + const users = $app.dao().findRecordsByExpr("verified_users", $dbx.exp("1=1")); + const instances = $app.dao().findRecordsByExpr("instances", $dbx.exp("instances.uid in (select id from verified_users)")); + return c.json(200, { + users, + instances + }); }; -// src/lib/handlers/notify/api/HandleProcessSingleNotification.ts -var HandleProcessSingleNotification = (c) => { - const log = mkLog(`process_single_notification`); - log(`start`); - const dao = $app.dao(); - const processNotification = mkNotificationProcessor( - log, - dao, - !!c.queryParam(`test`) - ); - try { - const notification = dao.findFirstRecordByData( - `notifications`, - `delivered`, - `` - ); - if (!notification) { - return c.json(200, `No notifications to send`); - } - processNotification(notification); - } catch (e) { - c.json(500, `${e}`); - } - return c.json(200, { status: "ok" }); +//#endregion +//#region src/lib/util/mkNotificationProcessor.ts +const mkNotificationProcessor = (log, dao, test = false) => (notificationRec) => { + log({ notificationRec }); + const channel = notificationRec.getString(`channel`); + dao.expandRecord(notificationRec, ["message_template", "user"]); + const messageTemplateRec = notificationRec.expandedOne("message_template"); + if (!messageTemplateRec) throw new Error(`Missing message template`); + const userRec = notificationRec.expandedOne("user"); + if (!userRec) throw new Error(`Missing user record`); + const vars = JSON.parse(notificationRec.getString(`message_template_vars`)); + const to = userRec.email(); + const subject = interpolateString(messageTemplateRec.getString(`subject`), vars); + const html = interpolateString(messageTemplateRec.getString(`body_html`), vars); + log({ + channel, + messageTemplateRec, + userRec, + vars, + to, + subject, + html + }); + switch (channel) { + case `email`: + /** @type {Partial} */ + const msgArgs = { + from: { + address: $app.settings().meta.senderAddress, + name: $app.settings().meta.senderName + }, + to: [{ address: to }], + bcc: [{ address: `pockethost+notifications@benallfree.com` }], + subject, + html + }; + if (test) { + msgArgs.to = [{ address: `ben@benallfree.com` }]; + msgArgs.subject = `***TEST ${to} *** ${msgArgs.subject}`; + } + log({ msgArgs }); + const msg = new MailerMessage(msgArgs); + $app.newMailClient().send(msg); + log(`email sent`); + break; + case `lemonbot`: + const url = test ? process.env.DISCORD_TEST_CHANNEL_URL : process.env.DISCORD_STREAM_CHANNEL_URL; + if (url) { + const params = { + url, + method: "POST", + body: JSON.stringify({ content: subject }), + headers: { "content-type": "application/json" }, + timeout: 5 + }; + log(`sending discord message`, params); + const res = $http.send(params); + log(`discord sent`, res); + } + break; + default: throw new Error(`Unsupported channel: ${channel}`); + } + if (!test) { + notificationRec.set(`delivered`, new DateTime()); + dao.saveRecord(notificationRec); + } }; -// src/lib/handlers/notify/model/HandleProcessNotification.ts -var HandleProcessNotification = (e) => { - const dao = e.dao || $app.dao(); - const log = mkLog(`notification:sendImmediately`); - const audit = mkAudit(log, dao); - const processNotification = mkNotificationProcessor(log, dao, false); - const notificationRec = e.model; - log({ notificationRec }); - try { - dao.expandRecord(notificationRec, ["message_template"]); - const messageTemplateRec = notificationRec.expandedOne(`message_template`); - if (!messageTemplateRec) { - throw new Error(`Missing message template`); - } - processNotification(notificationRec); - } catch (e2) { - audit(`ERROR`, `${e2}`, { - notification: notificationRec.getId() - }); - } +//#endregion +//#region src/lib/handlers/notify/api/HandleProcessSingleNotification.ts +const HandleProcessSingleNotification = (c) => { + const log = mkLog(`process_single_notification`); + log(`start`); + const dao = $app.dao(); + const processNotification = mkNotificationProcessor(log, dao, !!c.queryParam(`test`)); + try { + const notification = dao.findFirstRecordByData(`notifications`, `delivered`, ``); + if (!notification) return c.json(200, `No notifications to send`); + processNotification(notification); + } catch (e) { + c.json(500, `${e}`); + } + return c.json(200, { status: "ok" }); }; -// src/lib/handlers/notify/model/HandleUserWelcomeMessage.ts -var HandleUserWelcomeMessage = (e) => { - const dao = e.dao || $app.dao(); - const newModel = ( - /** @type {models.Record} */ - e.model - ); - const oldModel = newModel.originalCopy(); - const log = mkLog(`user-welcome-msg`); - const notify = mkNotifier(log, dao); - const audit = mkAudit(log, dao); - try { - log({ newModel, oldModel }); - const isVerified = newModel.getBool("verified"); - if (!isVerified) return; - if (isVerified === oldModel.getBool(`verified`)) return; - log(`user just became verified`); - const uid = newModel.getId(); - notify(`email`, `welcome`, uid); - newModel.set(`welcome`, new DateTime()); - } catch (e2) { - audit(`ERROR`, `${e2}`, { user: newModel.getId() }); - } +//#endregion +//#region src/lib/handlers/notify/model/HandleProcessNotification.ts +const HandleProcessNotification = (e) => { + const dao = e.dao || $app.dao(); + const log = mkLog(`notification:sendImmediately`); + const audit = mkAudit(log, dao); + const processNotification = mkNotificationProcessor(log, dao, false); + const notificationRec = e.model; + log({ notificationRec }); + try { + dao.expandRecord(notificationRec, ["message_template"]); + const messageTemplateRec = notificationRec.expandedOne(`message_template`); + if (!messageTemplateRec) throw new Error(`Missing message template`); + processNotification(notificationRec); + } catch (e$1) { + audit(`ERROR`, `${e$1}`, { notification: notificationRec.getId() }); + } }; -// src/lib/handlers/signup/error.ts -var error = (fieldName, slug, description, extra) => new ApiError(500, description, { - [fieldName]: new ValidationError(slug, description), - ...extra +//#endregion +//#region src/lib/handlers/notify/model/HandleUserWelcomeMessage.ts +const HandleUserWelcomeMessage = (e) => { + const dao = e.dao || $app.dao(); + const newModel = e.model; + const oldModel = newModel.originalCopy(); + const log = mkLog(`user-welcome-msg`); + const notify = mkNotifier(log, dao); + const audit = mkAudit(log, dao); + try { + log({ + newModel, + oldModel + }); + const isVerified = newModel.getBool("verified"); + if (!isVerified) return; + if (isVerified === oldModel.getBool(`verified`)) return; + log(`user just became verified`); + const uid = newModel.getId(); + notify(`email`, `welcome`, uid); + newModel.set(`welcome`, new DateTime()); + } catch (e$1) { + audit(`ERROR`, `${e$1}`, { user: newModel.getId() }); + } +}; + +//#endregion +//#region src/lib/handlers/signup/error.ts +const error = (fieldName, slug, description, extra) => new ApiError(500, description, { + [fieldName]: new ValidationError(slug, description), + ...extra }); -// src/lib/handlers/signup/isAvailable.ts -var isAvailable = (slug) => { - try { - const record = $app.dao().findFirstRecordByData("instances", "subdomain", slug); - return false; - } catch { - return true; - } +//#endregion +//#region src/lib/handlers/signup/isAvailable.ts +const isAvailable = (slug) => { + try { + const record = $app.dao().findFirstRecordByData("instances", "subdomain", slug); + return false; + } catch { + return true; + } }; -// src/lib/handlers/signup/random-words/wordList.ts -var wordList = [ - "ability", - "able", - "aboard", - "about", - "above", - "accept", - "accident", - "according", - "account", - "accurate", - "acres", - "across", - "act", - "action", - "active", - "activity", - "actual", - "actually", - "add", - "addition", - "additional", - "adjective", - "adult", - "adventure", - "advice", - "affect", - "afraid", - "after", - "afternoon", - "again", - "against", - "age", - "ago", - "agree", - "ahead", - "aid", - "air", - "airplane", - "alike", - "alive", - "all", - "allow", - "almost", - "alone", - "along", - "aloud", - "alphabet", - "already", - "also", - "although", - "am", - "among", - "amount", - "ancient", - "angle", - "angry", - "animal", - "announced", - "another", - "answer", - "ants", - "any", - "anybody", - "anyone", - "anything", - "anyway", - "anywhere", - "apart", - "apartment", - "appearance", - "apple", - "applied", - "appropriate", - "are", - "area", - "arm", - "army", - "around", - "arrange", - "arrangement", - "arrive", - "arrow", - "art", - "article", - "as", - "aside", - "ask", - "asleep", - "at", - "ate", - "atmosphere", - "atom", - "atomic", - "attached", - "attack", - "attempt", - "attention", - "audience", - "author", - "automobile", - "available", - "average", - "avoid", - "aware", - "away", - "baby", - "back", - "bad", - "badly", - "bag", - "balance", - "ball", - "balloon", - "band", - "bank", - "bar", - "bare", - "bark", - "barn", - "base", - "baseball", - "basic", - "basis", - "basket", - "bat", - "battle", - "be", - "bean", - "bear", - "beat", - "beautiful", - "beauty", - "became", - "because", - "become", - "becoming", - "bee", - "been", - "before", - "began", - "beginning", - "begun", - "behavior", - "behind", - "being", - "believed", - "bell", - "belong", - "below", - "belt", - "bend", - "beneath", - "bent", - "beside", - "best", - "bet", - "better", - "between", - "beyond", - "bicycle", - "bigger", - "biggest", - "bill", - "birds", - "birth", - "birthday", - "bit", - "bite", - "black", - "blank", - "blanket", - "blew", - "blind", - "block", - "blood", - "blow", - "blue", - "board", - "boat", - "body", - "bone", - "book", - "border", - "born", - "both", - "bottle", - "bottom", - "bound", - "bow", - "bowl", - "box", - "boy", - "brain", - "branch", - "brass", - "brave", - "bread", - "break", - "breakfast", - "breath", - "breathe", - "breathing", - "breeze", - "brick", - "bridge", - "brief", - "bright", - "bring", - "broad", - "broke", - "broken", - "brother", - "brought", - "brown", - "brush", - "buffalo", - "build", - "building", - "built", - "buried", - "burn", - "burst", - "bus", - "bush", - "business", - "busy", - "but", - "butter", - "buy", - "by", - "cabin", - "cage", - "cake", - "call", - "calm", - "came", - "camera", - "camp", - "can", - "canal", - "cannot", - "cap", - "capital", - "captain", - "captured", - "car", - "carbon", - "card", - "care", - "careful", - "carefully", - "carried", - "carry", - "case", - "cast", - "castle", - "cat", - "catch", - "cattle", - "caught", - "cause", - "cave", - "cell", - "cent", - "center", - "central", - "century", - "certain", - "certainly", - "chain", - "chair", - "chamber", - "chance", - "change", - "changing", - "chapter", - "character", - "characteristic", - "charge", - "chart", - "check", - "cheese", - "chemical", - "chest", - "chicken", - "chief", - "child", - "children", - "choice", - "choose", - "chose", - "chosen", - "church", - "circle", - "circus", - "citizen", - "city", - "class", - "classroom", - "claws", - "clay", - "clean", - "clear", - "clearly", - "climate", - "climb", - "clock", - "close", - "closely", - "closer", - "cloth", - "clothes", - "clothing", - "cloud", - "club", - "coach", - "coal", - "coast", - "coat", - "coffee", - "cold", - "collect", - "college", - "colony", - "color", - "column", - "combination", - "combine", - "come", - "comfortable", - "coming", - "command", - "common", - "community", - "company", - "compare", - "compass", - "complete", - "completely", - "complex", - "composed", - "composition", - "compound", - "concerned", - "condition", - "congress", - "connected", - "consider", - "consist", - "consonant", - "constantly", - "construction", - "contain", - "continent", - "continued", - "contrast", - "control", - "conversation", - "cook", - "cookies", - "cool", - "copper", - "copy", - "corn", - "corner", - "correct", - "correctly", - "cost", - "cotton", - "could", - "count", - "country", - "couple", - "courage", - "course", - "court", - "cover", - "cow", - "cowboy", - "crack", - "cream", - "create", - "creature", - "crew", - "crop", - "cross", - "crowd", - "cry", - "cup", - "curious", - "current", - "curve", - "customs", - "cut", - "cutting", - "daily", - "damage", - "dance", - "danger", - "dangerous", - "dark", - "darkness", - "date", - "daughter", - "dawn", - "day", - "dead", - "deal", - "dear", - "death", - "decide", - "declared", - "deep", - "deeply", - "deer", - "definition", - "degree", - "depend", - "depth", - "describe", - "desert", - "design", - "desk", - "detail", - "determine", - "develop", - "development", - "diagram", - "diameter", - "did", - "die", - "differ", - "difference", - "different", - "difficult", - "difficulty", - "dig", - "dinner", - "direct", - "direction", - "directly", - "dirt", - "dirty", - "disappear", - "discover", - "discovery", - "discuss", - "discussion", - "disease", - "dish", - "distance", - "distant", - "divide", - "division", - "do", - "doctor", - "does", - "dog", - "doing", - "doll", - "dollar", - "done", - "donkey", - "door", - "dot", - "double", - "doubt", - "down", - "dozen", - "draw", - "drawn", - "dream", - "dress", - "drew", - "dried", - "drink", - "drive", - "driven", - "driver", - "driving", - "drop", - "dropped", - "drove", - "dry", - "duck", - "due", - "dug", - "dull", - "during", - "dust", - "duty", - "each", - "eager", - "ear", - "earlier", - "early", - "earn", - "earth", - "easier", - "easily", - "east", - "easy", - "eat", - "eaten", - "edge", - "education", - "effect", - "effort", - "egg", - "eight", - "either", - "electric", - "electricity", - "element", - "elephant", - "eleven", - "else", - "empty", - "end", - "enemy", - "energy", - "engine", - "engineer", - "enjoy", - "enough", - "enter", - "entire", - "entirely", - "environment", - "equal", - "equally", - "equator", - "equipment", - "escape", - "especially", - "essential", - "establish", - "even", - "evening", - "event", - "eventually", - "ever", - "every", - "everybody", - "everyone", - "everything", - "everywhere", - "evidence", - "exact", - "exactly", - "examine", - "example", - "excellent", - "except", - "exchange", - "excited", - "excitement", - "exciting", - "exclaimed", - "exercise", - "exist", - "expect", - "experience", - "experiment", - "explain", - "explanation", - "explore", - "express", - "expression", - "extra", - "eye", - "face", - "facing", - "fact", - "factor", - "factory", - "failed", - "fair", - "fairly", - "fall", - "fallen", - "familiar", - "family", - "famous", - "far", - "farm", - "farmer", - "farther", - "fast", - "fastened", - "faster", - "fat", - "father", - "favorite", - "fear", - "feathers", - "feature", - "fed", - "feed", - "feel", - "feet", - "fell", - "fellow", - "felt", - "fence", - "few", - "fewer", - "field", - "fierce", - "fifteen", - "fifth", - "fifty", - "fight", - "fighting", - "figure", - "fill", - "film", - "final", - "finally", - "find", - "fine", - "finest", - "finger", - "finish", - "fire", - "fireplace", - "firm", - "first", - "fish", - "five", - "fix", - "flag", - "flame", - "flat", - "flew", - "flies", - "flight", - "floating", - "floor", - "flow", - "flower", - "fly", - "fog", - "folks", - "follow", - "food", - "foot", - "football", - "for", - "force", - "foreign", - "forest", - "forget", - "forgot", - "forgotten", - "form", - "former", - "fort", - "forth", - "forty", - "forward", - "fought", - "found", - "four", - "fourth", - "fox", - "frame", - "free", - "freedom", - "frequently", - "fresh", - "friend", - "friendly", - "frighten", - "frog", - "from", - "front", - "frozen", - "fruit", - "fuel", - "full", - "fully", - "fun", - "function", - "funny", - "fur", - "furniture", - "further", - "future", - "gain", - "game", - "garage", - "garden", - "gas", - "gasoline", - "gate", - "gather", - "gave", - "general", - "generally", - "gentle", - "gently", - "get", - "getting", - "giant", - "gift", - "girl", - "give", - "given", - "giving", - "glad", - "glass", - "globe", - "go", - "goes", - "gold", - "golden", - "gone", - "good", - "goose", - "got", - "government", - "grabbed", - "grade", - "gradually", - "grain", - "grandfather", - "grandmother", - "graph", - "grass", - "gravity", - "gray", - "great", - "greater", - "greatest", - "greatly", - "green", - "grew", - "ground", - "group", - "grow", - "grown", - "growth", - "guard", - "guess", - "guide", - "gulf", - "gun", - "habit", - "had", - "hair", - "half", - "halfway", - "hall", - "hand", - "handle", - "handsome", - "hang", - "happen", - "happened", - "happily", - "happy", - "harbor", - "hard", - "harder", - "hardly", - "has", - "hat", - "have", - "having", - "hay", - "he", - "headed", - "heading", - "health", - "heard", - "hearing", - "heart", - "heat", - "heavy", - "height", - "held", - "hello", - "help", - "helpful", - "her", - "herd", - "here", - "herself", - "hidden", - "hide", - "high", - "higher", - "highest", - "highway", - "hill", - "him", - "himself", - "his", - "history", - "hit", - "hold", - "hole", - "hollow", - "home", - "honor", - "hope", - "horn", - "horse", - "hospital", - "hot", - "hour", - "house", - "how", - "however", - "huge", - "human", - "hundred", - "hung", - "hungry", - "hunt", - "hunter", - "hurried", - "hurry", - "hurt", - "husband", - "ice", - "idea", - "identity", - "if", - "ill", - "image", - "imagine", - "immediately", - "importance", - "important", - "impossible", - "improve", - "in", - "inch", - "include", - "including", - "income", - "increase", - "indeed", - "independent", - "indicate", - "individual", - "industrial", - "industry", - "influence", - "information", - "inside", - "instance", - "instant", - "instead", - "instrument", - "interest", - "interior", - "into", - "introduced", - "invented", - "involved", - "iron", - "is", - "island", - "it", - "its", - "itself", - "jack", - "jar", - "jet", - "job", - "join", - "joined", - "journey", - "joy", - "judge", - "jump", - "jungle", - "just", - "keep", - "kept", - "key", - "kids", - "kill", - "kind", - "kitchen", - "knew", - "knife", - "know", - "knowledge", - "known", - "label", - "labor", - "lack", - "lady", - "laid", - "lake", - "lamp", - "land", - "language", - "large", - "larger", - "largest", - "last", - "late", - "later", - "laugh", - "law", - "lay", - "layers", - "lead", - "leader", - "leaf", - "learn", - "least", - "leather", - "leave", - "leaving", - "led", - "left", - "leg", - "length", - "lesson", - "let", - "letter", - "level", - "library", - "lie", - "life", - "lift", - "light", - "like", - "likely", - "limited", - "line", - "lion", - "lips", - "liquid", - "list", - "listen", - "little", - "live", - "living", - "load", - "local", - "locate", - "location", - "log", - "lonely", - "long", - "longer", - "look", - "loose", - "lose", - "loss", - "lost", - "lot", - "loud", - "love", - "lovely", - "low", - "lower", - "luck", - "lucky", - "lunch", - "lungs", - "lying", - "machine", - "machinery", - "mad", - "made", - "magic", - "magnet", - "mail", - "main", - "mainly", - "major", - "make", - "making", - "man", - "managed", - "manner", - "manufacturing", - "many", - "map", - "mark", - "market", - "married", - "mass", - "massage", - "master", - "material", - "mathematics", - "matter", - "may", - "maybe", - "me", - "meal", - "mean", - "means", - "meant", - "measure", - "meat", - "medicine", - "meet", - "melted", - "member", - "memory", - "men", - "mental", - "merely", - "met", - "metal", - "method", - "mice", - "middle", - "might", - "mighty", - "mile", - "military", - "milk", - "mill", - "mind", - "mine", - "minerals", - "minute", - "mirror", - "missing", - "mission", - "mistake", - "mix", - "mixture", - "model", - "modern", - "molecular", - "moment", - "money", - "monkey", - "month", - "mood", - "moon", - "more", - "morning", - "most", - "mostly", - "mother", - "motion", - "motor", - "mountain", - "mouse", - "mouth", - "move", - "movement", - "movie", - "moving", - "mud", - "muscle", - "music", - "musical", - "must", - "my", - "myself", - "mysterious", - "nails", - "name", - "nation", - "national", - "native", - "natural", - "naturally", - "nature", - "near", - "nearby", - "nearer", - "nearest", - "nearly", - "necessary", - "neck", - "needed", - "needle", - "needs", - "negative", - "neighbor", - "neighborhood", - "nervous", - "nest", - "never", - "new", - "news", - "newspaper", - "next", - "nice", - "night", - "nine", - "no", - "nobody", - "nodded", - "noise", - "none", - "noon", - "nor", - "north", - "nose", - "not", - "note", - "noted", - "nothing", - "notice", - "noun", - "now", - "number", - "numeral", - "nuts", - "object", - "observe", - "obtain", - "occasionally", - "occur", - "ocean", - "of", - "off", - "offer", - "office", - "officer", - "official", - "oil", - "old", - "older", - "oldest", - "on", - "once", - "one", - "only", - "onto", - "open", - "operation", - "opinion", - "opportunity", - "opposite", - "or", - "orange", - "orbit", - "order", - "ordinary", - "organization", - "organized", - "origin", - "original", - "other", - "ought", - "our", - "ourselves", - "out", - "outer", - "outline", - "outside", - "over", - "own", - "owner", - "oxygen", - "pack", - "package", - "page", - "paid", - "pain", - "paint", - "pair", - "palace", - "pale", - "pan", - "paper", - "paragraph", - "parallel", - "parent", - "park", - "part", - "particles", - "particular", - "particularly", - "partly", - "parts", - "party", - "pass", - "passage", - "past", - "path", - "pattern", - "pay", - "peace", - "pen", - "pencil", - "people", - "per", - "percent", - "perfect", - "perfectly", - "perhaps", - "period", - "person", - "personal", - "pet", - "phrase", - "physical", - "piano", - "pick", - "picture", - "pictured", - "pie", - "piece", - "pig", - "pile", - "pilot", - "pine", - "pink", - "pipe", - "pitch", - "place", - "plain", - "plan", - "plane", - "planet", - "planned", - "planning", - "plant", - "plastic", - "plate", - "plates", - "play", - "pleasant", - "please", - "pleasure", - "plenty", - "plural", - "plus", - "pocket", - "poem", - "poet", - "poetry", - "point", - "pole", - "police", - "policeman", - "political", - "pond", - "pony", - "pool", - "poor", - "popular", - "population", - "porch", - "port", - "position", - "positive", - "possible", - "possibly", - "post", - "pot", - "potatoes", - "pound", - "pour", - "powder", - "power", - "powerful", - "practical", - "practice", - "prepare", - "present", - "president", - "press", - "pressure", - "pretty", - "prevent", - "previous", - "price", - "pride", - "primitive", - "principal", - "principle", - "printed", - "private", - "prize", - "probably", - "problem", - "process", - "produce", - "product", - "production", - "program", - "progress", - "promised", - "proper", - "properly", - "property", - "protection", - "proud", - "prove", - "provide", - "public", - "pull", - "pupil", - "pure", - "purple", - "purpose", - "push", - "put", - "putting", - "quarter", - "queen", - "question", - "quick", - "quickly", - "quiet", - "quietly", - "quite", - "rabbit", - "race", - "radio", - "railroad", - "rain", - "raise", - "ran", - "ranch", - "range", - "rapidly", - "rate", - "rather", - "raw", - "rays", - "reach", - "read", - "reader", - "ready", - "real", - "realize", - "rear", - "reason", - "recall", - "receive", - "recent", - "recently", - "recognize", - "record", - "red", - "refer", - "refused", - "region", - "regular", - "related", - "relationship", - "religious", - "remain", - "remarkable", - "remember", - "remove", - "repeat", - "replace", - "replied", - "report", - "represent", - "require", - "research", - "respect", - "rest", - "result", - "return", - "review", - "rhyme", - "rhythm", - "rice", - "rich", - "ride", - "riding", - "right", - "ring", - "rise", - "rising", - "river", - "road", - "roar", - "rock", - "rocket", - "rocky", - "rod", - "roll", - "roof", - "room", - "root", - "rope", - "rose", - "rough", - "round", - "route", - "row", - "rubbed", - "rubber", - "rule", - "ruler", - "run", - "running", - "rush", - "sad", - "saddle", - "safe", - "safety", - "said", - "sail", - "sale", - "salmon", - "salt", - "same", - "sand", - "sang", - "sat", - "satellites", - "satisfied", - "save", - "saved", - "saw", - "say", - "scale", - "scared", - "scene", - "school", - "science", - "scientific", - "scientist", - "score", - "screen", - "sea", - "search", - "season", - "seat", - "second", - "secret", - "section", - "see", - "seed", - "seeing", - "seems", - "seen", - "seldom", - "select", - "selection", - "sell", - "send", - "sense", - "sent", - "sentence", - "separate", - "series", - "serious", - "serve", - "service", - "sets", - "setting", - "settle", - "settlers", - "seven", - "several", - "shade", - "shadow", - "shake", - "shaking", - "shall", - "shallow", - "shape", - "share", - "sharp", - "she", - "sheep", - "sheet", - "shelf", - "shells", - "shelter", - "shine", - "shinning", - "ship", - "shirt", - "shoe", - "shoot", - "shop", - "shore", - "short", - "shorter", - "shot", - "should", - "shoulder", - "shout", - "show", - "shown", - "shut", - "sick", - "sides", - "sight", - "sign", - "signal", - "silence", - "silent", - "silk", - "silly", - "silver", - "similar", - "simple", - "simplest", - "simply", - "since", - "sing", - "single", - "sink", - "sister", - "sit", - "sitting", - "situation", - "six", - "size", - "skill", - "skin", - "sky", - "slabs", - "slave", - "sleep", - "slept", - "slide", - "slight", - "slightly", - "slip", - "slipped", - "slope", - "slow", - "slowly", - "small", - "smaller", - "smallest", - "smell", - "smile", - "smoke", - "smooth", - "snake", - "snow", - "so", - "soap", - "social", - "society", - "soft", - "softly", - "soil", - "solar", - "sold", - "soldier", - "solid", - "solution", - "solve", - "some", - "somebody", - "somehow", - "someone", - "something", - "sometime", - "somewhere", - "son", - "song", - "soon", - "sort", - "sound", - "source", - "south", - "southern", - "space", - "speak", - "special", - "species", - "specific", - "speech", - "speed", - "spell", - "spend", - "spent", - "spider", - "spin", - "spirit", - "spite", - "split", - "spoken", - "sport", - "spread", - "spring", - "square", - "stage", - "stairs", - "stand", - "standard", - "star", - "stared", - "start", - "state", - "statement", - "station", - "stay", - "steady", - "steam", - "steel", - "steep", - "stems", - "step", - "stepped", - "stick", - "stiff", - "still", - "stock", - "stomach", - "stone", - "stood", - "stop", - "stopped", - "store", - "storm", - "story", - "stove", - "straight", - "strange", - "stranger", - "straw", - "stream", - "street", - "strength", - "stretch", - "strike", - "string", - "strip", - "strong", - "stronger", - "struck", - "structure", - "struggle", - "stuck", - "student", - "studied", - "studying", - "subject", - "substance", - "success", - "successful", - "such", - "sudden", - "suddenly", - "sugar", - "suggest", - "suit", - "sum", - "summer", - "sun", - "sunlight", - "supper", - "supply", - "support", - "suppose", - "sure", - "surface", - "surprise", - "surrounded", - "swam", - "sweet", - "swept", - "swim", - "swimming", - "swing", - "swung", - "syllable", - "symbol", - "system", - "table", - "tail", - "take", - "taken", - "tales", - "talk", - "tall", - "tank", - "tape", - "task", - "taste", - "taught", - "tax", - "tea", - "teach", - "teacher", - "team", - "tears", - "teeth", - "telephone", - "television", - "tell", - "temperature", - "ten", - "tent", - "term", - "terrible", - "test", - "than", - "thank", - "that", - "thee", - "them", - "themselves", - "then", - "theory", - "there", - "therefore", - "these", - "they", - "thick", - "thin", - "thing", - "think", - "third", - "thirty", - "this", - "those", - "thou", - "though", - "thought", - "thousand", - "thread", - "three", - "threw", - "throat", - "through", - "throughout", - "throw", - "thrown", - "thumb", - "thus", - "thy", - "tide", - "tie", - "tight", - "tightly", - "till", - "time", - "tin", - "tiny", - "tip", - "tired", - "title", - "to", - "tobacco", - "today", - "together", - "told", - "tomorrow", - "tone", - "tongue", - "tonight", - "too", - "took", - "tool", - "top", - "topic", - "torn", - "total", - "touch", - "toward", - "tower", - "town", - "toy", - "trace", - "track", - "trade", - "traffic", - "trail", - "train", - "transportation", - "trap", - "travel", - "treated", - "tree", - "triangle", - "tribe", - "trick", - "tried", - "trip", - "troops", - "tropical", - "trouble", - "truck", - "trunk", - "truth", - "try", - "tube", - "tune", - "turn", - "twelve", - "twenty", - "twice", - "two", - "type", - "typical", - "uncle", - "under", - "underline", - "understanding", - "unhappy", - "union", - "unit", - "universe", - "unknown", - "unless", - "until", - "unusual", - "up", - "upon", - "upper", - "upward", - "us", - "use", - "useful", - "using", - "usual", - "usually", - "valley", - "valuable", - "value", - "vapor", - "variety", - "various", - "vast", - "vegetable", - "verb", - "vertical", - "very", - "vessels", - "victory", - "view", - "village", - "visit", - "visitor", - "voice", - "volume", - "vote", - "vowel", - "voyage", - "wagon", - "wait", - "walk", - "wall", - "want", - "war", - "warm", - "warn", - "was", - "wash", - "waste", - "watch", - "water", - "wave", - "way", - "we", - "weak", - "wealth", - "wear", - "weather", - "week", - "weigh", - "weight", - "welcome", - "well", - "went", - "were", - "west", - "western", - "wet", - "whale", - "what", - "whatever", - "wheat", - "wheel", - "when", - "whenever", - "where", - "wherever", - "whether", - "which", - "while", - "whispered", - "whistle", - "white", - "who", - "whole", - "whom", - "whose", - "why", - "wide", - "widely", - "wife", - "wild", - "will", - "willing", - "win", - "wind", - "window", - "wing", - "winter", - "wire", - "wise", - "wish", - "with", - "within", - "without", - "wolf", - "women", - "won", - "wonder", - "wonderful", - "wood", - "wooden", - "wool", - "word", - "wore", - "work", - "worker", - "world", - "worried", - "worry", - "worse", - "worth", - "would", - "wrapped", - "write", - "writer", - "writing", - "written", - "wrong", - "wrote", - "yard", - "year", - "yellow", - "yes", - "yesterday", - "yet", - "you", - "young", - "younger", - "your", - "yourself", - "youth", - "zero", - "zebra", - "zipper", - "zoo", - "zulu" +//#endregion +//#region src/lib/handlers/signup/random-words/wordList.ts +const wordList = [ + "ability", + "able", + "aboard", + "about", + "above", + "accept", + "accident", + "according", + "account", + "accurate", + "acres", + "across", + "act", + "action", + "active", + "activity", + "actual", + "actually", + "add", + "addition", + "additional", + "adjective", + "adult", + "adventure", + "advice", + "affect", + "afraid", + "after", + "afternoon", + "again", + "against", + "age", + "ago", + "agree", + "ahead", + "aid", + "air", + "airplane", + "alike", + "alive", + "all", + "allow", + "almost", + "alone", + "along", + "aloud", + "alphabet", + "already", + "also", + "although", + "am", + "among", + "amount", + "ancient", + "angle", + "angry", + "animal", + "announced", + "another", + "answer", + "ants", + "any", + "anybody", + "anyone", + "anything", + "anyway", + "anywhere", + "apart", + "apartment", + "appearance", + "apple", + "applied", + "appropriate", + "are", + "area", + "arm", + "army", + "around", + "arrange", + "arrangement", + "arrive", + "arrow", + "art", + "article", + "as", + "aside", + "ask", + "asleep", + "at", + "ate", + "atmosphere", + "atom", + "atomic", + "attached", + "attack", + "attempt", + "attention", + "audience", + "author", + "automobile", + "available", + "average", + "avoid", + "aware", + "away", + "baby", + "back", + "bad", + "badly", + "bag", + "balance", + "ball", + "balloon", + "band", + "bank", + "bar", + "bare", + "bark", + "barn", + "base", + "baseball", + "basic", + "basis", + "basket", + "bat", + "battle", + "be", + "bean", + "bear", + "beat", + "beautiful", + "beauty", + "became", + "because", + "become", + "becoming", + "bee", + "been", + "before", + "began", + "beginning", + "begun", + "behavior", + "behind", + "being", + "believed", + "bell", + "belong", + "below", + "belt", + "bend", + "beneath", + "bent", + "beside", + "best", + "bet", + "better", + "between", + "beyond", + "bicycle", + "bigger", + "biggest", + "bill", + "birds", + "birth", + "birthday", + "bit", + "bite", + "black", + "blank", + "blanket", + "blew", + "blind", + "block", + "blood", + "blow", + "blue", + "board", + "boat", + "body", + "bone", + "book", + "border", + "born", + "both", + "bottle", + "bottom", + "bound", + "bow", + "bowl", + "box", + "boy", + "brain", + "branch", + "brass", + "brave", + "bread", + "break", + "breakfast", + "breath", + "breathe", + "breathing", + "breeze", + "brick", + "bridge", + "brief", + "bright", + "bring", + "broad", + "broke", + "broken", + "brother", + "brought", + "brown", + "brush", + "buffalo", + "build", + "building", + "built", + "buried", + "burn", + "burst", + "bus", + "bush", + "business", + "busy", + "but", + "butter", + "buy", + "by", + "cabin", + "cage", + "cake", + "call", + "calm", + "came", + "camera", + "camp", + "can", + "canal", + "cannot", + "cap", + "capital", + "captain", + "captured", + "car", + "carbon", + "card", + "care", + "careful", + "carefully", + "carried", + "carry", + "case", + "cast", + "castle", + "cat", + "catch", + "cattle", + "caught", + "cause", + "cave", + "cell", + "cent", + "center", + "central", + "century", + "certain", + "certainly", + "chain", + "chair", + "chamber", + "chance", + "change", + "changing", + "chapter", + "character", + "characteristic", + "charge", + "chart", + "check", + "cheese", + "chemical", + "chest", + "chicken", + "chief", + "child", + "children", + "choice", + "choose", + "chose", + "chosen", + "church", + "circle", + "circus", + "citizen", + "city", + "class", + "classroom", + "claws", + "clay", + "clean", + "clear", + "clearly", + "climate", + "climb", + "clock", + "close", + "closely", + "closer", + "cloth", + "clothes", + "clothing", + "cloud", + "club", + "coach", + "coal", + "coast", + "coat", + "coffee", + "cold", + "collect", + "college", + "colony", + "color", + "column", + "combination", + "combine", + "come", + "comfortable", + "coming", + "command", + "common", + "community", + "company", + "compare", + "compass", + "complete", + "completely", + "complex", + "composed", + "composition", + "compound", + "concerned", + "condition", + "congress", + "connected", + "consider", + "consist", + "consonant", + "constantly", + "construction", + "contain", + "continent", + "continued", + "contrast", + "control", + "conversation", + "cook", + "cookies", + "cool", + "copper", + "copy", + "corn", + "corner", + "correct", + "correctly", + "cost", + "cotton", + "could", + "count", + "country", + "couple", + "courage", + "course", + "court", + "cover", + "cow", + "cowboy", + "crack", + "cream", + "create", + "creature", + "crew", + "crop", + "cross", + "crowd", + "cry", + "cup", + "curious", + "current", + "curve", + "customs", + "cut", + "cutting", + "daily", + "damage", + "dance", + "danger", + "dangerous", + "dark", + "darkness", + "date", + "daughter", + "dawn", + "day", + "dead", + "deal", + "dear", + "death", + "decide", + "declared", + "deep", + "deeply", + "deer", + "definition", + "degree", + "depend", + "depth", + "describe", + "desert", + "design", + "desk", + "detail", + "determine", + "develop", + "development", + "diagram", + "diameter", + "did", + "die", + "differ", + "difference", + "different", + "difficult", + "difficulty", + "dig", + "dinner", + "direct", + "direction", + "directly", + "dirt", + "dirty", + "disappear", + "discover", + "discovery", + "discuss", + "discussion", + "disease", + "dish", + "distance", + "distant", + "divide", + "division", + "do", + "doctor", + "does", + "dog", + "doing", + "doll", + "dollar", + "done", + "donkey", + "door", + "dot", + "double", + "doubt", + "down", + "dozen", + "draw", + "drawn", + "dream", + "dress", + "drew", + "dried", + "drink", + "drive", + "driven", + "driver", + "driving", + "drop", + "dropped", + "drove", + "dry", + "duck", + "due", + "dug", + "dull", + "during", + "dust", + "duty", + "each", + "eager", + "ear", + "earlier", + "early", + "earn", + "earth", + "easier", + "easily", + "east", + "easy", + "eat", + "eaten", + "edge", + "education", + "effect", + "effort", + "egg", + "eight", + "either", + "electric", + "electricity", + "element", + "elephant", + "eleven", + "else", + "empty", + "end", + "enemy", + "energy", + "engine", + "engineer", + "enjoy", + "enough", + "enter", + "entire", + "entirely", + "environment", + "equal", + "equally", + "equator", + "equipment", + "escape", + "especially", + "essential", + "establish", + "even", + "evening", + "event", + "eventually", + "ever", + "every", + "everybody", + "everyone", + "everything", + "everywhere", + "evidence", + "exact", + "exactly", + "examine", + "example", + "excellent", + "except", + "exchange", + "excited", + "excitement", + "exciting", + "exclaimed", + "exercise", + "exist", + "expect", + "experience", + "experiment", + "explain", + "explanation", + "explore", + "express", + "expression", + "extra", + "eye", + "face", + "facing", + "fact", + "factor", + "factory", + "failed", + "fair", + "fairly", + "fall", + "fallen", + "familiar", + "family", + "famous", + "far", + "farm", + "farmer", + "farther", + "fast", + "fastened", + "faster", + "fat", + "father", + "favorite", + "fear", + "feathers", + "feature", + "fed", + "feed", + "feel", + "feet", + "fell", + "fellow", + "felt", + "fence", + "few", + "fewer", + "field", + "fierce", + "fifteen", + "fifth", + "fifty", + "fight", + "fighting", + "figure", + "fill", + "film", + "final", + "finally", + "find", + "fine", + "finest", + "finger", + "finish", + "fire", + "fireplace", + "firm", + "first", + "fish", + "five", + "fix", + "flag", + "flame", + "flat", + "flew", + "flies", + "flight", + "floating", + "floor", + "flow", + "flower", + "fly", + "fog", + "folks", + "follow", + "food", + "foot", + "football", + "for", + "force", + "foreign", + "forest", + "forget", + "forgot", + "forgotten", + "form", + "former", + "fort", + "forth", + "forty", + "forward", + "fought", + "found", + "four", + "fourth", + "fox", + "frame", + "free", + "freedom", + "frequently", + "fresh", + "friend", + "friendly", + "frighten", + "frog", + "from", + "front", + "frozen", + "fruit", + "fuel", + "full", + "fully", + "fun", + "function", + "funny", + "fur", + "furniture", + "further", + "future", + "gain", + "game", + "garage", + "garden", + "gas", + "gasoline", + "gate", + "gather", + "gave", + "general", + "generally", + "gentle", + "gently", + "get", + "getting", + "giant", + "gift", + "girl", + "give", + "given", + "giving", + "glad", + "glass", + "globe", + "go", + "goes", + "gold", + "golden", + "gone", + "good", + "goose", + "got", + "government", + "grabbed", + "grade", + "gradually", + "grain", + "grandfather", + "grandmother", + "graph", + "grass", + "gravity", + "gray", + "great", + "greater", + "greatest", + "greatly", + "green", + "grew", + "ground", + "group", + "grow", + "grown", + "growth", + "guard", + "guess", + "guide", + "gulf", + "gun", + "habit", + "had", + "hair", + "half", + "halfway", + "hall", + "hand", + "handle", + "handsome", + "hang", + "happen", + "happened", + "happily", + "happy", + "harbor", + "hard", + "harder", + "hardly", + "has", + "hat", + "have", + "having", + "hay", + "he", + "headed", + "heading", + "health", + "heard", + "hearing", + "heart", + "heat", + "heavy", + "height", + "held", + "hello", + "help", + "helpful", + "her", + "herd", + "here", + "herself", + "hidden", + "hide", + "high", + "higher", + "highest", + "highway", + "hill", + "him", + "himself", + "his", + "history", + "hit", + "hold", + "hole", + "hollow", + "home", + "honor", + "hope", + "horn", + "horse", + "hospital", + "hot", + "hour", + "house", + "how", + "however", + "huge", + "human", + "hundred", + "hung", + "hungry", + "hunt", + "hunter", + "hurried", + "hurry", + "hurt", + "husband", + "ice", + "idea", + "identity", + "if", + "ill", + "image", + "imagine", + "immediately", + "importance", + "important", + "impossible", + "improve", + "in", + "inch", + "include", + "including", + "income", + "increase", + "indeed", + "independent", + "indicate", + "individual", + "industrial", + "industry", + "influence", + "information", + "inside", + "instance", + "instant", + "instead", + "instrument", + "interest", + "interior", + "into", + "introduced", + "invented", + "involved", + "iron", + "is", + "island", + "it", + "its", + "itself", + "jack", + "jar", + "jet", + "job", + "join", + "joined", + "journey", + "joy", + "judge", + "jump", + "jungle", + "just", + "keep", + "kept", + "key", + "kids", + "kill", + "kind", + "kitchen", + "knew", + "knife", + "know", + "knowledge", + "known", + "label", + "labor", + "lack", + "lady", + "laid", + "lake", + "lamp", + "land", + "language", + "large", + "larger", + "largest", + "last", + "late", + "later", + "laugh", + "law", + "lay", + "layers", + "lead", + "leader", + "leaf", + "learn", + "least", + "leather", + "leave", + "leaving", + "led", + "left", + "leg", + "length", + "lesson", + "let", + "letter", + "level", + "library", + "lie", + "life", + "lift", + "light", + "like", + "likely", + "limited", + "line", + "lion", + "lips", + "liquid", + "list", + "listen", + "little", + "live", + "living", + "load", + "local", + "locate", + "location", + "log", + "lonely", + "long", + "longer", + "look", + "loose", + "lose", + "loss", + "lost", + "lot", + "loud", + "love", + "lovely", + "low", + "lower", + "luck", + "lucky", + "lunch", + "lungs", + "lying", + "machine", + "machinery", + "mad", + "made", + "magic", + "magnet", + "mail", + "main", + "mainly", + "major", + "make", + "making", + "man", + "managed", + "manner", + "manufacturing", + "many", + "map", + "mark", + "market", + "married", + "mass", + "massage", + "master", + "material", + "mathematics", + "matter", + "may", + "maybe", + "me", + "meal", + "mean", + "means", + "meant", + "measure", + "meat", + "medicine", + "meet", + "melted", + "member", + "memory", + "men", + "mental", + "merely", + "met", + "metal", + "method", + "mice", + "middle", + "might", + "mighty", + "mile", + "military", + "milk", + "mill", + "mind", + "mine", + "minerals", + "minute", + "mirror", + "missing", + "mission", + "mistake", + "mix", + "mixture", + "model", + "modern", + "molecular", + "moment", + "money", + "monkey", + "month", + "mood", + "moon", + "more", + "morning", + "most", + "mostly", + "mother", + "motion", + "motor", + "mountain", + "mouse", + "mouth", + "move", + "movement", + "movie", + "moving", + "mud", + "muscle", + "music", + "musical", + "must", + "my", + "myself", + "mysterious", + "nails", + "name", + "nation", + "national", + "native", + "natural", + "naturally", + "nature", + "near", + "nearby", + "nearer", + "nearest", + "nearly", + "necessary", + "neck", + "needed", + "needle", + "needs", + "negative", + "neighbor", + "neighborhood", + "nervous", + "nest", + "never", + "new", + "news", + "newspaper", + "next", + "nice", + "night", + "nine", + "no", + "nobody", + "nodded", + "noise", + "none", + "noon", + "nor", + "north", + "nose", + "not", + "note", + "noted", + "nothing", + "notice", + "noun", + "now", + "number", + "numeral", + "nuts", + "object", + "observe", + "obtain", + "occasionally", + "occur", + "ocean", + "of", + "off", + "offer", + "office", + "officer", + "official", + "oil", + "old", + "older", + "oldest", + "on", + "once", + "one", + "only", + "onto", + "open", + "operation", + "opinion", + "opportunity", + "opposite", + "or", + "orange", + "orbit", + "order", + "ordinary", + "organization", + "organized", + "origin", + "original", + "other", + "ought", + "our", + "ourselves", + "out", + "outer", + "outline", + "outside", + "over", + "own", + "owner", + "oxygen", + "pack", + "package", + "page", + "paid", + "pain", + "paint", + "pair", + "palace", + "pale", + "pan", + "paper", + "paragraph", + "parallel", + "parent", + "park", + "part", + "particles", + "particular", + "particularly", + "partly", + "parts", + "party", + "pass", + "passage", + "past", + "path", + "pattern", + "pay", + "peace", + "pen", + "pencil", + "people", + "per", + "percent", + "perfect", + "perfectly", + "perhaps", + "period", + "person", + "personal", + "pet", + "phrase", + "physical", + "piano", + "pick", + "picture", + "pictured", + "pie", + "piece", + "pig", + "pile", + "pilot", + "pine", + "pink", + "pipe", + "pitch", + "place", + "plain", + "plan", + "plane", + "planet", + "planned", + "planning", + "plant", + "plastic", + "plate", + "plates", + "play", + "pleasant", + "please", + "pleasure", + "plenty", + "plural", + "plus", + "pocket", + "poem", + "poet", + "poetry", + "point", + "pole", + "police", + "policeman", + "political", + "pond", + "pony", + "pool", + "poor", + "popular", + "population", + "porch", + "port", + "position", + "positive", + "possible", + "possibly", + "post", + "pot", + "potatoes", + "pound", + "pour", + "powder", + "power", + "powerful", + "practical", + "practice", + "prepare", + "present", + "president", + "press", + "pressure", + "pretty", + "prevent", + "previous", + "price", + "pride", + "primitive", + "principal", + "principle", + "printed", + "private", + "prize", + "probably", + "problem", + "process", + "produce", + "product", + "production", + "program", + "progress", + "promised", + "proper", + "properly", + "property", + "protection", + "proud", + "prove", + "provide", + "public", + "pull", + "pupil", + "pure", + "purple", + "purpose", + "push", + "put", + "putting", + "quarter", + "queen", + "question", + "quick", + "quickly", + "quiet", + "quietly", + "quite", + "rabbit", + "race", + "radio", + "railroad", + "rain", + "raise", + "ran", + "ranch", + "range", + "rapidly", + "rate", + "rather", + "raw", + "rays", + "reach", + "read", + "reader", + "ready", + "real", + "realize", + "rear", + "reason", + "recall", + "receive", + "recent", + "recently", + "recognize", + "record", + "red", + "refer", + "refused", + "region", + "regular", + "related", + "relationship", + "religious", + "remain", + "remarkable", + "remember", + "remove", + "repeat", + "replace", + "replied", + "report", + "represent", + "require", + "research", + "respect", + "rest", + "result", + "return", + "review", + "rhyme", + "rhythm", + "rice", + "rich", + "ride", + "riding", + "right", + "ring", + "rise", + "rising", + "river", + "road", + "roar", + "rock", + "rocket", + "rocky", + "rod", + "roll", + "roof", + "room", + "root", + "rope", + "rose", + "rough", + "round", + "route", + "row", + "rubbed", + "rubber", + "rule", + "ruler", + "run", + "running", + "rush", + "sad", + "saddle", + "safe", + "safety", + "said", + "sail", + "sale", + "salmon", + "salt", + "same", + "sand", + "sang", + "sat", + "satellites", + "satisfied", + "save", + "saved", + "saw", + "say", + "scale", + "scared", + "scene", + "school", + "science", + "scientific", + "scientist", + "score", + "screen", + "sea", + "search", + "season", + "seat", + "second", + "secret", + "section", + "see", + "seed", + "seeing", + "seems", + "seen", + "seldom", + "select", + "selection", + "sell", + "send", + "sense", + "sent", + "sentence", + "separate", + "series", + "serious", + "serve", + "service", + "sets", + "setting", + "settle", + "settlers", + "seven", + "several", + "shade", + "shadow", + "shake", + "shaking", + "shall", + "shallow", + "shape", + "share", + "sharp", + "she", + "sheep", + "sheet", + "shelf", + "shells", + "shelter", + "shine", + "shinning", + "ship", + "shirt", + "shoe", + "shoot", + "shop", + "shore", + "short", + "shorter", + "shot", + "should", + "shoulder", + "shout", + "show", + "shown", + "shut", + "sick", + "sides", + "sight", + "sign", + "signal", + "silence", + "silent", + "silk", + "silly", + "silver", + "similar", + "simple", + "simplest", + "simply", + "since", + "sing", + "single", + "sink", + "sister", + "sit", + "sitting", + "situation", + "six", + "size", + "skill", + "skin", + "sky", + "slabs", + "slave", + "sleep", + "slept", + "slide", + "slight", + "slightly", + "slip", + "slipped", + "slope", + "slow", + "slowly", + "small", + "smaller", + "smallest", + "smell", + "smile", + "smoke", + "smooth", + "snake", + "snow", + "so", + "soap", + "social", + "society", + "soft", + "softly", + "soil", + "solar", + "sold", + "soldier", + "solid", + "solution", + "solve", + "some", + "somebody", + "somehow", + "someone", + "something", + "sometime", + "somewhere", + "son", + "song", + "soon", + "sort", + "sound", + "source", + "south", + "southern", + "space", + "speak", + "special", + "species", + "specific", + "speech", + "speed", + "spell", + "spend", + "spent", + "spider", + "spin", + "spirit", + "spite", + "split", + "spoken", + "sport", + "spread", + "spring", + "square", + "stage", + "stairs", + "stand", + "standard", + "star", + "stared", + "start", + "state", + "statement", + "station", + "stay", + "steady", + "steam", + "steel", + "steep", + "stems", + "step", + "stepped", + "stick", + "stiff", + "still", + "stock", + "stomach", + "stone", + "stood", + "stop", + "stopped", + "store", + "storm", + "story", + "stove", + "straight", + "strange", + "stranger", + "straw", + "stream", + "street", + "strength", + "stretch", + "strike", + "string", + "strip", + "strong", + "stronger", + "struck", + "structure", + "struggle", + "stuck", + "student", + "studied", + "studying", + "subject", + "substance", + "success", + "successful", + "such", + "sudden", + "suddenly", + "sugar", + "suggest", + "suit", + "sum", + "summer", + "sun", + "sunlight", + "supper", + "supply", + "support", + "suppose", + "sure", + "surface", + "surprise", + "surrounded", + "swam", + "sweet", + "swept", + "swim", + "swimming", + "swing", + "swung", + "syllable", + "symbol", + "system", + "table", + "tail", + "take", + "taken", + "tales", + "talk", + "tall", + "tank", + "tape", + "task", + "taste", + "taught", + "tax", + "tea", + "teach", + "teacher", + "team", + "tears", + "teeth", + "telephone", + "television", + "tell", + "temperature", + "ten", + "tent", + "term", + "terrible", + "test", + "than", + "thank", + "that", + "thee", + "them", + "themselves", + "then", + "theory", + "there", + "therefore", + "these", + "they", + "thick", + "thin", + "thing", + "think", + "third", + "thirty", + "this", + "those", + "thou", + "though", + "thought", + "thousand", + "thread", + "three", + "threw", + "throat", + "through", + "throughout", + "throw", + "thrown", + "thumb", + "thus", + "thy", + "tide", + "tie", + "tight", + "tightly", + "till", + "time", + "tin", + "tiny", + "tip", + "tired", + "title", + "to", + "tobacco", + "today", + "together", + "told", + "tomorrow", + "tone", + "tongue", + "tonight", + "too", + "took", + "tool", + "top", + "topic", + "torn", + "total", + "touch", + "toward", + "tower", + "town", + "toy", + "trace", + "track", + "trade", + "traffic", + "trail", + "train", + "transportation", + "trap", + "travel", + "treated", + "tree", + "triangle", + "tribe", + "trick", + "tried", + "trip", + "troops", + "tropical", + "trouble", + "truck", + "trunk", + "truth", + "try", + "tube", + "tune", + "turn", + "twelve", + "twenty", + "twice", + "two", + "type", + "typical", + "uncle", + "under", + "underline", + "understanding", + "unhappy", + "union", + "unit", + "universe", + "unknown", + "unless", + "until", + "unusual", + "up", + "upon", + "upper", + "upward", + "us", + "use", + "useful", + "using", + "usual", + "usually", + "valley", + "valuable", + "value", + "vapor", + "variety", + "various", + "vast", + "vegetable", + "verb", + "vertical", + "very", + "vessels", + "victory", + "view", + "village", + "visit", + "visitor", + "voice", + "volume", + "vote", + "vowel", + "voyage", + "wagon", + "wait", + "walk", + "wall", + "want", + "war", + "warm", + "warn", + "was", + "wash", + "waste", + "watch", + "water", + "wave", + "way", + "we", + "weak", + "wealth", + "wear", + "weather", + "week", + "weigh", + "weight", + "welcome", + "well", + "went", + "were", + "west", + "western", + "wet", + "whale", + "what", + "whatever", + "wheat", + "wheel", + "when", + "whenever", + "where", + "wherever", + "whether", + "which", + "while", + "whispered", + "whistle", + "white", + "who", + "whole", + "whom", + "whose", + "why", + "wide", + "widely", + "wife", + "wild", + "will", + "willing", + "win", + "wind", + "window", + "wing", + "winter", + "wire", + "wise", + "wish", + "with", + "within", + "without", + "wolf", + "women", + "won", + "wonder", + "wonderful", + "wood", + "wooden", + "wool", + "word", + "wore", + "work", + "worker", + "world", + "worried", + "worry", + "worse", + "worth", + "would", + "wrapped", + "write", + "writer", + "writing", + "written", + "wrong", + "wrote", + "yard", + "year", + "yellow", + "yes", + "yesterday", + "yet", + "you", + "young", + "younger", + "your", + "yourself", + "youth", + "zero", + "zebra", + "zipper", + "zoo", + "zulu" ]; -// src/lib/handlers/signup/random-words/index.ts -var shortestWordSize = wordList.reduce( - (shortestWord, currentWord) => currentWord.length < shortestWord.length ? currentWord : shortestWord -).length; -var longestWordSize = wordList.reduce( - (longestWord, currentWord) => currentWord.length > longestWord.length ? currentWord : longestWord -).length; +//#endregion +//#region src/lib/handlers/signup/random-words/index.ts +const shortestWordSize = wordList.reduce((shortestWord, currentWord) => currentWord.length < shortestWord.length ? currentWord : shortestWord).length; +const longestWordSize = wordList.reduce((longestWord, currentWord) => currentWord.length > longestWord.length ? currentWord : longestWord).length; function generate(options) { - const { minLength, maxLength, ...rest } = options || {}; - function word() { - let min = typeof minLength !== "number" ? shortestWordSize : limitWordSize(minLength); - const max = typeof maxLength !== "number" ? longestWordSize : limitWordSize(maxLength); - if (min > max) min = max; - let rightSize = false; - let wordUsed; - while (!rightSize) { - wordUsed = generateRandomWord(); - rightSize = wordUsed.length <= max && wordUsed.length >= min; - } - return wordUsed; - } - function generateRandomWord() { - return wordList[randInt(wordList.length)]; - } - function limitWordSize(wordSize) { - if (wordSize < shortestWordSize) wordSize = shortestWordSize; - if (wordSize > longestWordSize) wordSize = longestWordSize; - return wordSize; - } - function randInt(lessThan) { - const r = Math.random(); - return Math.floor(r * lessThan); - } - if (options === void 0) { - return word(); - } - if (typeof options === "number") { - options = { exactly: options }; - } else if (Object.keys(rest).length === 0) { - return word(); - } - if (options.exactly) { - options.min = options.exactly; - options.max = options.exactly; - } - if (typeof options.wordsPerString !== "number") { - options.wordsPerString = 1; - } - if (typeof options.formatter !== "function") { - options.formatter = (word2) => word2; - } - if (typeof options.separator !== "string") { - options.separator = " "; - } - const total = options.min + randInt(options.max + 1 - options.min); - let results = []; - let token = ""; - let relativeIndex = 0; - for (let i = 0; i < total * options.wordsPerString; i++) { - if (relativeIndex === options.wordsPerString - 1) { - token += options.formatter(word(), relativeIndex); - } else { - token += options.formatter(word(), relativeIndex) + options.separator; - } - relativeIndex++; - if ((i + 1) % options.wordsPerString === 0) { - results.push(token); - token = ""; - relativeIndex = 0; - } - } - if (typeof options.join === "string") { - results = results.join(options.join); - } - return results; + const { minLength, maxLength,...rest } = options || {}; + function word() { + let min = typeof minLength !== "number" ? shortestWordSize : limitWordSize(minLength); + const max = typeof maxLength !== "number" ? longestWordSize : limitWordSize(maxLength); + if (min > max) min = max; + let rightSize = false; + let wordUsed; + while (!rightSize) { + wordUsed = generateRandomWord(); + rightSize = wordUsed.length <= max && wordUsed.length >= min; + } + return wordUsed; + } + function generateRandomWord() { + return wordList[randInt(wordList.length)]; + } + function limitWordSize(wordSize) { + if (wordSize < shortestWordSize) wordSize = shortestWordSize; + if (wordSize > longestWordSize) wordSize = longestWordSize; + return wordSize; + } + function randInt(lessThan) { + const r = Math.random(); + return Math.floor(r * lessThan); + } + if (options === void 0) return word(); + if (typeof options === "number") options = { exactly: options }; + else if (Object.keys(rest).length === 0) return word(); + if (options.exactly) { + options.min = options.exactly; + options.max = options.exactly; + } + if (typeof options.wordsPerString !== "number") options.wordsPerString = 1; + if (typeof options.formatter !== "function") options.formatter = (word$1) => word$1; + if (typeof options.separator !== "string") options.separator = " "; + const total = options.min + randInt(options.max + 1 - options.min); + let results = []; + let token = ""; + let relativeIndex = 0; + for (let i = 0; i < total * options.wordsPerString; i++) { + if (relativeIndex === options.wordsPerString - 1) token += options.formatter(word(), relativeIndex); + else token += options.formatter(word(), relativeIndex) + options.separator; + relativeIndex++; + if ((i + 1) % options.wordsPerString === 0) { + results.push(token); + token = ""; + relativeIndex = 0; + } + } + if (typeof options.join === "string") results = results.join(options.join); + return results; } -// src/lib/handlers/signup/api/HandleSignupCheck.ts -var HandleSignupCheck = (c) => { - const instanceName = (() => { - const name = c.queryParam("name").trim(); - if (name) { - if (name.match(/^[a-z][a-z0-9-]{2,39}$/) === null) { - throw error( - `instanceName`, - `invalid`, - `Instance name must begin with a letter, be between 3-40 characters, and can only contain a-z, 0-9, and hyphen (-).` - ); - } - if (isAvailable(name)) { - return name; - } - throw error( - `instanceName`, - `exists`, - `Instance name ${name} is not available.` - ); - } else { - let i = 0; - while (true) { - i++; - if (i > 100) { - return +/* @__PURE__ */ new Date(); - } - const slug = generate(2).join(`-`); - if (isAvailable(slug)) return slug; - } - } - })(); - return c.json(200, { instanceName }); +//#endregion +//#region src/lib/handlers/signup/api/HandleSignupCheck.ts +const HandleSignupCheck = (c) => { + const instanceName = (() => { + const name = c.queryParam("name").trim(); + if (name) { + if (name.match(/^[a-z][a-z0-9-]{2,39}$/) === null) throw error(`instanceName`, `invalid`, `Instance name must begin with a letter, be between 3-40 characters, and can only contain a-z, 0-9, and hyphen (-).`); + if (isAvailable(name)) return name; + throw error(`instanceName`, `exists`, `Instance name ${name} is not available.`); + } else { + let i = 0; + while (true) { + i++; + if (i > 100) return +/* @__PURE__ */ new Date(); + const slug = generate(2).join(`-`); + if (isAvailable(slug)) return slug; + } + } + })(); + return c.json(200, { instanceName }); }; -// src/lib/handlers/signup/api/HandleSignupConfirm.ts -var HandleSignupConfirm = (c) => { - const dao = $app.dao(); - const parsed = (() => { - const rawBody = readerToString(c.request().body); - try { - const parsed2 = JSON.parse(rawBody); - return parsed2; - } catch (e) { - throw new BadRequestError( - `Error parsing payload. You call this JSON? ${rawBody}`, - e - ); - } - })(); - const email = parsed.email?.trim().toLowerCase(); - const password = parsed.password?.trim(); - const desiredInstanceName = parsed.instanceName?.trim(); - const region = parsed.region?.trim(); - const version = parsed.version?.trim() || versions[0]; - if (!email) { - throw error(`email`, "required", "Email is required"); - } - if (!password) { - throw error(`password`, `required`, "Password is required"); - } - if (!desiredInstanceName) { - throw error(`instanceName`, `required`, `Instance name is required`); - } - const userExists = (() => { - try { - const record = dao.findFirstRecordByData("users", "email", email); - return true; - } catch { - return false; - } - })(); - if (userExists) { - throw error( - `email`, - `exists`, - `That user account already exists. Try a password reset.` - ); - } - dao.runInTransaction((txDao) => { - const usersCollection = dao.findCollectionByNameOrId("users"); - const instanceCollection = $app.dao().findCollectionByNameOrId("instances"); - const user = new Record(usersCollection); - try { - const username = $app.dao().suggestUniqueAuthRecordUsername( - "users", - "user" + $security.randomStringWithAlphabet(5, "123456789") - ); - user.set("username", username); - user.set("email", email); - user.set("subscription", "free"); - user.set("subscription_quantity", 0); - user.setPassword(password); - txDao.saveRecord(user); - } catch (e) { - throw error(`email`, `fail`, `Could not create user: ${e}`); - } - try { - const instance = new Record(instanceCollection); - instance.set("subdomain", desiredInstanceName); - instance.set("region", region || `sfo-2`); - instance.set("uid", user.get("id")); - instance.set("status", "idle"); - instance.set("power", true); - instance.set("syncAdmin", true); - instance.set("dev", true); - instance.set("version", version); - txDao.saveRecord(instance); - } catch (e) { - if (`${e}`.match(/ UNIQUE /)) { - throw error( - `instanceName`, - `exists`, - `Instance name was taken, sorry about that. Try another.` - ); - } - throw error(`instanceName`, `fail`, `Could not create instance: ${e}`); - } - $mails.sendRecordVerification($app, user); - }); - return c.json(200, { status: "ok" }); +//#endregion +//#region src/lib/handlers/signup/api/HandleSignupConfirm.ts +const HandleSignupConfirm = (c) => { + const dao = $app.dao(); + const parsed = (() => { + const rawBody = readerToString(c.request().body); + try { + const parsed$1 = JSON.parse(rawBody); + return parsed$1; + } catch (e) { + throw new BadRequestError(`Error parsing payload. You call this JSON? ${rawBody}`, e); + } + })(); + const email = parsed.email?.trim().toLowerCase(); + const password = parsed.password?.trim(); + const desiredInstanceName = parsed.instanceName?.trim(); + const region = parsed.region?.trim(); + const version = parsed.version?.trim() || versions[0]; + if (!email) throw error(`email`, "required", "Email is required"); + if (!password) throw error(`password`, `required`, "Password is required"); + if (!desiredInstanceName) throw error(`instanceName`, `required`, `Instance name is required`); + const userExists = (() => { + try { + const record = dao.findFirstRecordByData("users", "email", email); + return true; + } catch { + return false; + } + })(); + if (userExists) throw error(`email`, `exists`, `That user account already exists. Try a password reset.`); + dao.runInTransaction((txDao) => { + const usersCollection = dao.findCollectionByNameOrId("users"); + const instanceCollection = $app.dao().findCollectionByNameOrId("instances"); + const user = new Record(usersCollection); + try { + const username = $app.dao().suggestUniqueAuthRecordUsername("users", "user" + $security.randomStringWithAlphabet(5, "123456789")); + user.set("username", username); + user.set("email", email); + user.set("subscription", "free"); + user.set("subscription_quantity", 0); + user.setPassword(password); + txDao.saveRecord(user); + } catch (e) { + throw error(`email`, `fail`, `Could not create user: ${e}`); + } + try { + const instance = new Record(instanceCollection); + instance.set("subdomain", desiredInstanceName); + instance.set("region", region || `sfo-2`); + instance.set("uid", user.get("id")); + instance.set("status", "idle"); + instance.set("power", true); + instance.set("syncAdmin", true); + instance.set("dev", true); + instance.set("version", version); + txDao.saveRecord(instance); + } catch (e) { + if (`${e}`.match(/ UNIQUE /)) throw error(`instanceName`, `exists`, `Instance name was taken, sorry about that. Try another.`); + throw error(`instanceName`, `fail`, `Could not create instance: ${e}`); + } + $mails.sendRecordVerification($app, user); + }); + return c.json(200, { status: "ok" }); }; -// src/lib/handlers/sns/api/HandleSesError.ts +//#endregion +//#region src/lib/handlers/sns/api/HandleSesError.ts function isSnsSubscriptionConfirmationEvent(event) { - return event.Type === "SubscriptionConfirmation"; + return event.Type === "SubscriptionConfirmation"; } function isSnsNotificationEvent(event) { - return event.Type === "Notification"; + return event.Type === "Notification"; } function isSnsNotificationBouncePayload(payload) { - return payload.notificationType === "Bounce"; + return payload.notificationType === "Bounce"; } function isSnsNotificationComplaintPayload(payload) { - return payload.notificationType === "Complaint"; + return payload.notificationType === "Complaint"; } -var HandleSesError = (c) => { - const dao = $app.dao(); - const log = mkLog(`sns`); - const audit = mkAudit(log, dao); - const processBounce = (emailAddress) => { - log(`Processing ${emailAddress}`); - const extra = { - email: emailAddress - }; - try { - const user = dao.findFirstRecordByData("users", "email", emailAddress); - log(`user is`, user); - extra.user = user.getId(); - user.setVerified(false); - dao.saveRecord(user); - audit("PBOUNCE", `User ${emailAddress} has been disabled`, extra); - } catch (e) { - audit("PBOUNCE_ERR", `${e}`, extra); - } - }; - const raw = readerToString(c.request().body); - const data = JSON.parse(raw); - log(JSON.stringify(data, null, 2)); - if (isSnsSubscriptionConfirmationEvent(data)) { - const url = data.SubscribeURL; - log(url); - $http.send({ url }); - return c.json(200, { status: "ok" }); - } - if (isSnsNotificationEvent(data)) { - const msg = JSON.parse(data.Message); - log(msg); - if (isSnsNotificationBouncePayload(msg)) { - log(`Message is a bounce`); - const { bounce } = msg; - const { bounceType } = bounce; - switch (bounceType) { - case `Permanent`: - log(`Message is a permanent bounce`); - const { bouncedRecipients } = bounce; - bouncedRecipients.forEach((recipient) => { - const { emailAddress } = recipient; - processBounce(emailAddress); - }); - break; - default: - audit("SNS_ERR", `Unrecognized bounce type ${bounceType}`, { - raw - }); - } - } else if (isSnsNotificationComplaintPayload(msg)) { - log(`Message is a Complaint`, msg); - const { complaint } = msg; - const { complainedRecipients } = complaint; - complainedRecipients.forEach((recipient) => { - const { emailAddress } = recipient; - log(`Processing ${emailAddress}`); - try { - const user = $app.dao().findFirstRecordByData("users", "email", emailAddress); - log(`user is`, user); - user.set(`unsubscribe`, true); - dao.saveRecord(user); - audit("COMPLAINT", `User ${emailAddress} has been unsubscribed`, { - emailAddress, - user: user.getId() - }); - } catch (e) { - audit("COMPLAINT_ERR", `${emailAddress} is not in the system.`, { - emailAddress - }); - } - }); - } else { - audit("SNS_ERR", `Unrecognized notification type ${data.Type}`, { - raw - }); - } - } - audit(`SNS_ERR`, `Message ${data.Type} not handled`, { - raw - }); - return c.json(200, { status: "ok" }); +const HandleSesError = (c) => { + const dao = $app.dao(); + const log = mkLog(`sns`); + const audit = mkAudit(log, dao); + const processBounce = (emailAddress) => { + log(`Processing ${emailAddress}`); + const extra = { email: emailAddress }; + try { + const user = dao.findFirstRecordByData("users", "email", emailAddress); + log(`user is`, user); + extra.user = user.getId(); + user.setVerified(false); + dao.saveRecord(user); + audit("PBOUNCE", `User ${emailAddress} has been disabled`, extra); + } catch (e) { + audit("PBOUNCE_ERR", `${e}`, extra); + } + }; + const raw = readerToString(c.request().body); + const data = JSON.parse(raw); + log(JSON.stringify(data, null, 2)); + if (isSnsSubscriptionConfirmationEvent(data)) { + const url = data.SubscribeURL; + log(url); + $http.send({ url }); + return c.json(200, { status: "ok" }); + } + if (isSnsNotificationEvent(data)) { + const msg = JSON.parse(data.Message); + log(msg); + if (isSnsNotificationBouncePayload(msg)) { + log(`Message is a bounce`); + const { bounce } = msg; + const { bounceType } = bounce; + switch (bounceType) { + case `Permanent`: + log(`Message is a permanent bounce`); + const { bouncedRecipients } = bounce; + bouncedRecipients.forEach((recipient) => { + const { emailAddress } = recipient; + processBounce(emailAddress); + }); + break; + default: audit("SNS_ERR", `Unrecognized bounce type ${bounceType}`, { raw }); + } + } else if (isSnsNotificationComplaintPayload(msg)) { + log(`Message is a Complaint`, msg); + const { complaint } = msg; + const { complainedRecipients } = complaint; + complainedRecipients.forEach((recipient) => { + const { emailAddress } = recipient; + log(`Processing ${emailAddress}`); + try { + const user = $app.dao().findFirstRecordByData("users", "email", emailAddress); + log(`user is`, user); + user.set(`unsubscribe`, true); + dao.saveRecord(user); + audit("COMPLAINT", `User ${emailAddress} has been unsubscribed`, { + emailAddress, + user: user.getId() + }); + } catch (e) { + audit("COMPLAINT_ERR", `${emailAddress} is not in the system.`, { emailAddress }); + } + }); + } else audit("SNS_ERR", `Unrecognized notification type ${data.Type}`, { raw }); + } + audit(`SNS_ERR`, `Message ${data.Type} not handled`, { raw }); + return c.json(200, { status: "ok" }); }; -// src/lib/handlers/stats/api/HandleStatsRequest.ts -var HandleStatsRequest = (c) => { - const result = new DynamicModel({ - total_flounder_subscribers: 0 - }); - $app.dao().db().select("total_flounder_subscribers").from("stats").one(result); - return c.json(200, result); +//#endregion +//#region src/lib/handlers/stats/api/HandleStatsRequest.ts +const HandleStatsRequest = (c) => { + const result = new DynamicModel({ total_flounder_subscribers: 0 }); + $app.dao().db().select("total_flounder_subscribers").from("stats").one(result); + return c.json(200, result); }; -// src/lib/handlers/user/api/HandleUserTokenRequest.ts -var HandleUserTokenRequest = (c) => { - const dao = $app.dao(); - const log = mkLog(`user-token`); - const id = c.pathParam("id"); - if (!id) { - throw new BadRequestError(`User ID is required.`); - } - const rec = dao.findRecordById("users", id); - const tokenKey = rec.getString("tokenKey"); - const passwordHash = rec.getString("passwordHash"); - const email = rec.getString(`email`); - return c.json(200, { email, passwordHash, tokenKey }); +//#endregion +//#region src/lib/handlers/user/api/HandleUserTokenRequest.ts +const HandleUserTokenRequest = (c) => { + const dao = $app.dao(); + const log = mkLog(`user-token`); + const id = c.pathParam("id"); + if (!id) throw new BadRequestError(`User ID is required.`); + const rec = dao.findRecordById("users", id); + const tokenKey = rec.getString("tokenKey"); + const passwordHash = rec.getString("passwordHash"); + const email = rec.getString(`email`); + return c.json(200, { + email, + passwordHash, + tokenKey + }); }; -// src/lib/handlers/versions/api/HandleVersionsRequest.ts -var HandleVersionsRequest = (c) => { - return c.json(200, { versions }); +//#endregion +//#region src/lib/handlers/versions/api/HandleVersionsRequest.ts +/** Return a list of available PocketBase versions */ +const HandleVersionsRequest = (c) => { + return c.json(200, { versions }); }; -// Annotate the CommonJS export names for ESM import in node: -0 && (module.exports = { - HandleInstanceBeforeUpdate, - HandleInstanceCreate, - HandleInstanceDelete, - HandleInstanceResolve, - HandleInstanceUpdate, - HandleInstanceVersionValidation, - HandleInstancesResetIdle, - HandleLemonSqueezySale, - HandleMailSend, - HandleMetaUpdateAtBoot, - HandleMigrateInstanceVersions, - HandleMigrateRegions, - HandleMirrorData, - HandleNotifyDiscordAfterCreate, - HandleProcessNotification, - HandleProcessSingleNotification, - HandleSesError, - HandleSignupCheck, - HandleSignupConfirm, - HandleStatsRequest, - HandleUserTokenRequest, - HandleUserWelcomeMessage, - HandleVersionsRequest -}); + +//#endregion +exports.HandleInstanceBeforeUpdate = HandleInstanceBeforeUpdate; +exports.HandleInstanceCreate = HandleInstanceCreate; +exports.HandleInstanceDelete = HandleInstanceDelete; +exports.HandleInstanceResolve = HandleInstanceResolve; +exports.HandleInstanceUpdate = HandleInstanceUpdate; +exports.HandleInstanceVersionValidation = HandleInstanceVersionValidation; +exports.HandleInstancesResetIdle = HandleInstancesResetIdle; +exports.HandleLemonSqueezySale = HandleLemonSqueezySale; +exports.HandleMailSend = HandleMailSend; +exports.HandleMetaUpdateAtBoot = HandleMetaUpdateAtBoot; +exports.HandleMigrateCnamesToDomains = HandleMigrateCnamesToDomains; +exports.HandleMigrateInstanceVersions = HandleMigrateInstanceVersions; +exports.HandleMigrateRegions = HandleMigrateRegions; +exports.HandleMirrorData = HandleMirrorData; +exports.HandleNotifyDiscordAfterCreate = HandleNotifyDiscordAfterCreate; +exports.HandleProcessNotification = HandleProcessNotification; +exports.HandleProcessSingleNotification = HandleProcessSingleNotification; +exports.HandleSesError = HandleSesError; +exports.HandleSignupCheck = HandleSignupCheck; +exports.HandleSignupConfirm = HandleSignupConfirm; +exports.HandleStatsRequest = HandleStatsRequest; +exports.HandleUserTokenRequest = HandleUserTokenRequest; +exports.HandleUserWelcomeMessage = HandleUserWelcomeMessage; +exports.HandleVersionsRequest = HandleVersionsRequest; \ No newline at end of file diff --git a/packages/pockethost/src/mothership-app/pb_hooks/mothership.pb.js b/packages/pockethost/src/mothership-app/pb_hooks/mothership.pb.js index bf60b150..414eb0f4 100644 --- a/packages/pockethost/src/mothership-app/pb_hooks/mothership.pb.js +++ b/packages/pockethost/src/mothership-app/pb_hooks/mothership.pb.js @@ -1,128 +1,111 @@ -// src/lib/handlers/instance/hooks.ts -routerAdd( - "PUT", - "/api/instance/:id", - (c) => { - return require(`${__hooks}/mothership`).HandleInstanceUpdate(c); - }, - $apis.requireRecordAuth() -); -routerAdd( - "POST", - "/api/instance", - (c) => { - return require(`${__hooks}/mothership`).HandleInstanceCreate(c); - }, - $apis.requireRecordAuth() -); -routerAdd( - "DELETE", - "/api/instance/:id", - (c) => { - return require(`${__hooks}/mothership`).HandleInstanceDelete(c); - }, - $apis.requireRecordAuth() -); -routerAdd( - "GET", - "/api/instance/resolve", - (c) => { - return require(`${__hooks}/mothership`).HandleInstanceResolve(c); - }, - $apis.requireAdminAuth() -); + +//#region src/lib/handlers/instance/hooks.ts +routerAdd("PUT", "/api/instance/:id", (c) => { + return require(`${__hooks}/mothership`).HandleInstanceUpdate(c); +}, $apis.requireRecordAuth()); +routerAdd("POST", "/api/instance", (c) => { + return require(`${__hooks}/mothership`).HandleInstanceCreate(c); +}, $apis.requireRecordAuth()); +routerAdd("DELETE", "/api/instance/:id", (c) => { + return require(`${__hooks}/mothership`).HandleInstanceDelete(c); +}, $apis.requireRecordAuth()); +routerAdd("GET", "/api/instance/resolve", (c) => { + return require(`${__hooks}/mothership`).HandleInstanceResolve(c); +}, $apis.requireAdminAuth()); +/** Validate instance version */ onModelBeforeCreate((e) => { - return require(`${__hooks}/mothership`).HandleInstanceVersionValidation(e); -}, "instances"); -onModelAfterCreate((e) => { + return require(`${__hooks}/mothership`).HandleInstanceVersionValidation(e); }, "instances"); +onModelAfterCreate((e) => {}, "instances"); +onAfterBootstrap((e) => {}); +onAfterBootstrap((e) => {}); +/** Reset instance status to idle on start */ onAfterBootstrap((e) => { + return require(`${__hooks}/mothership`).HandleInstancesResetIdle(e); }); +/** Migrate existing cnames to domains table */ onAfterBootstrap((e) => { + return require(`${__hooks}/mothership`).HandleMigrateCnamesToDomains(e); }); -onAfterBootstrap((e) => { - return require(`${__hooks}/mothership`).HandleInstancesResetIdle(e); -}); +/** Validate instance version */ onModelBeforeUpdate((e) => { - return require(`${__hooks}/mothership`).HandleInstanceBeforeUpdate(e); + return require(`${__hooks}/mothership`).HandleInstanceBeforeUpdate(e); }, "instances"); -// src/lib/handlers/lemon/hooks.ts +//#endregion +//#region src/lib/handlers/lemon/hooks.ts routerAdd("POST", "/api/ls", (c) => { - return require(`${__hooks}/mothership`).HandleLemonSqueezySale(c); + return require(`${__hooks}/mothership`).HandleLemonSqueezySale(c); }); -// src/lib/handlers/mail/hooks.ts -routerAdd( - "POST", - "/api/mail", - (c) => { - return require(`${__hooks}/mothership`).HandleMailSend(c); - }, - $apis.requireAdminAuth() -); +//#endregion +//#region src/lib/handlers/mail/hooks.ts +routerAdd("POST", "/api/mail", (c) => { + return require(`${__hooks}/mothership`).HandleMailSend(c); +}, $apis.requireAdminAuth()); -// src/lib/handlers/meta/hooks.ts +//#endregion +//#region src/lib/handlers/meta/hooks.ts onAfterBootstrap((e) => { - return require(`${__hooks}/mothership`).HandleMetaUpdateAtBoot(e); + return require(`${__hooks}/mothership`).HandleMetaUpdateAtBoot(e); }); -// src/lib/handlers/mirror/hooks.ts -routerAdd( - "GET", - "/api/mirror", - (c) => { - return require(`${__hooks}/mothership`).HandleMirrorData(c); - }, - $apis.gzip(), - $apis.requireAdminAuth() -); +//#endregion +//#region src/lib/handlers/mirror/hooks.ts +routerAdd("GET", "/api/mirror", (c) => { + return require(`${__hooks}/mothership`).HandleMirrorData(c); +}, $apis.gzip(), $apis.requireAdminAuth()); -// src/lib/handlers/notify/hooks.ts +//#endregion +//#region src/lib/handlers/notify/hooks.ts routerAdd(`GET`, `api/process_single_notification`, (c) => { - return require(`${__hooks}/mothership`).HandleProcessSingleNotification(c); + return require(`${__hooks}/mothership`).HandleProcessSingleNotification(c); }); onModelAfterCreate((e) => { - return require(`${__hooks}/mothership`).HandleProcessNotification(e); + return require(`${__hooks}/mothership`).HandleProcessNotification(e); }, `notifications`); onModelBeforeUpdate((e) => { - return require(`${__hooks}/mothership`).HandleUserWelcomeMessage(e); + return require(`${__hooks}/mothership`).HandleUserWelcomeMessage(e); }, "users"); -// src/lib/handlers/outpost/hooks.ts +//#endregion +//#region src/lib/handlers/outpost/hooks.ts routerAdd("GET", "/api/unsubscribe", (c) => { - return require(`${__hooks}/mothership`).HandleOutpostUnsubscribe(c); + return require(`${__hooks}/mothership`).HandleOutpostUnsubscribe(c); }); -// src/lib/handlers/signup/hooks.ts +//#endregion +//#region src/lib/handlers/signup/hooks.ts routerAdd("GET", "/api/signup", (c) => { - return require(`${__hooks}/mothership`).HandleSignupCheck(c); + return require(`${__hooks}/mothership`).HandleSignupCheck(c); }); routerAdd("POST", "/api/signup", (c) => { - return require(`${__hooks}/mothership`).HandleSignupConfirm(c); + return require(`${__hooks}/mothership`).HandleSignupConfirm(c); }); -// src/lib/handlers/sns/hooks.ts +//#endregion +//#region src/lib/handlers/sns/hooks.ts routerAdd("POST", "/api/sns", (c) => { - return require(`${__hooks}/mothership`).HandleSesError(c); + return require(`${__hooks}/mothership`).HandleSesError(c); }); -// src/lib/handlers/stats/hooks.ts +//#endregion +//#region src/lib/handlers/stats/hooks.ts routerAdd("GET", "/api/stats", (c) => { - return require(`${__hooks}/mothership`).HandleStatsRequest(c); + return require(`${__hooks}/mothership`).HandleStatsRequest(c); }); -// src/lib/handlers/user/hooks.ts -routerAdd( - "GET", - "/api/userToken/:id", - (c) => { - return require(`${__hooks}/mothership`).HandleUserTokenRequest(c); - }, - $apis.requireAdminAuth() -); +//#endregion +//#region src/lib/handlers/user/hooks.ts +routerAdd("GET", "/api/userToken/:id", (c) => { + return require(`${__hooks}/mothership`).HandleUserTokenRequest(c); +}, $apis.requireAdminAuth()); -// src/lib/handlers/versions/hooks.ts +//#endregion +//#region src/lib/handlers/versions/hooks.ts +/** Return a list of available PocketBase versions */ routerAdd("GET", "/api/versions", (c) => { - return require(`${__hooks}/mothership`).HandleVersionsRequest(c); + return require(`${__hooks}/mothership`).HandleVersionsRequest(c); }); + +//#endregion \ No newline at end of file diff --git a/packages/pockethost/src/mothership-app/pb_migrations/1752815917_created_domains.js b/packages/pockethost/src/mothership-app/pb_migrations/1752815917_created_domains.js new file mode 100644 index 00000000..0f39608b --- /dev/null +++ b/packages/pockethost/src/mothership-app/pb_migrations/1752815917_created_domains.js @@ -0,0 +1,76 @@ +/// +migrate( + (db) => { + const collection = new Collection({ + id: 'jw1wz9nmkvnhcm6', + created: '2025-07-18 05:18:37.698Z', + updated: '2025-07-18 05:18:37.698Z', + name: 'domains', + type: 'base', + system: false, + schema: [ + { + system: false, + id: 'fhie5snn', + name: 'instance', + type: 'relation', + required: false, + presentable: false, + unique: false, + options: { + collectionId: 'etae8tuiaxl6xfv', + cascadeDelete: false, + minSelect: null, + maxSelect: 1, + displayFields: null, + }, + }, + { + system: false, + id: 'wn3oncif', + name: 'domain', + type: 'text', + required: false, + presentable: false, + unique: false, + options: { + min: null, + max: null, + pattern: '', + }, + }, + { + system: false, + id: 'vzkcvhhg', + name: 'active', + type: 'bool', + required: false, + presentable: false, + unique: false, + options: {}, + }, + ], + indexes: [ + 'CREATE UNIQUE INDEX `idx_gtGcwf2` ON `domains` (`domain`)', + 'CREATE INDEX `idx_OtPOwXe` ON `domains` (`instance`)', + 'CREATE INDEX `idx_0omsTdi` ON `domains` (`created`)', + 'CREATE INDEX `idx_uMaIOVQ` ON `domains` (`updated`)', + 'CREATE UNIQUE INDEX `idx_VLBOSap` ON `domains` (\n `instance`,\n `domain`\n)', + ], + listRule: null, + viewRule: null, + createRule: null, + updateRule: null, + deleteRule: null, + options: {}, + }) + + return Dao(db).saveCollection(collection) + }, + (db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId('jw1wz9nmkvnhcm6') + + return dao.deleteCollection(collection) + }, +) diff --git a/packages/pockethost/src/mothership-app/src/lib/handlers/instance/bootstrap/HandleMigrateCnamesToDomains.ts b/packages/pockethost/src/mothership-app/src/lib/handlers/instance/bootstrap/HandleMigrateCnamesToDomains.ts new file mode 100644 index 00000000..44cf322e --- /dev/null +++ b/packages/pockethost/src/mothership-app/src/lib/handlers/instance/bootstrap/HandleMigrateCnamesToDomains.ts @@ -0,0 +1,76 @@ +import { mkLog } from '$util/Logger' + +export const HandleMigrateCnamesToDomains = (e: core.BootstrapEvent) => { + const dao = $app.dao() + const log = mkLog(`bootstrap:migrate-cnames`) + + log(`Starting cname to domains migration`) + + try { + // Check if domains table exists + const domainsCollection = dao.findCollectionByNameOrId('domains') + if (!domainsCollection) { + log(`Domains collection not found, skipping migration`) + return + } + + // Check if there are any instances with cnames that haven't been migrated + log(`Checking for instances with cnames`) + const instancesWithCnames = dao.findRecordsByFilter( + 'instances', + "cname != NULL && cname != ''", + ) + + if (instancesWithCnames.length === 0) { + log(`No cnames to migrate`) + return + } + + log(`Found ${instancesWithCnames.length} instances with cnames`) + + // Check which ones are already migrated + const unmigrated = instancesWithCnames.filter((instance) => { + if (!instance) return false + try { + const existingDomain = dao.findFirstRecordByFilter( + 'domains', + `instance = "${instance.getId()}"`, + ) + return !existingDomain + } catch (e) { + // Not found means not migrated + return true + } + }) + + if (unmigrated.length === 0) { + log(`All cnames already migrated`) + return + } + + log(`Found ${unmigrated.length} cnames to migrate`) + + // Migrate cnames to domains table + let migrated = 0 + unmigrated.forEach((instance) => { + if (!instance) return + try { + const domainsCollection = dao.findCollectionByNameOrId('domains') + const domainRecord = new Record(domainsCollection) + + domainRecord.set('instance', instance.getId()) + domainRecord.set('domain', instance.getString('cname')) + domainRecord.set('active', instance.getBool('cname_active')) + + dao.saveRecord(domainRecord) + migrated++ + } catch (error) { + log(`Failed to migrate cname for instance ${instance.getId()}:`, error) + } + }) + + log(`Successfully migrated ${migrated} cnames to domains table`) + } catch (error) { + log(`Error migrating cnames: ${error}`) + } +} diff --git a/packages/pockethost/src/mothership-app/src/lib/handlers/instance/hooks.ts b/packages/pockethost/src/mothership-app/src/lib/handlers/instance/hooks.ts index a981e502..62cffdca 100644 --- a/packages/pockethost/src/mothership-app/src/lib/handlers/instance/hooks.ts +++ b/packages/pockethost/src/mothership-app/src/lib/handlers/instance/hooks.ts @@ -52,6 +52,11 @@ onAfterBootstrap((e) => { return require(`${__hooks}/mothership`).HandleInstancesResetIdle(e) }) +/** Migrate existing cnames to domains table */ +onAfterBootstrap((e) => { + return require(`${__hooks}/mothership`).HandleMigrateCnamesToDomains(e) +}) + /** Validate instance version */ onModelBeforeUpdate((e) => { return require(`${__hooks}/mothership`).HandleInstanceBeforeUpdate(e) diff --git a/packages/pockethost/src/mothership-app/src/lib/handlers/instance/index.ts b/packages/pockethost/src/mothership-app/src/lib/handlers/instance/index.ts index 9c30ce1f..910ef2f7 100644 --- a/packages/pockethost/src/mothership-app/src/lib/handlers/instance/index.ts +++ b/packages/pockethost/src/mothership-app/src/lib/handlers/instance/index.ts @@ -3,6 +3,7 @@ export * from './api/HandleInstanceDelete' export * from './api/HandleInstanceResolve' export * from './api/HandleInstanceUpdate' export * from './bootstrap/HandleInstancesResetIdle' +export * from './bootstrap/HandleMigrateCnamesToDomains' export * from './bootstrap/HandleMigrateInstanceVersions' export * from './bootstrap/HandleMigrateRegions' export * from './model/HandleInstanceBeforeUpdate'