@meith/settings 0.33.4 → 0.35.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/settings",
3
- "version": "0.33.4",
3
+ "version": "0.35.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "zod": "^4.4.3",
23
- "@meith/core": "0.33.4",
24
- "@meith/i18n": "0.33.4"
23
+ "@meith/core": "0.35.0",
24
+ "@meith/i18n": "0.35.0"
25
25
  }
26
26
  }
@@ -0,0 +1 @@
1
+ export const SCHEDULE_TIME_PATTERN = /^([01]\d|2[0-3]):([0-5]\d)$/
@@ -2,6 +2,7 @@ import { z } from 'zod'
2
2
 
3
3
  import { normaliseLocale, SOURCE_LOCALE } from '@meith/i18n'
4
4
 
5
+ import { SCHEDULE_TIME_PATTERN } from './backup-schedule'
5
6
  import { DEFAULT_PRIVACY_POLICY, DEFAULT_TERMS_OF_SERVICE } from './legal'
6
7
  import { mailEndpointProblem } from './mail'
7
8
  import { isUsableFeedUrl, isUsableIssuer, isUsableOrigin } from './origin'
@@ -23,6 +24,7 @@ export type SettingGroup =
23
24
  | 'federation'
24
25
  | 'antispam'
25
26
  | 'push'
27
+ | 'backup'
26
28
  | 'legal'
27
29
 
