@dokebi/cli 0.4.2 → 0.4.27

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 (200) hide show
  1. package/AI.md +279 -57
  2. package/README.md +35 -10
  3. package/catalog.json +2520 -258
  4. package/describe.schema.json +748 -0
  5. package/dist/api-app.d.ts +2 -0
  6. package/dist/api-app.js +35 -0
  7. package/dist/app-urls.js +3 -2
  8. package/dist/bin.js +638 -37
  9. package/dist/capability-guide.d.ts +30 -0
  10. package/dist/capability-guide.js +122 -0
  11. package/dist/catalog.d.ts +16 -2
  12. package/dist/catalog.js +19 -7
  13. package/dist/check/asset-rules.d.ts +6 -0
  14. package/dist/check/asset-rules.js +163 -0
  15. package/dist/check/browser-sources.d.ts +20 -0
  16. package/dist/check/browser-sources.js +89 -0
  17. package/dist/check/console-rules.d.ts +6 -0
  18. package/dist/check/console-rules.js +40 -0
  19. package/dist/check/error-rules.d.ts +21 -0
  20. package/dist/check/error-rules.js +212 -0
  21. package/dist/check/form-rules.d.ts +12 -0
  22. package/dist/check/form-rules.js +69 -6
  23. package/dist/check/formatter-rules.d.ts +2 -0
  24. package/dist/check/formatter-rules.js +71 -0
  25. package/dist/check/i18n-rules.d.ts +27 -0
  26. package/dist/check/i18n-rules.js +353 -0
  27. package/dist/check/index.d.ts +15 -1
  28. package/dist/check/index.js +89 -11
  29. package/dist/check/mail-rules.d.ts +4 -0
  30. package/dist/check/mail-rules.js +72 -0
  31. package/dist/check/module-rules.d.ts +13 -0
  32. package/dist/check/module-rules.js +104 -0
  33. package/dist/check/packages.d.ts +8 -0
  34. package/dist/check/packages.js +16 -0
  35. package/dist/check/pool-rules.d.ts +8 -0
  36. package/dist/check/pool-rules.js +164 -0
  37. package/dist/check/problems.d.ts +6 -1
  38. package/dist/check/problems.js +100 -2
  39. package/dist/check/project-rules.d.ts +23 -3
  40. package/dist/check/project-rules.js +265 -23
  41. package/dist/check/seal-rules.d.ts +6 -0
  42. package/dist/check/seal-rules.js +68 -0
  43. package/dist/check/security-rules.d.ts +10 -0
  44. package/dist/check/security-rules.js +450 -0
  45. package/dist/check/shared-state-rules.d.ts +15 -0
  46. package/dist/check/shared-state-rules.js +117 -0
  47. package/dist/check/source-rules.d.ts +26 -3
  48. package/dist/check/source-rules.js +249 -28
  49. package/dist/check/structure-rules.d.ts +23 -0
  50. package/dist/check/structure-rules.js +502 -0
  51. package/dist/check/timer-rules.d.ts +7 -0
  52. package/dist/check/timer-rules.js +543 -0
  53. package/dist/check/typescript.d.ts +35 -5
  54. package/dist/check/typescript.js +163 -17
  55. package/dist/check/vite-rules.d.ts +2 -0
  56. package/dist/check/vite-rules.js +136 -0
  57. package/dist/check/wait-rules.d.ts +16 -0
  58. package/dist/check/wait-rules.js +337 -0
  59. package/dist/commands.d.ts +7 -0
  60. package/dist/commands.js +381 -36
  61. package/dist/config-diff.d.ts +42 -0
  62. package/dist/config-diff.js +289 -0
  63. package/dist/config-edit.d.ts +35 -2
  64. package/dist/config-edit.js +145 -13
  65. package/dist/config-init.d.ts +15 -0
  66. package/dist/config-init.js +61 -0
  67. package/dist/config-paths.d.ts +22 -0
  68. package/dist/config-paths.js +82 -0
  69. package/dist/config-secrets.d.ts +46 -0
  70. package/dist/config-secrets.js +187 -0
  71. package/dist/config-show.d.ts +82 -0
  72. package/dist/config-show.js +563 -0
  73. package/dist/config-values.d.ts +8 -0
  74. package/dist/config-values.js +68 -0
  75. package/dist/configuration-scaffold.d.ts +42 -0
  76. package/dist/configuration-scaffold.js +258 -4
  77. package/dist/configuration.d.ts +63 -3
  78. package/dist/configuration.js +178 -4
  79. package/dist/deploy-scripts.d.ts +4 -0
  80. package/dist/deploy-scripts.js +361 -0
  81. package/dist/deploy-templates.d.ts +46 -0
  82. package/dist/deploy-templates.js +379 -0
  83. package/dist/deploy.d.ts +46 -0
  84. package/dist/deploy.js +592 -0
  85. package/dist/deployment.d.ts +2 -0
  86. package/dist/deployment.js +111 -0
  87. package/dist/describe-db.d.ts +22 -0
  88. package/dist/describe-db.js +176 -0
  89. package/dist/describe.d.ts +92 -0
  90. package/dist/describe.js +417 -0
  91. package/dist/dev-compose.d.ts +20 -0
  92. package/dist/dev-compose.js +154 -0
  93. package/dist/dev-guard.js +44 -0
  94. package/dist/dev-host.d.ts +13 -0
  95. package/dist/dev-host.js +55 -0
  96. package/dist/dev-infra.d.ts +72 -4
  97. package/dist/dev-infra.js +237 -17
  98. package/dist/dev-watch.d.ts +34 -0
  99. package/dist/dev-watch.js +192 -0
  100. package/dist/diagnostics.d.ts +10 -7
  101. package/dist/diagnostics.js +22 -4
  102. package/dist/drift.d.ts +50 -0
  103. package/dist/drift.js +218 -0
  104. package/dist/env-file.d.ts +25 -1
  105. package/dist/env-file.js +64 -6
  106. package/dist/errors.d.ts +7 -1
  107. package/dist/example-config.d.ts +2 -0
  108. package/dist/example-config.js +3 -0
  109. package/dist/first-admin.d.ts +43 -0
  110. package/dist/first-admin.js +175 -0
  111. package/dist/formatter-ignore.d.ts +37 -0
  112. package/dist/formatter-ignore.js +344 -0
  113. package/dist/generator.d.ts +45 -3
  114. package/dist/generator.js +205 -15
  115. package/dist/guide.d.ts +1 -1
  116. package/dist/guide.js +42 -21
  117. package/dist/install-module.d.ts +20 -2
  118. package/dist/install-module.js +45 -8
  119. package/dist/install-plan.js +5 -3
  120. package/dist/layout.d.ts +12 -1
  121. package/dist/layout.js +19 -6
  122. package/dist/log-writer-control.d.ts +2 -0
  123. package/dist/log-writer-control.js +17 -2
  124. package/dist/log-writer.js +43 -10
  125. package/dist/logging-config.d.ts +8 -3
  126. package/dist/logging-config.js +19 -5
  127. package/dist/manifest.d.ts +29 -1
  128. package/dist/manifest.js +84 -6
  129. package/dist/merge-install-defaults.d.ts +0 -1
  130. package/dist/merge-install-defaults.js +13 -47
  131. package/dist/npm-client.d.ts +40 -4
  132. package/dist/npm-client.js +152 -15
  133. package/dist/npm-paths.js +18 -4
  134. package/dist/openapi.d.ts +27 -0
  135. package/dist/openapi.js +649 -0
  136. package/dist/option-fixes.d.ts +39 -0
  137. package/dist/option-fixes.js +130 -0
  138. package/dist/output.d.ts +1 -1
  139. package/dist/output.js +5 -5
  140. package/dist/plugin-lifecycle.d.ts +34 -2
  141. package/dist/plugin-lifecycle.js +195 -16
  142. package/dist/plugin-options.d.ts +19 -4
  143. package/dist/plugin-options.js +138 -6
  144. package/dist/plugins-command.js +44 -7
  145. package/dist/process-group.d.ts +39 -0
  146. package/dist/process-group.js +132 -0
  147. package/dist/production-doctor.d.ts +3 -0
  148. package/dist/production-doctor.js +176 -0
  149. package/dist/project-guide.d.ts +19 -1
  150. package/dist/project-guide.js +119 -20
  151. package/dist/project-inspector.d.ts +15 -0
  152. package/dist/project-inspector.js +497 -33
  153. package/dist/project-state.d.ts +5 -0
  154. package/dist/project-state.js +8 -0
  155. package/dist/project.d.ts +203 -19
  156. package/dist/project.js +3006 -333
  157. package/dist/recipe-facts.d.ts +127 -0
  158. package/dist/recipe-facts.js +563 -0
  159. package/dist/recipe-login.d.ts +63 -0
  160. package/dist/recipe-login.js +464 -0
  161. package/dist/recipe-mail.d.ts +21 -0
  162. package/dist/recipe-mail.js +214 -0
  163. package/dist/recipe-members.d.ts +35 -0
  164. package/dist/recipe-members.js +178 -0
  165. package/dist/recipe-record.d.ts +82 -7
  166. package/dist/recipe-record.js +226 -22
  167. package/dist/recipe-reset.d.ts +2 -0
  168. package/dist/recipe-reset.js +117 -0
  169. package/dist/recipe-site.d.ts +48 -0
  170. package/dist/recipe-site.js +396 -0
  171. package/dist/recipe-step.d.ts +42 -0
  172. package/dist/recipe-step.js +89 -0
  173. package/dist/recipe.d.ts +56 -20
  174. package/dist/recipe.js +703 -131
  175. package/dist/registry.d.ts +98 -2
  176. package/dist/registry.js +171 -5
  177. package/dist/reloadable.d.ts +35 -0
  178. package/dist/reloadable.js +257 -0
  179. package/dist/remote-verify.d.ts +31 -0
  180. package/dist/remote-verify.js +204 -0
  181. package/dist/server.d.ts +43 -4
  182. package/dist/server.js +601 -52
  183. package/dist/settings.d.ts +48 -0
  184. package/dist/settings.js +168 -0
  185. package/dist/site-auth.d.ts +39 -0
  186. package/dist/site-auth.js +195 -0
  187. package/dist/template-fixes.d.ts +26 -0
  188. package/dist/template-fixes.js +285 -0
  189. package/dist/test-reporter.d.ts +49 -0
  190. package/dist/test-reporter.js +190 -0
  191. package/dist/transaction.d.ts +47 -0
  192. package/dist/transaction.js +241 -7
  193. package/dist/verify-tests.d.ts +98 -0
  194. package/dist/verify-tests.js +449 -0
  195. package/dist/verify.d.ts +103 -3
  196. package/dist/verify.js +272 -21
  197. package/dist/view-choice.d.ts +58 -0
  198. package/dist/view-choice.js +131 -0
  199. package/dist/vite-watch.js +4 -2
  200. package/package.json +8 -6
