mothership bundle

This commit is contained in:
Ben Allfree 2024-11-21 19:46:56 -08:00
parent e6f4618101
commit 8f382dce79
94 changed files with 5207 additions and 1418 deletions

View File

@ -53,11 +53,13 @@
"rizzdown",
"Rpcs",
"semvers",
"Squeezy",
"sslify",
"superadmin",
"syslogd",
"tailable",
"TRYFETCH",
"tsup",
"Ulimits",
"unsub",
"Unsub",
@ -66,6 +68,7 @@
"Unwatching",
"unzipper",
"upserting",
"Virtio"
"Virtio",
"xsignature"
]
}

View File

@ -1,5 +1,11 @@
# pockethost
## 2.0.2
### Patch Changes
- Refactored mothership to TS using JSVM bundling
## 2.0.1
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "pockethost",
"version": "2.0.1",
"version": "2.0.2",
"author": {
"name": "Ben Allfree",
"url": "https://github.com/benallfree"

View File

@ -94,7 +94,7 @@ export const createSettings = () => ({
MOTHERSHIP_ADMIN_PASSWORD: mkString(),
PH_MOTHERSHIP_APP_ROOT: mkString(_MOTHERSHIP_APP_ROOT()),
MOTHERSHIP_MIGRATIONS_DIR: mkPath(_MOTHERSHIP_APP_ROOT(`pb_migrations`)),
MOTHERSHIP_HOOKS_DIR: mkPath(_MOTHERSHIP_APP_ROOT(`pb_hooks`, `src`)),
MOTHERSHIP_HOOKS_DIR: mkPath(_MOTHERSHIP_APP_ROOT(`pb_hooks`)),
MOTHERSHIP_APP_DIR: mkPath(_MOTHERSHIP_APP_ROOT(`ph_app`), {
required: false,
}),

View File

@ -1,6 +1,12 @@
{
"name": "pockethost-mothership-app",
"dependencies": {
"pocketpages": "^0.8.0"
"scripts": {
"build": "tsup",
"dev": "tsup --watch"
},
"devDependencies": {
"@s-libs/micro-dash": "^18.0.0",
"tsup": "^8.3.5",
"type-fest": "^4.6.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,122 @@
// 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()
);
onModelBeforeCreate((e) => {
return require(`${__hooks}/mothership`).HandleInstanceVersionValidation(e);
}, "instances");
onModelAfterUpdate((e) => {
}, "instances");
onModelAfterCreate((e) => {
}, "instances");
onAfterBootstrap((e) => {
});
onAfterBootstrap((e) => {
});
onAfterBootstrap((e) => {
return require(`${__hooks}/mothership`).HandleInstancesResetIdle(e);
});
onModelBeforeUpdate((e) => {
return require(`${__hooks}/mothership`).HandleInstanceBeforeUpdate(e);
}, "instances");
// src/lib/handlers/lemon/hooks.ts
routerAdd("POST", "/api/ls", (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()
);
// src/lib/handlers/meta/hooks.ts
onAfterBootstrap((e) => {
return require(`${__hooks}/mothership`).HandleMetaUpdateAtBoot(e);
});
// src/lib/handlers/mirror/hooks.ts
routerAdd(
"GET",
"/api/mirror",
(c) => {
return require(`${__hooks}/HandleMirrorData`).HandleMirrorData(c);
},
$apis.gzip(),
$apis.requireAdminAuth()
);
// src/lib/handlers/notify/hooks.ts
routerAdd(`GET`, `api/process_single_notification`, (c) => {
return require(`${__hooks}/mothership`).HandleProcessSingleNotification(c);
});
onModelAfterCreate((e) => {
return require(`${__hooks}/mothership`).HandleProcessNotification(e);
}, `notifications`);
onModelBeforeUpdate((e) => {
return require(`${__hooks}/mothership`).HandleUserWelcomeMessage(e);
}, "users");
// src/lib/handlers/outpost/hooks.ts
routerAdd("GET", "/api/unsubscribe", (c) => {
return require(`${__hooks}/mothership`).HandleOutpostUnsubscribe(c);
});
// src/lib/handlers/signup/hooks.ts
routerAdd("GET", "/api/signup", (c) => {
return require(`${__hooks}/mothership`).HandleSignupCheck(c);
});
routerAdd("POST", "/api/signup", (c) => {
return require(`${__hooks}/mothership`).HandleSignupConfirm(c);
});
// src/lib/handlers/sns/hooks.ts
routerAdd("POST", "/api/sns", (c) => {
return require(`${__hooks}/mothership`).HandleSesError(c);
});
// src/lib/handlers/stats/hooks.ts
routerAdd("GET", "/api/stats", (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()
);
// src/lib/handlers/versions/hooks.ts
routerAdd("GET", "/api/versions", (c) => {
return require(`${__hooks}/mothership`).HandleVersionsRequest(c);
});

View File

@ -1,65 +0,0 @@
/*
{
"subdomain": "foo"
}
*/
routerAdd(
'POST',
'/api/instance',
(c) => {
const dao = $app.dao()
const { mkLog, versions } = /** @type {Lib} */ (
require(`${__hooks}/lib.js`)
)
const log = mkLog(`POST:instance`)
const authRecord = /** @type {models.Record} */ (c.get('authRecord')) // empty if not authenticated as regular auth record
log(`***authRecord`, JSON.stringify(authRecord))
if (!authRecord) {
throw new Error(`Expected authRecord here`)
}
log(`***TOP OF POST`)
let data = /** @type{ {subdomain?: string} } */ (
new DynamicModel({
subdomain: '',
})
)
log(`***before bind`)
c.bind(data)
log(`***after bind`)
// This is necessary for destructuring to work correctly
data = JSON.parse(JSON.stringify(data))
const { subdomain, 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('status', 'idle')
record.set('version', versions[0])
record.set('syncAdmin', true)
record.set('notifyMaintenanceMode', true)
const form = new RecordUpsertForm($app, record)
form.submit()
return c.json(200, { instance: record })
},
$apis.requireRecordAuth(),
)

View File

@ -1,69 +0,0 @@
/*
{
"id": "kz4ngg77eaw1ho0",
"fields": {
"maintenance": true
"name": '',
"version": ''
}
}
*/
routerAdd(
'DELETE',
'/api/instance/:id',
(c) => {
const dao = $app.dao()
const { mkLog } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
const log = mkLog(`DELETE:instance`)
log(`TOP OF DELETE`)
let data = new DynamicModel({
id: '',
})
c.bind(data)
log(`After bind`)
// This is necessary for destructuring to work correctly
data = JSON.parse(JSON.stringify(data))
const id = c.pathParam('id')
log(
`vars`,
JSON.stringify({
id,
}),
)
const authRecord = /** @type {models.Record} */ (c.get('authRecord')) // empty if not authenticated as regular auth record
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' })
},
$apis.requireRecordAuth(),
)

View File

@ -1,7 +0,0 @@
/// <reference path="../types/types.d.ts" />
/** Reset instance status to idle on start */
onAfterBootstrap((e) => {
const dao = $app.dao()
dao.db().newQuery(`update instances set status='idle'`).execute()
})

View File

@ -1,121 +0,0 @@
/// <reference path="../types/types.d.ts" />
/*
{
"id": "kz4ngg77eaw1ho0",
"fields": {
"maintenance": true
"name": '',
"version": ''
}
}
*/
routerAdd(
'PUT',
'/api/instance/:id',
(c) => {
const dao = $app.dao()
const { mkLog, removeEmptyKeys } = /** @type {Lib} */ (
require(`${__hooks}/lib.js`)
)
const log = mkLog(`PUT:instance`)
log(`TOP OF PUT`)
let data =
/**
* @type {{
* id: string
* fields: {
* subdomain: string | null
* maintenance: boolean | null
* version: string | null
* secrets: StringKvLookup | null
* syncAdmin: boolean | null
* dev: boolean | null
* cname: string | null
* notifyMaintenanceMode: boolean | null
* }
* }} }
*/
(
new DynamicModel({
id: '',
fields: {
subdomain: null,
maintenance: null,
version: null,
secrets: null,
syncAdmin: null,
dev: null,
cname: null,
notifyMaintenanceMode: null,
},
})
)
c.bind(data)
log(`After bind`)
// This is necessary for destructuring to work correctly
data = JSON.parse(JSON.stringify(data))
const id = c.pathParam('id')
const {
fields: {
subdomain,
maintenance,
version,
secrets,
syncAdmin,
dev,
cname,
notifyMaintenanceMode,
},
} = data
log(
`vars`,
JSON.stringify({
id,
subdomain,
maintenance,
version,
secrets,
syncAdmin,
dev,
cname,
notifyMaintenanceMode,
}),
)
const record = dao.findRecordById('instances', id)
const authRecord = /** @type {models.Record} */ (c.get('authRecord')) // empty if not authenticated as regular auth record
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,
maintenance,
secrets,
syncAdmin,
dev,
cname,
notifyMaintenanceMode,
})
const form = new RecordUpsertForm($app, record)
form.loadData(sanitized)
form.submit()
return c.json(200, { status: 'ok' })
},
$apis.requireRecordAuth(),
)

View File

@ -1,183 +0,0 @@
/// <reference types="../types/lib.d.ts" />
/** @type {Lib['mkAudit']} */
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,
}),
)
}
}
/** @type {Lib['mkLog']} */
const mkLog =
(namespace) =>
/**
* @param {...any} s
* @returns
*/
(...s) =>
console.log(
`[${namespace}]`,
...s.map((p) => {
if (typeof p === 'object') return JSON.stringify(p, null, 2)
return p
}),
)
/**
* @param {...any} args
* @returns
*/
const dbg = (...args) => console.log(args)
/**
* @param {string} template
* @param {{ [_: string]: string }} dict
* @returns
*/
function interpolateString(template, dict) {
return template.replace(/\{\$(\w+)\}/g, (match, key) => {
dbg({ match, key })
const lowerKey = key.toLowerCase()
return dict.hasOwnProperty(lowerKey) ? dict[lowerKey] || '' : match
})
}
/** @type {Lib['mkNotifier']} */
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)
}
/** @type {Lib['mkNotificationProcessor']} */
const mkNotificationProcessor =
(log, dao, test = false) =>
(notificationRec) => {
log({ notificationRec })
const channel = notificationRec.getString(`channel`)
dao.expandRecord(notificationRec, ['message_template', 'user'], null)
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<mailer.Message_In>} */
const msgArgs = {
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,
}
if (test) {
msgArgs.to = [{ address: `ben@benallfree.com` }]
msgArgs.subject = `***TEST ${to} *** ${msgArgs.subject}`
}
log({ msgArgs })
// @ts-ignore
const msg = new MailerMessage(msgArgs)
$app.newMailClient().send(msg)
log(`email sent`)
break
case `lemonbot`:
const params = {
url: test
? process.env.DISCORD_TEST_CHANNEL_URL
: process.env.DISCORD_STREAM_CHANNEL_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)
}
}
/**
* @template T
* @param {T} obj
* @returns T
*/
function removeEmptyKeys(obj) {
const sanitized = Object.entries(obj).reduce((acc, [key, value]) => {
if (value !== null && value !== undefined) {
acc[key] = value
}
return acc
}, /** @type {T} */ ({}))
return sanitized
}
/** @type {Lib['versions']} */
const versions = require(`${__hooks}/versions.cjs`)
module.exports = {
mkAudit,
mkNotificationProcessor,
mkLog,
mkNotifier,
removeEmptyKeys,
versions,
}

