pockethost 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE.md +16 -0
  2. package/dist/index.mjs +3156 -0
  3. package/dist/instance-app/migrations/1699198734_pockethost_defaults.js +39 -0
  4. package/dist/instance-app/pb_hooks/_ph_admin_sync.pb.js +71 -0
  5. package/dist/instance-app/pb_hooks/_ph_lib.js +19 -0
  6. package/dist/instance-app/tsconfig.json +8 -0
  7. package/dist/instance-app/types/goja.d.ts +58 -0
  8. package/dist/instance-app/types/lib.d.ts +5 -0
  9. package/dist/instance-app/types/types.d.ts +19573 -0
  10. package/dist/mothership-app/migrations/1705062576_collections_snapshot.js +1863 -0
  11. package/dist/mothership-app/migrations/1705083826_updated_stats.js +721 -0
  12. package/dist/mothership-app/migrations/1706060446_updated_instances.js +44 -0
  13. package/dist/mothership-app/pb_hooks/src/instances-create.pb.js +65 -0
  14. package/dist/mothership-app/pb_hooks/src/instances-delete.pb.js +69 -0
  15. package/dist/mothership-app/pb_hooks/src/instances-migrate-versions.pb.js +33 -0
  16. package/dist/mothership-app/pb_hooks/src/instances-notify-discord-after-create.pb.js +31 -0
  17. package/dist/mothership-app/pb_hooks/src/instances-notify-on-maintenance-mode.pb.js +52 -0
  18. package/dist/mothership-app/pb_hooks/src/instances-reset-idle.pb.js +7 -0
  19. package/dist/mothership-app/pb_hooks/src/instances-update.pb.js +121 -0
  20. package/dist/mothership-app/pb_hooks/src/instances-validate-before-create.pb.js +16 -0
  21. package/dist/mothership-app/pb_hooks/src/instances-validate-before-update.pb.js +43 -0
  22. package/dist/mothership-app/pb_hooks/src/lib.js +171 -0
  23. package/dist/mothership-app/pb_hooks/src/ls.pb.js +159 -0
  24. package/dist/mothership-app/pb_hooks/src/mail.pb.js +49 -0
  25. package/dist/mothership-app/pb_hooks/src/notifications.pb.js +62 -0
  26. package/dist/mothership-app/pb_hooks/src/random-words.js +2085 -0
  27. package/dist/mothership-app/pb_hooks/src/signup.pb.js +214 -0
  28. package/dist/mothership-app/pb_hooks/src/sns.pb.js +133 -0
  29. package/dist/mothership-app/pb_hooks/src/unsubscribe.pb.js +33 -0
  30. package/dist/mothership-app/pb_hooks/src/user-token.pb.js +33 -0
  31. package/dist/mothership-app/pb_hooks/src/user-welcome-msg.pb.js +32 -0
  32. package/dist/mothership-app/pb_hooks/src/versions-route.pb.js +12 -0
  33. package/dist/mothership-app/pb_hooks/src/versions.js +0 -0
  34. package/dist/mothership-app/pb_hooks/tsconfig.json +8 -0
  35. package/dist/mothership-app/pb_hooks/types/global.d.ts +1 -0
  36. package/dist/mothership-app/pb_hooks/types/goja.d.ts +58 -0
  37. package/dist/mothership-app/pb_hooks/types/lemon.d.ts +12 -0
  38. package/dist/mothership-app/pb_hooks/types/lib.d.ts +55 -0
  39. package/dist/mothership-app/pb_hooks/types/types.d.ts +19573 -0
  40. package/dist/mothership-app/ph_app/index.js +11 -0
  41. package/dist/mothership-app/ph_app/package-lock.json +657 -0
  42. package/dist/mothership-app/ph_app/package.json +14 -0
  43. package/package.json +144 -0
  44. package/readme.md +168 -0