28
30
  interface SettingDefinitionBase<T> {
@@ -34,6 +36,7 @@ interface SettingDefinitionBase<T> {
34
36
  readonly default: T
35
37
  readonly invalidates?: readonly string[]
36
38
  readonly secret?: boolean
39
+ readonly sealed?: boolean
37
40
  readonly ui?: {
38
41
  readonly multiline?: boolean
39
42
  readonly options?: readonly {
@@ -1170,6 +1173,252 @@ export const SETTING_DEFINITIONS = [
1170
1173
  default: 'https://www.meith.dev/marketplace/v1.json',
1171
1174
  ui: { advanced: true },
1172
1175
  }),
1176
+ define({
1177
+ key: 'backup.schedule',
1178
+ group: 'backup',
1179
+ label: 'Automatic backups',
1180
+ description:
1181
+ 'How often the board takes a backup of itself without being asked. Each run ' +
1182
+ 'bundles the database dump and, by default, the uploads into one file in the ' +
1183
+ 'backup directory, ships it to the off-site destination below when one is ' +
1184
+ 'configured, and prunes the oldest bundles under the retention rules. A ' +
1185
+ 'failed run is reported on the System screen and notified to administrators ' +
1186
+ 'the way any failing task is.',
1187
+ schema: z.enum(['off', 'daily', 'weekly']),
1188
+ default: 'off',
1189
+ ui: {
1190
+ options: [
1191
+ { value: 'off', label: 'Off' },
1192
+ { value: 'daily', label: 'Every day' },
1193
+ { value: 'weekly', label: 'Every week' },
1194
+ ],
1195
+ },
1196
+ }),
1197
+ define({
1198
+ key: 'backup.time',
1199
+ group: 'backup',
1200
+ label: 'Time of day',
1201
+ description:
1202
+ 'When the automatic backup starts, as a 24-hour clock in UTC — 02:00 is two in ' +
1203
+ 'the morning UTC, whatever the server or your browser thinks the local time is. ' +
1204
+ 'Pick the quietest hour the board has; the dump reads every table while it runs.',
1205
+ schema: z
1206
+ .string()
1207
+ .trim()
1208
+ .refine((value) => SCHEDULE_TIME_PATTERN.test(value), 'Give a time as HH:MM, in UTC.'),
1209
+ default: '02:00',
1210
+ }),
1211
+ define({
1212
+ key: 'backup.weekday',
1213
+ group: 'backup',
1214
+ label: 'Day of the week',
1215
+ description: 'Which day a weekly backup runs on. Ignored unless the schedule is weekly.',
1216
+ schema: z.enum(['0', '1', '2', '3', '4', '5', '6']),
1217
+ default: '1',
1218
+ ui: {
1219
+ options: [
1220
+ { value: '1', label: 'Monday' },
1221
+ { value: '2', label: 'Tuesday' },
1222
+ { value: '3', label: 'Wednesday' },
1223
+ { value: '4', label: 'Thursday' },
1224
+ { value: '5', label: 'Friday' },
1225
+ { value: '6', label: 'Saturday' },
1226
+ { value: '0', label: 'Sunday' },
1227
+ ],
1228
+ },
1229
+ }),
1230
+ define({
1231
+ key: 'backup.keep',
1232
+ group: 'backup',
1233
+ label: 'Backups to keep',
1234
+ description:
1235
+ 'How many of the newest bundles survive a run, in the backup directory and at ' +
1236
+ 'the off-site destination alike. Pruning happens only after a new bundle is ' +
1237
+ 'safely written, so a run that fails never eats the good ones. Each bundle ' +
1238
+ 'that carries the uploads is roughly the size of the board, so this is also ' +
1239
+ 'the disk knob.',
1240
+ schema: z.number().int().min(1).max(365),
1241
+ default: 7,
1242
+ ui: { min: 1, max: 365 },
1243
+ }),
1244
+ define({
1245
+ key: 'backup.keep_days',
1246
+ group: 'backup',
1247
+ label: 'Days to keep a backup',
1248
+ description:
1249
+ 'Bundles older than this many days are pruned as well, however few remain — ' +
1250
+ 'except the newest, which always survives. 0 keeps a bundle until the count ' +
1251
+ 'above pushes it out.',
1252
+ schema: z.number().int().min(0).max(3650),
1253
+ default: 0,
1254
+ ui: { min: 0, max: 3650 },
1255
+ }),
1256
+ define({
1257
+ key: 'backup.uploads',
1258
+ group: 'backup',
1259
+ label: 'Uploads in the bundle',
1260
+ description:
1261
+ 'Whether a bundle carries the avatars, attachments and board images. ' +
1262
+ '"Automatic" includes them when they live on the local disk or a Vercel Blob ' +
1263
+ 'store, where the bundle is the only copy, and leaves an S3 bucket alone, ' +
1264
+ 'because a bucket has a backup story of its own. A bundle without the uploads ' +
1265
+ 'restores a board whose posts have broken images.',
1266
+ schema: z.enum(['auto', 'include', 'skip']),
1267
+ default: 'auto',
1268
+ ui: {
1269
+ options: [
1270
+ { value: 'auto', label: 'Automatic' },
1271
+ { value: 'include', label: 'Always include' },
1272
+ { value: 'skip', label: 'Never include' },
1273
+ ],
1274
+ },
1275
+ }),
1276
+ define({
1277
+ key: 'backup.before_upgrade',
1278
+ group: 'backup',
1279
+ label: 'Back up before migrating',
1280
+ description:
1281
+ 'Take a bundle before the migrator applies a pending schema migration, so a ' +
1282
+ 'new release always has a restore point in front of it. The bundle lands in ' +
1283
+ 'the backup directory and ships off-site like any other. The upgrade is ' +
1284
+ 'refused if that backup fails — a migration is forward-only, and the backup ' +
1285
+ 'is the only way back. Needs the migrate container to mount the backup ' +
1286
+ 'directory, which the shipped compose files do.',
1287
+ schema: z.boolean(),
1288
+ default: false,
1289
+ }),
1290
+ define({
1291
+ key: 'backup.destination',
1292
+ group: 'backup',
1293
+ label: 'Off-site destination',
1294
+ description:
1295
+ 'Where every bundle is also shipped, so a backup survives the server. "None" ' +
1296
+ 'keeps bundles on this machine only. An S3-compatible bucket takes the bucket ' +
1297
+ 'fields below; a WebDAV folder — Nextcloud, ownCloud, a Hetzner Storage Box — ' +
1298
+ 'takes the WebDAV fields. Setting BACKUP_S3_* or BACKUP_WEBDAV_* in the ' +
1299
+ 'environment overrides this whole group and makes its fields inert.',
1300
+ schema: z.enum(['none', 's3', 'webdav']),
1301
+ default: 'none',
1302
+ ui: {
1303
+ options: [
1304
+ { value: 'none', label: 'None — bundles stay on the server' },
1305
+ { value: 's3', label: 'An S3-compatible bucket' },
1306
+ { value: 'webdav', label: 'A WebDAV folder' },
1307
+ ],
1308
+ },
1309
+ }),
1310
+ define({
1311
+ key: 'backup.s3_bucket',
1312
+ group: 'backup',
1313
+ label: 'Off-site bucket',
1314
+ description:
1315
+ 'An S3-compatible bucket every bundle is also shipped to — Backblaze B2, ' +
1316
+ 'Cloudflare R2, Hetzner, Scaleway, MinIO on a machine you trust. A bucket of ' +
1317
+ 'its own, never the one the uploads live in: a backup of the uploads should ' +
1318
+ 'not sit where losing the uploads loses it. Read when the destination above ' +
1319
+ 'is the bucket.',
1320
+ schema: z.string().trim().max(255),
1321
+ default: '',
1322
+ }),
1323
+ define({
1324
+ key: 'backup.s3_region',
1325
+ group: 'backup',
1326
+ label: 'Bucket region',
1327
+ description: 'The bucket’s region, or "auto" for a provider that ignores it, such as R2.',
1328
+ schema: z.string().trim().max(100),
1329
+ default: '',
1330
+ }),
1331
+ define({
1332
+ key: 'backup.s3_endpoint',
1333
+ group: 'backup',
1334
+ label: 'Endpoint',
1335
+ description:
1336
+ 'The API address for anything that is not AWS itself — R2, MinIO, Spaces. Leave ' +
1337
+ 'empty for AWS. Switches the client to path-style addressing, the way ' +
1338
+ 'S3_ENDPOINT does for the uploads.',
1339
+ schema: z
1340
+ .string()
1341
+ .trim()
1342
+ .refine(
1343
+ (value) => value === '' || z.string().url().safeParse(value).success,
1344
+ 'That does not look like a URL.',
1345
+ ),
1346
+ default: '',
1347
+ }),
1348
+ define({
1349
+ key: 'backup.s3_prefix',
1350
+ group: 'backup',
1351
+ label: 'Key prefix',
1352
+ description:
1353
+ 'A folder inside the bucket, so one bucket can hold several boards. Left ' +
1354
+ 'empty, bundles sit at the root.',
1355
+ schema: z.string().trim().max(200),
1356
+ default: '',
1357
+ }),
1358
+ define({
1359
+ key: 'backup.s3_access_key_id',
1360
+ group: 'backup',
1361
+ label: 'Access key id',
1362
+ description:
1363
+ 'A credential with write and list on that bucket alone — nothing wider. It ' +
1364
+ 'ships bundles, lists them, and prunes the oldest.',
1365
+ schema: z.string().trim().max(255),
1366
+ default: '',
1367
+ }),
1368
+ define({
1369
+ key: 'backup.s3_secret_access_key',
1370
+ group: 'backup',
1371
+ label: 'Secret access key',
1372
+ description:
1373
+ 'Stored on the board, sealed under AUTH_SECRET: the database row holds ' +
1374
+ 'ciphertext, and a copy of the database alone cannot read it. The seal is ' +
1375
+ 'also why AUTH_SECRET has to survive a restore for the destination to.',
1376
+ schema: z.string().max(255),
1377
+ default: '',
1378
+ secret: true,
1379
+ sealed: true,
1380
+ }),
1381
+ define({
1382
+ key: 'backup.webdav_url',
1383
+ group: 'backup',
1384
+ label: 'WebDAV folder address',
1385
+ description:
1386
+ 'The full address of a folder that already exists, such as ' +
1387
+ 'https://cloud.example/remote.php/dav/files/backups/board/ on Nextcloud. ' +
1388
+ 'Bundles are written into it and the oldest pruned from it; the folder itself ' +
1389
+ 'is never created or removed.',
1390
+ schema: z
1391
+ .string()
1392
+ .trim()
1393
+ .refine(
1394
+ (value) => value === '' || /^https?:\/\/[^?#]+$/.test(value),
1395
+ 'Give an http:// or https:// address of a folder, with no query string.',
1396
+ ),
1397
+ default: '',
1398
+ }),
1399
+ define({
1400
+ key: 'backup.webdav_username',
1401
+ group: 'backup',
1402
+ label: 'WebDAV username',
1403
+ description:
1404
+ 'The account the bundles are written as. Nextcloud and ownCloud let you make an ' +
1405
+ 'app password for one purpose; use that rather than the account password.',
1406
+ schema: z.string().trim().max(255),
1407
+ default: '',
1408
+ }),
1409
+ define({
1410
+ key: 'backup.webdav_password',
1411
+ group: 'backup',
1412
+ label: 'WebDAV password',
1413
+ description:
1414
+ 'Stored on the board, sealed under AUTH_SECRET the way the bucket secret is: ' +
1415
+ 'the database row holds ciphertext, and a copy of the database alone cannot ' +
1416
+ 'read it.',
1417
+ schema: z.string().max(255),
1418
+ default: '',
1419
+ secret: true,
1420
+ sealed: true,
1421
+ }),
1173
1422
  ] as const
1174
1423
 
1175
1424
  export type SettingKey = (typeof SETTING_DEFINITIONS)[number]['key']