View File

@ -1,52 +0,0 @@
/// <reference path="../types/types.d.ts" />
routerAdd(
'POST',
'/api/mail',
(c) => {
const { mkLog } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
const log = mkLog(`mail`)
let data = /** @type {{ to: string; subject: string; body: string }} */ (
new DynamicModel({
to: '',
subject: '',
body: '',
})
)
log(`before bind`)
c.bind(data)
log(`after bind`)
// This is necessary for destructuring to work correctly
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: subject,
html: body,
})
$app.newMailClient().send(email)
const msg = `Sent to ${to}`
log(msg)
return c.json(200, { status: 'ok' })
},
$apis.requireAdminAuth(),
)

View File

@ -1,22 +0,0 @@
/// <reference path="../types/types.d.ts" />
routerAdd(
'GET',
'/api/mirror',
(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 })
},
$apis.gzip(),
$apis.requireAdminAuth(),
)

View File

@ -1,61 +0,0 @@
/// <reference path="../types/types.d.ts" />
/// <reference path="../types/lib.d.ts" />
routerAdd(`GET`, `api/process_single_notification`, (c) => {
const { mkLog, mkNotificationProcessor } = /** @type {Lib} */ (
require(`${__hooks}/lib.js`)
)
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' })
})
onModelAfterCreate((e) => {
const dao = e.dao || $app.dao()
const { mkNotificationProcessor, mkLog, mkAudit } = /** @type {Lib} */ (
require(`${__hooks}/lib.js`)
)
const log = mkLog(`notification:sendImmediately`)
const audit = mkAudit(log, dao)
const processNotification = mkNotificationProcessor(log, dao, false)
const notificationRec = /** @type {models.Record} */ (e.model)
log({ notificationRec })
try {
dao.expandRecord(notificationRec, ['message_template'], null)
const messageTemplateRec = notificationRec.expandedOne(`message_template`)
if (!messageTemplateRec) {
throw new Error(`Missing message template`)
}
processNotification(notificationRec)
} catch (e) {
audit(`ERROR`, `${e}`, {
notification: notificationRec.getId(),
})
}
}, `notifications`)

View File

@ -1,10 +0,0 @@
// src/pb/pb_hooks/pocodex.pb.ts
try {
require("pocodex/dist/pb").Init();
} catch (e) {
console.log(`WARNING: pocodex not loaded: ${e}`);
if (e instanceof Error) {
console.log(e.stack);
}
}
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vLi4vc3JjL3BiL3BiX2hvb2tzL3BvY29kZXgucGIudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbInRyeSB7XG4gIHJlcXVpcmUoJ3BvY29kZXgvZGlzdC9wYicpLkluaXQoKVxufSBjYXRjaCAoZSkge1xuICBjb25zb2xlLmxvZyhgV0FSTklORzogcG9jb2RleCBub3QgbG9hZGVkOiAke2V9YClcbiAgaWYgKGUgaW5zdGFuY2VvZiBFcnJvcikge1xuICAgIGNvbnNvbGUubG9nKGUuc3RhY2spXG4gIH1cbn1cbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBQSxJQUFJO0FBQ0YsVUFBUSxpQkFBaUIsRUFBRSxLQUFLO0FBQ2xDLFNBQVMsR0FBRztBQUNWLFVBQVEsSUFBSSxnQ0FBZ0MsQ0FBQyxFQUFFO0FBQy9DLE1BQUksYUFBYSxPQUFPO0FBQ3RCLFlBQVEsSUFBSSxFQUFFLEtBQUs7QUFBQSxFQUNyQjtBQUNGOyIsCiAgIm5hbWVzIjogW10KfQo=

View File