@@ -0,0 +1,44 @@
1
+ /// <reference path="../pb_data/types.d.ts" />
2
+ migrate((db) => {
3
+ const dao = new Dao(db)
4
+ const collection = dao.findCollectionByNameOrId("etae8tuiaxl6xfv")
5
+
6
+ // update
7
+ collection.schema.addField(new SchemaField({
8
+ "system": false,
9
+ "id": "5oz9huwg",
10
+ "name": "cname",
11
+ "type": "text",
12
+ "required": false,
13
+ "presentable": false,
14
+ "unique": false,
15
+ "options": {
16
+ "min": null,
17
+ "max": null,
18
+ "pattern": "^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,24}$"
19
+ }
20
+ }))
21
+
22
+ return dao.saveCollection(collection)
23
+ }, (db) => {
24
+ const dao = new Dao(db)
25
+ const collection = dao.findCollectionByNameOrId("etae8tuiaxl6xfv")
26
+
27
+ // update
28
+ collection.schema.addField(new SchemaField({
29
+ "system": false,
30
+ "id": "5oz9huwg",
31
+ "name": "cname",
32
+ "type": "text",
33
+ "required": false,
34
+ "presentable": false,
35
+ "unique": false,
36
+ "options": {
37
+ "min": null,
38
+ "max": null,
39
+ "pattern": "^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}$"
40
+ }
41
+ }))
42
+
43
+ return dao.saveCollection(collection)
44
+ })
@@ -0,0 +1,65 @@
1
+ /*
2
+ {
3
+ "subdomain": "foo"
4
+ }
5
+ */
6
+ routerAdd(
7
+ 'POST',
8
+ '/api/instance',
9
+ (c) => {
10
+ const dao = $app.dao()
11
+ const { audit, mkLog } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
12
+
13
+ const log = mkLog(`POST:instance`)
14
+
15
+ const authRecord = /** @type {models.Record} */ (c.get('authRecord')) // empty if not authenticated as regular auth record
16
+ log(`***authRecord`, JSON.stringify(authRecord))
17
+
18
+ if (!authRecord) {
19
+ throw new Error(`Expected authRecord here`)
20
+ }
21
+
22
+ log(`***TOP OF POST`)
23
+ let data = /** @type{ {subdomain?: string} } */ (
24
+ new DynamicModel({
25
+ subdomain: '',
26
+ })
27
+ )
28
+
29
+ log(`***before bind`)
30
+
31
+ c.bind(data)
32
+
33
+ log(`***after bind`)
34
+
35
+ // This is necessary for destructuring to work correctly
36
+ data = JSON.parse(JSON.stringify(data))
37
+
38
+ const { subdomain } = data
39
+
40
+ log(`***vars`, JSON.stringify({ subdomain }))
41
+
42
+ if (!subdomain) {
43
+ throw new BadRequestError(
44
+ `Subdomain is required when creating an instance.`,
45
+ )
46
+ }
47
+
48
+ const { versions } = require(`${__hooks}/versions.js`)
49
+
50
+ const collection = dao.findCollectionByNameOrId('instances')
51
+ const record = new Record(collection)
52
+ record.set('uid', authRecord.getId())
53
+ record.set('subdomain', subdomain)
54
+ record.set('status', 'idle')
55
+ record.set('version', versions[0])
56
+ record.set('syncAdmin', true)
57
+ record.set('notifyMaintenanceMode', true)
58
+
59
+ const form = new RecordUpsertForm($app, record)
60
+ form.submit()
61
+
62
+ return c.json(200, { instance: record })
63
+ },
64
+ $apis.requireRecordAuth(),
65
+ )
@@ -0,0 +1,69 @@
1
+ /*
2
+ {
3
+ "id": "kz4ngg77eaw1ho0",
4
+ "fields": {
5
+ "maintenance": true
6
+ "name": '',
7
+ "version": ''
8
+ }
9
+ }
10
+ */
11
+ routerAdd(
12
+ 'DELETE',
13
+ '/api/instance/:id',
14
+ (c) => {
15
+ const dao = $app.dao()
16
+ const { audit, mkLog } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
17
+
18
+ const log = mkLog(`DELETE:instance`)
19
+
20
+ log(`TOP OF DELETE`)
21
+ let data = new DynamicModel({
22
+ id: '',
23
+ })
24
+
25
+ c.bind(data)
26
+ log(`After bind`)
27
+
28
+ // This is necessary for destructuring to work correctly
29
+ data = JSON.parse(JSON.stringify(data))
30
+
31
+ const id = c.pathParam('id')
32
+
33
+ log(
34
+ `vars`,
35
+ JSON.stringify({
36
+ id,
37
+ }),
38
+ )
39
+
40
+ const authRecord = /** @type {models.Record} */ (c.get('authRecord')) // empty if not authenticated as regular auth record
41
+ log(`authRecord`, JSON.stringify(authRecord))
42
+
43
+ if (!authRecord) {
44
+ throw new BadRequestError(`Expected authRecord here`)
45
+ }
46
+
47
+ const record = dao.findRecordById('instances', id)
48
+ if (!record) {
49
+ throw new BadRequestError(`Instance ${id} not found.`)
50
+ }
51
+ if (record.get('uid') !== authRecord.id) {
52
+ throw new BadRequestError(`Not authorized`)
53
+ }
54
+
55
+ if (record.getString('status').toLowerCase() !== 'idle') {
56
+ throw new BadRequestError(`Instance must be shut down first.`)
57
+ }
58
+
59
+ const path = [$os.getenv('DATA_ROOT'), id].join('/')
60
+ log(`path ${path}`)
61
+ const res = $os.removeAll(path)
62
+ log(`res`, res)
63
+
64
+ dao.deleteRecord(record)
65
+
66
+ return c.json(200, { status: 'ok' })
67
+ },
68
+ $apis.requireRecordAuth(),
69
+ )
@@ -0,0 +1,33 @@
1
+ /** Migrate version numbers */
2
+ onAfterBootstrap((e) => {
3
+ const dao = $app.dao()
4
+ const { audit, mkLog } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
5
+
6
+ const log = mkLog(`bootstrap`)
7
+
8
+ const records = dao.findRecordsByFilter(`instances`, '1=1')
9
+ const { versions } = require(`${__hooks}/versions.js`)
10
+ const unrecognized = []
11
+ records.forEach((record) => {
12
+ const v = record.get('version').trim()
13
+ if (versions.includes(v)) return
14
+ const newVersion = (() => {
15
+ if (v.startsWith(`~`)) {
16
+ const [major, minor] = v.slice(1).split('.')
17
+ const newVersion = [major, minor, '*'].join('.')
18
+ return newVersion
19
+ } else {
20
+ if (v === `^0` || v === `0` || v === '1') {
21
+ return versions[0]
22
+ }
23
+ }
24
+ })()
25
+ if (versions.includes(newVersion)) {
26
+ record.set(`version`, newVersion)
27
+ dao.saveRecord(record)
28
+ } else {
29
+ unrecognized.push(v)
30
+ }
31
+ })
32
+ log({ unrecognized })
33
+ })
@@ -0,0 +1,31 @@
1
+ /// <reference path="../types/types.d.ts" />
2
+
3
+ onModelAfterCreate((e) => {
4
+ const dao = e.dao || $app.dao()
5
+ const { audit, mkLog } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
6
+
7
+ const log = mkLog(`instances:create:discord:notify`)
8
+
9
+ const webhookUrl = $os.getenv('DISCORD_POCKETSTREAM_URL')
10
+ if (!webhookUrl) {
11
+ return
12
+ }
13
+ const version = e.model.get('version')
14
+
15
+ try {
16
+ const res = $http.send({
17
+ url: webhookUrl,
18
+ method: 'POST',
19
+ body: JSON.stringify({
20
+ content: `Someone just created an app running PocketBase v${version}`,
21
+ }),
22
+ headers: { 'content-type': 'application/json' },
23
+ timeout: 5, // in seconds
24
+ })
25
+ } catch (e) {
26
+ audit(`ERROR`, `Instance creation discord notify failed with ${e}`, {
27
+ log,
28
+ dao,
29
+ })
30
+ }
31
+ }, 'instances')
@@ -0,0 +1,52 @@
1
+ onModelAfterUpdate((e) => {
2
+ const dao = e.dao || $app.dao()
3
+
4
+ const newModel = /** @type {models.Record} */ (e.model)
5
+ const oldModel = newModel.originalCopy()
6
+
7
+ const { mkLog, enqueueNotification, audit } = /** @type {Lib} */ (
8
+ require(`${__hooks}/lib.js`)
9
+ )
10
+
11
+ const log = mkLog(`maintenance-mode`)
12
+
13
+ try {
14
+ // Bail out if we aren't in maintenance mode
15
+ const isMaintenance = newModel.get('maintenance')
16
+ // log({ isMaintenance })
17
+ if (!isMaintenance) return
18
+
19
+ // Bail out if the maintenance mode flag has not changed
20
+ if (isMaintenance === oldModel.get(`maintenance`)) return
21
+
22
+ log(`switched`)
23
+ const uid = newModel.get(`uid`)
24
+ const user = dao.findRecordById('users', uid)
25
+
26
+ // Bail out if the user has notifications disabled globally or for the instance
27
+ const shouldNotify =
28
+ user.getBool(`notifyMaintenanceMode`) &&
29
+ newModel.getBool(`notifyMaintenanceMode`)
30
+ log({ shouldNotify })
31
+ if (!shouldNotify) return
32
+
33
+ // Send the email warning about maintenance mode
34
+ const instanceId = newModel.getId()
35
+ const subdomain = newModel.getString(`subdomain`)
36
+ const address = user.getString(`email`)
37
+ log({ instanceId, subdomain, address })
38
+ enqueueNotification(`email`, `maintenance-mode`, uid, {
39
+ log,
40
+ dao,
41
+ message_template_vars: {
42
+ subdomain,
43
+ instanceId,
44
+ },
45
+ })
46
+ } catch (e) {
47
+ audit('ERROR', `Failed to enqueue template with ${e}`, {
48
+ log,
49
+ dao,
50
+ })
51
+ }
52
+ }, 'instances')
@@ -0,0 +1,7 @@
1
+ /// <reference path="../types/types.d.ts" />
2
+
3
+ /** Reset instance status to idle on start */
4
+ onAfterBootstrap((e) => {
5
+ const dao = $app.dao()
6
+ dao.db().newQuery(`update instances set status='idle'`).execute()
7
+ })
@@ -0,0 +1,121 @@
1
+ /// <reference path="../types/types.d.ts" />
2
+
3
+ /*
4
+ {
5
+ "id": "kz4ngg77eaw1ho0",
6
+ "fields": {
7
+ "maintenance": true
8
+ "name": '',
9
+ "version": ''
10
+ }
11
+ }
12
+ */
13
+ routerAdd(
14
+ 'PUT',
15
+ '/api/instance/:id',
16
+ (c) => {
17
+ const dao = $app.dao()
18
+ const { mkLog, audit, removeEmptyKeys } = /** @type {Lib} */ (
19
+ require(`${__hooks}/lib.js`)
20
+ )
21
+ const log = mkLog(`PUT:instance`)
22
+
23
+ log(`TOP OF PUT`)
24
+
25
+ let data =
26
+ /**
27
+ * @type {{
28
+ * id: string
29
+ * fields: {
30
+ * subdomain: string | null
31
+ * maintenance: boolean | null
32
+ * version: string | null
33
+ * secrets: StringKvLookup | null
34
+ * syncAdmin: boolean | null
35
+ * dev: boolean | null
36
+ * cname: string | null
37
+ * notifyMaintenanceMode: boolean | null
38
+ * }
39
+ * }} }
40
+ */
41
+ (
42
+ new DynamicModel({
43
+ id: '',
44
+ fields: {
45
+ subdomain: null,
46
+ maintenance: null,
47
+ version: null,
48
+ secrets: null,
49
+ syncAdmin: null,
50
+ dev: null,
51
+ cname: null,
52
+ notifyMaintenanceMode: null,
53
+ },
54
+ })
55
+ )
56
+
57
+ c.bind(data)
58
+ log(`After bind`)
59
+
60
+ // This is necessary for destructuring to work correctly
61
+ data = JSON.parse(JSON.stringify(data))
62
+
63
+ const id = c.pathParam('id')
64
+ const {
65
+ fields: {
66
+ subdomain,
67
+ maintenance,
68
+ version,
69
+ secrets,
70
+ syncAdmin,
71
+ dev,
72
+ cname,
73
+ notifyMaintenanceMode,
74
+ },
75
+ } = data
76
+
77
+ log(
78
+ `vars`,
79
+ JSON.stringify({
80
+ id,
81
+ subdomain,
82
+ maintenance,
83
+ version,
84
+ secrets,
85
+ syncAdmin,
86
+ dev,
87
+ cname,
88
+ notifyMaintenanceMode,
89
+ }),
90
+ )
91
+
92
+ const record = dao.findRecordById('instances', id)
93
+ const authRecord = /** @type {models.Record} */ (c.get('authRecord')) // empty if not authenticated as regular auth record
94
+ log(`authRecord`, JSON.stringify(authRecord))
95
+
96
+ if (!authRecord) {
97
+ throw new Error(`Expected authRecord here`)
98
+ }
99
+ if (record.get('uid') !== authRecord.id) {
100
+ throw new BadRequestError(`Not authorized`)
101
+ }
102
+
103
+ const sanitized = removeEmptyKeys({
104
+ subdomain,
105
+ version,
106
+ maintenance,
107
+ secrets,
108
+ syncAdmin,
109
+ dev,
110
+ cname,
111
+ notifyMaintenanceMode,
112
+ })
113
+
114
+ const form = new RecordUpsertForm($app, record)
115
+ form.loadData(sanitized)
116
+ form.submit()
117
+
118
+ return c.json(200, { status: 'ok' })
119
+ },
120
+ $apis.requireRecordAuth(),
121
+ )
@@ -0,0 +1,16 @@
1
+ /// <reference path="../types/types.d.ts" />
2
+
3
+ /** Validate instance version */
4
+ onModelBeforeCreate((e) => {
5
+ const dao = e.dao || $app.dao()
6
+ const { versions } = require(`${__hooks}/versions.js`)
7
+
8
+ const version = e.model.get('version')
9
+ if (!versions.includes(version)) {
10
+ throw new BadRequestError(
11
+ `Invalid version ${version}. Version must be one of: ${versions.join(
12
+ ', ',
13
+ )}`,
14
+ )
15
+ }
16
+ }, 'instances')
@@ -0,0 +1,43 @@
1
+ /// <reference path="../types/types.d.ts" />
2
+
3
+ /** Validate instance version */
4
+ onModelBeforeUpdate((e) => {
5
+ const dao = e.dao || $app.dao()
6
+ const { versions } = require(`${__hooks}/versions.js`)
7
+
8
+ const version = e.model.get('version')
9
+ if (!versions.includes(version)) {
10
+ throw new BadRequestError(
11
+ `Invalid version ${version}. Version must be one of: ${versions.join(
12
+ ', ',
13
+ )}`,
14
+ )
15
+ }
16
+
17
+ const id = e.model.getId()
18
+ const cname = e.model.get('cname')
19
+ if (cname.length > 0) {
20
+ const result = new DynamicModel({
21
+ id: '',
22
+ })
23
+
24
+ const inUse = (() => {
25
+ try {
26
+ dao
27
+ .db()
28
+ .newQuery(
29
+ `select id from instances where cname='${cname}' and id <> '${id}'`,
30
+ )
31
+ .one(result)
32
+ } catch (e) {
33
+ console.log(`*** cname OK ${cname}`)
34
+ return false
35
+ }
36
+ return true
37
+ })()
38
+
39
+ if (inUse) {
40
+ throw new BadRequestError(`Custom domain already in use.`)
41
+ }
42
+ }
43
+ }, 'instances')
@@ -0,0 +1,171 @@
1
+ /** @type {Lib['audit']} */
2
+ const audit = (event, note, { log, dao, extra = {} }) => {
3
+ log(`AUDIT:${event}: ${note}`, extra)
4
+ dao.saveRecord(
5
+ new Record(dao.findCollectionByNameOrId('audit'), {
6
+ event,
7
+ note,
8
+ ...extra,
9
+ }),
10
+ )
11
+ }
12
+
13
+ /** @type {Lib['mkLog']} */
14
+ const mkLog =
15
+ (namespace) =>
16
+ /**
17
+ * @param {...any} s
18
+ * @returns
19
+ */
20
+ (...s) =>
21
+ console.log(
22
+ `[${namespace}]`,
23
+ ...s.map((p) => {
24
+ if (typeof p === 'object') return JSON.stringify(p, null, 2)
25
+ return p
26
+ }),
27
+ )
28
+
29
+ /**
30
+ * @param {...any} args
31
+ * @returns
32
+ */
33
+ const dbg = (...args) => console.log(args)
34
+
35
+ /**
36
+ * @param {string} template
37
+ * @param {{ [_: string]: string }} dict
38
+ * @returns
39
+ */
40
+ function interpolateString(template, dict) {
41
+ return template.replace(/\{\$(\w+)\}/g, (match, key) => {
42
+ dbg({ match, key })
43
+ const lowerKey = key.toLowerCase()
44
+ return dict.hasOwnProperty(lowerKey) ? dict[lowerKey] || '' : match
45
+ })
46
+ }
47
+
48
+ /** @type {Lib['enqueueNotification']} */
49
+ const enqueueNotification = (
50
+ channel,
51
+ template,
52
+ user_id,
53
+ { log, dao, message_template_vars = {} },
54
+ ) => {
55
+ log({ channel, template, user_id })
56
+ const emailTemplate = dao.findFirstRecordByData(
57
+ 'message_templates',
58
+ `slug`,
59
+ template,
60
+ )
61
+ log(`got email template`, emailTemplate)
62
+ if (!emailTemplate) throw new Error(`Template ${template} not found`)
63
+ const emailNotification = new Record(
64
+ dao.findCollectionByNameOrId('notifications'),
65
+ {
66
+ user: user_id,
67
+ channel,
68
+ message_template: emailTemplate.getId(),
69
+ message_template_vars,
70
+ },
71
+ )
72
+ log(`built notification record`, emailNotification)
73
+ dao.saveRecord(emailNotification)
74
+ }
75
+
76
+ /** @type {Lib['processNotification']} */
77
+ const processNotification = (notificationRec, { log, dao, test = false }) => {
78
+ log({ notificationRec })
79
+
80
+ const channel = notificationRec.getString(`channel`)
81
+ dao.expandRecord(notificationRec, ['message_template', 'user'], null)
82
+
83
+ const messageTemplateRec = notificationRec.expandedOne('message_template')
84
+ if (!messageTemplateRec) {
85
+ throw new Error(`Missing message template`)
86
+ }
87
+ const userRec = notificationRec.expandedOne('user')
88
+ if (!userRec) {
89
+ throw new Error(`Missing user record`)
90
+ }
91
+ const vars = JSON.parse(notificationRec.getString(`message_template_vars`))
92
+ const to = userRec.email()
93
+ const subject = interpolateString(
94
+ messageTemplateRec.getString(`subject`),
95
+ vars,
96
+ )
97
+ const html = interpolateString(
98
+ messageTemplateRec.getString(`body_html`),
99
+ vars,
100
+ )
101
+
102
+ log({ channel, messageTemplateRec, userRec, vars, to, subject, html })
103
+ switch (channel) {
104
+ case `email`:
105
+ /** @type {Partial<mailer.Message_In>} */
106
+ const msgArgs = {
107
+ from: {
108
+ address: $app.settings().meta.senderAddress,
109
+ name: $app.settings().meta.senderName,
110
+ },
111
+ to: [{ address: to }],
112
+ bcc: [{ address: `pockethost+notifications@benallfree.com` }],
113
+ subject,
114
+ html,
115
+ }
116
+ if (test) {
117
+ msgArgs.to = [{ address: `ben@benallfree.com` }]
118
+ msgArgs.subject = `***TEST ${to} *** ${msgArgs.subject}`
119
+ }
120
+ log({ msgArgs })
121
+ // @ts-ignore
122
+ const msg = new MailerMessage(msgArgs)
123
+ $app.newMailClient().send(msg)
124
+ log(`email sent`)
125
+ break
126
+
127
+ case `lemonbot`:
128
+ const res = $http.send({
129
+ url: test
130
+ ? `https://discord.com/api/webhooks/1194056858516869213/pwT-ymSxiPmLN5-M2a7FsesvrbtdmwUsKXWIKqebROJEfyP0E3-aSseUjDg4ojBkWV4D` // Test channel
131
+ : `https://discord.com/api/webhooks/1193619183594901575/JVDfdUz2HPEUk-nG1RfI3BK2Czyx5vw1YmeH7cNfgvXbHNGPH0oJncOYqxMA_u5b2u57`, // Stream channel
132
+ method: 'POST',
133
+ body: JSON.stringify({
134
+ content: subject,
135
+ }),
136
+ headers: { 'content-type': 'application/json' },
137
+ timeout: 5, // in seconds
138
+ })
139
+ log(`discord sent`)
140
+ break
141
+ default:
142
+ throw new Error(`Unsupported channel: ${channel}`)
143
+ }
144
+ if (!test) {
145
+ notificationRec.set(`delivered`, new DateTime())
146
+ dao.saveRecord(notificationRec)
147
+ }
148
+ }
149
+
150
+ /**
151
+ * @template T
152
+ * @param {T} obj
153
+ * @returns T
154
+ */
155
+ function removeEmptyKeys(obj) {
156
+ const sanitized = Object.entries(obj).reduce((acc, [key, value]) => {
157
+ if (value !== null && value !== undefined) {
158
+ acc[key] = value
159
+ }
160
+ return acc
161
+ }, /** @type {T} */ ({}))
162
+ return sanitized
163
+ }
164
+
165
+ module.exports = {
166
+ audit,
167
+ processNotification,
168
+ mkLog,
169
+ enqueueNotification,
170
+ removeEmptyKeys,
171
+ }