@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,76 @@
1
+ /**
2
+ * Process-wide database singleton + repository factory used by every Route
3
+ * Handler.
4
+ *
5
+ * Route Handlers run inside the long-lived Next.js Node process, so we open
6
+ * the SQLite database once on first use and reuse the connection. Each call
7
+ * to {@link getRepositories} hands back fresh repository instances — the
8
+ * prepared statements they cache live on the repo itself (a few ms of
9
+ * preparation cost per request is negligible compared to JSON
10
+ * serialisation), and re-instantiating per call keeps the API surface flat.
11
+ *
12
+ * Tests inject their own in-memory DB via {@link resetDbForTesting}; the
13
+ * same hook is used by `pnpm dev` to drop and re-open the connection when
14
+ * Next.js hot-reloads a module that imports this file.
15
+ */
16
+ import { mkdirSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { openDatabase } from '../db/connection.js';
19
+ import { runMigrations } from '../db/migrate.js';
20
+ import { CertRepository } from '../repositories/cert-repository.js';
21
+ import { SiteRepository } from '../repositories/site-repository.js';
22
+ import { UpstreamRepository } from '../repositories/upstream-repository.js';
23
+ let dbInstance = null;
24
+ /**
25
+ * Return the process-wide database connection, opening it on first use.
26
+ *
27
+ * The state directory is `$ZOOMIES_STATE_DIR` if set, otherwise
28
+ * `<cwd>/.zoomies`. The directory is created recursively if missing. The
29
+ * database file lives at `<stateDir>/zoomies.db` and is migrated on open.
30
+ */
31
+ export function getDb() {
32
+ if (dbInstance !== null) {
33
+ return dbInstance;
34
+ }
35
+ const stateDir = process.env.ZOOMIES_STATE_DIR ?? join(process.cwd(), '.zoomies');
36
+ mkdirSync(stateDir, { recursive: true });
37
+ dbInstance = openDatabase(join(stateDir, 'zoomies.db'));
38
+ runMigrations(dbInstance);
39
+ return dbInstance;
40
+ }
41
+ /**
42
+ * Construct a fresh set of repositories backed by the shared connection.
43
+ *
44
+ * Cheap to call per-request; repository constructors only prepare a handful
45
+ * of statements against an already-open database handle.
46
+ */
47
+ export function getRepositories() {
48
+ const db = getDb();
49
+ return {
50
+ sites: new SiteRepository(db),
51
+ upstreams: new UpstreamRepository(db),
52
+ certs: new CertRepository(db),
53
+ };
54
+ }
55
+ /**
56
+ * Replace (or clear) the cached database singleton.
57
+ *
58
+ * - Tests pass in their own in-memory connection so handlers operate on
59
+ * the same DB the test seeded.
60
+ * - Passing `null` or no argument closes any existing connection and
61
+ * forces the next {@link getDb} call to re-open from disk; used by the
62
+ * `pnpm dev` watcher when modules reload.
63
+ */
64
+ export function resetDbForTesting(db) {
65
+ if (dbInstance !== null && dbInstance !== db) {
66
+ try {
67
+ dbInstance.close();
68
+ }
69
+ catch {
70
+ // Ignore close failures — the handle may already be closed by the
71
+ // caller (tests often own the lifecycle of their own in-memory DB).
72
+ }
73
+ }
74
+ dbInstance = db ?? null;
75
+ }
76
+ //# sourceMappingURL=db-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-context.js","sourceRoot":"","sources":["../../../src/server/api/db-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAIjC,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wCAAwC,CAAC;AAE5E,IAAI,UAAU,GAA6B,IAAI,CAAC;AAEhD;;;;;;GAMG;AACH,MAAM,UAAU,KAAK;IACnB,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACxB,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,CAAC,CAAC;IAClF,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEzC,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC;IACxD,aAAa,CAAC,UAAU,CAAC,CAAC;IAC1B,OAAO,UAAU,CAAC;AACpB,CAAC;AAQD;;;;;GAKG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;IACnB,OAAO;QACL,KAAK,EAAE,IAAI,cAAc,CAAC,EAAE,CAAC;QAC7B,SAAS,EAAE,IAAI,kBAAkB,CAAC,EAAE,CAAC;QACrC,KAAK,EAAE,IAAI,cAAc,CAAC,EAAE,CAAC;KAC9B,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAA6B;IAC7D,IAAI,UAAU,KAAK,IAAI,IAAI,UAAU,KAAK,EAAE,EAAE,CAAC;QAC7C,IAAI,CAAC;YACH,UAAU,CAAC,KAAK,EAAE,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,kEAAkE;YAClE,oEAAoE;QACtE,CAAC;IACH,CAAC;IACD,UAAU,GAAG,EAAE,IAAI,IAAI,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Translates errors thrown inside the control-plane domain layer (and Zod
3
+ * validation failures at the request boundary) into HTTP-shaped responses.
4
+ *
5
+ * Handlers should call this from a single top-level `try { ... } catch` and
6
+ * pass the caught value through verbatim. The mapping never re-throws and
7
+ * never leaks unanticipated error messages — those collapse to a generic
8
+ * `internal` response with the original logged to stderr for operators.
9
+ */
10
+ export interface ApiErrorResponse {
11
+ status: number;
12
+ body: {
13
+ error: string;
14
+ code: string;
15
+ details?: unknown;
16
+ };
17
+ }
18
+ export declare function mapErrorToResponse(err: unknown): ApiErrorResponse;
19
+ //# sourceMappingURL=error-mapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-mapping.d.ts","sourceRoot":"","sources":["../../../src/server/api/error-mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1D;AAgBD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,CAmCjE"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Translates errors thrown inside the control-plane domain layer (and Zod
3
+ * validation failures at the request boundary) into HTTP-shaped responses.
4
+ *
5
+ * Handlers should call this from a single top-level `try { ... } catch` and
6
+ * pass the caught value through verbatim. The mapping never re-throws and
7
+ * never leaks unanticipated error messages — those collapse to a generic
8
+ * `internal` response with the original logged to stderr for operators.
9
+ */
10
+ import { ZodError } from 'zod';
11
+ import { UnauthorizedError } from '../auth/require-token.js';
12
+ import { ConflictError, DomainError, NotFoundError, ValidationError } from '../domain/errors.js';
13
+ /**
14
+ * Build a 500 response without echoing the inbound error's message. The
15
+ * original is logged so operators can diagnose, but we treat unanticipated
16
+ * messages as potentially sensitive (they may include file paths, SQL
17
+ * snippets, or other internals).
18
+ */
19
+ function internalServerError(err) {
20
+ console.error('zoomies: unhandled error in api handler:', err);
21
+ return {
22
+ status: 500,
23
+ body: { error: 'internal server error', code: 'internal' },
24
+ };
25
+ }
26
+ export function mapErrorToResponse(err) {
27
+ if (err instanceof NotFoundError) {
28
+ return { status: 404, body: { error: err.message, code: 'not_found' } };
29
+ }
30
+ if (err instanceof ConflictError) {
31
+ return { status: 409, body: { error: err.message, code: 'conflict' } };
32
+ }
33
+ if (err instanceof ValidationError) {
34
+ return { status: 400, body: { error: err.message, code: 'validation' } };
35
+ }
36
+ if (err instanceof UnauthorizedError) {
37
+ return { status: 401, body: { error: err.message, code: 'unauthorized' } };
38
+ }
39
+ if (err instanceof ZodError) {
40
+ return {
41
+ status: 400,
42
+ body: {
43
+ error: 'invalid request body',
44
+ code: 'validation',
45
+ details: err.flatten(),
46
+ },
47
+ };
48
+ }
49
+ // Any other `DomainError` subclass we add later still gets its declared
50
+ // `code` surfaced, but at a 500 since we don't know its semantics here.
51
+ if (err instanceof DomainError) {
52
+ return { status: 500, body: { error: err.message, code: err.code } };
53
+ }
54
+ return internalServerError(err);
55
+ }
56
+ //# sourceMappingURL=error-mapping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-mapping.js","sourceRoot":"","sources":["../../../src/server/api/error-mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AAE/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAOjG;;;;;GAKG;AACH,SAAS,mBAAmB,CAAC,GAAY;IACvC,OAAO,CAAC,KAAK,CAAC,0CAA0C,EAAE,GAAG,CAAC,CAAC;IAC/D,OAAO;QACL,MAAM,EAAE,GAAG;QACX,IAAI,EAAE,EAAE,KAAK,EAAE,uBAAuB,EAAE,IAAI,EAAE,UAAU,EAAE;KAC3D,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,IAAI,GAAG,YAAY,aAAa,EAAE,CAAC;QACjC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC;IAC1E,CAAC;IAED,IAAI,GAAG,YAAY,aAAa,EAAE,CAAC;QACjC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,CAAC;IACzE,CAAC;IAED,IAAI,GAAG,YAAY,eAAe,EAAE,CAAC;QACnC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,CAAC;IAC3E,CAAC;IAED,IAAI,GAAG,YAAY,iBAAiB,EAAE,CAAC;QACrC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,CAAC;IAC7E,CAAC;IAED,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;QAC5B,OAAO;YACL,MAAM,EAAE,GAAG;YACX,IAAI,EAAE;gBACJ,KAAK,EAAE,sBAAsB;gBAC7B,IAAI,EAAE,YAAY;gBAClB,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE;aACvB;SACF,CAAC;IACJ,CAAC;IAED,wEAAwE;IACxE,wEAAwE;IACxE,IAAI,GAAG,YAAY,WAAW,EAAE,CAAC;QAC/B,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;IACvE,CAAC;IAED,OAAO,mBAAmB,CAAC,GAAG,CAAC,CAAC;AAClC,CAAC"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Issue (or re-issue) a certificate for a single site, identified by id.
3
+ *
4
+ * The handler is framework-agnostic, mirroring {@link createSite} et al.:
5
+ * - Look up the site by id and surface a {@link NotFoundError} if absent.
6
+ * - Drive the ACME flow via the injected `issue` closure — the closure
7
+ * wraps {@link issueCertificate} in production and is mocked in tests.
8
+ * - Insert a new `certs` row on first issuance, or update the existing
9
+ * row when the hostname already has one (the cert table treats `domain`
10
+ * as the unique key).
11
+ *
12
+ * NGINX reload is intentionally NOT triggered here. The new cert files
13
+ * land on disk during issuance; the next mutation-driven reload (when a
14
+ * site is created/updated) or scheduled reload picks them up.
15
+ *
16
+ * TODO(phase-9): once the API → reload bridge lands, fire a reload here
17
+ * after a successful issuance so the new cert takes effect immediately.
18
+ */
19
+ import type { AcmeAccount } from '../../certs/acme-account.js';
20
+ import type { ChallengeStore } from '../../certs/challenge-store.js';
21
+ import type { IssueResult } from '../../certs/issue.js';
22
+ import type { Cert } from '../../domain/cert.js';
23
+ import type { CertRepository } from '../../repositories/cert-repository.js';
24
+ import type { SiteRepository } from '../../repositories/site-repository.js';
25
+ export interface IssueCertForSiteDeps {
26
+ siteRepo: SiteRepository;
27
+ certRepo: CertRepository;
28
+ account: AcmeAccount;
29
+ challengeStore: ChallengeStore;
30
+ certDir: string;
31
+ /**
32
+ * Injectable issuance closure. Production wires this to
33
+ * {@link issueCertificate}; tests substitute a fake that returns a canned
34
+ * {@link IssueResult} so no real ACME order is opened.
35
+ */
36
+ issue: (opts: {
37
+ domain: string;
38
+ account: AcmeAccount;
39
+ challengeStore: ChallengeStore;
40
+ certDir: string;
41
+ }) => Promise<IssueResult>;
42
+ }
43
+ /**
44
+ * Issue (or re-issue) the certificate for the site identified by `siteId`.
45
+ *
46
+ * Returns the resulting `Cert` row — newly created or freshly updated.
47
+ */
48
+ export declare function issueCertForSite(siteId: string, deps: IssueCertForSiteDeps): Promise<Cert>;
49
+ //# sourceMappingURL=site-cert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-cert.d.ts","sourceRoot":"","sources":["../../../../src/server/api/handlers/site-cert.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAE5E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,cAAc,CAAC;IACzB,OAAO,EAAE,WAAW,CAAC;IACrB,cAAc,EAAE,cAAc,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,WAAW,CAAC;QACrB,cAAc,EAAE,cAAc,CAAC;QAC/B,OAAO,EAAE,MAAM,CAAC;KACjB,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BhG"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Issue (or re-issue) a certificate for a single site, identified by id.
3
+ *
4
+ * The handler is framework-agnostic, mirroring {@link createSite} et al.:
5
+ * - Look up the site by id and surface a {@link NotFoundError} if absent.
6
+ * - Drive the ACME flow via the injected `issue` closure — the closure
7
+ * wraps {@link issueCertificate} in production and is mocked in tests.
8
+ * - Insert a new `certs` row on first issuance, or update the existing
9
+ * row when the hostname already has one (the cert table treats `domain`
10
+ * as the unique key).
11
+ *
12
+ * NGINX reload is intentionally NOT triggered here. The new cert files
13
+ * land on disk during issuance; the next mutation-driven reload (when a
14
+ * site is created/updated) or scheduled reload picks them up.
15
+ *
16
+ * TODO(phase-9): once the API → reload bridge lands, fire a reload here
17
+ * after a successful issuance so the new cert takes effect immediately.
18
+ */
19
+ import { NotFoundError } from '../../domain/errors.js';
20
+ /**
21
+ * Issue (or re-issue) the certificate for the site identified by `siteId`.
22
+ *
23
+ * Returns the resulting `Cert` row — newly created or freshly updated.
24
+ */
25
+ export async function issueCertForSite(siteId, deps) {
26
+ const site = deps.siteRepo.findById(siteId);
27
+ if (site === null) {
28
+ throw new NotFoundError(`site not found: ${siteId}`);
29
+ }
30
+ const result = await deps.issue({
31
+ domain: site.hostname,
32
+ account: deps.account,
33
+ challengeStore: deps.challengeStore,
34
+ certDir: deps.certDir,
35
+ });
36
+ const existing = deps.certRepo.findByDomain(site.hostname);
37
+ if (existing === null) {
38
+ return deps.certRepo.create({
39
+ domain: result.domain,
40
+ provider: 'acme',
41
+ pemPath: result.pemPath,
42
+ keyPath: result.keyPath,
43
+ notBefore: result.notBefore,
44
+ notAfter: result.notAfter,
45
+ });
46
+ }
47
+ return deps.certRepo.update(existing.id, {
48
+ pemPath: result.pemPath,
49
+ keyPath: result.keyPath,
50
+ notBefore: result.notBefore,
51
+ notAfter: result.notAfter,
52
+ });
53
+ }
54
+ //# sourceMappingURL=site-cert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-cert.js","sourceRoot":"","sources":["../../../../src/server/api/handlers/site-cert.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AA2BvD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,MAAc,EAAE,IAA0B;IAC/E,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,MAAM,IAAI,aAAa,CAAC,mBAAmB,MAAM,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC;QAC9B,MAAM,EAAE,IAAI,CAAC,QAAQ;QACrB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC3D,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC1B,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,QAAQ,EAAE,MAAM;YAChB,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,QAAQ,EAAE,MAAM,CAAC,QAAQ;SAC1B,CAAC,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,EAAE;QACvC,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,QAAQ,EAAE,MAAM,CAAC,QAAQ;KAC1B,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Framework-agnostic CRUD operations for {@link Site} aggregates.
3
+ *
4
+ * These handlers are pure functions over a {@link SiteRepository}. They:
5
+ * - parse raw input with Zod at the boundary,
6
+ * - delegate persistence to the repository,
7
+ * - promote repository-level absence (`findById === null`,
8
+ * `delete === false`) into {@link NotFoundError},
9
+ * - and otherwise let domain errors (`NotFoundError`, `ConflictError`,
10
+ * `ZodError`) bubble verbatim so the Route Handler can hand them to
11
+ * {@link mapErrorToResponse} unchanged.
12
+ *
13
+ * No NGINX reload, no filesystem writes, no logging. Reload triggering is
14
+ * deferred until Phase 7 wires the UI to these endpoints — that's when we
15
+ * actually have a consumer that benefits from a downstream `applyDesiredState`.
16
+ */
17
+ import type { z } from 'zod';
18
+ import { type Site } from '../../domain/site.js';
19
+ import type { SiteRepository } from '../../repositories/site-repository.js';
20
+ export interface SiteHandlerDeps {
21
+ siteRepo: SiteRepository;
22
+ }
23
+ export declare const CreateSiteInputSchema: z.ZodObject<{
24
+ hostname: z.ZodString;
25
+ upstreamId: z.ZodString;
26
+ tlsMode: z.ZodEnum<{
27
+ off: "off";
28
+ acme: "acme";
29
+ manual: "manual";
30
+ }>;
31
+ }, z.core.$strip>;
32
+ export declare const UpdateSiteInputSchema: z.ZodObject<{
33
+ hostname: z.ZodOptional<z.ZodString>;
34
+ upstreamId: z.ZodOptional<z.ZodString>;
35
+ tlsMode: z.ZodOptional<z.ZodEnum<{
36
+ off: "off";
37
+ acme: "acme";
38
+ manual: "manual";
39
+ }>>;
40
+ }, z.core.$strip>;
41
+ export type CreateSiteInput = z.infer<typeof CreateSiteInputSchema>;
42
+ export type UpdateSiteInput = z.infer<typeof UpdateSiteInputSchema>;
43
+ export declare function listSites(deps: SiteHandlerDeps): Site[];
44
+ export declare function getSite(id: string, deps: SiteHandlerDeps): Site;
45
+ /**
46
+ * Validate `rawInput` against {@link CreateSiteInputSchema} and persist it.
47
+ *
48
+ * `.parse` (not `.safeParse`) — the resulting `ZodError` is caught by the
49
+ * Route Handler's top-level `try/catch` and translated by
50
+ * `mapErrorToResponse` into a 400 with structured details.
51
+ */
52
+ export declare function createSite(rawInput: unknown, deps: SiteHandlerDeps): Site;
53
+ /**
54
+ * Partially update an existing site.
55
+ *
56
+ * Mirrors the repository semantics: an empty patch is accepted, leaves all
57
+ * fields untouched, and still bumps `updatedAt`. A non-existent id surfaces
58
+ * as {@link NotFoundError} from the repository.
59
+ */
60
+ export declare function updateSite(id: string, rawInput: unknown, deps: SiteHandlerDeps): Site;
61
+ /**
62
+ * Delete a site. The repository returns `false` for a no-op delete; we
63
+ * promote that to {@link NotFoundError} at the API boundary so callers can
64
+ * distinguish a successful 204 from a missing record.
65
+ */
66
+ export declare function deleteSite(id: string, deps: SiteHandlerDeps): void;
67
+ //# sourceMappingURL=sites.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sites.d.ts","sourceRoot":"","sources":["../../../../src/server/api/handlers/sites.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAE5E,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED,eAAO,MAAM,qBAAqB;;;;;;;;iBAIhC,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;iBAAkC,CAAC;AAErE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACpE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,wBAAgB,SAAS,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,EAAE,CAEvD;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI,CAM/D;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI,CAGzE;AAyBD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI,CAGrF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI,CAKlE"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Framework-agnostic CRUD operations for {@link Site} aggregates.
3
+ *
4
+ * These handlers are pure functions over a {@link SiteRepository}. They:
5
+ * - parse raw input with Zod at the boundary,
6
+ * - delegate persistence to the repository,
7
+ * - promote repository-level absence (`findById === null`,
8
+ * `delete === false`) into {@link NotFoundError},
9
+ * - and otherwise let domain errors (`NotFoundError`, `ConflictError`,
10
+ * `ZodError`) bubble verbatim so the Route Handler can hand them to
11
+ * {@link mapErrorToResponse} unchanged.
12
+ *
13
+ * No NGINX reload, no filesystem writes, no logging. Reload triggering is
14
+ * deferred until Phase 7 wires the UI to these endpoints — that's when we
15
+ * actually have a consumer that benefits from a downstream `applyDesiredState`.
16
+ */
17
+ import { NotFoundError } from '../../domain/errors.js';
18
+ import { SiteSchema } from '../../domain/site.js';
19
+ export const CreateSiteInputSchema = SiteSchema.omit({
20
+ id: true,
21
+ createdAt: true,
22
+ updatedAt: true,
23
+ });
24
+ export const UpdateSiteInputSchema = CreateSiteInputSchema.partial();
25
+ export function listSites(deps) {
26
+ return deps.siteRepo.list();
27
+ }
28
+ export function getSite(id, deps) {
29
+ const site = deps.siteRepo.findById(id);
30
+ if (site === null) {
31
+ throw new NotFoundError('site not found');
32
+ }
33
+ return site;
34
+ }
35
+ /**
36
+ * Validate `rawInput` against {@link CreateSiteInputSchema} and persist it.
37
+ *
38
+ * `.parse` (not `.safeParse`) — the resulting `ZodError` is caught by the
39
+ * Route Handler's top-level `try/catch` and translated by
40
+ * `mapErrorToResponse` into a 400 with structured details.
41
+ */
42
+ export function createSite(rawInput, deps) {
43
+ const input = CreateSiteInputSchema.parse(rawInput);
44
+ return deps.siteRepo.create(input);
45
+ }
46
+ function stripUndefined(input) {
47
+ const out = {};
48
+ for (const key of Object.keys(input)) {
49
+ const value = input[key];
50
+ if (value !== undefined) {
51
+ out[key] = value;
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+ /**
57
+ * Partially update an existing site.
58
+ *
59
+ * Mirrors the repository semantics: an empty patch is accepted, leaves all
60
+ * fields untouched, and still bumps `updatedAt`. A non-existent id surfaces
61
+ * as {@link NotFoundError} from the repository.
62
+ */
63
+ export function updateSite(id, rawInput, deps) {
64
+ const patch = UpdateSiteInputSchema.parse(rawInput);
65
+ return deps.siteRepo.update(id, stripUndefined(patch));
66
+ }
67
+ /**
68
+ * Delete a site. The repository returns `false` for a no-op delete; we
69
+ * promote that to {@link NotFoundError} at the API boundary so callers can
70
+ * distinguish a successful 204 from a missing record.
71
+ */
72
+ export function deleteSite(id, deps) {
73
+ const deleted = deps.siteRepo.delete(id);
74
+ if (!deleted) {
75
+ throw new NotFoundError('site not found');
76
+ }
77
+ }
78
+ //# sourceMappingURL=sites.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sites.js","sourceRoot":"","sources":["../../../../src/server/api/handlers/sites.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAa,MAAM,sBAAsB,CAAC;AAO7D,MAAM,CAAC,MAAM,qBAAqB,GAAG,UAAU,CAAC,IAAI,CAAC;IACnD,EAAE,EAAE,IAAI;IACR,SAAS,EAAE,IAAI;IACf,SAAS,EAAE,IAAI;CAChB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAAG,qBAAqB,CAAC,OAAO,EAAE,CAAC;AAKrE,MAAM,UAAU,SAAS,CAAC,IAAqB;IAC7C,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,EAAU,EAAE,IAAqB;IACvD,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACxC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,MAAM,IAAI,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,QAAiB,EAAE,IAAqB;IACjE,MAAM,KAAK,GAAG,qBAAqB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACpD,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACrC,CAAC;AAcD,SAAS,cAAc,CAAoC,KAAQ;IACjE,MAAM,GAAG,GAAqB,EAAE,CAAC;IACjC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAmB,EAAE,CAAC;QACvD,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,GAAG,CAAC,GAAG,CAAC,GAAG,KAA0C,CAAC;QACxD,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,EAAU,EAAE,QAAiB,EAAE,IAAqB;IAC7E,MAAM,KAAK,GAAG,qBAAqB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACpD,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,EAAU,EAAE,IAAqB;IAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACzC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Framework-agnostic CRUD operations for {@link Upstream} aggregates.
3
+ *
4
+ * Mirrors the site handlers in shape: Zod at the boundary, repository for
5
+ * persistence, domain errors bubble unchanged to {@link mapErrorToResponse}.
6
+ *
7
+ * One asymmetry vs. sites: `deleteUpstream` may need to translate a raw
8
+ * SQLite FOREIGN KEY error into a {@link ConflictError}. The migration
9
+ * declares `sites.upstream_id REFERENCES upstreams(id) ON DELETE RESTRICT`,
10
+ * so deleting an upstream that any site still references blows up with
11
+ * `SQLITE_CONSTRAINT_FOREIGNKEY`. The repository does not currently catch
12
+ * this — see the inline translation in {@link deleteUpstream}.
13
+ */
14
+ import type { z } from 'zod';
15
+ import { type Upstream } from '../../domain/upstream.js';
16
+ import type { UpstreamRepository } from '../../repositories/upstream-repository.js';
17
+ export interface UpstreamHandlerDeps {
18
+ upstreamRepo: UpstreamRepository;
19
+ }
20
+ export declare const CreateUpstreamInputSchema: z.ZodObject<{
21
+ name: z.ZodString;
22
+ targets: z.ZodArray<z.ZodObject<{
23
+ host: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6, z.ZodString]>;
24
+ port: z.ZodNumber;
25
+ weight: z.ZodNumber;
26
+ }, z.core.$strip>>;
27
+ loadBalancer: z.ZodEnum<{
28
+ round_robin: "round_robin";
29
+ least_conn: "least_conn";
30
+ ip_hash: "ip_hash";
31
+ }>;
32
+ }, z.core.$strip>;
33
+ export declare const UpdateUpstreamInputSchema: z.ZodObject<{
34
+ name: z.ZodOptional<z.ZodString>;
35
+ targets: z.ZodOptional<z.ZodArray<z.ZodObject<{
36
+ host: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6, z.ZodString]>;
37
+ port: z.ZodNumber;
38
+ weight: z.ZodNumber;
39
+ }, z.core.$strip>>>;
40
+ loadBalancer: z.ZodOptional<z.ZodEnum<{
41
+ round_robin: "round_robin";
42
+ least_conn: "least_conn";
43
+ ip_hash: "ip_hash";
44
+ }>>;
45
+ }, z.core.$strip>;
46
+ export type CreateUpstreamInput = z.infer<typeof CreateUpstreamInputSchema>;
47
+ export type UpdateUpstreamInput = z.infer<typeof UpdateUpstreamInputSchema>;
48
+ export declare function listUpstreams(deps: UpstreamHandlerDeps): Upstream[];
49
+ export declare function getUpstream(id: string, deps: UpstreamHandlerDeps): Upstream;
50
+ export declare function createUpstream(rawInput: unknown, deps: UpstreamHandlerDeps): Upstream;
51
+ export declare function updateUpstream(id: string, rawInput: unknown, deps: UpstreamHandlerDeps): Upstream;
52
+ /**
53
+ * Delete an upstream.
54
+ *
55
+ * - Missing id → {@link NotFoundError} (the repo returns `false`; we
56
+ * promote it to 404 at the API boundary).
57
+ * - Upstream still referenced by a site → {@link ConflictError}. The
58
+ * underlying SQLite FOREIGN KEY error is opaque to API consumers, so we
59
+ * translate it into a domain error here. (The site repository handles
60
+ * the inverse direction; the upstream repository was not updated for
61
+ * this case because it's a Phase 6 API-boundary concern.)
62
+ */
63
+ export declare function deleteUpstream(id: string, deps: UpstreamHandlerDeps): void;
64
+ //# sourceMappingURL=upstreams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstreams.d.ts","sourceRoot":"","sources":["../../../../src/server/api/handlers/upstreams.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AAEpF,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,kBAAkB,CAAC;CAClC;AAED,eAAO,MAAM,yBAAyB;;;;;;;;;;;;iBAIpC,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;iBAAsC,CAAC;AAE7E,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC5E,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAwB5E,wBAAgB,aAAa,CAAC,IAAI,EAAE,mBAAmB,GAAG,QAAQ,EAAE,CAEnE;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,GAAG,QAAQ,CAM3E;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,mBAAmB,GAAG,QAAQ,CAGrF;AAwBD,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,mBAAmB,GAAG,QAAQ,CAGjG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAe1E"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Framework-agnostic CRUD operations for {@link Upstream} aggregates.
3
+ *
4
+ * Mirrors the site handlers in shape: Zod at the boundary, repository for
5
+ * persistence, domain errors bubble unchanged to {@link mapErrorToResponse}.
6
+ *
7
+ * One asymmetry vs. sites: `deleteUpstream` may need to translate a raw
8
+ * SQLite FOREIGN KEY error into a {@link ConflictError}. The migration
9
+ * declares `sites.upstream_id REFERENCES upstreams(id) ON DELETE RESTRICT`,
10
+ * so deleting an upstream that any site still references blows up with
11
+ * `SQLITE_CONSTRAINT_FOREIGNKEY`. The repository does not currently catch
12
+ * this — see the inline translation in {@link deleteUpstream}.
13
+ */
14
+ import { ConflictError, NotFoundError } from '../../domain/errors.js';
15
+ import { UpstreamSchema } from '../../domain/upstream.js';
16
+ export const CreateUpstreamInputSchema = UpstreamSchema.omit({
17
+ id: true,
18
+ createdAt: true,
19
+ updatedAt: true,
20
+ });
21
+ export const UpdateUpstreamInputSchema = CreateUpstreamInputSchema.partial();
22
+ /**
23
+ * SQLite signals a deferred FOREIGN KEY violation via its internal trigger
24
+ * machinery, so `better-sqlite3` surfaces the constraint error with
25
+ * `code: 'SQLITE_CONSTRAINT_TRIGGER'` (and message "FOREIGN KEY constraint
26
+ * failed"). Direct-mode FK violations report `'SQLITE_CONSTRAINT_FOREIGNKEY'`.
27
+ * Match both — and only those — and require the message to mention the
28
+ * foreign key so we don't mis-classify unrelated trigger failures.
29
+ */
30
+ function isForeignKeyError(err) {
31
+ if (!(err instanceof Error))
32
+ return false;
33
+ const code = err.code;
34
+ if (typeof code !== 'string')
35
+ return false;
36
+ if (code !== 'SQLITE_CONSTRAINT_FOREIGNKEY' && code !== 'SQLITE_CONSTRAINT_TRIGGER') {
37
+ return false;
38
+ }
39
+ return /FOREIGN KEY constraint failed/i.test(err.message);
40
+ }
41
+ export function listUpstreams(deps) {
42
+ return deps.upstreamRepo.list();
43
+ }
44
+ export function getUpstream(id, deps) {
45
+ const upstream = deps.upstreamRepo.findById(id);
46
+ if (upstream === null) {
47
+ throw new NotFoundError('upstream not found');
48
+ }
49
+ return upstream;
50
+ }
51
+ export function createUpstream(rawInput, deps) {
52
+ const input = CreateUpstreamInputSchema.parse(rawInput);
53
+ return deps.upstreamRepo.create(input);
54
+ }
55
+ function stripUndefined(input) {
56
+ const out = {};
57
+ for (const key of Object.keys(input)) {
58
+ const value = input[key];
59
+ if (value !== undefined) {
60
+ out[key] = value;
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+ export function updateUpstream(id, rawInput, deps) {
66
+ const patch = UpdateUpstreamInputSchema.parse(rawInput);
67
+ return deps.upstreamRepo.update(id, stripUndefined(patch));
68
+ }
69
+ /**
70
+ * Delete an upstream.
71
+ *
72
+ * - Missing id → {@link NotFoundError} (the repo returns `false`; we
73
+ * promote it to 404 at the API boundary).
74
+ * - Upstream still referenced by a site → {@link ConflictError}. The
75
+ * underlying SQLite FOREIGN KEY error is opaque to API consumers, so we
76
+ * translate it into a domain error here. (The site repository handles
77
+ * the inverse direction; the upstream repository was not updated for
78
+ * this case because it's a Phase 6 API-boundary concern.)
79
+ */
80
+ export function deleteUpstream(id, deps) {
81
+ let deleted;
82
+ try {
83
+ deleted = deps.upstreamRepo.delete(id);
84
+ }
85
+ catch (err) {
86
+ if (isForeignKeyError(err)) {
87
+ throw new ConflictError('upstream is still referenced by one or more sites', {
88
+ cause: err,
89
+ });
90
+ }
91
+ throw err;
92
+ }
93
+ if (!deleted) {
94
+ throw new NotFoundError('upstream not found');
95
+ }
96
+ }
97
+ //# sourceMappingURL=upstreams.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstreams.js","sourceRoot":"","sources":["../../../../src/server/api/handlers/upstreams.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Bearer-token guard for Route Handlers and CLI-facing entry points.
3
+ *
4
+ * This is intentionally NOT a Next.js `middleware.ts` export — Next.js
5
+ * middleware runs in the Edge runtime, which doesn't expose Node's `crypto`
6
+ * primitives we need for a constant-time token comparison. Route Handlers
7
+ * call this helper directly in the Node runtime.
8
+ */
9
+ export declare class UnauthorizedError extends Error {
10
+ readonly code = "unauthorized";
11
+ constructor(message?: string);
12
+ }
13
+ export interface RequireTokenOptions {
14
+ /** Override env var for testing. */
15
+ expectedToken?: string;
16
+ }
17
+ /**
18
+ * Reads the bearer token from the Authorization header and compares it to
19
+ * `ZOOMIES_API_TOKEN` (or `opts.expectedToken`) using a constant-time
20
+ * comparison. Throws {@link UnauthorizedError} on any failure, returns void
21
+ * on success.
22
+ */
23
+ export declare function requireToken(headers: Headers | Record<string, string | string[] | undefined>, opts?: RequireTokenOptions): void;
24
+ //# sourceMappingURL=require-token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-token.d.ts","sourceRoot":"","sources":["../../../src/server/auth/require-token.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,IAAI,kBAAkB;gBAEnB,OAAO,GAAE,MAAuB;CAI7C;AAED,MAAM,WAAW,mBAAmB;IAClC,oCAAoC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA0DD;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EAChE,IAAI,CAAC,EAAE,mBAAmB,GACzB,IAAI,CA6BN"}