@ -1,217 +0,0 @@
/// <reference path="../types/types.d.ts" />
routerAdd(
'GET',
'/api/signup',
(c) => {
/**
* @param {string} slug
* @returns
*/
const isAvailable = (slug) => {
try {
const record = $app
.dao()
.findFirstRecordByData('instances', 'subdomain', slug)
return false
} catch {
return true
}
}
/**
* @param {string} fieldName
* @param {string} slug
* @param {string} description
* @param {StringKvLookup} [extra]
* @returns
*/
const error = (fieldName, slug, description, extra) =>
new ApiError(500, description, {
[fieldName]: new ValidationError(slug, description),
...extra,
})
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 {
const random = require(`${__hooks}/random-words.js`)
let i = 0
while (true) {
i++
if (i > 100) {
return +new Date()
}
const slug = random.generate(2).join(`-`)
if (isAvailable(slug)) return slug
}
}
})()
return c.json(200, { instanceName })
} /* optional middlewares */,
)
/*
// HTTP 200
{
status: 'ok'
}
// HTTP 500
{
"code": 500,
"message": "That user account already exists. Try a password reset.",
"data": {
"email": {
"code": "exists",
"message": "That user account already exists. Try a password reset."
}
}
}
{
"code": 500,
"message": "Instance name was taken, sorry about that. Try another.",
"data": {
"instanceName": {
"code": "exists",
"message": "Instance name was taken, sorry about that. Try another."
}
}
}
*/
// https://pocketbase.io/docs/js-routing/#sending-request-to-custom-routes-using-the-sdks
routerAdd(
'POST',
'/api/signup',
(c) => {
const { versions } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
const dao = $app.dao()
const parsed = (() => {
const rawBody = readerToString(c.request().body)
try {
const parsed = JSON.parse(rawBody)
return parsed
} 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()
/**
* @param {string} fieldName
* @param {string} slug
* @param {string} description
* @param {StringKvLookup} [extra]
* @returns
*/
const error = (fieldName, slug, description, extra) =>
new ApiError(500, description, {
[fieldName]: new ValidationError(slug, description),
...extra,
})
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('notifyMaintenanceMode', true)
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-1`)
instance.set('uid', user.get('id'))
instance.set('status', 'idle')
instance.set('notifyMaintenanceMode', true)
instance.set('syncAdmin', true)
instance.set('version', versions[0])
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' })
} /* optional middlewares */,
)

View File

@ -1,113 +0,0 @@
/// <reference path="../types/types.d.ts" />
routerAdd('POST', '/api/sns', (c) => {
const dao = $app.dao()
const { mkLog, mkAudit } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
const log = mkLog(`sns`)
const audit = mkAudit(log, dao)
const processBounce = (emailAddress) => {
log(`Processing ${emailAddress}`)
const extra = /** @type {{ email: string; user: string }} */ ({
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)
}
}
;[].forEach(processBounce)
const raw = readerToString(c.request().body)
const data = JSON.parse(raw)
log(JSON.stringify(data, null, 2))
const { Type } = data
switch (Type) {
case `SubscriptionConfirmation`:
const url = data.SubscribeURL
log(url)
$http.send({ url })
return c.json(200, { status: 'ok' })
case `Notification`:
const msg = JSON.parse(data.Message)
log(msg)
const { notificationType } = msg
switch (notificationType) {
case `Bounce`: {
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,
})
}
break
}
case `Complaint`:
{
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,
},
)
}
})
}
break
default:
audit(
'SNS_ERR',
`Unrecognized notification type ${notificationType}`,
{ raw },
)
}
break
default:
audit(`SNS_ERR`, `Message ${Type} not handled`, {
raw,
})
}
return c.json(200, { status: 'ok' })
})

View File

@ -1,20 +0,0 @@
/// <reference path="../types/types.d.ts" />
routerAdd(
'GET',
'/api/stats',
(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)
} /* optional middlewares */,
)

View File

@ -1,33 +0,0 @@
/// <reference path="../types/types.d.ts" />
/*
{
"id": "user-id"
}
*/
routerAdd(
'GET',
'/api/userToken/:id',
(c) => {
const dao = $app.dao()
const { mkLog } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
const log = mkLog(`user-token`)
const id = c.pathParam('id')
// log({ 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`)
// log({ email, passwordHash, tokenKey })
return c.json(200, { email, passwordHash, tokenKey })
},
$apis.requireAdminAuth(),
)

View File

@ -1,12 +0,0 @@
/// <reference path="../types/types.d.ts" />
/** Return a list of available PocketBase versions */
routerAdd(
'GET',
'/api/versions',
(c) => {
const { versions } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
return c.json(200, { versions })
} /* optional middlewares */,
)

View File

@ -1,167 +0,0 @@
module.exports = [
"0.22.*",
"0.22.27",
"0.22.26",
"0.22.25",
"0.22.24",
"0.22.23",
"0.22.22",
"0.22.21",
"0.22.20",
"0.22.19",
"0.22.18",
"0.22.17",
"0.22.16",
"0.22.15",
"0.22.14",
"0.22.13",
"0.22.12",
"0.22.11",
"0.22.10",
"0.22.9",
"0.22.8",
"0.22.7",
"0.22.6",
"0.22.5",
"0.22.4",
"0.22.3",
"0.22.2",
"0.22.1",
"0.22.0",
"0.21.*",
"0.21.3",
"0.21.2",
"0.21.1",
"0.21.0",
"0.20.*",
"0.20.7",
"0.20.6",
"0.20.5",
"0.20.4",
"0.20.3",
"0.20.2",
"0.20.1",
"0.20.0",
"0.19.*",
"0.19.4",
"0.19.3",
"0.19.2",
"0.19.1",
"0.19.0",
"0.18.*",
"0.18.10",
"0.18.9",
"0.18.8",
"0.18.7",
"0.18.6",
"0.18.5",
"0.18.4",
"0.18.3",
"0.18.2",
"0.18.1",
"0.18.0",
"0.17.*",
"0.17.7",
"0.17.6",
"0.17.5",
"0.17.4",
"0.17.3",
"0.17.2",
"0.17.1",
"0.17.0",
"0.16.*",
"0.16.10",
"0.16.9",
"0.16.8",
"0.16.7",
"0.16.6",
"0.16.5",
"0.16.4",
"0.16.3",
"0.16.2",
"0.16.1",
"0.16.0",
"0.15.*",
"0.15.3",
"0.15.2",
"0.15.1",
"0.15.0",
"0.14.*",
"0.14.5",
"0.14.4",
"0.14.3",
"0.14.2",
"0.14.1",
"0.14.0",
"0.13.*",
"0.13.4",
"0.13.3",
"0.13.2",
"0.13.1",
"0.13.0",
"0.12.*",
"0.12.3",
"0.12.2",
"0.12.1",
"0.12.0",
"0.11.*",
"0.11.4",
"0.11.3",
"0.11.2",
"0.11.1",
"0.11.0",
"0.10.*",
"0.10.4",
"0.10.3",
"0.10.2",
"0.10.1",
"0.10.0",
"0.9.*",
"0.9.2",
"0.9.1",
"0.9.0",
"0.8.*",
"0.8.0",
"0.7.*",
"0.7.10",
"0.7.9",
"0.7.8",
"0.7.7",
"0.7.6",
"0.7.5",
"0.7.4",
"0.7.3",
"0.7.2",
"0.7.1",
"0.7.0",
"0.6.*",
"0.6.0",
"0.5.*",
"0.5.2",
"0.5.1",
"0.5.0",
"0.4.*",
"0.4.2",
"0.4.1",
"0.4.0",
"0.3.*",
"0.3.4",
"0.3.3",
"0.3.2",
"0.3.1",
"0.3.0",
"0.2.*",
"0.2.8",
"0.2.7",
"0.2.6",
"0.2.5",
"0.2.4",
"0.2.3",
"0.2.2",
"0.2.1",
"0.2.0",
"0.1.*",
"0.1.2",
"0.1.1",
"0.1.0"
]

View File

@ -1 +0,0 @@
function seedrandom(seed: number): void

View File

@ -1,44 +0,0 @@
type Logger = (...args: any[]) => void
type StringKvLookup = { [_: string]: string }
type AuditEvents =
| 'ERROR'
| 'NOTIFICATION_ERR'
| 'LS'
| 'LS_ERR'
| 'PBOUNCE'
| 'PBOUNCE_ERR'
| 'SNS_ERR'
| 'COMPLAINT'
| 'COMPLAINT_ERR'
| 'UNSUBSCRIBE'
| 'UNSUBSCRIBE_ERR'
interface Lib {
mkLog: (namespace: string) => Logger
mkNotificationProcessor: (
log: Logger,
dao: daos.Dao,
test?: boolean,
) => (notificationRec: models.Record) => void
mkNotifier: (
log: Logger,
dao: daos.dao,
) => (
channel: 'email' | 'lemonbot',
template:
| 'maintenance-mode'
| 'lemon_order_email'
| 'lemon_order_discord'
| 'welcome',
user_id: string,
context: { [_: string]: any },
) => void
mkAudit: (
log: Logger,
dao: daos.Dao,
) => (event: AuditEvents, note: string, context: { [_: string]: any }) => void
removeEmptyKeys: <T>(obj: T) => T
versions: string[]
}

View File

@ -0,0 +1,167 @@
module.exports = [
'0.22.*',
'0.22.27',
'0.22.26',
'0.22.25',
'0.22.24',
'0.22.23',
'0.22.22',
'0.22.21',
'0.22.20',
'0.22.19',
'0.22.18',
'0.22.17',
'0.22.16',
'0.22.15',
'0.22.14',
'0.22.13',
'0.22.12',
'0.22.11',
'0.22.10',
'0.22.9',
'0.22.8',
'0.22.7',
'0.22.6',
'0.22.5',
'0.22.4',
'0.22.3',
'0.22.2',
'0.22.1',
'0.22.0',
'0.21.*',
'0.21.3',
'0.21.2',
'0.21.1',
'0.21.0',
'0.20.*',
'0.20.7',
'0.20.6',
'0.20.5',
'0.20.4',
'0.20.3',
'0.20.2',
'0.20.1',
'0.20.0',
'0.19.*',
'0.19.4',
'0.19.3',
'0.19.2',
'0.19.1',
'0.19.0',
'0.18.*',
'0.18.10',
'0.18.9',
'0.18.8',
'0.18.7',
'0.18.6',
'0.18.5',
'0.18.4',
'0.18.3',
'0.18.2',
'0.18.1',
'0.18.0',
'0.17.*',
'0.17.7',
'0.17.6',
'0.17.5',
'0.17.4',
'0.17.3',
'0.17.2',
'0.17.1',
'0.17.0',
'0.16.*',
'0.16.10',
'0.16.9',
'0.16.8',
'0.16.7',
'0.16.6',
'0.16.5',
'0.16.4',
'0.16.3',
'0.16.2',
'0.16.1',
'0.16.0',
'0.15.*',
'0.15.3',
'0.15.2',
'0.15.1',
'0.15.0',
'0.14.*',
'0.14.5',
'0.14.4',
'0.14.3',
'0.14.2',
'0.14.1',
'0.14.0',
'0.13.*',
'0.13.4',
'0.13.3',
'0.13.2',
'0.13.1',
'0.13.0',
'0.12.*',
'0.12.3',
'0.12.2',
'0.12.1',
'0.12.0',
'0.11.*',
'0.11.4',
'0.11.3',
'0.11.2',
'0.11.1',
'0.11.0',
'0.10.*',
'0.10.4',
'0.10.3',
'0.10.2',
'0.10.1',
'0.10.0',
'0.9.*',
'0.9.2',
'0.9.1',
'0.9.0',
'0.8.*',
'0.8.0',
'0.7.*',
'0.7.10',
'0.7.9',
'0.7.8',
'0.7.7',
'0.7.6',
'0.7.5',
'0.7.4',
'0.7.3',
'0.7.2',
'0.7.1',
'0.7.0',
'0.6.*',
'0.6.0',
'0.5.*',
'0.5.2',
'0.5.1',
'0.5.0',
'0.4.*',
'0.4.2',
'0.4.1',
'0.4.0',
'0.3.*',
'0.3.4',
'0.3.3',
'0.3.2',
'0.3.1',
'0.3.0',
'0.2.*',
'0.2.8',
'0.2.7',
'0.2.6',
'0.2.5',
'0.2.4',
'0.2.3',
'0.2.2',
'0.2.1',
'0.2.0',
'0.1.*',
'0.1.2',
'0.1.1',
'0.1.0',
]

View File

@ -0,0 +1,12 @@
import '../lib/handlers/instance/hooks'
import '../lib/handlers/lemon/hooks'
import '../lib/handlers/mail/hooks'
import '../lib/handlers/meta/hooks'
import '../lib/handlers/mirror/hooks'
import '../lib/handlers/notify/hooks'
import '../lib/handlers/outpost/hooks'
import '../lib/handlers/signup/hooks'
import '../lib/handlers/sns/hooks'
import '../lib/handlers/stats/hooks'
import '../lib/handlers/user/hooks'
import '../lib/handlers/versions/hooks'

View File

@ -0,0 +1,11 @@
export * from './instance'
export * from './lemon'
export * from './mail'
export * from './meta'
export * from './mirror'
export * from './notify'
export * from './signup'
export * from './sns'
export * from './stats'
export * from './user'
export * from './versions'

View File

@ -0,0 +1,54 @@
import { mkLog } from '$util/Logger'
import { versions } from '$util/versions'
export const HandleInstanceCreate = (c: echo.Context) => {
const dao = $app.dao()
const log = mkLog(`POST:instance`)
const authRecord = c.get('authRecord') as models.Record | undefined // empty if not authenticated as regular auth record
log(`***authRecord`, JSON.stringify(authRecord))
if (!authRecord) {
throw new Error(`Expected authRecord here`)
}
log(`***TOP OF POST`)
let data = new DynamicModel({
subdomain: '',
region: 'sfo-1',
}) as { subdomain?: string; region?: string }
log(`***before bind`)
c.bind(data)
log(`***after bind`)
// This is necessary for destructuring to work correctly
data = JSON.parse(JSON.stringify(data))
const { subdomain, 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('status', 'idle')
record.set('version', versions[0])
record.set('syncAdmin', true)
record.set('notifyMaintenanceMode', true)
const form = new RecordUpsertForm($app, record)
form.submit()
return c.json(200, { instance: record })
}

View File

@ -0,0 +1,55 @@
import { mkLog } from '$util/Logger'
export const HandleInstanceDelete = (c: echo.Context) => {
const dao = $app.dao()
const log = mkLog(`DELETE:instance`)
log(`TOP OF DELETE`)
let data = new DynamicModel({
id: '',
})
c.bind(data)
log(`After bind`)
// This is necessary for destructuring to work correctly
data = JSON.parse(JSON.stringify(data))
const id = c.pathParam('id')
log(
`vars`,
JSON.stringify({
id,
}),
)
const authRecord = c.get('authRecord') as models.Record | undefined // empty if not authenticated as regular auth record
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' })
}

View File

@ -0,0 +1,98 @@
import { mkLog, StringKvLookup } from '$util/Logger'
import { removeEmptyKeys } from '$util/removeEmptyKeys'
export const HandleInstanceUpdate = (c: echo.Context) => {
const dao = $app.dao()
const log = mkLog(`PUT:instance`)
log(`TOP OF PUT`)
let data = new DynamicModel({
id: '',
fields: {
subdomain: null,
maintenance: null,
version: null,
secrets: null,
syncAdmin: null,
dev: null,
cname: null,
notifyMaintenanceMode: null,
},
}) as {
id: string
fields: {
subdomain: string | null
maintenance: boolean | null
version: string | null
secrets: StringKvLookup | null
syncAdmin: boolean | null
dev: boolean | null
cname: string | null
notifyMaintenanceMode: boolean | null
}
}
c.bind(data)
log(`After bind`)
// This is necessary for destructuring to work correctly
data = JSON.parse(JSON.stringify(data))
const id = c.pathParam('id')
const {
fields: {
subdomain,
maintenance,
version,
secrets,
syncAdmin,
dev,
cname,
notifyMaintenanceMode,
},
} = data
log(
`vars`,
JSON.stringify({
id,
subdomain,
maintenance,
version,
secrets,
syncAdmin,
dev,
cname,
notifyMaintenanceMode,
}),
)
const record = dao.findRecordById('instances', id)
const authRecord = c.get('authRecord') as models.Record | undefined // empty if not authenticated as regular auth record
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,
maintenance,
secrets,
syncAdmin,
dev,
cname,
notifyMaintenanceMode,
})
const form = new RecordUpsertForm($app, record)
form.loadData(sanitized)
form.submit()
return c.json(200, { status: 'ok' })
}

