@yannelli/zoomies 0.0.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.
Files changed (160) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/dist/cli/client.d.ts +77 -0
  4. package/dist/cli/client.d.ts.map +1 -0
  5. package/dist/cli/client.js +300 -0
  6. package/dist/cli/client.js.map +1 -0
  7. package/dist/cli/commands/certs.d.ts +11 -0
  8. package/dist/cli/commands/certs.d.ts.map +1 -0
  9. package/dist/cli/commands/certs.js +110 -0
  10. package/dist/cli/commands/certs.js.map +1 -0
  11. package/dist/cli/commands/flags.d.ts +29 -0
  12. package/dist/cli/commands/flags.d.ts.map +1 -0
  13. package/dist/cli/commands/flags.js +104 -0
  14. package/dist/cli/commands/flags.js.map +1 -0
  15. package/dist/cli/commands/reload.d.ts +11 -0
  16. package/dist/cli/commands/reload.d.ts.map +1 -0
  17. package/dist/cli/commands/reload.js +35 -0
  18. package/dist/cli/commands/reload.js.map +1 -0
  19. package/dist/cli/commands/sites.d.ts +11 -0
  20. package/dist/cli/commands/sites.d.ts.map +1 -0
  21. package/dist/cli/commands/sites.js +221 -0
  22. package/dist/cli/commands/sites.js.map +1 -0
  23. package/dist/cli/commands/status.d.ts +10 -0
  24. package/dist/cli/commands/status.d.ts.map +1 -0
  25. package/dist/cli/commands/status.js +41 -0
  26. package/dist/cli/commands/status.js.map +1 -0
  27. package/dist/cli/commands/upstreams.d.ts +21 -0
  28. package/dist/cli/commands/upstreams.d.ts.map +1 -0
  29. package/dist/cli/commands/upstreams.js +248 -0
  30. package/dist/cli/commands/upstreams.js.map +1 -0
  31. package/dist/cli/dispatcher.d.ts +45 -0
  32. package/dist/cli/dispatcher.d.ts.map +1 -0
  33. package/dist/cli/dispatcher.js +192 -0
  34. package/dist/cli/dispatcher.js.map +1 -0
  35. package/dist/index.d.ts +12 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +42 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/server/api/db-context.d.ts +50 -0
  40. package/dist/server/api/db-context.d.ts.map +1 -0
  41. package/dist/server/api/db-context.js +76 -0
  42. package/dist/server/api/db-context.js.map +1 -0
  43. package/dist/server/api/error-mapping.d.ts +19 -0
  44. package/dist/server/api/error-mapping.d.ts.map +1 -0
  45. package/dist/server/api/error-mapping.js +56 -0
  46. package/dist/server/api/error-mapping.js.map +1 -0
  47. package/dist/server/api/handlers/site-cert.d.ts +49 -0
  48. package/dist/server/api/handlers/site-cert.d.ts.map +1 -0
  49. package/dist/server/api/handlers/site-cert.js +54 -0
  50. package/dist/server/api/handlers/site-cert.js.map +1 -0
  51. package/dist/server/api/handlers/sites.d.ts +67 -0
  52. package/dist/server/api/handlers/sites.d.ts.map +1 -0
  53. package/dist/server/api/handlers/sites.js +78 -0
  54. package/dist/server/api/handlers/sites.js.map +1 -0
  55. package/dist/server/api/handlers/upstreams.d.ts +64 -0
  56. package/dist/server/api/handlers/upstreams.d.ts.map +1 -0
  57. package/dist/server/api/handlers/upstreams.js +97 -0
  58. package/dist/server/api/handlers/upstreams.js.map +1 -0
  59. package/dist/server/auth/require-token.d.ts +24 -0
  60. package/dist/server/auth/require-token.d.ts.map +1 -0
  61. package/dist/server/auth/require-token.js +98 -0
  62. package/dist/server/auth/require-token.js.map +1 -0
  63. package/dist/server/certs/acme-account.d.ts +37 -0
  64. package/dist/server/certs/acme-account.d.ts.map +1 -0
  65. package/dist/server/certs/acme-account.js +49 -0
  66. package/dist/server/certs/acme-account.js.map +1 -0
  67. package/dist/server/certs/challenge-store.d.ts +53 -0
  68. package/dist/server/certs/challenge-store.d.ts.map +1 -0
  69. package/dist/server/certs/challenge-store.js +66 -0
  70. package/dist/server/certs/challenge-store.js.map +1 -0
  71. package/dist/server/certs/issue.d.ts +106 -0
  72. package/dist/server/certs/issue.d.ts.map +1 -0
  73. package/dist/server/certs/issue.js +107 -0
  74. package/dist/server/certs/issue.js.map +1 -0
  75. package/dist/server/certs/renew.d.ts +34 -0
  76. package/dist/server/certs/renew.d.ts.map +1 -0
  77. package/dist/server/certs/renew.js +36 -0
  78. package/dist/server/certs/renew.js.map +1 -0
  79. package/dist/server/certs/scheduler.d.ts +68 -0
  80. package/dist/server/certs/scheduler.d.ts.map +1 -0
  81. package/dist/server/certs/scheduler.js +76 -0
  82. package/dist/server/certs/scheduler.js.map +1 -0
  83. package/dist/server/db/connection.d.ts +10 -0
  84. package/dist/server/db/connection.d.ts.map +1 -0
  85. package/dist/server/db/connection.js +16 -0
  86. package/dist/server/db/connection.js.map +1 -0
  87. package/dist/server/db/migrate.d.ts +12 -0
  88. package/dist/server/db/migrate.d.ts.map +1 -0
  89. package/dist/server/db/migrate.js +37 -0
  90. package/dist/server/db/migrate.js.map +1 -0
  91. package/dist/server/db/migrations/0001_init.sql +42 -0
  92. package/dist/server/domain/cert.d.ts +17 -0
  93. package/dist/server/domain/cert.d.ts.map +1 -0
  94. package/dist/server/domain/cert.js +22 -0
  95. package/dist/server/domain/cert.js.map +1 -0
  96. package/dist/server/domain/errors.d.ts +36 -0
  97. package/dist/server/domain/errors.d.ts.map +1 -0
  98. package/dist/server/domain/errors.js +37 -0
  99. package/dist/server/domain/errors.js.map +1 -0
  100. package/dist/server/domain/site.d.ts +15 -0
  101. package/dist/server/domain/site.d.ts.map +1 -0
  102. package/dist/server/domain/site.js +25 -0
  103. package/dist/server/domain/site.js.map +1 -0
  104. package/dist/server/domain/upstream.d.ts +25 -0
  105. package/dist/server/domain/upstream.d.ts.map +1 -0
  106. package/dist/server/domain/upstream.js +24 -0
  107. package/dist/server/domain/upstream.js.map +1 -0
  108. package/dist/server/index.d.ts +2 -0
  109. package/dist/server/index.d.ts.map +1 -0
  110. package/dist/server/index.js +4 -0
  111. package/dist/server/index.js.map +1 -0
  112. package/dist/server/reload/atomic-write.d.ts +44 -0
  113. package/dist/server/reload/atomic-write.d.ts.map +1 -0
  114. package/dist/server/reload/atomic-write.js +151 -0
  115. package/dist/server/reload/atomic-write.js.map +1 -0
  116. package/dist/server/reload/health-probe.d.ts +62 -0
  117. package/dist/server/reload/health-probe.d.ts.map +1 -0
  118. package/dist/server/reload/health-probe.js +105 -0
  119. package/dist/server/reload/health-probe.js.map +1 -0
  120. package/dist/server/reload/reload.d.ts +118 -0
  121. package/dist/server/reload/reload.d.ts.map +1 -0
  122. package/dist/server/reload/reload.js +232 -0
  123. package/dist/server/reload/reload.js.map +1 -0
  124. package/dist/server/renderer/render-bundle.d.ts +18 -0
  125. package/dist/server/renderer/render-bundle.d.ts.map +1 -0
  126. package/dist/server/renderer/render-bundle.js +32 -0
  127. package/dist/server/renderer/render-bundle.js.map +1 -0
  128. package/dist/server/renderer/render-site.d.ts +5 -0
  129. package/dist/server/renderer/render-site.d.ts.map +1 -0
  130. package/dist/server/renderer/render-site.js +144 -0
  131. package/dist/server/renderer/render-site.js.map +1 -0
  132. package/dist/server/repositories/cert-repository.d.ts +19 -0
  133. package/dist/server/repositories/cert-repository.d.ts.map +1 -0
  134. package/dist/server/repositories/cert-repository.js +112 -0
  135. package/dist/server/repositories/cert-repository.js.map +1 -0
  136. package/dist/server/repositories/site-repository.d.ts +17 -0
  137. package/dist/server/repositories/site-repository.d.ts.map +1 -0
  138. package/dist/server/repositories/site-repository.js +122 -0
  139. package/dist/server/repositories/site-repository.js.map +1 -0
  140. package/dist/server/repositories/upstream-repository.d.ts +22 -0
  141. package/dist/server/repositories/upstream-repository.d.ts.map +1 -0
  142. package/dist/server/repositories/upstream-repository.js +142 -0
  143. package/dist/server/repositories/upstream-repository.js.map +1 -0
  144. package/dist/server/validator/nginx-binary.d.ts +9 -0
  145. package/dist/server/validator/nginx-binary.d.ts.map +1 -0
  146. package/dist/server/validator/nginx-binary.js +11 -0
  147. package/dist/server/validator/nginx-binary.js.map +1 -0
  148. package/dist/server/validator/validate.d.ts +29 -0
  149. package/dist/server/validator/validate.d.ts.map +1 -0
  150. package/dist/server/validator/validate.js +69 -0
  151. package/dist/server/validator/validate.js.map +1 -0
  152. package/dist/server/worker/main.d.ts +43 -0
  153. package/dist/server/worker/main.d.ts.map +1 -0
  154. package/dist/server/worker/main.js +181 -0
  155. package/dist/server/worker/main.js.map +1 -0
  156. package/dist/version.d.ts +2 -0
  157. package/dist/version.d.ts.map +1 -0
  158. package/dist/version.js +2 -0
  159. package/dist/version.js.map +1 -0
  160. package/package.json +84 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Reload orchestrator — the single chokepoint that turns a freshly rendered