@@ -0,0 +1,379 @@
1
+ // The texts `dokebi deploy init` and `dokebi deploy proxy` write (D-0184): one instance of an
2
+ // app on a Linux server, laid out so staging and production on the same server never share a
3
+ // port, unit, user, settings, data, log, backup or database name.
4
+ export function layout(app, instance, port) {
5
+ return {
6
+ app,
7
+ instance,
8
+ port,
9
+ unit: `${app}-${instance}`,
10
+ settings: `/etc/${app}/${instance}/dokebi.yaml`,
11
+ secrets: `/etc/${app}/${instance}/secrets.yaml`,
12
+ code: `/srv/${app}/${instance}`,
13
+ data: `/var/lib/${app}/${instance}`,
14
+ logs: `/var/log/${app}/${instance}`,
15
+ backups: `/var/backups/${app}/${instance}`,
16
+ database: `${app}_${instance}`.replaceAll('-', '_'),
17
+ };
18
+ }
19
+ export function systemdUnit(at) {
20
+ return `[Unit]
21
+ Description=${at.app} (${at.instance})
22
+ Documentation=file://${at.code}/current/DEPLOY.md
23
+ After=network-online.target
24
+ Wants=network-online.target
25
+ # Start after the services this app connects to, e.g.:
26
+ # After=network-online.target postgresql.service redis-server.service
27
+
28
+ [Service]
29
+ Type=exec
30
+ User=${at.unit}
31
+ Group=${at.unit}
32
+ WorkingDirectory=${at.code}/current
33
+ Environment=NODE_ENV=production
34
+ Environment=DOKEBI_CONFIG=${at.settings}
35
+ Environment=DOKEBI_SECRETS=${at.secrets}
36
+ # The node of the server (command -v node); Node.js 24 or later.
37
+ ExecStart=/usr/bin/node src/main.ts
38
+ # The app drains on SIGTERM; keep TimeoutStopSec above its drain and cleanup budget.
39
+ KillSignal=SIGTERM
40
+ TimeoutStopSec=60
41
+ Restart=on-failure
42
+ RestartSec=5
43
+ # Writable: ${at.data} (StateDirectory: storage and other app files) and
44
+ # ${at.logs} (LogsDirectory: file log targets). Records on stdout go to the journal:
45
+ # journalctl -u ${at.unit}
46
+ StateDirectory=${at.app}/${at.instance}
47
+ StateDirectoryMode=0700
48
+ LogsDirectory=${at.app}/${at.instance}
49
+ LogsDirectoryMode=0750
50
+ SyslogIdentifier=${at.unit}
51
+ UMask=0077
52
+ NoNewPrivileges=yes
53
+ ProtectSystem=strict
54
+ ProtectHome=yes
55
+ PrivateTmp=yes
56
+ PrivateDevices=yes
57
+ ProtectKernelTunables=yes
58
+ ProtectKernelModules=yes
59
+ ProtectControlGroups=yes
60
+ RestrictSUIDSGID=yes
61
+ LockPersonality=yes
62
+
63
+ [Install]
64
+ WantedBy=multi-user.target
65
+ `;
66
+ }
67
+ export function backupService(at) {
68
+ return `[Unit]
69
+ Description=${at.app} (${at.instance}) database backup
70
+ Documentation=file://${at.code}/current/DEPLOY.md
71
+
72
+ [Service]
73
+ Type=oneshot
74
+ User=${at.unit}
75
+ Group=${at.unit}
76
+ WorkingDirectory=${at.code}/current
77
+ Environment=DOKEBI_CONFIG=${at.settings}
78
+ Environment=DOKEBI_SECRETS=${at.secrets}
79
+ Environment=DOKEBI_BACKUP_DIR=${at.backups}
80
+ # Days, weeks, months of backups to keep (the newest of each).
81
+ Environment=DOKEBI_BACKUP_KEEP=7,4,6
82
+ ExecStart=/usr/bin/node deploy/backup.mjs
83
+ SyslogIdentifier=${at.unit}-backup
84
+ UMask=0077
85
+ NoNewPrivileges=yes
86
+ ProtectSystem=strict
87
+ ProtectHome=yes
88
+ PrivateTmp=yes
89
+ ReadWritePaths=${at.backups}
90
+ `;
91
+ }
92
+ export function backupTimer(at) {
93
+ return `[Unit]
94
+ Description=Daily database backup of ${at.app} (${at.instance})
95
+
96
+ [Timer]
97
+ OnCalendar=*-*-* 03:30:00
98
+ RandomizedDelaySec=30min
99
+ Persistent=true
100
+
101
+ [Install]
102
+ WantedBy=timers.target
103
+ `;
104
+ }
105
+ export function composeFile(at) {
106
+ return `# ${at.app} (${at.instance}): docker compose -f deploy/compose/${at.instance}.yaml up -d --build
107
+ # The container uses the host network, as the systemd unit would: the app listens on
108
+ # 127.0.0.1:${at.port} (settings) behind the reverse proxy of the host, and reaches databases
109
+ # on the host at 127.0.0.1. Settings and secrets are mounted read-only from /etc.
110
+ name: ${at.unit}
111
+ services:
112
+ app:
113
+ build:
114
+ context: ../..
115
+ image: ${at.app}:${at.instance}
116
+ init: true
117
+ network_mode: host
118
+ # The owner of ${at.secrets} (mode 0600); create that user on the host.
119
+ user: "\${DOKEBI_UID:-1000}:\${DOKEBI_GID:-1000}"
120
+ environment:
121
+ NODE_ENV: production
122
+ DOKEBI_CONFIG: ${at.settings}
123
+ DOKEBI_SECRETS: ${at.secrets}
124
+ volumes:
125
+ - type: bind
126
+ source: /etc/${at.app}/${at.instance}
127
+ target: /etc/${at.app}/${at.instance}
128
+ read_only: true
129
+ bind:
130
+ create_host_path: false
131
+ - type: bind
132
+ source: ${at.data}
133
+ target: ${at.data}
134
+ bind:
135
+ create_host_path: false
136
+ - type: bind
137
+ source: ${at.logs}
138
+ target: ${at.logs}
139
+ bind:
140
+ create_host_path: false
141
+ stop_grace_period: 60s
142
+ restart: unless-stopped
143
+ logging:
144
+ driver: json-file
145
+ options:
146
+ max-size: 10m
147
+ max-file: "5"
148
+ `;
149
+ }
150
+ /** DEPLOY.md: the instance table, setup, a release, rollback, backups (D-0184 §5, §6). */
151
+ export function deployGuide(input) {
152
+ const { app, target } = input;
153
+ const systemd = target === 'systemd';
154
+ const migrate = input.plugins.includes('dokebi-migrate');
155
+ const db = input.plugins.includes('dokebi-db');
156
+ const storage = input.plugins.includes('dokebi-storage');
157
+ const first = input.instances[0];
158
+ const rows = input.instances
159
+ .map((at) => `| ${at.instance} | \`${at.unit}\` | 127.0.0.1:${at.port} | \`${at.settings}\` | \`${at.secrets}\` | \`${at.code}/current\` | \`${at.data}\` | \`${at.logs}\` | \`${at.backups}\` | \`${at.database}\` |`)
160
+ .join('\n');
161
+ const env = (at) => `DOKEBI_CONFIG=${at.settings} DOKEBI_SECRETS=${at.secrets}`;
162
+ return `# Deploying ${app}
163
+
164
+ Written by \`npx dokebi deploy init\` (target: ${target}). Edit it freely: a later \`deploy init\`
165
+ rewrites this file only while it is unchanged (DEPLOY_FILE_KEPT otherwise). Each instance is
166
+ separate on the server: its own system user, port, settings, secrets, code, data, logs, backups
167
+ and database.
168
+
169
+ | Instance | ${systemd ? 'Unit and user' : 'Compose project'} | Listens on | Settings | Secrets | Code | Data | Logs | Backups | Database |
170
+ |---|---|---|---|---|---|---|---|---|---|
171
+ ${rows}
172
+
173
+ The repository keeps, per instance, \`deploy/<instance>/dokebi.example.yaml\` (the settings
174
+ without secrets: copy it to the settings path) and \`deploy/<instance>/secrets.example.yaml\`
175
+ (the secret paths with placeholders: the values live only in your secret store). The app refuses
176
+ to start while a path of the settings' \`secrets:\` list is empty (CONFIG_SECRETS_MISSING), the
177
+ secrets file is readable by others (CONFIG_SECRETS_UNSAFE) or both files set the same option
178
+ (CONFIG_SECRETS_CONFLICT).${input.secrets.length ? `\n\nSecrets of this app: ${input.secrets.map((path) => `\`${path}\``).join(', ')}.` : ''}
179
+
180
+ ## 1. Prepare the server (once per instance)
181
+
182
+ Node.js 24 or later${db ? ', the database server and its client tools (pg_dump and pg_restore of the same major version, or the MongoDB Database Tools)' : ''}${systemd ? '' : ', Docker with Compose'} and a reverse proxy (\`npx dokebi deploy proxy --caddy|--nginx --domain <host> --instance <name>\` writes its configuration).
183
+ For each instance (here ${first.instance}):
184
+
185
+ \`\`\`sh
186
+ sudo useradd --system --home ${first.code} --shell /usr/sbin/nologin ${first.unit}
187
+ sudo install -d -o ${first.unit} -g ${first.unit} -m 0750 ${first.code} ${first.code}/releases
188
+ sudo install -d -o root -g ${first.unit} -m 0750 /etc/${app}/${first.instance}
189
+ sudo install -o ${first.unit} -g ${first.unit} -m 0600 deploy/${first.instance}/dokebi.example.yaml ${first.settings}
190
+ # The secrets file comes from your secret store, never from the repository:
191
+ sudo install -o ${first.unit} -g ${first.unit} -m 0600 /dev/null ${first.secrets} # then write it (secrets.example.yaml lists the paths)
192
+ ${systemd ? '' : `sudo install -d -o ${first.unit} -g ${first.unit} -m 0700 ${first.data}\nsudo install -d -o ${first.unit} -g ${first.unit} -m 0750 ${first.logs}\n`}sudo install -d -o ${first.unit} -g ${first.unit} -m 0700 ${first.backups}
193
+ \`\`\`
194
+
195
+ Fill the settings file: the placeholders left in it (\`<…>\`), \`dokebi-app.domains\` with the
196
+ public names, and \`dokebi-web.trustProxy\` with the addresses of the reverse proxy (127.0.0.1
197
+ and ::1 when it runs on the same server). ${db ? `Create the database \`${first.database}\` and its own user for this instance; its URL goes in the secrets file.` : ''}
198
+ ${systemd ? `\nInstall the units: \`sudo cp deploy/systemd/${first.unit}.service deploy/systemd/${first.unit}-backup.service deploy/systemd/${first.unit}-backup.timer /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable ${first.unit} ${first.unit}-backup.timer\`.` : `\nThe compose file runs the container as \`DOKEBI_UID\`:\`DOKEBI_GID\`: set them to the uid and gid of \`${first.unit}\`, the owner of the secrets file.`}
199
+
200
+ Before the first start, check the instance with the settings it will read:
201
+
202
+ \`\`\`sh
203
+ cd ${first.code}/current && sudo -u ${first.unit} env NODE_ENV=production ${env(first)} npx -y @dokebi/cli doctor --production
204
+ sudo -u ${first.unit} env ${env(first)} npx -y @dokebi/cli config secrets
205
+ \`\`\`
206
+
207
+ ## 2. Release (every deploy)
208
+
209
+ Run as the instance's user, in this order; stop at the first step that fails.
210
+
211
+ 1. **Build a new release folder** (never change the running one):
212
+ \`git clone --depth 1 --branch <tag> <repository> ${first.code}/releases/<id>\`, then in it
213
+ \`npm ci\`, \`npm run build --if-present\` and \`npm prune --omit=dev\`.${migrate
214
+ ? `
215
+ 2. **Migrate before the switch:** \`${env(first)} npx dokebi-migrate up --phase pre-deploy --json\`
216
+ in the new folder. Exit code 0 means applied (or nothing to do); 1 means stop: read \`error.code\`.
217
+ \`LOCK_TIMEOUT\` means another run holds the migration lock (wait, never delete it);
218
+ \`MIGRATION_TIMEOUT\` a statement waited too long (the file is not recorded).
219
+ Pre-deploy files only add (a nullable column, a table, an index), so the running release keeps
220
+ working with them. With \`options.dokebi-migrate.onPending: fail\` the new release refuses to
221
+ start while a pre-deploy file is pending.`
222
+ : ''}
223
+ ${migrate ? '3' : '2'}. **Switch:** \`ln -sfn releases/<id> ${first.code}/current\` and ${systemd ? `\`sudo systemctl restart ${first.unit}\`` : `\`docker compose -f deploy/compose/${first.instance}.yaml up -d --build\``}.
224
+ The old process drains on SIGTERM.
225
+ ${migrate ? '4' : '3'}. **Check:** \`npx -y @dokebi/cli verify --remote https://<domain> --json\` (TLS, health, security
226
+ headers; exit 1 on a failure) and \`${systemd ? `journalctl -u ${first.unit} -n 100` : `docker compose -f deploy/compose/${first.instance}.yaml logs --tail 100 app`}\`.${migrate
227
+ ? `
228
+ 5. **Migrate after the switch:** \`${env(first)} npx dokebi-migrate up --json\` applies the
229
+ post-deploy files (backfills, constraints, dropping what the old release used).`
230
+ : ''}
231
+ ${migrate ? '6' : '4'}. Keep the last 5 release folders; remove older ones.
232
+
233
+ ## 3. Rollback
234
+
235
+ Point \`current\` back to the previous release and restart ${systemd ? `(\`sudo systemctl restart ${first.unit}\`)` : '(`docker compose ... up -d --build`)'}.${migrate
236
+ ? ` The
237
+ pre-deploy migrations of the new release stay: they only added, so the previous release works
238
+ with them. Run \`npx dokebi-migrate down --to <name>\` only when a migration itself must go, after
239
+ a backup and knowing what its \`down\` deletes; post-deploy files may have dropped data the
240
+ previous release needs, so restore from a backup instead when one ran.`
241
+ : ''}
242
+
243
+ ## 4. Backups and restore drills
244
+ ${db
245
+ ? `
246
+ ${systemd ? `\`${first.unit}-backup.timer\` runs \`deploy/backup.mjs\` every day at 03:30 (with up to 30 minutes of random delay)` : `Run \`deploy/backup.mjs\` every day from the host's scheduler (it needs Node.js and the database client tools there)`}:
247
+ every \`dokebi-db\` connection with a postgres or mongodb URL is dumped with \`pg_dump -Fc\` or
248
+ \`mongodump --archive --gzip\` into \`${first.backups}/<connection>/<UTC time>.*\` (mode 0600). It
249
+ keeps the newest backup of each of the last 7 days, 4 weeks and 6 months
250
+ (\`DOKEBI_BACKUP_KEEP=7,4,6\`) and prints one JSON line (\`ok\`, the files, their sizes). Passwords
251
+ never reach a command line.
252
+
253
+ - **Copy them off the server.** A backup on the same disk is lost with the disk: sync
254
+ \`/var/backups/${app}\` to another place (object storage, another server) after the run, encrypted.
255
+ - **Restore drill, every month** (and after a database upgrade): create an empty scratch database
256
+ (e.g. \`${first.database}_drill\`), then
257
+ \`${env(first)} DOKEBI_BACKUP_DIR=${first.backups} DOKEBI_RESTORE_URL=<scratch database URL> node deploy/restore-drill.mjs\`.
258
+ It restores the newest backup there, refuses the live database, compares tables (or
259
+ collections) and the migration history with the live one, and prints one JSON line. Drop the
260
+ scratch database afterwards.
261
+ - **A real restore:** stop the instance, restore the chosen backup into a new database
262
+ (\`pg_restore --no-owner --dbname=<new database> <file>\` or
263
+ \`mongorestore --archive=<file> --gzip --nsFrom=<old>.* --nsTo=<new>.*\`), point the URL in the
264
+ secrets file at it, run \`npx dokebi-migrate status --json\` (it must list no missing file), then
265
+ start.
266
+ - MySQL and MariaDB connections are not dumped by the script: use
267
+ \`mysqldump --single-transaction --routines --triggers\` from your scheduler.
268
+ `
269
+ : `
270
+ This app has no @dokebi/db connection. Back up what it keeps on the server: ${first.data} and
271
+ the settings (the secrets stay in your secret store).
272
+ `}${storage ? `- **Files:** \`${first.data}\` holds the uploaded files of @dokebi/storage (\`dokebi-storage.directory\`); back that folder up with your file backup, at the same time as the database.\n` : ''}- **Secrets:** the secret store is the original of \`secrets.yaml\`; nothing to back up on the server.
273
+
274
+ ## 5. Logs
275
+
276
+ Records go to stdout as JSON lines: ${systemd ? `\`journalctl -u ${first.unit}\`` : `\`docker compose -f deploy/compose/${first.instance}.yaml logs app\``}.
277
+ A file target of @dokebi/logger writes under \`${first.logs}\`.
278
+ `;
279
+ }
280
+ export function caddyfile(input) {
281
+ const { at } = input;
282
+ return `# ${at.app} (${at.instance}): Caddy terminates TLS (automatic certificates) for these names and
283
+ # passes requests to the app on 127.0.0.1:${at.port}. Caddy sets X-Forwarded-For, -Proto and
284
+ # -Host; the app trusts them only from dokebi-web.trustProxy (127.0.0.1 and ::1 here).
285
+ # WebSocket upgrades pass through; responses are not buffered (SSE streams flush at once).
286
+ # The app sets Cache-Control (immutable for built assets, the rules of @dokebi/static) and
287
+ # compresses itself, so Caddy neither caches nor compresses.
288
+ ${input.domains.join(', ')} {
289
+ request_body {
290
+ max_size ${input.maxBodyBytes}
291
+ }
292
+ reverse_proxy 127.0.0.1:${at.port} {
293
+ flush_interval -1
294
+ transport http {
295
+ read_timeout ${input.readTimeoutSeconds}s
296
+ }
297
+ }
298
+ # ${input.live} and ${input.ready} answer as the app answers them (never cached):
299
+ # what an uptime monitor and dokebi verify --remote read.
300
+ header ${input.live} Cache-Control no-store
301
+ header ${input.ready} Cache-Control no-store
302
+ log {
303
+ output file /var/log/caddy/${at.unit}.access.log
304
+ }
305
+ }
306
+ `;
307
+ }
308
+ export function nginxConf(input) {
309
+ const { at } = input;
310
+ const names = input.domains.join(' ');
311
+ const main = input.domains[0];
312
+ const upstream = at.unit.replaceAll('-', '_');
313
+ return `# ${at.app} (${at.instance}): nginx terminates TLS for these names and passes requests to the
314
+ # app on 127.0.0.1:${at.port}. The app trusts X-Forwarded-* only from dokebi-web.trustProxy
315
+ # (127.0.0.1 and ::1 here). Certificates: certbot certonly --webroot -w /var/www/letsencrypt -d ${main}
316
+ # Install: copy to /etc/nginx/conf.d/${at.unit}.conf, then nginx -t && systemctl reload nginx.
317
+
318
+ ${input.streams
319
+ ? `# WebSocket: an Upgrade request keeps its Connection header.
320
+ map $http_upgrade $${upstream}_connection {
321
+ default upgrade;
322
+ '' close;
323
+ }
324
+
325
+ `
326
+ : ''}upstream ${upstream} {
327
+ server 127.0.0.1:${at.port};
328
+ keepalive 16;
329
+ }
330
+
331
+ server {
332
+ listen 80;
333
+ listen [::]:80;
334
+ server_name ${names};
335
+ location /.well-known/acme-challenge/ { root /var/www/letsencrypt; }
336
+ location / { return 308 https://$host$request_uri; }
337
+ }
338
+
339
+ server {
340
+ # nginx 1.25.1 and later prefer "http2 on;" (this form still works there, with a warning).
341
+ listen 443 ssl http2;
342
+ listen [::]:443 ssl http2;
343
+ server_name ${names};
344
+
345
+ ssl_certificate /etc/letsencrypt/live/${main}/fullchain.pem;
346
+ ssl_certificate_key /etc/letsencrypt/live/${main}/privkey.pem;
347
+ ssl_protocols TLSv1.2 TLSv1.3;
348
+ ssl_session_cache shared:${upstream}_tls:10m;
349
+
350
+ # The app takes bodies up to this size (dokebi-web maxBodyBytes, uploads of @dokebi/storage).
351
+ client_max_body_size ${input.maxBodyBytes};
352
+ # The app compresses and sets Cache-Control (immutable built assets, @dokebi/static rules):
353
+ # nginx neither compresses again nor caches.
354
+ gzip off;
355
+
356
+ proxy_http_version 1.1;
357
+ proxy_set_header Host $host;
358
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
359
+ proxy_set_header X-Forwarded-Proto $scheme;
360
+ proxy_set_header X-Forwarded-Host $host;
361
+ proxy_set_header Upgrade $http_upgrade;
362
+ proxy_set_header Connection ${input.streams ? `$${upstream}_connection` : '""'};
363
+ # SSE answers carry X-Accel-Buffering: no, so nginx passes each event at once.
364
+ proxy_read_timeout ${input.readTimeoutSeconds}s;
365
+ proxy_send_timeout ${input.readTimeoutSeconds}s;
366
+
367
+ # Health routes as the app answers them, never cached (monitoring, dokebi verify --remote).
368
+ location = ${input.live} { proxy_pass http://${upstream}; add_header Cache-Control no-store always; }
369
+ location = ${input.ready} { proxy_pass http://${upstream}; add_header Cache-Control no-store always; }
370
+
371
+ location / {
372
+ proxy_pass http://${upstream};
373
+ }
374
+
375
+ access_log /var/log/nginx/${at.unit}.access.log;
376
+ error_log /var/log/nginx/${at.unit}.error.log;
377
+ }
378
+ `;
379
+ }
@@ -0,0 +1,46 @@
1
+ import { type InstanceLayout } from './deploy-templates.js';
2
+ export interface DeployWarning {
3
+ readonly code: string;
4
+ readonly path?: string;
5
+ readonly hint: string;
6
+ }
7
+ export interface DeployResult {
8
+ readonly app: string;
9
+ readonly changed: string[];
10
+ readonly unchanged: string[];
11
+ /** Files you edited since deploy init wrote them: left as they are (DEPLOY_FILE_KEPT). */
12
+ readonly kept: string[];
13
+ readonly warnings?: DeployWarning[];
14
+ readonly nextSteps?: ReadonlyArray<{
15
+ command: string;
16
+ why: string;
17
+ }>;
18
+ }
19
+ export interface DeployInitResult extends DeployResult {
20
+ readonly target: 'systemd' | 'compose';
21
+ readonly instances: ReadonlyArray<InstanceLayout>;
22
+ /** The option paths the settings samples leave to the secrets file (by name). */
23
+ readonly secrets: readonly string[];
24
+ }
25
+ export interface DeployProxyResult extends DeployResult {
26
+ readonly proxy: 'caddy' | 'nginx';
27
+ readonly instance: string;
28
+ readonly file: string;
29
+ readonly domains: readonly string[];
30
+ }
31
+ export interface DeployInitOptions {
32
+ readonly target: 'systemd' | 'compose';
33
+ readonly instances: readonly string[];
34
+ readonly port?: number;
35
+ readonly dryRun?: boolean;
36
+ }
37
+ /** `dokebi deploy init` (D-0184 §2). */
38
+ export declare function deployInit(input: string, options: DeployInitOptions): Promise<DeployInitResult>;
39
+ export interface DeployProxyOptions {
40
+ readonly proxy: 'caddy' | 'nginx';
41
+ readonly domains: readonly string[];
42
+ readonly instance: string;
43
+ readonly dryRun?: boolean;
44
+ }
45
+ /** `dokebi deploy proxy` (D-0184 §3). */
46
+ export declare function deployProxy(input: string, options: DeployProxyOptions): Promise<DeployProxyResult>;