View File

@ -0,0 +1,4 @@
export const HandleInstancesResetIdle = (e: core.BootstrapEvent) => {
const dao = $app.dao()
dao.db().newQuery(`update instances set status='idle'`).execute()
}

View File

@ -1,14 +1,13 @@
/** Migrate version numbers */
onAfterBootstrap((e) => {
import { mkLog } from '$util/Logger'
import { versions } from '$util/versions'
export const HandleMigrateInstanceVersions = (e: core.BootstrapEvent) => {
const dao = $app.dao()
const { audit, mkLog, versions } = /** @type {Lib} */ (
require(`${__hooks}/lib.js`)
)
const log = mkLog(`bootstrap`)
const records = dao.findRecordsByFilter(`instances`, '1=1')
const unrecognized = []
const records = dao.findRecordsByFilter(`instances`, '1=1').filter((r) => !!r)
const unrecognized: string[] = []
records.forEach((record) => {
const v = record.get('version').trim()
if (versions.includes(v)) return
@ -31,4 +30,4 @@ onAfterBootstrap((e) => {
}
})
log({ unrecognized })
})
}

View File

@ -1,5 +1,5 @@
/** Migrate version numbers */
onAfterBootstrap((e) => {
export const HandleMigrateRegions = (e: core.BootstrapEvent) => {
const dao = $app.dao()
console.log(`***Migrating regions`)
@ -7,4 +7,4 @@ onAfterBootstrap((e) => {
.db()
.newQuery(`update instances set region='sfo-1' where region=''`)
.execute()
})
}

View File

@ -0,0 +1,54 @@
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(),
)
/** Validate instance version */
onModelBeforeCreate((e) => {
return require(`${__hooks}/mothership`).HandleInstanceVersionValidation(e)
}, 'instances')
onModelAfterUpdate((e) => {
// return require(`${__hooks}/mothership`).HandleNotifyMaintenanceMode(e)
}, 'instances')
onModelAfterCreate((e) => {
// return require(`${__hooks}/mothership`).HandleNotifyDiscordAfterCreate(e)
}, 'instances')
onAfterBootstrap((e) => {
// return require(`${__hooks}/mothership`).HandleMigrateInstanceVersions(e)
})
onAfterBootstrap((e) => {
// return require(`${__hooks}/mothership`).HandleMigrateRegions(e)
})
/** Reset instance status to idle on start */
onAfterBootstrap((e) => {
return require(`${__hooks}/mothership`).HandleInstancesResetIdle(e)
})
/** Validate instance version */
onModelBeforeUpdate((e) => {
return require(`${__hooks}/mothership`).HandleInstanceBeforeUpdate(e)
}, 'instances')

View File

@ -0,0 +1,10 @@
export * from './api/HandleInstanceCreate'
export * from './api/HandleInstanceDelete'
export * from './api/HandleInstanceUpdate'
export * from './bootstrap/HandleInstancesResetIdle'
export * from './bootstrap/HandleMigrateInstanceVersions'
export * from './bootstrap/HandleMigrateRegions'
export * from './model/HandleInstanceBeforeUpdate'
export * from './model/HandleInstanceVersionValidation'
export * from './model/HandleNotifyDiscordAfterCreate'
export * from './model/HandleNotifyMaintenanceMode'

View File

@ -1,9 +1,8 @@
/// <reference path="../types/types.d.ts" />
import { mkLog } from '$util/Logger'
import { versions } from '$util/versions'
/** Validate instance version */
onModelBeforeUpdate((e) => {
export const HandleInstanceBeforeUpdate = (e: core.ModelEvent) => {
const dao = e.dao || $app.dao()
const { versions, mkLog } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
const log = mkLog(`instances-validate-before-update`)
@ -42,4 +41,4 @@ onModelBeforeUpdate((e) => {
throw new BadRequestError(`Custom domain already in use.`)
}
}
}, 'instances')
}

View File

@ -1,9 +1,6 @@
/// <reference path="../types/types.d.ts" />
/** Validate instance version */
onModelBeforeCreate((e) => {
const { versions } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
import { versions } from '$util/versions'
export const HandleInstanceVersionValidation = (e: core.ModelEvent) => {
const dao = e.dao || $app.dao()
const version = e.model.get('version')
@ -14,4 +11,4 @@ onModelBeforeCreate((e) => {
)}`,
)
}
}, 'instances')
}

View File

@ -1,8 +1,8 @@
/// <reference path="../types/types.d.ts" />
import { mkLog } from '$util/Logger'
import { mkAudit } from '$util/mkAudit'
onModelAfterCreate((e) => {
export const HandleNotifyDiscordAfterCreate = (e: core.ModelEvent) => {
const dao = e.dao || $app.dao()
const { mkAudit, mkLog } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
const log = mkLog(`instances:create:discord:notify`)
const audit = mkAudit(log, dao)
@ -26,4 +26,4 @@ onModelAfterCreate((e) => {
} catch (e) {
audit(`ERROR`, `Instance creation discord notify failed with ${e}`)
}
}, 'instances')
}

View File

@ -1,14 +1,13 @@
onModelAfterUpdate((e) => {
return
import { mkLog } from '$util/Logger'
import { mkAudit } from '$util/mkAudit'
import { mkNotifier } from '$util/mkNotifier'
export const HandleNotifyMaintenanceMode = (e: core.ModelEvent) => {
const dao = e.dao || $app.dao()
const newModel = /** @type {models.Record} */ (e.model)
const newModel = e.model as models.Record
const oldModel = newModel.originalCopy()
const { mkLog, mkNotifier, mkAudit } = /** @type {Lib} */ (
require(`${__hooks}/lib.js`)
)
const log = mkLog(`maintenance-mode`)
const audit = mkAudit(log, dao)
const notify = mkNotifier(log, dao)
@ -44,4 +43,4 @@ onModelAfterUpdate((e) => {
} catch (e) {
audit(`ERROR`, `Failed to enqueue template with ${e}`)
}
}, 'instances')
}

View File

@ -1,16 +1,52 @@
routerAdd('POST', '/api/ls', (c) => {
import { mkLog } from '$util/Logger'
import { mkAudit } from '$util/mkAudit'
import { mkNotifier } from '$util/mkNotifier'
import { PartialDeep } from 'type-fest'
type LemonSqueezyDebugContext = PartialDeep<{
secret: string
raw: string
body_hash: string
xsignature_header: string
data: {
data: {
attributes: {
product_id: string
product_name: string
first_order_item: {
product_id: string
product_name: string
}
}
type: string
}
meta: {
event_name: string
custom_data: {
user_id: string
}
}
}
type: string
event_name: string
user_id: string
product_id: number
product_name: string
}>
export const HandleLemonSqueezySale = (c: echo.Context) => {
const dao = $app.dao()
const { mkAudit, mkLog, mkNotifier } = /** @type {Lib} */ (
require(`${__hooks}/lib.js`)
)
const log = mkLog(`ls`)
const audit = mkAudit(log, dao)
const context = {}
const context: LemonSqueezyDebugContext = {}
log(`Top of ls`)
try {
context.secret = $os.getenv('LS_WEBHOOK_SECRET')
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)
@ -53,10 +89,11 @@ routerAdd('POST', '/api/ls', (c) => {
log(`user ID ok`, context.user_id)
}
context.product_id =
context.type === 'orders'
? context.data?.data?.attributes.first_order_item?.product_id
: context.data?.data?.attributes?.product_id
context.product_id = parseInt(
(context.type === 'orders'
? 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 {
@ -65,7 +102,7 @@ routerAdd('POST', '/api/ls', (c) => {
context.product_name =
context.type === 'orders'
? context.data?.data?.attributes.first_order_item?.product_name
? context.data?.data?.attributes?.first_order_item?.product_name
: context.data?.data?.attributes?.product_name
if (!context.product_name) {
throw new Error(`No product name`)
@ -77,7 +114,7 @@ routerAdd('POST', '/api/ls', (c) => {
try {
return dao.findFirstRecordByData('users', 'id', context.user_id)
} catch (e) {
throw new Error(`User ${user_id} not found`)
throw new Error(`User ${context.user_id} not found`)
}
})()
log(`user record ok`, userRec)
@ -95,8 +132,9 @@ routerAdd('POST', '/api/ls', (c) => {
subscription_expired: () => {
signup_canceller()
},
}
const event_handler = event_name_map[context.event_name]
} as const
const event_handler =
event_name_map[context.event_name as keyof typeof event_name_map]
if (!event_handler) {
log(`unsupported event`, context.event_name)
return c.json(200, {
@ -149,8 +187,11 @@ routerAdd('POST', '/api/ls', (c) => {
userRec.set(`subscription`, `flounder`)
userRec.set(`subscription_interval`, `year`)
},
}
const product_handler = product_handler_map[context.product_id]
} as const
const product_handler =
product_handler_map[
context.product_id as keyof typeof product_handler_map
]
if (!product_handler) {
throw new Error(`No product handler for ${context.product_id}`)
} else {
@ -165,7 +206,11 @@ routerAdd('POST', '/api/ls', (c) => {
log(`saved user`)
const notify = mkNotifier(log, txDao)
notify(`lemonbot`, `lemon_order_discord`, context.user_id, context)
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`)
})
log(`database updated`)
@ -190,4 +235,4 @@ routerAdd('POST', '/api/ls', (c) => {
audit(`LS_ERR`, `${e}`, context)
return c.json(500, { status: `error`, error: e.message })
}
})
}

