@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.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/dist/cli/client.d.ts +77 -0
- package/dist/cli/client.d.ts.map +1 -0
- package/dist/cli/client.js +300 -0
- package/dist/cli/client.js.map +1 -0
- package/dist/cli/commands/certs.d.ts +11 -0
- package/dist/cli/commands/certs.d.ts.map +1 -0
- package/dist/cli/commands/certs.js +110 -0
- package/dist/cli/commands/certs.js.map +1 -0
- package/dist/cli/commands/flags.d.ts +29 -0
- package/dist/cli/commands/flags.d.ts.map +1 -0
- package/dist/cli/commands/flags.js +104 -0
- package/dist/cli/commands/flags.js.map +1 -0
- package/dist/cli/commands/reload.d.ts +11 -0
- package/dist/cli/commands/reload.d.ts.map +1 -0
- package/dist/cli/commands/reload.js +35 -0
- package/dist/cli/commands/reload.js.map +1 -0
- package/dist/cli/commands/sites.d.ts +11 -0
- package/dist/cli/commands/sites.d.ts.map +1 -0
- package/dist/cli/commands/sites.js +221 -0
- package/dist/cli/commands/sites.js.map +1 -0
- package/dist/cli/commands/status.d.ts +10 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +41 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/upstreams.d.ts +21 -0
- package/dist/cli/commands/upstreams.d.ts.map +1 -0
- package/dist/cli/commands/upstreams.js +248 -0
- package/dist/cli/commands/upstreams.js.map +1 -0
- package/dist/cli/dispatcher.d.ts +45 -0
- package/dist/cli/dispatcher.d.ts.map +1 -0
- package/dist/cli/dispatcher.js +192 -0
- package/dist/cli/dispatcher.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/server/api/db-context.d.ts +50 -0
- package/dist/server/api/db-context.d.ts.map +1 -0
- package/dist/server/api/db-context.js +76 -0
- package/dist/server/api/db-context.js.map +1 -0
- package/dist/server/api/error-mapping.d.ts +19 -0
- package/dist/server/api/error-mapping.d.ts.map +1 -0
- package/dist/server/api/error-mapping.js +56 -0
- package/dist/server/api/error-mapping.js.map +1 -0
- package/dist/server/api/handlers/site-cert.d.ts +49 -0
- package/dist/server/api/handlers/site-cert.d.ts.map +1 -0
- package/dist/server/api/handlers/site-cert.js +54 -0
- package/dist/server/api/handlers/site-cert.js.map +1 -0
- package/dist/server/api/handlers/sites.d.ts +67 -0
- package/dist/server/api/handlers/sites.d.ts.map +1 -0
- package/dist/server/api/handlers/sites.js +78 -0
- package/dist/server/api/handlers/sites.js.map +1 -0
- package/dist/server/api/handlers/upstreams.d.ts +64 -0
- package/dist/server/api/handlers/upstreams.d.ts.map +1 -0
- package/dist/server/api/handlers/upstreams.js +97 -0
- package/dist/server/api/handlers/upstreams.js.map +1 -0
- package/dist/server/auth/require-token.d.ts +24 -0
- package/dist/server/auth/require-token.d.ts.map +1 -0
- package/dist/server/auth/require-token.js +98 -0
- package/dist/server/auth/require-token.js.map +1 -0
- package/dist/server/certs/acme-account.d.ts +37 -0
- package/dist/server/certs/acme-account.d.ts.map +1 -0
- package/dist/server/certs/acme-account.js +49 -0
- package/dist/server/certs/acme-account.js.map +1 -0
- package/dist/server/certs/challenge-store.d.ts +53 -0
- package/dist/server/certs/challenge-store.d.ts.map +1 -0
- package/dist/server/certs/challenge-store.js +66 -0
- package/dist/server/certs/challenge-store.js.map +1 -0
- package/dist/server/certs/issue.d.ts +106 -0
- package/dist/server/certs/issue.d.ts.map +1 -0
- package/dist/server/certs/issue.js +107 -0
- package/dist/server/certs/issue.js.map +1 -0
- package/dist/server/certs/renew.d.ts +34 -0
- package/dist/server/certs/renew.d.ts.map +1 -0
- package/dist/server/certs/renew.js +36 -0
- package/dist/server/certs/renew.js.map +1 -0
- package/dist/server/certs/scheduler.d.ts +68 -0
- package/dist/server/certs/scheduler.d.ts.map +1 -0
- package/dist/server/certs/scheduler.js +76 -0
- package/dist/server/certs/scheduler.js.map +1 -0
- package/dist/server/db/connection.d.ts +10 -0
- package/dist/server/db/connection.d.ts.map +1 -0
- package/dist/server/db/connection.js +16 -0
- package/dist/server/db/connection.js.map +1 -0
- package/dist/server/db/migrate.d.ts +12 -0
- package/dist/server/db/migrate.d.ts.map +1 -0
- package/dist/server/db/migrate.js +37 -0
- package/dist/server/db/migrate.js.map +1 -0
- package/dist/server/db/migrations/0001_init.sql +42 -0
- package/dist/server/domain/cert.d.ts +17 -0
- package/dist/server/domain/cert.d.ts.map +1 -0
- package/dist/server/domain/cert.js +22 -0
- package/dist/server/domain/cert.js.map +1 -0
- package/dist/server/domain/errors.d.ts +36 -0
- package/dist/server/domain/errors.d.ts.map +1 -0
- package/dist/server/domain/errors.js +37 -0
- package/dist/server/domain/errors.js.map +1 -0
- package/dist/server/domain/site.d.ts +15 -0
- package/dist/server/domain/site.d.ts.map +1 -0
- package/dist/server/domain/site.js +25 -0
- package/dist/server/domain/site.js.map +1 -0
- package/dist/server/domain/upstream.d.ts +25 -0
- package/dist/server/domain/upstream.d.ts.map +1 -0
- package/dist/server/domain/upstream.js +24 -0
- package/dist/server/domain/upstream.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +4 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/reload/atomic-write.d.ts +44 -0
- package/dist/server/reload/atomic-write.d.ts.map +1 -0
- package/dist/server/reload/atomic-write.js +151 -0
- package/dist/server/reload/atomic-write.js.map +1 -0
- package/dist/server/reload/health-probe.d.ts +62 -0
- package/dist/server/reload/health-probe.d.ts.map +1 -0
- package/dist/server/reload/health-probe.js +105 -0
- package/dist/server/reload/health-probe.js.map +1 -0
- package/dist/server/reload/reload.d.ts +118 -0
- package/dist/server/reload/reload.d.ts.map +1 -0
- package/dist/server/reload/reload.js +232 -0
- package/dist/server/reload/reload.js.map +1 -0
- package/dist/server/renderer/render-bundle.d.ts +18 -0
- package/dist/server/renderer/render-bundle.d.ts.map +1 -0
- package/dist/server/renderer/render-bundle.js +32 -0
- package/dist/server/renderer/render-bundle.js.map +1 -0
- package/dist/server/renderer/render-site.d.ts +5 -0
- package/dist/server/renderer/render-site.d.ts.map +1 -0
- package/dist/server/renderer/render-site.js +144 -0
- package/dist/server/renderer/render-site.js.map +1 -0
- package/dist/server/repositories/cert-repository.d.ts +19 -0
- package/dist/server/repositories/cert-repository.d.ts.map +1 -0
- package/dist/server/repositories/cert-repository.js +112 -0
- package/dist/server/repositories/cert-repository.js.map +1 -0
- package/dist/server/repositories/site-repository.d.ts +17 -0
- package/dist/server/repositories/site-repository.d.ts.map +1 -0
- package/dist/server/repositories/site-repository.js +122 -0
- package/dist/server/repositories/site-repository.js.map +1 -0
- package/dist/server/repositories/upstream-repository.d.ts +22 -0
- package/dist/server/repositories/upstream-repository.d.ts.map +1 -0
- package/dist/server/repositories/upstream-repository.js +142 -0
- package/dist/server/repositories/upstream-repository.js.map +1 -0
- package/dist/server/validator/nginx-binary.d.ts +9 -0
- package/dist/server/validator/nginx-binary.d.ts.map +1 -0
- package/dist/server/validator/nginx-binary.js +11 -0
- package/dist/server/validator/nginx-binary.js.map +1 -0
- package/dist/server/validator/validate.d.ts +29 -0
- package/dist/server/validator/validate.d.ts.map +1 -0
- package/dist/server/validator/validate.js +69 -0
- package/dist/server/validator/validate.js.map +1 -0
- package/dist/server/worker/main.d.ts +43 -0
- package/dist/server/worker/main.d.ts.map +1 -0
- package/dist/server/worker/main.js +181 -0
- package/dist/server/worker/main.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- 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"}
|