3
+ * bundle into a live NGINX configuration.
4
+ *
5
+ * The orchestrator is the only place in the codebase allowed to mutate the
6
+ * managed sites directory or send a signal to NGINX. It is deliberately
7
+ * dependency-injected: every external side effect (validate, write, delete,
8
+ * reload, probe, listdir) is exposed via {@link ReloadDeps} so tests can drive
9
+ * each failure path without touching the real filesystem or nginx binary.
10
+ *
11
+ * Flow (each step short-circuits on failure with a labeled {@link ApplyResult}):
12
+ *
13
+ * 1. Validate the candidate via `nginx -t` — pure check, no disk writes.
14
+ * 2. Diff disk against `rendered` to compute the writes + orphan deletes.
15
+ * 3. Apply writes/deletes, accumulating rollback handles in order.
16
+ * 4. SIGHUP NGINX. If it refuses, roll back everything and reload again.
17
+ * 5. Probe the configured health URL. If it fails, roll back + reload again.
18
+ * 6. Success: discard rollback handles.
19
+ *
20
+ * On any step that triggers a rollback we re-run the reload so NGINX matches
21
+ * the disk we just restored. If that second reload also fails, we log both
22
+ * stderrs and still return the original failure — operator intervention is
23
+ * needed anyway, and recursing would hide the real cause.
24
+ */
25
+ import { execa } from 'execa';
26
+ import { readdir } from 'node:fs/promises';
27
+ import { basename, resolve } from 'node:path';
28
+ import { getNginxBinary } from '../validator/nginx-binary.js';
29
+ import { validateConfig } from '../validator/validate.js';
30
+ import { deleteAtomic as deleteAtomicImpl, writeAtomic as writeAtomicImpl, } from './atomic-write.js';
31
+ import { probeHealth } from './health-probe.js';
32
+ /** Default argv for `nginx -s reload`. Kept as a constant so tests can compare by reference. */
33
+ const DEFAULT_RELOAD_ARGS = ['-s', 'reload'];
34
+ /**
35
+ * Concatenate the rendered per-site fragments into a single candidate the
36
+ * validator can `nginx -t`. Blank-line separated so NGINX parses cleanly even
37
+ * when individual fragments end without trailing newlines.
38
+ */
39
+ function joinRendered(rendered) {
40
+ return Array.from(rendered.values()).join('\n\n');
41
+ }
42
+ /**
43
+ * Default `listManagedFiles` — read every `*.conf` directly inside `sitesDir`
44
+ * and return absolute paths. ENOENT (the dir hasn't been created yet) is
45
+ * treated as "empty" so first-run installs work without a pre-step.
46
+ *
47
+ * We do not recurse: Zoomies owns this directory flatly. If an operator drops
48
+ * a subdirectory in here, we ignore it.
49
+ */
50
+ async function defaultListManagedFiles(sitesDir) {
51
+ try {
52
+ const entries = await readdir(sitesDir);
53
+ return entries.filter((name) => name.endsWith('.conf')).map((name) => resolve(sitesDir, name));
54
+ }
55
+ catch (err) {
56
+ if (err.code === 'ENOENT') {
57
+ return [];
58
+ }
59
+ throw err;
60
+ }
61
+ }
62
+ /**
63
+ * Default `reload` — thin wrapper around `execa` that follows the project's
64
+ * no-shell rule (argv array, never a string). `reject: false` lets us surface
65
+ * a non-zero exit as a normal return value instead of an exception, which is
66
+ * what the orchestrator needs to decide on rollback.
67
+ */
68
+ async function defaultReload(bin, args) {
69
+ const result = await execa(bin, [...args], { reject: false });
70
+ return {
71
+ exitCode: result.exitCode ?? null,
72
+ stderr: result.stderr ?? '',
73
+ };
74
+ }
75
+ /**
76
+ * Build the full {@link ReloadDeps} record, layering caller overrides onto
77
+ * the defaults. The defaults are constructed lazily here (rather than at
78
+ * module load) so tests that stub the validator or atomic-write modules can
79
+ * still benefit from those stubs without monkey-patching this file.
80
+ */
81
+ function resolveDeps(overrides) {
82
+ return {
83
+ validate: overrides?.validate ?? validateConfig,
84
+ reload: overrides?.reload ?? defaultReload,
85
+ probe: overrides?.probe ?? probeHealth,
86
+ listManagedFiles: overrides?.listManagedFiles ?? defaultListManagedFiles,
87
+ writeAtomic: overrides?.writeAtomic ?? writeAtomicImpl,
88
+ deleteAtomic: overrides?.deleteAtomic ?? deleteAtomicImpl,
89
+ };
90
+ }
91
+ /**
92
+ * Determine which files on disk are no longer wanted (orphans to delete) and
93
+ * which sites need their config written (the full `rendered` set — we always
94
+ * overwrite, even if the contents happen to match what is on disk, because
95
+ * `writeAtomic`'s rollback semantics rely on snapshotting the prior bytes).
96
+ *
97
+ * Orphans are matched by basename: a file `foo.conf` whose `foo` is not a key
98
+ * in `rendered` gets deleted. Subtle edge case: a file ending in `.conf.new`
99
+ * would not be filtered here because we accept only basename-ends-with-conf
100
+ * via `defaultListManagedFiles`. That filter lives in the listing layer so
101
+ * the orchestrator can stay agnostic about disk layout.
102
+ */
103
+ function planDiff(rendered, sitesDir, existing) {
104
+ const desiredIds = new Set(rendered.keys());
105
+ const toDelete = [];
106
+ for (const filePath of existing) {
107
+ const id = basename(filePath, '.conf');
108
+ if (!desiredIds.has(id)) {
109
+ toDelete.push(filePath);
110
+ }
111
+ }
112
+ const toWrite = [];
113
+ for (const [siteId, contents] of rendered) {
114
+ toWrite.push({ path: resolve(sitesDir, `${siteId}.conf`), contents });
115
+ }
116
+ return { toWrite, toDelete };
117
+ }
118
+ /**
119
+ * Restore every rollback handle in reverse order, swallowing individual
120
+ * failures so a broken restore in the middle of the chain doesn't strand the
121
+ * earlier handles. We log each failure so the operator can investigate.
122
+ *
123
+ * Reverse order matters because the handles model a sequence of changes:
124
+ * the last write may have replaced a file that the previous delete had just
125
+ * removed. Unwinding LIFO mirrors the apply order and keeps the intermediate
126
+ * states consistent.
127
+ */
128
+ async function rollbackAll(handles) {
129
+ for (let i = handles.length - 1; i >= 0; i -= 1) {
130
+ const handle = handles[i];
131
+ if (handle === undefined) {
132
+ // Guard for noUncheckedIndexedAccess; cannot actually happen because i
133
+ // is always within bounds, but the type-system needs the check.
134
+ continue;
135
+ }
136
+ try {
137
+ await handle.restore();
138
+ }
139
+ catch (err) {
140
+ console.error('zoomies: rollback handle failed to restore:', err);
141
+ }
142
+ }
143
+ }
144
+ /**
145
+ * Apply a freshly rendered bundle to the managed sites directory.
146
+ *
147
+ * Contract:
148
+ * - `rendered` is the output of `renderBundle`: a map from site id to the
149
+ * NGINX `server { ... }` snippet for that site. Site ids are presumed
150
+ * safe (UUIDs per Phase 1); we do not sanitize them against `..`.
151
+ * - `opts.sitesDir` is the absolute path of the directory Zoomies owns.
152
+ * Anything `*.conf` in there that is not a current site id is an orphan
153
+ * and will be deleted.
154
+ * - `opts.healthCheckUrl` is the post-reload smoke test target.
155
+ *
156
+ * Behaviour:
157
+ * - Pure validation failure -> no disk side effects.
158
+ * - Disk-apply failure -> rolled back to the pre-call state, no reload.
159
+ * - Reload or probe failure -> rolled back AND a second reload kicks NGINX
160
+ * back to the pre-call state. If that second reload also fails the
161
+ * result still reports the original step (reload/probe) — operator
162
+ * intervention is required either way.
163
+ *
164
+ * Returns an {@link ApplyResult}. Never throws on operational failures.
165
+ */
166
+ export async function applyDesiredState(rendered, opts) {
167
+ const deps = resolveDeps(opts.deps);
168
+ const reloadArgs = opts.nginxReloadArgs ?? DEFAULT_RELOAD_ARGS;
169
+ // Step 1: validate. Pure check — no disk writes, no NGINX signals.
170
+ const candidate = joinRendered(rendered);
171
+ const validation = await deps.validate(candidate);
172
+ if (!validation.ok) {
173
+ return { ok: false, step: 'validate', validation };
174
+ }
175
+ // Step 2: diff disk against desired state.
176
+ const existing = await deps.listManagedFiles(opts.sitesDir);
177
+ const { toWrite, toDelete } = planDiff(rendered, opts.sitesDir, existing);
178
+ // Step 3: apply writes/deletes, accumulating rollback handles in order.
179
+ // Any single failure rolls back everything accumulated so far and aborts
180
+ // BEFORE we touch NGINX — disk is back where it started, no reload needed.
181
+ const rollbacks = [];
182
+ try {
183
+ for (const { path, contents } of toWrite) {
184
+ const handle = await deps.writeAtomic(path, contents);
185
+ rollbacks.push(handle);
186
+ }
187
+ for (const path of toDelete) {
188
+ const handle = await deps.deleteAtomic(path);
189
+ rollbacks.push(handle);
190
+ }
191
+ }
192
+ catch (err) {
193
+ await rollbackAll(rollbacks);
194
+ const message = err instanceof Error ? err.message : String(err);
195
+ return { ok: false, step: 'write', message };
196
+ }
197
+ // Step 4: reload NGINX. If it refuses the new bundle, we need to roll the
198
+ // disk back AND tell NGINX to re-read the (now restored) config so its
199
+ // in-memory state matches what we just wrote.
200
+ const reloadResult = await deps.reload(getNginxBinary(), reloadArgs);
201
+ if (reloadResult.exitCode !== 0) {
202
+ await rollbackAll(rollbacks);
203
+ const secondReload = await deps.reload(getNginxBinary(), reloadArgs);
204
+ if (secondReload.exitCode !== 0) {
205
+ // Both reloads failed — log the post-rollback stderr so the operator
206
+ // has both ends of the story, but preserve the original failure as
207
+ // the reported step. The system is in a hand-fix state either way.
208
+ console.error('zoomies: post-rollback reload also failed (exitCode=%s): %s', secondReload.exitCode, secondReload.stderr);
209
+ }
210
+ return { ok: false, step: 'reload', message: reloadResult.stderr };
211
+ }
212
+ // Step 5: probe. The new config parsed and reloaded, but the upstream may
213
+ // still be unreachable from the freshly reloaded NGINX. A failed probe
214
+ // means the new bundle is bad in a way `nginx -t` cannot detect — roll
215
+ // back and re-reload to restore the prior working configuration.
216
+ const probeResult = await deps.probe({
217
+ url: opts.healthCheckUrl,
218
+ ...opts.healthCheckOptions,
219
+ });
220
+ if (!probeResult.ok) {
221
+ await rollbackAll(rollbacks);
222
+ const secondReload = await deps.reload(getNginxBinary(), reloadArgs);
223
+ if (secondReload.exitCode !== 0) {
224
+ console.error('zoomies: post-rollback reload also failed (exitCode=%s): %s', secondReload.exitCode, secondReload.stderr);
225
+ }
226
+ return { ok: false, step: 'probe', probe: probeResult };
227
+ }
228
+ // Step 6: success. Rollback handles are deliberately not invoked — the
229
+ // change is now the committed state.
230
+ return { ok: true, step: 'success' };
231
+ }
232
+ //# sourceMappingURL=reload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reload.js","sourceRoot":"","sources":["../../../src/server/reload/reload.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { Cert } from '../domain/cert.js';
2
+ import type { Site } from '../domain/site.js';
3
+ import type { Upstream } from '../domain/upstream.js';
4
+ /**
5
+ * Renders a complete bundle of sites into NGINX `server { ... }` snippets.
6
+ *
7
+ * Joins each {@link Site} to its referenced {@link Upstream} (by `upstreamId`)
8
+ * and to its matching {@link Cert} (by `cert.domain === site.hostname`). A
9
+ * missing upstream is a domain-invariant violation — every site must point
10
+ * at an upstream that exists — so we throw {@link NotFoundError} rather than
11
+ * silently degrading. A missing cert is fine; the renderer encodes "no cert
12
+ * yet" semantics per `tlsMode`.
13
+ *
14
+ * Pure function. No I/O. Returns a `Map` keyed by `site.id` to preserve the
15
+ * caller's iteration order and to make per-site lookups O(1) downstream.
16
+ */
17
+ export declare function renderBundle(sites: Site[], upstreams: Upstream[], certs: Cert[]): Map<string, string>;
18
+ //# sourceMappingURL=render-bundle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-bundle.d.ts","sourceRoot":"","sources":["../../../src/server/renderer/render-bundle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAE9C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGtD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,IAAI,EAAE,EACb,SAAS,EAAE,QAAQ,EAAE,EACrB,KAAK,EAAE,IAAI,EAAE,GACZ,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBrB"}
@@ -0,0 +1,32 @@
1
+ import { NotFoundError } from '../domain/errors.js';
2
+ import { renderSite } from './render-site.js';
3
+ /**
4
+ * Renders a complete bundle of sites into NGINX `server { ... }` snippets.
5
+ *
6
+ * Joins each {@link Site} to its referenced {@link Upstream} (by `upstreamId`)
7
+ * and to its matching {@link Cert} (by `cert.domain === site.hostname`). A
8
+ * missing upstream is a domain-invariant violation — every site must point
9
+ * at an upstream that exists — so we throw {@link NotFoundError} rather than
10
+ * silently degrading. A missing cert is fine; the renderer encodes "no cert
11
+ * yet" semantics per `tlsMode`.
12
+ *
13
+ * Pure function. No I/O. Returns a `Map` keyed by `site.id` to preserve the
14
+ * caller's iteration order and to make per-site lookups O(1) downstream.
15
+ */
16
+ export function renderBundle(sites, upstreams, certs) {
17
+ const upstreamsById = new Map(upstreams.map((u) => [u.id, u]));
18
+ // Certs are indexed by `domain` because the join key from a Site is its
19
+ // hostname, not a cert id. Stale cert rows (no matching site) are tolerated.
20
+ const certsByDomain = new Map(certs.map((c) => [c.domain, c]));
21
+ const rendered = new Map();
22
+ for (const site of sites) {
23
+ const upstream = upstreamsById.get(site.upstreamId);
24
+ if (upstream === undefined) {
25
+ throw new NotFoundError(`site ${site.id} references upstream ${site.upstreamId} which does not exist`);
26
+ }
27
+ const cert = certsByDomain.get(site.hostname) ?? null;
28
+ rendered.set(site.id, renderSite(site, upstream, cert));
29
+ }
30
+ return rendered;
31
+ }
32
+ //# sourceMappingURL=render-bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-bundle.js","sourceRoot":"","sources":["../../../src/server/renderer/render-bundle.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAC1B,KAAa,EACb,SAAqB,EACrB,KAAa;IAEb,MAAM,aAAa,GAAG,IAAI,GAAG,CAAmB,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACjF,wEAAwE;IACxE,6EAA6E;IAC7E,MAAM,aAAa,GAAG,IAAI,GAAG,CAAe,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAE7E,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACpD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,IAAI,aAAa,CACrB,QAAQ,IAAI,CAAC,EAAE,wBAAwB,IAAI,CAAC,UAAU,uBAAuB,CAC9E,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC;QACtD,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Cert } from '../domain/cert.js';
2
+ import type { Site } from '../domain/site.js';
3
+ import type { Upstream } from '../domain/upstream.js';
4
+ export declare function renderSite(site: Site, upstream: Upstream, cert: Cert | null): string;
5
+ //# sourceMappingURL=render-site.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-site.d.ts","sourceRoot":"","sources":["../../../src/server/renderer/render-site.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAkB,MAAM,uBAAuB,CAAC;AAwJtE,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,CAMpF"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Pure NGINX config renderer for a single {@link Site}.
3
+ *
4
+ * Input: already-validated domain entities. Output: a deterministic, contiguous
5
+ * string ending with a single trailing newline. The renderer performs **no**
6
+ * I/O — no filesystem, no exec, no clocks. Two invocations with identical
7
+ * inputs MUST produce byte-identical output (the test suite enforces this via
8
+ * golden fixtures).
9
+ *
10
+ * NGINX validation (`nginx -t`) and reload orchestration live in later phases;
11
+ * this module only generates the text.
12
+ */
13
+ /**
14
+ * NGINX `upstream` block identifiers must be valid identifiers — UUID hyphens
15
+ * break that. We sanitize once and reuse for both the upstream block name and
16
+ * the matching `proxy_pass` target inside the `server` block.
17
+ */
18
+ function upstreamBlockName(site) {
19
+ return `zoomies_${site.id.replace(/-/g, '_')}`;
20
+ }
21
+ function renderLoadBalancerDirective(upstream) {
22
+ // Round-robin is the NGINX default — emitting it explicitly would be noise.
23
+ if (upstream.loadBalancer === 'round_robin')
24
+ return '';
25
+ if (upstream.loadBalancer === 'least_conn')
26
+ return ' least_conn;\n';
27
+ return ' ip_hash;\n';
28
+ }
29
+ function renderTarget(target) {
30
+ // Always emit `weight=` (even for weight=1) so snapshots stay stable when
31
+ // the schema default eventually changes.
32
+ return ` server ${target.host}:${target.port} weight=${target.weight};\n`;
33
+ }
34
+ function renderUpstreamBlock(site, upstream) {
35
+ const name = upstreamBlockName(site);
36
+ const lbDirective = renderLoadBalancerDirective(upstream);
37
+ const targetLines = upstream.targets.map(renderTarget).join('');
38
+ return `upstream ${name} {\n${lbDirective}${targetLines} keepalive 32;\n}\n`;
39
+ }
40
+ /**
41
+ * The `location /` block is identical across HTTP and HTTPS server blocks —
42
+ * extract it so both sides stay in sync.
43
+ */
44
+ function renderProxyLocation(site) {
45
+ const name = upstreamBlockName(site);
46
+ return [
47
+ ' location / {\n',
48
+ ' proxy_http_version 1.1;\n',
49
+ ' proxy_set_header Host $host;\n',
50
+ ' proxy_set_header X-Real-IP $remote_addr;\n',
51
+ ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n',
52
+ ' proxy_set_header X-Forwarded-Proto $scheme;\n',
53
+ ' proxy_set_header Connection "";\n',
54
+ ` proxy_pass http://${name};\n`,
55
+ ' }\n',
56
+ ].join('');
57
+ }
58
+ function renderHttpRedirectServer(site) {
59
+ return [
60
+ 'server {\n',
61
+ ' listen 80;\n',
62
+ ' listen [::]:80;\n',
63
+ ` server_name ${site.hostname};\n`,
64
+ '\n',
65
+ ' return 301 https://$host$request_uri;\n',
66
+ '}\n',
67
+ ].join('');
68
+ }
69
+ function renderHttpsServer(site, cert) {
70
+ return [
71
+ 'server {\n',
72
+ ' listen 443 ssl;\n',
73
+ ' listen [::]:443 ssl;\n',
74
+ ` server_name ${site.hostname};\n`,
75
+ ` ssl_certificate ${cert.pemPath};\n`,
76
+ ` ssl_certificate_key ${cert.keyPath};\n`,
77
+ '\n',
78
+ renderProxyLocation(site),
79
+ '}\n',
80
+ ].join('');
81
+ }
82
+ function renderHttpServerOff(site) {
83
+ return [
84
+ 'server {\n',
85
+ ' listen 80;\n',
86
+ ' listen [::]:80;\n',
87
+ ` server_name ${site.hostname};\n`,
88
+ '\n',
89
+ renderProxyLocation(site),
90
+ '}\n',
91
+ ].join('');
92
+ }
93
+ function renderHttpServerAcmePending(site) {
94
+ return [
95
+ 'server {\n',
96
+ ' listen 80;\n',
97
+ ' listen [::]:80;\n',
98
+ ` server_name ${site.hostname};\n`,
99
+ '\n',
100
+ ' # acme: awaiting issuance\n',
101
+ ' location /.well-known/acme-challenge/ {\n',
102
+ ' root /var/lib/zoomies/acme;\n',
103
+ ' }\n',
104
+ '\n',
105
+ renderProxyLocation(site),
106
+ '}\n',
107
+ ].join('');
108
+ }
109
+ function renderHttpServerManualNoCert(site) {
110
+ return [
111
+ 'server {\n',
112
+ ' listen 80;\n',
113
+ ' listen [::]:80;\n',
114
+ ` server_name ${site.hostname};\n`,
115
+ '\n',
116
+ ' # tls=manual: cert missing, refusing to render https block\n',
117
+ '\n',
118
+ renderProxyLocation(site),
119
+ '}\n',
120
+ ].join('');
121
+ }
122
+ function renderServerBlocks(site, cert) {
123
+ if (site.tlsMode === 'off') {
124
+ return renderHttpServerOff(site);
125
+ }
126
+ if (site.tlsMode === 'acme') {
127
+ if (cert === null) {
128
+ return renderHttpServerAcmePending(site);
129
+ }
130
+ return `${renderHttpRedirectServer(site)}\n${renderHttpsServer(site, cert)}`;
131
+ }
132
+ // tlsMode === 'manual'
133
+ if (cert === null) {
134
+ return renderHttpServerManualNoCert(site);
135
+ }
136
+ return `${renderHttpRedirectServer(site)}\n${renderHttpsServer(site, cert)}`;
137
+ }
138
+ export function renderSite(site, upstream, cert) {
139
+ const header = `# managed-by zoomies — site:${site.id} upstream:${upstream.id}\n`;
140
+ const upstreamBlock = renderUpstreamBlock(site, upstream);
141
+ const serverBlocks = renderServerBlocks(site, cert);
142
+ return `${header}${upstreamBlock}\n${serverBlocks}`;
143
+ }
144
+ //# sourceMappingURL=render-site.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-site.js","sourceRoot":"","sources":["../../../src/server/renderer/render-site.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,IAAU;IACnC,OAAO,WAAW,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;AACjD,CAAC;AAED,SAAS,2BAA2B,CAAC,QAAkB;IACrD,4EAA4E;IAC5E,IAAI,QAAQ,CAAC,YAAY,KAAK,aAAa;QAAE,OAAO,EAAE,CAAC;IACvD,IAAI,QAAQ,CAAC,YAAY,KAAK,YAAY;QAAE,OAAO,mBAAmB,CAAC;IACvE,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED,SAAS,YAAY,CAAC,MAAsB;IAC1C,0EAA0E;IAC1E,yCAAyC;IACzC,OAAO,cAAc,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,WAAW,MAAM,CAAC,MAAM,KAAK,CAAC;AAC/E,CAAC;AAED,SAAS,mBAAmB,CAAC,IAAU,EAAE,QAAkB;IACzD,MAAM,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,WAAW,GAAG,2BAA2B,CAAC,QAAQ,CAAC,CAAC;IAC1D,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEhE,OAAO,YAAY,IAAI,OAAO,WAAW,GAAG,WAAW,wBAAwB,CAAC;AAClF,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB,CAAC,IAAU;IACrC,MAAM,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IACrC,OAAO;QACL,oBAAoB;QACpB,mCAAmC;QACnC,wCAAwC;QACxC,oDAAoD;QACpD,wEAAwE;QACxE,uDAAuD;QACvD,2CAA2C;QAC3C,6BAA6B,IAAI,KAAK;QACtC,SAAS;KACV,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACb,CAAC;AAED,SAAS,wBAAwB,CAAC,IAAU;IAC1C,OAAO;QACL,YAAY;QACZ,kBAAkB;QAClB,uBAAuB;QACvB,mBAAmB,IAAI,CAAC,QAAQ,KAAK;QACrC,IAAI;QACJ,6CAA6C;QAC7C,KAAK;KACN,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACb,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAU,EAAE,IAAU;IAC/C,OAAO;QACL,YAAY;QACZ,uBAAuB;QACvB,4BAA4B;QAC5B,mBAAmB,IAAI,CAAC,QAAQ,KAAK;QACrC,uBAAuB,IAAI,CAAC,OAAO,KAAK;QACxC,2BAA2B,IAAI,CAAC,OAAO,KAAK;QAC5C,IAAI;QACJ,mBAAmB,CAAC,IAAI,CAAC;QACzB,KAAK;KACN,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACb,CAAC;AAED,SAAS,mBAAmB,CAAC,IAAU;IACrC,OAAO;QACL,YAAY;QACZ,kBAAkB;QAClB,uBAAuB;QACvB,mBAAmB,IAAI,CAAC,QAAQ,KAAK;QACrC,IAAI;QACJ,mBAAmB,CAAC,IAAI,CAAC;QACzB,KAAK;KACN,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACb,CAAC;AAED,SAAS,2BAA2B,CAAC,IAAU;IAC7C,OAAO;QACL,YAAY;QACZ,kBAAkB;QAClB,uBAAuB;QACvB,mBAAmB,IAAI,CAAC,QAAQ,KAAK;QACrC,IAAI;QACJ,iCAAiC;QACjC,+CAA+C;QAC/C,uCAAuC;QACvC,SAAS;QACT,IAAI;QACJ,mBAAmB,CAAC,IAAI,CAAC;QACzB,KAAK;KACN,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACb,CAAC;AAED,SAAS,4BAA4B,CAAC,IAAU;IAC9C,OAAO;QACL,YAAY;QACZ,kBAAkB;QAClB,uBAAuB;QACvB,mBAAmB,IAAI,CAAC,QAAQ,KAAK;QACrC,IAAI;QACJ,kEAAkE;QAClE,IAAI;QACJ,mBAAmB,CAAC,IAAI,CAAC;QACzB,KAAK;KACN,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACb,CAAC;AAED,SAAS,kBAAkB,CAAC,IAAU,EAAE,IAAiB;IACvD,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAC3B,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAED,IAAI,IAAI,CAAC,OAAO,KAAK,MAAM,EAAE,CAAC;QAC5B,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,OAAO,2BAA2B,CAAC,IAAI,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,GAAG,wBAAwB,CAAC,IAAI,CAAC,KAAK,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;IAC/E,CAAC;IAED,uBAAuB;IACvB,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,OAAO,4BAA4B,CAAC,IAAI,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,GAAG,wBAAwB,CAAC,IAAI,CAAC,KAAK,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;AAC/E,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAU,EAAE,QAAkB,EAAE,IAAiB;IAC1E,MAAM,MAAM,GAAG,+BAA+B,IAAI,CAAC,EAAE,cAAc,QAAQ,CAAC,EAAE,IAAI,CAAC;IACnF,MAAM,aAAa,GAAG,mBAAmB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC1D,MAAM,YAAY,GAAG,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAEpD,OAAO,GAAG,MAAM,GAAG,aAAa,KAAK,YAAY,EAAE,CAAC;AACtD,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type Database from 'better-sqlite3';
2
+ import { type Cert } from '../domain/cert.js';
3
+ export declare class CertRepository {
4
+ private readonly db;
5
+ private readonly insertStmt;
6
+ private readonly selectByIdStmt;
7
+ private readonly selectByDomainStmt;
8
+ private readonly selectAllStmt;
9
+ private readonly updateStmt;
10
+ private readonly deleteStmt;
11
+ constructor(db: Database.Database);
12
+ create(input: Omit<Cert, 'id' | 'createdAt' | 'updatedAt'>): Cert;
13
+ findById(id: string): Cert | null;
14
+ findByDomain(domain: string): Cert | null;
15
+ list(): Cert[];
16
+ update(id: string, patch: Partial<Omit<Cert, 'id' | 'createdAt' | 'updatedAt'>>): Cert;
17
+ delete(id: string): boolean;
18
+ }
19
+ //# sourceMappingURL=cert-repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cert-repository.d.ts","sourceRoot":"","sources":["../../../src/server/repositories/cert-repository.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,mBAAmB,CAAC;AA0C1D,qBAAa,cAAc;IAQb,OAAO,CAAC,QAAQ,CAAC,EAAE;IAP/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;IAChC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAEC,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAqBlD,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,GAAG,IAAI;IAoCjE,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAKjC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAKzC,IAAI,IAAI,IAAI,EAAE;IAId,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,CAAC,GAAG,IAAI;IA6CtF,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;CAG5B"}
@@ -0,0 +1,112 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { CertSchema } from '../domain/cert.js';
3
+ import { ConflictError, NotFoundError } from '../domain/errors.js';
4
+ function isUniqueConstraintError(err) {
5
+ return (typeof err === 'object' &&
6
+ err !== null &&
7
+ typeof err.code === 'string' &&
8
+ err.code.startsWith('SQLITE_CONSTRAINT_UNIQUE'));
9
+ }
10
+ function rowToCert(row) {
11
+ return CertSchema.parse({
12
+ id: row.id,
13
+ domain: row.domain,
14
+ provider: row.provider,
15
+ pemPath: row.pem_path,
16
+ keyPath: row.key_path,
17
+ notBefore: row.not_before,
18
+ notAfter: row.not_after,
19
+ createdAt: row.created_at,
20
+ updatedAt: row.updated_at,
21
+ });
22
+ }
23
+ export class CertRepository {
24
+ db;
25
+ insertStmt;
26
+ selectByIdStmt;
27
+ selectByDomainStmt;
28
+ selectAllStmt;
29
+ updateStmt;
30
+ deleteStmt;
31
+ constructor(db) {
32
+ this.db = db;
33
+ this.insertStmt = db.prepare('INSERT INTO certs (id, domain, provider, pem_path, key_path, not_before, not_after, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
34
+ this.selectByIdStmt = db.prepare('SELECT id, domain, provider, pem_path, key_path, not_before, not_after, created_at, updated_at FROM certs WHERE id = ?');
35
+ this.selectByDomainStmt = db.prepare('SELECT id, domain, provider, pem_path, key_path, not_before, not_after, created_at, updated_at FROM certs WHERE domain = ?');
36
+ this.selectAllStmt = db.prepare('SELECT id, domain, provider, pem_path, key_path, not_before, not_after, created_at, updated_at FROM certs ORDER BY domain ASC');
37
+ this.updateStmt = db.prepare('UPDATE certs SET domain = ?, provider = ?, pem_path = ?, key_path = ?, not_before = ?, not_after = ?, updated_at = ? WHERE id = ?');
38
+ this.deleteStmt = db.prepare('DELETE FROM certs WHERE id = ?');
39
+ }
40
+ create(input) {
41
+ const id = randomUUID();
42
+ const now = new Date().toISOString();
43
+ try {
44
+ this.insertStmt.run(id, input.domain, input.provider, input.pemPath, input.keyPath, input.notBefore, input.notAfter, now, now);
45
+ }
46
+ catch (err) {
47
+ if (isUniqueConstraintError(err)) {
48
+ throw new ConflictError(`domain already has a cert: ${input.domain}`, { cause: err });
49
+ }
50
+ throw err;
51
+ }
52
+ return rowToCert({
53
+ id,
54
+ domain: input.domain,
55
+ provider: input.provider,
56
+ pem_path: input.pemPath,
57
+ key_path: input.keyPath,
58
+ not_before: input.notBefore,
59
+ not_after: input.notAfter,
60
+ created_at: now,
61
+ updated_at: now,
62
+ });
63
+ }
64
+ findById(id) {
65
+ const row = this.selectByIdStmt.get(id);
66
+ return row ? rowToCert(row) : null;
67
+ }
68
+ findByDomain(domain) {
69
+ const row = this.selectByDomainStmt.get(domain);
70
+ return row ? rowToCert(row) : null;
71
+ }
72
+ list() {
73
+ return this.selectAllStmt.all().map(rowToCert);
74
+ }
75
+ update(id, patch) {
76
+ const existing = this.selectByIdStmt.get(id);
77
+ if (!existing) {
78
+ throw new NotFoundError(`cert not found: ${id}`);
79
+ }
80
+ const now = new Date().toISOString();
81
+ const nextDomain = patch.domain ?? existing.domain;
82
+ const nextProvider = patch.provider ?? existing.provider;
83
+ const nextPemPath = patch.pemPath ?? existing.pem_path;
84
+ const nextKeyPath = patch.keyPath ?? existing.key_path;
85
+ const nextNotBefore = patch.notBefore ?? existing.not_before;
86
+ const nextNotAfter = patch.notAfter ?? existing.not_after;
87
+ try {
88
+ this.updateStmt.run(nextDomain, nextProvider, nextPemPath, nextKeyPath, nextNotBefore, nextNotAfter, now, id);
89
+ }
90
+ catch (err) {
91
+ if (isUniqueConstraintError(err)) {
92
+ throw new ConflictError(`domain already has a cert: ${nextDomain}`, { cause: err });
93
+ }
94
+ throw err;
95
+ }
96
+ return rowToCert({
97
+ id,
98
+ domain: nextDomain,
99
+ provider: nextProvider,
100
+ pem_path: nextPemPath,
101
+ key_path: nextKeyPath,
102
+ not_before: nextNotBefore,
103
+ not_after: nextNotAfter,
104
+ created_at: existing.created_at,
105
+ updated_at: now,
106
+ });
107
+ }
108
+ delete(id) {
109
+ return this.deleteStmt.run(id).changes > 0;
110
+ }
111
+ }
112
+ //# sourceMappingURL=cert-repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cert-repository.js","sourceRoot":"","sources":["../../../src/server/repositories/cert-repository.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type Database from 'better-sqlite3';
2
+ import { type Site } from '../domain/site.js';
3
+ type SiteInsert = Omit<Site, 'id' | 'createdAt' | 'updatedAt'>;
4
+ type SiteUpdate = Partial<Omit<Site, 'id' | 'createdAt' | 'updatedAt'>>;
5
+ export declare class SiteRepository {
6
+ #private;
7
+ private readonly db;
8
+ constructor(db: Database.Database);
9
+ create(input: SiteInsert): Site;
10
+ findById(id: string): Site | null;
11
+ findByHostname(hostname: string): Site | null;
12
+ list(): Site[];
13
+ update(id: string, patch: SiteUpdate): Site;
14
+ delete(id: string): boolean;
15
+ }
16
+ export {};
17
+ //# sourceMappingURL=site-repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-repository.d.ts","sourceRoot":"","sources":["../../../src/server/repositories/site-repository.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAG3C,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAkC1D,KAAK,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,CAAC;AAC/D,KAAK,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,CAAC,CAAC;AAUxE,qBAAa,cAAc;;IAQb,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAWlD,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAqB/B,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAKjC,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAK7C,IAAI,IAAI,IAAI,EAAE;IAKd,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI;IA8B3C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;CA8B5B"}