View File

@ -0,0 +1,3 @@
routerAdd('POST', '/api/ls', (c) => {
return require(`${__hooks}/mothership`).HandleLemonSqueezySale(c)
})

View File

@ -0,0 +1 @@
export * from './api/HandleLemonSqueezySale'

View File

@ -0,0 +1,44 @@
import { mkLog } from '$util/Logger'
export const HandleMailSend = (c: echo.Context) => {
const log = mkLog(`mail`)
let data = new DynamicModel({
to: '',
subject: '',
body: '',
}) as { to: string; subject: string; body: string }
log(`before bind`)
c.bind(data)
log(`after bind`)
// This is necessary for destructuring to work correctly
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 is string => !!e)
.map((e) => ({ address: e })),
subject: subject,
html: body,
})
$app.newMailClient().send(email)
const msg = `Sent to ${to}`
log(msg)
return c.json(200, { status: 'ok' })
}

View File

@ -0,0 +1,8 @@
routerAdd(
'POST',
'/api/mail',
(c) => {
return require(`${__hooks}/mothership`).HandleMailSend(c)
},
$apis.requireAdminAuth(),
)

View File

@ -0,0 +1 @@
export * from './api/HandleMailSend'

View File

@ -0,0 +1,26 @@
import { mkLog } from '$util/Logger'
export const HandleMetaUpdateAtBoot = (c: core.BootstrapEvent) => {
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()
}

View File

@ -0,0 +1,3 @@
onAfterBootstrap((e) => {
return require(`${__hooks}/mothership`).HandleMetaUpdateAtBoot(e)
})

View File

@ -0,0 +1 @@
export * from './boot/HandleMetaUpdateAtBoot'

View File

@ -0,0 +1,12 @@
export const HandleMirrorData = (c: echo.Context) => {
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 })
}

View File

@ -0,0 +1,9 @@
routerAdd(
'GET',
'/api/mirror',
(c) => {
return require(`${__hooks}/HandleMirrorData`).HandleMirrorData(c)
},
$apis.gzip(),
$apis.requireAdminAuth(),
)

View File

@ -0,0 +1 @@
export * from './api/HandleMirrorData'

View File

@ -0,0 +1,30 @@
import { mkLog } from '$util/Logger'
import { mkNotificationProcessor } from '$util/mkNotificationProcessor'
export const HandleProcessSingleNotification = (c: echo.Context) => {
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' })
}

View File

@ -0,0 +1,11 @@
routerAdd(`GET`, `api/process_single_notification`, (c) => {
return require(`${__hooks}/mothership`).HandleProcessSingleNotification(c)
})
onModelAfterCreate((e) => {
return require(`${__hooks}/mothership`).HandleProcessNotification(e)
}, `notifications`)
onModelBeforeUpdate((e) => {
return require(`${__hooks}/mothership`).HandleUserWelcomeMessage(e)
}, 'users')

View File

@ -0,0 +1,2 @@
export * from './api/HandleProcessSingleNotification'
export * from './model/HandleUserWelcomeMessage'

View File

@ -0,0 +1,29 @@
import { mkLog } from '$util/Logger'
import { mkAudit } from '$util/mkAudit'
import { mkNotificationProcessor } from '$util/mkNotificationProcessor'
export const HandleProcessNotification = (e: core.ModelEvent) => {
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 as models.Record
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) {
audit(`ERROR`, `${e}`, {
notification: notificationRec.getId(),
})
}
}

View File

@ -1,14 +1,12 @@
/// <reference path="../types/types.d.ts" />
import { mkLog } from '$util/Logger'
import { mkAudit } from '$util/mkAudit'
import { mkNotifier } from '$util/mkNotifier'
onModelBeforeUpdate((e) => {
export const HandleUserWelcomeMessage = (e: core.ModelEvent) => {
const dao = e.dao || $app.dao()
const newModel = /** @type {models.Record} */ (e.model)
const newModel = /** @type {models.Record} */ e.model
const oldModel = newModel.originalCopy()
const { mkAudit, mkLog, mkNotifier } = /** @type {Lib} */ (
require(`${__hooks}/lib.js`)
)
const log = mkLog(`user-welcome-msg`)
const notify = mkNotifier(log, dao)
const audit = mkAudit(log, dao)
@ -31,4 +29,4 @@ onModelBeforeUpdate((e) => {
} catch (e) {
audit(`ERROR`, `${e}`, { user: newModel.getId() })
}
}, 'users')
}

View File

@ -1,8 +1,8 @@
/// <reference path="../types/types.d.ts" />
import { mkLog } from '$util/Logger'
import { mkAudit } from '$util/mkAudit'
routerAdd('GET', '/api/unsubscribe', (c) => {
export const HandleOutpostUnsubscribe = (c: echo.Context) => {
const dao = $app.dao()
const { mkLog, mkAudit } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
const log = mkLog(`unsubscribe`)
const audit = mkAudit(log, dao)
@ -31,4 +31,4 @@ routerAdd('GET', '/api/unsubscribe', (c) => {
audit('UNSUBSCRIBE_ERR', `User ${id} not found`)
return c.html(200, `<p>Looks like you're already unsubscribed.`)
}
})
}

View File

@ -0,0 +1,3 @@
routerAdd('GET', '/api/unsubscribe', (c) => {
return require(`${__hooks}/mothership`).HandleOutpostUnsubscribe(c)
})

View File

@ -0,0 +1 @@
export * from './api/HandleOutpostUnsubscribe'

View File

@ -0,0 +1,37 @@
import { error } from '../error'
import { isAvailable } from '../isAvailable'
import { generate } from '../random-words'
export const HandleSignupCheck = (c: echo.Context) => {
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 +new Date()
}
const slug = generate(2).join(`-`)
if (isAvailable(slug)) return slug
}
}
})()
return c.json(200, { instanceName })
}

View File

@ -0,0 +1,99 @@
import { versions } from '$util/versions'
import { error } from '../error'
export const HandleSignupConfirm = (c: echo.Context) => {
const dao = $app.dao()
const parsed = (() => {
const rawBody = readerToString(c.request().body)
try {
const parsed = JSON.parse(rawBody)
return parsed
} 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()
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('notifyMaintenanceMode', true)
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-1`)
instance.set('uid', user.get('id'))
instance.set('status', 'idle')
instance.set('notifyMaintenanceMode', true)
instance.set('syncAdmin', true)
instance.set('version', versions[0])
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' })
}

View File

@ -0,0 +1,12 @@
import { StringKvLookup } from '$util/Logger'
export const error = (
fieldName: string,
slug: string,
description: string,
extra?: StringKvLookup,
) =>
new ApiError(500, description, {
[fieldName]: new ValidationError(slug, description),
...extra,
})

View File

@ -0,0 +1,8 @@
routerAdd('GET', '/api/signup', (c) => {
return require(`${__hooks}/mothership`).HandleSignupCheck(c)
})
// https://pocketbase.io/docs/js-routing/#sending-request-to-custom-routes-using-the-sdks
routerAdd('POST', '/api/signup', (c) => {
return require(`${__hooks}/mothership`).HandleSignupConfirm(c)
})

View File

@ -0,0 +1,2 @@
export * from './api/HandleSignupCheck'
export * from './api/HandleSignupConfirm'

View File

@ -0,0 +1,10 @@
export const isAvailable = (slug: string) => {
try {
const record = $app
.dao()
.findFirstRecordByData('instances', 'subdomain', slug)
return false
} catch {
return true
}
}

View File

@ -0,0 +1,125 @@
import { wordList } from './wordList'
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
export function generate(options: any) {
// initalize random number generator for words if options.seed is provided
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)]!
}
// limits the size of words to the minimum and maximum possible
function limitWordSize(wordSize: number) {
if (wordSize < shortestWordSize) wordSize = shortestWordSize
if (wordSize > longestWordSize) wordSize = longestWordSize
return wordSize
}
// random int as seeded by options.seed if applicable, or Math.random() otherwise
function randInt(lessThan: number) {
const r = Math.random()
return Math.floor(r * lessThan)
}
// No arguments = generate one word
if (options === undefined) {
return word()
}
// Just a number = return that many words
if (typeof options === 'number') {
options = { exactly: options }
} else if (Object.keys(rest).length === 0) {
return word()
}
// options supported: exactly, min, max, join
if (options.exactly) {
options.min = options.exactly
options.max = options.exactly
}
// not a number = one word par string
if (typeof options.wordsPerString !== 'number') {
options.wordsPerString = 1
}
//not a function = returns the raw word
if (typeof options.formatter !== 'function') {
options.formatter = (word: string) => word
}
//not a string = separator is a space
if (typeof options.separator !== 'string') {
options.separator = ' '
}
const total = options.min + randInt(options.max + 1 - options.min)
let results: any = []
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
}
export function count(options: any) {
let { minLength, maxLength } = options || {}
if (typeof minLength !== 'number') {
minLength = shortestWordSize
}
if (typeof maxLength !== 'number') {
maxLength = longestWordSize
}
return wordList.filter(
(word) => word.length >= minLength && word.length <= maxLength,
).length
}

View File

@ -1,4 +1,4 @@
const wordList = [
export const wordList = [
'ability',
'able',
'aboard',
@ -1952,134 +1952,3 @@ const wordList = [
'zoo',
'zulu',
]
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) {
// initalize random number generator for words if options.seed is provided
const random = options?.seed ? new seedrandom(options.seed) : null
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)]
}
// limits the size of words to the minimum and maximum possible
function limitWordSize(wordSize) {
if (wordSize < shortestWordSize) wordSize = shortestWordSize
if (wordSize > longestWordSize) wordSize = longestWordSize
return wordSize
}
// random int as seeded by options.seed if applicable, or Math.random() otherwise
function randInt(lessThan) {
const r = random ? random() : Math.random()
return Math.floor(r * lessThan)
}
// No arguments = generate one word
if (options === undefined) {
return word()
}
// Just a number = return that many words
if (typeof options === 'number') {
options = { exactly: options }
} else if (Object.keys(rest).length === 0) {
return word()
}
// options supported: exactly, min, max, join
if (options.exactly) {
options.min = options.exactly
options.max = options.exactly
}
// not a number = one word par string
if (typeof options.wordsPerString !== 'number') {
options.wordsPerString = 1
}
//not a function = returns the raw word
if (typeof options.formatter !== 'function') {
options.formatter = (word) => word
}
//not a string = separator is a space
if (typeof options.separator !== 'string') {
options.separator = ' '
}
const total = options.min + randInt(options.max + 1 - options.min)
/** @type {string[] | string} */
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
}
function count(options) {
let { minLength, maxLength } = options || {}
if (typeof minLength !== 'number') {
minLength = shortestWordSize
}
if (typeof maxLength !== 'number') {
maxLength = longestWordSize
}
return wordList.filter(
(word) => word.length >= minLength && word.length <= maxLength,
).length
}
module.exports = {
count,
generate,
}

View File

@ -0,0 +1,149 @@
import { mkLog } from '$util/Logger'
import { mkAudit } from '$util/mkAudit'
type SnsSubscriptionConfirmationEvent = {
Type: 'SubscriptionConfirmation'
SubscribeURL: string
}
type SnsNotificationEvent = {
Type: 'Notification'
Message: string
}
type SnsNotificationBouncePayload = {
notificationType: 'Bounce'
bounce: {
bounceType: 'Permanent'
bouncedRecipients: { emailAddress: string }[]
}
}
type SnsNotificationComplaintPayload = {
notificationType: 'Complaint'
complaint: {
complainedRecipients: { emailAddress: string }[]
}
}
type SnsNotificationPayload =
| SnsNotificationBouncePayload
| SnsNotificationComplaintPayload
type SnsEvent = SnsSubscriptionConfirmationEvent | SnsNotificationEvent
function isSnsSubscriptionConfirmationEvent(
event: SnsEvent,
): event is SnsSubscriptionConfirmationEvent {
return event.Type === 'SubscriptionConfirmation'
}
function isSnsNotificationEvent(
event: SnsEvent,
): event is SnsNotificationEvent {
return event.Type === 'Notification'
}
function isSnsNotificationBouncePayload(
payload: SnsNotificationPayload,
): payload is SnsNotificationBouncePayload {
return payload.notificationType === 'Bounce'
}
function isSnsNotificationComplaintPayload(
payload: SnsNotificationPayload,
): payload is SnsNotificationComplaintPayload {
return payload.notificationType === 'Complaint'
}
export const HandleSesError = (c: echo.Context) => {
const dao = $app.dao()
const log = mkLog(`sns`)
const audit = mkAudit(log, dao)
const processBounce = (emailAddress: string) => {
log(`Processing ${emailAddress}`)
const extra = {
email: emailAddress,
} as { email: string; user: string }
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) as SnsEvent
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) as SnsNotificationPayload
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' })
}

View File

@ -0,0 +1,3 @@
routerAdd('POST', '/api/sns', (c) => {
return require(`${__hooks}/mothership`).HandleSesError(c)
})

View File

@ -0,0 +1 @@
export * from './api/HandleSesError'

View File

@ -0,0 +1,9 @@
export const HandleStatsRequest = (c: echo.Context) => {
const result = new DynamicModel({
total_flounder_subscribers: 0,
})
$app.dao().db().select('total_flounder_subscribers').from('stats').one(result)
return c.json(200, result)
}

View File

@ -0,0 +1,3 @@
routerAdd('GET', '/api/stats', (c) => {
return require(`${__hooks}/mothership`).HandleStatsRequest(c)
})

View File

@ -0,0 +1 @@
export * from './api/HandleStatsRequest'

View File

@ -0,0 +1,22 @@
import { mkLog } from '$util/Logger'
export const HandleUserTokenRequest = (c: echo.Context) => {
const dao = $app.dao()
const log = mkLog(`user-token`)
const id = c.pathParam('id')
// log({ 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`)
// log({ email, passwordHash, tokenKey })
return c.json(200, { email, passwordHash, tokenKey })
}

View File

@ -0,0 +1,8 @@
routerAdd(
'GET',
'/api/userToken/:id',
(c) => {
return require(`${__hooks}/mothership`).HandleUserTokenRequest(c)
},
$apis.requireAdminAuth(),
)

View File

@ -0,0 +1 @@
export * from './api/HandleUserTokenRequest'

View File

@ -0,0 +1,6 @@
import { versions } from '$util/versions'
/** Return a list of available PocketBase versions */
export const HandleVersionsRequest = (c: echo.Context) => {
return c.json(200, { versions })
}

View File

@ -0,0 +1,4 @@
/** Return a list of available PocketBase versions */
routerAdd('GET', '/api/versions', (c) => {
return require(`${__hooks}/mothership`).HandleVersionsRequest(c)
})

View File

@ -0,0 +1 @@
export * from './api/HandleVersionsRequest'

View File

@ -0,0 +1 @@
export * from './handlers'

View File

@ -0,0 +1,24 @@
export type Logger = (...args: any[]) => void
export type StringKvLookup = { [_: string]: string }
export const mkLog =
(namespace: string) =>
(...s: any[]) =>
console.log(
`[${namespace}]`,
...s.map((p) => {
if (typeof p === 'object') return JSON.stringify(p, null, 2)
return p
}),
)
export const dbg = (...args: any[]) => console.log(args)
export const interpolateString = (template: string, dict: StringKvLookup) => {
return template.replace(/\{\$(\w+)\}/g, (match, key) => {
dbg({ match, key })
const lowerKey = key.toLowerCase()
return dict.hasOwnProperty(lowerKey) ? dict[lowerKey] || '' : match
})
}

View File

@ -0,0 +1,35 @@
import { Logger } from './Logger'
type AuditEvents =
| 'ERROR'
| 'NOTIFICATION_ERR'
| 'LS'
| 'LS_ERR'
| 'PBOUNCE'
| 'PBOUNCE_ERR'
| 'SNS_ERR'
| 'COMPLAINT'
| 'COMPLAINT_ERR'
| 'UNSUBSCRIBE'
| 'UNSUBSCRIBE_ERR'
export const mkAudit = (
log: Logger,
dao: daos.Dao,
): ((
event: AuditEvents,
note: string,
context?: { [_: string]: any },
) => void) => {
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,
}),
)
}
}

View File

@ -0,0 +1,82 @@
import { Logger, interpolateString } from './Logger'
export const mkNotificationProcessor =
(log: Logger, dao: daos.Dao, test = false) =>
(notificationRec: models.Record) => {
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<mailer.Message_In>} */
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 || true) {
msgArgs.to = [{ address: `ben@benallfree.com` }]
msgArgs.subject = `***TEST ${to} *** ${msgArgs.subject}`
}
log({ msgArgs })
// @ts-ignore
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: HttpSendConfig = {
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)
}
}

View File

@ -0,0 +1,30 @@
import { Logger } from './Logger'
export const mkNotifier =
(log: Logger, dao: daos.Dao) =>
<TContext extends Record<string, any>>(
channel: 'email' | 'lemonbot',
template: string,
user_id: string,
context: TContext = {} as TContext,
) => {
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)
}

View File

@ -0,0 +1,17 @@
import { reduce } from '@s-libs/micro-dash'
export const removeEmptyKeys = <T extends Record<string, unknown>>(
obj: T,
): T => {
const sanitized = reduce(
obj,
(acc, value, key) => {
if (value !== null && value !== undefined) {
acc[key] = value
}
return acc
},
{} as T,
)
return sanitized
}

View File

@ -0,0 +1 @@
export const versions = require(`${__hooks}/versions.cjs`)

View File

@ -357,7 +357,7 @@ interface MailerMessage extends mailer.Message{} // merge
* @group PocketBase
*/
declare class MailerMessage implements mailer.Message {
constructor(message?: Partial<mailer.Message>)
constructor(message?: Partial<mailer.Message_Create> )
}
interface Command extends cobra.Command{} // merge
@ -994,6 +994,16 @@ interface FormData {
set(key:string, value:any): void
}
interface HttpSendConfig {
url: string,
body?: string|FormData,
method?: string, // default to "GET"
headers?: { [key:string]: string },
timeout?: number, // default to 120
// deprecated, please use body instead
data?: { [key:string]: any },
}
/**
* `$http` defines common methods for working with HTTP requests.
*
@ -1019,16 +1029,7 @@ declare namespace $http {
* console.log(res.json) // the response body as parsed json array or map
* ```
*/
function send(config: {
url: string,
body?: string|FormData,
method?: string, // default to "GET"
headers?: { [key:string]: string },
timeout?: number, // default to 120
// deprecated, please use body instead
data?: { [key:string]: any },
}): {
function send(config: HttpSendConfig): {
statusCode: number,
headers: { [key:string]: Array<string> },
cookies: { [key:string]: http.Cookie },
@ -13420,7 +13421,7 @@ namespace daos {
* )
* ```
*/
findRecordsByFilter(collectionNameOrId: string, filter: string, sort: string, limit: number, offset: number, ...params: dbx.Params[]): Array<(models.Record | undefined)>
findRecordsByFilter(collectionNameOrId: string, filter: string, sort?: string, limit?: number, offset?: number, ...params: dbx.Params[]): Array<(models.Record | undefined)>
}
interface Dao {
/**
@ -13539,7 +13540,7 @@ namespace daos {
*
* Returns a map with the failed expand parameters and their errors.
*/
expandRecord(record: models.Record, expands: Array<string>, optFetchFunc: ExpandFetchFunc): _TygojaDict
expandRecord(record: models.Record, expands: Array<string>, optFetchFunc?: ExpandFetchFunc): _TygojaDict
}
interface Dao {
/**
@ -18530,6 +18531,19 @@ namespace mailer {
headers: _TygojaDict
attachments: _TygojaDict
}
interface Message_Create {
from: mail.Address_Create
to: Array<mail.Address_Create>
bcc: Array<mail.Address_Create>
cc: Array<mail.Address_Create>
subject: string
html: string
text: string
headers: _TygojaDict
attachments: _TygojaDict
}
}
/**
@ -19621,11 +19635,11 @@ namespace mail {
* An address such as "Barry Gibbs <bg@example.com>" is represented
* as Address{Name: "Barry Gibbs", Address: "bg@example.com"}.
*/
interface Address {
name: string // Proper name; may be empty.
interface Address_Create {
name?: string // Proper name; may be empty.
address: string // user@domain
}
interface Address {
interface Address extends Address_Create {
/**
* String formats the address as a valid RFC 5322 address.
* If the address's name contains non-ASCII characters

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"strict": false,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"noImplicitAny": true,
"target": "ES6",
"module": "CommonJS",
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"$util/*": ["src/lib/util/*"]
}
},
"include": ["src/**/*.ts", "src/lib/random-words.js"]
}

View File

@ -0,0 +1,19 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: {
'mothership.pb': 'src/hooks/index.ts',
mothership: 'src/lib/index.ts',
},
format: ['cjs'],
dts: false,
clean: false,
outDir: 'pb_hooks',
shims: true,
skipNodeModulesBundle: false,
target: 'node20',
platform: 'node',
minify: false,
sourcemap: false,
bundle: true,
})

429
pnpm-lock.yaml generated
View File

@ -129,7 +129,7 @@ importers:
version: 4.2.19
svelte-check:
specifier: ^4.0.4
version: 4.0.4(svelte@4.2.19)(typescript@5.6.3)
version: 4.0.4(picomatch@4.0.2)(svelte@4.2.19)(typescript@5.6.3)
svelte-fa:
specifier: ^4.0.3
version: 4.0.3(svelte@4.2.19)
@ -317,6 +317,18 @@ importers:
packages/pockethost-instance: {}
packages/pockethost/src/mothership-app:
devDependencies:
'@s-libs/micro-dash':
specifier: ^18.0.0
version: 18.0.0
tsup:
specifier: ^8.3.5
version: 8.3.5(jiti@2.3.3)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.5.1)
type-fest:
specifier: ^4.6.0
version: 4.26.1
packages:
'@alloc/quick-lru@5.2.0':
@ -483,6 +495,12 @@ packages:
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.24.0':
resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.17.19':
resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
engines: {node: '>=12'}
@ -501,6 +519,12 @@ packages:
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.24.0':
resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.17.19':
resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==}
engines: {node: '>=12'}
@ -519,6 +543,12 @@ packages:
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.24.0':
resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.17.19':
resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==}
engines: {node: '>=12'}
@ -537,6 +567,12 @@ packages:
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.24.0':
resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.17.19':
resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==}
engines: {node: '>=12'}
@ -555,6 +591,12 @@ packages:
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.24.0':
resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.17.19':
resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==}
engines: {node: '>=12'}
@ -573,6 +615,12 @@ packages:
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.24.0':
resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.17.19':
resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==}
engines: {node: '>=12'}
@ -591,6 +639,12 @@ packages:
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.24.0':
resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.17.19':
resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==}
engines: {node: '>=12'}
@ -609,6 +663,12 @@ packages:
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.24.0':
resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.17.19':
resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==}
engines: {node: '>=12'}
@ -627,6 +687,12 @@ packages:
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.24.0':
resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.17.19':
resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==}
engines: {node: '>=12'}
@ -645,6 +711,12 @@ packages:
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.24.0':
resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.17.19':
resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==}
engines: {node: '>=12'}
@ -663,6 +735,12 @@ packages:
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.24.0':
resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.17.19':
resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==}
engines: {node: '>=12'}
@ -681,6 +759,12 @@ packages:
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.24.0':
resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.17.19':
resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==}
engines: {node: '>=12'}
@ -699,6 +783,12 @@ packages:
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.24.0':
resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.17.19':
resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==}
engines: {node: '>=12'}
@ -717,6 +807,12 @@ packages:
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.24.0':
resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.17.19':
resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==}
engines: {node: '>=12'}
@ -735,6 +831,12 @@ packages:
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.24.0':
resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.17.19':
resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==}
engines: {node: '>=12'}
@ -753,6 +855,12 @@ packages:
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.24.0':
resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.17.19':
resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==}
engines: {node: '>=12'}
@ -771,6 +879,12 @@ packages:
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.24.0':
resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-x64@0.17.19':
resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==}
engines: {node: '>=12'}
@ -789,12 +903,24 @@ packages:
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.24.0':
resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.23.1':
resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-arm64@0.24.0':
resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.17.19':
resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==}
engines: {node: '>=12'}
@ -813,6 +939,12 @@ packages:
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.24.0':
resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.17.19':
resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==}
engines: {node: '>=12'}
@ -831,6 +963,12 @@ packages:
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.24.0':
resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.17.19':
resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==}
engines: {node: '>=12'}
@ -849,6 +987,12 @@ packages:
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.24.0':
resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.17.19':
resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==}
engines: {node: '>=12'}
@ -867,6 +1011,12 @@ packages:
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.24.0':
resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.17.19':
resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==}
engines: {node: '>=12'}
@ -885,6 +1035,12 @@ packages:
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.24.0':
resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@fastify/busboy@2.1.1':
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
engines: {node: '>=14'}
@ -1571,6 +1727,12 @@ packages:
resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==}
engines: {node: '>=10.0.0'}
bundle-require@5.0.0:
resolution: {integrity: sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
esbuild: '>=0.18'
bunyan@1.8.15:
resolution: {integrity: sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==}
engines: {'0': node >=0.10.0}
@ -1580,6 +1742,10 @@ packages:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
call-bind@1.0.7:
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
engines: {node: '>= 0.4'}
@ -1675,6 +1841,10 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
consola@3.2.3:
resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==}
engines: {node: ^14.18.0 || >=16.10.0}
constantinople@4.0.1:
resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==}
@ -1985,6 +2155,11 @@ packages:
engines: {node: '>=18'}
hasBin: true
esbuild@0.24.0:
resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==}
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@ -2085,6 +2260,14 @@ packages:
picomatch:
optional: true
fdir@6.4.2:
resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fecha@4.2.3:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
@ -2449,6 +2632,10 @@ packages:
resolution: {integrity: sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==}
hasBin: true
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
@ -2496,6 +2683,10 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
load-tsconfig@0.2.5:
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
locate-character@3.0.0:
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
@ -2516,6 +2707,9 @@ packages:
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash.sortby@4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
lodash.startcase@4.4.0:
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
@ -2928,10 +3122,17 @@ packages:
picocolors@1.1.0:
resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
picomatch@4.0.2:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
@ -3357,6 +3558,10 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
engines: {node: '>= 8'}
sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
deprecated: Please use @jridgewell/sourcemap-codec instead
@ -3560,6 +3765,13 @@ packages:
tiny-glob@0.2.9:
resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==}
tinyexec@0.3.1:
resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==}
tinyglobby@0.2.10:
resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==}
engines: {node: '>=12.0.0'}
tmp@0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
@ -3590,6 +3802,13 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
tr46@1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
triple-beam@1.4.1:
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
engines: {node: '>= 14.0.0'}
@ -3603,6 +3822,25 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsup@8.3.5:
resolution: {integrity: sha512-Tunf6r6m6tnZsG9GYWndg0z8dEV7fD733VBFzFJ5Vcm1FtlXB8xBD/rtrBi2a3YKEV7hHtxiZtW5EAVADoe1pA==}
engines: {node: '>=18'}
hasBin: true
peerDependencies:
'@microsoft/api-extractor': ^7.36.0
'@swc/core': ^1
postcss: ^8.4.12
typescript: '>=4.5.0'
peerDependenciesMeta:
'@microsoft/api-extractor':
optional: true
'@swc/core':
optional: true
postcss:
optional: true
typescript:
optional: true
tsx@4.19.2:
resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==}
engines: {node: '>=18.0.0'}
@ -3760,6 +3998,12 @@ packages:
resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
engines: {node: '>= 8'}
webidl-conversions@4.0.2:
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
whatwg-url@7.1.0:
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
@ -4104,6 +4348,9 @@ snapshots:
'@esbuild/aix-ppc64@0.23.1':
optional: true
'@esbuild/aix-ppc64@0.24.0':
optional: true
'@esbuild/android-arm64@0.17.19':
optional: true
@ -4113,6 +4360,9 @@ snapshots:
'@esbuild/android-arm64@0.23.1':
optional: true
'@esbuild/android-arm64@0.24.0':
optional: true
'@esbuild/android-arm@0.17.19':
optional: true
@ -4122,6 +4372,9 @@ snapshots:
'@esbuild/android-arm@0.23.1':
optional: true
'@esbuild/android-arm@0.24.0':
optional: true
'@esbuild/android-x64@0.17.19':
optional: true
@ -4131,6 +4384,9 @@ snapshots:
'@esbuild/android-x64@0.23.1':
optional: true
'@esbuild/android-x64@0.24.0':
optional: true
'@esbuild/darwin-arm64@0.17.19':
optional: true
@ -4140,6 +4396,9 @@ snapshots:
'@esbuild/darwin-arm64@0.23.1':
optional: true
'@esbuild/darwin-arm64@0.24.0':
optional: true
'@esbuild/darwin-x64@0.17.19':
optional: true
@ -4149,6 +4408,9 @@ snapshots:
'@esbuild/darwin-x64@0.23.1':
optional: true
'@esbuild/darwin-x64@0.24.0':
optional: true
'@esbuild/freebsd-arm64@0.17.19':
optional: true
@ -4158,6 +4420,9 @@ snapshots:
'@esbuild/freebsd-arm64@0.23.1':
optional: true
'@esbuild/freebsd-arm64@0.24.0':
optional: true
'@esbuild/freebsd-x64@0.17.19':
optional: true
@ -4167,6 +4432,9 @@ snapshots:
'@esbuild/freebsd-x64@0.23.1':
optional: true
'@esbuild/freebsd-x64@0.24.0':
optional: true
'@esbuild/linux-arm64@0.17.19':
optional: true
@ -4176,6 +4444,9 @@ snapshots:
'@esbuild/linux-arm64@0.23.1':
optional: true
'@esbuild/linux-arm64@0.24.0':
optional: true
'@esbuild/linux-arm@0.17.19':
optional: true
@ -4185,6 +4456,9 @@ snapshots:
'@esbuild/linux-arm@0.23.1':
optional: true
'@esbuild/linux-arm@0.24.0':
optional: true
'@esbuild/linux-ia32@0.17.19':
optional: true
@ -4194,6 +4468,9 @@ snapshots:
'@esbuild/linux-ia32@0.23.1':
optional: true
'@esbuild/linux-ia32@0.24.0':
optional: true
'@esbuild/linux-loong64@0.17.19':
optional: true
@ -4203,6 +4480,9 @@ snapshots:
'@esbuild/linux-loong64@0.23.1':
optional: true
'@esbuild/linux-loong64@0.24.0':
optional: true
'@esbuild/linux-mips64el@0.17.19':
optional: true
@ -4212,6 +4492,9 @@ snapshots:
'@esbuild/linux-mips64el@0.23.1':
optional: true
'@esbuild/linux-mips64el@0.24.0':
optional: true
'@esbuild/linux-ppc64@0.17.19':
optional: true
@ -4221,6 +4504,9 @@ snapshots:
'@esbuild/linux-ppc64@0.23.1':
optional: true
'@esbuild/linux-ppc64@0.24.0':
optional: true
'@esbuild/linux-riscv64@0.17.19':
optional: true
@ -4230,6 +4516,9 @@ snapshots:
'@esbuild/linux-riscv64@0.23.1':
optional: true
'@esbuild/linux-riscv64@0.24.0':
optional: true
'@esbuild/linux-s390x@0.17.19':
optional: true
@ -4239,6 +4528,9 @@ snapshots:
'@esbuild/linux-s390x@0.23.1':
optional: true
'@esbuild/linux-s390x@0.24.0':
optional: true
'@esbuild/linux-x64@0.17.19':
optional: true
@ -4248,6 +4540,9 @@ snapshots:
'@esbuild/linux-x64@0.23.1':
optional: true
'@esbuild/linux-x64@0.24.0':
optional: true
'@esbuild/netbsd-x64@0.17.19':
optional: true
@ -4257,9 +4552,15 @@ snapshots:
'@esbuild/netbsd-x64@0.23.1':
optional: true
'@esbuild/netbsd-x64@0.24.0':
optional: true
'@esbuild/openbsd-arm64@0.23.1':
optional: true
'@esbuild/openbsd-arm64@0.24.0':
optional: true
'@esbuild/openbsd-x64@0.17.19':
optional: true
@ -4269,6 +4570,9 @@ snapshots:
'@esbuild/openbsd-x64@0.23.1':
optional: true
'@esbuild/openbsd-x64@0.24.0':
optional: true
'@esbuild/sunos-x64@0.17.19':
optional: true
@ -4278,6 +4582,9 @@ snapshots:
'@esbuild/sunos-x64@0.23.1':
optional: true
'@esbuild/sunos-x64@0.24.0':
optional: true
'@esbuild/win32-arm64@0.17.19':
optional: true
@ -4287,6 +4594,9 @@ snapshots:
'@esbuild/win32-arm64@0.23.1':
optional: true
'@esbuild/win32-arm64@0.24.0':
optional: true
'@esbuild/win32-ia32@0.17.19':
optional: true
@ -4296,6 +4606,9 @@ snapshots:
'@esbuild/win32-ia32@0.23.1':
optional: true
'@esbuild/win32-ia32@0.24.0':
optional: true
'@esbuild/win32-x64@0.17.19':
optional: true
@ -4305,6 +4618,9 @@ snapshots:
'@esbuild/win32-x64@0.23.1':
optional: true
'@esbuild/win32-x64@0.24.0':
optional: true
'@fastify/busboy@2.1.1': {}
'@fortawesome/fontawesome-common-types@6.6.0': {}
@ -4984,6 +5300,11 @@ snapshots:
buildcheck@0.0.6:
optional: true
bundle-require@5.0.0(esbuild@0.24.0):
dependencies:
esbuild: 0.24.0
load-tsconfig: 0.2.5
bunyan@1.8.15:
optionalDependencies:
dtrace-provider: 0.8.8
@ -4993,6 +5314,8 @@ snapshots:
bytes@3.1.2: {}
cac@6.7.14: {}
call-bind@1.0.7:
dependencies:
es-define-property: 1.0.0
@ -5101,6 +5424,8 @@ snapshots:
concat-map@0.0.1: {}
consola@3.2.3: {}
constantinople@4.0.1:
dependencies:
'@babel/parser': 7.26.2
@ -5487,6 +5812,33 @@ snapshots:
'@esbuild/win32-ia32': 0.23.1
'@esbuild/win32-x64': 0.23.1
esbuild@0.24.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.24.0
'@esbuild/android-arm': 0.24.0
'@esbuild/android-arm64': 0.24.0
'@esbuild/android-x64': 0.24.0
'@esbuild/darwin-arm64': 0.24.0
'@esbuild/darwin-x64': 0.24.0
'@esbuild/freebsd-arm64': 0.24.0
'@esbuild/freebsd-x64': 0.24.0
'@esbuild/linux-arm': 0.24.0
'@esbuild/linux-arm64': 0.24.0
'@esbuild/linux-ia32': 0.24.0
'@esbuild/linux-loong64': 0.24.0
'@esbuild/linux-mips64el': 0.24.0
'@esbuild/linux-ppc64': 0.24.0
'@esbuild/linux-riscv64': 0.24.0
'@esbuild/linux-s390x': 0.24.0
'@esbuild/linux-x64': 0.24.0
'@esbuild/netbsd-x64': 0.24.0
'@esbuild/openbsd-arm64': 0.24.0
'@esbuild/openbsd-x64': 0.24.0
'@esbuild/sunos-x64': 0.24.0
'@esbuild/win32-arm64': 0.24.0
'@esbuild/win32-ia32': 0.24.0
'@esbuild/win32-x64': 0.24.0
escalade@3.2.0: {}
escape-html@1.0.3: {}
@ -5591,7 +5943,13 @@ snapshots:
dependencies:
pend: 1.2.0
fdir@6.4.0: {}
fdir@6.4.0(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
fdir@6.4.2(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
fecha@4.2.3: {}
@ -5988,6 +6346,8 @@ snapshots:
jiti@2.3.3:
optional: true
joycon@3.1.1: {}
js-cookie@3.0.5: {}
js-stringify@1.0.2:
@ -6026,6 +6386,8 @@ snapshots:
lines-and-columns@1.2.4: {}
load-tsconfig@0.2.5: {}
locate-character@3.0.0: {}
locate-path@5.0.0:
@ -6042,6 +6404,8 @@ snapshots:
lodash.merge@4.6.2: {}
lodash.sortby@4.7.0: {}
lodash.startcase@4.4.0: {}
lodash.truncate@4.4.2: {}
@ -6488,8 +6852,12 @@ snapshots:
picocolors@1.1.0: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
picomatch@4.0.2: {}
pify@2.3.0: {}
pify@3.0.0: {}
@ -6533,7 +6901,6 @@ snapshots:
postcss: 8.4.47
tsx: 4.19.2
yaml: 2.5.1
optional: true
postcss-nested@6.2.0(postcss@8.4.47):
dependencies:
@ -6991,6 +7358,10 @@ snapshots:
source-map@0.6.1: {}
source-map@0.8.0-beta.0:
dependencies:
whatwg-url: 7.1.0
sourcemap-codec@1.4.8: {}
spawndamnit@2.0.0:
@ -7078,11 +7449,11 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
svelte-check@4.0.4(svelte@4.2.19)(typescript@5.6.3):
svelte-check@4.0.4(picomatch@4.0.2)(svelte@4.2.19)(typescript@5.6.3):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
chokidar: 4.0.1
fdir: 6.4.0
fdir: 6.4.0(picomatch@4.0.2)
picocolors: 1.1.0
sade: 1.8.1
svelte: 4.2.19
@ -7227,6 +7598,13 @@ snapshots:
globalyzer: 0.1.0
globrex: 0.1.2
tinyexec@0.3.1: {}
tinyglobby@0.2.10:
dependencies:
fdir: 6.4.2(picomatch@4.0.2)
picomatch: 4.0.2
tmp@0.0.33:
dependencies:
os-tmpdir: 1.0.2
@ -7251,6 +7629,12 @@ snapshots:
totalist@3.0.1: {}
tr46@1.0.1:
dependencies:
punycode: 2.3.1
tree-kill@1.2.2: {}
triple-beam@1.4.1: {}
ts-interface-checker@0.1.13: {}
@ -7259,6 +7643,33 @@ snapshots:
tslib@2.8.1: {}
tsup@8.3.5(jiti@2.3.3)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.5.1):
dependencies:
bundle-require: 5.0.0(esbuild@0.24.0)
cac: 6.7.14
chokidar: 4.0.1
consola: 3.2.3
debug: 4.3.7
esbuild: 0.24.0
joycon: 3.1.1
picocolors: 1.1.1
postcss-load-config: 6.0.1(jiti@2.3.3)(postcss@8.4.47)(tsx@4.19.2)(yaml@2.5.1)
resolve-from: 5.0.0
rollup: 4.24.0
source-map: 0.8.0-beta.0
sucrase: 3.35.0
tinyexec: 0.3.1
tinyglobby: 0.2.10
tree-kill: 1.2.2
optionalDependencies:
postcss: 8.4.47
typescript: 5.6.3
transitivePeerDependencies:
- jiti
- supports-color
- tsx
- yaml
tsx@4.19.2:
dependencies:
esbuild: 0.23.1
@ -7384,6 +7795,14 @@ snapshots:
web-streams-polyfill@3.2.1: {}
webidl-conversions@4.0.2: {}
whatwg-url@7.1.0:
dependencies:
lodash.sortby: 4.7.0
tr46: 1.0.1
webidl-conversions: 4.0.2
which-module@2.0.1: {}
which@1.3.1:

View File

@ -1,2 +1,3 @@
packages:
- 'packages/*'
- 'packages/pockethost/src/mothership-app'